const std = @import("std"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const diagnostics = @import("../../core/workspace/diagnostics.zig"); const managed_execution = @import("../../core/execution/managed_execution.zig"); const display_width = @import("../../core/shared/display_width.zig"); const input_action = @import("../../core/input/input_action.zig"); const io_mod = @import("../../core/shared/io.zig"); const activity_status = @import("../../core/output/activity_status.zig"); const activity_runtime = @import("../../core/output/activity_runtime.zig"); const transcript_release = @import("../../core/output/transcript_release.zig"); const transcript_presentation = @import("../../core/output/transcript_presentation.zig"); const full_transcript_page = @import("../../core/output/full_transcript_page.zig"); const diff_mod = @import("../../core/output/diff.zig"); const worker_status = @import("../../core/output/worker_status.zig"); const footer_viewport_runtime = @import("../footer/viewport.zig"); const render_request = @import("../render_request.zig"); const command_output_runtime = @import("command_output_runtime.zig"); const input_visual_layout = @import("../input/visual_layout.zig"); const user_message_card = @import("../assistant/user_message_card.zig"); const render_engine = @import("../render_engine.zig"); const build_checkpoint = @import("../render_engine/build_checkpoint.zig"); const transcript_io = @import("io.zig"); const transcript_painter = @import("painter.zig"); const source_preparation = @import("source_preparation.zig"); const full_transcript_worker = @import("full_transcript_worker.zig"); const tool_group_projection = @import("tool_group_projection.zig"); const transcript_store = @import("store.zig"); const full_transcript_screen = @import("../full_transcript_screen.zig"); const transcript_viewport_runtime = @import("viewport_runtime.zig"); const transcript_writer = @import("writer.zig"); const ui_render = @import("../render.zig"); const types = @import("../../core/shared/types.zig"); /// One timestamped full-detail record (session assembly, network call, /// recovery transition) shown only in the ctrl+o full transcript. Kept out of /// the transcript entry store so inline rendering, retention, replay, and /// resume never observe it. const FullDetailRecord = struct { notice: types.SemanticNotice, created_at_ms: i64, pub fn deinit(self: *FullDetailRecord, alloc: Allocator) void { types.freeSemanticNotice(alloc, self.notice); self.* = undefined; } }; /// Synthetic full-detail records share entry id 0: the store never issues 0, /// and the id orders below every real entry, which keeps row-ordered /// bookmark lookups consistent with the records' position at the top. const full_detail_record_entry_id: u32 = 0; const command_output_content = @import("../../core/tooling/command_output_content.zig"); const captured_command = @import("../../core/tooling/captured_command.zig"); const tool_result_errors = @import("../../core/tooling/tool_result_errors.zig"); const vt_emulator = @import("../../core/terminal/engine.zig"); const result_store = @import("../../core/session/result_store.zig"); const session_child_store = @import("../../core/session/session_child_store.zig"); const Allocator = std.mem.Allocator; const Layout = types.Layout; const Metrics = types.Metrics; const transcript_blocks = render_engine.transcript_blocks; const viewport_selection = render_engine.viewport_selection; pub const CommandOutputDisplayState = transcript_blocks.CommandOutputDisplayState; pub const Styles = transcript_blocks.Styles; pub const AssistantTurnSegments = transcript_blocks.AssistantTurnSegments; pub const RawEntryClass = transcript_blocks.RawEntryClass; pub const TranscriptEntry = transcript_blocks.TranscriptEntry; pub const TranscriptPreparationSource = source_preparation.TranscriptPreparationSource; pub const VisibleTranscriptSnapshot = struct { text: []u8, terminal_rows: u16, terminal_cols: u16, complete: bool, pub fn deinit(self: *VisibleTranscriptSnapshot, alloc: Allocator) void { alloc.free(self.text); self.* = undefined; } }; pub const ResizeObservationPhase = enum { live, settle_pending, reset_queued, }; pub const ResizeObservationResult = union(enum) { awaiting, measured: i32, unavailable, }; pub const ResizeObservation = struct { layout: types.Layout, apply_after_ms: i64, expected_row: ?u32, result: ResizeObservationResult, phase: ResizeObservationPhase, }; const ViewportSelection = viewport_selection.ViewportSelection; pub const TranscriptCommitDiagnosticState = enum { invalid, stable, recovering, }; pub const TranscriptCommitDiagnostic = struct { state: TranscriptCommitDiagnosticState, stable_start_line: ?usize = null, visual_offset: u32 = 0, history_visual_offset: u32 = 0, total_visual_rows: u32 = 0, source_bytes: usize = 0, layout_id: u64 = 0, remaining_inline_rows: u32 = 0, remaining_unplanned_scroll_rows: u16 = 0, }; pub const StableTranscriptProjection = struct { selection: ViewportSelection, visual_offset: u32, history_visual_offset: u32, total_visual_rows: u32, flow_unchanged: bool, cursor_row: u16, cursor_col: u16, occupied_last_row: u16, occupied_last_row_blank: bool, layout_id: u64, measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin = null, }; pub const MeasuredResizeProjection = union(enum) { stable, recovering: struct { accepted_visual_offset: u32, source_cols: u16, }, }; const CommittedTranscriptAnchor = struct { selection: ViewportSelection, visual_offset: u32, // Footer reflow can move the logical tail without advancing rows into // terminal history. The committed grid projection resumes from this offset. history_visual_offset: u32, total_visual_rows: u32, flow: []u8, retention_identity: source_preparation.RetentionIdentity = .{}, flow_materialized: bool = true, cursor_row: u16, cursor_col: u16, occupied_last_row: u16, occupied_last_row_blank: bool, layout_id: u64, measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin = null, row_provenance: []transcript_blocks.RowProvenance = &.{}, normal_buffer_recovery_pending: bool = false, /// Stable-anchor settlement obligation. It is preserved while final rows /// remain ahead of history and cleared once history reaches the viewport. /// The offset gap alone is insufficient because footer displacement can /// create the same shape without content debt. history_catchup_pending: bool = false, fn deinit(self: *CommittedTranscriptAnchor, alloc: Allocator) void { self.retention_identity.deinit(alloc); if (self.flow.len > 0) alloc.free(self.flow); if (self.row_provenance.len > 0) alloc.free(self.row_provenance); self.* = undefined; } }; fn committedProjectionVisualOffset(anchor: CommittedTranscriptAnchor) u32 { return if (anchor.normal_buffer_recovery_pending) anchor.history_visual_offset else anchor.visual_offset; } const TranscriptRecoveryReceipt = struct { accepted_visual_offset: u32 = 0, accepted_history_visual_offset: u32 = 0, attempt_visual_offset: u32 = 0, attempt_total_visual_rows: u32 = 0, materialized_visual_offset: u32 = 0, materialized_visual_rows: u16 = 0, materialized_flow_len: ?usize = null, remaining_resize_reflow_rows: u16 = 0, remaining_semantic_rows: u32 = 0, // Only this remaining semantic suffix may advance the current projection. remaining_semantic_progress_rows: u32 = 0, remaining_unplanned_scroll_rows: u16 = 0, attempt_cols: u16 = 0, attempt_resize_history_row_delta: ?i32 = null, attempt_resize_history_committed: bool = false, projection_rebase_pending: bool = false, tracks_semantic_progress: bool = false, flow: []u8 = &.{}, flow_owned: bool = true, retention_identity: source_preparation.RetentionIdentity = .{}, presentation_resume_bytes: []u8 = &.{}, presentation_pending_wrap: bool = false, presentation_valid: bool = false, cursor_row: u16 = 1, cursor_col: u16 = 1, document_append_committed: bool = false, measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin = null, row_provenance: []transcript_blocks.RowProvenance = &.{}, normal_buffer_recovery_pending: bool = false, fn deinit(self: *TranscriptRecoveryReceipt, alloc: Allocator) void { self.retention_identity.deinit(alloc); if (self.flow_owned and self.flow.len > 0) alloc.free(self.flow); if (self.presentation_resume_bytes.len > 0) alloc.free(self.presentation_resume_bytes); if (self.row_provenance.len > 0) alloc.free(self.row_provenance); self.* = undefined; } }; const TranscriptCommitState = union(enum) { invalid, stable: CommittedTranscriptAnchor, recovering: TranscriptRecoveryReceipt, fn deinit(self: *TranscriptCommitState, alloc: Allocator) void { switch (self.*) { .invalid => {}, .stable => |*anchor| anchor.deinit(alloc), .recovering => |*receipt| receipt.deinit(alloc), } self.* = .invalid; } }; test "retention rebase carries stable and recovering source boundaries" { const alloc = std.testing.allocator; var before = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible(alloc, try alloc.dupe(u8, "a\nb\nc\nd\ne\nf"), 80, null); defer before.deinit(alloc); var after = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible(alloc, try alloc.dupe(u8, "c\nd\ne\nf"), 80, null); defer after.deinit(alloc); var provenance: [6]transcript_blocks.LineProvenance = undefined; for (&provenance, 0..) |*line, index| line.* = .{ .entry = .{ .entry_id = @intCast(index + 1), .entry_class = .unknown_raw } }; before.line_provenance = try alloc.dupe(transcript_blocks.LineProvenance, &provenance); after.line_provenance = try alloc.dupe(transcript_blocks.LineProvenance, provenance[2..]); var mapping = try source_preparation.RetentionRebase.init(alloc, &before, &after, &.{}); defer mapping.deinit(alloc); for ([_]bool{ false, true }) |recovering| { var runtime = TranscriptRuntime{ .detached_commit_alloc = alloc }; defer runtime.deinit(alloc); const flow = try alloc.dupe(u8, before.bytes); runtime.transcript_commit_state = if (recovering) .{ .recovering = .{ .flow = flow, .accepted_visual_offset = 4, .accepted_history_visual_offset = 3, .attempt_visual_offset = 5, .attempt_total_visual_rows = 6, .materialized_visual_offset = 4, .materialized_visual_rows = 2, .materialized_flow_len = before.bytes.len, .attempt_cols = 80, .remaining_semantic_rows = 2, .remaining_semantic_progress_rows = 1, } } else .{ .stable = .{ .flow = flow, .visual_offset = 4, .history_visual_offset = 3, .total_visual_rows = 6, .selection = .{ .start_line = 4, .top_row = 1, .bottom_row = 2, .partial_skip_rows = 0, .line_count = 2 }, .cursor_row = 3, .cursor_col = 1, .occupied_last_row = 2, .occupied_last_row_blank = false, .layout_id = 1, } }; var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); try std.testing.expectError(error.OutOfMemory, runtime.rebaseRetainedTranscript(failing.allocator(), mapping, .{})); try std.testing.expectEqual(@as(u32, 3), runtime.transcriptCommitDiagnostic().history_visual_offset); try runtime.rebaseRetainedTranscript(alloc, mapping, .{}); const diagnostic = runtime.transcriptCommitDiagnostic(); try std.testing.expectEqual(@as(u32, 1), diagnostic.history_visual_offset); try std.testing.expectEqual(@as(u32, 2), diagnostic.visual_offset); try std.testing.expectEqual(@as(u32, 4), diagnostic.total_visual_rows); switch (runtime.transcript_commit_state) { .invalid => return error.TestUnexpectedResult, .stable => |anchor| { try std.testing.expectEqualStrings(after.bytes, anchor.flow); try std.testing.expectEqual(@as(usize, 2), anchor.selection.start_line); }, .recovering => |receipt| { try std.testing.expectEqualStrings(after.bytes, receipt.flow); try std.testing.expectEqual(@as(u32, 2), receipt.materialized_visual_offset); try std.testing.expectEqual(@as(u16, 2), receipt.materialized_visual_rows); try std.testing.expectEqual(after.bytes.len, receipt.materialized_flow_len.?); try std.testing.expectEqual(@as(u32, 3), receipt.attempt_visual_offset); try std.testing.expectEqual(@as(u32, 2), receipt.remaining_semantic_rows); try std.testing.expectEqual(@as(u32, 1), receipt.remaining_semantic_progress_rows); }, } } } test "retention rebase recovery rejects the removed entry partial-row endpoint" { const alloc = std.testing.allocator; var before = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible(alloc, try alloc.dupe(u8, "abcdefghi\njklmnopqr"), 3, null); defer before.deinit(alloc); var after = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible(alloc, try alloc.dupe(u8, "jklmnopqr"), 3, null); defer after.deinit(alloc); const lines = [_]transcript_blocks.LineProvenance{ .{ .entry = .{ .entry_id = 1, .entry_class = .unknown_raw } }, .{ .entry = .{ .entry_id = 2, .entry_class = .unknown_raw } }, }; before.line_provenance = try alloc.dupe(transcript_blocks.LineProvenance, &lines); after.line_provenance = try alloc.dupe(transcript_blocks.LineProvenance, lines[1..]); var mapping = try source_preparation.RetentionRebase.init(alloc, &before, &after, &.{}); defer mapping.deinit(alloc); var runtime = TranscriptRuntime{ .detached_commit_alloc = alloc, .transcript_commit_state = .{ .recovering = .{ .flow = try alloc.dupe(u8, before.bytes), .accepted_visual_offset = 2, .accepted_history_visual_offset = 2, .materialized_visual_offset = 2, .materialized_visual_rows = 4, .materialized_flow_len = before.bytes.len, .attempt_cols = 3, .attempt_visual_offset = 2, .attempt_total_visual_rows = 6, } } }; defer runtime.deinit(alloc); try runtime.rebaseRetainedTranscript(alloc, mapping, .{}); const receipt = runtime.transcript_commit_state.recovering; try std.testing.expectEqual(@as(u32, 0), receipt.accepted_visual_offset); try std.testing.expectEqual(@as(u32, 0), receipt.accepted_history_visual_offset); try std.testing.expectEqual(@as(u32, 0), receipt.materialized_visual_offset); try std.testing.expectEqual(@as(u16, 3), receipt.materialized_visual_rows); try std.testing.expectEqual(@as(?usize, null), receipt.materialized_flow_len); } fn recoveryAppendPrefixMatches( target_flow: []const u8, receipt: TranscriptRecoveryReceipt, materialized_flow_len: usize, ) bool { return materialized_flow_len <= receipt.flow.len and materialized_flow_len <= target_flow.len and std.mem.eql( u8, target_flow[0..materialized_flow_len], receipt.flow[0..materialized_flow_len], ); } pub const TranscriptScrollFacts = struct { publication_hold: enum { none, restore, finality } = .none, source_visual_offset: u32 = 0, target_visual_offset: u32 = 0, resize_reflow_rows: u16 = 0, semantic_rows: u32 = 0, semantic_progress_rows: u32 = 0, planned_rows: u16 = 0, source_compatible: bool = false, geometry_rebase: bool = false, recovery_rebase: bool = false, recovery_progress_compatible: bool = false, recovery_tracks_semantic_progress: bool = false, recovery_projection_deferred: bool = false, /// The finality floor withheld release this frame (mutable rows above /// the viewport top), or final rows remain to settle beyond what this /// frame planned. Routes into history_catchup_pending at commit so the /// existing catch-up machinery settles the debt; footer displacement /// (visual ahead of history with nothing to settle) stays unflagged. finality_hold: bool = false, }; pub const RetainedTranscriptBody = render_engine.frame_retention.RetainedTranscriptBody; pub const TranscriptBodyDisposition = union(enum) { paint, retain_committed: RetainedTranscriptBody, }; fn nextResizeBottomAnchorPending( pending: bool, terminal_reset_commit: bool, resize_reset_commit: bool, tail_kind: ?TranscriptBlockKind, occupied_last_row: u16, content_bottom_row: u16, ) bool { const next = if (terminal_reset_commit) resize_reset_commit else pending; if (tail_kind == .cancel_notice and occupied_last_row == content_bottom_row) { return false; } return next; } test "resize bottom anchor remains pending until a cancel projection reaches it" { const Case = struct { pending: bool, terminal_reset_commit: bool, resize_reset_commit: bool, tail_kind: ?TranscriptBlockKind, occupied_last_row: u16, content_bottom_row: u16, expected: bool, }; const cases = [_]Case{ .{ .pending = false, .terminal_reset_commit = false, .resize_reset_commit = false, .tail_kind = null, .occupied_last_row = 4, .content_bottom_row = 8, .expected = false }, .{ .pending = false, .terminal_reset_commit = true, .resize_reset_commit = true, .tail_kind = null, .occupied_last_row = 4, .content_bottom_row = 8, .expected = true }, .{ .pending = true, .terminal_reset_commit = false, .resize_reset_commit = false, .tail_kind = .assistant_turn, .occupied_last_row = 4, .content_bottom_row = 8, .expected = true }, .{ .pending = true, .terminal_reset_commit = false, .resize_reset_commit = false, .tail_kind = .cancel_notice, .occupied_last_row = 4, .content_bottom_row = 8, .expected = true }, .{ .pending = true, .terminal_reset_commit = false, .resize_reset_commit = false, .tail_kind = .cancel_notice, .occupied_last_row = 8, .content_bottom_row = 8, .expected = false }, .{ .pending = true, .terminal_reset_commit = true, .resize_reset_commit = false, .tail_kind = null, .occupied_last_row = 4, .content_bottom_row = 8, .expected = false }, }; for (cases) |case| { try std.testing.expectEqual( case.expected, nextResizeBottomAnchorPending( case.pending, case.terminal_reset_commit, case.resize_reset_commit, case.tail_kind, case.occupied_last_row, case.content_bottom_row, ), ); } } const MaterializedEndpoint = struct { visual_offset: u32 = 0, visual_rows: u16 = 0, flow_len: ?usize = null, cursor_row: u16 = 1, cursor_col: u16 = 1, }; pub const TranscriptTransition = struct { scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, document_append: render_engine.frame_scroll_plan.FrameDocumentAppend, /// Final rows exist beyond materialized history that this frame could /// not release; consuming the transition requests a follow-up frame so /// the debt settles promptly instead of waiting for outside activity. history_settle_pending: bool = false, /// Owns `document_append.bytes` when append preparation allocates wire bytes. document_append_bytes: []u8 = &.{}, resize_reflow_rows: u16 = 0, semantic_rows: u32 = 0, semantic_progress_rows: u32 = 0, recovery_rebase: bool = false, recovery_progress_compatible: bool = false, recovery_tracks_semantic_progress: bool = false, recovery_projection_deferred: bool = false, body_disposition: TranscriptBodyDisposition = .paint, target_flow: []u8, // Painter borrows outlive sealing when the receipt keeps only a prefix. presentation_flow: []u8 = &.{}, target_flow_owned: bool = true, retention_identity: source_preparation.RetentionIdentity = .{}, borrows_full_page: bool = false, presentation_resume_bytes: []u8 = &.{}, presentation_pending_wrap: bool = false, presentation_valid: bool = false, target_cache: std.ArrayList(u8), target_cache_unchanged: bool = false, target_cache_origin_untrimmed: bool = false, folded_summary_indices: []usize, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, selection: ViewportSelection, visual_offset: u32, history_visual_offset: u32, source_endpoint_visual_offset: u32 = 0, total_visual_rows: u32, materialized: MaterializedEndpoint = .{}, // The exact transcript endpoint if this frame appends no transcript bytes. materialized_without_append: MaterializedEndpoint, cursor_row: u16, cursor_col: u16, replaceable_last_line: bool, replaceable_start: usize, replaceable_row: u16, trace_transcript_frame: bool, trace_visible_rows: u16, measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin = null, row_provenance: []transcript_blocks.RowProvenance = &.{}, normal_buffer_recovery_pending: bool = false, history_catchup_pending: bool = false, consumed: bool = false, pub fn deinit(self: *TranscriptTransition, alloc: Allocator) void { self.retention_identity.deinit(alloc); if (self.target_flow_owned and self.target_flow.len > 0) alloc.free(self.target_flow); alloc.free(self.presentation_flow); if (self.presentation_resume_bytes.len > 0) alloc.free(self.presentation_resume_bytes); if (self.document_append_bytes.len > 0) { alloc.free(self.document_append_bytes); } self.target_cache.deinit(alloc); if (self.folded_summary_indices.len > 0) alloc.free(self.folded_summary_indices); if (self.row_provenance.len > 0) alloc.free(self.row_provenance); self.* = undefined; } }; const MaterializedTranscriptProjection = struct { raw_flow_exact: bool = false, visual_rows: u16 = 0, }; const TranscriptBlockKind = transcript_blocks.TranscriptBlockKind; const WalkResult = transcript_blocks.WalkResult; const walkText = transcript_blocks.walkText; fn formatDurationCompact(buf: []u8, duration_ms: u64) []const u8 { const total_seconds = duration_ms / std.time.ms_per_s; if (total_seconds < 60) { return std.fmt.bufPrint(buf, "{d}s", .{total_seconds}) catch "0s"; } if (total_seconds < 60 * 60) { const minutes = total_seconds / 60; const seconds = total_seconds % 60; return std.fmt.bufPrint(buf, "{d}m {d}s", .{ minutes, seconds }) catch "1m 0s"; } const hours = total_seconds / (60 * 60); const minutes = (total_seconds % (60 * 60)) / 60; return std.fmt.bufPrint(buf, "{d}h {d:0>2}m", .{ hours, minutes }) catch "1h 00m"; } fn formatTurnSummaryLine(buf: []u8, summary: types.TurnSummary) []const u8 { var turn_buf: [24]u8 = undefined; const turn = formatDurationCompact(&turn_buf, summary.turn_duration_ms); var out: std.Io.Writer = .fixed(buf); out.print(" {s}", .{turn}) catch return " "; activity_status.appendTokenProgressSuffix(&out, summary.token_progress) catch return out.buffered(); return out.buffered(); } fn lifecycleEventTurnId(event: types.ToolLifecycleEvent) u64 { return switch (event) { .provisional => |payload| payload.id.turn_id, .authoritative_started => |payload| payload.id.turn_id, .progress => |payload| payload.id.turn_id, .terminal => |payload| payload.id.turn_id, .turn_finished => |payload| payload.turn_id, }; } fn admitLifecycleStart( state: *const activity_runtime.ToolActivityState, id: types.ToolLifecycleId, ) bool { return switch (state.admitStart(id)) { .accepted => true, .invalid_identity => false, .finalized => |fence| blk: { debug_trace.logf( "ui_activity", "lifecycle rejected by finalized fence turn_id={d} fence={d} kind=start", .{ id.turn_id, fence }, ); break :blk false; }, }; } fn traceLateLifecycleEvent(id: types.ToolLifecycleId, event_kind: []const u8) void { debug_trace.logf( "ui_activity", "late lifecycle event ignored turn_id={d} kind={s}", .{ id.turn_id, event_kind }, ); } fn lifecycleStartLine( alloc: Allocator, tool_name: ?[]const u8, ) ![]u8 { var label_buf: [512]u8 = undefined; const label = std.fmt.bufPrint( &label_buf, "● {s}", .{tool_name orelse "tool"}, ) catch "● tool"; return std.fmt.allocPrint( alloc, "{s}{s}\n", .{ label, ui_render.reset_style }, ); } fn lifecycleLine(alloc: Allocator, text: []const u8) ![]u8 { if (std.mem.endsWith(u8, text, "\n")) return alloc.dupe(u8, text); return std.fmt.allocPrint(alloc, "{s}\n", .{text}); } const LifecycleMarker = struct { style: []const u8, glyph: []const u8, }; fn lifecycleMarker(kind: types.ToolOutcomeKind) LifecycleMarker { return switch (kind) { .completed => .{ .style = ui_render.system_notice_text_style, .glyph = "●" }, .failed => .{ .style = ui_render.red_style, .glyph = "●" }, .denied => .{ .style = ui_render.red_style, .glyph = "⊘" }, .cancelled => .{ .style = ui_render.warning_style, .glyph = "■" }, .deferred => .{ .style = ui_render.warning_style, .glyph = "↻" }, }; } fn leadingSgrPrefixEnd(text: []const u8) usize { var index: usize = 0; while (index + 2 <= text.len and text[index] == 0x1b and text[index + 1] == '[') { var end = index + 2; while (end < text.len and (text[end] < 0x40 or text[end] > 0x7e)) : (end += 1) {} if (end == text.len or text[end] != 'm') break; index = end + 1; } return index; } fn lifecycleTerminalLine( alloc: Allocator, kind: types.ToolOutcomeKind, summary: []const u8, ) ![]u8 { const marker = lifecycleMarker(kind); const prefix_end = leadingSgrPrefixEnd(summary); const has_marker = std.mem.startsWith(u8, summary[prefix_end..], "●"); const cancelled = kind == .cancelled; const text_style = if (cancelled) ui_render.hint_style else ""; const text_reset = if (cancelled) ui_render.reset_style else ""; const follow_up = if (cancelled) " · What can fx do differently?" else ""; const normalized = if (has_marker) try std.fmt.allocPrint( alloc, "{s}{s}{s}{s}{s}{s}{s}{s}", .{ marker.style, marker.glyph, ui_render.reset_style, summary[0..prefix_end], text_style, summary[prefix_end + "●".len ..], text_reset, follow_up, }, ) else try std.fmt.allocPrint( alloc, "{s}{s}{s} {s}{s}{s}{s}", .{ marker.style, marker.glyph, ui_render.reset_style, text_style, summary, text_reset, follow_up, }, ); defer alloc.free(normalized); return lifecycleLine(alloc, normalized); } /// Pure formatter shared by live lifecycle presentation and detached resume /// projection. The caller owns the returned line. pub fn formatHistoricalToolStatusLine( alloc: Allocator, kind: types.ToolOutcomeKind, summary: []const u8, ) ![]u8 { return lifecycleTerminalLine(alloc, kind, summary); } fn lifecycleFallbackLine( alloc: Allocator, outcome: types.TurnPresentationOutcome, was_provisional: bool, ) ![]u8 { return switch (outcome) { .completed => lifecycleLine(alloc, if (was_provisional) "● Tool was not executed" else "● Tool completion was not reported"), .interrupted => lifecycleTerminalLine(alloc, .cancelled, "Tool cancelled"), .failed => lifecycleTerminalLine(alloc, .failed, "Tool failed"), .paused => lifecycleLine(alloc, "● Tool state preserved for recovery"), }; } fn lifecycleFallbackDisposition( outcome: types.TurnPresentationOutcome, was_provisional: bool, ) ?transcript_blocks.ToolFallbackDisposition { if (outcome != .completed) return null; return if (was_provisional) .not_executed else .completion_unreported; } fn traceLifecycleFailure( event_kind: []const u8, turn_id: u64, err: anyerror, ) void { debug_trace.logf( "ui_activity", "lifecycle transaction failed kind={s} turn_id={d} err={s}", .{ event_kind, turn_id, @errorName(err) }, ); } test "formatTurnSummaryLine renders total duration and tokens" { var buf: [128]u8 = undefined; const line = formatTurnSummaryLine(&buf, .{ .thinking_duration_ms = 15_000, .turn_duration_ms = 130_000, .token_progress = .{ .input_tokens = 10_000, .output_tokens = 5_000 }, }); try std.testing.expectEqualStrings(" 2m 10s (↑10k ↓5k)", line); } test "formatTurnSummaryLine renders only total duration without token estimate" { var buf: [128]u8 = undefined; const line = formatTurnSummaryLine(&buf, .{ .thinking_duration_ms = 3_000, .turn_duration_ms = 3_600_000, }); try std.testing.expectEqualStrings(" 1h 00m", line); } test "appendTurnSummaryEntry stores classified dim transcript row" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); _ = try runtime.appendTurnSummaryEntry(alloc, .{ .thinking_duration_ms = 15_000, .turn_duration_ms = 130_000, .token_progress = .{ .input_tokens = 10_000, .output_tokens = 5_000 }, }); try std.testing.expectEqual(@as(usize, 1), runtime.entries.items.len); try std.testing.expect(runtime.entries.items[0] == .raw_bytes); try std.testing.expectEqual(RawEntryClass.turn_summary, runtime.entries.items[0].raw_bytes.class); try std.testing.expect(std.mem.find(u8, runtime.entries.items[0].raw_bytes.bytes, " 2m 10s (↑10k ↓5k)") != null); const rendered = try renderEntriesToBytes(alloc, runtime.entries.items, runtime.layout.cols, .{}); defer alloc.free(rendered); try std.testing.expectEqualStrings("\x1b[38;5;245m 2m 10s (↑10k ↓5k)\x1b[0m\n", rendered); } test "recovered route status is transient and final summary stays normal" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); runtime.worker_status.set_route_recovery(.{ .kind = .auto_recovered, .succeeded_attempt = 3, .attempt_limit = 3, }, 1_000); switch (runtime.activityProjection()) { .turn_thinking => |thinking| { try std.testing.expectEqual(activity_runtime.ActivityProjection.Tone.success, thinking.tone); try std.testing.expectEqualStrings("✓ recovered · attempt 3", thinking.label); }, .none, .tool_slot => return error.TestUnexpectedResult, } try std.testing.expect(!runtime.worker_status.expire_transient(2_499)); try std.testing.expect(runtime.worker_status.expire_transient(2_500)); try std.testing.expect(runtime.activityProjection() == .none); runtime.worker_status.set_route_recovery(.{ .kind = .auto_recovered, .succeeded_attempt = 3, .attempt_limit = 3, }, 3_000); _ = try runtime.appendTurnSummaryEntry(alloc, .{ .thinking_duration_ms = 15_000, .turn_duration_ms = 130_000, .token_progress = .{ .input_tokens = 10_000, .output_tokens = 5_000 }, }); try std.testing.expect(runtime.activityProjection() == .none); try std.testing.expect(std.mem.find(u8, runtime.entries.items[0].raw_bytes.bytes, ui_render.dim_style) != null); try std.testing.expect(std.mem.find(u8, runtime.entries.items[0].raw_bytes.bytes, " 2m 10s (↑10k ↓5k)") != null); try std.testing.expect(std.mem.find(u8, runtime.entries.items[0].raw_bytes.bytes, "✓ recovered") == null); } test "appendTurnSummaryEntry leaves terminal route status visible" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); runtime.worker_status.set_route_recovery(.{ .kind = .content_filter }, 0); _ = try runtime.appendTurnSummaryEntry(alloc, .{ .thinking_duration_ms = 15_000, .turn_duration_ms = 130_000, .token_progress = .{ .input_tokens = 10_000, .output_tokens = 5_000 }, }); switch (runtime.activityProjection()) { .turn_thinking => |thinking| try std.testing.expectEqualStrings("⚠ blocked · content filter", thinking.label), .none, .tool_slot => return error.TestUnexpectedResult, } try std.testing.expect(std.mem.find(u8, runtime.entries.items[0].raw_bytes.bytes, " 2m 10s (↑10k ↓5k)") != null); } test "full transcript projection opens without folded command output" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); try std.testing.expect(try runtime.setTranscriptPresentationDepth(alloc, .full)); try std.testing.expect(runtime.fullTranscriptActive()); } test "closing the full transcript resets bounded paging to the tail" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); try std.testing.expect(try runtime.setTranscriptPresentationDepth(alloc, .full)); runtime.full_transcript_page_anchor = .{ .entry_index = 17 }; try std.testing.expect(try runtime.setTranscriptPresentationDepth(alloc, .inline_mode)); try std.testing.expect(std.meta.eql( @as(full_transcript_page.Anchor, .tail), runtime.full_transcript_page_anchor, )); } test "live command keeps the full transcript stable until completion" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .full_transcript = .{ .depth = .full }, .full_transcript_content_revision = 40, .command_output_display = .{ .open_command_block = 0 }, .full_transcript_installed_page = .{ .source = .{ .request = .{ .content_revision = 40, .cols = 80, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, }, .projection = .{ .styles = .{} }, }, }; defer runtime.deinit(alloc); for (0..32) |_| { runtime.markTranscriptContentDirty(); try std.testing.expect(!runtime.render_requests.hasReason(.transcript)); } runtime.command_output_display.open_command_block = null; runtime.markTranscriptContentDirty(); try std.testing.expect(runtime.render_requests.hasReason(.transcript)); runtime.command_output_display.open_command_block = 0; runtime.full_transcript_open_request = runtime.desiredFullTranscriptPageRequest(); try runtime.prewarmFullTranscriptPage(null, null); try std.testing.expect(runtime.full_transcript_page_load.busy()); } test "installed full transcript stays stable only while its command is active" { var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .full_transcript_content_revision = 41, .full_transcript_installed_page = .{ .source = .{ .request = .{ .content_revision = 40, .cols = 80, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, }, .projection = .{ .styles = .{} }, }, }; defer runtime.deinit(std.testing.allocator); try std.testing.expect(runtime.installedFullTranscriptPageProjection() == null); runtime.full_transcript_prepared_page_visible = true; try std.testing.expect(runtime.installedFullTranscriptPageProjection() != null); runtime.full_transcript_prepared_page_visible = false; runtime.command_output_display.open_command_block = 0; try std.testing.expect(runtime.installedFullTranscriptPageProjection() != null); runtime.full_transcript_content_revision = 48; try std.testing.expect(runtime.installedFullTranscriptPageProjection() != null); runtime.command_output_display.open_command_block = null; try std.testing.expect(runtime.installedFullTranscriptPageProjection() == null); } test "active full transcript keeps its installed page while replacement loads" { const alloc = std.testing.allocator; const page_alloc = std.heap.c_allocator; const prepared_source = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible( page_alloc, try page_alloc.dupe(u8, "installed page\n"), 80, null, ); var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .full_transcript = .{ .depth = .full }, .full_transcript_content_revision = 41, .full_transcript_installed_page = .{ .source = .{ .request = .{ .content_revision = 40, .cols = 80, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, }, .projection = .{ .styles = .{} }, .prepared_window = .{ .source = prepared_source, .start_row = 0, }, }, }; defer runtime.deinit(alloc); const installed = &runtime.full_transcript_installed_page.?.projection; const visible = try runtime.preparedFullTranscriptPageProjectionInterruptible( null, null, null, ); try std.testing.expect(visible == installed); try std.testing.expect(runtime.full_transcript_page_load.busy()); } test "active full transcript defers repaint while width replacement loads" { const alloc = std.testing.allocator; const page_alloc = std.heap.c_allocator; const prepared_source = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible( page_alloc, try page_alloc.dupe(u8, "installed page\n"), 80, null, ); var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 100, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .full_transcript = .{ .depth = .full }, .full_transcript_content_revision = 41, .full_transcript_installed_page = .{ .source = .{ .request = .{ .content_revision = 40, .cols = 80, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, }, .projection = .{ .styles = .{} }, .prepared_window = .{ .source = prepared_source, .start_row = 0, }, }, }; defer runtime.deinit(alloc); try std.testing.expectError( error.InputPending, runtime.preparedFullTranscriptPageProjectionInterruptible( null, null, null, ), ); try std.testing.expect(runtime.full_transcript_page_load.busy()); } test "cross-page replacement preserves its pending boundary until adoption" { const alloc = std.testing.allocator; const page_alloc = std.heap.c_allocator; const Case = struct { installed_anchor: full_transcript_page.Anchor, boundary_index: usize, }; const cases = [_]Case{ .{ .installed_anchor = .tail, .boundary_index = 743 }, .{ .installed_anchor = .{ .entry_index = 743 }, .boundary_index = 871 }, }; for (cases) |case| { var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full, .follow_tail = false }, }; defer runtime.deinit(alloc); for (0..1_000) |_| { _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row\n", .unknown_raw, ); } const installed_request = full_transcript_page.Request{ .content_revision = runtime.full_transcript_content_revision, .cols = runtime.layout.cols, .anchor = case.installed_anchor, }; runtime.full_transcript_installed_page = .{ .source = .{ .request = installed_request, .range = full_transcript_page.sourceRange( installed_request, runtime.entries.items.len, ), }, .projection = .{ .styles = .{} }, .prepared_window = .{ .source = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible( page_alloc, try page_alloc.dupe(u8, "installed page\n"), runtime.layout.cols, null, ), .start_row = 0, }, }; runtime.full_transcript_page_anchor = .{ .entry_index = case.boundary_index }; runtime.full_transcript = runtime.full_transcript.select_page_boundary( runtime.entries.items[case.boundary_index].id(), ); const before = runtime.full_transcript.snapshot(); try std.testing.expectError( error.InputPending, runtime.preparedFullTranscriptPageProjectionInterruptible( null, null, null, ), ); try std.testing.expectEqualDeep(before, runtime.full_transcript.snapshot()); try std.testing.expect(runtime.full_transcript_page_load.busy()); } } test "restored full transcript opens without replacing its exact offset" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 12, .cols = 60, .content_bottom = 8, .divider_top_row = 9, .input_row = 10, .divider_bottom_row = 11, .hint_row = 12, }, .full_transcript = .{ .depth = .full, .scroll_rows = 47, .follow_tail = false, }, }; defer runtime.deinit(alloc); runtime.deferRestoredFullTranscriptOpen(); try std.testing.expectEqual( transcript_presentation.Depth.inline_mode, runtime.full_transcript.depth, ); try std.testing.expect(try runtime.setTranscriptPresentationDepth(alloc, .full)); const selected = runtime.full_transcript.select_visual_offset(100, 8, &.{}); try std.testing.expectEqual(@as(u32, 47), selected.offset); } test "full transcript viewport snapshot restores reading position" { var source = TranscriptRuntime{ .full_transcript = .{ .depth = .full, .scroll_rows = 37, .follow_tail = false, .anchor_entry_id = 19, .anchor_pending = true, }, .full_transcript_page_anchor = .{ .entry_index = 17 }, }; defer source.deinit(std.testing.allocator); const snapshot = source.snapshotFullTranscriptViewport(); var restored = TranscriptRuntime{}; defer restored.deinit(std.testing.allocator); restored.restoreFullTranscriptViewport(snapshot); try std.testing.expect(std.meta.eql( snapshot, restored.snapshotFullTranscriptViewport(), )); try std.testing.expect(std.meta.eql( source.full_transcript_page_anchor, restored.full_transcript_page_anchor, )); } test "full transcript page snapshot retains active command records" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const entry_id = try runtime.appendRawTranscriptEntryClassified( alloc, "│ compact output\n", .command_output, ); var block = CommandOutputBlock{ .entry_id = entry_id, .total_lines = 1, .retained_text_bytes = "ACTIVE_SNAPSHOT_RECORD\n".len, }; try block.lines.append(alloc, .{ .stream = .stdout, .text = try alloc.dupe(u8, "ACTIVE_SNAPSHOT_RECORD\n"), .entry_id = entry_id, .terminated = true, }); try block.source_entry_ids.append(alloc, entry_id); try runtime.command_output_blocks.append(alloc, block); var source = try runtime.snapshotFullTranscriptPage(.{ .content_revision = runtime.full_transcript_content_revision, .cols = 80, .anchor = .tail, }, null, null); defer source.deinit(std.heap.c_allocator); try std.testing.expectEqual(@as(usize, 1), source.command_blocks.items.len); try std.testing.expectEqual(@as(usize, 1), source.command_blocks.items[0].lines.items.len); try std.testing.expectEqualStrings( "ACTIVE_SNAPSHOT_RECORD\n", source.command_blocks.items[0].lines.items[0].text, ); } test "full transcript prewarm rejects an oversized main-thread snapshot" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const oversized = try alloc.alloc( u8, full_transcript_snapshot_clone_max_bytes + 1, ); @memset(oversized, 'x'); _ = try runtime.appendRawBytesEntryClassified( alloc, oversized, .unknown_raw, ); try runtime.prewarmFullTranscriptPage(null, null); try std.testing.expect(!runtime.full_transcript_page_load.busy()); try std.testing.expect(runtime.full_transcript_failed_request != null); try std.testing.expect(!runtime.requestFullTranscriptOpen()); try std.testing.expect(runtime.takeFullTranscriptPreparationFailure()); try std.testing.expect(!runtime.fullTranscriptPreparedForOpen()); } test "full transcript page navigation preserves tail intent while the page loads" { var runtime = TranscriptRuntime{ .layout = .{ .rows = 12, .cols = 60, .content_bottom = 8, .divider_top_row = 9, .input_row = 10, .divider_bottom_row = 11, .hint_row = 12, }, .full_transcript = .{ .depth = .full }, }; const task = try std.heap.c_allocator.create(full_transcript_worker.Task); task.* = .{ .source = .{ .request = .{ .content_revision = 0, .cols = 60, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, .styles = .{}, } }; runtime.full_transcript_page_load.task = task; defer runtime.deinit(std.testing.allocator); runtime.scrollFullTranscript(.up, .page); try std.testing.expect(runtime.full_transcript.follow_tail); try std.testing.expectEqual(@as(u32, 0), runtime.full_transcript.scroll_rows); } test "cancelled full transcript page completion is never installed" { var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(12) }; runtime.layout.cols = 60; defer runtime.deinit(std.testing.allocator); const task = try std.heap.c_allocator.create(full_transcript_worker.Task); task.* = .{ .source = .{ .request = .{ .content_revision = 0, .cols = 60, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, .styles = .{}, }, .projection = .{ .styles = .{} }, }; task.cancel_requested.store(true, .release); task.done.store(true, .release); runtime.full_transcript_page_load.task = task; try std.testing.expect(!try runtime.pollFullTranscriptPageLoad()); try std.testing.expect(runtime.full_transcript_installed_page == null); } test "full transcript page boundaries preserve monotonic navigation" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full, .follow_tail = false }, }; defer runtime.deinit(alloc); for (0..1_000) |_| { _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row\n", .unknown_raw, ); } const PageWindow = struct { fn install( current: *TranscriptRuntime, request: full_transcript_page.Request, ) !*InstalledFullTranscriptPage { const range = full_transcript_page.sourceRange( request, current.entries.items.len, ); if (current.full_transcript_installed_page == null) { current.full_transcript_installed_page = .{ .source = .{ .request = request, .range = range, }, .projection = .{ .styles = .{} }, }; } const page = ¤t.full_transcript_installed_page.?; page.source.request = request; page.source.range = range; page.projection.measured_item_rows.clearRetainingCapacity(); for (range.start..range.end) |entry_index| { try page.projection.measured_item_rows.append(std.heap.c_allocator, .{ .entry_id = current.entries.items[entry_index].id(), .row = @intCast(entry_index - range.start), }); } page.projection.measured_total_rows = @intCast(range.len()); return page; } }; const tail_request = full_transcript_page.Request{ .content_revision = runtime.full_transcript_content_revision, .cols = runtime.layout.cols, .anchor = .tail, }; var installed = try PageWindow.install(&runtime, tail_request); const tail_start = installed.source.range.start; runtime.scrollFullTranscript(.up, .page); const older_anchor = switch (runtime.full_transcript_page_anchor) { .entry_index => |index| index, .tail => return error.TestExpectedOlderPage, }; try std.testing.expectEqual(@as(usize, 743), older_anchor); try std.testing.expect(runtime.full_transcript.bookmark_pending); try std.testing.expectEqual( @as(?u32, runtime.entries.items[older_anchor].id()), runtime.full_transcript.bookmark_entry_id, ); var older_request = tail_request; older_request.anchor = .{ .entry_index = older_anchor }; installed = try PageWindow.install(&runtime, older_request); var selected = runtime.full_transcript.select_visual_offset( installed.projection.measured_total_rows, 20, installed.projection.measured_item_rows.items, ); runtime.full_transcript = selected.state; const after_page_up = installed.source.range.start + @as(usize, @intCast(selected.offset)); try std.testing.expectEqual(@as(usize, 743), after_page_up); try std.testing.expect(after_page_up < tail_start); const max_offset = installed.projection.measured_total_rows - 20; runtime.full_transcript.scroll_rows = max_offset; runtime.full_transcript.follow_tail = true; const before_page_down = installed.source.range.start + @as(usize, max_offset); runtime.scrollFullTranscript(.down, .page); const newer_anchor = switch (runtime.full_transcript_page_anchor) { .entry_index => |index| index, .tail => return error.TestExpectedNewerPage, }; try std.testing.expectEqual(@as(usize, 871), newer_anchor); try std.testing.expect(runtime.full_transcript.bookmark_pending); try std.testing.expectEqual( @as(?u32, runtime.entries.items[newer_anchor].id()), runtime.full_transcript.bookmark_entry_id, ); var newer_request = older_request; newer_request.anchor = .{ .entry_index = newer_anchor }; installed = try PageWindow.install(&runtime, newer_request); selected = runtime.full_transcript.select_visual_offset( installed.projection.measured_total_rows, 20, installed.projection.measured_item_rows.items, ); const after_page_down = installed.source.range.start + @as(usize, @intCast(selected.offset)); try std.testing.expectEqual(@as(usize, 871), after_page_down); try std.testing.expect(after_page_down > before_page_down); } test "opening the full transcript leaves compact command projection unchanged" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stdout, "first\n", true); try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stdout, "second\n", true); try runtime.flushCommandOutputSummary(alloc, &metrics, styles, true); var compact_before = try runtime.prepareTranscriptSource(alloc, null); defer compact_before.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), compact_before.bytes.len); _ = try runtime.setTranscriptPresentationDepth(alloc, .full); var compact_after = try runtime.prepareTranscriptSource(alloc, null); defer compact_after.deinit(alloc); try std.testing.expectEqualStrings(compact_before.bytes, compact_after.bytes); } test "full transcript staging paints its selected wheel viewport without reselection" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full, .scroll_rows = 3, .follow_tail = false, }, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-0\nrow-1\nrow-2\nrow-3\nrow-4\nrow-5\nrow-6\nrow-7\n", .unknown_raw, ); var projection = try runtime.buildFullTranscriptProjection(alloc, null); defer projection.deinit(alloc); var metrics: Metrics = .{}; var staged = try runtime.prepareFullTranscriptSurfacePaint( alloc, &metrics, &projection, null, .{ .top = 1, .bottom = 4 }, ); defer staged.deinit(alloc); try std.testing.expectEqual(@as(u32, 3), runtime.full_transcript.scroll_rows); try std.testing.expect(!runtime.full_transcript.follow_tail); try std.testing.expectEqual(@as(usize, 0), staged.prepared.selection.start_line); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-3") != null); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-0") == null); } test "installed full transcript paints from the worker indexed source" { const alloc = std.testing.allocator; const page_alloc = std.heap.c_allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full, .scroll_rows = 3, .follow_tail = false, }, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\nrow-0\nrow-1\nrow-2\nrow-3\nrow-4\nrow-5\nrow-6\nrow-7\n", .unknown_raw, ); var projection = try runtime.buildFullTranscriptProjection(page_alloc, null); const bytes = try full_transcript_screen.renderProjectionViewportSourceBoundedInterruptible( page_alloc, &projection, null, runtime.layout.cols, 256, 0, 2 * 1024 * 1024, null, ); const prepared_source = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible( page_alloc, bytes, runtime.layout.cols, null, ); runtime.full_transcript_installed_page = .{ .source = .{ .request = runtime.desiredFullTranscriptPageRequest(), .range = .{ .start = 0, .end = runtime.entries.items.len }, }, .projection = projection, .prepared_window = .{ .source = prepared_source, .start_row = 0, }, }; const installed = &runtime.full_transcript_installed_page.?; var metrics: Metrics = .{}; var staged = try runtime.prepareFullTranscriptSurfacePaint( alloc, &metrics, &installed.projection, null, .{ .top = 1, .bottom = 4 }, ); defer staged.deinit(alloc); try std.testing.expect(staged.owned_source == null); try std.testing.expect(staged.borrowed_source == &installed.prepared_window.?.source); try std.testing.expectEqual(@as(?u16, 4), staged.prepared.projection_visual_rows); try std.testing.expect(staged.prepared.selection.last_visible_row <= 4); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-0") != null); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-7") != null); } test "clear retains installed full transcript page until window worker terminates" { const alloc = std.testing.allocator; const page_alloc = std.heap.c_allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, } }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-0\nrow-1\nrow-2\nrow-3\nrow-4\n", .unknown_raw, ); runtime.full_transcript_installed_page = .{ .source = .{ .request = runtime.desiredFullTranscriptPageRequest(), .range = .{ .start = 0, .end = runtime.entries.items.len }, }, .projection = try runtime.buildFullTranscriptProjection(page_alloc, null), }; const page = &runtime.full_transcript_installed_page.?; const task = try page_alloc.create(full_transcript_worker.WindowTask); task.* = .{ .request = .{ .page_request = page.source.request, .target_offset = 1, .start_row = 0, .row_count = 4, }, .projection = &page.projection, .capability = null, }; runtime.full_transcript_window_load.task = task; runtime.clearFullTranscriptDetails(alloc); try std.testing.expect(runtime.full_transcript_installed_page != null); try std.testing.expect(task.cancel_requested.load(.acquire)); task.done.store(true, .release); try std.testing.expect(!try runtime.pollFullTranscriptPageLoad()); try std.testing.expect(runtime.full_transcript_installed_page == null); } test "compact transcript cache survives navigation and invalidates on content change" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full }, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-0\nrow-1\nrow-2\nrow-3\nrow-4\n", .unknown_raw, ); var compact = try runtime.cachedTranscriptSource(alloc); defer compact.deinit(alloc); const cached_compact = runtime.compact_transcript_source_cache.find( runtime.full_transcript_content_revision, runtime.layout.cols, runtime.has_committed_frame, ).?; const compact_bytes = cached_compact.bytes.ptr; try std.testing.expect(cached_compact.hard_line_starts.len > 0); try std.testing.expectEqual( cached_compact.hard_line_starts.len, cached_compact.transcript_visible_lines.len, ); try std.testing.expectEqual( cached_compact.hard_line_starts.len, cached_compact.transcript_line_visual_rows.len, ); try std.testing.expectEqual( cached_compact.transcript_visible_lines.len, compact.transcript_visible_lines.len, ); runtime.markTranscriptDirty(); var same_compact = try runtime.cachedTranscriptSource(alloc); defer same_compact.deinit(alloc); try std.testing.expectEqual(compact_bytes, runtime.compact_transcript_source_cache.find( runtime.full_transcript_content_revision, runtime.layout.cols, runtime.has_committed_frame, ).?.bytes.ptr); runtime.has_committed_frame = true; try std.testing.expectEqual(@as(?*TranscriptPreparationSource, null), runtime.compact_transcript_source_cache.find( runtime.full_transcript_content_revision, runtime.layout.cols, runtime.has_committed_frame, )); var committed_compact = try runtime.cachedTranscriptSource(alloc); committed_compact.deinit(alloc); runtime.has_committed_frame = false; runtime.markTranscriptContentDirty(); var rebuilt_compact = try runtime.cachedTranscriptSource(alloc); defer rebuilt_compact.deinit(alloc); try std.testing.expect(compact_bytes != runtime.compact_transcript_source_cache.find( runtime.full_transcript_content_revision, runtime.layout.cols, runtime.has_committed_frame, ).?.bytes.ptr); } test "cached compact transcript refreshes after streamed assistant text" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "existing transcript\n", .unknown_raw, ); var before = try runtime.cachedTranscriptSource(alloc); defer before.deinit(alloc); try std.testing.expect(std.mem.find(u8, before.bytes, "streamed assistant") == null); var metrics = Metrics{}; _ = try runtime.streamAssistantChunk(alloc, &metrics, "streamed assistant\n"); var after = try runtime.cachedTranscriptSource(alloc); defer after.deinit(alloc); try std.testing.expect(std.mem.find(u8, after.bytes, "streamed assistant") != null); } test "clearing a transcript releases compact source and installed page" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full }, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "old session transcript\n", .unknown_raw, ); var compact = try runtime.cachedTranscriptSource(alloc); compact.deinit(alloc); const content_revision = runtime.full_transcript_content_revision; runtime.full_transcript_installed_page = .{ .source = .{ .request = .{ .content_revision = content_revision, .cols = runtime.layout.cols, .anchor = .tail, }, .range = .{ .start = 0, .end = 0 }, }, .projection = .{ .styles = .{} }, }; try std.testing.expect(runtime.compact_transcript_source_cache.find( content_revision, runtime.layout.cols, runtime.has_committed_frame, ) != null); try std.testing.expect(runtime.full_transcript_installed_page != null); runtime.clearTranscript(alloc); try std.testing.expect(runtime.compact_transcript_source_cache.find( content_revision, runtime.layout.cols, runtime.has_committed_frame, ) == null); try std.testing.expect(runtime.full_transcript_installed_page == null); } test "cached compact transcript preserves pending resume flow" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "retained tail only\n", .unknown_raw, ); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, "full resume history\nretained tail only\n"), ); var source = try runtime.cachedTranscriptSource(alloc); defer source.deinit(alloc); try std.testing.expect(std.mem.find(u8, source.bytes, "full resume history") != null); } const PendingBuildProbe = struct { polls: usize = 0, pending_after: ?usize = null, fn pending(context: *anyopaque) bool { const self: *PendingBuildProbe = @ptrCast(@alignCast(context)); self.polls += 1; return if (self.pending_after) |limit| self.polls >= limit else false; } }; test "pending resume source publishes its line index once" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8) }; runtime.layout.cols = 40; defer runtime.deinit(alloc); var flow: std.ArrayList(u8) = .empty; defer flow.deinit(alloc); for (0..5_000) |_| try flow.appendSlice(alloc, "resume row\n"); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try flow.toOwnedSlice(alloc), ); const flow_ptr = runtime.pendingResumeFlow().ptr; var cancelled_probe = PendingBuildProbe{ .pending_after = 1 }; var cancelled = build_checkpoint.BuildCheckpoint.init( &cancelled_probe, PendingBuildProbe.pending, ); try std.testing.expectError( error.InputPending, runtime.pendingResumeSourceInterruptible(alloc, &cancelled), ); try std.testing.expectEqual(@as(usize, 0), runtime.pending_resume_source.?.hard_line_starts.len); try std.testing.expectEqual(flow_ptr, runtime.pendingResumeFlow().ptr); var build_probe = PendingBuildProbe{}; var build = build_checkpoint.BuildCheckpoint.init(&build_probe, PendingBuildProbe.pending); const source = (try runtime.pendingResumeSourceInterruptible(alloc, &build)).?; const source_ptr = source; const index_ptr = source.hard_line_starts.ptr; try std.testing.expect(source.hard_line_starts.len > 0); runtime.layout.cols = 41; var resize_probe = PendingBuildProbe{ .pending_after = 2 }; var resize = build_checkpoint.BuildCheckpoint.init(&resize_probe, PendingBuildProbe.pending); try std.testing.expectError( error.InputPending, runtime.pendingResumeSourceInterruptible(alloc, &resize), ); try std.testing.expectEqual(source_ptr, &runtime.pending_resume_source.?); try std.testing.expectEqual(@as(u16, 40), runtime.pending_resume_source.?.cols); runtime.layout.cols = 40; for (0..3) |_| { var reuse_probe = PendingBuildProbe{}; var reuse = build_checkpoint.BuildCheckpoint.init(&reuse_probe, PendingBuildProbe.pending); const reused = (try runtime.pendingResumeSourceInterruptible(alloc, &reuse)).?; try std.testing.expectEqual(source_ptr, reused); try std.testing.expectEqual(index_ptr, reused.hard_line_starts.ptr); try std.testing.expectEqual(@as(usize, 1), reuse_probe.polls); } var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, source, .{ .top = 1, .bottom = 4 }, ); defer prepared.deinit(alloc); try std.testing.expectEqual(source.transcript_visible_lines.ptr, prepared.sourceVisibleLines().ptr); try std.testing.expectEqual(source.transcript_line_visual_rows.ptr, prepared.sourceVisualRows().ptr); try std.testing.expectEqual( source.transcript_visual_row_offsets[source.transcript_visual_row_offsets.len - 1], prepared.sourceTotalVisualRows(), ); } test "full transcript staging keeps its selected viewport visible during resize recovery" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, .full_transcript = .{ .depth = .full, .scroll_rows = 3, .follow_tail = false, }, .resize_history_row_delta = 22, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-0\nrow-1\nrow-2\nrow-3\nrow-4\nrow-5\nrow-6\nrow-7\n", .unknown_raw, ); var projection = try runtime.buildFullTranscriptProjection(alloc, null); defer projection.deinit(alloc); var metrics: Metrics = .{}; var staged = try runtime.prepareFullTranscriptSurfacePaint( alloc, &metrics, &projection, null, .{ .top = 1, .bottom = 4 }, ); defer staged.deinit(alloc); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-3") != null); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-0") == null); try std.testing.expectEqual(@as(?i32, 22), runtime.resize_history_row_delta); try std.testing.expectEqual(@as(usize, 0), staged.prepared.selection.start_line); try std.testing.expect(staged.prepared.selection.last_visible_row >= staged.prepared.selection.top_row); } test "auto permission notice is retained without changing compact transcript geometry" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); const cursor_row_before = runtime.cursor_row; const cursor_col_before = runtime.cursor_col; const viewport_before = runtime.viewport_top_row; _ = try runtime.appendSemanticNotice(alloc, .{ .topic = "system", .tone = .information, .body = "Auto agent approved this request: Running command.", .visibility = .full_only, }); try std.testing.expectEqual(@as(usize, 1), runtime.entries.items.len); try std.testing.expect(runtime.entries.items[0] == .semantic_notice); try std.testing.expectEqualStrings("system", runtime.entries.items[0].semantic_notice.topic); try std.testing.expectEqual(types.NoticeTone.information, runtime.entries.items[0].semantic_notice.tone); try std.testing.expectEqualStrings( "Auto agent approved this request: Running command.", runtime.entries.items[0].semantic_notice.body, ); try std.testing.expectEqual(types.NoticeVisibility.full_only, runtime.entries.items[0].semantic_notice.visibility); try std.testing.expectEqual(@as(usize, 0), runtime.transcript.items.len); try std.testing.expectEqual(cursor_row_before, runtime.cursor_row); try std.testing.expectEqual(cursor_col_before, runtime.cursor_col); try std.testing.expectEqual(viewport_before, runtime.viewport_top_row); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "● Ran printf done\n", .tool_status, ); var compact = try runtime.prepareTranscriptSource(alloc, null); defer compact.deinit(alloc); try std.testing.expect(std.mem.find(u8, compact.bytes, "Auto agent approved") == null); try std.testing.expect(std.mem.find(u8, compact.bytes, "1 tool call") != null); try std.testing.expect(std.mem.find(u8, compact.bytes, "Ran printf done") != null); var projection = try runtime.buildFullTranscriptProjection(alloc, null); defer projection.deinit(alloc); const full = try full_transcript_screen.renderProjectionViewportSourceInterruptible( alloc, &projection, null, runtime.layout.cols, 20, 0, null, ); defer alloc.free(full); const notice_index = std.mem.find(u8, full, "Auto agent approved") orelse return error.TestExpectedAutoPermissionNotice; const status_index = std.mem.find(u8, full, "Ran printf done") orelse return error.TestExpectedToolStatus; try std.testing.expect(notice_index < status_index); } fn checkAutoPermissionNoticeAllocationFailures(alloc: Allocator) !void { var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); const cursor_row_before = runtime.cursor_row; const cursor_col_before = runtime.cursor_col; _ = runtime.appendSemanticNotice(alloc, .{ .topic = "system", .tone = .information, .body = "Auto agent approved this request: Running command.", .visibility = .full_only, }) catch |err| { try std.testing.expectEqual(@as(usize, 0), runtime.entries.items.len); try std.testing.expectEqual(@as(usize, 0), runtime.transcript.items.len); try std.testing.expectEqual(cursor_row_before, runtime.cursor_row); try std.testing.expectEqual(cursor_col_before, runtime.cursor_col); return switch (err) { error.WriteFailed => error.OutOfMemory, else => err, }; }; try std.testing.expectEqual(@as(usize, 1), runtime.entries.items.len); try std.testing.expectEqual(@as(usize, 0), runtime.transcript.items.len); } test "auto permission notice write is atomic across allocation failures" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkAutoPermissionNoticeAllocationFailures, .{}, ); } test "full transcript opening follows the tail instead of consuming its compact anchor" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-0\nrow-1\nrow-2\n", .unknown_raw, ); const anchor_id = try runtime.appendRawTranscriptEntryClassified( alloc, "anchor\n", .unknown_raw, ); runtime.full_transcript = runtime.full_transcript.capture_anchor(anchor_id).with_depth(.full); _ = try runtime.appendRawTranscriptEntryClassified( alloc, "row-4\nrow-5\nrow-6\nrow-7\n", .unknown_raw, ); var projection = try runtime.buildFullTranscriptProjection(alloc, null); defer projection.deinit(alloc); const measurement = try full_transcript_screen.measureProjectionInterruptible( alloc, &projection, null, runtime.layout.cols, null, ); var metrics: Metrics = .{}; var staged = try runtime.prepareFullTranscriptSurfacePaint( alloc, &metrics, &projection, null, .{ .top = 1, .bottom = 4 }, ); defer staged.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), staged.prepared.selection.start_line); try std.testing.expectEqual( measurement.total_rows -| 4, runtime.full_transcript.scroll_rows, ); try std.testing.expect(runtime.full_transcript.follow_tail); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-7") != null); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "anchor") == null); try std.testing.expect(std.mem.find(u8, staged.source().bytes, "row-0") == null); try std.testing.expect(!runtime.full_transcript.anchor_pending); } test "full transcript primary restore repairs resize debt after geometry returns" { var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; runtime.full_transcript_open_content_revision = 7; runtime.full_transcript_content_revision = 7; runtime.full_transcript_open_cols = 80; runtime.full_transcript_open_rows = 24; try std.testing.expectEqual(FullTranscriptPrimaryRestore.exact, runtime.fullTranscriptPrimaryRestore()); runtime.terminal_reset_pending = true; try std.testing.expectEqual(FullTranscriptPrimaryRestore.resized, runtime.fullTranscriptPrimaryRestore()); runtime.full_transcript_content_revision = 8; try std.testing.expectEqual(FullTranscriptPrimaryRestore.resized, runtime.fullTranscriptPrimaryRestore()); } test "full transcript resized close queues primary recovery" { var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; runtime.render_requests.completeResizeGeometry(true); runtime.pending_resize_observation = .{ .layout = runtime.layout, .apply_after_ms = 17, .expected_row = null, .result = .unavailable, .phase = .settle_pending, }; runtime.terminal_reset_pending = true; runtime.resize_history_row_delta = 4; runtime.owned_top_row = 5; runtime.repaintRestoredPrimaryTranscriptAfterResize(); try std.testing.expect(!runtime.render_requests.pending_settled_width_reflow); try std.testing.expectEqual(@as(?ResizeObservation, null), runtime.pending_resize_observation); try std.testing.expect(runtime.terminal_reset_pending); try std.testing.expectEqual(@as(?i32, null), runtime.resize_history_row_delta); const invalidations = runtime.render_requests.pendingInvalidations(); try std.testing.expectEqual(@as(u8, 1), invalidations.len); try std.testing.expectEqual(@as(u16, 5), invalidations.ranges()[0].top); try std.testing.expectEqual(@as(u16, 24), invalidations.ranges()[0].bottom); try std.testing.expect(runtime.render_requests.hasReason(.external_damage)); } test "full transcript resized restore bounds invalidation to the current terminal" { for ([_]u16{ 0, 25 }) |owned_top| { var runtime = TranscriptRuntime{ .layout = std.mem.zeroes(Layout), .owned_top_row = owned_top }; runtime.layout.rows = 24; runtime.repaintRestoredPrimaryTranscriptAfterResize(); const invalidations = runtime.render_requests.pendingInvalidations(); try std.testing.expectEqual(@as(u8, 1), invalidations.len); try std.testing.expectEqual(@as(u16, 1), invalidations.ranges()[0].top); try std.testing.expectEqual(@as(u16, 24), invalidations.ranges()[0].bottom); } var empty = TranscriptRuntime{ .layout = std.mem.zeroes(Layout) }; empty.repaintRestoredPrimaryTranscriptAfterResize(); try std.testing.expect(empty.render_requests.pendingInvalidations().isEmpty()); } test "full transcript opening waits for primary damage and remains cancellable" { var runtime = TranscriptRuntime{ .layout = std.mem.zeroes(Layout) }; runtime.layout.rows = 24; runtime.layout.cols = 80; try std.testing.expect(runtime.requestFullTranscriptOpen()); runtime.repaintRestoredPrimaryTranscriptAfterResize(); try std.testing.expect(!runtime.requestFullTranscriptOpen()); try std.testing.expect(runtime.full_transcript_open_request != null); var interrupted = (try runtime.render_requests.beginAttempt()).?; interrupted.restore(); try std.testing.expect(runtime.cancelPendingFullTranscriptOpen()); try std.testing.expect(!runtime.requestFullTranscriptOpen()); var committed = (try runtime.render_requests.beginAttempt()).?; committed.commit(0, 0, false); try std.testing.expect(runtime.requestFullTranscriptOpen()); try std.testing.expect(runtime.full_transcript_open_request == null); } test "authoritative lifecycle detail is retained by its compact status entry" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const id = types.ToolLifecycleId{ .turn_id = 1, .call_id = "read-1" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .presentation_group_id = .{ .turn_id = 1, .anchor_step_id = 4 }, .reconciles_provisional_call_id = null, .tool_name = "read_file", .activity_kind = .read, .arguments_json = "{\"path\":\"README.md\"}", } }); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Read README.md" }, .result = "# README\nfull result", } }); const entry_id = runtime.toolActivityRecord(id).?.entry_id; const detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqual(types.ToolActivityKind.read, detail.activity_kind.?); try std.testing.expectEqualStrings("{\"path\":\"README.md\"}", detail.arguments_json.?); try std.testing.expectEqualStrings("# README\nfull result", detail.result.?); try std.testing.expectEqual(@as(u64, 1), detail.lifecycle_id.?.turn_id); try std.testing.expectEqualStrings("read-1", detail.lifecycle_id.?.call_id); try std.testing.expect(detail.lifecycle_id.?.call_id.ptr != id.call_id.ptr); try std.testing.expectEqual( types.ToolPresentationGroupId{ .turn_id = 1, .anchor_step_id = 4 }, detail.presentation_group_id.?, ); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expect(std.mem.find(u8, source.bytes, "Read README.md") != null); try std.testing.expect(std.mem.find(u8, source.bytes, "input") == null); try std.testing.expect(std.mem.find(u8, source.bytes, "full result") == null); } test "paused tool turn can resume the same lifecycle identity before finalization" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); const id = types.ToolLifecycleId{ .turn_id = 41, .call_id = "resume-tool" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "read_file", .activity_kind = .read, } }); _ = try runtime.applyToolLifecycle(alloc, .{ .turn_finished = .{ .turn_id = id.turn_id, .outcome = .paused, } }); try runtime.finishLifecycleBatch(alloc); try std.testing.expectEqual(@as(u64, 0), runtime.finalizedToolTurnWatermark()); try std.testing.expect(runtime.toolActivityRecord(id) == null); var paused_source = try runtime.prepareTranscriptSource(alloc, null); defer paused_source.deinit(alloc); try std.testing.expect(std.mem.find( u8, paused_source.bytes, "Tool state preserved for recovery", ) != null); _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "read_file", .activity_kind = .read, } }); try std.testing.expect(runtime.toolActivityRecord(id) != null); try std.testing.expectEqual(@as(usize, 1), runtime.activeToolActivityCount()); _ = try runtime.applyToolLifecycle(alloc, .{ .turn_finished = .{ .turn_id = id.turn_id, .outcome = .completed, } }); try runtime.finishLifecycleBatch(alloc); try std.testing.expectEqual(id.turn_id, runtime.finalizedToolTurnWatermark()); } test "provisional lifecycle owns semantic detail before authoritative reconciliation" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); const id = types.ToolLifecycleId{ .turn_id = 7, .call_id = "provisional-command" }; _ = try runtime.applyToolLifecycle(alloc, .{ .provisional = .{ .id = id, .tool_name = null, .activity_kind = .command, } }); const entry_id = runtime.toolActivityRecord(id).?.entry_id; const initial_detail = runtime.toolDetailForEntry(entry_id); try std.testing.expect(initial_detail != null); try std.testing.expectEqualStrings("tool", initial_detail.?.tool_name); try std.testing.expectEqual(types.ToolActivityKind.command, initial_detail.?.activity_kind.?); try std.testing.expectEqual(@as(u64, 7), initial_detail.?.lifecycle_id.?.turn_id); try std.testing.expectEqualStrings("provisional-command", initial_detail.?.lifecycle_id.?.call_id); try std.testing.expect(initial_detail.?.lifecycle_id.?.call_id.ptr != id.call_id.ptr); _ = try runtime.applyToolLifecycle(alloc, .{ .provisional = .{ .id = id, .tool_name = "run_command", .activity_kind = .command, } }); const updated_detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqualStrings("run_command", updated_detail.tool_name); try std.testing.expectEqual(@as(usize, 1), runtime.tool_details.items.len); } test "nonzero command remains Ran in the current compact projection" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const id = types.ToolLifecycleId{ .turn_id = 1, .call_id = "exit-7" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "terminal", .activity_kind = .command, .arguments_json = "{\"action\":\"exec\",\"command\":\"false\"}", } }); try std.testing.expect(runtime.toolActivityRecord(id).?.captured_command); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran false" }, .result_memory = .{ .command_process_presentation = .{ .exit_code = 7 } }, } }); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expect(std.mem.find(u8, source.bytes, "Ran \x1b[38;5;245m\x1b[38;5;252mfalse\x1b[39m") != null); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, source.bytes, "│ exit code 7")); try std.testing.expect(std.mem.find(u8, source.bytes, "Failed") == null); } test "terminal lifecycle records distinguish captured exec from durable actions" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); const exec_id = types.ToolLifecycleId{ .turn_id = 1, .call_id = "exec" }; const start_id = types.ToolLifecycleId{ .turn_id = 1, .call_id = "start" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = exec_id, .reconciles_provisional_call_id = null, .tool_name = "terminal", .activity_kind = .command, .arguments_json = "{\"action\":\"exec\",\"command\":\"printf exec\"}", } }); _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = start_id, .reconciles_provisional_call_id = null, .tool_name = "terminal", .activity_kind = .command, .arguments_json = "{\"action\":\"start\",\"command\":\"printf start\"}", } }); try std.testing.expect(runtime.toolActivityRecord(exec_id).?.captured_command); try std.testing.expect(!runtime.toolActivityRecord(start_id).?.captured_command); } test "nonzero streamed command reuses its active output block" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 2, .call_id = "streamed-exit-7" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "terminal", .activity_kind = .command, .arguments_json = "{\"action\":\"exec\",\"command\":\"printf lines; exit 7\"}", } }); const started_entry_id = runtime.toolActivityRecord(id).?.entry_id; try std.testing.expect(runtime.toolDetailForEntry(started_entry_id).?.isCapturedCommand()); var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle( alloc, &metrics, styles, id, .stdout, "line-1\nline-2\nline-3\nline-4\nline-5\nline-6\n", true, ); try std.testing.expect(runtime.toolDetailForEntry(started_entry_id).?.isCapturedCommand()); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran printf lines; exit 7" }, .result_memory = .{ .command_process_presentation = .{ .exit_code = 7 } }, } }); try std.testing.expectEqual(@as(usize, 1), runtime.command_output_blocks.items.len); try runtime.flushCommandOutputSummaryForLifecycle( alloc, &metrics, styles, id, true, ); const block = runtime.command_output_blocks.items[0]; const status_entry_id = runtime.toolActivityRecord(id).?.entry_id; try std.testing.expect(runtime.toolDetailForEntry(status_entry_id).?.isCapturedCommand()); try std.testing.expectEqual( block.entry_id, runtime.toolDetailForEntry(status_entry_id).?.command_output_entry_id, ); try std.testing.expect(command_output_runtime.processPresentationForBlock(&runtime, block) != null); var projection = try command_output_runtime.renderCompactCommandOutputWithProcessPresentation( alloc, block, .{ .styles = styles }, 80, command_output_runtime.processPresentationForBlock(&runtime, block), ); defer projection.deinit(alloc); try std.testing.expectEqual( @as(usize, command_output_runtime.compact_output_row_limit + 1), std.mem.count(u8, projection.bytes.items, "\n") + 1, ); try std.testing.expect(std.mem.find(u8, projection.bytes.items, "│ line-4") != null); try std.testing.expect(std.mem.find(u8, projection.bytes.items, "│ line-5") == null); try std.testing.expect(std.mem.find(u8, projection.bytes.items, "│ … 2 lines more (ctrl+o to view)") != null); try std.testing.expectEqual( @as(usize, 1), std.mem.count(u8, projection.bytes.items, "│ exit code 7"), ); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expect(std.mem.find(u8, source.bytes, "\x1b[38;5;252mprintf\x1b[39m") != null); try std.testing.expect(std.mem.find(u8, source.bytes, "\x1b[38;5;252m;\x1b[39m") != null); try std.testing.expect(std.mem.find(u8, source.bytes, "\x1b[38;5;252mexit\x1b[39m") != null); try std.testing.expect(std.mem.find(u8, source.bytes, "\x1b[38;5;250m7\x1b[39m") == null); try std.testing.expect(std.mem.find(u8, source.bytes, "Failed") == null); } test "compact run command group stays width safe after resize" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 24, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 3, .call_id = "long-command-header" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf a deliberately long command preview\"}", } }); var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle( alloc, &metrics, styles, id, .stdout, "payload\n", true, ); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran printf a deliberately long command preview that exceeds both narrow and normal transcript widths without losing its typed command identity", }, } }); try runtime.flushCommandOutputSummaryForLifecycle( alloc, &metrics, styles, id, true, ); const status_entry_id = runtime.toolActivityRecord(id).?.entry_id; const output_entry_id = runtime.command_output_blocks.items[0].entry_id.?; for ([_]u16{ 24, 80 }) |cols| { runtime.layout.cols = cols; var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); var status_line: ?usize = null; var output_line: ?usize = null; var status_line_count: usize = 0; for (source.line_provenance, 0..) |provenance, line_index| { switch (provenance) { .entry => |entry| { if (entry.entry_id == status_entry_id) { if (status_line == null) status_line = line_index; status_line_count += 1; } if (entry.entry_id == output_entry_id and output_line == null) { output_line = line_index; } }, else => {}, } } try std.testing.expectEqual(@as(usize, 2), status_line_count); try std.testing.expect(output_line == null); try std.testing.expect(status_line != null); var lines = std.mem.splitScalar(u8, source.bytes, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); } try std.testing.expect(std.mem.find(u8, source.bytes, "…") != null); } } fn checkToolDetailResultReplacementAllocationFailures(alloc: Allocator) !void { var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); var tool_name: ?[]u8 = try alloc.dupe(u8, "read_file"); errdefer if (tool_name) |value| alloc.free(value); var previous_result: ?[]u8 = try alloc.dupe(u8, "previous result"); errdefer if (previous_result) |value| alloc.free(value); var previous_artifact: ?[]u8 = try alloc.dupe(u8, "fx-command-previous.log"); errdefer if (previous_artifact) |value| alloc.free(value); try runtime.tool_details.append(alloc, .{ .entry_id = 1, .tool_name = tool_name.?, .result = previous_result.?, .command_artifact_handle = previous_artifact.?, }); tool_name = null; previous_result = null; previous_artifact = null; runtime.updateToolDetailTerminal( alloc, 1, null, "read_file", .read, .completed, "replacement result", null, "fx-command-replacement.log", null, ) catch |err| { try std.testing.expectEqualStrings( "previous result", runtime.toolDetailForEntry(1).?.result.?, ); try std.testing.expectEqualStrings( "fx-command-previous.log", runtime.toolDetailForEntry(1).?.command_artifact_handle.?, ); return err; }; try std.testing.expectEqualStrings( "replacement result", runtime.toolDetailForEntry(1).?.result.?, ); try std.testing.expectEqualStrings( "fx-command-replacement.log", runtime.toolDetailForEntry(1).?.command_artifact_handle.?, ); } test "tool detail result replacement preserves the prior allocation on failure" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkToolDetailResultReplacementAllocationFailures, .{}, ); } test "historical tool detail attaches to the exact replayed status entry" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .completed, "Read README.md", true, ); try runtime.attachHistoricalToolDetail( alloc, entry_id, .{ .id = "replayed-read", .name = "read_file", .arguments_json = "{\"path\":\"README.md\"}" }, .read, .{ .tool_call_id = @constCast("replayed-read"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("# README\nfull replayed result"), .output_bytes = 29, .stored_output_bytes = 29, .created_at_ms = 123, }, ); const detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqualStrings("{\"path\":\"README.md\"}", detail.arguments_json.?); try std.testing.expectEqualStrings("# README\nfull replayed result", detail.result.?); try std.testing.expectEqual(types.ToolOutcomeKind.completed, detail.outcome.?); try std.testing.expectEqual(@as(i64, 123), detail.created_at_ms); } test "historical context-withheld tool detail keeps the call without result evidence" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .deferred, "Reading project instructions before continuing: Running cat nested/input.txt", true, ); try runtime.attachHistoricalToolDetail( alloc, entry_id, .{ .id = "deferred-command", .name = "run_command", .arguments_json = "{\"command\":\"cat nested/input.txt\"}", }, .command, .{ .tool_call_id = @constCast("deferred-command"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast(types.context_deferred_tool_result_output), .output_handle = @constCast("should-not-be-exposed.txt"), .preview = @constCast("should not be exposed"), .output_bytes = types.context_deferred_tool_result_output.len, .stored_output_bytes = types.context_deferred_tool_result_output.len, .truncated = true, .command_output_replay = .unavailable, .command_process_presentation = .{ .exit_code = 7 }, }, ); const detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqualStrings("run_command", detail.tool_name); try std.testing.expectEqualStrings("{\"command\":\"cat nested/input.txt\"}", detail.arguments_json.?); try std.testing.expectEqual(types.ToolOutcomeKind.deferred, detail.outcome.?); try std.testing.expect(detail.result == null); try std.testing.expect(detail.result_handle == null); try std.testing.expect(detail.command_artifact_handle == null); try std.testing.expect(detail.command_output_replay == null); try std.testing.expect(detail.command_process_presentation == null); try std.testing.expect(detail.command_output_entry_id == null); const legacy_entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .denied, "Not executed: Reading stale.txt", true, ); try runtime.attachHistoricalToolDetail( alloc, legacy_entry_id, .{ .id = "legacy-deferred-read", .name = "read_file", .arguments_json = "{\"path\":\"stale.txt\"}", }, .read, .{ .tool_call_id = @constCast("legacy-deferred-read"), .tool_name = @constCast("read_file"), .status = .failure, .output = @constCast(types.deferred_tool_result_output), .output_bytes = types.deferred_tool_result_output.len, .stored_output_bytes = types.deferred_tool_result_output.len, }, ); const legacy_detail = runtime.toolDetailForEntry(legacy_entry_id).?; try std.testing.expectEqual(types.ToolOutcomeKind.denied, legacy_detail.outcome.?); try std.testing.expect(legacy_detail.result == null); const failed_entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .failed, "Failed printf failure", true, ); try runtime.attachHistoricalToolDetail( alloc, failed_entry_id, .{ .id = "failed-command", .name = "run_command", .arguments_json = "{\"command\":\"printf failure; exit 7\"}", }, .command, .{ .tool_call_id = @constCast("failed-command"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast("ordinary failure"), .output_handle = @constCast("ordinary-failure.txt"), .output_bytes = 16, .stored_output_bytes = 16, }, ); const failed_detail = runtime.toolDetailForEntry(failed_entry_id).?; try std.testing.expectEqual(types.ToolOutcomeKind.failed, failed_detail.outcome.?); try std.testing.expectEqualStrings("ordinary failure", failed_detail.result.?); try std.testing.expectEqualStrings("ordinary-failure.txt", failed_detail.result_handle.?); } test "historical permission denial keeps denied outcome without internal result detail" { const alloc = std.testing.allocator; const denial_output = "{\"error\":{\"type\":\"tool_permission_denied\",\"reason\":\"auto_denied\"}}"; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .denied, "Denied by auto agent pwd", true, ); try runtime.attachHistoricalToolDetail( alloc, entry_id, .{ .id = "denied-command", .name = "terminal", .arguments_json = "{\"action\":\"exec\",\"command\":\"pwd\"}", }, .command, .{ .tool_call_id = @constCast("denied-command"), .tool_name = @constCast("terminal"), .status = .failure, .output = @constCast(denial_output), .output_bytes = denial_output.len, .stored_output_bytes = denial_output.len, }, ); const detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqual(types.ToolOutcomeKind.denied, detail.outcome.?); try std.testing.expect(detail.result == null); try std.testing.expect(detail.result_handle == null); try std.testing.expect(detail.command_output_entry_id == null); } test "historical command detail keeps artifact handles after command block attaches" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDir( io_mod.getIo(), "session", std.Io.File.Permissions.fromMode(0o700), ); var session_dir = try tmp.dir.openDir(io_mod.getIo(), "session", .{ .iterate = true, .follow_symlinks = false, }); defer session_dir.close(io_mod.getIo()); const session_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "session"); defer alloc.free(session_path); var capability = try session_child_store.SessionChildCapability.initForTesting( alloc, session_dir, session_path, .writable, .{}, ); defer capability.deinit(); const stored_result = "exit_code=0\n\nRESUMED_EXACT_RESULT\n\n"; const result_handle = try result_store.storeLargeResultManaged( alloc, &capability, "replayed-command", "run_command", stored_result, ); defer alloc.free(result_handle); defer result_store.deleteManaged(&capability, result_handle) catch {}; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const raw_preview = "exit_code=0\ntruncated=true\nstdout_bytes=21\nstderr_bytes=0\noutput_file=/tmp/fx-command-replayed.log\nRESUMED_EXACT_RESULT\n"; const raw_result = try std.fmt.allocPrint( alloc, "\n{s}\n\n" ++ "{s}", .{ result_handle, stored_result.len, raw_preview, result_handle }, ); defer alloc.free(raw_result); var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .completed, "Ran printf restored", true, ); try runtime.attachHistoricalToolDetail( alloc, entry_id, .{ .id = "replayed-command", .name = "run_command", .arguments_json = "{\"command\":\"printf restored\"}" }, .command, .{ .tool_call_id = @constCast("replayed-command"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast(raw_result), .output_handle = result_handle, .preview = @constCast(raw_preview), .output_bytes = stored_result.len, .stored_output_bytes = stored_result.len, .truncated = true, }, ); runtime.attachHistoricalCommandOutput(alloc, entry_id); const fallback_detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expect(fallback_detail.command_output_entry_id == null); try std.testing.expectEqualStrings(raw_result, fallback_detail.result.?); try std.testing.expectEqualStrings(result_handle, fallback_detail.result_handle.?); try std.testing.expectEqualStrings("fx-command-replayed.log", fallback_detail.command_artifact_handle.?); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "RESUMED_EXACT_RESULT\n", true, ); try runtime.flushCommandOutputSummary(alloc, &metrics, styles, true); runtime.attachHistoricalCommandOutput(alloc, entry_id); const command_detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expect(command_detail.command_output_entry_id != null); try std.testing.expectEqualStrings(raw_result, command_detail.result.?); try std.testing.expectEqualStrings(result_handle, command_detail.result_handle.?); try std.testing.expectEqualStrings("fx-command-replayed.log", command_detail.command_artifact_handle.?); runtime.full_transcript.depth = .full; var projection = try runtime.buildFullTranscriptProjection(alloc, null); defer projection.deinit(alloc); const full_source = try full_transcript_screen.renderProjectionViewportSourceInterruptible( alloc, &projection, &capability, runtime.layout.cols, 10, 0, null, ); defer alloc.free(full_source); try std.testing.expectEqual( @as(usize, 1), std.mem.count(u8, full_source, "RESUMED_EXACT_RESULT"), ); try std.testing.expect(std.mem.find(u8, full_source, "exit_code=0") == null); try std.testing.expect(std.mem.find(u8, full_source, "") == null); try std.testing.expect(std.mem.find(u8, full_source, "") == null); } test "historical nonzero command keeps replay ownership outside compact sidebands" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .completed, "Ran false", true, ); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "one\ntwo\nthree\nfour\nfive\nsix\n", true, ); try runtime.flushCommandOutputSummary(alloc, &metrics, styles, true); const command_output_entry_id = runtime.latestCommandOutputEntryId().?; const replay_handle = "fx-command-replay-historical.bin"; try runtime.attachHistoricalToolDetailAfterCommandOutput( alloc, entry_id, .{ .id = "historical-exit", .name = "run_command", .arguments_json = "{\"command\":\"false\"}" }, .command, .{ .tool_call_id = @constCast("historical-exit"), .tool_name = @constCast("run_command"), .status = .failure, .output = @constCast("exit_code=7\n(no output)\n"), .output_bytes = 24, .stored_output_bytes = 24, .command_output_replay = .{ .available = .{ .handle = replay_handle, .framed_bytes = 123, } }, .command_process_presentation = .{ .exit_code = 7 }, }, ); runtime.attachHistoricalCommandOutput(alloc, entry_id); try std.testing.expectEqual(@as(usize, 1), runtime.command_output_blocks.items.len); const detail = runtime.toolDetailForEntry(entry_id).?; try std.testing.expectEqual(types.ToolOutcomeKind.completed, detail.outcome.?); try std.testing.expectEqual(command_output_entry_id, detail.command_output_entry_id.?); try std.testing.expectEqual( types.CommandProcessPresentation{ .exit_code = 7 }, detail.command_process_presentation.?, ); const replay = detail.command_output_replay.?; const descriptor = switch (replay) { .available => |value| value, .unavailable => return error.TestExpectedReplay, }; try std.testing.expectEqualStrings(replay_handle, descriptor.handle); try std.testing.expect(descriptor.handle.ptr != replay_handle.ptr); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), std.mem.count(u8, source.bytes, "│ ")); try std.testing.expect(std.mem.find(u8, source.bytes, "Ran \x1b[38;5;245m\x1b[38;5;252mfalse\x1b[39m") != null); try std.testing.expect(std.mem.find(u8, source.bytes, "│ exit code 7") == null); try std.testing.expect(std.mem.find(u8, source.bytes, "│ … 2 lines more") == null); try std.testing.expect(std.mem.find(u8, source.bytes, "│ five") == null); try std.testing.expect(std.mem.find(u8, source.bytes, "Failed") == null); } test "historical command detail ignores artifact-shaped command output" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); const raw_result = "exit_code=0\n\noutput_file=/tmp/fx-command-other.log\n\n"; var metrics: Metrics = .{}; const entry_id = try runtime.writeCompletedToolStatusReturningEntryId( alloc, &metrics, .completed, "Ran printf artifact-shaped-output", true, ); try runtime.attachHistoricalToolDetail( alloc, entry_id, .{ .id = "replayed-command", .name = "run_command", .arguments_json = "{\"command\":\"printf artifact-shaped-output\"}" }, .command, .{ .tool_call_id = @constCast("replayed-command"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast(raw_result), .output_bytes = raw_result.len, .stored_output_bytes = raw_result.len, }, ); try std.testing.expect(runtime.toolDetailForEntry(entry_id).?.command_artifact_handle == null); } test "full transcript retains previews when stored result handles cannot be opened" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "results"); const result_dir = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "results"); defer alloc.free(result_dir); var capability = try session_child_store.SessionChildCapability.initLegacyRoute( alloc, result_dir, .tool_results, .writable, ); defer capability.deinit(); const handle = try result_store.storeLargeResultManaged( alloc, &capability, "detail", "read_file", "FULL_TRANSCRIPT_SIDECAR_BODY\n", ); defer alloc.free(handle); defer result_store.deleteManaged(&capability, handle) catch {}; var runtime = TranscriptRuntime{}; defer runtime.deinit(alloc); try runtime.tool_details.append(alloc, .{ .entry_id = 1, .tool_name = try alloc.dupe(u8, "read_file"), .result = try alloc.dupe(u8, "preview"), .result_handle = try alloc.dupe(u8, handle), }); try runtime.tool_details.append(alloc, .{ .entry_id = 2, .tool_name = try alloc.dupe(u8, "read_file"), .result_handle = try alloc.dupe(u8, "missing-result.txt"), }); try std.testing.expect(runtime.hasStoredResultDetails()); try std.testing.expectEqualStrings( "preview", runtime.toolDetailForEntry(1).?.result.?, ); try std.testing.expect(runtime.toolDetailForEntry(2).?.result == null); } test "command terminal detail owns the consolidated command output entry" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-9" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf demo\"}", } }); var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, id, .stdout, "first\n", true); try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, id, .stdout, "second\n", true); try runtime.flushCommandOutputSummaryForLifecycle( alloc, &metrics, styles, id, true, ); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran printf demo" }, .command_artifact_handle = "fx-command-live.log", } }); const status_entry_id = runtime.toolActivityRecord(id).?.entry_id; const detail = runtime.toolDetailForEntry(status_entry_id).?; try std.testing.expectEqual(runtime.command_output_blocks.items[0].entry_id, detail.command_output_entry_id); try std.testing.expectEqualStrings("fx-command-live.log", detail.command_artifact_handle.?); } test "late command output reopens its completed lifecycle block" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); var metrics: Metrics = .{}; const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 7, .call_id = "late-output" }; try runtime.writeCommandOutputChunkForLifecycle( alloc, &metrics, styles, id, .stdout, "one\ntwo\nthree\nfour\nfive\nsix\n", true, ); try runtime.flushCommandOutputSummaryForLifecycle( alloc, &metrics, styles, id, true, ); const entry_id = runtime.command_output_blocks.items[0].entry_id.?; try runtime.writeCommandOutputChunkForLifecycle( alloc, &metrics, styles, id, .stdout, "seven\neight\n", true, ); try std.testing.expectEqual(@as(usize, 1), runtime.command_output_blocks.items.len); try std.testing.expectEqual(entry_id, runtime.command_output_blocks.items[0].entry_id.?); try std.testing.expectEqual(@as(usize, 8), runtime.command_output_blocks.items[0].total_lines); runtime.resetCommandOutputDisplay(alloc, "worker inactive"); try std.testing.expectEqual(@as(usize, 1), runtime.command_output_blocks.items.len); } test "command terminal detail resolves split command output source rows" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-split" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf demo\"}", } }); var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, id, .stdout, "first\n", true); _ = try runtime.appendRawTranscriptEntryClassified(alloc, "intervening\n", .unknown_raw); try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, id, .stdout, "second\n", true); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, id, true); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran printf demo" }, } }); const status_entry_id = runtime.toolActivityRecord(id).?.entry_id; const block = runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.source_entry_ids.items.len); try std.testing.expectEqual(status_entry_id, runtime.detailEntryIdForCommandOutputSource(block.source_entry_ids.items[0]).?); try std.testing.expectEqual(status_entry_id, runtime.detailEntryIdForCommandOutputSource(block.source_entry_ids.items[1]).?); } test "command output consolidation preserves current compact ownership" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 32, .cols = 100, .content_bottom = 28, .divider_top_row = 29, .input_row = 30, .divider_bottom_row = 31, .hint_row = 32, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const id = types.ToolLifecycleId{ .turn_id = 1, .call_id = "streaming-command" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"stream\"}", } }); const status_entry_id = runtime.toolActivityRecord(id).?.entry_id; var metrics: Metrics = .{}; for (0..80) |index| { var line: [32]u8 = undefined; const text = try std.fmt.bufPrint(&line, "STREAM {d:0>3}\n", .{index + 1}); try runtime.writeCommandOutputChunkForLifecycle( alloc, &metrics, styles, id, .stdout, text, true, ); } var live = try runtime.prepareTranscriptSourceOmittingEntry(alloc, status_entry_id); defer live.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), live.bytes.len); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, id, true); var compact = try runtime.prepareTranscriptSource(alloc, null); defer compact.deinit(alloc); try std.testing.expect(std.mem.indexOf(u8, compact.bytes, "STREAM 001") == null); try std.testing.expect(std.mem.indexOf(u8, compact.bytes, "1 tool call") != null); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = id, .outcome = .{ .kind = .completed, .summary = "Ran stream" }, } }); var completed = try runtime.prepareTranscriptSource(alloc, null); defer completed.deinit(alloc); const status_pos = std.mem.indexOf(u8, completed.bytes, "stream") orelse return error.MissingCommandStatus; try std.testing.expect(std.mem.indexOf(u8, completed.bytes, "STREAM 001") == null); try std.testing.expect(status_pos < completed.bytes.len); const output_entry_id = runtime.command_output_blocks.items[0].entry_id.?; const output_index = entryIndex(&runtime, output_entry_id).?; const status_index = entryIndex(&runtime, status_entry_id).?; try std.testing.expect(status_index < output_index); } fn entryIndex(runtime: *const TranscriptRuntime, entry_id: u32) ?usize { for (runtime.entries.items, 0..) |entry, index| { if (entry.id() == entry_id) return index; } return null; } test "command output records span callbacks and keep first-byte ownership" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 40, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "hel", true, ); const first_entry_id = runtime.command_output_blocks.items[0].lines.items[0].entry_id.?; const intervening_entry_id = try runtime.appendRawTranscriptEntryClassified( alloc, "intervening\n", .unknown_raw, ); try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "lo\nnext\n", true, ); const block = runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.lines.items.len); try std.testing.expectEqualStrings("hello", block.lines.items[0].text); try std.testing.expectEqualStrings("next", block.lines.items[1].text); const second_entry_id = block.lines.items[1].entry_id.?; try std.testing.expect(entryIndex(&runtime, first_entry_id).? < entryIndex(&runtime, intervening_entry_id).?); try std.testing.expect(entryIndex(&runtime, intervening_entry_id).? < entryIndex(&runtime, second_entry_id).?); } test "split utf8 keeps cross-stream first-byte ownership and pure ansi is pruned" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 40, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stdout, "\xe2\x82", true); const stdout_entry_id = runtime.command_output_blocks.items[0].lines.items[0].entry_id.?; try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stderr, "err\n", true); const stderr_entry_id = runtime.command_output_blocks.items[0].lines.items[1].entry_id.?; try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stdout, "\xac\n", true); try runtime.writeCommandOutputChunk(alloc, &metrics, styles, .stdout, "\x1b[31m", true); try runtime.flushCommandOutputSummary(alloc, &metrics, styles, true); const block = runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.lines.items.len); try std.testing.expectEqualStrings("€", block.lines.items[0].text); try std.testing.expectEqualStrings("err", block.lines.items[1].text); try std.testing.expectEqual(@as(usize, 0), block.lines.items[0].record_ordinal); try std.testing.expectEqual(@as(usize, 1), block.lines.items[1].record_ordinal); try std.testing.expect(entryIndex(&runtime, stdout_entry_id).? < entryIndex(&runtime, stderr_entry_id).?); } test "prepared canonical command mutation stays bounded across twenty four thousand records" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 72, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; for (0..24_000) |_| { try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "record\n", true, ); try std.testing.expect( runtime.retainedStructuredBytesForCommandOutput() <= runtime.max_retained_transcript_bytes, ); } const block = runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 24_000), block.total_lines); try std.testing.expect(block.retention_overflow); try std.testing.expect(block.lines.items.len < block.total_lines); for (block.lines.items, 0..) |line, record_ordinal| { try std.testing.expectEqual(record_ordinal, line.record_ordinal); } } test "oversized open command record stops growing after overflow" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 72, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .max_retained_transcript_bytes = 8 * 1024, }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; var fragment: [4096]u8 = undefined; @memset(&fragment, 'x'); var retained_after_overflow: ?usize = null; var source_entry_id: ?u32 = null; for (0..256) |_| { try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, &fragment, true, ); const block = runtime.command_output_blocks.items[0]; if (source_entry_id == null) source_entry_id = block.lines.items[0].entry_id; try std.testing.expectEqual(source_entry_id, block.lines.items[0].entry_id); try std.testing.expect( runtime.retainedStructuredBytesForCommandOutput() <= runtime.max_retained_transcript_bytes, ); if (block.retention_overflow) { if (retained_after_overflow) |retained| { try std.testing.expectEqual(retained, block.retained_text_bytes); } else { retained_after_overflow = block.retained_text_bytes; } } } try std.testing.expect(retained_after_overflow != null); var failing = std.testing.FailingAllocator.init(alloc, .{ .fail_index = 0 }); try runtime.writeCommandOutputChunk( failing.allocator(), &metrics, styles, .stdout, &fragment, true, ); try std.testing.expectEqual(@as(usize, 0), failing.allocations); } test "descriptor capped bare carriage return replaces one dropped record" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 72, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "kept\n", true, ); const kept_entry_id = runtime.command_output_blocks.items[0].lines.items[0].entry_id.?; runtime.max_retained_transcript_bytes = runtime.retainedStructuredBytesForCommandOutput(); _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "dropped\rreplacement", true, ); var block = &runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.total_lines); try std.testing.expectEqual(@as(usize, 1), block.lines.items.len); try std.testing.expectEqual(kept_entry_id, block.lines.items[0].entry_id.?); try std.testing.expect(block.overflow_open_records[0]); try std.testing.expect(block.overflow_record_visible[0]); _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "\n", true, ); block = &runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.total_lines); try std.testing.expectEqual(@as(usize, 1), block.lines.items.len); try std.testing.expectEqual(kept_entry_id, block.lines.items[0].entry_id.?); try std.testing.expect(!block.overflow_open_records[0]); } test "descriptor capped ansi-only prefix keeps later visible overflow record" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 72, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "kept\n", true, ); runtime.max_retained_transcript_bytes = runtime.retainedStructuredBytesForCommandOutput(); _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "\x1b[31m", true, ); var block = &runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.total_lines); try std.testing.expect(block.overflow_open_records[0]); try std.testing.expect(!block.overflow_record_visible[0]); try std.testing.expect(block.decoders[0].isIdle()); _ = try command_output_runtime.writeCommandOutputChunkUncommitted( &runtime, alloc, &metrics, styles, null, .stdout, "visible", true, ); block = &runtime.command_output_blocks.items[0]; try std.testing.expect(block.overflow_record_visible[0]); runtime.max_retained_transcript_bytes = std.math.maxInt(usize); _ = try command_output_runtime.flushCommandOutputSummaryUncommitted( &runtime, alloc, &metrics, styles, null, true, ); try std.testing.expectEqual( @as(usize, 2), runtime.command_output_blocks.items[0].total_lines, ); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), source.bytes.len); } test "descriptor capped split osc terminator preserves record boundaries" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 72, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; var metrics: Metrics = .{}; try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "kept\n", true, ); runtime.max_retained_transcript_bytes = runtime.retainedStructuredBytesForCommandOutput(); try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "\x1b]0;title", true, ); var block = &runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 2), block.total_lines); try std.testing.expect(block.overflow_open_records[0]); try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "\x07", true, ); try runtime.writeCommandOutputChunk( alloc, &metrics, styles, .stdout, "\nnext\n", true, ); block = &runtime.command_output_blocks.items[0]; try std.testing.expectEqual(@as(usize, 3), block.total_lines); try std.testing.expect(!block.overflow_open_records[0]); } test "command terminal detail resolves its lifecycle output instead of the latest block" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const first = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-first" }; const second = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-second" }; for ([_]types.ToolLifecycleId{ first, second }) |id| { _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = id, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf demo\"}", } }); } var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, first, .stdout, "first\n", true); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, first, true); try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, second, .stdout, "second\n", true); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, second, true); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = first, .outcome = .{ .kind = .completed, .summary = "Ran first" }, } }); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = second, .outcome = .{ .kind = .completed, .summary = "Ran second" }, } }); const first_status = runtime.toolActivityRecord(first).?.entry_id; const second_status = runtime.toolActivityRecord(second).?.entry_id; try std.testing.expectEqual(runtime.command_output_blocks.items[0].entry_id, runtime.toolDetailForEntry(first_status).?.command_output_entry_id); try std.testing.expectEqual(runtime.command_output_blocks.items[1].entry_id, runtime.toolDetailForEntry(second_status).?.command_output_entry_id); } test "command output consolidation keeps compact tool order" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const first = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-first" }; const second = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-second" }; _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = first, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf first\"}", } }); _ = try runtime.applyToolLifecycle(alloc, .{ .authoritative_started = .{ .id = second, .reconciles_provisional_call_id = null, .tool_name = "run_command", .activity_kind = .command, .arguments_json = "{\"command\":\"printf second\"}", } }); var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, first, .stdout, "FIRST_OUTPUT\n", true); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = first, .outcome = .{ .kind = .completed, .summary = "Ran first" }, } }); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, first, true); try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, second, .stdout, "SECOND_OUTPUT\n", true); _ = try runtime.applyToolLifecycle(alloc, .{ .terminal = .{ .id = second, .outcome = .{ .kind = .completed, .summary = "Ran second" }, } }); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, second, true); var compact = try runtime.prepareTranscriptSource(alloc, null); defer compact.deinit(alloc); const first_status = std.mem.indexOf(u8, compact.bytes, "first") orelse return error.MissingFirstStatus; const second_status = std.mem.indexOf(u8, compact.bytes, "second") orelse return error.MissingSecondStatus; try std.testing.expect(first_status < second_status); try std.testing.expect(std.mem.indexOf(u8, compact.bytes, "FIRST_OUTPUT") == null); try std.testing.expect(std.mem.indexOf(u8, compact.bytes, "SECOND_OUTPUT") == null); const first_status_entry = runtime.toolActivityRecord(first).?.entry_id; const second_status_entry = runtime.toolActivityRecord(second).?.entry_id; try std.testing.expectEqual(runtime.command_output_blocks.items[0].entry_id, runtime.toolDetailForEntry(first_status_entry).?.command_output_entry_id); try std.testing.expectEqual(runtime.command_output_blocks.items[1].entry_id, runtime.toolDetailForEntry(second_status_entry).?.command_output_entry_id); } test "command output completion ignores mismatched lifecycle" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, } }; defer runtime.deinit(alloc); const styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }; const first = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-first" }; const second = types.ToolLifecycleId{ .turn_id = 9, .call_id = "command-second" }; var metrics: Metrics = .{}; try runtime.writeCommandOutputChunkForLifecycle(alloc, &metrics, styles, first, .stdout, "first\n", true); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, null, true); try std.testing.expect(runtime.command_output_display.open_command_block != null); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, second, true); try std.testing.expect(runtime.command_output_display.open_command_block != null); try std.testing.expect(runtime.command_output_blocks.items[0].entry_id == null); try runtime.flushCommandOutputSummaryForLifecycle(alloc, &metrics, styles, first, true); try std.testing.expect(runtime.command_output_display.open_command_block == null); try std.testing.expect(runtime.command_output_blocks.items[0].entry_id != null); } test "retintEntriesForTheme rewrites owned presentation without touching external raw bytes" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }, .owned_top_row = 1, }; defer runtime.deinit(alloc); const owned_id = try runtime.appendRawTranscriptEntryClassified( alloc, "\x1b[38;5;252m■\x1b[38;5;255m cancelled\x1b[39m\n", .tool_status, ); const command_id = try runtime.appendRawTranscriptEntryClassified( alloc, "\x1b[38;5;252mcommand\x1b[39m\n", .command_output, ); const unknown_id = try runtime.appendRawTranscriptEntryClassified( alloc, "\x1b[38;5;252munknown\x1b[39m\n", .unknown_raw, ); const assistant_id = try runtime.appendAssistantTurnEntry(alloc); const segments = runtime.lookupAssistantSegments(assistant_id).?; try segments.text.appendSlice(alloc, "\x1b[38;5;245mcode\x1b[39m \x1b[38;5;252m✓\x1b[39m\n"); try runtime.retintEntriesForTheme(alloc, false, true); try std.testing.expectEqual(owned_id, runtime.entries.items[0].id()); try std.testing.expectEqual(command_id, runtime.entries.items[1].id()); try std.testing.expectEqual(unknown_id, runtime.entries.items[2].id()); try std.testing.expectEqual(assistant_id, runtime.entries.items[3].id()); try std.testing.expectEqualStrings( "\x1b[38;5;238m■\x1b[38;5;235m cancelled\x1b[39m\n", runtime.entries.items[0].raw_bytes.bytes, ); try std.testing.expectEqualStrings( "\x1b[38;5;252mcommand\x1b[39m\n", runtime.entries.items[1].raw_bytes.bytes, ); try std.testing.expectEqualStrings( "\x1b[38;5;252munknown\x1b[39m\n", runtime.entries.items[2].raw_bytes.bytes, ); try std.testing.expectEqualStrings( "\x1b[38;5;247mcode\x1b[39m \x1b[38;5;238m✓\x1b[39m\n", runtime.entries.items[3].assistant_turn.segments.text.items, ); } pub fn wrapAssistantText(alloc: Allocator, text: []const u8, cols: u16) ![]u8 { return transcript_blocks.wrapAssistantText(alloc, text, cols); } const default_max_retained_transcript_bytes: usize = 1024 * 1024; const full_transcript_snapshot_clone_max_bytes: usize = 8 * 1024 * 1024; const resume_publication_rows_per_frame: u32 = 64; pub const PaintTraceState = struct { transcript_top: u16 = 0, transcript_bottom: u16 = 0, transcript_start_line: usize = 0, transcript_total_lines: usize = 0, transcript_visible_rows: u16 = 0, transcript_partial_skip_rows: u16 = 0, transcript_entries: usize = 0, transcript_initialized: bool = false, transcript_log_frame: bool = false, footer_top: u16 = 0, footer_input_base: u16 = 0, footer_hint: u16 = 0, footer_activity_row: ?u16 = null, footer_activity_reserved: u16 = 0, footer_activity_kind: u8 = 0, footer_initialized: bool = false, }; const CommandOutputBlock = command_output_runtime.CommandOutputBlock; const FoldedCommandBlock = command_output_runtime.FoldedCommandBlock; const CommandOutputRenderPolicy = command_output_runtime.CommandOutputRenderPolicy; pub const ToolDetailRecord = transcript_blocks.ToolDetailRecord; const PendingToolDetailStart = struct { tool_name: ?[]u8 = null, captured_command: ?bool = null, activity_kind: ?types.ToolActivityKind = null, arguments_json: ?[]u8 = null, lifecycle_id: ?types.ToolLifecycleId = null, replace_lifecycle: bool = false, fn deinit(self: *PendingToolDetailStart, alloc: Allocator) void { const tool_name = self.tool_name; const arguments_json = self.arguments_json; const lifecycle_id = self.lifecycle_id; self.* = .{}; if (tool_name) |value| alloc.free(value); if (arguments_json) |value| alloc.free(value); if (lifecycle_id) |id| alloc.free(@constCast(id.call_id)); } }; const PendingToolDetailTerminal = struct { result: ?[]u8 = null, result_handle: ?[]u8 = null, command_artifact_handle: ?[]u8 = null, command_output_replay: ?types.CommandOutputReplay = null, command_process_presentation: ?types.CommandProcessPresentation = null, command_output_entry_id: ?u32 = null, command_process_entry: ?PendingCommandProcessEntry = null, fn deinit(self: *PendingToolDetailTerminal, alloc: Allocator) void { if (self.result) |value| alloc.free(value); if (self.result_handle) |value| alloc.free(value); if (self.command_artifact_handle) |value| alloc.free(value); if (self.command_output_replay) |replay| { types.freeCommandOutputReplay(alloc, replay); } if (self.command_process_entry) |*entry| entry.deinit(alloc); self.* = .{}; } }; const PendingCommandProcessEntry = struct { entry_id: u32, bytes: ?[]u8 = null, block: ?CommandOutputBlock = null, fn deinit(self: *PendingCommandProcessEntry, alloc: Allocator) void { if (self.bytes) |value| alloc.free(value); if (self.block) |*value| value.deinit(alloc); self.bytes = null; self.block = null; } }; pub fn renderEntriesToBytes( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, ) ![]u8 { return transcript_blocks.renderEntriesToBytes(alloc, entries, cols, styles); } pub const FullTranscriptPrimaryRestore = enum { changed, exact, resized, }; const InstalledFullTranscriptPage = struct { source: full_transcript_worker.InstalledSource, projection: full_transcript_screen.Projection, prepared_window: ?full_transcript_worker.PreparedWindow = null, measured_total_rows: u32 = 0, measured_anchor_row: ?u32 = null, measured_item_rows: []transcript_presentation.ItemRow = &.{}, measurement_snapshot_ready: bool = false, presented_offset: u32 = 0, fn deinit(self: *InstalledFullTranscriptPage) void { if (self.prepared_window) |*window| window.deinit(std.heap.c_allocator); if (self.measured_item_rows.len > 0) { std.heap.c_allocator.free(self.measured_item_rows); } self.projection.deinit(std.heap.c_allocator); self.source.deinit(std.heap.c_allocator); self.* = undefined; } }; const CachedCompactTranscriptSource = struct { source: TranscriptPreparationSource, content_revision: u64, cols: u16, has_committed_frame: bool, }; const CompactTranscriptSourceCache = struct { const capacity = 4; entries: [capacity]?CachedCompactTranscriptSource = .{null} ** capacity, next_replacement: usize = 0, fn deinit(self: *CompactTranscriptSourceCache, alloc: Allocator) void { for (&self.entries) |*entry| { if (entry.*) |*cached| cached.source.deinit(alloc); } self.* = .{}; } fn find( self: *CompactTranscriptSourceCache, content_revision: u64, cols: u16, has_committed_frame: bool, ) ?*TranscriptPreparationSource { for (&self.entries) |*entry| { if (entry.*) |*cached| { if (cached.content_revision == content_revision and cached.cols == cols and cached.has_committed_frame == has_committed_frame) { return &cached.source; } } } return null; } fn insert( self: *CompactTranscriptSourceCache, alloc: Allocator, replacement: CachedCompactTranscriptSource, ) *TranscriptPreparationSource { var index = self.next_replacement; for (self.entries, 0..) |entry, candidate| { if (entry == null) { index = candidate; break; } } else { self.next_replacement = (self.next_replacement + 1) % capacity; } if (self.entries[index]) |*cached| cached.source.deinit(alloc); self.entries[index] = replacement; return &self.entries[index].?.source; } }; test "render diagnostic commit skips unchanged same-row paints" { diagnostics.resetForTest(); defer diagnostics.resetForTest(); const shell: TranscriptRuntime = .{ .layout = .{ .rows = 4, .cols = 20, .content_bottom = 1, .divider_top_row = 2, .input_row = 3, .divider_bottom_row = 4, .hint_row = 4, } }; const before = shell.transcriptCommitDiagnostic(); const scroll_plan = render_engine.frame_scroll_plan.merge(4, 1, 0, 0); var result: render_engine.terminal_diff.FrameCommitResult = .{ .bytes_written = 12, .changed_cells = 1, .full_repaint = false, .committed_cursor_row = 3, .committed_cursor_col = 1, .shadow_state = .committed, .next_invalidation = .{}, }; shell.recordRenderCommit(before, false, false, scroll_plan, result, false); var events: [2]diagnostics.RenderEvent = undefined; try std.testing.expectEqual(@as(usize, 0), diagnostics.snapshotRenderEvents(&events)); result.full_repaint = true; shell.recordRenderCommit(before, false, false, scroll_plan, result, false); try std.testing.expectEqual(@as(usize, 1), diagnostics.snapshotRenderEvents(&events)); try std.testing.expectEqual(.commit, events[0].kind); try std.testing.expect(!events[0].truncated); try std.testing.expect(std.mem.find(u8, events[0].detail(), "repaint=true") != null); } pub const TranscriptRuntime = struct { stdout_file: std.Io.File = std.Io.File.stdout(), test_frame_sink: if (@import("builtin").is_test) ?render_engine.terminal_diff.FrameSink else void = if (@import("builtin").is_test) null else {}, sync_updates_enabled: bool = true, history_reset_uses_ris: bool = false, layout: Layout = undefined, cursor_row: u16 = 1, cursor_col: u16 = 1, /// Top terminal row owned by fx. Rows above this contain pre-fx scrollback /// and must not be touched by fx repaint logic. /// Initialized to the cursor row captured at `initViewport`, and /// decreased as the transcript scrolls until it reaches row 1. viewport_top_row: u16 = 1, /// First row fx is allowed to clear or repaint. This is set after /// launch-time reservation has created fx-owned rows. Rows above it /// belong to pre-fx terminal content and must not be touched by /// resize/layout repair. owned_top_row: u16 = 1, /// Set when a resize leaves no drawable rows below `owned_top_row`. /// The next successful repaint uses natural terminal scroll to /// compact older visible rows into scrollback before drawing the /// latest fx frame. pending_scroll_compact: bool = false, /// Minimum body rows fx should keep available for the visible /// viewport. Startup sets this to the welcome banner height so a /// later resize cannot leave the renderer with a partial banner /// band and stale resize-reflow cells above it. min_visible_viewport_rows: u16 = 0, /// Resize-only guard rows to clear above `viewport_top_row` on the /// next replay. Real terminals can reflow old fx cells just above /// the logical band during a drag; this lets the settled pass erase /// that residue without widening normal streaming paints. reflow_clear_guard_rows: u16 = 0, /// Signed cursor displacement measured across the current settled /// resize. Positive rows entered history; negative rows returned. resize_history_row_delta: ?i32 = null, /// Pre-paint cursor evidence retained from live geometry through the /// matching canonical reset commit. pending_resize_observation: ?ResizeObservation = null, /// Forces the next viewport diff paint to start from a real terminal /// clear. This is reserved for resize/reflow repair; normal streamed /// token paints must diff in place so cleared frames do not enter /// terminal scrollback. viewport_clear_pending: bool = false, /// A settled terminal resize needs a canonical repaint from row one. /// It remains pending until the reset frame commits successfully. terminal_reset_pending: bool = false, /// A committed resize reset established terminal-bottom ownership that /// the next cancellation projection must preserve. resize_bottom_anchor_pending: bool = false, render_requests: render_request.RenderRequestState = .{}, /// Set after the frame commit path has successfully written and fed /// a frame. Resize-time invalid terminal dimensions before this point /// still use the startup error path. has_committed_frame: bool = false, committed_frame_layout: render_engine.frame_layout.CommittedLayoutSnapshot = .{}, transcript_commit_state: TranscriptCommitState = .invalid, publication_projection_invalid: bool = false, /// True while the terminal reports zero, unreadable, or too-small /// dimensions after the first committed frame. Normal painters stay /// idle until a valid resize clears this flag. terminal_dimensions_invalid: bool = false, transcript: std.ArrayList(u8) = .empty, /// Complete historical flow and its immutable line index, retained until /// the first successful terminal publication. pending_resume_source: ?TranscriptPreparationSource = null, folded_command_blocks: std.ArrayList(FoldedCommandBlock) = .empty, command_output_blocks: std.ArrayList(CommandOutputBlock) = .empty, /// Viewer state used only while Ctrl-O owns the alternate screen. /// The compact transcript keeps its independent viewport selection. full_transcript: transcript_presentation.State = .{}, full_transcript_open_content_revision: ?u64 = null, full_transcript_open_cols: u16 = 0, full_transcript_open_rows: u16 = 0, full_transcript_page_load: full_transcript_worker.Load = .{}, full_transcript_window_load: full_transcript_worker.WindowLoad = .{}, full_transcript_page_anchor: full_transcript_page.Anchor = .tail, full_transcript_installed_page: ?InstalledFullTranscriptPage = null, full_transcript_installed_page_retired: bool = false, full_transcript_failed_request: ?full_transcript_page.Request = null, full_transcript_failure_pending: bool = false, full_transcript_open_request: ?full_transcript_page.Request = null, full_transcript_restore_open_pending: bool = false, full_transcript_prepared_page_visible: bool = false, full_transcript_content_revision: u64 = 0, compact_transcript_source_cache: CompactTranscriptSourceCache = .{}, /// When enabled, compact transcript tool groups render only their summary /// header while the full transcript retains every individual tool call. collapse_tool_calls: bool = false, /// Structured-entry store used to regenerate transcript bytes at the /// current width while retaining the raw byte buffer for append paths /// that still write pre-rendered transcript content. entries: std.ArrayList(TranscriptEntry) = .empty, /// Borrowed only by a detached store transaction; never installed on the live runtime. retention_prefix_trims: ?*std.ArrayList(transcript_store.RetentionPrefixTrim) = null, lifecycle_state: activity_runtime.ToolActivityState = .{}, worker_status: worker_status.State = .none, /// Core-owned producer state and policy for native-history release. transcript_release: transcript_release.State = .{}, /// Sorted by entry id so exact lookup stays bounded as history grows. tool_details: std.ArrayList(ToolDetailRecord) = .empty, /// Full-detail records shown only in the ctrl+o full transcript. Sibling /// of `tool_details`: parallel to the entry store, never inline, never /// persisted, cleared with the transcript. full_detail_records: std.ArrayList(FullDetailRecord) = .empty, /// Monotonically increasing id stamped onto each new entry by the /// `append*Entry` helpers. Starts at 1 so 0 can be reserved as a /// sentinel. Never reset on `clearTranscript` — even after a wipe, /// subsequent ids remain unique within the session, which keeps /// debug traces unambiguous across transcript resets. next_entry_id: u32 = 1, /// Cols at which the transcript byte buffer was last regenerated from /// entries. A cache-origin proof is valid only at this width. /// Zero before the first prepared transcript frame. last_rendered_cols: u16 = 0, /// True only when the visible cache was rebuilt from an untrimmed, /// canonical entry source at `last_rendered_cols`. transcript_cache_origin_untrimmed: bool = false, /// Visible byte-cache cap. This bounds `transcript.items`, which is /// the rendered tail used by viewport paint. Structured entries and /// folded command output are bounded separately by /// `max_retained_transcript_bytes` below. max_transcript_bytes: usize = 256 * 1024, /// Structured retention cap for raw/user/assistant entries plus /// folded command output lines. This model is the source of truth /// for reconstructive paint, resize, and diff restyling, so it gets /// its own conservative budget instead of borrowing the smaller /// visible byte-cache limit. max_retained_transcript_bytes: usize = default_max_retained_transcript_bytes, painting: bool = false, /// Renderer-local cache state set by transcript mutations. The same /// mutation must request `.transcript`; this flag never authorizes a /// frame on its own. transcript_band_dirty: bool = false, /// False until the first non-empty viewport paint has established /// fx's transcript band. Before that point transcript writes are /// model updates only: emitting scroll newlines would move pre-fx /// shell rows before the renderer has had a chance to respect /// `viewport_top_row`. has_painted_transcript: bool = false, footer_viewport: footer_viewport_runtime.FooterViewport = .{}, extra_input_rows: u16 = 0, footer_reserved_base_rows: u16 = 3, command_output_display: CommandOutputDisplayState = .{}, command_output_render: CommandOutputRenderPolicy = .{}, shimmer_active: bool = false, shimmer_is_overlay: bool = false, paint_trace: PaintTraceState = .{}, last_visible_transcript_top_row: u16 = 1, last_visible_transcript_start_line: usize = 0, last_visible_transcript_partial_skip_rows: u16 = 0, last_visible_transcript_split_active: bool = false, last_visible_transcript_split_prefix_lines: usize = 0, last_visible_transcript_split_suffix_start_line: usize = 0, last_visible_transcript_last_row: u16 = 0, last_visible_transcript_last_row_blank: bool = false, last_visible_transcript_tail_kind: ?TranscriptBlockKind = null, last_viewport_selection: ?ViewportSelection = null, /// Detached alternate-screen views may request the ordinary compact /// transcript at a stable distance from its tail. Inline chat leaves this /// unset and keeps the existing viewport policy. tail_viewport_request: ?viewport_selection.TailViewportPolicy = null, tail_viewport_resolution: ?viewport_selection.TailOffsetResolution = null, last_paint_bottom_reserved_rows: u16 = 0, replaceable_last_line: bool = false, replaceable_row: u16 = 1, replaceable_start: usize = 0, /// In-process terminal shadow used as the previous state for cell diffs. /// Bootstrap enables it unconditionally; FX_TRACE dumps it at synchronized /// update boundaries. shadow_vt: ?*vt_emulator.Grid = null, shadow_vt_alloc: ?Allocator = null, /// Detached presentations share a physical terminal shadow with their /// owning surface, but retain independent transcript commit state. /// This allocator releases that commit state without claiming ownership /// of the shared shadow grid. detached_commit_alloc: ?Allocator = null, ui_observer: render_engine.ui_observer.UiObserver = .{}, pub noinline fn init() TranscriptRuntime { var result: TranscriptRuntime = .{ .compact_transcript_source_cache = undefined, }; for (&result.compact_transcript_source_cache.entries) |*entry| { entry.* = null; } result.compact_transcript_source_cache.next_replacement = 0; return result; } pub fn enableShadowVt(self: *TranscriptRuntime, alloc: Allocator) !void { return transcript_io.enableShadowVt(self, alloc); } pub fn disableShadowVt(self: *TranscriptRuntime) void { return transcript_io.disableShadowVt(self); } pub fn bindDetachedCommitAllocator( self: *TranscriptRuntime, alloc: Allocator, ) void { std.debug.assert(self.shadow_vt == null); self.detached_commit_alloc = alloc; } pub fn deinit(self: *TranscriptRuntime, alloc: Allocator) void { self.disableShadowVt(); self.full_transcript_window_load.deinit(); self.full_transcript_page_load.deinit(); self.discardInstalledFullTranscriptPage(); self.compact_transcript_source_cache.deinit(alloc); self.lifecycle_state.deinit(alloc); for (self.tool_details.items) |*detail| detail.deinit(alloc); self.tool_details.deinit(alloc); self.clearFullDetailRecords(alloc); self.full_detail_records.deinit(alloc); self.transcript.deinit(alloc); _ = self.releasePendingResumeSource(alloc); for (self.folded_command_blocks.items) |*block| block.deinit(alloc); self.folded_command_blocks.deinit(alloc); for (self.command_output_blocks.items) |*block| block.deinit(alloc); self.command_output_blocks.deinit(alloc); for (self.entries.items) |*entry| entry.deinit(alloc); self.entries.deinit(alloc); self.transcript_commit_state.deinit(alloc); self.footer_viewport.deinit(alloc); self.ui_observer.deinit(alloc); } fn releasePendingResumeSource(self: *TranscriptRuntime, alloc: Allocator) usize { var source = self.pending_resume_source orelse return 0; const released_bytes = source.bytes.len; source.deinit(alloc); self.pending_resume_source = null; return released_bytes; } pub fn pendingResumeFlow(self: *const TranscriptRuntime) []const u8 { return if (self.pending_resume_source) |*source| source.bytes else &.{}; } pub fn committedRowProvenance( self: *const TranscriptRuntime, ) []const transcript_blocks.RowProvenance { return switch (self.transcript_commit_state) { .invalid => &.{}, .stable => |anchor| anchor.row_provenance, .recovering => |receipt| receipt.row_provenance, }; } pub fn beginPaint(self: *TranscriptRuntime) usize { std.debug.assert(!self.painting); self.painting = true; return self.transcript.items.len; } pub fn endPaint(self: *TranscriptRuntime, entry_len: usize) void { self.painting = false; const exit_len = self.transcript.items.len; if (exit_len != entry_len) { debug_trace.logf("paint", "transcript mutated during paint: entry_len={d} exit_len={d}", .{ entry_len, exit_len }); } } pub fn beginPaintSilent(self: *TranscriptRuntime) void { std.debug.assert(!self.painting); self.painting = true; } pub fn endPaintSilent(self: *TranscriptRuntime) void { self.painting = false; } pub fn assertCanMutateTranscript(self: *TranscriptRuntime) !void { if (@import("builtin").is_test and self.painting) return error.PaintGuardViolated; std.debug.assert(!self.painting); } pub fn initBacking(self: *TranscriptRuntime, alloc: Allocator) !void { return transcript_store.initBacking(self, alloc); } pub fn ensurePaintReservation(self: *TranscriptRuntime, alloc: Allocator) !void { return transcript_store.ensurePaintReservation(self, alloc); } pub fn rebuildTranscriptFromRendered(self: *TranscriptRuntime, alloc: Allocator, bytes: []const u8, label: []const u8) !void { return transcript_store.rebuildTranscriptFromRendered(self, alloc, bytes, label); } pub fn replaceableEntryId(self: *const TranscriptRuntime) ?u32 { return transcript_store.replaceableEntryId(self); } pub fn refreshFoldedCommandSummaryIndices(self: *TranscriptRuntime, alloc: Allocator) !void { return transcript_store.refreshFoldedCommandSummaryIndices(self, alloc); } pub fn rebuildTranscriptCacheFromEntries(self: *TranscriptRuntime, alloc: Allocator, label: []const u8) !void { return transcript_store.rebuildTranscriptCacheFromEntries(self, alloc, label); } pub fn rebuildTranscriptCacheAfterStructuredRewrite(self: *TranscriptRuntime, alloc: Allocator, label: []const u8) !void { return transcript_store.rebuildTranscriptCacheAfterStructuredRewrite(self, alloc, label); } pub fn reconcileDetachedInstallSource( self: *TranscriptRuntime, source: []const u8, ) void { transcript_store.reconcileDetachedInstallSource(self, source); } pub fn enforceStructuredRetention(self: *TranscriptRuntime, alloc: Allocator, protected_id: ?u32) !void { return transcript_store.enforceStructuredRetention(self, alloc, protected_id); } pub fn enforceStructuredRetentionAndReport( self: *TranscriptRuntime, alloc: Allocator, protected_id: ?u32, ) !bool { return transcript_store.enforceStructuredRetentionAndReport(self, alloc, protected_id); } pub fn lifecyclePinCount(self: *const TranscriptRuntime) usize { return transcript_store.lifecyclePinCount(self); } pub fn isLifecyclePinned(self: *const TranscriptRuntime, entry_id: u32) bool { return transcript_store.isLifecyclePinned(self, entry_id); } pub fn applyToolLifecycle( self: *TranscriptRuntime, alloc: Allocator, event: types.ToolLifecycleEvent, ) !?types.ToolActivityKind { return self.applyToolLifecycleWithAnchorPolicy(alloc, event, false); } pub fn applyToolLifecyclePreservingNormalBufferAnchor( self: *TranscriptRuntime, alloc: Allocator, event: types.ToolLifecycleEvent, ) !?types.ToolActivityKind { return self.applyToolLifecycleWithAnchorPolicy(alloc, event, true); } fn applyToolLifecycleWithAnchorPolicy( self: *TranscriptRuntime, alloc: Allocator, event: types.ToolLifecycleEvent, preserve_normal_buffer_anchor: bool, ) !?types.ToolActivityKind { const result = switch (event) { .provisional => |provisional| self.applyProvisionalLifecycle( alloc, provisional.id, provisional.presentation_group_id, provisional.tool_name, provisional.activity_kind, ), .authoritative_started => |started| self.applyAuthoritativeLifecycle( alloc, started.id, started.presentation_group_id, started.reconciles_provisional_call_id, started.tool_name, started.activity_kind, started.arguments_json, started.place_after_current_transcript, ), .progress => |progress| self.applyProgressLifecycle( alloc, progress.id, progress.text, ), .terminal => |terminal| self.applyTerminalLifecycle( alloc, terminal.id, terminal.outcome, terminal.result, terminal.result_memory, terminal.command_artifact_handle, ), .turn_finished => |finished| self.applyTurnFinishedLifecycle( alloc, finished, preserve_normal_buffer_anchor, ), } catch |err| { traceLifecycleFailure( @tagName(event), lifecycleEventTurnId(event), err, ); return err; }; debug_trace.eventf( "ui_activity", "lifecycle_reduced", .{}, "kind={s} turn_id={d} records={d}", .{ @tagName(event), lifecycleEventTurnId(event), self.lifecycle_state.recordCount(), }, ); return result; } pub fn finishLifecycleBatch( self: *TranscriptRuntime, alloc: Allocator, ) !void { const watermark = self.lifecycle_state.batch_finalized_turn_watermark orelse return; const entry_ids = try self.lifecycle_state.cleanupEntryIds(alloc, watermark); defer alloc.free(entry_ids); transcript_store.clearLifecyclePinsAtomic(self, alloc, entry_ids) catch |err| { traceLifecycleFailure("batch_cleanup", watermark, err); return err; }; self.lifecycle_state.commitCleanup(alloc, watermark); } pub fn activityProjection( self: *const TranscriptRuntime, ) activity_runtime.ActivityProjection { if (self.worker_status.projection()) |projection| return projection; const record = self.lifecycle_state.focusedRecord() orelse { return .none; }; const label = transcript_store.toolStatusEntryLabel(self, record.entry_id) orelse { debug_trace.logf( "ui_activity", "focused lifecycle missing transcript entry turn_id={d} entry_id={d}", .{ record.id.turn_id, record.entry_id }, ); return .none; }; return .{ .tool_slot = .{ .entry_id = record.entry_id, .fallback_label = label, .active = record.phase != .terminal, .kind = record.activity_kind, } }; } pub fn worker_status_state(self: *TranscriptRuntime) *worker_status.State { return &self.worker_status; } pub fn focusedToolEntryId(self: *const TranscriptRuntime) ?u32 { const record = self.lifecycle_state.focusedRecord() orelse return null; return record.entry_id; } pub fn focusedToolActivityKind( self: *const TranscriptRuntime, ) ?types.ToolActivityKind { const record = self.lifecycle_state.focusedRecord() orelse return null; return record.activity_kind; } pub fn toolActivityRecord( self: *const TranscriptRuntime, id: types.ToolLifecycleId, ) ?activity_runtime.ToolPresentationRecord { return self.lifecycle_state.record(id); } pub fn attachCommandOutputEntryForLifecycle( self: *TranscriptRuntime, lifecycle_id: ?types.ToolLifecycleId, command_output_entry_id: u32, ) void { const id = lifecycle_id orelse return; for (self.tool_details.items) |*detail| { if (!command_output_runtime.sameLifecycleId(detail.lifecycle_id, id)) continue; detail.command_output_entry_id = command_output_entry_id; return; } } pub fn toolActivityRecordCount(self: *const TranscriptRuntime) usize { return self.lifecycle_state.recordCount(); } pub fn activeToolActivityCount(self: *const TranscriptRuntime) usize { return self.lifecycle_state.activeCount(); } /// Publishes the terminal cancellation presentation immediately while the /// worker retains lifecycle ownership until each tool actually settles. pub fn presentActiveToolCancellation( self: *TranscriptRuntime, alloc: Allocator, ) !bool { const focused = self.lifecycle_state.focusedRecord() orelse return false; const active = try self.lifecycle_state.collectFallbackRecords( alloc, focused.id.turn_id, ); defer alloc.free(active); if (active.len == 0) return false; const line = try lifecycleTerminalLine(alloc, .cancelled, "Cancelled"); defer alloc.free(line); const updates = try alloc.alloc(transcript_store.LifecycleEntryUpdate, active.len); defer alloc.free(updates); var detail_starts: std.ArrayList(PendingToolDetailStart) = .empty; defer { for (detail_starts.items) |*detail_start| detail_start.deinit(alloc); detail_starts.deinit(alloc); } try detail_starts.ensureTotalCapacity(alloc, active.len); var missing_detail_count: usize = 0; for (active, 0..) |entry, index| { if (self.toolDetailPtr(entry.record.entry_id) == null) { missing_detail_count += 1; } detail_starts.appendAssumeCapacity(try self.prepareToolDetailStart( alloc, entry.record.entry_id, entry.record.id, entry.record.tool_name orelse "tool", entry.record.activity_kind, null, )); updates[index] = .{ .entry_id = entry.record.entry_id, .bytes = line, }; } try transcript_store.replacePinnedToolStatusesAtomic( self, alloc, updates, missing_detail_count, null, ); self.lifecycle_state.markTurnCancellationPresented(focused.id.turn_id); for (active, detail_starts.items) |entry, *detail_start| { entry.record.cancellation_presentation = .tool_status; self.commitToolDetailStart( alloc, entry.record.entry_id, detail_start, ); const detail = self.toolDetailPtr(entry.record.entry_id).?; detail.outcome = .cancelled; detail.fallback_disposition = null; } return true; } pub fn finalizedToolTurnWatermark(self: *const TranscriptRuntime) u64 { return self.lifecycle_state.finalized_turn_watermark; } pub fn clearTranscript(self: *TranscriptRuntime, alloc: Allocator) void { const pending_resume_bytes = self.releasePendingResumeSource(alloc); if (pending_resume_bytes > 0) { debug_trace.logf( "session", "dropping pending resume flow during transcript clear bytes={d}", .{pending_resume_bytes}, ); } self.lifecycle_state.deinit(alloc); self.worker_status.reset(); self.clearToolDetails(alloc); self.clearFullDetailRecords(alloc); return transcript_store.clearTranscript(self, alloc); } /// Prepends the live full-detail records (session assembly, network, /// recovery) to the first page of a full-transcript source as synthetic /// notice entries. Records never enter the store, so paging, retention, /// and resume keep their existing contracts. fn prependFullDetailRecords( self: *const TranscriptRuntime, alloc: Allocator, source: *full_transcript_worker.Source, ) !void { if (self.full_detail_records.items.len == 0) return; if (source.range.start != 0) return; const insert_at: usize = if (source.entries.items.len > 0 and source.entries.items[0] == .raw_bytes and source.entries.items[0].raw_bytes.class == .welcome) 1 else 0; for (self.full_detail_records.items, 0..) |record, index| { const topic = try alloc.dupe(u8, record.notice.topic); errdefer alloc.free(topic); const body = try alloc.dupe(u8, record.notice.body); errdefer alloc.free(body); try source.entries.insert(alloc, insert_at + index, .{ .semantic_notice = .{ .id = full_detail_record_entry_id, .created_at_ms = record.created_at_ms, .topic = topic, .tone = record.notice.tone, .body = body, .visibility = .full_only, } }); } } pub fn snapshotVisibleTranscriptText( self: *const TranscriptRuntime, alloc: Allocator, max_bytes: usize, ) !?VisibleTranscriptSnapshot { const grid = self.shadow_vt orelse return null; if (!self.has_painted_transcript or self.last_visible_transcript_top_row == 0 or self.last_visible_transcript_last_row < self.last_visible_transcript_top_row) { return null; } const footer_gap_rows = if (!self.committed_frame_layout.footer_area.isEmpty() and self.committed_frame_layout.footer_area.top > self.last_visible_transcript_last_row) self.committed_frame_layout.footer_area.top - self.last_visible_transcript_last_row - 1 else 0; if (footer_gap_rows >= max_bytes) return null; const text_byte_limit = max_bytes - footer_gap_rows; var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); var start_row = self.last_visible_transcript_top_row; const leading_welcome_visible = if (self.entries.items.len > 0 and self.entries.items[0] == .raw_bytes and self.entries.items[0].raw_bytes.class == .welcome and self.last_viewport_selection != null) self.last_viewport_selection.?.start_line == 0 or (self.last_viewport_selection.?.split_active and self.last_viewport_selection.?.split_prefix_lines > 0) else false; if (leading_welcome_visible) { var saw_welcome_text = false; while (start_row <= self.last_visible_transcript_last_row) : (start_row += 1) { var line: std.ArrayList(u8) = .empty; defer line.deinit(grid.alloc); try grid.rowTextTrimmed(start_row, &line); if (line.items.len > 0) { saw_welcome_text = true; } else if (saw_welcome_text) { start_row += 1; break; } } if (!saw_welcome_text or start_row > self.last_visible_transcript_last_row) return null; } var selected_start: ?u16 = null; var selected_bytes: usize = 0; var row = self.last_visible_transcript_last_row; while (true) { var line: std.Io.Writer.Allocating = .init(grid.alloc); defer line.deinit(); try grid.rowStyledTextTrimmed(row, &line.writer); if (line.written().len >= text_byte_limit - selected_bytes) break; selected_bytes += line.written().len + 1; selected_start = row; if (row == start_row) break; row -= 1; } row = selected_start orelse return null; var has_content = false; while (row <= self.last_visible_transcript_last_row) : (row += 1) { var line: std.Io.Writer.Allocating = .init(grid.alloc); defer line.deinit(); try grid.rowStyledTextTrimmed(row, &line.writer); has_content = has_content or line.written().len > 0; try out.appendSlice(alloc, line.written()); try out.append(alloc, '\n'); } if (!has_content) { out.deinit(alloc); return null; } try out.appendNTimes(alloc, '\n', footer_gap_rows); return .{ .text = try out.toOwnedSlice(alloc), .terminal_rows = grid.rows, .terminal_cols = grid.cols, .complete = selected_start.? == start_row, }; } pub fn resetVisualEpoch(self: *TranscriptRuntime, alloc: Allocator, welcome: []const u8) !void { return transcript_store.resetVisualEpoch(self, alloc, welcome); } fn replacePinnedLifecycleStatus( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, line: []const u8, place_after_current_transcript: bool, ) !bool { return if (place_after_current_transcript) transcript_store.replacePinnedToolStatusAtEndAtomic( self, alloc, entry_id, line, ) else transcript_store.replacePinnedToolStatusAtomic( self, alloc, entry_id, line, ); } fn applyProvisionalLifecycle( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, presentation_group_id: ?types.ToolPresentationGroupId, tool_name: ?[]const u8, activity_kind: types.ToolActivityKind, ) !?types.ToolActivityKind { if (!admitLifecycleStart(&self.lifecycle_state, id)) return null; if (self.lifecycle_state.recordPtr(id)) |record| { if (record.phase == .terminal) { traceLateLifecycleEvent(id, "provisional"); return null; } const name = tool_name orelse return null; var detail_start = try self.prepareToolDetailStart( alloc, record.entry_id, id, name, activity_kind, null, ); defer detail_start.deinit(alloc); const owned_name = try alloc.dupe(u8, name); errdefer alloc.free(owned_name); const line = try lifecycleStartLine(alloc, name); defer alloc.free(line); if (!try self.replacePinnedLifecycleStatus( alloc, record.entry_id, line, false, )) return error.MissingLifecycleTranscriptEntry; activity_runtime.replaceToolName(alloc, record, owned_name); record.activity_kind = activity_kind; self.commitLifecycleToolDetailStart( alloc, id, record.entry_id, &detail_start, ); self.commitToolPresentationGroup(record.entry_id, id, presentation_group_id); return null; } var detail_start = try self.prepareToolDetailStart( alloc, null, id, tool_name orelse "tool", activity_kind, null, ); defer detail_start.deinit(alloc); const entry_id = try self.createToolActivity( alloc, id, tool_name, activity_kind, .provisional, ); self.commitLifecycleToolDetailStart(alloc, id, entry_id, &detail_start); self.commitToolPresentationGroup(entry_id, id, presentation_group_id); return null; } fn applyAuthoritativeLifecycle( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, presentation_group_id: ?types.ToolPresentationGroupId, reconciliation_alias: ?[]const u8, tool_name: []const u8, activity_kind: types.ToolActivityKind, arguments_json: ?[]const u8, place_after_current_transcript: bool, ) !?types.ToolActivityKind { if (!admitLifecycleStart(&self.lifecycle_state, id)) return null; const alias_id: ?types.ToolLifecycleId = if (reconciliation_alias) |call_id| if (call_id.len > 0) .{ .turn_id = id.turn_id, .call_id = call_id } else null else null; const alias_is_distinct = if (alias_id) |alias| alias.turn_id != id.turn_id or !std.mem.eql(u8, alias.call_id, id.call_id) else false; if (alias_is_distinct) { const alias = alias_id.?; if (self.lifecycle_state.record(alias)) |alias_record| { if (self.lifecycle_state.record(id) != null) { debug_trace.logf( "ui_activity", "lifecycle identity collision turn_id={d} kind=authoritative_started", .{id.turn_id}, ); return error.LifecycleReconciliationCollision; } if (alias_record.phase == .terminal) { traceLateLifecycleEvent(alias, "authoritative_started"); return null; } var detail_start = try self.prepareToolDetailStart( alloc, alias_record.entry_id, id, tool_name, activity_kind, arguments_json, ); defer detail_start.deinit(alloc); try self.lifecycle_state.ensureRecordCapacity(alloc); const owned_call_id = try alloc.dupe(u8, id.call_id); errdefer alloc.free(owned_call_id); const owned_tool_name = try alloc.dupe(u8, tool_name); errdefer alloc.free(owned_tool_name); const line = try lifecycleStartLine(alloc, tool_name); defer alloc.free(line); if (!try self.replacePinnedLifecycleStatus( alloc, alias_record.entry_id, line, place_after_current_transcript, )) return error.MissingLifecycleTranscriptEntry; self.lifecycle_state.rekeyOwned( alloc, alias, .{ .turn_id = id.turn_id, .call_id = owned_call_id }, owned_tool_name, activity_kind, ); self.commitLifecycleToolDetailStart( alloc, id, alias_record.entry_id, &detail_start, ); self.commitToolPresentationGroup(alias_record.entry_id, id, presentation_group_id); return activity_kind; } } if (self.lifecycle_state.recordPtr(id)) |record| { if (record.phase == .terminal) { traceLateLifecycleEvent(id, "authoritative_started"); return null; } var detail_start = try self.prepareToolDetailStart( alloc, record.entry_id, id, tool_name, activity_kind, arguments_json, ); defer detail_start.deinit(alloc); const owned_tool_name = try alloc.dupe(u8, tool_name); errdefer alloc.free(owned_tool_name); const line = try lifecycleStartLine(alloc, tool_name); defer alloc.free(line); if (!try self.replacePinnedLifecycleStatus( alloc, record.entry_id, line, place_after_current_transcript, )) return error.MissingLifecycleTranscriptEntry; const first_authoritative = self.lifecycle_state.promoteOwned( alloc, record, owned_tool_name, activity_kind, ); self.commitLifecycleToolDetailStart( alloc, id, record.entry_id, &detail_start, ); self.commitToolPresentationGroup(record.entry_id, id, presentation_group_id); return if (first_authoritative) activity_kind else null; } var detail_start = try self.prepareToolDetailStart( alloc, null, id, tool_name, activity_kind, arguments_json, ); defer detail_start.deinit(alloc); const entry_id = try self.createToolActivity( alloc, id, tool_name, activity_kind, .authoritative, ); self.commitLifecycleToolDetailStart(alloc, id, entry_id, &detail_start); self.commitToolPresentationGroup(entry_id, id, presentation_group_id); return activity_kind; } fn applyProgressLifecycle( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, text: []const u8, ) !?types.ToolActivityKind { const record = self.lifecycle_state.recordPtr(id) orelse { if (self.lifecycle_state.isFenced(id.turn_id)) { traceLateLifecycleEvent(id, "progress"); return null; } debug_trace.logf( "ui_activity", "progress without lifecycle turn_id={d}", .{id.turn_id}, ); return error.UnknownToolLifecycleIdentity; }; if (record.phase == .terminal) { traceLateLifecycleEvent(id, "progress"); return null; } const line = try lifecycleLine(alloc, text); defer alloc.free(line); if (!try self.replacePinnedLifecycleStatus( alloc, record.entry_id, line, false, )) return error.MissingLifecycleTranscriptEntry; return null; } fn applyTerminalLifecycle( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, outcome: types.ToolOutcome, result: ?[]const u8, result_memory: ?types.ToolResultMemory, command_artifact_handle: ?[]const u8, ) !?types.ToolActivityKind { const record = self.lifecycle_state.recordPtr(id) orelse { if (self.lifecycle_state.isFenced(id.turn_id)) { traceLateLifecycleEvent(id, "terminal"); return null; } debug_trace.logf( "ui_activity", "terminal without lifecycle turn_id={d}", .{id.turn_id}, ); return error.UnknownToolLifecycleIdentity; }; const line = try lifecycleTerminalLine(alloc, outcome.kind, outcome.summary); defer alloc.free(line); const append_turn_cancellation = outcome.kind != .cancelled and self.lifecycle_state.needsTurnCancellationNoticeAfterTerminal(id); const turn_cancellation_line = if (append_turn_cancellation) try lifecycleTerminalLine(alloc, .cancelled, "Cancelled") else null; defer if (turn_cancellation_line) |value| alloc.free(value); var detail_start: ?PendingToolDetailStart = if (self.toolDetailPtr(record.entry_id) == null) try self.prepareToolDetailStart( alloc, record.entry_id, id, record.tool_name orelse "tool", record.activity_kind, null, ) else null; defer if (detail_start) |*pending| pending.deinit(alloc); var detail_update = try self.prepareToolDetailTerminal( alloc, id, record.activity_kind, result, result_memory, command_artifact_handle, null, @as(u32, @intFromBool(append_turn_cancellation)), ); defer detail_update.deinit(alloc); if (!try transcript_store.replacePinnedToolStatusForTerminalAtomic( self, alloc, record.entry_id, line, @intFromBool(detail_start != null), @intFromBool(detail_update.command_process_entry != null), turn_cancellation_line, )) return error.MissingLifecycleTranscriptEntry; record.phase = .terminal; record.cancellation_presentation = if (outcome.kind == .cancelled) .tool_status else if (record.cancellation_presentation != .none) .replaced else .none; if (append_turn_cancellation) { self.lifecycle_state.markTurnCancellationNoticePresented(id.turn_id); } if (detail_start) |*pending| { self.commitLifecycleToolDetailStart( alloc, id, record.entry_id, pending, ); } self.commitToolDetailTerminal( alloc, record.entry_id, record.activity_kind, outcome.kind, &detail_update, ); return null; } fn applyTurnFinishedLifecycle( self: *TranscriptRuntime, alloc: Allocator, finished: types.TurnFinished, preserve_normal_buffer_anchor: bool, ) !?types.ToolActivityKind { if (finished.turn_id == 0) return error.InvalidTurnId; if (!self.lifecycle_state.shouldApplyTurnFinished(finished.turn_id)) return null; const fallbacks = try self.lifecycle_state.collectFallbackRecords( alloc, finished.turn_id, ); defer alloc.free(fallbacks); const updates = try alloc.alloc( transcript_store.LifecycleEntryUpdate, fallbacks.len, ); defer alloc.free(updates); var lines: std.ArrayList([]u8) = .empty; defer { for (lines.items) |line| alloc.free(line); lines.deinit(alloc); } var detail_starts: std.ArrayList(PendingToolDetailStart) = .empty; defer { for (detail_starts.items) |*detail_start| detail_start.deinit(alloc); detail_starts.deinit(alloc); } var missing_detail_count: usize = 0; try lines.ensureTotalCapacity(alloc, fallbacks.len); try detail_starts.ensureTotalCapacity(alloc, fallbacks.len); for (fallbacks, 0..) |fallback, index| { if (self.toolDetailPtr(fallback.record.entry_id) == null) { missing_detail_count += 1; } detail_starts.appendAssumeCapacity(try self.prepareToolDetailStart( alloc, fallback.record.entry_id, fallback.record.id, fallback.record.tool_name orelse "tool", fallback.record.activity_kind, null, )); const line = try lifecycleFallbackLine( alloc, finished.outcome, fallback.was_provisional, ); lines.append(alloc, line) catch |err| { alloc.free(line); return err; }; updates[index] = .{ .entry_id = fallback.record.entry_id, .bytes = line, }; } const fallback_outcome: ?types.ToolOutcomeKind = switch (finished.outcome) { .completed => .completed, .interrupted => .cancelled, .failed => .failed, .paused => null, }; const append_turn_cancellation = finished.outcome == .interrupted and self.lifecycle_state.needsTurnCancellationNoticeAfterTurnFinished( finished.turn_id, ); const turn_cancellation_line = if (append_turn_cancellation) try lifecycleTerminalLine(alloc, .cancelled, "Cancelled") else null; defer if (turn_cancellation_line) |value| alloc.free(value); const replace_result = if (preserve_normal_buffer_anchor) transcript_store.replacePinnedToolStatusesPreservingNormalBufferAnchorAtomic( self, alloc, updates, missing_detail_count, turn_cancellation_line, ) else transcript_store.replacePinnedToolStatusesAtomic( self, alloc, updates, missing_detail_count, turn_cancellation_line, ); replace_result catch |err| { traceLifecycleFailure("turn_finished", finished.turn_id, err); return err; }; for (fallbacks, detail_starts.items) |fallback, *detail_start| { self.commitToolDetailStart(alloc, fallback.record.entry_id, detail_start); const detail = self.toolDetailPtr(fallback.record.entry_id).?; detail.outcome = fallback_outcome; fallback.record.cancellation_presentation = if (fallback_outcome == .cancelled) .tool_status else .none; detail.fallback_disposition = lifecycleFallbackDisposition( finished.outcome, fallback.was_provisional, ); } if (finished.outcome == .interrupted) { self.lifecycle_state.markTurnInterrupted(finished.turn_id); if (append_turn_cancellation) { self.lifecycle_state.markTurnCancellationNoticePresented(finished.turn_id); } } if (finished.outcome == .paused) { const entry_ids = try self.lifecycle_state.pausedTurnEntryIds( alloc, finished.turn_id, ); defer alloc.free(entry_ids); transcript_store.clearLifecyclePinsAtomic(self, alloc, entry_ids) catch |err| { traceLifecycleFailure("turn_paused", finished.turn_id, err); return err; }; self.lifecycle_state.releasePausedTurn(alloc, finished.turn_id); } else { self.lifecycle_state.commitTurnFinished(finished.turn_id, fallbacks); } return null; } fn createToolActivity( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, tool_name: ?[]const u8, activity_kind: types.ToolActivityKind, phase: activity_runtime.ToolLifecyclePhase, ) !u32 { try self.lifecycle_state.ensureRecordCapacity(alloc); const owned_call_id = try alloc.dupe(u8, id.call_id); errdefer alloc.free(owned_call_id); const owned_tool_name = if (tool_name) |name| try alloc.dupe(u8, name) else null; errdefer if (owned_tool_name) |name| alloc.free(name); const line = try lifecycleStartLine(alloc, tool_name); defer alloc.free(line); const entry_id = try transcript_store.appendPinnedToolStatusAtomic( self, alloc, line, ); self.lifecycle_state.putOwnedRecord( .{ .turn_id = id.turn_id, .call_id = owned_call_id }, owned_tool_name, activity_kind, entry_id, phase, ); return entry_id; } const ToolDetailSearch = struct { index: usize, found: bool, }; fn toolDetailSearch(self: *const TranscriptRuntime, entry_id: u32) ToolDetailSearch { var low: usize = 0; var high = self.tool_details.items.len; while (low < high) { const middle = low + (high - low) / 2; if (self.tool_details.items[middle].entry_id < entry_id) { low = middle + 1; } else { high = middle; } } return .{ .index = low, .found = low < self.tool_details.items.len and self.tool_details.items[low].entry_id == entry_id, }; } pub fn toolDetailForEntry(self: *const TranscriptRuntime, entry_id: u32) ?*const ToolDetailRecord { const search = self.toolDetailSearch(entry_id); if (!search.found) return null; return &self.tool_details.items[search.index]; } pub fn setToolCommandMetadata( self: *TranscriptRuntime, alloc: Allocator, id: types.ToolLifecycleId, display: []const u8, action_label: []const u8, ) !void { const record = self.lifecycle_state.record(id) orelse return error.UnknownToolLifecycleIdentity; try self.setToolCommandMetadataForEntry( alloc, record.entry_id, display, action_label, ); } pub fn setToolCommandMetadataForEntry( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, display: []const u8, action_label: []const u8, ) !void { const detail = self.toolDetailPtr(entry_id) orelse return error.MissingToolDetail; const owned_display = try alloc.dupe(u8, display); errdefer alloc.free(owned_display); const owned_label = try alloc.dupe(u8, action_label); if (detail.command_display) |previous| alloc.free(previous); if (detail.command_action_label) |previous| alloc.free(previous); detail.command_display = owned_display; detail.command_action_label = owned_label; self.markTranscriptContentDirtyFrom(entry_id); } fn toolDetailPtr(self: *TranscriptRuntime, entry_id: u32) ?*ToolDetailRecord { const search = self.toolDetailSearch(entry_id); if (!search.found) return null; return &self.tool_details.items[search.index]; } pub fn attachHistoricalToolDetail( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, activity_kind: types.ToolActivityKind, result: types.PersistedToolResult, ) !void { return self.attachHistoricalToolDetailResolved( alloc, entry_id, call, activity_kind, result, null, null, ); } pub fn attachHistoricalToolDetailWithLifecycle( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, activity_kind: types.ToolActivityKind, result: types.PersistedToolResult, lifecycle_id: ?types.ToolLifecycleId, ) !void { return self.attachHistoricalToolDetailResolved( alloc, entry_id, call, activity_kind, result, lifecycle_id, null, ); } pub fn attachHistoricalToolDetailAfterCommandOutput( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, activity_kind: types.ToolActivityKind, result: types.PersistedToolResult, ) !void { try self.attachHistoricalToolDetailResolved( alloc, entry_id, call, activity_kind, result, null, self.latestCommandOutputEntryId(), ); if (try command_output_runtime.syncCommandOutputBlockEntries(self, alloc)) { try self.rebuildTranscriptCacheAfterStructuredRewrite( alloc, "historical command process presentation", ); self.recomputeCursorFromTranscript(); self.markTranscriptContentDirty(); } } pub fn attachHistoricalToolDetailAfterCommandOutputDetached( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, activity_kind: types.ToolActivityKind, result: types.PersistedToolResult, ) !void { try self.attachHistoricalToolDetailResolved( alloc, entry_id, call, activity_kind, result, null, self.latestCommandOutputEntryId(), ); } fn attachHistoricalToolDetailResolved( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, activity_kind: types.ToolActivityKind, result: types.PersistedToolResult, lifecycle_id: ?types.ToolLifecycleId, command_output_entry_id: ?u32, ) !void { const context_deferred = types.isContextDeferredToolResult(result); const deferred = types.isDeferredToolResult(result); const permission_denied = tool_result_errors.toolPermissionDenialReason(result.output) != null; const shell_command = !deferred and !permission_denied and activity_kind == .command and std.mem.eql(u8, call.name, "shell"); const projected_shell_result = if (shell_command) try managed_execution.modelOutputDelta(alloc, result.output) else null; defer if (projected_shell_result) |value| alloc.free(value); const presentation_result = projected_shell_result orelse result.output; const command_artifact_handle = if (!deferred and !permission_denied and activity_kind == .command and !shell_command) command_output_runtime.commandArtifactHandleFromResult(result.output) else null; try self.upsertToolDetailStart(alloc, entry_id, lifecycle_id, call.name, activity_kind, call.arguments_json); try self.updateToolDetailTerminal( alloc, entry_id, lifecycle_id, call.name, activity_kind, if (context_deferred) .deferred else if (deferred or permission_denied) .denied else if (result.terminal_action_presentation) |presentation| presentation.outcomeKind() else if (result.status == .success or (activity_kind == .command and result.command_process_presentation != null)) .completed else .failed, if (deferred or permission_denied) null else presentation_result, if (deferred or permission_denied) null else .{ .output_handle = if (shell_command) null else result.output_handle, .preview = projected_shell_result orelse result.preview, .output_bytes = result.output_bytes, .stored_output_bytes = result.stored_output_bytes, .truncated = result.truncated, .command_output_replay = result.command_output_replay, .command_process_presentation = result.command_process_presentation, .terminal_action_presentation = result.terminal_action_presentation, }, command_artifact_handle, if (deferred or permission_denied) null else command_output_entry_id, ); if (self.toolDetailPtr(entry_id)) |detail| { detail.created_at_ms = result.created_at_ms; detail.origin = .recorded; } } pub fn attachHistoricalToolCallWithoutResult( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, ) !void { try self.upsertToolDetailStart(alloc, entry_id, null, call.name, null, call.arguments_json); if (self.toolDetailPtr(entry_id)) |detail| { detail.outcome = .failed; detail.origin = .recorded; } } pub fn attachHistoricalCommandOutput( self: *TranscriptRuntime, _: Allocator, entry_id: u32, ) void { const detail = self.toolDetailPtr(entry_id) orelse return; const command_output_entry_id = self.latestCommandOutputEntryId() orelse return; detail.command_output_entry_id = command_output_entry_id; } pub fn attachHistoricalCancelledCommandDetail( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, call: types.ToolCall, command_artifact_handle: ?[]const u8, replayed_output: bool, ) !void { try self.upsertToolDetailStart(alloc, entry_id, null, call.name, .command, call.arguments_json); try self.updateToolDetailTerminal( alloc, entry_id, null, call.name, .command, .cancelled, null, null, command_artifact_handle, if (replayed_output) self.latestCommandOutputEntryId() else null, ); if (self.toolDetailPtr(entry_id)) |detail| detail.origin = .recorded; } fn upsertToolDetailStart( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, lifecycle_id: ?types.ToolLifecycleId, tool_name: []const u8, activity_kind: ?types.ToolActivityKind, arguments_json: ?[]const u8, ) !void { var detail_start = try self.prepareToolDetailStart( alloc, entry_id, lifecycle_id, tool_name, activity_kind, arguments_json, ); defer detail_start.deinit(alloc); self.commitToolDetailStart(alloc, entry_id, &detail_start); } fn prepareToolDetailStart( self: *TranscriptRuntime, alloc: Allocator, entry_id: ?u32, lifecycle_id: ?types.ToolLifecycleId, tool_name: []const u8, activity_kind: ?types.ToolActivityKind, arguments_json: ?[]const u8, ) !PendingToolDetailStart { const existing = if (entry_id) |id| self.toolDetailPtr(id) else null; if (existing == null) try self.tool_details.ensureUnusedCapacity(alloc, 1); var pending: PendingToolDetailStart = .{}; errdefer pending.deinit(alloc); pending.tool_name = try alloc.dupe(u8, tool_name); pending.activity_kind = activity_kind; if (arguments_json) |value| { pending.arguments_json = try alloc.dupe(u8, value); pending.captured_command = try captured_command.isToolCall(alloc, tool_name, value); } else if (std.mem.eql(u8, tool_name, "run_command")) { pending.captured_command = true; } if (lifecycle_id) |id| { if (existing == null or !command_output_runtime.sameLifecycleId(existing.?.lifecycle_id, id)) { const owned_call_id = try alloc.dupe(u8, id.call_id); pending.lifecycle_id = .{ .turn_id = id.turn_id, .call_id = owned_call_id, }; pending.replace_lifecycle = true; } } return pending; } fn commitToolDetailStart( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, pending: *PendingToolDetailStart, ) void { const search = self.toolDetailSearch(entry_id); if (search.found) { const detail = &self.tool_details.items[search.index]; alloc.free(detail.tool_name); detail.tool_name = pending.tool_name.?; pending.tool_name = null; if (pending.captured_command) |value| detail.captured_command = value; if (pending.activity_kind) |kind| detail.activity_kind = kind; if (pending.arguments_json) |arguments_json| { if (detail.arguments_json) |old| alloc.free(old); detail.arguments_json = arguments_json; pending.arguments_json = null; } if (pending.replace_lifecycle) { if (detail.lifecycle_id) |previous| alloc.free(@constCast(previous.call_id)); detail.lifecycle_id = pending.lifecycle_id; pending.lifecycle_id = null; } self.markTranscriptContentDirtyFrom(entry_id); return; } const detail = ToolDetailRecord{ .entry_id = entry_id, .tool_name = pending.tool_name.?, .captured_command = pending.captured_command orelse false, .activity_kind = pending.activity_kind, .arguments_json = pending.arguments_json, .lifecycle_id = pending.lifecycle_id, }; if (search.index == self.tool_details.items.len) { self.tool_details.appendAssumeCapacity(detail); } else { self.tool_details.insertAssumeCapacity(search.index, detail); } pending.tool_name = null; pending.arguments_json = null; pending.lifecycle_id = null; self.markTranscriptContentDirtyFrom(entry_id); } fn commitLifecycleToolDetailStart( self: *TranscriptRuntime, alloc: Allocator, lifecycle_id: types.ToolLifecycleId, entry_id: u32, pending: *PendingToolDetailStart, ) void { const classified = pending.captured_command; self.commitToolDetailStart(alloc, entry_id, pending); if (classified) |captured| { if (self.lifecycle_state.recordPtr(lifecycle_id)) |record| { record.captured_command = captured; } } } fn commitToolPresentationGroup( self: *TranscriptRuntime, entry_id: u32, lifecycle_id: types.ToolLifecycleId, presentation_group_id: ?types.ToolPresentationGroupId, ) void { const incoming = presentation_group_id orelse return; if (incoming.turn_id == 0 or incoming.anchor_step_id == 0 or incoming.turn_id != lifecycle_id.turn_id) { debug_trace.logf( "ui_activity", "invalid tool presentation group ignored turn_id={d} group_turn_id={d} anchor_step_id={d}", .{ lifecycle_id.turn_id, incoming.turn_id, incoming.anchor_step_id }, ); return; } const detail = self.toolDetailPtr(entry_id) orelse return; if (detail.presentation_group_id) |existing| { if (existing.turn_id != incoming.turn_id or existing.anchor_step_id != incoming.anchor_step_id) { debug_trace.logf( "ui_activity", "tool presentation group change ignored turn_id={d} existing_anchor_step_id={d} incoming_anchor_step_id={d}", .{ lifecycle_id.turn_id, existing.anchor_step_id, incoming.anchor_step_id, }, ); } return; } detail.presentation_group_id = incoming; self.markTranscriptContentDirtyFrom(entry_id); } fn updateToolDetailTerminal( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, lifecycle_id: ?types.ToolLifecycleId, tool_name: []const u8, activity_kind: types.ToolActivityKind, outcome: types.ToolOutcomeKind, result: ?[]const u8, result_memory: ?types.ToolResultMemory, command_artifact_handle: ?[]const u8, command_output_entry_id_override: ?u32, ) !void { var pending = try self.prepareToolDetailTerminal( alloc, lifecycle_id, activity_kind, result, result_memory, command_artifact_handle, command_output_entry_id_override, 0, ); defer pending.deinit(alloc); if (self.toolDetailPtr(entry_id) == null) { try self.upsertToolDetailStart(alloc, entry_id, lifecycle_id, tool_name, activity_kind, null); } self.commitToolDetailTerminal(alloc, entry_id, activity_kind, outcome, &pending); } fn prepareToolDetailTerminal( self: *TranscriptRuntime, alloc: Allocator, lifecycle_id: ?types.ToolLifecycleId, activity_kind: types.ToolActivityKind, result: ?[]const u8, result_memory: ?types.ToolResultMemory, command_artifact_handle: ?[]const u8, command_output_entry_id_override: ?u32, command_process_entry_offset: u32, ) !PendingToolDetailTerminal { var pending: PendingToolDetailTerminal = .{}; errdefer pending.deinit(alloc); if (result) |value| pending.result = try alloc.dupe(u8, value); const result_handle = if (result_memory) |memory| memory.output_handle else null; if (result_handle) |handle| pending.result_handle = try alloc.dupe(u8, handle); if (command_artifact_handle) |handle| { pending.command_artifact_handle = try alloc.dupe(u8, handle); } if (result_memory) |memory| { if (memory.command_output_replay) |replay| { pending.command_output_replay = try types.dupeCommandOutputReplay(alloc, replay); } } pending.command_process_presentation = if (result_memory) |memory| memory.command_process_presentation else null; pending.command_output_entry_id = if (activity_kind == .command) command_output_entry_id_override orelse self.commandOutputEntryIdForLifecycle(lifecycle_id) else null; if (activity_kind == .command and pending.command_process_presentation != null and pending.command_output_entry_id == null) { pending.command_process_entry = try self.prepareCommandProcessPresentationEntry( alloc, lifecycle_id, command_process_entry_offset, ); pending.command_output_entry_id = pending.command_process_entry.?.entry_id; } return pending; } fn commitToolDetailTerminal( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, activity_kind: types.ToolActivityKind, outcome: types.ToolOutcomeKind, pending: *PendingToolDetailTerminal, ) void { if (pending.command_process_entry) |*entry| { self.commitCommandProcessPresentationEntry(entry); pending.command_process_entry = null; } const detail = self.toolDetailPtr(entry_id).?; detail.activity_kind = activity_kind; detail.outcome = outcome; detail.fallback_disposition = null; if (pending.result) |owned| { if (detail.result) |old| alloc.free(old); detail.result = owned; pending.result = null; } if (pending.result_handle) |owned| { if (detail.result_handle) |old| alloc.free(old); detail.result_handle = owned; pending.result_handle = null; } if (pending.command_artifact_handle) |owned| { if (detail.command_artifact_handle) |old| alloc.free(old); detail.command_artifact_handle = owned; pending.command_artifact_handle = null; } if (pending.command_output_replay) |owned| { if (detail.command_output_replay) |old| { types.freeCommandOutputReplay(alloc, old); } detail.command_output_replay = owned; pending.command_output_replay = null; } detail.command_process_presentation = pending.command_process_presentation; if (activity_kind == .command) { detail.command_output_entry_id = pending.command_output_entry_id; } self.markTranscriptContentDirtyFrom(entry_id); } pub fn hasStoredResultDetails(self: *const TranscriptRuntime) bool { for (self.tool_details.items) |detail| { if (detail.result_handle != null or detail.command_artifact_handle != null or detail.command_output_replay != null) return true; } return false; } fn commandOutputEntryIdForLifecycle( self: *const TranscriptRuntime, lifecycle_id: ?types.ToolLifecycleId, ) ?u32 { const id = lifecycle_id orelse return null; for (self.command_output_blocks.items) |block| { if (!command_output_runtime.sameLifecycleId(block.lifecycle_id, id)) continue; if (block.entry_id) |entry_id| return entry_id; if (block.live_entry_ids.items.len > 0) return block.live_entry_ids.items[0]; if (block.source_entry_ids.items.len > 0) return block.source_entry_ids.items[0]; for (block.lines.items) |line| { if (line.entry_id) |entry_id| return entry_id; } return null; } return null; } fn prepareCommandProcessPresentationEntry( self: *TranscriptRuntime, alloc: Allocator, lifecycle_id: ?types.ToolLifecycleId, entry_id_offset: u32, ) !PendingCommandProcessEntry { var pending: PendingCommandProcessEntry = .{ .entry_id = self.next_entry_id +% entry_id_offset, }; errdefer pending.deinit(alloc); pending.bytes = try alloc.alloc(u8, 0); const line_text = try alloc.alloc(u8, 0); var line_text_owned = true; errdefer if (line_text_owned) alloc.free(line_text); pending.block = .{ .entry_id = pending.entry_id }; const block = &pending.block.?; if (lifecycle_id) |id| { const call_id = try alloc.dupe(u8, id.call_id); block.lifecycle_id = .{ .turn_id = id.turn_id, .call_id = call_id, }; } try block.lines.append(alloc, .{ .stream = .stdout, .text = line_text, .entry_id = pending.entry_id, .visible = false, }); line_text_owned = false; try block.source_entry_ids.append(alloc, pending.entry_id); try self.entries.ensureUnusedCapacity(alloc, 1); try self.command_output_blocks.ensureUnusedCapacity(alloc, 1); return pending; } fn commitCommandProcessPresentationEntry( self: *TranscriptRuntime, pending: *PendingCommandProcessEntry, ) void { std.debug.assert(self.next_entry_id == pending.entry_id); self.next_entry_id +%= 1; self.entries.appendAssumeCapacity(.{ .raw_bytes = .{ .id = pending.entry_id, .created_at_ms = io_mod.milliTimestamp(), .bytes = pending.bytes.?, .class = .command_output, } }); pending.bytes = null; self.command_output_blocks.appendAssumeCapacity(pending.block.?); pending.block = null; self.markTranscriptContentDirtyFrom(pending.entry_id); } fn latestCommandOutputEntryId(self: *const TranscriptRuntime) ?u32 { var index = self.command_output_blocks.items.len; while (index > 0) { index -= 1; if (self.command_output_blocks.items[index].entry_id) |entry_id| return entry_id; } return null; } pub fn detailEntryIdForCommandOutputSource( self: *const TranscriptRuntime, source_entry_id: u32, ) ?u32 { for (self.command_output_blocks.items) |block| { var owns_source = block.entry_id == source_entry_id; if (!owns_source) { for (block.source_entry_ids.items) |source_id| { if (source_id == source_entry_id) { owns_source = true; break; } } } if (!owns_source) { for (block.live_entry_ids.items) |live_entry_id| { if (live_entry_id == source_entry_id) { owns_source = true; break; } } } if (!owns_source) continue; const lifecycle_id = block.lifecycle_id orelse return null; for (self.tool_details.items) |detail| { if (command_output_runtime.sameLifecycleId( detail.lifecycle_id, lifecycle_id, )) return detail.entry_id; } return null; } return null; } fn clearToolDetails(self: *TranscriptRuntime, alloc: Allocator) void { for (self.tool_details.items) |*detail| detail.deinit(alloc); self.tool_details.clearRetainingCapacity(); } fn clearFullDetailRecords(self: *TranscriptRuntime, alloc: Allocator) void { for (self.full_detail_records.items) |*record| record.deinit(alloc); self.full_detail_records.clearRetainingCapacity(); } /// Appends one full-only detail record (session assembly, network, /// recovery) and marks the full transcript content dirty so an open /// ctrl+o view reloads it. Never touches the entry store. pub fn appendFullDetailRecord( self: *TranscriptRuntime, alloc: Allocator, notice: types.SemanticNotice, ) !void { const owned = try types.dupeSemanticNotice(alloc, notice); errdefer types.freeSemanticNotice(alloc, owned); try self.full_detail_records.append(alloc, .{ .notice = owned, .created_at_ms = io_mod.milliTimestamp(), }); self.markTranscriptContentDirty(); } pub fn pruneToolDetailsForRetainedEntries( self: *TranscriptRuntime, alloc: Allocator, retained_entry_ids: *const transcript_store.EntryIdSet, ) void { var write_index: usize = 0; for (self.tool_details.items, 0..) |detail, read_index| { if (!retained_entry_ids.contains(detail.entry_id)) { var removed = detail; removed.deinit(alloc); continue; } if (write_index != read_index) { self.tool_details.items[write_index] = detail; } write_index += 1; } self.tool_details.items.len = write_index; } pub fn clearFullTranscriptDetails(self: *TranscriptRuntime, alloc: Allocator) void { self.compact_transcript_source_cache.deinit(alloc); self.resetFullTranscriptPageNavigation(); if (self.full_transcript_window_load.busy()) { self.full_transcript_installed_page_retired = self.full_transcript_installed_page != null; } else { self.discardInstalledFullTranscriptPage(); } self.clearToolDetails(alloc); self.clearFullDetailRecords(alloc); self.full_transcript = self.full_transcript.closed(); } fn discardInstalledFullTranscriptPage(self: *TranscriptRuntime) void { if (self.full_transcript_installed_page) |*page| page.deinit(); self.full_transcript_installed_page = null; self.full_transcript_installed_page_retired = false; } pub fn resetCommandOutputDisplay(self: *TranscriptRuntime, alloc: Allocator, reason: []const u8) void { return command_output_runtime.resetCommandOutputDisplay(self, alloc, reason); } pub fn retainedStructuredBytesForCommandOutput( self: *const TranscriptRuntime, ) usize { return transcript_store.retainedStructuredBytes(self); } /// Append a raw-bytes entry. Takes ownership of `bytes` — the slice /// must have been allocated by `alloc` and must not be freed by the /// caller; the entry is freed on `clearTranscript` or `deinit`. /// Returns the stamped entry id. pub fn appendRawBytesEntry(self: *TranscriptRuntime, alloc: Allocator, bytes: []const u8) !u32 { return transcript_store.appendRawBytesEntry(self, alloc, bytes); } pub fn appendRawBytesEntryClassified( self: *TranscriptRuntime, alloc: Allocator, bytes: []const u8, class: RawEntryClass, ) !u32 { return transcript_store.appendRawBytesEntryClassified(self, alloc, bytes, class); } pub fn appendRawBytesEntryClassifiedAt( self: *TranscriptRuntime, alloc: Allocator, bytes: []const u8, class: RawEntryClass, created_at_ms: i64, ) !u32 { return transcript_store.appendRawBytesEntryClassifiedAt( self, alloc, bytes, class, created_at_ms, ); } pub fn insertRawBytesEntryClassifiedAfter( self: *TranscriptRuntime, alloc: Allocator, after_entry_id: u32, bytes: []const u8, class: RawEntryClass, ) !?u32 { return transcript_store.insertRawBytesEntryClassifiedAfter(self, alloc, after_entry_id, bytes, class); } pub fn insertRawBytesEntryClassifiedAt( self: *TranscriptRuntime, alloc: Allocator, index: usize, bytes: []const u8, class: RawEntryClass, ) !u32 { return transcript_store.insertRawBytesEntryClassifiedAt(self, alloc, index, bytes, class); } pub fn rowForEntryId(self: *TranscriptRuntime, entry_id: u32) ?u16 { return transcript_viewport_runtime.rowForEntryId(self, entry_id); } pub fn toolStatusEntryLabel(self: *const TranscriptRuntime, entry_id: u32) ?[]const u8 { return transcript_store.toolStatusEntryLabel(self, entry_id); } pub fn transientAssistantGapRows(self: *const TranscriptRuntime) u16 { return transcript_viewport_runtime.transientAssistantGapRows(self); } pub fn needsIdleFooterBottomReservation(self: *const TranscriptRuntime) bool { return transcript_viewport_runtime.needsIdleFooterBottomReservation(self); } pub fn idleFooterGapOffset(self: *const TranscriptRuntime) u16 { return transcript_viewport_runtime.idleFooterGapOffset(self); } pub fn bannerFooterGapOffset(self: *const TranscriptRuntime) u16 { return transcript_viewport_runtime.bannerFooterGapOffset(self); } pub fn preservedBottomReservationRows(self: *const TranscriptRuntime) u16 { return transcript_viewport_runtime.preservedBottomReservationRows(self); } pub fn trimTrailingBlankLines(self: *TranscriptRuntime) void { return transcript_store.trimTrailingBlankLines(self); } pub fn appendRawTranscriptEntry(self: *TranscriptRuntime, alloc: Allocator, text: []const u8) !u32 { return transcript_store.appendRawTranscriptEntry(self, alloc, text); } pub fn appendRawTranscriptEntryClassified( self: *TranscriptRuntime, alloc: Allocator, text: []const u8, class: RawEntryClass, ) !u32 { return transcript_store.appendRawTranscriptEntryClassified(self, alloc, text, class); } pub fn appendTurnSummaryEntry(self: *TranscriptRuntime, alloc: Allocator, summary: types.TurnSummary) !u32 { var line_buf: [128]u8 = undefined; const line = formatTurnSummaryLine(&line_buf, summary); const entry = try std.fmt.allocPrint(alloc, "{s}{s}{s}\n", .{ ui_render.dim_style, line, ui_render.reset_style }); errdefer alloc.free(entry); const entry_id = try transcript_store.appendRawBytesEntryClassifiedAt( self, alloc, entry, .turn_summary, if (summary.completed_at_ms > 0) summary.completed_at_ms else io_mod.milliTimestamp(), ); self.markTranscriptContentDirtyFrom(entry_id); if (self.worker_status.clear_recovered_route()) self.render_requests.request(.footer); return entry_id; } /// Swap the bytes of the `raw_bytes` entry with `entry_id` to a dup of /// `new_bytes` and regenerate the transcript byte buffer. Returns false /// when no such entry exists (e.g. cleared by `clearTranscript`). pub fn updateRawBytesEntry( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, new_bytes: []const u8, ) !bool { return transcript_store.updateRawBytesEntry(self, alloc, entry_id, new_bytes); } pub fn setRawEntryClass(self: *TranscriptRuntime, entry_id: u32, class: RawEntryClass) bool { return transcript_store.setRawEntryClass(self, entry_id, class); } pub fn rawEntryClass(self: *const TranscriptRuntime, entry_id: u32) ?RawEntryClass { return transcript_store.rawEntryClass(self, entry_id); } /// Renders and records one borrowed user turn as a single transcript /// mutation. The caller retains ownership of `user` and `skill_tokens` /// on success and error. pub fn writeUserPromptCard( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, user: types.UserTurn, has_prior_turns: bool, skill_tokens: []const input_visual_layout.SkillTokenSpan, ) !u32 { return transcript_store.writeUserPromptCard( self, alloc, metrics, user, has_prior_turns, skill_tokens, ); } /// Consumes `turn` regardless of outcome. All owned slices must use `alloc`; /// the entry owns them on success and this method frees them on failure. /// Returns the stamped entry id. pub fn appendUserTurnOwned(self: *TranscriptRuntime, alloc: Allocator, turn: types.UserTurn) !u32 { return transcript_store.appendUserTurnOwned(self, alloc, turn); } pub fn appendUserTurnOwnedWithSkillTokens( self: *TranscriptRuntime, alloc: Allocator, turn: types.UserTurn, skill_tokens: []const input_visual_layout.SkillTokenSpan, ) !u32 { return transcript_store.appendUserTurnOwnedWithSkillTokens(self, alloc, turn, skill_tokens); } /// Open a new assistant-turn entry and return its id. Callers that /// need to extend the segments text should re-resolve the pointer /// with `lookupAssistantSegments(id)` immediately before each use; /// a raw segments pointer held across any subsequent `entries` /// append is invalidated by ArrayList realloc. pub fn appendAssistantTurnEntry(self: *TranscriptRuntime, alloc: Allocator) !u32 { return transcript_store.appendAssistantTurnEntry(self, alloc); } /// Takes ownership of `table` only when it returns successfully. pub fn appendAssistantTableOwned( self: *TranscriptRuntime, alloc: Allocator, table: @import("../../core/agent/assistant_presentation.zig").TablePayload, ) !u32 { return transcript_store.appendAssistantTableOwned(self, alloc, table); } /// Takes ownership of `block` only when it returns successfully. pub fn appendAssistantCodeBlockOwned( self: *TranscriptRuntime, alloc: Allocator, block: @import("../../core/agent/assistant_presentation.zig").CodeBlockPayload, ) !u32 { return transcript_store.appendAssistantCodeBlockOwned(self, alloc, block); } pub fn appendAssistantThematicRule(self: *TranscriptRuntime, alloc: Allocator) !u32 { return transcript_store.appendAssistantThematicRule(self, alloc); } /// Walk `entries` and return a pointer to the segments of the /// assistant-turn entry with `id`, or null if no such entry exists /// (including when `id` belongs to a `raw_bytes` or `user_turn` /// entry). The returned pointer is stable only until the next /// `entries` append; resolve immediately before use. pub fn lookupAssistantSegments(self: *TranscriptRuntime, id: u32) ?*AssistantTurnSegments { return transcript_store.lookupAssistantSegments(self, id); } /// Return a pointer to the trailing entry's segments if that entry /// is an assistant turn; otherwise null. Used by the pacer to /// append to the in-progress response instead of opening a new /// turn when mid-stream. pub fn tailAssistantSegments(self: *TranscriptRuntime) ?*AssistantTurnSegments { return transcript_store.tailAssistantSegments(self); } /// Appends paced text to the trailing assistant entry, creating one if needed, /// and mirrors it into the transcript buffer. Using `writeTranscript` here /// would also create raw entries and double-count content during reflow. pub fn streamAssistantChunk( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, ) !u32 { const transcript_was_pending = self.render_requests.hasReason(.transcript); const entry_id = try transcript_store.streamAssistantChunk(self, alloc, metrics, text); if (!transcript_was_pending and self.nativeHistoryActive() and std.mem.findScalar(u8, text, '\n') == null) { self.render_requests.clearReason(.transcript); debug_trace.logf( "scroll", "assistant_partial_paint_deferred bytes={d} entry_id={d}", .{ text.len, entry_id }, ); } return entry_id; } pub fn retintEntriesForTheme( self: *TranscriptRuntime, alloc: Allocator, from_light: bool, to_light: bool, ) !void { return transcript_store.retintEntriesForTheme(self, alloc, from_light, to_light); } pub fn setTranscriptPresentationDepth( self: *TranscriptRuntime, alloc: Allocator, depth: transcript_presentation.Depth, ) !bool { const current = self.full_transcript.depth; if (current == depth) { if (depth == .inline_mode) { self.full_transcript_restore_open_pending = false; } return false; } const restored_open = current == .inline_mode and depth != .inline_mode and self.full_transcript_restore_open_pending; if (current == .inline_mode and depth != .inline_mode) { self.full_transcript_prepared_page_visible = false; self.full_transcript_open_content_revision = self.full_transcript_content_revision; self.full_transcript_open_cols = self.layout.cols; self.full_transcript_open_rows = self.layout.rows; if (!restored_open) try self.captureFullTranscriptAnchor(alloc); } self.full_transcript = self.full_transcript.with_depth(depth); if (depth != .inline_mode) self.full_transcript_restore_open_pending = false; if (depth == .inline_mode) { self.full_transcript_restore_open_pending = false; self.full_transcript_open_content_revision = null; self.full_transcript_open_cols = 0; self.full_transcript_open_rows = 0; self.resetFullTranscriptPageNavigation(); } self.markTranscriptDirty(); return true; } pub fn fullTranscriptPrimaryRestore(self: *const TranscriptRuntime) FullTranscriptPrimaryRestore { const open_revision = self.full_transcript_open_content_revision orelse return .changed; const same_geometry = self.full_transcript_open_cols == self.layout.cols and self.full_transcript_open_rows == self.layout.rows; const needs_repair = !same_geometry or self.terminal_reset_pending or self.resize_history_row_delta != null; if (needs_repair) return .resized; return if (open_revision != self.full_transcript_content_revision) .changed else .exact; } pub fn retainRestoredPrimaryTranscript(self: *TranscriptRuntime) void { self.transcript_band_dirty = false; self.render_requests.clearReason(.transcript); self.render_requests.request(.footer); debug_trace.logf( "full_transcript_cache", "close reused restored primary transcript revision={d}", .{self.full_transcript_content_revision}, ); } pub fn repaintRestoredPrimaryTranscriptAfterResize(self: *TranscriptRuntime) void { self.terminal_reset_pending = true; self.resize_history_row_delta = null; self.pending_resize_observation = null; self.render_requests.acknowledgeSettledResizeCommit(); self.invalidateTranscriptAnchor("full transcript restored resized primary"); // A visible-band repaint can discard rows displaced by terminal reflow. // Rebuild them through normal resize recovery after restoring the primary screen. if (self.layout.rows > 0) { self.render_requests.requestInvalidation(.{ .reason = .external_clear, .top = if (self.owned_top_row == 0 or self.owned_top_row > self.layout.rows) 1 else self.owned_top_row, .bottom = self.layout.rows, }); } debug_trace.logf( "full_transcript_cache", "close repaints restored resized primary revision={d} layout={d}x{d}", .{ self.full_transcript_content_revision, self.layout.cols, self.layout.rows }, ); } fn captureFullTranscriptAnchor(self: *TranscriptRuntime, alloc: Allocator) !void { _ = alloc; self.full_transcript = self.full_transcript.clear_anchor(); const selection = self.last_viewport_selection orelse return; const provenance = self.committedRowProvenance(); var source: ?transcript_blocks.LineProvenance = null; for (provenance) |row| { if (row.row < selection.top_row) continue; source = row.source; break; } const source_entry_id = switch (source orelse return) { .entry => |entry| entry.entry_id, .folded_command_output => |output| output.entry_id, else => null, }; const anchor_entry_id = if (source_entry_id) |entry_id| self.detailEntryIdForCommandOutputSource(entry_id) orelse entry_id else null; self.full_transcript = self.full_transcript.capture_anchor(anchor_entry_id); } pub fn fullTranscriptActive(self: *const TranscriptRuntime) bool { return self.full_transcript.depth.active(); } pub fn fullTranscriptFocusedWorkActive(self: *const TranscriptRuntime) bool { return self.full_transcript_window_load.busy() or (self.full_transcript_page_load.busy() and self.full_transcript_open_request != null); } pub fn transcriptPresentationDepth( self: *const TranscriptRuntime, ) transcript_presentation.Depth { return self.full_transcript.depth; } pub const FullTranscriptViewportSnapshot = struct { presentation: transcript_presentation.Snapshot, page_anchor: full_transcript_page.Anchor, }; pub fn snapshotFullTranscriptViewport( self: *const TranscriptRuntime, ) FullTranscriptViewportSnapshot { return .{ .presentation = self.full_transcript.snapshot(), .page_anchor = self.full_transcript_page_anchor, }; } pub fn restoreFullTranscriptViewport( self: *TranscriptRuntime, snapshot: FullTranscriptViewportSnapshot, ) void { self.full_transcript = .from_snapshot(snapshot.presentation); self.full_transcript_page_anchor = snapshot.page_anchor; self.markTranscriptDirty(); } pub fn requestTailViewport( self: *TranscriptRuntime, request: viewport_selection.TailViewportPolicy, ) void { const changed = if (self.tail_viewport_request) |current| !std.meta.eql(current, request) else true; self.tail_viewport_request = request; self.tail_viewport_resolution = null; const has_committed_anchor = switch (self.transcript_commit_state) { .invalid => false, .stable, .recovering => true, }; if (changed and has_committed_anchor) { self.invalidateTranscriptAnchor("tail viewport changed"); } } pub fn resolvedTailViewport( self: *const TranscriptRuntime, ) ?viewport_selection.TailOffsetResolution { return self.tail_viewport_resolution; } pub fn closeFullTranscriptState(self: *TranscriptRuntime) void { self.full_transcript = self.full_transcript.closed(); self.resetFullTranscriptPageNavigation(); } fn resetFullTranscriptPageNavigation(self: *TranscriptRuntime) void { self.full_transcript_page_anchor = .tail; self.full_transcript_prepared_page_visible = false; self.full_transcript_open_request = null; self.full_transcript_restore_open_pending = false; self.full_transcript_window_load.cancelActive(); self.full_transcript_page_load.cancelActive(); } pub fn fullTranscriptAnchorEntryId(self: *const TranscriptRuntime) ?u32 { return self.full_transcript.anchor_entry_id; } pub fn retargetFullTranscriptAnchor(self: *TranscriptRuntime, entry_id: u32) void { self.full_transcript = self.full_transcript.retarget_anchor(entry_id); } pub fn scrollFullTranscript( self: *TranscriptRuntime, direction: input_action.MouseWheel, unit: FullTranscriptScrollUnit, ) void { if (self.full_transcript_page_load.busy() and self.installedFullTranscriptPageProjection() == null) { debug_trace.logf( "full_transcript_cache", "scroll_ignored reason=page_loading direction={s} unit={s}", .{ @tagName(direction), @tagName(unit) }, ); return; } const rows = switch (unit) { .wheel => full_transcript_wheel_rows, .page => self.fullTranscriptPageRows(), }; const before = self.full_transcript.scroll_rows; self.full_transcript = self.full_transcript.scroll(switch (direction) { .up => .up, .down => .down, }, rows); const local_visible_rows = @max(@as(u32, 1), self.layout.rows -| 4); const local_total_rows = if (self.full_transcript_installed_page) |*page| self.installedPageMeasurement(page).total_rows else 0; const local_max_offset = local_total_rows -| local_visible_rows; const adjacent_anchor = switch (direction) { .up => if (before == 0 and self.full_transcript.scroll_rows == 0) if (self.full_transcript_installed_page) |*page| full_transcript_page.previousAnchor(page.source.range) else null else null, .down => if (before >= local_max_offset and self.full_transcript_installed_page != null) full_transcript_page.nextAnchor( self.full_transcript_installed_page.?.source.range, self.entries.items.len, ) else null, }; if (adjacent_anchor) |anchor| { const boundary_index = switch (anchor) { .entry_index => |index| index, .tail => unreachable, }; if (boundary_index < self.entries.items.len) { self.full_transcript_page_anchor = anchor; self.full_transcript = self.full_transcript.select_page_boundary( self.entries.items[boundary_index].id(), ); } } debug_trace.logf( "full_transcript_cache", "scroll depth={s} direction={s} unit={s} rows={d} before={d} after={d}", .{ @tagName(self.full_transcript.depth), @tagName(direction), @tagName(unit), rows, before, self.full_transcript.scroll_rows, }, ); } pub const FullTranscriptScrollUnit = enum { wheel, page, }; const full_transcript_wheel_rows: u32 = 3; fn fullTranscriptPageRows(self: *const TranscriptRuntime) u32 { const first_row = if (self.owned_top_row == 0) 1 else self.owned_top_row; if (self.layout.content_bottom >= first_row) { return @max(@as(u32, 1), @as(u32, self.layout.content_bottom - first_row + 1)); } return 1; } pub fn initViewport(self: *TranscriptRuntime, metrics: *Metrics, requested_start_row: u16) !void { return transcript_viewport_runtime.initViewport(self, metrics, requested_start_row); } pub fn initViewportWithReservedRows(self: *TranscriptRuntime, metrics: *Metrics, requested_start_row: u16, min_body_rows: u16) !void { return transcript_viewport_runtime.initViewportWithReservedRows(self, metrics, requested_start_row, min_body_rows); } pub fn requestTerminalReset(self: *TranscriptRuntime, metrics: *Metrics) !void { const before = self.transcriptCommitDiagnostic(); diagnostics.recordRenderEvent( .reset, "state={s} view={d} history={d} terminal={d}x{d} owned_top={d} cursor={d},{d}", .{ @tagName(before.state), before.visual_offset, before.history_visual_offset, self.layout.cols, self.layout.rows, self.owned_top_row, self.cursor_row, self.cursor_col }, ); const min_body_rows = self.min_visible_viewport_rows; self.reflow_clear_guard_rows = 0; self.resize_history_row_delta = null; self.viewport_clear_pending = false; // Job-control resume (and other full resets) reanchor at row 1. Drop // pending clears recorded against the pre-reset footer geometry — // e.g. eraseCurrentFrame at owned_top mid-scrollback — or the first // post-reset plan validates them against the new compact bands and // aborts with InvalidInvalidationRange. self.render_requests.clearPendingInvalidations(); self.footer_viewport.has_frame = false; self.footer_viewport.clearExternalInvalidation(); self.terminal_reset_pending = true; try self.initViewportWithReservedRows(metrics, 1, min_body_rows); } pub fn requestTerminalResetAfterResize( self: *TranscriptRuntime, metrics: *Metrics, history_row_delta: ?i32, ) !void { try self.requestTerminalReset(metrics); self.resize_history_row_delta = history_row_delta; } pub fn reserveVisibleViewportRows(self: *TranscriptRuntime, min_body_rows: u16) void { return transcript_viewport_runtime.reserveVisibleViewportRows(self, min_body_rows); } pub fn reanchorTop(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics) !void { return transcript_viewport_runtime.reanchorTop(self, alloc, metrics); } pub fn footerTopRowForExtra(self: TranscriptRuntime, extra: u16) u16 { return transcript_viewport_runtime.footerTopRowForExtra(self, extra); } pub fn forgetShimmer(self: *TranscriptRuntime) void { self.shimmer_active = false; self.shimmer_is_overlay = false; } pub fn transcriptCommitDiagnostic(self: *const TranscriptRuntime) TranscriptCommitDiagnostic { return switch (self.transcript_commit_state) { .invalid => .{ .state = .invalid }, .stable => |anchor| .{ .state = .stable, .stable_start_line = anchor.selection.start_line, .visual_offset = anchor.visual_offset, .history_visual_offset = anchor.history_visual_offset, .total_visual_rows = anchor.total_visual_rows, .source_bytes = anchor.flow.len, .layout_id = anchor.layout_id, }, .recovering => |receipt| .{ .state = .recovering, .visual_offset = receipt.accepted_visual_offset, .history_visual_offset = receipt.accepted_history_visual_offset, .total_visual_rows = receipt.attempt_total_visual_rows, .source_bytes = receipt.flow.len, .remaining_inline_rows = @as(u32, receipt.remaining_resize_reflow_rows) + receipt.remaining_semantic_rows, .remaining_unplanned_scroll_rows = receipt.remaining_unplanned_scroll_rows, }, }; } pub fn normalBufferRecoveryPending(self: *const TranscriptRuntime) bool { return switch (self.transcript_commit_state) { .invalid => false, .stable => |anchor| anchor.normal_buffer_recovery_pending, .recovering => |receipt| receipt.normal_buffer_recovery_pending, }; } pub fn historyCatchupPending(self: *const TranscriptRuntime) bool { return switch (self.transcript_commit_state) { .invalid, .recovering => false, .stable => |anchor| anchor.history_catchup_pending, }; } pub fn stableTranscriptProjectionForFlow( self: *const TranscriptRuntime, rendered_flow: []const u8, ) ?StableTranscriptProjection { const anchor = switch (self.transcript_commit_state) { .stable => |value| value, .invalid, .recovering => return null, }; if (!std.mem.startsWith(u8, rendered_flow, anchor.flow)) return null; return .{ .selection = anchor.selection, .visual_offset = committedProjectionVisualOffset(anchor), .history_visual_offset = anchor.history_visual_offset, .total_visual_rows = anchor.total_visual_rows, .flow_unchanged = rendered_flow.len == anchor.flow.len, .cursor_row = anchor.cursor_row, .cursor_col = anchor.cursor_col, .occupied_last_row = anchor.occupied_last_row, .occupied_last_row_blank = anchor.occupied_last_row_blank, .layout_id = anchor.layout_id, .measured_history_origin = anchor.measured_history_origin, }; } pub fn measuredResizeProjectionForFlow( self: *const TranscriptRuntime, rendered_flow: []const u8, ) ?MeasuredResizeProjection { return switch (self.transcript_commit_state) { .invalid => null, .stable => if (self.stableTranscriptProjectionForFlow(rendered_flow) != null) .stable else null, .recovering => |receipt| blk: { if (receipt.attempt_cols == 0 or !std.mem.startsWith(u8, rendered_flow, receipt.flow)) { break :blk null; } break :blk .{ .recovering = .{ .accepted_visual_offset = receipt.accepted_visual_offset, .source_cols = receipt.attempt_cols, } }; }, }; } pub fn measuredHistoryOriginForFlow( self: *const TranscriptRuntime, rendered_flow: []const u8, ) ?transcript_painter.MeasuredHistoryOriginProjection { return switch (self.transcript_commit_state) { .invalid => null, .stable => |anchor| blk: { if (!std.mem.startsWith(u8, rendered_flow, anchor.flow)) { break :blk null; } const origin = anchor.measured_history_origin orelse break :blk null; break :blk .{ .origin = origin, .current_cols = self.committed_frame_layout.terminal_cols, }; }, .recovering => |receipt| blk: { if (!std.mem.startsWith(u8, rendered_flow, receipt.flow)) { break :blk null; } const origin = receipt.measured_history_origin orelse break :blk null; break :blk .{ .origin = origin, .current_cols = receipt.attempt_cols, }; }, }; } pub fn invalidateTranscriptAnchor(self: *TranscriptRuntime, reason: []const u8) void { if (self.committedRetentionIdentity()) |identity| { if (identity.publication_entries.len > 0 or (self.fullTranscriptActive() and identity.lines.len > 0)) { switch (self.transcript_commit_state) { .stable => |*anchor| { anchor.flow_materialized = false; anchor.normal_buffer_recovery_pending = true; anchor.history_catchup_pending = true; }, .recovering => |*receipt| { receipt.projection_rebase_pending = true; receipt.materialized_flow_len = null; receipt.presentation_valid = false; }, .invalid => unreachable, } self.publication_projection_invalid = true; debug_trace.logf("scroll", "transcript_publication_invalidation_deferred reason={s}", .{reason}); self.markTranscriptDirty(); return; } } self.resetTranscriptAnchor(reason); } pub fn resetTranscriptAnchor(self: *TranscriptRuntime, reason: []const u8) void { self.publication_projection_invalid = false; const diagnostic = self.transcriptCommitDiagnostic(); if (diagnostic.state != .invalid) { diagnostics.recordRenderEvent( .source_invalidated, "reason={s} state={s} view={d} history={d} rows={d} source_bytes={d} recovery={} catchup={}", .{ reason, @tagName(diagnostic.state), diagnostic.visual_offset, diagnostic.history_visual_offset, diagnostic.total_visual_rows, diagnostic.source_bytes, self.normalBufferRecoveryPending(), self.historyCatchupPending() }, ); debug_trace.logf( "scroll", "transcript_anchor_invalidate state={s} stable_start={d} visual_offset={d} reason={s}", .{ @tagName(diagnostic.state), diagnostic.stable_start_line orelse 0, diagnostic.visual_offset, reason, }, ); } const alloc = self.shadow_vt_alloc orelse self.detached_commit_alloc orelse { std.debug.assert(self.transcript_commit_state == .invalid); return; }; self.transcript_commit_state.deinit(alloc); } pub fn committedRetentionIdentity(self: *const TranscriptRuntime) ?source_preparation.RetentionIdentity { return switch (self.transcript_commit_state) { .invalid => null, .stable => |anchor| anchor.retention_identity, .recovering => |receipt| receipt.retention_identity, }; } pub fn prepareCommittedRetentionSource(self: *const TranscriptRuntime, alloc: Allocator) !?TranscriptPreparationSource { return self.prepareCommittedRetentionSourceInterruptible(alloc, null); } pub fn prepareCommittedRetentionSourceInterruptible(self: *const TranscriptRuntime, alloc: Allocator, checkpoint: ?*build_checkpoint.BuildCheckpoint) !?TranscriptPreparationSource { try build_checkpoint.poll(checkpoint); const cols = self.retentionSourceCols() orelse return null; const flow = switch (self.transcript_commit_state) { .invalid => return null, .stable => |anchor| anchor.flow, .recovering => |receipt| receipt.flow, }; const identity = self.committedRetentionIdentity().?; var source = try source_preparation.prepareIndexedFullTranscriptWindowSourceInterruptible(alloc, try alloc.dupe(u8, flow), cols, checkpoint); errdefer source.deinit(alloc); source.line_provenance = try alloc.dupe(transcript_blocks.LineProvenance, identity.lines); source.publication_entries = try alloc.dupe(u32, identity.publication_entries); return source; } pub fn retentionSourceCols(self: *const TranscriptRuntime) ?u16 { const cols = switch (self.transcript_commit_state) { .invalid => return null, .stable => if (self.fullTranscriptActive() and self.full_transcript_open_cols > 0) self.full_transcript_open_cols else self.committed_frame_layout.terminal_cols, .recovering => |receipt| receipt.attempt_cols, }; return if (cols == 0) null else cols; } fn retainedHistoryOffset(mapping: source_preparation.RetentionRebase, identity: source_preparation.RetentionIdentity, history: u32) u32 { var result = mapping.visual(history); if (identity.publication_entries.len == 0) return result; var owner: ?u32 = null; for (mapping.before.line_provenance, 0..) |line, index| { owner = source_preparation.publication_owner(owner, line); if (mapping.before.transcript_visual_row_offsets[index + 1] <= history) continue; if (owner) |id| { if (std.mem.findScalar(u32, identity.publication_entries, id) != null) result = @min(result, mapping.visual(@max(history, mapping.before.transcript_visual_row_offsets[index]))); } } return result; } /// Rebase source-relative state before publishing a retention mutation. /// Allocation completes before any live boundary or owned flow is changed. pub fn rebaseRetainedTranscript( self: *TranscriptRuntime, alloc: Allocator, mapping: source_preparation.RetentionRebase, retained_identity: source_preparation.RetentionIdentity, ) !void { const flow = switch (self.transcript_commit_state) { .invalid => return, .stable => |anchor| anchor.flow, .recovering => |receipt| receipt.flow, }; var identity = try retained_identity.clone(alloc); errdefer identity.deinit(alloc); const retained_flow = try alloc.dupe(u8, mapping.retained.bytes[0..mapping.byte(flow.len)]); const diagnostic = self.transcriptCommitDiagnostic(); switch (self.transcript_commit_state) { .invalid => unreachable, .stable => |*anchor| { const old_start = mapping.before.byteAtVisualOffset(anchor.history_visual_offset); const mapped_history = retainedHistoryOffset(mapping, retained_identity, anchor.history_visual_offset); const new_start = mapping.retained.byteAtVisualOffset(mapped_history); anchor.flow_materialized = anchor.flow_materialized and std.mem.eql(u8, mapping.before.bytes[old_start..], mapping.retained.bytes[new_start..]); if (!anchor.flow_materialized) anchor.normal_buffer_recovery_pending = true; alloc.free(anchor.flow); anchor.flow = retained_flow; anchor.retention_identity.deinit(alloc); anchor.retention_identity = identity; anchor.visual_offset = mapping.visual(anchor.visual_offset); anchor.history_visual_offset = mapped_history; anchor.total_visual_rows = mapping.visual(anchor.total_visual_rows); anchor.selection = rebaseRetentionSelection(anchor.selection, mapping); anchor.measured_history_origin = rebaseRetentionHistoryOrigin(anchor.measured_history_origin, mapping); }, .recovering => |*receipt| { if (receipt.materialized_flow_len) |end| { const old_start = mapping.before.byteAtVisualOffset(receipt.materialized_visual_offset); const new_start = mapping.retained.byteAtVisualOffset(mapping.visual(receipt.materialized_visual_offset)); const new_end = mapping.byte(end); if (old_start > end or new_start > new_end or !std.mem.eql(u8, mapping.before.bytes[old_start..end], mapping.retained.bytes[new_start..new_end])) { debug_trace.logf("scroll", "retention invalidated materialized endpoint after projection change", .{}); receipt.materialized_flow_len = null; receipt.presentation_valid = false; } } if (receipt.flow_owned) alloc.free(receipt.flow); receipt.flow = retained_flow; receipt.flow_owned = true; receipt.retention_identity.deinit(alloc); receipt.retention_identity = identity; receipt.remaining_semantic_rows = mapping.visual(receipt.accepted_history_visual_offset +| receipt.remaining_semantic_rows) -| mapping.visual(receipt.accepted_history_visual_offset); receipt.remaining_semantic_progress_rows = mapping.visual(receipt.accepted_visual_offset +| receipt.remaining_semantic_progress_rows) -| mapping.visual(receipt.accepted_visual_offset); receipt.materialized_visual_rows = @intCast(mapping.visual(receipt.materialized_visual_offset +| receipt.materialized_visual_rows) -| mapping.visual(receipt.materialized_visual_offset)); receipt.accepted_visual_offset = mapping.visual(receipt.accepted_visual_offset); receipt.accepted_history_visual_offset = retainedHistoryOffset(mapping, retained_identity, receipt.accepted_history_visual_offset); receipt.attempt_visual_offset = mapping.visual(receipt.attempt_visual_offset); receipt.attempt_total_visual_rows = mapping.visual(receipt.attempt_total_visual_rows); receipt.materialized_visual_offset = mapping.visual(receipt.materialized_visual_offset); if (receipt.materialized_flow_len) |len| receipt.materialized_flow_len = mapping.byte(len); receipt.measured_history_origin = rebaseRetentionHistoryOrigin(receipt.measured_history_origin, mapping); }, } if (self.last_viewport_selection) |*selection| { selection.* = rebaseRetentionSelection(selection.*, mapping); self.last_visible_transcript_partial_skip_rows = selection.partial_skip_rows; self.last_visible_transcript_split_prefix_lines = selection.split_prefix_lines; } self.last_visible_transcript_start_line = mapping.line(self.last_visible_transcript_start_line); if (self.last_visible_transcript_split_active) self.last_visible_transcript_split_suffix_start_line = mapping.line(self.last_visible_transcript_split_suffix_start_line); debug_trace.logf("scroll", "transcript_retention_rebase state={s} history={d}->{d} view={d}->{d}", .{ @tagName(diagnostic.state), diagnostic.history_visual_offset, mapping.visual(diagnostic.history_visual_offset), diagnostic.visual_offset, mapping.visual(diagnostic.visual_offset), }); } fn rebaseRetentionSelection(value: ViewportSelection, mapping: source_preparation.RetentionRebase) ViewportSelection { var selection = value; selection.start_line = mapping.line(value.start_line); selection.line_count = mapping.line(value.start_line +| value.line_count) -| selection.start_line; if (value.split_active) { selection.split_prefix_lines = mapping.line(value.split_prefix_lines); selection.split_suffix_start_line = mapping.line(value.split_suffix_start_line); } const old_offsets = mapping.before.transcript_visual_row_offsets; const new_offsets = mapping.retained.transcript_visual_row_offsets; selection.partial_skip_rows = if (value.start_line < old_offsets.len and selection.start_line < new_offsets.len) @intCast(mapping.visual(old_offsets[value.start_line] +| value.partial_skip_rows) -| new_offsets[selection.start_line]) else 0; return selection; } fn rebaseRetentionHistoryOrigin( value: ?transcript_painter.MeasuredHistoryOrigin, mapping: source_preparation.RetentionRebase, ) ?transcript_painter.MeasuredHistoryOrigin { var origin = value orelse return null; if (origin.source_cols != mapping.before.cols) { debug_trace.logf("scroll", "retention dropped measured history origin source_cols={d} retained_cols={d}", .{ origin.source_cols, mapping.before.cols }); return null; } origin.source_visual_offset = mapping.visual(origin.source_visual_offset); if (origin.endpoint) |*endpoint| { endpoint.span_start_flow_offset = mapping.byte(endpoint.span_start_flow_offset); endpoint.flow_offset = mapping.byte(endpoint.flow_offset); } return origin; } pub fn reconcileTranscriptCommitSource( self: *TranscriptRuntime, rendered_flow: []const u8, force_rebase: bool, reason: []const u8, ) void { self.reconcileTranscriptCommitSourceWithRewriteMode( rendered_flow, force_rebase, reason, .strict, ); } pub fn reconcileTranscriptCommitSourceWithRewriteMode( self: *TranscriptRuntime, rendered_flow: []const u8, force_rebase: bool, reason: []const u8, mode: transcript_store.TranscriptSourceRewriteMode, ) void { if (!force_rebase and mode == .preserve_same_epoch) { if (self.committedRetentionIdentity()) |identity| { // Source preparation includes these already-published projections // until a receipt releases them; the recorded store does not. if (identity.publication_entries.len > 0) return; } } switch (self.transcript_commit_state) { .invalid => {}, .stable => |*anchor| { const source_compatible = std.mem.startsWith(u8, rendered_flow, anchor.flow); if (force_rebase or !source_compatible) { if (!force_rebase and mode == .preserve_same_epoch) { if (self.structuredRewritePreserveBlocker(anchor.*, rendered_flow)) |blocker| { debug_trace.logf( "scroll", "transcript_anchor_structured_rewrite_fallback blocker={s} reason={s}", .{ blocker, reason }, ); } else { debug_trace.logf( "scroll", "transcript_anchor_structured_rewrite_preserve stable_start={d} visual_offset={d} reason={s}", .{ anchor.selection.start_line, anchor.visual_offset, reason, }, ); diagnostics.recordRenderEvent( .source_rewrite, "action=preserve reason={s} old_bytes={d} new_bytes={d} view={d} history={d} rows={d} start={d} layout={x} recovery={}->true catchup={}", .{ reason, anchor.flow.len, rendered_flow.len, anchor.visual_offset, anchor.history_visual_offset, anchor.total_visual_rows, anchor.selection.start_line, anchor.layout_id, anchor.normal_buffer_recovery_pending, anchor.history_catchup_pending }, ); anchor.normal_buffer_recovery_pending = true; return; } } self.invalidateTranscriptAnchor(reason); } }, .recovering => |*receipt| { if (!force_rebase and std.mem.startsWith(u8, rendered_flow, receipt.flow)) { return; } receipt.measured_history_origin = null; if (receipt.projection_rebase_pending and !receipt.tracks_semantic_progress and receipt.remaining_semantic_rows == 0 and receipt.remaining_semantic_progress_rows == 0 and !receipt.document_append_committed) { return; } debug_trace.logf( "scroll", "transcript_recovery_source_rebase remaining_resize_reflow_rows={d} dropped_semantic_rows={d} append_committed={s} reason={s}", .{ receipt.remaining_resize_reflow_rows, receipt.remaining_semantic_rows, if (receipt.document_append_committed) "true" else "false", reason, }, ); diagnostics.recordRenderEvent( .source_rewrite, "action=rebase_recovery reason={s} old_bytes={d} new_bytes={d} view={d} history={d} dropped_rows={d} append_committed={}", .{ reason, receipt.flow.len, rendered_flow.len, receipt.accepted_visual_offset, receipt.accepted_history_visual_offset, receipt.remaining_semantic_rows, receipt.document_append_committed }, ); receipt.remaining_semantic_rows = 0; receipt.remaining_semantic_progress_rows = 0; receipt.projection_rebase_pending = true; receipt.tracks_semantic_progress = false; receipt.document_append_committed = false; }, } } fn structuredRewritePreserveBlocker( self: *const TranscriptRuntime, anchor: CommittedTranscriptAnchor, rendered_flow: []const u8, ) ?[]const u8 { if (anchor.flow.len == 0) return "empty_anchor"; if (rendered_flow.len == 0) return "empty_rendered_flow"; if (self.resize_history_row_delta != null) return "resize_history_pending"; if (self.layout.cols == 0 or self.layout.rows == 0) return "empty_runtime_layout"; const committed = self.committed_frame_layout; if (anchor.layout_id != committed.layout_id) return "layout_epoch_mismatch"; if (committed.terminal_cols == 0 or committed.terminal_rows == 0) { return "empty_committed_geometry"; } if (committed.terminal_cols != self.layout.cols or committed.terminal_rows != self.layout.rows) { return "geometry_changed"; } if (committed.transcript_area.isEmpty()) return "empty_transcript_area"; if (anchor.visual_offset > anchor.total_visual_rows) return "invalid_visual_offset"; if (anchor.history_visual_offset > anchor.total_visual_rows) { return "invalid_history_visual_offset"; } return null; } fn materializedTranscriptProjection( self: *const TranscriptRuntime, source_visual_offset: u32, area: render_engine.frame_layout.FrameRect, ) ?MaterializedTranscriptProjection { const area_rows = if (area.isEmpty()) 0 else area.height(); return switch (self.transcript_commit_state) { .invalid => null, .stable => |anchor| if (committedProjectionVisualOffset(anchor) != source_visual_offset) null else .{ .raw_flow_exact = anchor.flow_materialized, .visual_rows = projectionVisualRowsInArea( anchor.total_visual_rows, committedProjectionVisualOffset(anchor), area, ), }, .recovering => |receipt| if (receipt.materialized_visual_offset != source_visual_offset) null else .{ .raw_flow_exact = receipt.materialized_flow_len != null, .visual_rows = @min( receipt.materialized_visual_rows, area_rows, ), }, }; } fn plannedTranscriptRows( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, source_visual_offset: u32, resize_reflow_rows: u16, semantic_rows: u32, semantic_progress_rows: u32, ) u16 { const semantic_cap = if (self.pending_resume_source != null) resume_publication_rows_per_frame else std.math.maxInt(u32); const planned = frameInlineScrollBudgetCapped( resize_reflow_rows, semantic_rows, semantic_cap, ); if (semantic_rows == 0) return planned; const materialized = self.materializedTranscriptProjection( source_visual_offset, prepared.projectionArea(), ); if (materialized) |projection| { if (!projection.raw_flow_exact) { return frameInlineScrollBudgetCapped( resize_reflow_rows, semantic_rows, @min(projection.visual_rows, semantic_cap), ); } } const accepted = acceptedTranscriptRowsForInlineRows( planned, resize_reflow_rows, semantic_rows, semantic_progress_rows, ); if (accepted.semantic_progress_rows == 0) return planned; const accepts_all_semantic_rows = accepted.semantic_rows == semantic_rows and accepted.semantic_progress_rows == semantic_progress_rows; if (accepts_all_semantic_rows) return planned; const exactness = transcript_painter.preparedTranscriptStagedFlowEndExactness( prepared, self.layout.cols, prepared.projectionArea(), source_visual_offset + accepted.semantic_progress_rows, ); if (exactness == null or exactness.?) return planned; const projection = materialized orelse return planned; return frameInlineScrollBudgetCapped( resize_reflow_rows, semantic_rows, @min(projection.visual_rows, semantic_cap), ); } pub fn planTranscriptScroll( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, ) TranscriptScrollFacts { return self.planTranscriptScrollForFrame(prepared, false, false); } /// Selects the finality floor in flow bytes from the prepared paint's /// activity-independent candidates plus the frame-fresh producer facts /// (lifecycle watermark, assistant-tail writability, replaceable tail). /// Null means the whole flow is final. Pure over its inputs, so repeated /// calls within one frame agree. fn finalityFloorBytes( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, ) ?usize { return self.transcript_release.finality_floor( prepared.finality_candidates, self.finalizedToolTurnWatermark(), if (prepared.replaceable_last_line) prepared.replaceable_start else null, ); } /// Visual rows contributed by the flow prefix [0..flow_offset). The /// prefix boundary always lands on an entry start, which is a hard-line /// start; a mid-line offset rounds down to its line, the safe direction. fn preparedVisualRowsForFlowOffset( prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, flow_offset: usize, ) u32 { if (flow_offset == 0) return 0; if (flow_offset >= prepared.bytes.len) return prepared.sourceTotalVisualRows(); const lines = prepared.sourceVisibleLines(); const visual_rows = prepared.sourceVisualRows(); var rows: u32 = 0; for (lines, 0..) |line, index| { switch (line) { .transcript => |t| if (t.ref.ref.start >= flow_offset) return rows, .folded_line => {}, } if (index < visual_rows.len) rows += visual_rows[index]; } return rows; } /// The largest visual offset whose rows are all backed by final entries, /// clamped to the frame's target offset. Release permission never /// extends past this value. fn finalityReleasableOffset( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, target_offset: u32, ) u32 { const floor_bytes = self.finalityFloorBytes(prepared) orelse return target_offset; const floor_rows = preparedVisualRowsForFlowOffset(prepared, floor_bytes); return @min(target_offset, floor_rows); } pub fn planTranscriptScrollForFrame( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, footer_reservation_changed: bool, replay_displaced_footer_history: bool, ) TranscriptScrollFacts { var facts = self.planTranscriptScrollForFrameUnfiltered(prepared, footer_reservation_changed, replay_displaced_footer_history); if (self.committedRetentionIdentity()) |identity| { const restore_projection = self.publication_projection_invalid or self.committed_frame_layout.terminal_cols != self.layout.cols or self.committed_frame_layout.terminal_rows != self.layout.rows or facts.recovery_rebase or facts.recovery_projection_deferred; if (identity.publication_entries.len > 0 and (restore_projection or facts.finality_hold) and !self.terminal_reset_pending) { facts.publication_hold = if (restore_projection) .restore else .finality; if (restore_projection) { facts.semantic_rows = 0; facts.semantic_progress_rows = 0; facts.planned_rows = self.plannedTranscriptRows(prepared, facts.source_visual_offset, facts.resize_reflow_rows, 0, 0); } } } return facts; } fn planTranscriptScrollForFrameUnfiltered( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, footer_reservation_changed: bool, replay_displaced_footer_history: bool, ) TranscriptScrollFacts { const target_offset = prepared.sourceVisualOffset(); const target_total_visual_rows = prepared.sourceTotalVisualRows(); if (self.terminal_reset_pending) { debug_trace.logf( "scroll", "transcript_transition_plan source_state=reset target_visual_offset={d} target_total_visual_rows={d} planned_rows=0", .{ target_offset, target_total_visual_rows }, ); return .{ .target_visual_offset = target_offset, .recovery_rebase = true, }; } const anchor = switch (self.transcript_commit_state) { .stable => |value| value, .recovering => |receipt| { const source_equal = std.mem.eql(u8, prepared.bytes, receipt.flow); const source_prefix_growth = prepared.bytes.len > receipt.flow.len and std.mem.startsWith(u8, prepared.bytes, receipt.flow); const source_compatible = source_equal or source_prefix_growth; const source_visual_compatible = if (source_equal) target_total_visual_rows == receipt.attempt_total_visual_rows else if (source_prefix_growth) target_total_visual_rows >= receipt.attempt_total_visual_rows else false; const resize_history_compatible = self.resize_history_row_delta == null or (!receipt.attempt_resize_history_committed and self.resize_history_row_delta == receipt.attempt_resize_history_row_delta); const recovery_progress_compatible = source_compatible and source_visual_compatible and receipt.attempt_cols == self.layout.cols and resize_history_compatible; const recovery_rebase = receipt.projection_rebase_pending or !recovery_progress_compatible; const recovery_tracks_semantic_progress = receipt.tracks_semantic_progress and source_compatible; const growth_rows: u32 = if (recovery_progress_compatible and recovery_tracks_semantic_progress and source_prefix_growth) target_total_visual_rows - receipt.attempt_total_visual_rows else 0; const measured_resize_rebase = self.resize_history_row_delta != null and prepared.measured_resize_target_provenance == .recovering; const resize_reflow_rows = if (measured_resize_rebase) 0 else receipt.remaining_resize_reflow_rows; const retained_semantic_rows = if (source_compatible) receipt.remaining_semantic_rows else 0; const retained_semantic_progress_rows = if (recovery_progress_compatible and recovery_tracks_semantic_progress) receipt.remaining_semantic_progress_rows else 0; const candidate_semantic_rows = retained_semantic_rows + growth_rows; const candidate_semantic_progress_rows = retained_semantic_progress_rows + growth_rows; // Retained debt repairs the attempted frame. Only source // growth is a new content release that needs finality. const semantic_rows, const semantic_progress_rows = if (self.finalityFloorBytes(prepared) != null) bounded: { const releasable_offset = self.finalityReleasableOffset( prepared, target_offset, ); const bounded_growth_rows = @min( growth_rows, releasable_offset -| (receipt.accepted_history_visual_offset + retained_semantic_rows), ); const bounded_rows = retained_semantic_rows + bounded_growth_rows; const bounded_progress_growth_rows = @min( growth_rows, releasable_offset -| (receipt.accepted_visual_offset + retained_semantic_progress_rows), ); break :bounded .{ bounded_rows, @min( retained_semantic_progress_rows + bounded_progress_growth_rows, bounded_rows, ), }; } else .{ candidate_semantic_rows, candidate_semantic_progress_rows, }; const acknowledged_visual_offset = receipt.accepted_visual_offset + semantic_progress_rows; const recovery_projection_deferred = recovery_progress_compatible and recovery_tracks_semantic_progress and transcript_painter.preparedTranscriptProjectionExceedsArea( prepared, prepared.projectionArea(), acknowledged_visual_offset, ); const planned_rows = if (recovery_projection_deferred) 0 else self.plannedTranscriptRows( prepared, receipt.accepted_visual_offset, resize_reflow_rows, semantic_rows, semantic_progress_rows, ); debug_trace.logf( "scroll", "transcript_transition_plan source_state=recovering source_visual_offset={d} attempt_visual_offset={d} target_visual_offset={d} attempt_total_visual_rows={d} target_total_visual_rows={d} remaining_resize_reflow_rows={d} remaining_semantic_rows={d} remaining_semantic_progress_rows={d} growth_rows={d} semantic_progress_rows={d} planned_rows={d} source_compatible={s} recovery_progress_compatible={s} recovery_rebase={s} recovery_projection_deferred={s}", .{ receipt.accepted_visual_offset, receipt.attempt_visual_offset, target_offset, receipt.attempt_total_visual_rows, target_total_visual_rows, receipt.remaining_resize_reflow_rows, receipt.remaining_semantic_rows, receipt.remaining_semantic_progress_rows, growth_rows, semantic_progress_rows, planned_rows, if (source_compatible) "true" else "false", if (recovery_progress_compatible) "true" else "false", if (recovery_rebase) "true" else "false", if (recovery_projection_deferred) "true" else "false", }, ); return .{ .source_visual_offset = receipt.accepted_visual_offset, .target_visual_offset = target_offset, .resize_reflow_rows = resize_reflow_rows, .semantic_rows = semantic_rows, .semantic_progress_rows = semantic_progress_rows, .planned_rows = planned_rows, .source_compatible = source_compatible, .recovery_rebase = recovery_rebase, .recovery_progress_compatible = recovery_progress_compatible, .recovery_tracks_semantic_progress = recovery_tracks_semantic_progress, .recovery_projection_deferred = recovery_projection_deferred, }; }, .invalid => { // Materialize authoritative pre-viewport rows into native scrollback. if (self.pendingResumeFlow().len > 0) { const semantic_rows: u32 = frameInlineScrollBudget(0, target_offset); return .{ .target_visual_offset = target_offset, .semantic_rows = semantic_rows, .semantic_progress_rows = semantic_rows, .planned_rows = self.plannedTranscriptRows( prepared, 0, 0, semantic_rows, semantic_rows, ), .source_compatible = true, .recovery_progress_compatible = true, .recovery_tracks_semantic_progress = true, }; } return .{ .target_visual_offset = target_offset, .recovery_rebase = true, }; }, }; const source_equal = std.mem.eql(u8, prepared.bytes, anchor.flow); const source_prefix_growth = prepared.bytes.len > anchor.flow.len and std.mem.startsWith(u8, prepared.bytes, anchor.flow); const source_compatible = source_equal or source_prefix_growth; const source_visual_growth = source_prefix_growth and target_total_visual_rows > anchor.total_visual_rows; const width_stable = self.committed_frame_layout.terminal_cols == 0 or self.committed_frame_layout.terminal_cols == self.layout.cols; const geometry_rebase = footer_reservation_changed and width_stable and self.resize_history_row_delta == null; const displaced_history_replay = if (geometry_rebase and replay_displaced_footer_history) compatibleHistoryReplayRange( prepared, self.layout.cols, prepared.bytes, anchor.flow, anchor.history_visual_offset, target_offset, ) else null; const recovery_rebase = (!source_compatible and !geometry_rebase) or !width_stable or self.resize_history_row_delta != null; // Release requires two independent facts: permission (the rows are // final, expressed by the finality floor) and ability (this frame // materializes their exact bytes, which an incompatible frame // cannot). Either fact missing forces zero release; the frame // re-anchors on the new flow and the derived history debt settles // on the next compatible frame through the existing replay. const releasable_offset = self.finalityReleasableOffset(prepared, target_offset); const releasable_advance = releasable_offset -| anchor.history_visual_offset; const committed_projection_offset = committedProjectionVisualOffset(anchor); const geometry_history_rows: u32 = if (geometry_rebase and ((source_compatible and source_visual_growth) or displaced_history_replay != null)) target_offset -| anchor.history_visual_offset else 0; // Geometry displacement (footer growth over transcript rows) keeps // its pre-existing semantics: displaced rows are preserved into // history even when mutable, because inline rendering has nowhere // else to keep them. The finality clamp governs content-driven // release only. const semantic_rows: u32 = if (geometry_rebase) geometry_history_rows else if (!recovery_rebase) // Normal-buffer recovery restores rows the takeover already // displaced into scrollback; that restore is display repair, // not a content release, so the finality clamp does not apply. if (anchor.normal_buffer_recovery_pending) target_offset -| anchor.history_visual_offset else if (anchor.history_catchup_pending) releasable_advance else // Rows leaving the screen top are [visual..visual+scroll); // the floor clamps that range, so the budget is measured // from the committed viewport, not from history. releasable_offset -| anchor.visual_offset else if (self.compatibleCommittedHistoryReplayRange( prepared, anchor, prepared.bytes, releasable_offset, ) != null) releasable_advance else 0; // A finality hold is either the floor sitting below the viewport // (mutable rows above the top) or a same-geometry incompatible frame // that had permission to release but no ability to materialize; // geometry-driven recovery (resize, reflow) owns its own debt. const finality_hold = releasable_offset < target_offset or (recovery_rebase and width_stable and self.committed_frame_layout.terminal_rows == self.layout.rows and self.resize_history_row_delta == null and releasable_offset > anchor.history_visual_offset +| semantic_rows); const semantic_progress_rows: u32 = if (source_compatible or geometry_rebase) semantic_rows else 0; const resize_reflow_candidate = self.calculateAnchorResizeReflowScrollRows(prepared.selection); const measured_boundary_row_pending = self.resize_history_row_delta != null and self.fullTranscriptActive() and resize_reflow_candidate == 1; const resize_reflow_rows = if (self.resize_history_row_delta == null or measured_boundary_row_pending) resize_reflow_candidate else 0; const planned = self.plannedTranscriptRows( prepared, committed_projection_offset, resize_reflow_rows, semantic_rows, semantic_rows, ); if (semantic_rows > releasable_advance) { diagnostics.recordRenderEvent( .transition, "release_past_finality view={d}->{d} history={d} releasable={d} semantic={d} planned={d} compatible={} geometry={} recovery_rebase={} restore={} catchup={}", .{ committed_projection_offset, target_offset, anchor.history_visual_offset, releasable_offset, semantic_rows, planned, source_compatible, geometry_rebase, recovery_rebase, anchor.normal_buffer_recovery_pending, anchor.history_catchup_pending }, ); } debug_trace.logf( "scroll", "transcript_transition_plan source_state=stable source_start={d} target_start={d} source_visual_offset={d} source_history_visual_offset={d} target_visual_offset={d} releasable_visual_offset={d} source_total_visual_rows={d} target_total_visual_rows={d} resize_reflow_candidate={d} resize_reflow_rows={d} semantic_rows={d} planned_rows={d} width_stable={s} source_compatible={s} footer_reservation_changed={s} replay_displaced_footer_history={s} geometry_rebase={s} recovery_rebase={s}", .{ anchor.selection.start_line, prepared.selection.start_line, anchor.visual_offset, anchor.history_visual_offset, target_offset, releasable_offset, anchor.total_visual_rows, target_total_visual_rows, resize_reflow_candidate, resize_reflow_rows, semantic_rows, planned, if (width_stable) "true" else "false", if (source_compatible) "true" else "false", if (footer_reservation_changed) "true" else "false", if (replay_displaced_footer_history) "true" else "false", if (geometry_rebase) "true" else "false", if (recovery_rebase) "true" else "false", }, ); return .{ .source_visual_offset = committed_projection_offset, .target_visual_offset = target_offset, .resize_reflow_rows = resize_reflow_rows, .semantic_rows = semantic_rows, .semantic_progress_rows = semantic_progress_rows, .planned_rows = planned, .source_compatible = source_compatible, .geometry_rebase = geometry_rebase, .recovery_rebase = recovery_rebase, .finality_hold = finality_hold, .recovery_progress_compatible = !recovery_rebase, .recovery_tracks_semantic_progress = source_compatible or geometry_rebase, }; } fn calculateAnchorResizeReflowScrollRows( self: *const TranscriptRuntime, next_selection: ViewportSelection, ) u16 { const anchor = switch (self.transcript_commit_state) { .stable => |value| value, .invalid, .recovering => return 0, }; const committed = self.committed_frame_layout; if (anchor.layout_id != committed.layout_id or !self.viewport_clear_pending or self.owned_top_row != 1 or committed.transcript_area.top != 1 or next_selection.top_row != 1 or next_selection.bottom_row < next_selection.top_row or self.layout.cols == 0 or self.layout.rows == 0 or committed.terminal_cols == 0 or committed.terminal_rows == 0 or self.layout.cols >= committed.terminal_cols) { return 0; } const shadow = self.shadow_vt orelse return 0; if (shadow.cols != committed.terminal_cols or shadow.rows != committed.terminal_rows or committed.transcript_area.isEmpty() or shadow.cursor_row <= committed.transcript_area.bottom) { return 0; } const cursor_reflowed_row = reflowedRows( shadow, 1, shadow.cursor_row, self.layout.cols, true, ); if (cursor_reflowed_row <= self.layout.rows) return 0; const chrome_rows = reflowedRows( shadow, committed.transcript_area.bottom + 1, shadow.cursor_row, self.layout.cols, true, ); const physical_transcript_rows = @as(u32, self.layout.rows) -| chrome_rows; const target_transcript_rows = @as(u32, next_selection.bottom_row) - next_selection.top_row + 1; return @intCast(@min( physical_transcript_rows -| target_transcript_rows, @as(u32, std.math.maxInt(u16)), )); } fn layoutForTranscriptProjection( layout: Layout, target_area: render_engine.frame_layout.FrameRect, ) !Layout { if (target_area.bottom > layout.rows) { return error.InvalidTranscriptTransition; } var projection_layout = layout; projection_layout.content_bottom = @max( projection_layout.content_bottom, target_area.bottom, ); return projection_layout; } const TransitionTarget = struct { body_disposition: TranscriptBodyDisposition = .paint, selection: ViewportSelection, visual_offset: u32, history_visual_offset: u32, source_endpoint_visual_offset: u32, total_visual_rows: u32, cursor_row: u16, cursor_col: u16, measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin, normal_buffer_recovery_pending: bool = false, history_catchup_pending: bool = false, staged_flow_end: ?usize = null, projection_staged: bool = false, // The frame slides the viewport past withheld release with an // in-place repaint. Any document append is bounded separately to // the rows the frame may release. hold_staged: bool = false, fn init( prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, scroll_facts: TranscriptScrollFacts, ) TransitionTarget { return .{ .selection = prepared.selection, .visual_offset = scroll_facts.target_visual_offset, .history_visual_offset = scroll_facts.target_visual_offset, .source_endpoint_visual_offset = scroll_facts.target_visual_offset, .total_visual_rows = prepared.sourceTotalVisualRows(), .cursor_row = prepared.cursor.cursor_row, .cursor_col = prepared.cursor.cursor_col, .measured_history_origin = prepared.measured_history_origin, }; } fn retainCommittedAnchor( self: *TransitionTarget, anchor: CommittedTranscriptAnchor, retained: RetainedTranscriptBody, ) void { self.body_disposition = .{ .retain_committed = retained }; self.selection = anchor.selection; self.visual_offset = anchor.visual_offset; self.history_visual_offset = anchor.history_visual_offset; self.normal_buffer_recovery_pending = anchor.normal_buffer_recovery_pending; self.total_visual_rows = anchor.total_visual_rows; self.cursor_row = anchor.cursor_row; self.cursor_col = anchor.cursor_col; } fn usePreparedProjection( self: *TransitionTarget, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, ) void { self.selection = prepared.selection; self.cursor_row = prepared.cursor.cursor_row; self.cursor_col = prepared.cursor.cursor_col; } fn stagePreparedProjection( self: *TransitionTarget, alloc: Allocator, layout: Layout, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_area: render_engine.frame_layout.FrameRect, visual_offset: u32, ) !void { const projection_layout = try layoutForTranscriptProjection(layout, target_area); self.visual_offset = visual_offset; if (visual_offset == self.total_visual_rows) { try transcript_painter.reprojectPreparedTranscriptForVisualOffset( alloc, projection_layout, prepared, target_area, visual_offset, ); self.staged_flow_end = transcript_painter.preparedTranscriptFlowOffsetForVisualOffset( prepared, layout.cols, visual_offset, ); } else { const staged = try transcript_painter.stagePreparedTranscriptForVisualOffset( alloc, projection_layout, prepared, target_area, visual_offset, ); self.staged_flow_end = staged.flow_end; } self.projection_staged = true; self.usePreparedProjection(prepared); } fn stagePreparedHistoryProjection( self: *TransitionTarget, alloc: Allocator, layout: Layout, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_area: render_engine.frame_layout.FrameRect, ) !void { const logical_visual_offset = self.visual_offset; const previous_start_line = self.selection.start_line; try self.stagePreparedProjection( alloc, layout, prepared, target_area, self.history_visual_offset, ); self.visual_offset = logical_visual_offset; self.source_endpoint_visual_offset = self.history_visual_offset; diagnostics.recordRenderEvent( .history_projection, "logical_view={d} history={d} start={d}->{d} endpoint={d} cursor={d},{d} terminal={d}x{d} area={d}..{d}", .{ logical_visual_offset, self.history_visual_offset, previous_start_line, self.selection.start_line, self.source_endpoint_visual_offset, self.cursor_row, self.cursor_col, layout.cols, layout.rows, target_area.top, target_area.bottom }, ); } }; pub const ResolvedTranscriptTarget = struct { target: TransitionTarget, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, scroll_facts: TranscriptScrollFacts, accepted: AcceptedTranscriptRows, source_endpoint_cursor_row: u16, source_endpoint_cursor_col: u16, destructive_invalidation: bool, activity_overlay_active: bool, borrows_pending_resume: bool, pub fn selection(self: ResolvedTranscriptTarget) ViewportSelection { return self.target.selection; } pub fn cursorRow(self: ResolvedTranscriptTarget) u16 { return self.target.cursor_row; } pub fn cursorCol(self: ResolvedTranscriptTarget) u16 { return self.target.cursor_col; } pub fn bodyDisposition(self: ResolvedTranscriptTarget) TranscriptBodyDisposition { return self.target.body_disposition; } pub fn occupiedTranscriptRows(self: ResolvedTranscriptTarget) u16 { const area = self.target_layout.transcript_area; if (area.isEmpty() or self.target.selection.last_visible_row < area.top) return 0; return self.target.selection.last_visible_row - area.top + 1; } pub fn applyToPaintPlan( self: ResolvedTranscriptTarget, plan: *render_engine.paint_plan.PaintPlan, ) void { plan.viewport = self.target.selection; if (plan.cursor_target) |*cursor| { cursor.row = self.target.cursor_row; cursor.col = self.target.cursor_col; } } }; fn stableRetainedTransitionBody( self: *const TranscriptRuntime, anchor: CommittedTranscriptAnchor, source_bytes: []const u8, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, destructive_invalidation: bool, activity_overlay_active: bool, ) ?RetainedTranscriptBody { if (!anchor.flow_materialized) return null; return render_engine.frame_retention.stableRetainedTranscriptBody(.{ .full_transcript_active = self.fullTranscriptActive(), .committed_layout_id = anchor.layout_id, .committed_flow = anchor.flow, .committed_occupied_last_row = anchor.occupied_last_row, .source_layout = self.committed_frame_layout, .source_bytes = source_bytes, .target_layout = target_layout, .scroll_plan = scroll_plan, .destructive_invalidation = destructive_invalidation, .activity_overlay_active = activity_overlay_active, }); } fn paintedStableTargetWouldRewind( self: *const TranscriptRuntime, target: TransitionTarget, anchor: CommittedTranscriptAnchor, scroll_facts: TranscriptScrollFacts, destructive_invalidation: bool, ) bool { const same_width = self.committed_frame_layout.terminal_cols == self.layout.cols; return !self.fullTranscriptActive() and target.body_disposition == .paint and scroll_facts.source_compatible and !scroll_facts.geometry_rebase and same_width and self.resize_history_row_delta == null and !destructive_invalidation and target.visual_offset >= target.history_visual_offset and scroll_facts.target_visual_offset < anchor.visual_offset; } fn semanticProgressNeedsStaging( target: TransitionTarget, scroll_facts: TranscriptScrollFacts, accepted_semantic_progress_rows: u32, ) bool { return target.body_disposition == .paint and !scroll_facts.recovery_rebase and scroll_facts.recovery_tracks_semantic_progress and accepted_semantic_progress_rows < scroll_facts.semantic_progress_rows; } fn stableHistoryFloor( self: *const TranscriptRuntime, target: TransitionTarget, anchor: CommittedTranscriptAnchor, scroll_facts: TranscriptScrollFacts, ) ?u32 { const stable_terminal_geometry = self.committed_frame_layout.terminal_cols == self.layout.cols and self.committed_frame_layout.terminal_rows == self.layout.rows; if (self.fullTranscriptActive() or target.body_disposition != .paint or !stable_terminal_geometry or self.resize_history_row_delta != null) { return null; } if (target.visual_offset < target.history_visual_offset) { return target.history_visual_offset; } const committed_offset = committedProjectionVisualOffset(anchor); if (!scroll_facts.source_compatible and target.visual_offset < committed_offset and target.total_visual_rows > committed_offset) { return committed_offset; } return null; } fn recoveringCanAdvanceSemanticProgress(scroll_facts: TranscriptScrollFacts) bool { return scroll_facts.recovery_progress_compatible and scroll_facts.recovery_tracks_semantic_progress and !scroll_facts.recovery_projection_deferred; } fn resolveTransitionTarget( self: *const TranscriptRuntime, alloc: Allocator, source_bytes: []const u8, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, projection_area: render_engine.frame_layout.FrameRect, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, scroll_facts: TranscriptScrollFacts, accepted_semantic_rows: u32, accepted_semantic_progress_rows: u32, destructive_invalidation: bool, activity_overlay_active: bool, ) !TransitionTarget { var target = TransitionTarget.init(prepared, scroll_facts); switch (self.transcript_commit_state) { .stable => |anchor| { target.history_visual_offset = @min( target.total_visual_rows, anchor.history_visual_offset +| accepted_semantic_rows, ); target.normal_buffer_recovery_pending = anchor.normal_buffer_recovery_pending; target.history_catchup_pending = anchor.history_catchup_pending; // A retained body advances only by the released rows. When // the finality clamp withholds release from the plan itself // (as opposed to a partial acceptance, which the recovery // debt machinery owns), the viewport must still follow the // flow, so the frame repaints instead of retaining. const plan_covers_advance = scroll_facts.target_visual_offset -| committedProjectionVisualOffset(anchor) <= scroll_facts.semantic_rows; if (!scroll_facts.geometry_rebase and plan_covers_advance) { if (self.stableRetainedTransitionBody( anchor, source_bytes, target_layout, scroll_plan, destructive_invalidation, activity_overlay_active, )) |retained| { target.retainCommittedAnchor(anchor, retained); } } if (self.paintedStableTargetWouldRewind( target, anchor, scroll_facts, destructive_invalidation, )) { diagnostics.recordRenderEvent( .transition, "rejected=rewind view={d}->{d} history={d}->{d} requested_view={d} compatible={} geometry={} restore={} catchup={}", .{ anchor.visual_offset, target.visual_offset, anchor.history_visual_offset, target.history_visual_offset, scroll_facts.target_visual_offset, scroll_facts.source_compatible, scroll_facts.geometry_rebase, target.normal_buffer_recovery_pending, target.history_catchup_pending }, ); return error.InvalidTranscriptTransition; } if (semanticProgressNeedsStaging( target, scroll_facts, accepted_semantic_progress_rows, )) { try target.stagePreparedProjection( alloc, self.layout, prepared, projection_area, scroll_facts.source_visual_offset + accepted_semantic_progress_rows, ); } if (target.body_disposition == .paint and target.normal_buffer_recovery_pending and scroll_facts.source_compatible and target.visual_offset > target.history_visual_offset) { try target.stagePreparedHistoryProjection( alloc, self.layout, prepared, projection_area, ); } if (target.normal_buffer_recovery_pending and scroll_facts.geometry_rebase and target.history_visual_offset >= target.visual_offset) { target.normal_buffer_recovery_pending = false; } if (!target.projection_staged and target.body_disposition == .paint and !scroll_facts.geometry_rebase and !scroll_facts.recovery_rebase and !self.fullTranscriptActive() and !prepared.selection.split_active and !projection_area.isEmpty() and scroll_facts.target_visual_offset -| committedProjectionVisualOffset(anchor) > scroll_facts.semantic_rows and scroll_facts.target_visual_offset >= target.history_visual_offset) { // The viewport advanced further than the plan may // release: slide the window with an in-place repaint at // the target offset. The rows passed over stay // unreleased and settle later through the replay. try target.stagePreparedProjection( alloc, self.layout, prepared, projection_area, scroll_facts.target_visual_offset, ); target.hold_staged = true; debug_trace.logf( "scroll", "transcript_transition_hold_release target_visual_offset={d} history_visual_offset={d} semantic_rows={d}", .{ scroll_facts.target_visual_offset, target.history_visual_offset, scroll_facts.semantic_rows, }, ); } if (self.stableHistoryFloor(target, anchor, scroll_facts)) |history_floor| { try target.stagePreparedProjection( alloc, self.layout, prepared, projection_area, history_floor, ); target.source_endpoint_visual_offset = history_floor; debug_trace.logf( "scroll", "transcript_transition_hold_history_floor source_visual_offset={d} target_visual_offset={d}", .{ history_floor, scroll_facts.target_visual_offset }, ); } if (scroll_facts.geometry_rebase) { target.history_catchup_pending = target.visual_offset > target.history_visual_offset; } else if (target.history_visual_offset >= target.visual_offset) { target.history_catchup_pending = false; } else if (scroll_facts.finality_hold) { // Finality withheld release. Preserve the obligation on // stable anchors until history catches the viewport; // footer displacement alone never creates one. target.history_catchup_pending = true; } }, .recovering => |receipt| { target.history_visual_offset = @min( target.total_visual_rows, receipt.accepted_history_visual_offset +| accepted_semantic_rows, ); target.normal_buffer_recovery_pending = receipt.normal_buffer_recovery_pending; if (scroll_facts.publication_hold == .none and recoveringCanAdvanceSemanticProgress(scroll_facts)) { target.visual_offset = scroll_facts.source_visual_offset + accepted_semantic_progress_rows; if (accepted_semantic_progress_rows < scroll_facts.semantic_progress_rows) { try target.stagePreparedProjection( alloc, self.layout, prepared, projection_area, target.visual_offset, ); } else { const projection_layout = try layoutForTranscriptProjection( self.layout, projection_area, ); try transcript_painter.reprojectPreparedTranscriptForVisualOffset( alloc, projection_layout, prepared, projection_area, target.visual_offset, ); target.usePreparedProjection(prepared); } } }, .invalid => if (semanticProgressNeedsStaging( target, scroll_facts, accepted_semantic_progress_rows, )) { try target.stagePreparedProjection( alloc, self.layout, prepared, projection_area, accepted_semantic_progress_rows, ); }, } if (scroll_facts.publication_hold != .none) { try target.stagePreparedProjection(alloc, self.layout, prepared, projection_area, target.history_visual_offset); target.source_endpoint_visual_offset = target.history_visual_offset; target.history_catchup_pending = scroll_facts.target_visual_offset > target.history_visual_offset; target.normal_buffer_recovery_pending = scroll_facts.publication_hold == .restore and target.history_catchup_pending; } if (target.measured_history_origin == null and !prepared.measured_history_origin_consumed and scroll_facts.source_compatible and !destructive_invalidation) { target.measured_history_origin = switch (self.transcript_commit_state) { .invalid => null, .stable => |anchor| anchor.measured_history_origin, .recovering => |receipt| receipt.measured_history_origin, }; } if (!scroll_facts.source_compatible) { target.measured_history_origin = null; } return target; } const TransitionHistoryReplay = struct { flow_start: usize, flow_end: usize, clear_rows: u16, }; fn committedLayoutMatches( self: *const TranscriptRuntime, anchor: CommittedTranscriptAnchor, ) bool { return anchor.layout_id == self.committed_frame_layout.layout_id and self.committed_frame_layout.terminal_cols == self.layout.cols and self.committed_frame_layout.terminal_rows == self.layout.rows; } fn compatibleHistoryReplayRange( prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, cols: u16, target_flow: []const u8, committed_flow: []const u8, source_history_visual_offset: u32, target_history_visual_offset: u32, ) ?TransitionHistoryReplay { if (target_history_visual_offset <= source_history_visual_offset) return null; const flow_start = transcript_painter.preparedTranscriptFlowOffsetForVisualOffset( prepared, cols, source_history_visual_offset, ) orelse return null; const flow_end = transcript_painter.preparedTranscriptFlowOffsetForVisualOffset( prepared, cols, target_history_visual_offset, ) orelse return null; if (flow_end <= flow_start or flow_start > committed_flow.len) return null; const overlap_end = @min(flow_end, committed_flow.len); if (overlap_end > target_flow.len or !std.mem.eql( u8, target_flow[0..overlap_end], committed_flow[0..overlap_end], )) { return null; } const clear_rows = std.math.cast( u16, target_history_visual_offset - source_history_visual_offset, ) orelse return null; return .{ .flow_start = flow_start, .flow_end = flow_end, .clear_rows = clear_rows, }; } fn compatibleCommittedHistoryReplayRange( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, anchor: CommittedTranscriptAnchor, target_flow: []const u8, target_history_visual_offset: u32, ) ?TransitionHistoryReplay { if (!self.committedLayoutMatches(anchor)) return null; if (self.resize_history_row_delta != null) return null; return compatibleHistoryReplayRange( prepared, self.layout.cols, target_flow, anchor.flow, anchor.history_visual_offset, target_history_visual_offset, ); } const TransitionAppendBase = struct { flow_len: usize, history_replay: ?TransitionHistoryReplay = null, visual_offset: u32, visual_rows: u16, cursor_row: u16, cursor_col: u16, }; fn resolveTransitionAppendBase( self: *const TranscriptRuntime, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, prior_owned_top: u16, scroll_facts: TranscriptScrollFacts, destructive_invalidation: bool, target_flow: []const u8, target_history_visual_offset: u32, ) !?TransitionAppendBase { switch (self.transcript_commit_state) { .stable => |anchor| { if (!destructive_invalidation) { const replay_from_history = target_history_visual_offset > anchor.history_visual_offset and (scroll_facts.geometry_rebase or anchor.normal_buffer_recovery_pending or anchor.history_catchup_pending); if (replay_from_history) { const replay = self.compatibleCommittedHistoryReplayRange( prepared, anchor, target_flow, target_history_visual_offset, ); if (replay) |history_replay| { return .{ .flow_len = history_replay.flow_start, .history_replay = history_replay, .visual_offset = anchor.history_visual_offset, .visual_rows = projectionVisualRowsInArea( anchor.total_visual_rows, anchor.history_visual_offset, self.committed_frame_layout.transcript_area, ), .cursor_row = self.committed_frame_layout.transcript_area.top, .cursor_col = 1, }; } } if (anchor.flow_materialized and self.committedLayoutMatches(anchor) and target_flow.len >= anchor.flow.len and std.mem.startsWith(u8, target_flow, anchor.flow)) { return .{ .flow_len = anchor.flow.len, .visual_offset = committedProjectionVisualOffset(anchor), .visual_rows = projectionVisualRowsInArea( anchor.total_visual_rows, committedProjectionVisualOffset(anchor), self.committed_frame_layout.transcript_area, ), .cursor_row = anchor.cursor_row, .cursor_col = anchor.cursor_col, }; } } }, .recovering => |receipt| { if (receipt.retention_identity.publication_entries.len > 0 and self.committed_frame_layout.terminal_cols == self.layout.cols and self.committed_frame_layout.terminal_rows == self.layout.rows and !self.terminal_reset_pending) { if (compatibleHistoryReplayRange(prepared, self.layout.cols, target_flow, receipt.flow, receipt.accepted_history_visual_offset, target_history_visual_offset)) |replay| { return .{ .flow_len = replay.flow_start, .history_replay = replay, .visual_offset = receipt.accepted_history_visual_offset, .visual_rows = projectionVisualRowsInArea(receipt.attempt_total_visual_rows, receipt.accepted_history_visual_offset, self.committed_frame_layout.transcript_area), .cursor_row = self.committed_frame_layout.transcript_area.top, .cursor_col = 1, }; } } if (scroll_facts.recovery_progress_compatible and scroll_facts.recovery_tracks_semantic_progress and !scroll_facts.recovery_rebase and self.committed_frame_layout.terminal_cols == self.layout.cols and self.committed_frame_layout.terminal_rows == self.layout.rows and receipt.materialized_flow_len != null) { const materialized_flow_len = receipt.materialized_flow_len.?; if (recoveryAppendPrefixMatches( target_flow, receipt, materialized_flow_len, )) { return .{ .flow_len = materialized_flow_len, .visual_offset = receipt.materialized_visual_offset, .visual_rows = receipt.materialized_visual_rows, .cursor_row = receipt.cursor_row, .cursor_col = receipt.cursor_col, }; } } }, .invalid => if (self.pendingResumeFlow().len > 0 and !destructive_invalidation) { return .{ .flow_len = 0, .visual_offset = 0, .visual_rows = 0, .cursor_row = prior_owned_top, .cursor_col = 1, }; }, } return null; } const PlannedHistoryMaterialization = struct { visual_offset: u32, normal_buffer_recovery_pending: bool, }; fn resolvePlannedHistoryMaterialization( source_visual_offset: u32, target_visual_offset: u32, target_projection_offset: u32, document_append_planned: bool, recovery_pending: bool, ) PlannedHistoryMaterialization { if (!recovery_pending) { return .{ .visual_offset = target_visual_offset, .normal_buffer_recovery_pending = false, }; } if (target_visual_offset <= source_visual_offset) { return .{ .visual_offset = target_visual_offset, .normal_buffer_recovery_pending = recovery_pending, }; } if (document_append_planned) { return .{ .visual_offset = target_visual_offset, .normal_buffer_recovery_pending = recovery_pending and target_visual_offset < target_projection_offset, }; } return .{ .visual_offset = source_visual_offset, .normal_buffer_recovery_pending = true, }; } test "history offset advances only when transcript bytes are materialized" { const deferred = resolvePlannedHistoryMaterialization( 4, 12, 12, false, true, ); try std.testing.expectEqual(@as(u32, 4), deferred.visual_offset); try std.testing.expect(deferred.normal_buffer_recovery_pending); const caught_up = resolvePlannedHistoryMaterialization( 4, 12, 12, true, true, ); try std.testing.expectEqual(@as(u32, 12), caught_up.visual_offset); try std.testing.expect(!caught_up.normal_buffer_recovery_pending); const bounded = resolvePlannedHistoryMaterialization( 4, 8, 12, true, true, ); try std.testing.expectEqual(@as(u32, 8), bounded.visual_offset); try std.testing.expect(bounded.normal_buffer_recovery_pending); } fn resolveMaterializedEndpoint( target: TransitionTarget, source_endpoint_cursor_row: u16, source_endpoint_cursor_col: u16, append_base: ?TransitionAppendBase, projected_flow_end: ?usize, target_flow_len: usize, transcript_area: render_engine.frame_layout.FrameRect, ) MaterializedEndpoint { var endpoint: MaterializedEndpoint = .{ .visual_offset = target.history_visual_offset, .visual_rows = projectionVisualRowsInArea( target.total_visual_rows, target.history_visual_offset, transcript_area, ), .flow_len = projected_flow_end, .cursor_row = target.cursor_row, .cursor_col = target.cursor_col, }; const base = append_base orelse return endpoint; const flow_end = projected_flow_end orelse return endpoint; if (base.flow_len < flow_end) return endpoint; endpoint.flow_len = base.flow_len; if (base.flow_len == target_flow_len) { endpoint.visual_offset = target.source_endpoint_visual_offset; endpoint.visual_rows = projectionVisualRowsInArea( target.total_visual_rows, target.source_endpoint_visual_offset, transcript_area, ); endpoint.cursor_row = source_endpoint_cursor_row; endpoint.cursor_col = source_endpoint_cursor_col; } else { endpoint.visual_offset = base.visual_offset; endpoint.visual_rows = base.visual_rows; endpoint.cursor_row = base.cursor_row; endpoint.cursor_col = base.cursor_col; } return endpoint; } fn materializedEndpointWithoutAppend( self: *const TranscriptRuntime, document_append: render_engine.frame_scroll_plan.FrameDocumentAppend, planned: MaterializedEndpoint, ) MaterializedEndpoint { return switch (self.transcript_commit_state) { .stable => |anchor| .{ .visual_offset = committedProjectionVisualOffset(anchor), .visual_rows = projectionVisualRowsInArea( anchor.total_visual_rows, committedProjectionVisualOffset(anchor), self.committed_frame_layout.transcript_area, ), .flow_len = if (anchor.flow_materialized) anchor.flow.len else null, .cursor_row = anchor.cursor_row, .cursor_col = anchor.cursor_col, }, .recovering => |receipt| .{ .visual_offset = receipt.materialized_visual_offset, .visual_rows = receipt.materialized_visual_rows, .flow_len = receipt.materialized_flow_len, .cursor_row = receipt.cursor_row, .cursor_col = receipt.cursor_col, }, .invalid => if (document_append.isEmpty()) planned else .{ .visual_offset = 0, .visual_rows = 0, .flow_len = 0, .cursor_row = document_append.start_row, .cursor_col = document_append.start_col, }, }; } fn borrowsPendingResumeSource( self: *const TranscriptRuntime, source: *const TranscriptPreparationSource, ) bool { return if (self.pending_resume_source) |*pending| source == pending else false; } pub fn prepareTranscriptScrollFactsForFrame( self: *const TranscriptRuntime, alloc: Allocator, source: *const TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, footer_reservation_changed: bool, replay_displaced_footer_history: bool, ) !TranscriptScrollFacts { if (self.borrowsPendingResumeSource(source)) switch (self.transcript_commit_state) { .invalid => try transcript_painter.seedPreparedPresentationBoundary( alloc, prepared, 0, .{}, ), .recovering => |receipt| if (receipt.presentation_valid and receipt.materialized_flow_len != null) { try transcript_painter.seedPreparedPresentationBoundary( alloc, prepared, receipt.materialized_flow_len.?, .{ .resume_bytes = receipt.presentation_resume_bytes, .cursor_col = receipt.cursor_col, .pending_wrap = receipt.presentation_pending_wrap, }, ); }, .stable => {}, }; return self.planTranscriptScrollForFrame( prepared, footer_reservation_changed, replay_displaced_footer_history, ); } pub fn resolveTranscriptTransitionTargetForFrame( self: *const TranscriptRuntime, alloc: Allocator, source: *const TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, scroll_facts: TranscriptScrollFacts, destructive_invalidation: bool, activity_overlay_active: bool, ) !ResolvedTranscriptTarget { return self.resolveTranscriptTransitionTargetForFrameInArea( alloc, source, prepared, target_layout, target_layout.transcript_area, scroll_plan, scroll_facts, destructive_invalidation, activity_overlay_active, ); } pub fn resolveTranscriptTransitionTargetForFrameInArea( self: *const TranscriptRuntime, alloc: Allocator, source: *const TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, projection_area: render_engine.frame_layout.FrameRect, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, scroll_facts: TranscriptScrollFacts, destructive_invalidation: bool, activity_overlay_active: bool, ) !ResolvedTranscriptTarget { const target_area = target_layout.transcript_area; if (target_area.isEmpty() != projection_area.isEmpty() or (!target_area.isEmpty() and (projection_area.top != target_area.top or projection_area.bottom > target_area.bottom))) { return error.InvalidTranscriptTransition; } try scroll_plan.validate(self.layout.rows); if (scroll_plan.requested_inline_advance_rows != scroll_facts.planned_rows) { return error.InvalidFrameScrollPlan; } const accepted = acceptedTranscriptRows( scroll_plan, scroll_plan.terminal_scroll_rows, scroll_facts.resize_reflow_rows, scroll_facts.semantic_rows, scroll_facts.semantic_progress_rows, ); const source_endpoint_cursor_row = prepared.cursor.cursor_row; const source_endpoint_cursor_col = prepared.cursor.cursor_col; const target = self.resolveTransitionTarget( alloc, source.bytes, prepared, target_layout, projection_area, scroll_plan, scroll_facts, accepted.semantic_rows, accepted.semantic_progress_rows, destructive_invalidation, activity_overlay_active, ) catch |err| { const normal_buffer_recovery_pending = switch (self.transcript_commit_state) { .stable => |anchor| anchor.normal_buffer_recovery_pending, .recovering => |receipt| receipt.normal_buffer_recovery_pending, .invalid => false, }; debug_trace.logf( "scroll", "transcript_target_resolution_failed err={s} source_compatible={s} geometry_rebase={s} recovery_rebase={s} normal_buffer_recovery_pending={s} target_visual_offset={d}", .{ @errorName(err), if (scroll_facts.source_compatible) "true" else "false", if (scroll_facts.geometry_rebase) "true" else "false", if (scroll_facts.recovery_rebase) "true" else "false", if (normal_buffer_recovery_pending) "true" else "false", scroll_facts.target_visual_offset, }, ); return err; }; if (!projection_area.isEmpty() and target.selection.last_visible_row > projection_area.bottom) { debug_trace.logf( "scroll", "transcript_target_outside_candidate last_visible={d} target_area={d}..{d} body_disposition={s}", .{ target.selection.last_visible_row, projection_area.top, projection_area.bottom, @tagName(target.body_disposition), }, ); return error.InvalidTranscriptTransition; } return .{ .target = target, .target_layout = target_layout, .scroll_plan = scroll_plan, .scroll_facts = scroll_facts, .accepted = accepted, .source_endpoint_cursor_row = source_endpoint_cursor_row, .source_endpoint_cursor_col = source_endpoint_cursor_col, .destructive_invalidation = destructive_invalidation, .activity_overlay_active = activity_overlay_active, .borrows_pending_resume = self.borrowsPendingResumeSource(source), }; } pub fn finalizeTranscriptTransitionForFrame( self: *TranscriptRuntime, alloc: Allocator, source: *TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, plan: *render_engine.paint_plan.PaintPlan, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, footer_reservation_changed: bool, replay_displaced_footer_history: bool, ) !TranscriptTransition { const scroll_facts = try self.prepareTranscriptScrollFactsForFrame( alloc, source, prepared, footer_reservation_changed, replay_displaced_footer_history, ); const destructive_invalidation = render_engine.frame_retention.transcriptAreaHasDestructiveInvalidation( self.committed_frame_layout.transcript_area, plan.invalidation, ); const resolved = try self.resolveTranscriptTransitionTargetForFrame( alloc, source, prepared, target_layout, scroll_plan, scroll_facts, destructive_invalidation, plan.activity == .overlay_entry, ); resolved.applyToPaintPlan(plan); return self.sealTranscriptTransition( alloc, source, prepared, plan, resolved, ); } pub fn sealTranscriptTransition( self: *TranscriptRuntime, alloc: Allocator, source: *TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, plan: *const render_engine.paint_plan.PaintPlan, resolved: ResolvedTranscriptTarget, ) !TranscriptTransition { try resolved.scroll_plan.validate(self.layout.rows); const plan_layout = render_engine.frame_layout.CommittedLayoutSnapshot.fromPaintPlan(plan.*); if (!std.meta.eql(plan_layout, resolved.target_layout) or !std.meta.eql(plan.viewport, resolved.target.selection) or (plan.activity == .overlay_entry) != resolved.activity_overlay_active) { return error.InvalidTranscriptTransition; } const destructive_invalidation = render_engine.frame_retention.transcriptAreaHasDestructiveInvalidation( self.committed_frame_layout.transcript_area, plan.invalidation, ); if (destructive_invalidation != resolved.destructive_invalidation or self.borrowsPendingResumeSource(source) != resolved.borrows_pending_resume) { return error.InvalidTranscriptTransition; } switch (resolved.target.body_disposition) { .paint => if (!std.meta.eql(prepared.selection, resolved.target.selection) or prepared.cursor.cursor_row != resolved.target.cursor_row or prepared.cursor.cursor_col != resolved.target.cursor_col) { return error.InvalidTranscriptTransition; }, .retain_committed => |retained| { const anchor = switch (self.transcript_commit_state) { .stable => |value| value, .invalid, .recovering => return error.InvalidTranscriptTransition, }; if (!std.meta.eql(anchor.selection, resolved.target.selection) or anchor.cursor_row != resolved.target.cursor_row or anchor.cursor_col != resolved.target.cursor_col or anchor.occupied_last_row != retained.occupied_last_row) { return error.InvalidTranscriptTransition; } }, } const borrows_pending_resume = resolved.borrows_pending_resume; const borrows_full_page = self.borrowsInstalledFullTranscriptSource(source); const borrows_source = borrows_pending_resume or borrows_full_page; const scroll_plan = resolved.scroll_plan; const scroll_facts = resolved.scroll_facts; const accepted = resolved.accepted; const target_layout = resolved.target_layout; const source_endpoint_cursor_row = resolved.source_endpoint_cursor_row; const source_endpoint_cursor_col = resolved.source_endpoint_cursor_col; var target = resolved.target; const source_diagnostic = self.transcriptCommitDiagnostic(); debug_trace.logf( "scroll", "transcript_transition_finalize source_state={s} body_disposition={s} source_start={d} target_start={d} source_visual_offset={d} target_visual_offset={d} target_history_visual_offset={d} target_cursor={d},{d} source_layout_id={x} target_layout_id={x}", .{ @tagName(source_diagnostic.state), @tagName(target.body_disposition), source_diagnostic.stable_start_line orelse 0, target.selection.start_line, source_diagnostic.visual_offset, target.visual_offset, target.history_visual_offset, target.cursor_row, target.cursor_col, source_diagnostic.layout_id, target_layout.layout_id, }, ); var target_cache: std.ArrayList(u8) = .empty; errdefer target_cache.deinit(alloc); const target_cache_start = transcript_store.cappedTailStart(source.bytes, self.max_transcript_bytes); const target_cache_bytes = source.bytes[target_cache_start..]; if (!borrows_source) { try target_cache.ensureTotalCapacityPrecise( alloc, @max(target_cache_bytes.len, self.transcript.capacity), ); try target_cache.appendSlice(alloc, target_cache_bytes); } const target_cache_origin_untrimmed = source.cache_origin_untrimmed and target_cache_start == 0; var transition_replaceable_last_line = prepared.replaceable_last_line; var transition_replaceable_start = prepared.replaceable_start; if (transition_replaceable_last_line) { if (target_cache_bytes.len == 0 or prepared.replaceable_start >= source.bytes.len) { transition_replaceable_last_line = false; transition_replaceable_start = 0; } else if (prepared.replaceable_start > target_cache_start) { transition_replaceable_start = prepared.replaceable_start - target_cache_start; } else { transition_replaceable_start = 0; } } var retention_identity = try source_preparation.RetentionIdentity.capture(self, alloc, source); errdefer retention_identity.deinit(alloc); var target_flow = source.bytes; var presentation_flow: []u8 = &.{}; errdefer alloc.free(presentation_flow); if (!borrows_source) source.bytes = &.{}; errdefer if (!borrows_source and target_flow.len > 0) alloc.free(target_flow); const folded_summary_indices = source.folded_summary_indices; if (!borrows_source) source.folded_summary_indices = &.{}; errdefer if (!borrows_source and folded_summary_indices.len > 0) { alloc.free(folded_summary_indices); }; const source_row_provenance: []const transcript_blocks.RowProvenance = switch (target.body_disposition) { .paint => prepared.row_provenance.items, .retain_committed => switch (self.transcript_commit_state) { .invalid => &.{}, .stable => |anchor| anchor.row_provenance, .recovering => |receipt| receipt.row_provenance, }, }; const row_provenance = try alloc.dupe( transcript_blocks.RowProvenance, source_row_provenance, ); errdefer if (row_provenance.len > 0) alloc.free(row_provenance); const append_base = try self.resolveTransitionAppendBase( prepared, scroll_plan.prior_owned_top, scroll_facts, destructive_invalidation, target_flow, target.history_visual_offset, ); const append_is_history_replay = if (append_base) |base| base.history_replay != null else false; const source_flow_end_exact = switch (self.transcript_commit_state) { .recovering => |receipt| receipt.materialized_flow_len != null, .invalid, .stable => true, }; const natural_projected_flow_end: ?usize = if (target.projection_staged) target.staged_flow_end else if (source_flow_end_exact) target_flow.len else null; const held_projected_flow_end: ?usize = if (target.hold_staged) if (append_base) |base| held: { const base_visual_end = base.visual_offset + base.visual_rows; const target_base_flow_end = transcript_painter.preparedTranscriptFlowOffsetForVisualOffset( prepared, self.layout.cols, base_visual_end, ) orelse break :held null; if (target_base_flow_end < base.flow_len) break :held null; // Target growth can terminate the committed line at its byte // endpoint. Charge that cursor movement before advancing into // the final rows this frame may release. const drift = walkText( 1, base.cursor_col, target_flow[base.flow_len..target_base_flow_end], self.layout.cols, ).end_row - 1; if (drift > accepted.semantic_progress_rows) break :held base.flow_len; break :held transcript_painter.preparedTranscriptFlowOffsetForVisualOffset( prepared, self.layout.cols, base_visual_end + accepted.semantic_progress_rows - drift, ); } else null else null; const projected_flow_end: ?usize = if (append_base) |base| if (base.history_replay) |replay| replay.flow_end else held_projected_flow_end orelse natural_projected_flow_end else natural_projected_flow_end; var document_append = render_engine.frame_scroll_plan.FrameDocumentAppend{}; var document_append_bytes: []u8 = &.{}; var append_pending_wrap = false; var presentation_resume_bytes: []u8 = &.{}; var presentation_pending_wrap = false; var presentation_valid = false; errdefer if (document_append_bytes.len > 0) { alloc.free(document_append_bytes); }; errdefer if (presentation_resume_bytes.len > 0) { alloc.free(presentation_resume_bytes); }; if (plan.reset_terminal) { const replay_padding_rows = target_layout.terminal_rows -| target.cursor_row; document_append_bytes = try transcript_painter.prepareResetReplayDocument( self, alloc, target_flow, prepared.sourceVisibleLines(), prepared.sourceVisualRows(), prepared.transcript_ends_with_newline, replay_padding_rows, ); document_append = .{ .bytes = document_append_bytes, .start_row = 1, .start_col = 1, .reset_replay = true, }; debug_trace.logf( "scroll", "document_append_plan raw_bytes={d} wire_bytes={d} start={d},{d} source_visual_offset={d} target_visual_offset={d} planned_scroll_rows={d}", .{ target_flow.len, document_append.bytes.len, document_append.start_row, document_append.start_col, 0, target.visual_offset, scroll_plan.terminal_scroll_rows, }, ); } else if (append_base != null and (!scroll_facts.recovery_rebase or append_is_history_replay) and !scroll_facts.recovery_projection_deferred and (if (append_is_history_replay) accepted.semantic_rows > 0 else accepted.semantic_progress_rows > 0) and projected_flow_end != null and projected_flow_end.? > append_base.?.flow_len and scroll_plan.terminal_scroll_rows > 0) { const base = append_base.?; const raw_append = target_flow[base.flow_len..projected_flow_end.?]; const resume_boundary: ?transcript_painter.PresentationBoundary = if (base.flow_len == 0) .{} else switch (self.transcript_commit_state) { .recovering => |receipt| if (receipt.presentation_valid and receipt.materialized_flow_len == base.flow_len) .{ .resume_bytes = receipt.presentation_resume_bytes, .cursor_col = base.cursor_col, .pending_wrap = receipt.presentation_pending_wrap, } else null, .invalid, .stable => null, }; if (borrows_pending_resume and base.history_replay == null and resume_boundary != null) { var prepared_append = try transcript_painter.prepareResumeDocumentAppend( alloc, target_flow, self.layout.cols, base.flow_len, projected_flow_end.?, target.projection_staged, resume_boundary.?, true, ); defer prepared_append.deinit(alloc); append_pending_wrap = prepared_append.start_pending_wrap; document_append_bytes = prepared_append.bytes; prepared_append.bytes = &.{}; presentation_resume_bytes = prepared_append.endpoint_resume_bytes; prepared_append.endpoint_resume_bytes = &.{}; presentation_pending_wrap = prepared_append.endpoint_pending_wrap; presentation_valid = true; } else { var prepared_append = try transcript_painter.prepareTranscriptDocumentAppend( alloc, target_flow, self.layout.cols, base.flow_len, projected_flow_end.?, target.projection_staged, ); defer prepared_append.deinit(alloc); append_pending_wrap = prepared_append.start_pending_wrap; document_append_bytes = prepared_append.bytes; prepared_append.bytes = &.{}; } document_append = .{ .bytes = if (document_append_bytes.len > 0) document_append_bytes else raw_append, .start_row = base.cursor_row, .start_col = base.cursor_col, // A staged projection may already place the endpoint on the next row. .start_pending_wrap = base.history_replay == null and base.cursor_col == self.layout.cols and append_pending_wrap, .clear = if (base.history_replay) |replay| .{ .rows = replay.clear_rows } else .remainder, }; debug_trace.logf( "scroll", "document_append_plan raw_bytes={d} wire_bytes={d} start={d},{d} source_visual_offset={d} target_visual_offset={d} planned_scroll_rows={d}", .{ raw_append.len, document_append.bytes.len, document_append.start_row, document_append.start_col, base.visual_offset, target.visual_offset, scroll_plan.terminal_scroll_rows, }, ); } const source_history_visual_offset = switch (self.transcript_commit_state) { .invalid => 0, .stable => |anchor| anchor.history_visual_offset, .recovering => |receipt| receipt.accepted_history_visual_offset, }; const planned_history = resolvePlannedHistoryMaterialization( source_history_visual_offset, target.history_visual_offset, target.visual_offset, plan.reset_terminal or !document_append.isEmpty(), target.normal_buffer_recovery_pending, ); if (planned_history.visual_offset != target.history_visual_offset) { debug_trace.logf( "scroll", "transcript_history_materialization_deferred source_visual_offset={d} target_visual_offset={d}", .{ source_history_visual_offset, target.history_visual_offset }, ); } target.history_visual_offset = planned_history.visual_offset; target.normal_buffer_recovery_pending = planned_history.normal_buffer_recovery_pending; var materialized = resolveMaterializedEndpoint( target, source_endpoint_cursor_row, source_endpoint_cursor_col, append_base, if (append_is_history_replay) natural_projected_flow_end else projected_flow_end, target_flow.len, target_layout.transcript_area, ); if (!borrows_source and target.projection_staged and retention_identity.publication_entries.len > 0) { const prefix_end = @max(target.staged_flow_end orelse target_flow.len, source.publication_owned_end); if (prefix_end < target_flow.len) { const prefix = try alloc.dupe(u8, target_flow[0..prefix_end]); errdefer alloc.free(prefix); target.total_visual_rows = try retention_identity.retainPrefix(alloc, self.entries.items, prefix, self.layout.cols); presentation_flow = target_flow; target_flow = prefix; if (materialized.flow_len) |len| materialized.flow_len = @min(len, prefix_end); materialized.visual_rows = @intCast(@min(materialized.visual_rows, target.total_visual_rows -| materialized.visual_offset)); } } if (!scroll_facts.source_compatible or scroll_facts.geometry_rebase or scroll_facts.recovery_rebase or scroll_plan.terminal_scroll_rows > 0 or plan.reset_terminal) { const before = self.transcriptCommitDiagnostic(); diagnostics.recordRenderEvent( .transition, "prepared view={d}->{d} history={d}->{d} start={d}->{d} rows={d}->{d} bytes={d}->{d} compatible={} geometry={} rebase={} hold={} restore={} catchup={} append={d}@{d},{d} scroll={d} reset={} body={d}..{d} footer={d}..{d}", .{ before.visual_offset, target.visual_offset, before.history_visual_offset, target.history_visual_offset, before.stable_start_line orelse 0, target.selection.start_line, before.total_visual_rows, target.total_visual_rows, before.source_bytes, target_flow.len, scroll_facts.source_compatible, scroll_facts.geometry_rebase, scroll_facts.recovery_rebase, target.hold_staged, target.normal_buffer_recovery_pending, target.history_catchup_pending, document_append.bytes.len, document_append.start_row, document_append.start_col, scroll_plan.terminal_scroll_rows, plan.reset_terminal, target_layout.transcript_area.top, target_layout.transcript_area.bottom, target_layout.footer_area.top, target_layout.footer_area.bottom }, ); } return .{ .scroll_plan = scroll_plan, .document_append = document_append, .document_append_bytes = document_append_bytes, .history_settle_pending = scroll_facts.recovery_rebase and !self.fullTranscriptActive() and self.finalityReleasableOffset( prepared, scroll_facts.target_visual_offset, ) > target.history_visual_offset, .resize_reflow_rows = scroll_facts.resize_reflow_rows, .semantic_rows = scroll_facts.semantic_rows, .semantic_progress_rows = scroll_facts.semantic_progress_rows, .recovery_rebase = scroll_facts.recovery_rebase, .recovery_progress_compatible = scroll_facts.recovery_progress_compatible, .recovery_tracks_semantic_progress = scroll_facts.recovery_tracks_semantic_progress, .recovery_projection_deferred = scroll_facts.recovery_projection_deferred, .body_disposition = target.body_disposition, .target_flow = target_flow, .presentation_flow = presentation_flow, .retention_identity = retention_identity, .target_flow_owned = !borrows_source, .borrows_full_page = borrows_full_page, .presentation_resume_bytes = presentation_resume_bytes, .presentation_pending_wrap = presentation_pending_wrap, .presentation_valid = presentation_valid, .target_cache = target_cache, .target_cache_unchanged = borrows_source, .target_cache_origin_untrimmed = target_cache_origin_untrimmed, .folded_summary_indices = folded_summary_indices, .target_layout = target_layout, .selection = target.selection, .visual_offset = target.visual_offset, .history_visual_offset = target.history_visual_offset, .source_endpoint_visual_offset = target.source_endpoint_visual_offset, .total_visual_rows = target.total_visual_rows, .materialized = materialized, .materialized_without_append = self.materializedEndpointWithoutAppend( document_append, materialized, ), .cursor_row = target.cursor_row, .cursor_col = target.cursor_col, .replaceable_last_line = transition_replaceable_last_line, .replaceable_start = transition_replaceable_start, .replaceable_row = prepared.cursor.replaceable_row, .trace_transcript_frame = prepared.trace_transcript_frame, .trace_visible_rows = prepared.trace_visible_rows, .measured_history_origin = target.measured_history_origin, .row_provenance = row_provenance, .normal_buffer_recovery_pending = target.normal_buffer_recovery_pending, .history_catchup_pending = target.history_catchup_pending, }; } pub fn consumeTranscriptTransition( self: *TranscriptRuntime, alloc: Allocator, transition: *TranscriptTransition, result: render_engine.terminal_diff.FrameCommitResult, ) void { const scroll_commit = result.scrollCommit(transition.scroll_plan); self.consumeTranscriptTransitionWithCommit(alloc, transition, result, scroll_commit); } pub fn consumeFrameScrollCommit( self: *TranscriptRuntime, alloc: Allocator, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, result: render_engine.terminal_diff.FrameCommitResult, transition: ?*TranscriptTransition, ) void { const before = self.transcriptCommitDiagnostic(); const before_recovery_pending = self.normalBufferRecoveryPending(); const before_history_catchup_pending = self.historyCatchupPending(); const terminal_reset_commit = result.is_committed() and self.terminal_reset_pending; const resize_reset_commit = terminal_reset_commit and (self.pending_resize_observation != null or self.resize_history_row_delta != null); const scroll_commit = result.scrollCommit(scroll_plan); self.ackPreservedRowReleaseCommit(scroll_plan, scroll_commit); if (transition) |value| { std.debug.assert(std.meta.eql(scroll_plan, value.scroll_plan)); self.consumeTranscriptTransitionWithCommit(alloc, value, result, scroll_commit); } const committed_selection = if (result.is_committed()) if (transition) |value| value.selection else null else null; self.resize_bottom_anchor_pending = nextResizeBottomAnchorPending( self.resize_bottom_anchor_pending, terminal_reset_commit, resize_reset_commit, if (committed_selection) |selection| selection.tail_kind else null, if (committed_selection) |selection| selection.last_visible_row else 0, self.layout.content_bottom, ); self.recordRenderCommit( before, before_recovery_pending, before_history_catchup_pending, scroll_plan, result, terminal_reset_commit, ); } fn recordRenderCommit( self: *const TranscriptRuntime, before: TranscriptCommitDiagnostic, before_recovery_pending: bool, before_history_catchup_pending: bool, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, result: render_engine.terminal_diff.FrameCommitResult, reset: bool, ) void { const after = self.transcriptCommitDiagnostic(); // Idle animation and same-row token paints must not evict the transitions // that explain a jump before the user can request a trace. if (result.is_committed() and !result.full_repaint and !reset and scroll_plan.terminal_scroll_rows == 0 and before.state == after.state and before.visual_offset == after.visual_offset and before.history_visual_offset == after.history_visual_offset and before.total_visual_rows == after.total_visual_rows and before.layout_id == after.layout_id and before_recovery_pending == self.normalBufferRecoveryPending() and before_history_catchup_pending == self.historyCatchupPending()) return; diagnostics.recordRenderEvent( .commit, "state={s}->{s} result={s} view={d}->{d} history={d}->{d} rows={d}->{d} layout={x}->{x} terminal={d}x{d} cursor={d},{d} planned_scroll={d} accepted_scroll={d} bytes={d} repaint={} reset={} recovery={}->{} catchup={}->{}", .{ @tagName(before.state), @tagName(after.state), @tagName(result.state()), before.visual_offset, after.visual_offset, before.history_visual_offset, after.history_visual_offset, before.total_visual_rows, after.total_visual_rows, before.layout_id, after.layout_id, self.layout.cols, self.layout.rows, result.committed_cursor_row, result.committed_cursor_col, scroll_plan.terminal_scroll_rows, result.terminal_scroll_rows_applied(), result.bytes_written, result.full_repaint, reset, before_recovery_pending, self.normalBufferRecoveryPending(), before_history_catchup_pending, self.historyCatchupPending() }, ); } fn consumeTranscriptTransitionWithCommit( self: *TranscriptRuntime, alloc: Allocator, transition: *TranscriptTransition, result: render_engine.terminal_diff.FrameCommitResult, scroll_commit: render_engine.frame_scroll_plan.FrameScrollCommit, ) void { std.debug.assert(!transition.consumed); const commit = resolveTranscriptTransitionCommit(transition, result, scroll_commit); if (commit.accepted.resize_reflow_rows > 0 and transition.measured_history_origin != null) { debug_trace.logf( "resize", "measured_history_endpoint_commit rows={d} before={s} after={s}", .{ commit.accepted.resize_reflow_rows, if (transition.measured_history_origin.?.endpoint != null) "exact" else "unavailable", if (commit.committed_measured_history_origin.?.endpoint != null) "exact" else "unavailable", }, ); } if (result.is_committed()) { self.committed_frame_layout = transition.target_layout; } if (transition.borrows_full_page) { transition.target_flow = &.{}; transition.folded_summary_indices = &.{}; transition.consumed = true; debug_trace.logf( "scroll", "transcript_transition_commit state=full_page_borrowed", .{}, ); return; } self.publication_projection_invalid = !result.is_committed() and transition.retention_identity.publication_entries.len > 0; const borrowed_resume_source = if (!transition.target_flow_owned) if (self.pending_resume_source) |*source| if (transition.target_flow.len == 0 or source.bytes.ptr == transition.target_flow.ptr) source else null else null else null; if (commit.stable and !transition.target_flow_owned and borrowed_resume_source == null) { debug_trace.logf( "scroll", "transcript_transition_commit state=invalid reason=missing_borrowed_resume_source", .{}, ); transition.target_flow = &.{}; var previous_state = self.transcript_commit_state; self.transcript_commit_state = .invalid; previous_state.deinit(alloc); self.markTranscriptDirty(); } else if (commit.stable) { var previous_cache: std.ArrayList(u8) = .empty; if (!transition.target_cache_unchanged) { previous_cache = self.transcript; self.transcript = transition.target_cache; transition.target_cache = .empty; self.transcript_cache_origin_untrimmed = transition.target_cache_origin_untrimmed; } const target_has_content = transition.target_flow.len > 0; const retained_body = transition.body_disposition == .retain_committed; const summary_count = @min( self.folded_command_blocks.items.len, transition.folded_summary_indices.len, ); for (self.folded_command_blocks.items[0..summary_count], 0..) |*block, index| { block.summary_transcript_index = transition.folded_summary_indices[index]; } const stable_flow = transition.target_flow; const pending_resume_bytes = if (transition.target_flow_owned) self.releasePendingResumeSource(alloc) else blk: { const pending = borrowed_resume_source.?; pending.bytes = &.{}; const bytes = stable_flow.len; _ = self.releasePendingResumeSource(alloc); break :blk bytes; }; transition.target_flow = &.{}; var previous_state = self.transcript_commit_state; self.transcript_commit_state = .{ .stable = .{ .selection = transition.selection, .visual_offset = transition.visual_offset, .history_visual_offset = transition.history_visual_offset, .total_visual_rows = transition.total_visual_rows, .flow = stable_flow, .retention_identity = transition.retention_identity, .flow_materialized = if (!retained_body) (transition.retention_identity.publication_entries.len == 0 or transition.materialized.flow_len == stable_flow.len) else switch (previous_state) { .stable => |anchor| anchor.flow_materialized, .invalid, .recovering => false, }, .cursor_row = transition.cursor_row, .cursor_col = transition.cursor_col, .occupied_last_row = transition.selection.last_visible_row, .occupied_last_row_blank = transition.selection.last_visible_row_blank, .layout_id = transition.target_layout.layout_id, .measured_history_origin = commit.committed_measured_history_origin, .row_provenance = transition.row_provenance, .normal_buffer_recovery_pending = transition.normal_buffer_recovery_pending and (transition.retention_identity.publication_entries.len == 0 or transition.history_catchup_pending or transition.history_visual_offset < transition.visual_offset), .history_catchup_pending = transition.history_catchup_pending, } }; transition.row_provenance = &.{}; transition.retention_identity = .{}; self.last_rendered_cols = transition.target_layout.terminal_cols; if (pending_resume_bytes > 0) { self.invalidateTranscriptAnchor("resume publication retained source handoff"); if (transition.target_flow_owned) { debug_trace.logf( "session", "pending resume flow dropped reason=superseded bytes={d}", .{pending_resume_bytes}, ); } else { debug_trace.logf( "session", "resume historical flow committed bytes={d}", .{pending_resume_bytes}, ); } self.markTranscriptDirty(); } if (!retained_body and !self.fullTranscriptActive()) { self.last_visible_transcript_top_row = transition.selection.top_row; self.last_visible_transcript_start_line = transition.selection.start_line; self.last_visible_transcript_partial_skip_rows = transition.selection.partial_skip_rows; self.last_visible_transcript_split_active = transition.selection.split_active; self.last_visible_transcript_split_prefix_lines = transition.selection.split_prefix_lines; self.last_visible_transcript_split_suffix_start_line = transition.selection.split_suffix_start_line; self.last_visible_transcript_last_row = transition.selection.last_visible_row; self.last_visible_transcript_last_row_blank = transition.selection.last_visible_row_blank; self.last_visible_transcript_tail_kind = transition.selection.tail_kind; self.last_viewport_selection = transition.selection; self.cursor_row = transition.cursor_row; self.cursor_col = transition.cursor_col; self.replaceable_last_line = transition.replaceable_last_line; self.replaceable_start = transition.replaceable_start; self.replaceable_row = transition.replaceable_row; self.paint_trace.transcript_initialized = true; self.paint_trace.transcript_top = transition.selection.top_row; self.paint_trace.transcript_bottom = transition.selection.bottom_row; self.paint_trace.transcript_start_line = transition.selection.start_line; self.paint_trace.transcript_total_lines = transition.selection.line_count; self.paint_trace.transcript_visible_rows = transition.trace_visible_rows; self.paint_trace.transcript_partial_skip_rows = transition.selection.partial_skip_rows; self.paint_trace.transcript_entries = self.entries.items.len; self.paint_trace.transcript_log_frame = transition.trace_transcript_frame; } if (target_has_content and !self.has_painted_transcript) { self.has_painted_transcript = true; } previous_state.deinit(alloc); previous_cache.deinit(alloc); debug_trace.logf( "scroll", "transcript_transition_commit state=stable start={d} visual_offset={d} history_visual_offset={d} layout_id={x} cursor={d},{d}", .{ transition.selection.start_line, transition.visual_offset, transition.history_visual_offset, transition.target_layout.layout_id, transition.cursor_row, transition.cursor_col, }, ); if (transition.normal_buffer_recovery_pending or (commit.accepted.semantic_rows > 0 and if (self.committedRetentionIdentity()) |identity| identity.publication_entries.len > 0 else false)) { self.markTranscriptDirty(); } } else { const recovery = resolveTranscriptRecoveryCommit(self, transition, result, commit); var previous_state = self.transcript_commit_state; var presentation_resume_bytes: []u8 = &.{}; var presentation_pending_wrap = false; var presentation_valid = false; if (result.document_append_applied() and transition.presentation_valid) { presentation_resume_bytes = transition.presentation_resume_bytes; presentation_pending_wrap = transition.presentation_pending_wrap; presentation_valid = true; transition.presentation_resume_bytes = &.{}; transition.presentation_valid = false; } else switch (previous_state) { .recovering => |*receipt| { presentation_resume_bytes = receipt.presentation_resume_bytes; presentation_pending_wrap = receipt.presentation_pending_wrap; presentation_valid = receipt.presentation_valid; receipt.presentation_resume_bytes = &.{}; receipt.presentation_valid = false; }, .invalid, .stable => {}, } self.transcript_commit_state = .{ .recovering = .{ .accepted_visual_offset = recovery.accepted_visual_offset, .accepted_history_visual_offset = recovery.accepted_history_visual_offset, .attempt_visual_offset = transition.source_endpoint_visual_offset, .attempt_total_visual_rows = transition.total_visual_rows, .materialized_visual_offset = recovery.materialized_visual_offset, .materialized_visual_rows = recovery.materialized_visual_rows, .materialized_flow_len = recovery.materialized_flow_len, .remaining_resize_reflow_rows = commit.remaining_resize_reflow_rows, .remaining_semantic_rows = commit.remaining_semantic_rows, .remaining_semantic_progress_rows = commit.remaining_semantic_progress_rows, .remaining_unplanned_scroll_rows = commit.remaining_unplanned_scroll_rows, .attempt_cols = transition.target_layout.terminal_cols, .attempt_resize_history_row_delta = self.resize_history_row_delta, .attempt_resize_history_committed = result.is_committed(), .projection_rebase_pending = transition.recovery_rebase, .tracks_semantic_progress = transition.recovery_tracks_semantic_progress, .flow = transition.target_flow, .retention_identity = transition.retention_identity, .flow_owned = transition.target_flow_owned, .presentation_resume_bytes = presentation_resume_bytes, .presentation_pending_wrap = presentation_pending_wrap, .presentation_valid = presentation_valid, .cursor_row = recovery.materialized_cursor_row, .cursor_col = recovery.materialized_cursor_col, .document_append_committed = recovery.document_append_committed, .measured_history_origin = commit.committed_measured_history_origin, .row_provenance = transition.row_provenance, .normal_buffer_recovery_pending = transition.normal_buffer_recovery_pending, } }; transition.target_flow = &.{}; transition.row_provenance = &.{}; transition.retention_identity = .{}; previous_state.deinit(alloc); if (commit.complete and !transition.recovery_projection_deferred and (commit.remaining_resize_reflow_rows > 0 or commit.remaining_semantic_rows > 0)) { self.markTranscriptDirty(); } debug_trace.logf( "scroll", "transcript_transition_commit state=recovering accepted_resize_reflow_rows={d} accepted_semantic_rows={d} accepted_semantic_progress_rows={d} remaining_resize_reflow_rows={d} remaining_semantic_rows={d} remaining_semantic_progress_rows={d} remaining_unplanned_scroll_rows={d} recovery_rebase={s} recovery_projection_deferred={s} append_committed={s} shadow_state={s}", .{ commit.accepted.resize_reflow_rows, commit.accepted.semantic_rows, commit.accepted.semantic_progress_rows, commit.remaining_resize_reflow_rows, commit.remaining_semantic_rows, commit.remaining_semantic_progress_rows, commit.remaining_unplanned_scroll_rows, if (transition.recovery_rebase) "true" else "false", if (transition.recovery_projection_deferred) "true" else "false", if (recovery.document_append_committed) "true" else "false", @tagName(result.state()), }, ); } if (transition.folded_summary_indices.len > 0) { alloc.free(transition.folded_summary_indices); transition.folded_summary_indices = &.{}; } if (transition.history_settle_pending) { // Final rows remain unmaterialized past this commit; guarantee a // follow-up frame so the debt settles without external activity. self.render_requests.request(.transcript); } transition.consumed = true; } /// Writes only to the byte buffer for callers that separately append a /// structured entry. Use `writeTranscript` when raw-entry mirroring is needed. pub fn writeTranscriptBytes(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool) !void { return transcript_writer.writeTranscriptBytes(self, alloc, metrics, text, record); } pub fn estimatedReflowedCursorRow(self: *const TranscriptRuntime, target_cols: u16) ?u32 { const shadow = self.shadow_vt orelse return null; if (target_cols == 0 or shadow.cursor_row == 0) return null; return reflowedRows(shadow, 1, shadow.cursor_row, target_cols, true); } pub fn ackPreservedRowRelease( self: *TranscriptRuntime, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, committed_terminal_scroll_rows: u16, ) !void { try scroll_plan.validate(self.layout.rows); self.ackPreservedRowReleaseAssumeValid(scroll_plan, committed_terminal_scroll_rows); } pub fn ackPreservedRowReleaseAssumeValid( self: *TranscriptRuntime, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, committed_terminal_scroll_rows: u16, ) void { const scroll_commit = scroll_plan.commit(committed_terminal_scroll_rows); self.ackPreservedRowReleaseCommit(scroll_plan, scroll_commit); } fn ackPreservedRowReleaseCommit( self: *TranscriptRuntime, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, scroll_commit: render_engine.frame_scroll_plan.FrameScrollCommit, ) void { const accepted = scroll_commit.accepted_rows; const raw_owned_top = self.owned_top_row; const raw_viewport_top = self.viewport_top_row; const normalized_viewport_top = render_engine.frame_scroll_plan.normalizeOwnedTop( self.viewport_top_row, self.layout.rows, ); self.owned_top_row = scroll_plan.prior_owned_top -| accepted.preserved_release_rows; self.viewport_top_row = @max( self.owned_top_row, normalized_viewport_top -| accepted.preserved_release_rows, ); debug_trace.logf( "scroll", "preserved_row_release requested={d} planned={d} committed_terminal={d} accepted_terminal={d} unplanned_terminal={d} accepted={d} raw_owned_top={d} raw_viewport_top={d} normalized_owned_top={d} normalized_viewport_top={d} owned_top={d} viewport_top={d}", .{ scroll_plan.requested_release_rows, scroll_plan.preserved_release_rows, scroll_commit.physical_terminal_scroll_rows, scroll_commit.accepted_terminal_scroll_rows, scroll_commit.unplanned_terminal_scroll_rows, accepted.preserved_release_rows, raw_owned_top, raw_viewport_top, scroll_plan.prior_owned_top, normalized_viewport_top, self.owned_top_row, self.viewport_top_row, }, ); } /// Writes to the byte buffer and mirrors the content as a raw entry. /// Structured writers must use `writeTranscriptBytes` to avoid duplication. pub fn writeTranscript(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool) !void { return transcript_writer.writeTranscript(self, alloc, metrics, text, record); } pub fn writeTranscriptClassified( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, class: RawEntryClass, ) !void { return transcript_writer.writeTranscriptClassified(self, alloc, metrics, text, record, class); } pub fn writeTranscriptBytesFrom( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, record: bool, entry_id: u32, ) !void { return transcript_writer.writeTranscriptBytesFrom( self, alloc, metrics, text, record, entry_id, ); } pub fn writeCompletedToolStatus( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, kind: types.ToolOutcomeKind, summary: []const u8, record: bool, ) !void { _ = try self.writeCompletedToolStatusReturningEntryId( alloc, metrics, kind, summary, record, ); } pub fn writeCompletedToolStatusReturningEntryId( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, kind: types.ToolOutcomeKind, summary: []const u8, record: bool, ) !u32 { const line = try lifecycleTerminalLine(alloc, kind, summary); defer alloc.free(line); return transcript_writer.writeTranscriptReturningEntryIdClassified( self, alloc, metrics, line, record, .tool_status, ); } pub fn writeTurnCancellation( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, record: bool, ) !void { const line = try lifecycleTerminalLine(alloc, .cancelled, "Cancelled"); defer alloc.free(line); try transcript_writer.writeTranscriptClassified( self, alloc, metrics, line, record, .turn_cancellation, ); } /// Rebuild the transcript cache at the current width and request a /// diagnostic full-frame repaint. Terminal mutation remains owned /// by the frame commit path. pub fn reconstructivePaint(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics) !void { return transcript_writer.reconstructivePaint(self, alloc, metrics); } pub fn appendReplaceableTranscriptLine(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8) !u32 { return transcript_store.appendReplaceableTranscriptLine(self, alloc, metrics, text); } pub fn appendReplaceableTranscriptLineClassified( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8, class: RawEntryClass, ) !u32 { return transcript_store.appendReplaceableTranscriptLineClassified(self, alloc, metrics, text, class); } pub fn replaceTrailingTranscriptLine(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, text: []const u8) !bool { return transcript_store.replaceTrailingTranscriptLine(self, alloc, metrics, text); } /// Returns the id of the mirrored `raw_bytes` entry, or `0` when /// `text` is empty (sentinel: `next_entry_id` starts at 1). pub fn appendReplaceableTranscriptLineSilent(self: *TranscriptRuntime, alloc: Allocator, text: []const u8) !u32 { return transcript_store.appendReplaceableTranscriptLineSilent(self, alloc, text); } pub fn appendReplaceableTranscriptLineSilentClassified(self: *TranscriptRuntime, alloc: Allocator, text: []const u8, class: RawEntryClass) !u32 { return transcript_store.appendReplaceableTranscriptLineSilentClassified(self, alloc, text, class); } pub fn replaceTrailingTranscriptLineSilent(self: *TranscriptRuntime, alloc: Allocator, text: []const u8) !bool { return transcript_store.replaceTrailingTranscriptLineSilent(self, alloc, text); } pub fn writeNotice( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, notice: types.SemanticNotice, record: bool, ) !void { return transcript_store.writeNotice(self, alloc, metrics, notice, record) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, }; } pub fn appendReplaceableSemanticNotice( self: *TranscriptRuntime, alloc: Allocator, notice: types.SemanticNotice, ) !u32 { return transcript_store.appendReplaceableSemanticNoticeAtomic(self, alloc, notice); } pub fn appendSemanticNotice( self: *TranscriptRuntime, alloc: Allocator, notice: types.SemanticNotice, ) !u32 { return transcript_store.appendSemanticNoticeAtomic(self, alloc, notice); } pub fn replaceSemanticNotice( self: *TranscriptRuntime, alloc: Allocator, entry_id: u32, notice: types.SemanticNotice, ) !bool { return transcript_store.replaceSemanticNoticeAtomic(self, alloc, entry_id, notice); } pub fn setCommandOutputRenderPolicy(self: *TranscriptRuntime, styles: Styles) void { return command_output_runtime.setCommandOutputRenderPolicy(self, styles); } pub fn retainedTranscriptStyles(self: *const TranscriptRuntime) Styles { return self.command_output_render.styles; } pub fn writeRecordedCommandOutputChunkAtomic( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, ) !?u32 { return transcript_store.writeRecordedCommandOutputChunkAtomic( self, alloc, metrics, styles, lifecycle_id, stream, text, ); } pub fn flushRecordedCommandOutputSummaryAtomic( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, ) !void { return transcript_store.flushRecordedCommandOutputSummaryAtomic( self, alloc, metrics, styles, lifecycle_id, ); } pub fn writeCommandOutputChunk(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, stream: command_output_content.Stream, text: []const u8, record: bool) !void { return command_output_runtime.writeCommandOutputChunk(self, alloc, metrics, styles, stream, text, record); } pub fn writeCommandOutputChunkForLifecycle( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, record: bool, ) !void { return command_output_runtime.writeCommandOutputChunkForLifecycle( self, alloc, metrics, styles, lifecycle_id, stream, text, record, ); } pub fn flushCommandOutputSummary(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, record: bool) !void { return command_output_runtime.flushCommandOutputSummary(self, alloc, metrics, styles, record); } pub fn flushCommandOutputSummaryForLifecycle( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, record: bool, ) !void { return command_output_runtime.flushCommandOutputSummaryForLifecycle( self, alloc, metrics, styles, lifecycle_id, record, ); } pub fn openCommandOutputLifecycleId( self: *const TranscriptRuntime, ) ?types.ToolLifecycleId { return command_output_runtime.openCommandOutputLifecycleId(self); } pub fn frameSink(self: *TranscriptRuntime) render_engine.terminal_diff.FrameSink { if (comptime @import("builtin").is_test) { if (self.test_frame_sink) |sink| return sink; } return transcript_io.frameSinkFrom(@ptrCast(self), writeFrameSink); } pub fn writeNotificationBell( self: *TranscriptRuntime, metrics: *Metrics, ) void { switch (transcript_io.writeFrameBytes(self, metrics, "\x07")) { .complete => {}, .partial => |partial| debug_trace.logf( "notifications", "terminal bell write failed accepted_bytes={d} err={s}", .{ partial.accepted_bytes, @errorName(partial.err) }, ), } } fn writeFrameSink(ctx: *anyopaque, metrics: *Metrics, bytes: []const u8) render_engine.terminal_diff.FrameSinkWriteResult { const self: *TranscriptRuntime = @ptrCast(@alignCast(ctx)); return transcript_io.writeFrameBytes(self, metrics, bytes); } pub fn recomputeCursorFromTranscript(self: *TranscriptRuntime) void { self.resetCursorTracking(); if (self.transcript.items.len == 0) return; _ = self.advanceCursor(self.transcript.items); } pub fn prepareTranscriptSurfacePaint( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, bottom_reserved_rows: u16, ) !transcript_painter.PreparedTranscriptSurfacePaint { return transcript_painter.prepareTranscriptSurfacePaint(self, alloc, metrics, bottom_reserved_rows); } pub fn prepareTranscriptSource( self: *TranscriptRuntime, alloc: Allocator, tracked_entry_id: ?u32, ) !TranscriptPreparationSource { _ = try transcript_store.settleTranscriptPublication(self, alloc, null); if (self.pending_resume_source) |*source| { return source.clone(alloc); } return source_preparation.prepareTranscriptSource(self, alloc, tracked_entry_id); } pub fn cachedTranscriptSource( self: *TranscriptRuntime, alloc: Allocator, ) !TranscriptPreparationSource { return self.cachedTranscriptSourceInterruptible(alloc, null) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, }; } pub fn pendingResumeSourceInterruptible( self: *TranscriptRuntime, alloc: Allocator, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !?*TranscriptPreparationSource { var source = if (self.pending_resume_source) |*value| value else return null; if (source.cols != self.layout.cols) { debug_trace.logf( "full_transcript_cache", "pending_resume_rebuild old_cols={d} new_cols={d}", .{ source.cols, self.layout.cols }, ); try build_checkpoint.poll(checkpoint); const bytes = try alloc.dupe(u8, source.bytes); var replacement = try source_preparation.prepareFullTranscriptViewportSourceInterruptible( self, alloc, bytes, checkpoint, ); errdefer replacement.deinit(alloc); try replacement.ensureLineIndexInterruptible(alloc, checkpoint); const previous_bytes = source.bytes; source.deinit(alloc); self.pending_resume_source = replacement; source = &self.pending_resume_source.?; switch (self.transcript_commit_state) { .recovering => |*receipt| if (!receipt.flow_owned) { std.debug.assert(receipt.flow.ptr == previous_bytes.ptr); receipt.flow = source.bytes; }, .invalid, .stable => {}, } } else if (source.hard_line_starts.len == 0) { try source.ensureLineIndexInterruptible(alloc, checkpoint); } else { try build_checkpoint.poll(checkpoint); } return source; } pub fn cachedTranscriptSourceInterruptible( self: *TranscriptRuntime, alloc: Allocator, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !TranscriptPreparationSource { _ = try transcript_store.settleTranscriptPublication(self, alloc, checkpoint); if (self.pending_resume_source != null) { return self.prepareTranscriptSource(alloc, null); } if (self.compact_transcript_source_cache.find( self.full_transcript_content_revision, self.layout.cols, self.has_committed_frame, )) |source| { source.preview.cursor_row = self.cursor_row; source.preview.cursor_col = self.cursor_col; source.preview.replaceable_row = self.replaceable_row; source.replaceable_row = self.replaceable_row; return source.clone(alloc); } var source = try source_preparation.prepareTranscriptSourceInterruptible( self, alloc, null, null, null, checkpoint, ); errdefer source.deinit(alloc); try source.ensureLineIndexInterruptible(alloc, checkpoint); const cached = self.compact_transcript_source_cache.insert(alloc, .{ .source = source, .content_revision = self.full_transcript_content_revision, .cols = self.layout.cols, .has_committed_frame = self.has_committed_frame, }); return cached.clone(alloc); } pub fn prepareTranscriptSourceForFrameInterruptible( self: *TranscriptRuntime, alloc: Allocator, focused_entry_id: ?u32, omitted_entry_id: ?u32, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !TranscriptPreparationSource { _ = try transcript_store.settleTranscriptPublication(self, alloc, checkpoint); return source_preparation.prepareTranscriptSourceInterruptible( self, alloc, focused_entry_id, focused_entry_id, omitted_entry_id, checkpoint, ); } pub fn prepareTranscriptSourceOmittingEntry( self: *TranscriptRuntime, alloc: Allocator, omitted_entry_id: u32, ) !TranscriptPreparationSource { return source_preparation.prepareTranscriptSourceOmittingEntry(self, alloc, omitted_entry_id); } pub fn prepareTranscriptSourceWithFocusedEntry( self: *TranscriptRuntime, alloc: Allocator, focused_entry_id: u32, ) !TranscriptPreparationSource { return source_preparation.prepareTranscriptSourceWithFocusedEntry(self, alloc, focused_entry_id); } pub fn prepareFullTranscriptViewportSource( self: *TranscriptRuntime, alloc: Allocator, bytes: []u8, ) !TranscriptPreparationSource { return source_preparation.prepareFullTranscriptViewportSource(self, alloc, bytes); } pub const FullTranscriptSurfacePaint = struct { owned_source: ?TranscriptPreparationSource = null, borrowed_source: ?*TranscriptPreparationSource = null, prepared: transcript_painter.PreparedTranscriptSurfacePaint, pub fn source(self: *FullTranscriptSurfacePaint) *TranscriptPreparationSource { if (self.owned_source) |*owned| return owned; return self.borrowed_source.?; } pub fn deinit(self: *FullTranscriptSurfacePaint, alloc: Allocator) void { self.prepared.deinit(alloc); if (self.owned_source) |*source_value| source_value.deinit(alloc); self.* = undefined; } }; pub noinline fn buildFullTranscriptProjection( self: *TranscriptRuntime, alloc: Allocator, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, ) !full_transcript_screen.Projection { self.prepareFullTranscriptProjectionLayout(); return self.buildFullTranscriptProjectionUncached( alloc, full_diff_resolver, self.fullTranscriptAnchorEntryId(), null, ) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, }; } pub fn pollFullTranscriptPageLoad( self: *TranscriptRuntime, ) !bool { if (self.full_transcript_window_load.takeCompleted()) |window_task| { defer window_task.deinit(); if (self.full_transcript_installed_page_retired) { self.discardInstalledFullTranscriptPage(); return false; } const page = if (self.full_transcript_installed_page) |*value| value else null; if (page) |installed| { if (installed.measurement_snapshot_ready and full_transcript_page.sameRequest(installed.source.request, window_task.request.page_request) and installed.projection.measurement_cols != installed.source.request.cols) { debug_trace.logf( "full_transcript_cache", "page_invalidated reason=stored_result_changed revision={d} cols={d}", .{ installed.source.request.content_revision, installed.source.request.cols }, ); self.discardInstalledFullTranscriptPage(); self.full_transcript_failed_request = null; return true; } } if (window_task.cancel_requested.load(.acquire)) { // Superseded scroll windows are expected and have no visible // failure state; the latest offset schedules on the next tick. } else if (window_task.failure) |failure| { self.full_transcript_failed_request = window_task.request.page_request; self.full_transcript_failure_pending = true; if (failure != error.InputPending) { debug_trace.logf( "full_transcript_cache", "window_build_failed revision={d} cols={d} offset={d} err={s}", .{ window_task.request.page_request.content_revision, window_task.request.page_request.cols, window_task.request.target_offset, @errorName(failure), }, ); } } else if (page != null and !window_task.cancel_requested.load(.acquire) and full_transcript_page.sameRequest( page.?.source.request, window_task.request.page_request, )) { if (window_task.takePreparedWindow()) |window| { if (page.?.prepared_window) |*current| { current.deinit(std.heap.c_allocator); } page.?.prepared_window = window; page.?.presented_offset = window.target_offset; self.full_transcript_failed_request = null; return true; } } } const task = self.full_transcript_page_load.takeCompleted() orelse return false; defer task.deinit(); const request = task.source.request; const desired = self.desiredFullTranscriptPageRequest(); var installed = false; if (task.cancel_requested.load(.acquire)) { // Closing, resizing, or superseding a page intentionally cancels // its worker. It must not poison the next open request. } else if (task.failure) |failure| { self.full_transcript_failed_request = request; self.full_transcript_failure_pending = true; if (failure != error.InputPending) { debug_trace.logf( "full_transcript_cache", "page_build_failed revision={d} cols={d} err={s}", .{ request.content_revision, request.cols, @errorName(failure) }, ); } } else if (!task.cancel_requested.load(.acquire) and full_transcript_page.sameSurface(desired, request)) { if (task.projection) |projection| { const prepared_window = task.prepared_window orelse return error.MissingPreparedFullTranscriptSource; const measured_item_rows = try std.heap.c_allocator.dupe( transcript_presentation.ItemRow, projection.measured_item_rows.items, ); errdefer if (measured_item_rows.len > 0) { std.heap.c_allocator.free(measured_item_rows); }; const source = task.takeInstalledSource(); std.debug.assert(!self.full_transcript_installed_page_retired); if (self.full_transcript_installed_page) |*page| page.deinit(); self.full_transcript_installed_page = .{ .source = source, .projection = task.takeProjection().?, .prepared_window = task.takePreparedWindow().?, .measured_total_rows = projection.measured_total_rows, .measured_anchor_row = projection.measured_anchor_row, .measured_item_rows = measured_item_rows, .measurement_snapshot_ready = true, .presented_offset = prepared_window.target_offset, }; self.full_transcript_installed_page_retired = false; self.full_transcript_failed_request = null; installed = true; } } return installed or self.full_transcript.depth == .full; } pub fn prewarmFullTranscriptPage( self: *TranscriptRuntime, capability: ?*session_child_store.SessionChildCapability, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, ) !void { try self.ensureFullTranscriptPageLoad(capability, full_diff_resolver); } pub fn fullTranscriptPreparedForOpen(self: *const TranscriptRuntime) bool { if (self.entries.items.len == 0) return true; if (self.full_transcript_installed_page_retired) return false; const page = if (self.full_transcript_installed_page) |*value| value else return false; return page.prepared_window != null and full_transcript_page.sameRequest( self.desiredFullTranscriptPageRequest(), page.source.request, ); } pub fn requestFullTranscriptOpen(self: *TranscriptRuntime) bool { self.full_transcript_restore_open_pending = false; // Pending primary damage must commit before another buffer can consume it. if (self.fullTranscriptPreparedForOpen() and self.render_requests.pendingInvalidations().isEmpty()) { self.full_transcript_open_request = null; debug_trace.logf("full_transcript", "open_request state=ready", .{}); return true; } if (self.full_transcript_open_request != null) { self.full_transcript_open_request = null; debug_trace.logf("full_transcript", "open_request state=cancelled", .{}); return false; } const request = self.desiredFullTranscriptPageRequest(); if (self.full_transcript_failed_request) |failed| { if (full_transcript_page.sameRequest( request, failed, )) { self.full_transcript_open_request = request; self.full_transcript_failure_pending = true; debug_trace.logf("full_transcript", "open_request state=failed", .{}); return false; } } self.full_transcript_open_request = request; debug_trace.logf("full_transcript", "open_request state=pending", .{}); return false; } pub fn deferRestoredFullTranscriptOpen(self: *TranscriptRuntime) void { std.debug.assert(self.full_transcript.depth == .full); self.full_transcript_open_request = self.desiredFullTranscriptPageRequest(); self.full_transcript = self.full_transcript.defer_full_open(); self.full_transcript_restore_open_pending = true; debug_trace.logf( "full_transcript", "restored_open deferred scroll_rows={d} follow_tail={} bookmark_pending={} bookmark_entry_id={d}", .{ self.full_transcript.scroll_rows, self.full_transcript.follow_tail, self.full_transcript.bookmark_pending, self.full_transcript.bookmark_entry_id orelse 0, }, ); } pub fn cancelPendingFullTranscriptOpen(self: *TranscriptRuntime) bool { if (self.full_transcript_open_request == null) return false; self.full_transcript_open_request = null; self.full_transcript_restore_open_pending = false; debug_trace.logf("full_transcript", "open_request state=cancelled", .{}); return true; } pub fn takeReadyFullTranscriptOpen(self: *TranscriptRuntime) bool { const requested = self.full_transcript_open_request orelse return false; if (!self.render_requests.pendingInvalidations().isEmpty()) return false; if (self.full_transcript_installed_page_retired) return false; const page = if (self.full_transcript_installed_page) |*value| value else return false; if (page.prepared_window == null or !full_transcript_page.sameRequest(requested, page.source.request)) return false; self.full_transcript_open_request = null; return true; } pub fn takeFullTranscriptPreparationFailure(self: *TranscriptRuntime) bool { const pending = self.full_transcript_failure_pending and (self.fullTranscriptActive() or self.full_transcript_open_request != null); if (!pending) return false; self.full_transcript_failure_pending = false; self.full_transcript_open_request = null; return true; } pub fn preparedFullTranscriptPageProjectionInterruptible( self: *TranscriptRuntime, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, capability: ?*session_child_store.SessionChildCapability, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !*full_transcript_screen.Projection { try build_checkpoint.poll(checkpoint); std.debug.assert(self.full_transcript.depth == .full); try self.ensureFullTranscriptPageLoad(capability, full_diff_resolver); if (self.installedFullTranscriptPageProjection()) |projection| return projection; if (!self.full_transcript_installed_page_retired) { if (self.full_transcript_installed_page) |*page| { if (page.prepared_window != null and full_transcript_page.sameSurface( self.desiredFullTranscriptPageRequest(), page.source.request, )) { return &page.projection; } } } return error.InputPending; } fn installedFullTranscriptPageProjection( self: *TranscriptRuntime, ) ?*full_transcript_screen.Projection { if (self.full_transcript_installed_page_retired) return null; const page = if (self.full_transcript_installed_page) |*value| value else return null; const desired = self.desiredFullTranscriptPageRequest(); if (full_transcript_page.sameRequest(desired, page.source.request)) { return &page.projection; } if (!full_transcript_page.sameSurface(desired, page.source.request)) { return null; } if (self.full_transcript_prepared_page_visible or (self.full_transcript_page_load.busy() and self.command_output_display.open_command_block != null)) { return &page.projection; } if (self.command_output_display.open_command_block == null) return null; return &page.projection; } pub fn preparedFullTranscriptPageCapability( self: *TranscriptRuntime, ) ?*session_child_store.SessionChildCapability { if (self.full_transcript_installed_page_retired) return null; const page = if (self.full_transcript_installed_page) |*value| value else return null; return if (page.source.capability) |*capability| capability else null; } fn desiredFullTranscriptPageRequest( self: *const TranscriptRuntime, ) full_transcript_page.Request { return .{ .content_revision = self.full_transcript_content_revision, .cols = self.layout.cols, .anchor = self.full_transcript_page_anchor, }; } fn ensureFullTranscriptPageLoad( self: *TranscriptRuntime, capability: ?*session_child_store.SessionChildCapability, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, ) !void { if (self.full_transcript_installed_page_retired) { if (self.full_transcript_window_load.busy()) { self.full_transcript_window_load.cancelActive(); return; } self.discardInstalledFullTranscriptPage(); } const request = self.full_transcript_open_request orelse self.desiredFullTranscriptPageRequest(); if (self.full_transcript_failed_request) |failed| { if (full_transcript_page.sameRequest(request, failed)) return; self.full_transcript_failed_request = null; } if (self.full_transcript_installed_page) |*page| { if (full_transcript_page.sameRequest(request, page.source.request)) return; } if (self.command_output_display.open_command_block != null and self.full_transcript_open_request == null) { if (self.full_transcript_installed_page) |*page| { if (full_transcript_page.sameSurface( request, page.source.request, )) return; } } if (self.full_transcript_page_load.hasRequest(request)) return; if (self.full_transcript_window_load.busy()) { self.full_transcript_window_load.cancelActive(); return; } if (self.full_transcript_page_load.busy()) { if (!self.full_transcript_page_load.hasCompatibleRequest(request)) { self.full_transcript_page_load.cancelActive(); } return; } var source = self.snapshotFullTranscriptPage( request, capability, full_diff_resolver, ) catch |err| switch (err) { error.FullTranscriptPageSnapshotTooLarge => { self.full_transcript_failed_request = request; self.full_transcript_failure_pending = true; debug_trace.logf( "full_transcript_cache", "page_snapshot_rejected revision={d} cols={d} err={s}", .{ request.content_revision, request.cols, @errorName(err) }, ); return; }, else => |other| return other, }; var source_owned = true; errdefer if (source_owned) source.deinit(std.heap.c_allocator); try self.full_transcript_page_load.schedule(source); self.full_transcript_failed_request = null; source_owned = false; } fn snapshotFullTranscriptPage( self: *const TranscriptRuntime, request: full_transcript_page.Request, capability: ?*session_child_store.SessionChildCapability, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, ) !full_transcript_worker.Source { const alloc = std.heap.c_allocator; const range = full_transcript_page.sourceRange( request, self.entries.items.len, ); if (self.fullTranscriptPageSnapshotBytes( range, full_diff_resolver, ) > full_transcript_snapshot_clone_max_bytes) { return error.FullTranscriptPageSnapshotTooLarge; } var source = full_transcript_worker.Source{ .request = request, .range = range, .styles = self.command_output_render.styles, .presentation = self.full_transcript, .visible_rows = @intCast(@min( self.fullTranscriptPageRows(), std.math.maxInt(u16), )), }; errdefer source.deinit(alloc); try source.entries.ensureTotalCapacity(alloc, range.len()); try source.details.ensureTotalCapacity(alloc, range.len()); try source.command_blocks.ensureTotalCapacity( alloc, @min(self.command_output_blocks.items.len, range.len()), ); for (self.entries.items[range.start..range.end]) |entry| { source.entries.appendAssumeCapacity( try transcript_store.cloneEntryForSnapshot(alloc, entry), ); if (self.toolDetailForEntry(entry.id())) |detail| { source.details.appendAssumeCapacity( try transcript_store.cloneToolDetailForSnapshot( alloc, detail.*, ), ); } } const page_id_bounds = snapshotEntryIdBounds(source.entries.items); for (self.command_output_blocks.items) |block| { if (!snapshotCommandBlockIntersects( source.entries.items, page_id_bounds, block, )) continue; const block_index = try appendSnapshotCommandBlock( alloc, &source, block, ); const snapshot_block = &source.command_blocks.items[block_index]; if (page_id_bounds) |bounds| { try appendSnapshotEntryIds( alloc, &snapshot_block.source_entry_ids, block.source_entry_ids.items, bounds, ); try appendSnapshotEntryIds( alloc, &snapshot_block.live_entry_ids, block.live_entry_ids.items, bounds, ); } if (block.lifecycle_id) |lifecycle_id| { for (self.tool_details.items) |detail| { if (!command_output_runtime.sameLifecycleId( detail.lifecycle_id, lifecycle_id, )) continue; try appendSnapshotToolDetail(alloc, &source, detail); try appendSnapshotToolHeading(alloc, &source, detail); break; } } } var detail_index: usize = 0; while (detail_index < source.details.items.len) : (detail_index += 1) { const detail = source.details.items[detail_index]; for (self.command_output_blocks.items) |block| { const same_output_entry = detail.command_output_entry_id != null and block.entry_id == detail.command_output_entry_id; const same_lifecycle = if (detail.lifecycle_id) |lifecycle_id| command_output_runtime.sameLifecycleId( block.lifecycle_id, lifecycle_id, ) else false; if (!same_output_entry and !same_lifecycle) continue; _ = try appendSnapshotCommandBlock(alloc, &source, block); break; } } if (full_diff_resolver) |resolver| { try appendSnapshotFullDiffs(alloc, &source, resolver); } if (capability) |current| { source.capability = try current.cloneReadOnly(alloc); } try self.prependFullDetailRecords(alloc, &source); debug_trace.logf( "full_transcript_cache", "page_snapshot revision={d} cols={d} range={d}..{d} entries={d} details={d} blocks={d} diffs={d}", .{ request.content_revision, request.cols, range.start, range.end, source.entries.items.len, source.details.items.len, source.command_blocks.items.len, source.full_diffs.items.len, }, ); return source; } fn fullTranscriptPageSnapshotBytes( self: *const TranscriptRuntime, range: full_transcript_page.SourceRange, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, ) usize { const entries = self.entries.items[range.start..range.end]; var total: usize = entries.len *| @sizeOf(TranscriptEntry); for (entries) |entry| { total +|= transcript_store.entrySnapshotRetainedBytes(entry); if (full_diff_resolver) |resolver| { const raw = switch (entry) { .raw_bytes => |value| value, else => continue, }; if (raw.class != .diff_block) continue; const marker_id = diff_mod.markedDiffBlockId(raw.bytes) orelse continue; if (resolver.full_for_marker(resolver.context, marker_id)) |content| { total +|= content.len; } } if (total > full_transcript_snapshot_clone_max_bytes) return total; } const bounds = snapshotEntryIdBounds(entries); for (self.tool_details.items) |detail| { if (bounds) |page_bounds| { if (detail.entry_id < page_bounds.min or detail.entry_id > page_bounds.max) { continue; } } else continue; total +|= toolDetailSnapshotBytes(detail); if (total > full_transcript_snapshot_clone_max_bytes) return total; } for (self.command_output_blocks.items) |block| { if (!snapshotCommandBlockIntersects(entries, bounds, block)) continue; total +|= command_output_runtime.commandOutputBlockRetainedBytes(block); if (total > full_transcript_snapshot_clone_max_bytes) return total; } return total; } fn appendSnapshotFullDiffs( alloc: Allocator, source: *full_transcript_worker.Source, resolver: full_transcript_screen.FullDiffResolver, ) !void { for (source.entries.items) |entry| { const raw = switch (entry) { .raw_bytes => |value| value, else => continue, }; if (raw.class != .diff_block) continue; const marker_id = diff_mod.markedDiffBlockId(raw.bytes) orelse continue; var duplicate = false; for (source.full_diffs.items) |existing| { if (existing.marker_id == marker_id) { duplicate = true; break; } } if (duplicate) continue; const content = resolver.full_for_marker( resolver.context, marker_id, ) orelse continue; try source.appendFullDiff(alloc, marker_id, content); } for (source.details.items) |detail| { const lifecycle_id = detail.lifecycle_id orelse continue; if (!resolver.has_full_for_lifecycle( resolver.context, lifecycle_id, )) continue; var duplicate = false; for (source.full_diff_lifecycles.items) |existing| { if (command_output_runtime.sameLifecycleId( existing, lifecycle_id, )) { duplicate = true; break; } } if (duplicate) continue; try source.appendFullDiffLifecycle(alloc, lifecycle_id); } } fn snapshotContainsEntry( entries: []const TranscriptEntry, entry_id: u32, ) bool { for (entries) |entry| { if (entry.id() == entry_id) return true; } return false; } const SnapshotEntryIdBounds = struct { min: u32, max: u32, }; fn toolDetailSnapshotBytes(detail: ToolDetailRecord) usize { var total = @sizeOf(ToolDetailRecord) +| detail.tool_name.len; if (detail.arguments_json) |value| total +|= value.len; if (detail.result) |value| total +|= value.len; if (detail.result_handle) |value| total +|= value.len; if (detail.command_artifact_handle) |value| total +|= value.len; if (detail.command_output_replay) |replay| switch (replay) { .available => |value| total +|= value.handle.len, .unavailable => {}, }; if (detail.lifecycle_id) |value| total +|= value.call_id.len; return total; } fn snapshotEntryIdBounds( entries: []const TranscriptEntry, ) ?SnapshotEntryIdBounds { if (entries.len == 0) return null; var bounds = SnapshotEntryIdBounds{ .min = entries[0].id(), .max = entries[0].id(), }; for (entries[1..]) |entry| { bounds.min = @min(bounds.min, entry.id()); bounds.max = @max(bounds.max, entry.id()); } return bounds; } fn snapshotCommandBlockIntersects( entries: []const TranscriptEntry, bounds: ?SnapshotEntryIdBounds, block: CommandOutputBlock, ) bool { if (block.entry_id) |entry_id| { if (snapshotContainsEntry(entries, entry_id)) return true; } const page_bounds = bounds orelse return false; return sortedEntryIdsIntersect(block.source_entry_ids.items, page_bounds) or sortedEntryIdsIntersect(block.live_entry_ids.items, page_bounds); } fn sortedEntryIdsIntersect( ids: []const u32, bounds: SnapshotEntryIdBounds, ) bool { var low: usize = 0; var high = ids.len; while (low < high) { const middle = low + (high - low) / 2; if (ids[middle] < bounds.min) { low = middle + 1; } else { high = middle; } } return low < ids.len and ids[low] <= bounds.max; } fn appendSnapshotEntryIds( alloc: Allocator, destination: *std.ArrayList(u32), ids: []const u32, bounds: SnapshotEntryIdBounds, ) !void { var low: usize = 0; var high = ids.len; while (low < high) { const middle = low + (high - low) / 2; if (ids[middle] < bounds.min) { low = middle + 1; } else { high = middle; } } var index = low; while (index < ids.len and ids[index] <= bounds.max) : (index += 1) { try destination.append(alloc, ids[index]); } } fn appendSnapshotToolDetail( alloc: Allocator, source: *full_transcript_worker.Source, detail: ToolDetailRecord, ) !void { for (source.details.items) |existing| { if (existing.entry_id == detail.entry_id) return; } try source.details.append( alloc, try transcript_store.cloneToolDetailForSnapshot(alloc, detail), ); } fn appendSnapshotToolHeading( alloc: Allocator, source: *full_transcript_worker.Source, detail: ToolDetailRecord, ) !void { if (snapshotContainsEntry(source.entries.items, detail.entry_id)) return; const bytes = try std.fmt.allocPrint( alloc, "● {s}\n", .{detail.tool_name}, ); errdefer alloc.free(bytes); var created_at_ms: i64 = 0; for (source.entries.items) |entry| { if (entry.createdAtMs() <= 0) continue; created_at_ms = entry.createdAtMs(); break; } try source.entries.insert(alloc, 0, .{ .raw_bytes = .{ .id = detail.entry_id, .created_at_ms = created_at_ms, .bytes = bytes, .class = .tool_status, } }); } fn appendSnapshotCommandBlock( alloc: Allocator, source: *full_transcript_worker.Source, block: CommandOutputBlock, ) !usize { for (source.command_blocks.items, 0..) |existing, index| { if (existing.entry_id == block.entry_id and command_output_runtime.sameLifecycleId( existing.lifecycle_id, block.lifecycle_id, )) return index; } try source.command_blocks.append( alloc, try full_transcript_worker.cloneCommandBlockForPageSnapshot(alloc, block), ); return source.command_blocks.items.len - 1; } fn prepareFullTranscriptProjectionLayout(self: *TranscriptRuntime) void { self.full_transcript = self.full_transcript.prepare_projection( self.layout.cols, ); } fn buildFullTranscriptProjectionUncached( self: *TranscriptRuntime, alloc: Allocator, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, anchor_entry_id: ?u32, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !full_transcript_screen.Projection { return self.buildFullTranscriptProjectionUncachedFrom( alloc, full_diff_resolver, anchor_entry_id, 0, checkpoint, ); } fn buildFullTranscriptProjectionUncachedFrom( self: *TranscriptRuntime, alloc: Allocator, full_diff_resolver: ?full_transcript_screen.FullDiffResolver, anchor_entry_id: ?u32, start_index: usize, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !full_transcript_screen.Projection { var relationships = try tool_group_projection.buildExpandedRelationshipsInterruptible( alloc, self.entries.items, self.tool_details.items, checkpoint, ); defer relationships.deinit(alloc); var entry_actions = try tool_group_projection.materializeExpandedRelationshipsRangeInterruptible( alloc, &relationships, start_index, self.layout.cols, .{ .marker_style = user_message_card.promptMarkerStyle(), .text_style = ui_render.statusline_style, .reset_style = "\x1b[0m", }, checkpoint, ); defer entry_actions.deinit(alloc); return full_transcript_screen.buildProjectionWithEntryActionsInterruptible( alloc, self.entries.items[start_index..], self.tool_details.items, self.command_output_blocks.items, self.command_output_render.styles, self.layout.cols, anchor_entry_id, full_diff_resolver, entry_actions.entry_actions.items, checkpoint, ); } pub noinline fn prepareFullTranscriptSurfacePaint( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, projection: *full_transcript_screen.Projection, capability: ?*session_child_store.SessionChildCapability, area: render_engine.frame_layout.FrameRect, ) !FullTranscriptSurfacePaint { return self.prepareFullTranscriptSurfacePaintInterruptible( alloc, metrics, projection, capability, area, null, ) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, }; } pub noinline fn prepareFullTranscriptSurfacePaintInterruptible( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, projection: *full_transcript_screen.Projection, capability: ?*session_child_store.SessionChildCapability, area: render_engine.frame_layout.FrameRect, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !FullTranscriptSurfacePaint { if (self.installedFullTranscriptPreparedWindow(projection)) |window| { self.full_transcript_prepared_page_visible = true; const page = &self.full_transcript_installed_page.?; const measurement = self.installedPageMeasurement(page); const offset = selectProjectionViewportOffset( self, measurement, area.height(), ); const cached_rows = @as(u32, window.source.preview.natural_visual_rows); const cache_end = window.start_row +| cached_rows; if (offset >= window.start_row and (offset +| area.height() <= cache_end or cache_end == measurement.total_rows)) { const local_offset = offset - window.start_row; debug_trace.logf( "full_transcript_cache", "window cols={d} offset={d} local_offset={d} visible={d} source=indexed rows={d} cache_start={d} cache_rows={d}", .{ self.layout.cols, offset, local_offset, area.height(), measurement.total_rows, window.start_row, cached_rows, }, ); const prepared = try transcript_painter.prepareIndexedFullTranscriptSurfacePaintForArea( self, alloc, metrics, &window.source, area, local_offset, ); page.presented_offset = offset; return .{ .borrowed_source = &window.source, .prepared = prepared, }; } try self.ensureFullTranscriptWindowLoad( page, measurement, offset, area.height(), ); const stable_offset = @min( @max(page.presented_offset, window.start_row), cache_end -| @min(@as(u32, area.height()), cached_rows), ); const prepared = try transcript_painter.prepareIndexedFullTranscriptSurfacePaintForArea( self, alloc, metrics, &window.source, area, stable_offset -| window.start_row, ); return .{ .borrowed_source = &window.source, .prepared = prepared, }; } const source_bytes = try full_transcript_screen.renderProjectionViewportSourceWithSelectorInterruptible( alloc, projection, capability, self.layout.cols, area.height(), .{ .context = self, .select_offset = selectProjectionViewportOffset }, checkpoint, ); var source = try self.prepareFullTranscriptViewportSource(alloc, source_bytes); errdefer source.deinit(alloc); const prepared = try transcript_painter.preparePreselectedTranscriptSurfacePaintFromSourceForArea( self, alloc, metrics, &source, area, ); return .{ .owned_source = source, .prepared = prepared }; } fn installedFullTranscriptPreparedWindow( self: *TranscriptRuntime, projection: *const full_transcript_screen.Projection, ) ?*full_transcript_worker.PreparedWindow { if (self.full_transcript_installed_page_retired) return null; const page = if (self.full_transcript_installed_page) |*value| value else return null; if (&page.projection != projection) return null; return if (page.prepared_window) |*window| window else null; } fn installedPageMeasurement( self: *const TranscriptRuntime, page: *const InstalledFullTranscriptPage, ) full_transcript_screen.ProjectionMeasurement { _ = self; if (page.measurement_snapshot_ready) { return .{ .total_rows = page.measured_total_rows, .anchor_row = page.measured_anchor_row, .item_rows = page.measured_item_rows, }; } return .{ .total_rows = page.projection.measured_total_rows, .anchor_row = page.projection.measured_anchor_row, .item_rows = page.projection.measured_item_rows.items, }; } fn ensureFullTranscriptWindowLoad( self: *TranscriptRuntime, page: *InstalledFullTranscriptPage, measurement: full_transcript_screen.ProjectionMeasurement, target_offset: u32, visible_rows: u16, ) !void { if (self.full_transcript_page_load.busy()) return; if (self.full_transcript_window_load.hasTarget(target_offset)) return; if (self.full_transcript_window_load.busy()) { self.full_transcript_window_load.cancelActive(); return; } const request = full_transcript_worker.preparedWindowRequest( page.source.request, measurement.total_rows, target_offset, visible_rows, ); try self.full_transcript_window_load.schedule( request, &page.projection, if (page.source.capability) |*capability| capability else null, ); } fn borrowsInstalledFullTranscriptSource( self: *TranscriptRuntime, source: *const TranscriptPreparationSource, ) bool { if (self.full_transcript_installed_page_retired) return false; const page = if (self.full_transcript_installed_page) |*value| value else return false; const window = if (page.prepared_window) |*value| value else return false; return &window.source == source; } fn selectProjectionViewportOffset( context: *anyopaque, measurement: full_transcript_screen.ProjectionMeasurement, visible_rows: u16, ) u32 { const self: *TranscriptRuntime = @ptrCast(@alignCast(context)); const selection = self.full_transcript.select_visual_offset( measurement.total_rows, visible_rows, measurement.item_rows, ); self.full_transcript = selection.state; return selection.offset; } pub fn prepareTranscriptSurfacePaintFromSourceForArea( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect, ) !transcript_painter.PreparedTranscriptSurfacePaint { return self.prepareTranscriptSurfacePaintFromSourceForFrame( alloc, metrics, source, area, false, ); } pub fn prepareTranscriptSurfacePaintFromSourceForFrame( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, source: *const TranscriptPreparationSource, area: render_engine.frame_layout.FrameRect, footer_reservation_changed: bool, ) !transcript_painter.PreparedTranscriptSurfacePaint { return transcript_painter.prepareTranscriptSurfacePaintFromSourceForFrame( self, alloc, metrics, source, area, footer_reservation_changed, ); } pub fn prepareTranscriptSurfacePaintForArea( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, area: render_engine.frame_layout.FrameRect, ) !transcript_painter.PreparedTranscriptSurfacePaint { return transcript_painter.prepareTranscriptSurfacePaintForArea(self, alloc, metrics, area); } pub fn prepareTranscriptSurfacePaintForAreaTrackingEntry( self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, area: render_engine.frame_layout.FrameRect, entry_id: ?u32, ) !transcript_painter.PreparedTranscriptSurfacePaint { return transcript_painter.prepareTranscriptSurfacePaintForAreaTrackingEntry( self, alloc, metrics, area, entry_id, ); } pub fn previewTranscriptFlow( self: *TranscriptRuntime, alloc: Allocator, ) !render_engine.frame_layout.TranscriptFlowPreview { return transcript_painter.previewTranscriptFlow(self, alloc); } pub fn paintPreparedTranscriptIntoSurface( self: *TranscriptRuntime, alloc: Allocator, surface: *render_engine.frame_surface.FrameSurface, prepared: *const transcript_painter.PreparedTranscriptSurfacePaint, ) !render_engine.paint_plan.TranscriptPaintResult { return transcript_painter.paintPreparedTranscriptIntoSurface(self, alloc, surface, prepared); } pub fn markTranscriptDirty(self: *TranscriptRuntime) void { self.transcript_band_dirty = true; self.render_requests.request(.transcript); } fn markTranscriptContentPaint(self: *TranscriptRuntime) void { self.transcript_band_dirty = true; if (self.deferLiveFullTranscriptRefresh()) return; self.render_requests.request(.transcript); } fn deferLiveFullTranscriptRefresh(self: *const TranscriptRuntime) bool { if (self.full_transcript.depth != .full or self.command_output_display.open_command_block == null) { return false; } if (self.full_transcript_page_load.busy() or self.full_transcript_window_load.busy()) return true; if (self.full_transcript_installed_page_retired) return false; const page = if (self.full_transcript_installed_page) |*value| value else return false; if (page.source.request.cols != self.layout.cols or !std.meta.eql(page.source.request.anchor, self.full_transcript_page_anchor)) { return false; } return true; } pub fn nativeHistoryActive(self: *const TranscriptRuntime) bool { return switch (self.transcript_commit_state) { .invalid => false, .stable => |anchor| anchor.history_visual_offset > 0, .recovering => |receipt| receipt.accepted_history_visual_offset > 0, }; } pub fn markTranscriptContentDirty(self: *TranscriptRuntime) void { self.full_transcript_content_revision +%= 1; debug_trace.logf( "full_transcript_cache", "content_dirty content_revision={d} command_open={s}", .{ self.full_transcript_content_revision, if (self.command_output_display.open_command_block == null) "false" else "true", }, ); self.markTranscriptContentPaint(); } pub fn markTranscriptStructureDirty(self: *TranscriptRuntime) void { self.full_transcript_content_revision +%= 1; debug_trace.logf( "full_transcript_cache", "structure_dirty content_revision={d}", .{self.full_transcript_content_revision}, ); self.markTranscriptDirty(); } pub fn markTranscriptContentDirtyFrom( self: *TranscriptRuntime, entry_id: u32, ) void { self.full_transcript_content_revision +%= 1; debug_trace.logf( "full_transcript_cache", "content_dirty_from entry_id={d} content_revision={d} command_open={s}", .{ entry_id, self.full_transcript_content_revision, if (self.command_output_display.open_command_block == null) "false" else "true", }, ); self.markTranscriptContentPaint(); } pub fn markTranscriptCommandOutputDirty(self: *TranscriptRuntime) void { self.full_transcript_content_revision +%= 1; debug_trace.logf( "full_transcript_cache", "command_output_dirty content_revision={d}", .{self.full_transcript_content_revision}, ); self.markTranscriptContentPaint(); } pub fn markTranscriptCommandOutputDirtyFrom( self: *TranscriptRuntime, entry_id: u32, ) void { self.full_transcript_content_revision +%= 1; debug_trace.logf( "full_transcript_cache", "command_output_dirty_from entry_id={d} content_revision={d}", .{ entry_id, self.full_transcript_content_revision, }, ); self.markTranscriptContentPaint(); } pub fn recordFrameInvalidation( shell: *TranscriptRuntime, range: render_engine.paint_plan.FrameInvalidationRange, ) !void { if (range.top == 0 or range.bottom == 0 or range.bottom < range.top) { return error.InvalidFrameInvalidationRange; } const request_invalidations = shell.render_requests.pendingInvalidations(); if (request_invalidations.len >= render_engine.paint_plan.max_frame_invalidation_ranges) { var collapsed = request_invalidations.collapsedWith(range); const bottom = if (shell.layout.rows == 0) range.bottom else shell.layout.rows; var top = if (shell.owned_top_row == 0) @as(u16, 1) else shell.owned_top_row; if (bottom == 0) { top = range.top; } else if (top > bottom) { top = 1; } collapsed.top = top; collapsed.bottom = bottom; debug_trace.logf( "frame_plan", "collapse_pending_invalidations count={d} incoming_reason={s} collapsed_reason={s} top={d} bottom={d}", .{ request_invalidations.len, @tagName(range.reason), @tagName(collapsed.reason), collapsed.top, collapsed.bottom, }, ); shell.render_requests.replacePendingInvalidation(collapsed); } else { shell.render_requests.requestInvalidation(range); } } pub fn normalizeFrameInvalidations( shell: *const TranscriptRuntime, pending: *render_engine.paint_plan.FrameInvalidationSet, ) void { if (shell.layout.rows == 0) return; var i: usize = 0; while (i < pending.len) : (i += 1) { const range = &pending.items[i]; if (range.bottom <= shell.layout.rows and range.top <= range.bottom) continue; range.top = if (shell.owned_top_row == 0 or shell.owned_top_row > shell.layout.rows) 1 else shell.owned_top_row; range.bottom = shell.layout.rows; } } /// Updates the capped terminal cursor and returns the uncapped text walk. /// Scroll calculations must use `result.end_row`, not `self.cursor_row`, /// which cannot represent overflow below the viewport. Wrap-exact walks may /// return `layout.cols + 1` in `end_col`. pub fn advanceCursor(self: *TranscriptRuntime, text: []const u8) WalkResult { const result = walkText(@as(u32, self.cursor_row), self.cursor_col, text, self.layout.cols); self.cursor_col = result.end_col; self.cursor_row = @intCast(@min(result.end_row, @as(u32, self.layout.rows))); return result; } pub fn updateExtraInputRows(self: *TranscriptRuntime, alloc: Allocator, metrics: *Metrics, new_extra: u16) !void { _ = alloc; _ = metrics; if (new_extra == self.extra_input_rows) return; const old_extra = self.extra_input_rows; self.extra_input_rows = new_extra; self.markTranscriptDirty(); self.footer_viewport.invalidateAfterExternalClear(); if (self.layout.rows > 0) { const top = if (self.owned_top_row == 0 or self.owned_top_row > self.layout.rows) @as(u16, 1) else self.owned_top_row; try self.recordFrameInvalidation(.{ .reason = .external_clear, .top = top, .bottom = self.layout.rows, }); } debug_trace.logf( "frame_plan", "footer_extra_update_frame_deferred old={d} new={d} owned_top={d} rows={d}", .{ old_extra, new_extra, self.owned_top_row, self.layout.rows }, ); } pub fn resetCursorTracking(self: *TranscriptRuntime) void { self.cursor_row = 1; self.cursor_col = 1; } }; test "transcript runtime initializer preserves an empty compact source cache" { var runtime = TranscriptRuntime.init(); defer runtime.deinit(std.testing.allocator); for (runtime.compact_transcript_source_cache.entries) |entry| { try std.testing.expect(entry == null); } try std.testing.expectEqual( @as(usize, 0), runtime.compact_transcript_source_cache.next_replacement, ); } fn totalVisualRows(line_rows: []const u16) u32 { var total: u32 = 0; for (line_rows) |rows| { total += rows; } return total; } const AcceptedTranscriptRows = struct { resize_reflow_rows: u16, semantic_rows: u32, semantic_progress_rows: u32, }; const TranscriptTransitionCommit = struct { accepted: AcceptedTranscriptRows, remaining_resize_reflow_rows: u16, remaining_semantic_rows: u32, remaining_semantic_progress_rows: u32, remaining_unplanned_scroll_rows: u16, complete: bool, stable: bool, committed_measured_history_origin: ?transcript_painter.MeasuredHistoryOrigin, }; const TranscriptRecoveryCommit = struct { accepted_visual_offset: u32, accepted_history_visual_offset: u32, materialized_visual_offset: u32, materialized_visual_rows: u16, materialized_flow_len: ?usize, materialized_cursor_row: u16, materialized_cursor_col: u16, document_append_committed: bool, }; fn resolveRecoveryMaterializedEndpoint( transition: *const TranscriptTransition, result: render_engine.terminal_diff.FrameCommitResult, commit: TranscriptTransitionCommit, ) MaterializedEndpoint { var planned = transition.materialized; if (!commit.complete and !transition.document_append.isEmpty() and transition.document_append.clear == .rows) { // Replaying history does not prove that the subsequent viewport paint // reached its endpoint. Recovery must repaint before resuming there. debug_trace.logf("scroll", "history replay endpoint unconfirmed after partial frame", .{}); planned.flow_len = null; return planned; } if (commit.complete or result.document_append_applied() or !transition.recovery_progress_compatible or !transition.recovery_tracks_semantic_progress or transition.recovery_rebase) { return planned; } return transition.materialized_without_append; } fn resolveTranscriptTransitionCommit( transition: *const TranscriptTransition, result: render_engine.terminal_diff.FrameCommitResult, scroll_commit: render_engine.frame_scroll_plan.FrameScrollCommit, ) TranscriptTransitionCommit { const accepted = acceptedTranscriptRowsForInlineRows( scroll_commit.accepted_rows.inline_advance_rows, transition.resize_reflow_rows, transition.semantic_rows, transition.semantic_progress_rows, ); const remaining_resize_reflow_rows = transition.resize_reflow_rows - accepted.resize_reflow_rows; const remaining_semantic_rows = transition.semantic_rows - accepted.semantic_rows; const remaining_semantic_progress_rows = transition.semantic_progress_rows - accepted.semantic_progress_rows; const remaining_unplanned_scroll_rows = scroll_commit.unplanned_terminal_scroll_rows; const complete = result.is_committed() and scroll_commit.complete(); return .{ .accepted = accepted, .remaining_resize_reflow_rows = remaining_resize_reflow_rows, .remaining_semantic_rows = remaining_semantic_rows, .remaining_semantic_progress_rows = remaining_semantic_progress_rows, .remaining_unplanned_scroll_rows = remaining_unplanned_scroll_rows, .complete = complete, .stable = complete and !transition.recovery_projection_deferred and remaining_resize_reflow_rows == 0 and remaining_semantic_rows == 0 and remaining_unplanned_scroll_rows == 0, .committed_measured_history_origin = transcript_painter.advanceMeasuredHistoryOriginForCommittedRows( transition.target_flow, transition.measured_history_origin, transition.target_layout.terminal_cols, accepted.resize_reflow_rows, ), }; } fn resolveTranscriptRecoveryCommit( runtime: *const TranscriptRuntime, transition: *const TranscriptTransition, result: render_engine.terminal_diff.FrameCommitResult, commit: TranscriptTransitionCommit, ) TranscriptRecoveryCommit { const source_offset = switch (runtime.transcript_commit_state) { .stable => |anchor| committedProjectionVisualOffset(anchor), .recovering => |receipt| receipt.accepted_visual_offset, .invalid => 0, }; const source_history_offset = switch (runtime.transcript_commit_state) { .stable => |anchor| anchor.history_visual_offset, .recovering => |receipt| receipt.accepted_history_visual_offset, .invalid => 0, }; const projection_base_offset = if (transition.recovery_rebase and !transition.recovery_progress_compatible) transition.visual_offset else source_offset; const accepted_visual_offset = projection_base_offset + commit.accepted.semantic_progress_rows; const materialized = resolveRecoveryMaterializedEndpoint(transition, result, commit); return .{ .accepted_visual_offset = accepted_visual_offset, .accepted_history_visual_offset = @min( transition.total_visual_rows, source_history_offset +| commit.accepted.semantic_rows, ), .materialized_visual_offset = materialized.visual_offset, .materialized_visual_rows = materialized.visual_rows, .materialized_flow_len = materialized.flow_len, .materialized_cursor_row = materialized.cursor_row, .materialized_cursor_col = materialized.cursor_col, .document_append_committed = transition.recovery_tracks_semantic_progress and materialized.visual_offset > accepted_visual_offset, }; } test "resume view snapshot captures visible transcript styling" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout = .{ .rows = 6, .cols = 12, .content_bottom = 3, .divider_top_row = 4, .input_row = 5, .divider_bottom_row = 5, .hint_row = 6, }; try runtime.enableShadowVt(alloc); try runtime.shadow_vt.?.feed( "\x1b[1;1Hnot cached\x1b[2;1H" ++ "\x1b]8;;https://example.com\x1b\\" ++ "\x1b[1;3;38;2;1;2;3m\x1b[48;5;245mvisible one\x1b[0m" ++ "\x1b]8;;\x1b\\\x1b[3;1H\x1b[31mvisible two\x1b[0m" ++ "\x1b[4;1Hfooter", ); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 2; runtime.last_visible_transcript_last_row = 3; var snapshot = (try runtime.snapshotVisibleTranscriptText(alloc, 128)).?; defer snapshot.deinit(alloc); try std.testing.expect(snapshot.complete); try std.testing.expectEqual(@as(u16, 6), snapshot.terminal_rows); try std.testing.expectEqual(@as(u16, 12), snapshot.terminal_cols); try std.testing.expectEqualStrings( "\x1b[0m\x1b[1m\x1b[3m\x1b[38;2;1;2;3m" ++ "\x1b[48;5;245mvisible one\x1b[0m\n" ++ "\x1b[0m\x1b[31mvisible two\x1b[0m\n", snapshot.text, ); } test "resume view snapshot preserves the committed footer gap" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout.rows = 6; runtime.layout.cols = 12; runtime.committed_frame_layout.footer_area = .{ .top = 4, .bottom = 6 }; try runtime.enableShadowVt(alloc); try runtime.shadow_vt.?.feed("\x1b[1;1Hvisible"); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 1; runtime.last_visible_transcript_last_row = 1; var snapshot = (try runtime.snapshotVisibleTranscriptText(alloc, 128)).?; defer snapshot.deinit(alloc); try std.testing.expect(snapshot.complete); try std.testing.expectEqualStrings("visible\n\n\n", snapshot.text); } test "resume view snapshot omits a visible leading welcome entry" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout = .{ .rows = 6, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 6, .hint_row = 6, }; var metrics: Metrics = .{}; try runtime.writeTranscriptClassified( alloc, &metrics, "fx · Run /help for commands\n\n", true, .welcome, ); try runtime.enableShadowVt(alloc); try runtime.shadow_vt.?.feed( "\x1b[1;1Hfx · Run /help for commands" ++ "\x1b[3;1Hvisible one\x1b[4;1Hvisible two", ); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 1; runtime.last_visible_transcript_last_row = 4; runtime.last_viewport_selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = 4, }; var snapshot = (try runtime.snapshotVisibleTranscriptText(alloc, 128)).?; defer snapshot.deinit(alloc); try std.testing.expect(snapshot.complete); try std.testing.expectEqualStrings("visible one\nvisible two\n", snapshot.text); } test "resume view snapshot keeps one complete UTF-8 row at the byte limit" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout.rows = 6; runtime.layout.cols = 12; try runtime.enableShadowVt(alloc); try runtime.shadow_vt.?.feed("\x1b[1;1Hééé"); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 1; runtime.last_visible_transcript_last_row = 1; var snapshot = (try runtime.snapshotVisibleTranscriptText(alloc, 7)).?; defer snapshot.deinit(alloc); try std.testing.expect(snapshot.complete); try std.testing.expectEqualStrings("ééé\n", snapshot.text); try std.testing.expect(std.unicode.utf8ValidateSlice(snapshot.text)); } test "resume view snapshot drops oldest rows and retains the newest tail" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout.rows = 6; runtime.layout.cols = 12; try runtime.enableShadowVt(alloc); try runtime.shadow_vt.?.feed( "\x1b[1;1Holdest\x1b[2;1Høø\x1b[3;1HTAIL", ); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 1; runtime.last_visible_transcript_last_row = 3; var snapshot = (try runtime.snapshotVisibleTranscriptText(alloc, 10)).?; defer snapshot.deinit(alloc); try std.testing.expect(!snapshot.complete); try std.testing.expectEqualStrings("øø\nTAIL\n", snapshot.text); } test "resume view snapshot returns no text for empty visible rows" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); runtime.layout.rows = 6; runtime.layout.cols = 12; try runtime.enableShadowVt(alloc); runtime.has_painted_transcript = true; runtime.last_visible_transcript_top_row = 1; runtime.last_visible_transcript_last_row = 2; try std.testing.expect( try runtime.snapshotVisibleTranscriptText(alloc, 128) == null, ); } fn acceptedTranscriptRows( scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, committed_terminal_scroll_rows: u16, resize_reflow_rows: u16, semantic_rows: u32, semantic_progress_rows: u32, ) AcceptedTranscriptRows { const accepted_scroll_rows = scroll_plan.commit(committed_terminal_scroll_rows).accepted_rows; return acceptedTranscriptRowsForInlineRows( accepted_scroll_rows.inline_advance_rows, resize_reflow_rows, semantic_rows, semantic_progress_rows, ); } fn acceptedTranscriptRowsForInlineRows( accepted_inline_rows: u32, resize_reflow_rows: u16, semantic_rows: u32, semantic_progress_rows: u32, ) AcceptedTranscriptRows { const accepted_resize_reflow_rows: u16 = @intCast(@min( accepted_inline_rows, @as(u32, resize_reflow_rows), )); const accepted_semantic_rows = @min( accepted_inline_rows - @as(u32, accepted_resize_reflow_rows), semantic_rows, ); std.debug.assert(semantic_progress_rows <= semantic_rows); const unprojected_semantic_rows = semantic_rows - semantic_progress_rows; const accepted_unprojected_semantic_rows = @min( accepted_semantic_rows, unprojected_semantic_rows, ); return .{ .resize_reflow_rows = accepted_resize_reflow_rows, .semantic_rows = accepted_semantic_rows, .semantic_progress_rows = accepted_semantic_rows - accepted_unprojected_semantic_rows, }; } fn resolveAndSealTranscriptTransitionForRuntimeTest( runtime: *TranscriptRuntime, alloc: Allocator, source: *TranscriptPreparationSource, prepared: *transcript_painter.PreparedTranscriptSurfacePaint, target_layout: render_engine.frame_layout.CommittedLayoutSnapshot, plan: *render_engine.paint_plan.PaintPlan, scroll_plan: render_engine.frame_scroll_plan.FrameScrollPlan, ) !TranscriptTransition { const scroll_facts = try runtime.prepareTranscriptScrollFactsForFrame( alloc, source, prepared, false, false, ); const destructive_invalidation = render_engine.frame_retention.transcriptAreaHasDestructiveInvalidation( runtime.committed_frame_layout.transcript_area, plan.invalidation, ); const resolved = try runtime.resolveTranscriptTransitionTargetForFrame( alloc, source, prepared, target_layout, scroll_plan, scroll_facts, destructive_invalidation, plan.activity == .overlay_entry, ); resolved.applyToPaintPlan(plan); return runtime.sealTranscriptTransition( alloc, source, prepared, plan, resolved, ); } fn testFrameCommitResult( committed_rows: u16, shadow_state: render_engine.terminal_diff.ShadowCommitState, ) render_engine.terminal_diff.FrameCommitResult { return .{ .bytes_written = 1, .changed_cells = 1, .full_repaint = false, .terminal_scroll_rows_committed = committed_rows, .document_append_committed = false, .committed_cursor_row = 1, .committed_cursor_col = 1, .shadow_state = shadow_state, .next_invalidation = render_engine.paint_plan.FrameInvalidationSet.empty(), }; } test "transition commit snapshot derives accepted rows and stable state from frame result" { const scroll_plan = render_engine.frame_scroll_plan.merge(8, 1, 0, 2); var transition = TranscriptTransition{ .scroll_plan = scroll_plan, .document_append = .{}, .materialized_without_append = .{}, .resize_reflow_rows = 1, .semantic_rows = 1, .semantic_progress_rows = 1, .target_flow = &.{}, .target_cache = .empty, .folded_summary_indices = &.{}, .target_layout = .{ .terminal_rows = 8, .terminal_cols = 40, }, .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = 1, }, .visual_offset = 0, .history_visual_offset = 0, .total_visual_rows = 2, .cursor_row = 4, .cursor_col = 1, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 4, .trace_transcript_frame = false, .trace_visible_rows = 4, }; const partial_result = testFrameCommitResult(1, .terminal_partial_write); const partial = resolveTranscriptTransitionCommit( &transition, partial_result, scroll_plan.commit(partial_result.terminal_scroll_rows_committed), ); try std.testing.expectEqual(@as(u16, 1), partial.accepted.resize_reflow_rows); try std.testing.expectEqual(@as(u32, 0), partial.accepted.semantic_rows); try std.testing.expectEqual(@as(u16, 0), partial.remaining_resize_reflow_rows); try std.testing.expectEqual(@as(u32, 1), partial.remaining_semantic_rows); try std.testing.expect(!partial.complete); try std.testing.expect(!partial.stable); const complete_result = testFrameCommitResult(2, .committed); const complete = resolveTranscriptTransitionCommit( &transition, complete_result, scroll_plan.commit(complete_result.terminal_scroll_rows_committed), ); try std.testing.expectEqual(@as(u16, 1), complete.accepted.resize_reflow_rows); try std.testing.expectEqual(@as(u32, 1), complete.accepted.semantic_rows); try std.testing.expectEqual(@as(u32, 1), complete.accepted.semantic_progress_rows); try std.testing.expectEqual(@as(u16, 0), complete.remaining_resize_reflow_rows); try std.testing.expectEqual(@as(u32, 0), complete.remaining_semantic_rows); try std.testing.expect(complete.complete); try std.testing.expect(complete.stable); transition.recovery_projection_deferred = true; const deferred_result = testFrameCommitResult(2, .committed); const deferred = resolveTranscriptTransitionCommit( &transition, deferred_result, scroll_plan.commit(deferred_result.terminal_scroll_rows_committed), ); try std.testing.expect(deferred.complete); try std.testing.expect(!deferred.stable); } test "resume recovery selects a safe materialized endpoint for every outcome" { var target_flow = [_]u8{ 'f', 'i', 'r', 's', 't', '\n', 's', 'e', 'c', 'o', 'n', 'd' }; const scroll_plan = render_engine.frame_scroll_plan.merge(8, 1, 0, 1); var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8), .committed_frame_layout = .{ .terminal_rows = 8, .terminal_cols = 80, .transcript_area = .{ .top = 1, .bottom = 8 }, }, }; defer runtime.deinit(std.testing.allocator); const document_append: render_engine.frame_scroll_plan.FrameDocumentAppend = .{ .bytes = "first\n", .start_row = 2, .start_col = 3, }; const planned: MaterializedEndpoint = .{ .visual_offset = 1, .visual_rows = 1, .flow_len = 6, .cursor_row = 3, .cursor_col = 1, }; var transition = TranscriptTransition{ .scroll_plan = scroll_plan, .document_append = document_append, .semantic_rows = 2, .semantic_progress_rows = 2, .recovery_progress_compatible = true, .recovery_tracks_semantic_progress = true, .target_flow = &target_flow, .target_cache = .empty, .folded_summary_indices = &.{}, .target_layout = .{ .terminal_rows = 8, .terminal_cols = 80, }, .selection = .{ .top_row = 1, .bottom_row = 8, .start_line = 0, .partial_skip_rows = 0, .line_count = 2, }, .visual_offset = 1, .history_visual_offset = 1, .total_visual_rows = 2, .materialized = planned, .materialized_without_append = runtime.materializedEndpointWithoutAppend( document_append, planned, ), .cursor_row = 3, .cursor_col = 1, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 3, .trace_transcript_frame = false, .trace_visible_rows = 2, }; const result = testFrameCommitResult(0, .terminal_partial_write); const commit = resolveTranscriptTransitionCommit( &transition, result, scroll_plan.commit(result.terminal_scroll_rows_committed), ); const unapplied = resolveTranscriptRecoveryCommit(&runtime, &transition, result, commit); try std.testing.expectEqual(@as(u32, 0), unapplied.materialized_visual_offset); try std.testing.expectEqual(@as(u16, 0), unapplied.materialized_visual_rows); try std.testing.expectEqual(@as(?usize, 0), unapplied.materialized_flow_len); try std.testing.expectEqual(@as(u16, 2), unapplied.materialized_cursor_row); try std.testing.expectEqual(@as(u16, 3), unapplied.materialized_cursor_col); try std.testing.expectEqualDeep( transition.materialized_without_append, resolveRecoveryMaterializedEndpoint(&transition, result, commit), ); var complete_commit = commit; complete_commit.complete = true; try std.testing.expectEqualDeep( planned, resolveRecoveryMaterializedEndpoint(&transition, result, complete_commit), ); var appended_result = result; appended_result.document_append_committed = true; try std.testing.expectEqualDeep( planned, resolveRecoveryMaterializedEndpoint(&transition, appended_result, commit), ); transition.recovery_progress_compatible = false; try std.testing.expectEqualDeep( planned, resolveRecoveryMaterializedEndpoint(&transition, result, commit), ); transition.recovery_progress_compatible = true; transition.recovery_tracks_semantic_progress = false; try std.testing.expectEqualDeep( planned, resolveRecoveryMaterializedEndpoint(&transition, result, commit), ); transition.recovery_tracks_semantic_progress = true; transition.recovery_rebase = true; try std.testing.expectEqualDeep( planned, resolveRecoveryMaterializedEndpoint(&transition, result, commit), ); transition.recovery_rebase = false; transition.document_append = .{}; transition.materialized_without_append = runtime.materializedEndpointWithoutAppend( .{}, planned, ); const absent = resolveTranscriptRecoveryCommit(&runtime, &transition, result, commit); try std.testing.expectEqual(@as(u32, 1), absent.materialized_visual_offset); try std.testing.expectEqual(@as(u16, 1), absent.materialized_visual_rows); try std.testing.expectEqual(@as(?usize, 6), absent.materialized_flow_len); try std.testing.expectEqual(@as(u16, 3), absent.materialized_cursor_row); try std.testing.expectEqual(@as(u16, 1), absent.materialized_cursor_col); } test "partial resume publication retains its cursor and requests the next frame" { const alloc = std.testing.allocator; const scroll_plan = render_engine.frame_scroll_plan.merge(8, 1, 0, 128); var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8) }; defer runtime.deinit(alloc); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, "pending"), ); var transition = TranscriptTransition{ .scroll_plan = scroll_plan, .document_append = .{}, .materialized_without_append = .{}, .semantic_rows = 256, .semantic_progress_rows = 256, .recovery_progress_compatible = true, .recovery_tracks_semantic_progress = true, .target_flow = runtime.pending_resume_source.?.bytes, .target_flow_owned = false, .target_cache = .empty, .target_cache_unchanged = true, .folded_summary_indices = &.{}, .target_layout = .{ .terminal_rows = 8, .terminal_cols = 40 }, .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 252, .partial_skip_rows = 0, .line_count = 256, }, .visual_offset = 256, .history_visual_offset = 128, .source_endpoint_visual_offset = 256, .total_visual_rows = 256, .cursor_row = 4, .cursor_col = 1, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 4, .trace_transcript_frame = false, .trace_visible_rows = 4, }; defer transition.deinit(alloc); runtime.consumeTranscriptTransition( alloc, &transition, testFrameCommitResult(128, .committed), ); switch (runtime.transcript_commit_state) { .recovering => |receipt| { try std.testing.expectEqual(@as(u32, 128), receipt.accepted_visual_offset); try std.testing.expectEqual(@as(u32, 128), receipt.remaining_semantic_rows); }, .invalid, .stable => return error.TestExpectedRecoveringTranscript, } try std.testing.expectEqualStrings("pending", runtime.pendingResumeFlow()); try std.testing.expect(runtime.render_requests.hasReason(.transcript)); } test "owned stable transition traces a superseded pending resume source" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const root = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(root); const trace_path = try std.fs.path.join(alloc, &.{ root, "resume-source-drop.log" }); defer alloc.free(trace_path); debug_trace.resetForTest(); defer debug_trace.resetForTest(); try debug_trace.configureForTestWithScopes(alloc, trace_path, "session"); var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8) }; defer runtime.deinit(alloc); runtime.bindDetachedCommitAllocator(alloc); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, "stale"), ); var transition = TranscriptTransition{ .scroll_plan = render_engine.frame_scroll_plan.FrameScrollPlan.none(8, 1), .document_append = .{}, .target_flow = try alloc.dupe(u8, "fresh"), .target_cache = .empty, .folded_summary_indices = &.{}, .target_layout = .{ .terminal_rows = 8, .terminal_cols = 80 }, .selection = .{ .top_row = 1, .bottom_row = 1, .start_line = 0, .partial_skip_rows = 0, .line_count = 1, }, .visual_offset = 0, .history_visual_offset = 0, .total_visual_rows = 1, .materialized_without_append = .{}, .cursor_row = 1, .cursor_col = 6, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 1, .trace_transcript_frame = false, .trace_visible_rows = 1, }; defer transition.deinit(alloc); runtime.consumeTranscriptTransition( alloc, &transition, testFrameCommitResult(0, .committed), ); debug_trace.shutdown(); var trace_file = try std.Io.Dir.openFileAbsolute(std.testing.io, trace_path, .{}); defer trace_file.close(std.testing.io); const trace = try io_mod.readFileToEnd(alloc, &trace_file, 4096); defer alloc.free(trace); try std.testing.expect(std.mem.find( u8, trace, "pending resume flow dropped reason=superseded bytes=5", ) != null); } test "stable resume publication starts a fresh retained source epoch" { const alloc = std.testing.allocator; const retained_flow = "tail-1\n" ++ "tail-2\n" ++ "tail-3\n" ++ "tail-final"; const full_flow = "old-1\nold-2\nold-3\nold-4\n" ++ "old-5\nold-6\nold-7\nold-8\n" ++ retained_flow; const layout: Layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }; var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1, .viewport_top_row = 1, }; defer runtime.deinit(alloc); try runtime.enableShadowVt(alloc); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, full_flow), ); try runtime.transcript.appendSlice(alloc, retained_flow); var transition = TranscriptTransition{ .scroll_plan = render_engine.frame_scroll_plan.FrameScrollPlan.none( layout.rows, runtime.owned_top_row, ), .document_append = .{}, .materialized_without_append = .{}, .target_flow = runtime.pending_resume_source.?.bytes, .target_flow_owned = false, .target_cache = .empty, .target_cache_unchanged = true, .folded_summary_indices = &.{}, .target_layout = .{ .layout_id = 17, .owned_top = 1, .owned_band = .{ .top = 1, .bottom = 8 }, .body_area = .{ .top = 1, .bottom = 4 }, .transcript_area = .{ .top = 1, .bottom = 4 }, .footer_area = .{ .top = 5, .bottom = 8 }, .solved_frame_height = 8, .terminal_rows = 8, .terminal_cols = 40, }, .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 8, .partial_skip_rows = 0, .line_count = 12, .last_visible_row = 4, }, .visual_offset = 8, .history_visual_offset = 8, .total_visual_rows = 12, .cursor_row = 4, .cursor_col = 11, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 4, .trace_transcript_frame = false, .trace_visible_rows = 4, }; defer transition.deinit(alloc); runtime.consumeTranscriptTransition( alloc, &transition, testFrameCommitResult(0, .committed), ); try std.testing.expectEqual(@as(usize, 0), runtime.pendingResumeFlow().len); try std.testing.expectEqualStrings(retained_flow, runtime.transcript.items); try std.testing.expectEqual( TranscriptCommitDiagnosticState.invalid, runtime.transcriptCommitDiagnostic().state, ); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); try std.testing.expectEqualStrings(retained_flow, source.bytes); var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &source, .{ .top = 1, .bottom = 4 }, ); defer prepared.deinit(alloc); try std.testing.expectEqual(@as(usize, 0), prepared.selection.start_line); try std.testing.expectEqual(@as(usize, 4), prepared.selection.line_count); try std.testing.expectEqual(@as(u16, 4), prepared.cursor.cursor_row); const facts = runtime.planTranscriptScroll(&prepared); try std.testing.expectEqual(@as(u16, 0), facts.planned_rows); try std.testing.expectEqual(@as(u32, 0), facts.target_visual_offset); try std.testing.expect(facts.recovery_rebase); } test "oversized resume publication starts without a prior committed frame" { const alloc = std.testing.allocator; const layout = invalidationTestLayout(); var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1, }; defer runtime.deinit(alloc); var flow: std.ArrayList(u8) = .empty; defer flow.deinit(alloc); for (0..@as(usize, std.math.maxInt(u16)) + 21) |_| { try flow.appendSlice(alloc, "row\n"); } runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, flow.items), ); var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &source, .{ .top = 1, .bottom = layout.content_bottom }, ); defer prepared.deinit(alloc); const facts = runtime.planTranscriptScroll(&prepared); try std.testing.expect(facts.target_visual_offset > std.math.maxInt(u16)); try std.testing.expectEqual(@as(u32, std.math.maxInt(u16)), facts.semantic_rows); try std.testing.expectEqual( @as(u16, resume_publication_rows_per_frame), facts.planned_rows, ); try std.testing.expect(facts.recovery_progress_compatible); try std.testing.expect(facts.recovery_tracks_semantic_progress); const scroll_plan = render_engine.frame_scroll_plan.merge( layout.rows, runtime.owned_top_row, 0, facts.planned_rows, ); const accepted = acceptedTranscriptRows( scroll_plan, scroll_plan.terminal_scroll_rows, facts.resize_reflow_rows, facts.semantic_rows, facts.semantic_progress_rows, ); try std.testing.expectEqual( @as(u32, resume_publication_rows_per_frame), accepted.semantic_rows, ); try std.testing.expect(accepted.semantic_rows < facts.semantic_rows); const append_base = (try runtime.resolveTransitionAppendBase( &prepared, scroll_plan.prior_owned_top, facts, false, source.bytes, facts.target_visual_offset, )) orelse return error.TestExpectedResumeAppendBase; try std.testing.expectEqual(@as(usize, 0), append_base.flow_len); } test "pending tail projection seals against the complete frame layout" { const alloc = std.testing.allocator; const layout = invalidationTestLayout(); var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1, }; defer runtime.deinit(alloc); var flow: std.ArrayList(u8) = .empty; defer flow.deinit(alloc); for (0..100) |_| try flow.appendSlice(alloc, "row\n"); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, flow.items), ); const candidate = render_engine.frame_layout.solve(.{ .terminal = layout, .owned_top = runtime.owned_top_row, .footer = .{ .natural_rows = 3, .min_rows = 3, .max_rows = 3, }, .transcript = runtime.pending_resume_source.?.preview, .prior = runtime.committed_frame_layout, }); const projection_area = render_engine.frame_layout.FrameRect{ .top = candidate.transcript_area.top, .bottom = candidate.transcript_area.bottom - 1, }; const source = (try runtime.pendingResumeSourceInterruptible(alloc, null)) orelse return error.TestExpectedPendingResumeSource; var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, source, projection_area, ); errdefer prepared.deinit(alloc); const facts = try runtime.prepareTranscriptScrollFactsForFrame( alloc, source, &prepared, false, false, ); const scroll_plan = render_engine.frame_scroll_plan.merge( layout.rows, runtime.owned_top_row, 0, facts.planned_rows, ); const footer_rows = render_engine.footer_layout.resolve(.{ .footer_top_for_extra = candidate.footer_area.top, .terminal_rows = layout.rows, .activity_offset = 0, .extra_input_rows = 0, .input_extra = 0, .composer_top_chrome_rows = 0, .picker_rows = 0, .banner_active = false, }); var plan = candidate.toPaintPlan(.{ .footer_rows = footer_rows, .viewport = prepared.selection, .cursor_target = .{ .row = prepared.cursor.cursor_row, .col = prepared.cursor.cursor_col, .visible = true, }, }); const target_layout = render_engine.frame_layout.CommittedLayoutSnapshot.fromLayout(candidate); try std.testing.expectError( error.InvalidTranscriptTransition, runtime.resolveTranscriptTransitionTargetForFrameInArea( alloc, source, &prepared, target_layout, .{ .top = projection_area.top, .bottom = target_layout.transcript_area.bottom + 1, }, scroll_plan, facts, false, false, ), ); const resolved = try runtime.resolveTranscriptTransitionTargetForFrameInArea( alloc, source, &prepared, target_layout, projection_area, scroll_plan, facts, false, false, ); try std.testing.expectEqual(projection_area.bottom, resolved.selection().bottom_row); try std.testing.expectEqual( candidate.transcript_area.bottom, resolved.target_layout.transcript_area.bottom, ); resolved.applyToPaintPlan(&plan); var transition = try runtime.sealTranscriptTransition( alloc, source, &prepared, &plan, resolved, ); prepared.deinit(alloc); defer transition.deinit(alloc); try std.testing.expectEqual( candidate.transcript_area.bottom, transition.target_layout.transcript_area.bottom, ); } test "history recovery preserves retained geometry and stages painted frames" { const alloc = std.testing.allocator; for ([_]bool{ false, true }) |repaint| { const layout = invalidationTestLayout(); var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1 }; defer runtime.deinit(alloc); var flow: std.ArrayList(u8) = .empty; defer flow.deinit(alloc); for (0..79) |_| try flow.appendSlice(alloc, "row\n"); try flow.appendSlice(alloc, "row"); var source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, flow.items), ); defer source.deinit(alloc); const activity = render_engine.frame_layout.ActivityState{ .thinking = .{ .gap_above_activity = 0, .footer_gap_after_activity = 1, } }; const frame_input = render_engine.frame_layout.SolveInput{ .terminal = layout, .owned_top = 1, .footer = .{ .natural_rows = 3, .min_rows = 3, .max_rows = 3 }, .transcript = source.preview, .activity = activity, }; const candidate = render_engine.frame_layout.solve(frame_input); var prior_input = frame_input; if (repaint) prior_input.footer = .{ .natural_rows = 5, .min_rows = 5, .max_rows = 5 }; const committed = render_engine.frame_layout.CommittedLayoutSnapshot.fromLayout( render_engine.frame_layout.solve(prior_input), ); runtime.committed_frame_layout = committed; const committed_selection = ViewportSelection{ .top_row = 1, .bottom_row = 16, .start_line = 62, .partial_skip_rows = 0, .line_count = 80, .last_visible_row = 16, .replaceable_start_row = 1, }; // A pending card left fewer canonical rows in the committed frame. runtime.transcript_commit_state = .{ .stable = .{ .selection = committed_selection, .visual_offset = 64, .history_visual_offset = 62, .total_visual_rows = 80, .flow = try alloc.dupe(u8, flow.items), .cursor_row = 17, .cursor_col = 1, .occupied_last_row = 16, .occupied_last_row_blank = false, .layout_id = committed.layout_id, .normal_buffer_recovery_pending = true, } }; const area = render_engine.frame_layout.FrameRect{ .top = 1, .bottom = if (repaint) 16 else 18 }; var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForFrame( alloc, &metrics, &source, area, repaint, ); defer prepared.deinit(alloc); const facts = try runtime.prepareTranscriptScrollFactsForFrame( alloc, &source, &prepared, repaint, false, ); const scroll = render_engine.frame_scroll_plan.merge(layout.rows, 1, 0, facts.planned_rows); if (repaint) { try std.testing.expectEqual(@as(u32, 64), facts.target_visual_offset); try std.testing.expectEqual(@as(u32, 0), facts.semantic_rows); } const resolved = try runtime.resolveTranscriptTransitionTargetForFrameInArea( alloc, &source, &prepared, render_engine.frame_layout.CommittedLayoutSnapshot.fromLayout(candidate), area, scroll, facts, repaint, false, ); if (repaint) { try std.testing.expect(resolved.bodyDisposition() == .paint); try std.testing.expectEqual(@as(usize, 62), resolved.selection().start_line); try std.testing.expectEqual(@as(u16, 17), resolved.cursorRow()); try std.testing.expectEqual(@as(u16, 1), resolved.cursorCol()); try std.testing.expectEqual(@as(u16, 16), resolved.selection().last_visible_row); } else { try std.testing.expect(resolved.bodyDisposition() == .retain_committed); try std.testing.expectEqualDeep(committed_selection, resolved.selection()); try std.testing.expectEqual(@as(u16, 17), resolved.cursorRow()); try std.testing.expectEqual(@as(u16, 1), resolved.cursorCol()); } const footer_rows = render_engine.footer_layout.resolve(.{ .footer_top_for_extra = candidate.footer_area.top, .terminal_rows = layout.rows, .activity_offset = 0, .extra_input_rows = 0, .input_extra = 0, .composer_top_chrome_rows = 0, .picker_rows = 0, .banner_active = false, }); var plan = candidate.toPaintPlan(.{ .footer_rows = footer_rows, .viewport = resolved.selection(), .activity = render_engine.activity_placement.resolve( .{ .turn_thinking = .{ .label = "Thinking" } }, activity, candidate, ), .cursor_target = .{ .row = resolved.cursorRow(), .col = resolved.cursorCol(), .visible = true, }, }); if (repaint) try plan.invalidation.append(.{ .reason = .external_clear, .top = 1, .bottom = 18 }); var transition = try runtime.sealTranscriptTransition(alloc, &source, &prepared, &plan, resolved); defer transition.deinit(alloc); try std.testing.expectEqualDeep(resolved.selection(), transition.selection); try std.testing.expectEqual(resolved.cursorRow(), transition.cursor_row); try std.testing.expectEqual(resolved.cursorCol(), transition.cursor_col); } } test "transition commit keeps unplanned physical scroll as recovery debt" { const scroll_plan = render_engine.frame_scroll_plan.merge(8, 1, 0, 2); var transition = TranscriptTransition{ .scroll_plan = scroll_plan, .document_append = .{}, .materialized_without_append = .{}, .semantic_rows = 2, .semantic_progress_rows = 2, .target_flow = &.{}, .target_cache = .empty, .folded_summary_indices = &.{}, .target_layout = .{ .terminal_rows = 8, .terminal_cols = 40, }, .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 0, .partial_skip_rows = 0, .line_count = 2, }, .visual_offset = 0, .history_visual_offset = 0, .total_visual_rows = 2, .cursor_row = 4, .cursor_col = 1, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 4, .trace_transcript_frame = false, .trace_visible_rows = 4, }; const result = testFrameCommitResult(scroll_plan.terminal_scroll_rows + 1, .committed); const commit = resolveTranscriptTransitionCommit( &transition, result, scroll_plan.commit(result.terminal_scroll_rows_committed), ); try std.testing.expectEqual(@as(u32, 2), commit.accepted.semantic_rows); try std.testing.expectEqual(@as(u32, 0), commit.remaining_semantic_rows); try std.testing.expectEqual(@as(u16, 1), commit.remaining_unplanned_scroll_rows); try std.testing.expect(!commit.complete); try std.testing.expect(!commit.stable); var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8), .owned_top_row = 1, .viewport_top_row = 1, }; defer runtime.deinit(std.testing.allocator); runtime.consumeFrameScrollCommit( std.testing.allocator, scroll_plan, result, &transition, ); const diagnostic = runtime.transcriptCommitDiagnostic(); try std.testing.expectEqual(TranscriptCommitDiagnosticState.recovering, diagnostic.state); try std.testing.expectEqual(@as(u16, 1), diagnostic.remaining_unplanned_scroll_rows); } fn frameInlineScrollBudget(resize_reflow_rows: u16, semantic_rows: u32) u16 { return frameInlineScrollBudgetCapped( resize_reflow_rows, semantic_rows, std.math.maxInt(u32), ); } test "pending resume publication yields after a bounded row batch" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); const prepared = transcript_painter.PreparedTranscriptSurfacePaint{ .selection = .{ .top_row = 1, .bottom_row = 1, .start_line = 0, .partial_skip_rows = 0, .line_count = 1, } }; try std.testing.expectEqual( @as(u16, 1_000), runtime.plannedTranscriptRows(&prepared, 0, 0, 1_000, 0), ); runtime.pending_resume_source = try source_preparation.prepareFullTranscriptViewportSource( &runtime, alloc, try alloc.dupe(u8, "pending"), ); try std.testing.expectEqual( @as(u16, resume_publication_rows_per_frame), runtime.plannedTranscriptRows(&prepared, 0, 0, 1_000, 0), ); } fn frameInlineScrollBudgetCapped( resize_reflow_rows: u16, semantic_rows: u32, semantic_cap: u32, ) u16 { const semantic_budget = @min( @min(semantic_rows, semantic_cap), @as(u32, std.math.maxInt(u16) - resize_reflow_rows), ); return resize_reflow_rows + @as(u16, @intCast(semantic_budget)); } fn projectionVisualRows(total_visual_rows: u32, visual_offset: u32) u16 { return @intCast(@min( total_visual_rows -| visual_offset, @as(u32, std.math.maxInt(u16)), )); } fn projectionVisualRowsInArea( total_visual_rows: u32, visual_offset: u32, area: render_engine.frame_layout.FrameRect, ) u16 { if (area.isEmpty()) return 0; return @min( projectionVisualRows(total_visual_rows, visual_offset), area.height(), ); } fn visualOffsetForSelection(line_rows: []const u16, selection: ViewportSelection) u32 { var offset: u32 = 0; const end = @min(selection.start_line, line_rows.len); for (line_rows[0..end]) |rows| { offset += rows; } return offset + selection.partial_skip_rows; } fn reflowedRows( grid: *const vt_emulator.Grid, start_row: u16, end_row: u16, target_cols: u16, include_cursor: bool, ) u32 { if (target_cols == 0 or start_row == 0 or start_row > end_row or start_row > grid.rows) return 0; var total: u32 = 0; var row = start_row; const last_row = @min(end_row, grid.rows); while (row <= last_row) : (row += 1) { var occupied_col: u16 = 0; var col: u16 = 1; while (col <= grid.cols) : (col += 1) { const cell = grid.cellAt(row, col).?; if (cell.codepoint != ' ' or cell.combining_suffix_id != 0 or !cell.style.eql(.{})) { occupied_col = col; } } if (include_cursor and row == grid.cursor_row) { occupied_col = @max(occupied_col, grid.cursor_col); } total += if (occupied_col == 0) 1 else (@as(u32, occupied_col) - 1) / target_cols + 1; } return total; } test "reflowed rows count a combining suffix attached to a space" { var grid = try vt_emulator.Grid.init(std.testing.allocator, 4, 1); defer grid.deinit(); try grid.feed(" \u{0301}"); try std.testing.expectEqual( @as(u32, 2), reflowedRows(&grid, 1, 1, 2, false), ); } fn invalidationTestLayout() Layout { return .{ .rows = 24, .cols = 80, .content_bottom = 20, .divider_top_row = 21, .input_row = 22, .divider_bottom_row = 23, .hint_row = 24, }; } test "frame invalidations move into one request snapshot" { var runtime = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5 }; defer runtime.deinit(std.testing.allocator); try runtime.recordFrameInvalidation(.{ .reason = .resize, .top = 5, .bottom = 24 }); try runtime.recordFrameInvalidation(.{ .reason = .external_clear, .top = 21, .bottom = 24 }); var attempt = (try runtime.render_requests.beginAttempt()).?; try std.testing.expectEqual(@as(u8, 2), attempt.snapshot.invalidations.len); try std.testing.expectEqual(render_engine.paint_plan.FrameInvalidationReason.resize, attempt.snapshot.invalidations.ranges()[0].reason); try std.testing.expectEqual(render_engine.paint_plan.FrameInvalidationReason.external_clear, attempt.snapshot.invalidations.ranges()[1].reason); attempt.commit(0, render_request.animation_interval_ms, false); try std.testing.expect((try runtime.render_requests.beginAttempt()) == null); } test "recording a frame invalidation schedules request-owned work" { var runtime = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5 }; defer runtime.deinit(std.testing.allocator); try runtime.recordFrameInvalidation(.{ .reason = .external_clear, .top = 5, .bottom = 24 }); try std.testing.expect(runtime.render_requests.hasReason(.external_damage)); } test "terminal reset drops stale mid-scrollback footer clear before reanchor" { var metrics: Metrics = .{}; var runtime = TranscriptRuntime{ .layout = .{ .rows = 36, .cols = 120, .content_bottom = 32, .divider_top_row = 33, .input_row = 34, .divider_bottom_row = 35, .hint_row = 36, }, .owned_top_row = 22, .viewport_top_row = 22, .has_painted_transcript = true, .min_visible_viewport_rows = 11, }; defer runtime.deinit(std.testing.allocator); runtime.footer_viewport.has_frame = true; runtime.footer_viewport.geometry.top = 25; runtime.footer_viewport.externally_invalidated = true; try runtime.recordFrameInvalidation(.{ .reason = .external_clear, .top = 25, .bottom = 36, }); try runtime.requestTerminalReset(&metrics); try std.testing.expectEqual(@as(u16, 1), runtime.owned_top_row); try std.testing.expectEqual(@as(u16, 1), runtime.viewport_top_row); try std.testing.expect(!runtime.footer_viewport.has_frame); try std.testing.expect(!runtime.footer_viewport.externally_invalidated); const pending = runtime.render_requests.pendingInvalidations(); try std.testing.expectEqual(@as(u8, 1), pending.len); try std.testing.expectEqual( render_engine.paint_plan.FrameInvalidationReason.viewport_reanchor, pending.ranges()[0].reason, ); try std.testing.expectEqual(@as(u16, 1), pending.ranges()[0].top); try std.testing.expectEqual(@as(u16, 36), pending.ranges()[0].bottom); var plan = render_engine.paint_plan.PaintPlan{ .layout = runtime.layout, .viewport = .{ .top_row = 1, .bottom_row = 3, .start_line = 0, .partial_skip_rows = 0, .line_count = 3, .last_visible_row = 3, .last_visible_row_blank = false, .tail_kind = null, .replaceable_start_row = 1, .split_active = false, .split_prefix_lines = 0, .split_suffix_start_line = 0, }, .footer = .{ .top = 4, .top_divider = 4, .banner = 4, .banner_active = false, .input_base = 5, .picker_divider = 6, .picker_start = 7, .bottom_divider = 6, .hint = 7, .total_rows = 4, }, .activity = .none, .preserved_band = render_engine.paint_plan.FrameBand.empty(.preserved_shell), .transcript_band = .{ .top = 1, .bottom = 3, .owner = .transcript }, .blank_band = render_engine.paint_plan.FrameBand.empty(.gap), .activity_band = render_engine.paint_plan.FrameBand.empty(.activity), .footer_gap_band = render_engine.paint_plan.FrameBand.empty(.gap), .footer_band = .{ .top = 4, .bottom = 7, .owner = .footer }, .invalidation = pending, .footer_clean_allowed = false, .synchronized_update = false, .cursor_target = null, .bottom_reserved_rows = 0, .preserve_scrollback = true, }; try plan.validate(); try std.testing.expectError( error.InvalidInvalidationRange, render_engine.paint_plan.validateInvalidationRange(plan, .{ .reason = .external_clear, .top = 25, .bottom = 36, }), ); } test "marking transcript cache dirty also schedules transcript work" { var runtime = TranscriptRuntime{}; defer runtime.deinit(std.testing.allocator); runtime.markTranscriptDirty(); try std.testing.expect(runtime.transcript_band_dirty); try std.testing.expect(runtime.render_requests.hasReason(.transcript)); } test "frame invalidation overflow collapses to owned band with dominant reason" { var runtime = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 7 }; defer runtime.deinit(std.testing.allocator); var i: u8 = 0; while (i < render_engine.paint_plan.max_frame_invalidation_ranges) : (i += 1) { const row: u16 = 7 + i; try runtime.recordFrameInvalidation(.{ .reason = .external_clear, .top = row, .bottom = row }); } try runtime.recordFrameInvalidation(.{ .reason = .shadow_feed_failure, .top = 12, .bottom = 12 }); const pending = runtime.render_requests.pendingInvalidations(); try std.testing.expectEqual(@as(u8, 1), pending.len); try std.testing.expectEqual(render_engine.paint_plan.FrameInvalidationReason.shadow_feed_failure, pending.ranges()[0].reason); try std.testing.expectEqual(@as(u16, 7), pending.ranges()[0].top); try std.testing.expectEqual(@as(u16, 24), pending.ranges()[0].bottom); } test "preserved row acknowledgment lowers ownership and viewport boundaries" { var runtime = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5, .viewport_top_row = 7, }; defer runtime.deinit(std.testing.allocator); const scroll_plan = render_engine.frame_scroll_plan.merge(runtime.layout.rows, runtime.owned_top_row, 2, 0); try runtime.ackPreservedRowRelease(scroll_plan, 2); try std.testing.expectEqual(@as(u16, 3), runtime.owned_top_row); try std.testing.expectEqual(@as(u16, 5), runtime.viewport_top_row); } test "preserved row acknowledgment applies only committed rows inside the plan" { var partial = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5, .viewport_top_row = 7, }; defer partial.deinit(std.testing.allocator); const scroll_plan = render_engine.frame_scroll_plan.merge(partial.layout.rows, partial.owned_top_row, 2, 0); try partial.ackPreservedRowRelease(scroll_plan, 1); try std.testing.expectEqual(@as(u16, 4), partial.owned_top_row); try std.testing.expectEqual(@as(u16, 6), partial.viewport_top_row); var above_plan = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5, .viewport_top_row = 7, }; defer above_plan.deinit(std.testing.allocator); try above_plan.ackPreservedRowRelease(scroll_plan, 20); try std.testing.expectEqual(@as(u16, 3), above_plan.owned_top_row); try std.testing.expectEqual(@as(u16, 5), above_plan.viewport_top_row); } test "preserved row acknowledgment normalizes stale resize boundaries before release" { var full = TranscriptRuntime{ .layout = testLayoutWithRows(5), .owned_top_row = 8, .viewport_top_row = 9, }; defer full.deinit(std.testing.allocator); const full_plan = render_engine.frame_scroll_plan.merge(full.layout.rows, full.owned_top_row, 4, 0); try full.ackPreservedRowRelease(full_plan, 4); try std.testing.expectEqual(@as(u16, 1), full.owned_top_row); try std.testing.expectEqual(@as(u16, 1), full.viewport_top_row); var partial = TranscriptRuntime{ .layout = testLayoutWithRows(5), .owned_top_row = 8, .viewport_top_row = 9, }; defer partial.deinit(std.testing.allocator); const partial_plan = render_engine.frame_scroll_plan.merge(partial.layout.rows, partial.owned_top_row, 4, 0); try partial.ackPreservedRowRelease(partial_plan, 2); try std.testing.expectEqual(@as(u16, 3), partial.owned_top_row); try std.testing.expectEqual(@as(u16, 3), partial.viewport_top_row); } test "preserved row acknowledgment normalizes shrink boundaries even without accepted release" { var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(5), .owned_top_row = 8, .viewport_top_row = 9, }; defer runtime.deinit(std.testing.allocator); const scroll_plan = render_engine.frame_scroll_plan.FrameScrollPlan.none(runtime.layout.rows, runtime.owned_top_row); try runtime.ackPreservedRowRelease(scroll_plan, 0); try std.testing.expectEqual(@as(u16, 5), runtime.owned_top_row); try std.testing.expectEqual(@as(u16, 5), runtime.viewport_top_row); } test "preserved row acknowledgment keeps viewport at or below owned top boundary" { var runtime = TranscriptRuntime{ .layout = invalidationTestLayout(), .owned_top_row = 5, .viewport_top_row = 2, }; defer runtime.deinit(std.testing.allocator); const scroll_plan = render_engine.frame_scroll_plan.merge(runtime.layout.rows, runtime.owned_top_row, 4, 0); try runtime.ackPreservedRowRelease(scroll_plan, 4); try std.testing.expectEqual(@as(u16, 1), runtime.owned_top_row); try std.testing.expectEqual(@as(u16, 1), runtime.viewport_top_row); } test "transcript runtime owns render request state" { try std.testing.expect(@hasField(TranscriptRuntime, "render_requests")); } test "recovery append prefix requires exact materialized bytes" { const alloc = std.testing.allocator; const receipt_flow = try alloc.dupe(u8, "prefix-good"); defer alloc.free(receipt_flow); const receipt = TranscriptRecoveryReceipt{ .flow = receipt_flow, .materialized_flow_len = 7, }; try std.testing.expect(recoveryAppendPrefixMatches("prefix-next", receipt, 7)); try std.testing.expect(!recoveryAppendPrefixMatches("prefiy-next", receipt, 7)); try std.testing.expect(!recoveryAppendPrefixMatches("short", receipt, 7)); } test "measured recovery rebases from accepted projection before retiring resize debt" { const alloc = std.testing.allocator; const flow = "aaaaaaaaaaaaaaaaaaaa\n" ++ "bbbbbbbbbbbbbbbbbbbb\n" ++ "cccccccccccccccccccc\n" ++ "dddddddddddddddddddd\n" ++ "eeeeeeeeeeeeeeeeeeee\n" ++ "ffffffffffffffffffff\n" ++ "gggggggggggggggggggg\n" ++ "hhhhhhhhhhhhhhhhhhhh\n"; const layout: Layout = .{ .rows = 8, .cols = 10, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }; var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1, .resize_history_row_delta = 2, .committed_frame_layout = .{ .layout_id = 17, .owned_top = 1, .owned_band = .{ .top = 1, .bottom = 8 }, .body_area = .{ .top = 1, .bottom = 4 }, .transcript_area = .{ .top = 1, .bottom = 4 }, .footer_area = .{ .top = 5, .bottom = 8 }, .solved_frame_height = 8, .terminal_rows = 8, .terminal_cols = 20, }, }; defer runtime.deinit(alloc); try runtime.transcript.appendSlice(alloc, flow); runtime.transcript_commit_state = .{ .recovering = .{ .accepted_visual_offset = 6, .accepted_history_visual_offset = 6, .attempt_visual_offset = 8, .attempt_total_visual_rows = 8, .materialized_visual_offset = 7, .materialized_visual_rows = 1, .materialized_flow_len = flow.len, .remaining_resize_reflow_rows = 3, .remaining_semantic_rows = 2, .remaining_semantic_progress_rows = 2, .attempt_cols = 20, .projection_rebase_pending = false, .tracks_semantic_progress = true, .flow = try alloc.dupe(u8, flow), .cursor_row = 4, .cursor_col = 1, } }; const projection = runtime.measuredResizeProjectionForFlow(flow) orelse return error.TestExpectedMeasuredResizeProjection; switch (projection) { .stable => return error.TestExpectedRecoveringProjection, .recovering => |recovering| { try std.testing.expectEqual( @as(u32, 6), recovering.accepted_visual_offset, ); try std.testing.expectEqual(@as(u16, 20), recovering.source_cols); }, } var source = try runtime.prepareTranscriptSource(alloc, null); defer source.deinit(alloc); var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &source, .{ .top = 1, .bottom = 4 }, ); defer prepared.deinit(alloc); try std.testing.expectEqual( transcript_painter.MeasuredResizeTargetProvenance.recovering, prepared.measured_resize_target_provenance, ); const endpoint = prepared.measured_history_origin.?.endpoint orelse return error.TestExpectedMeasuredHistoryEndpoint; try std.testing.expectEqual(@as(usize, 147), endpoint.flow_offset); try std.testing.expectEqual( @as(u32, 14), visualOffsetForSelection(prepared.sourceVisualRows(), prepared.selection), ); const facts = runtime.planTranscriptScroll(&prepared); try std.testing.expectEqual(@as(u16, 0), facts.resize_reflow_rows); try std.testing.expectEqual(@as(u16, 2), facts.semantic_rows); try std.testing.expectEqual(@as(u32, 14), facts.target_visual_offset); const scroll_plan = render_engine.frame_scroll_plan.merge( runtime.layout.rows, runtime.owned_top_row, 0, facts.planned_rows, ); var plan: render_engine.paint_plan.PaintPlan = .{ .layout = runtime.layout, .viewport = prepared.selection, .footer = .{ .top = 5, .top_divider = 5, .banner = 5, .banner_active = false, .input_base = 6, .picker_divider = 7, .picker_start = 8, .bottom_divider = 7, .hint = 8, .total_rows = 4, }, .activity = .none, .preserved_band = render_engine.paint_plan.FrameBand.empty(.preserved_shell), .transcript_band = .{ .top = 1, .bottom = 4, .owner = .transcript }, .activity_band = render_engine.paint_plan.FrameBand.empty(.activity), .footer_band = .{ .top = 5, .bottom = 8, .owner = .footer }, .invalidation = render_engine.paint_plan.FrameInvalidationSet.empty(), .footer_clean_allowed = true, .synchronized_update = true, .cursor_target = .{ .row = prepared.cursor.cursor_row, .col = prepared.cursor.cursor_col, .visible = true, }, .bottom_reserved_rows = 0, .preserve_scrollback = true, }; var transition = try resolveAndSealTranscriptTransitionForRuntimeTest( &runtime, alloc, &source, &prepared, render_engine.frame_layout.CommittedLayoutSnapshot.fromPaintPlan(plan), &plan, scroll_plan, ); defer transition.deinit(alloc); runtime.consumeTranscriptTransition(alloc, &transition, .{ .bytes_written = 1, .changed_cells = 1, .full_repaint = true, .terminal_scroll_rows_committed = 0, .document_append_committed = false, .committed_cursor_row = transition.cursor_row, .committed_cursor_col = transition.cursor_col, .shadow_state = .committed, .next_invalidation = render_engine.paint_plan.FrameInvalidationSet.empty(), }); const committed = runtime.transcriptCommitDiagnostic(); try std.testing.expectEqual(TranscriptCommitDiagnosticState.recovering, committed.state); try std.testing.expectEqual(@as(u32, 14), committed.visual_offset); switch (runtime.transcript_commit_state) { .recovering => |receipt| { try std.testing.expectEqual(@as(u16, 0), receipt.remaining_resize_reflow_rows); try std.testing.expectEqual(@as(u32, 2), receipt.remaining_semantic_rows); }, .invalid, .stable => return error.TestExpectedRecoveringTranscript, } runtime.resize_history_row_delta = null; var retry_source = try runtime.prepareTranscriptSource(alloc, null); defer retry_source.deinit(alloc); var retry_prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &retry_source, .{ .top = 1, .bottom = 4 }, ); defer retry_prepared.deinit(alloc); const retry = runtime.planTranscriptScroll(&retry_prepared); try std.testing.expectEqual(@as(u16, 0), retry.resize_reflow_rows); try std.testing.expectEqual(@as(u16, 2), retry.semantic_rows); } test "source rewrite replays unchanged history prefix after footer projection rebase" { try expectHistoryReplayBoundary( "rewrite-row-01\nrewrite-row-02\nrewrite-row-03\nrewrite-row-04\n" ++ "rewrite-row-05\nrewrite-row-06\nrewrite-row-07\nrewrite-row-08\n" ++ "rewrite-row-09\nrewrite-row-10\nrewrite-row-11\nrewrite-row-12\n", false, ); } test "append pending wrap history replay starts a fresh row at a soft wrap" { try expectHistoryReplayBoundary("12345678" ** 60, true); } fn expectHistoryReplayBoundary(target_flow: []const u8, source_pending_wrap: bool) !void { const alloc = std.testing.allocator; const layout: Layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }; var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 1, .committed_frame_layout = .{ .layout_id = 17, .owned_top = 1, .owned_band = .{ .top = 1, .bottom = 8 }, .body_area = .{ .top = 1, .bottom = 4 }, .transcript_area = .{ .top = 1, .bottom = 4 }, .footer_area = .{ .top = 5, .bottom = 8 }, .solved_frame_height = 8, .terminal_rows = 8, .terminal_cols = 40, }, }; defer runtime.deinit(alloc); var source = try runtime.prepareFullTranscriptViewportSource( alloc, try alloc.dupe(u8, target_flow), ); defer source.deinit(alloc); var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &source, .{ .top = 1, .bottom = 4 }, ); defer prepared.deinit(alloc); const target_offset = visualOffsetForSelection( prepared.sourceVisualRows(), prepared.selection, ); try std.testing.expect(target_offset > 5); const history_offset = target_offset - 5; const previous_flow = try alloc.dupe(u8, target_flow); previous_flow[previous_flow.len - 2] = 'X'; runtime.transcript_commit_state = .{ .stable = .{ .selection = prepared.selection, .visual_offset = target_offset, .history_visual_offset = history_offset, .total_visual_rows = totalVisualRows(prepared.sourceVisualRows()), .flow = previous_flow, .cursor_row = prepared.cursor.cursor_row, .cursor_col = prepared.cursor.cursor_col, .occupied_last_row = prepared.selection.last_visible_row, .occupied_last_row_blank = prepared.selection.last_visible_row_blank, .layout_id = 17, .normal_buffer_recovery_pending = true, } }; const facts = runtime.planTranscriptScroll(&prepared); try std.testing.expect(!facts.source_compatible); try std.testing.expect(facts.recovery_rebase); try std.testing.expectEqual(target_offset, facts.target_visual_offset); try std.testing.expectEqual(target_offset - history_offset, facts.semantic_rows); const scroll_plan = render_engine.frame_scroll_plan.merge( runtime.layout.rows, runtime.owned_top_row, 0, facts.planned_rows, ); var plan: render_engine.paint_plan.PaintPlan = .{ .layout = runtime.layout, .viewport = prepared.selection, .footer = .{ .top = 5, .top_divider = 5, .banner = 5, .banner_active = false, .input_base = 6, .picker_divider = 7, .picker_start = 8, .bottom_divider = 7, .hint = 8, .total_rows = 4, }, .activity = .none, .preserved_band = render_engine.paint_plan.FrameBand.empty(.preserved_shell), .transcript_band = .{ .top = 1, .bottom = 4, .owner = .transcript }, .activity_band = render_engine.paint_plan.FrameBand.empty(.activity), .footer_band = .{ .top = 5, .bottom = 8, .owner = .footer }, .invalidation = render_engine.paint_plan.FrameInvalidationSet.empty(), .footer_clean_allowed = true, .synchronized_update = true, .cursor_target = .{ .row = prepared.cursor.cursor_row, .col = prepared.cursor.cursor_col, .visible = true, }, .bottom_reserved_rows = 0, .preserve_scrollback = true, }; var transition = try resolveAndSealTranscriptTransitionForRuntimeTest( &runtime, alloc, &source, &prepared, render_engine.frame_layout.CommittedLayoutSnapshot.fromPaintPlan(plan), &plan, scroll_plan, ); defer transition.deinit(alloc); try std.testing.expect(!transition.document_append.isEmpty()); try std.testing.expect(!transition.document_append.start_pending_wrap); try std.testing.expectEqual(@as(u16, 1), transition.document_append.start_col); const base = (try runtime.resolveTransitionAppendBase(&prepared, scroll_plan.prior_owned_top, facts, false, transition.target_flow, facts.target_visual_offset)).?; var boundary = try transcript_painter.prepareTranscriptDocumentAppend(alloc, transition.target_flow, layout.cols, base.flow_len, base.flow_len, false); defer boundary.deinit(alloc); try std.testing.expectEqual(source_pending_wrap, boundary.start_pending_wrap); // A history replay publishes a prefix, but the committed paint also // materializes the viewport. Its cursor must pair with that later byte end. try std.testing.expectEqual(transition.target_flow.len, transition.materialized.flow_len.?); for ([_]bool{ false, true }) |complete| { const result = render_engine.terminal_diff.FrameCommitResult{ .bytes_written = 1, .changed_cells = 0, .full_repaint = false, .document_append_committed = true, .terminal_scroll_rows_committed = transition.scroll_plan.terminal_scroll_rows, .committed_cursor_row = transition.cursor_row, .committed_cursor_col = transition.cursor_col, .shadow_state = if (complete) .committed else .terminal_partial_write, .next_invalidation = .empty(), }; const commit = resolveTranscriptTransitionCommit(&transition, result, result.scrollCommit(transition.scroll_plan)); const endpoint = resolveRecoveryMaterializedEndpoint(&transition, result, commit); try std.testing.expectEqual(if (complete) @as(?usize, transition.target_flow.len) else null, endpoint.flow_len); } switch (transition.document_append.clear) { .rows => |rows| try std.testing.expectEqual( @as(u16, @intCast(facts.semantic_rows)), rows, ), .remainder => return error.TestExpectedHistoryReplay, } } test "history replay starts at committed source top and follows resize eligibility" { const alloc = std.testing.allocator; const committed_flow = "source-row-01\nsource-row-02\nsource-row-03\nsource-row-04\n"; const target_flow = committed_flow ++ "target-row-05\ntarget-row-06\ntarget-row-07\ntarget-row-08\n" ++ "target-row-09\ntarget-row-10\ntarget-row-11\ntarget-row-12\n"; const layout: Layout = .{ .rows = 12, .cols = 40, .content_bottom = 8, .divider_top_row = 9, .input_row = 10, .divider_bottom_row = 11, .hint_row = 12, }; var runtime = TranscriptRuntime{ .layout = layout, .owned_top_row = 5, .viewport_top_row = 5, .committed_frame_layout = .{ .layout_id = 17, .owned_top = 5, .owned_band = .{ .top = 5, .bottom = 12 }, .body_area = .{ .top = 5, .bottom = 8 }, .transcript_area = .{ .top = 5, .bottom = 8 }, .footer_area = .{ .top = 9, .bottom = 12 }, .solved_frame_height = 8, .terminal_rows = 12, .terminal_cols = 40, }, .transcript_commit_state = .{ .stable = .{ .selection = .{ .top_row = 5, .bottom_row = 8, .start_line = 0, .partial_skip_rows = 0, .line_count = 4, .last_visible_row = 8, }, .visual_offset = 0, .history_visual_offset = 0, .total_visual_rows = 4, .flow = try alloc.dupe(u8, committed_flow), .cursor_row = 8, .cursor_col = 1, .occupied_last_row = 8, .occupied_last_row_blank = true, .layout_id = 17, .history_catchup_pending = true, } }, }; defer runtime.deinit(alloc); var source = try runtime.prepareFullTranscriptViewportSource( alloc, try alloc.dupe(u8, target_flow), ); defer source.deinit(alloc); var metrics: Metrics = .{}; var prepared = try runtime.prepareTranscriptSurfacePaintFromSourceForArea( alloc, &metrics, &source, .{ .top = 1, .bottom = 8 }, ); defer prepared.deinit(alloc); try std.testing.expectEqual(@as(u16, 1), prepared.projectionArea().top); const facts = runtime.planTranscriptScroll(&prepared); try std.testing.expect(facts.source_compatible); try std.testing.expectEqual(@as(u32, 4), facts.target_visual_offset); try std.testing.expectEqual(@as(u16, 4), facts.planned_rows); const append_base = (try runtime.resolveTransitionAppendBase( &prepared, runtime.owned_top_row, facts, false, source.bytes, facts.target_visual_offset, )) orelse return error.TestExpectedHistoryReplay; try std.testing.expect(append_base.history_replay != null); try std.testing.expectEqual( runtime.committed_frame_layout.transcript_area.top, append_base.cursor_row, ); try std.testing.expectEqual(@as(u16, 1), append_base.cursor_col); const scroll_plan = render_engine.frame_scroll_plan.merge( runtime.layout.rows, runtime.owned_top_row, runtime.owned_top_row - 1, facts.planned_rows, ); try std.testing.expectEqual(@as(u16, 4), scroll_plan.preserved_release_rows); try std.testing.expectEqual(@as(u16, 8), scroll_plan.terminal_scroll_rows); runtime.resize_history_row_delta = 1; const resize_facts = runtime.planTranscriptScroll(&prepared); const resize_append_base = (try runtime.resolveTransitionAppendBase( &prepared, scroll_plan.prior_owned_top, resize_facts, false, source.bytes, resize_facts.target_visual_offset, )) orelse return error.TestExpectedNaturalAppendBase; try std.testing.expect(resize_append_base.history_replay == null); } test "preserved source rewrite holds the normal buffer at committed history" { const alloc = std.testing.allocator; var runtime = TranscriptRuntime{ .layout = .{ .rows = 8, .cols = 40, .content_bottom = 4, .divider_top_row = 5, .input_row = 6, .divider_bottom_row = 7, .hint_row = 8, }, .committed_frame_layout = .{ .layout_id = 17, .owned_top = 1, .owned_band = .{ .top = 1, .bottom = 8 }, .body_area = .{ .top = 1, .bottom = 4 }, .transcript_area = .{ .top = 1, .bottom = 4 }, .footer_area = .{ .top = 5, .bottom = 8 }, .solved_frame_height = 8, .terminal_rows = 8, .terminal_cols = 40, }, .transcript_commit_state = .{ .stable = .{ .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 2, .partial_skip_rows = 0, .line_count = 6, .last_visible_row = 4, }, .visual_offset = 5, .history_visual_offset = 2, .total_visual_rows = 6, .flow = try alloc.dupe(u8, "old source\n"), .cursor_row = 4, .cursor_col = 1, .occupied_last_row = 4, .occupied_last_row_blank = false, .layout_id = 17, } }, }; defer runtime.deinit(alloc); runtime.reconcileTranscriptCommitSourceWithRewriteMode( "rewritten source\n", false, "test rewrite", .preserve_same_epoch, ); switch (runtime.transcript_commit_state) { .stable => |anchor| { try std.testing.expect(anchor.normal_buffer_recovery_pending); try std.testing.expectEqual( anchor.history_visual_offset, committedProjectionVisualOffset(anchor), ); }, .invalid, .recovering => return error.TestExpectedStableTranscript, } } test "frame scroll commit consumption releases rows before transition advancement" { var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(8), .owned_top_row = 4, .viewport_top_row = 4, }; defer runtime.deinit(std.testing.allocator); const scroll_plan = render_engine.frame_scroll_plan.merge(8, 4, 3, 1); var transition = TranscriptTransition{ .scroll_plan = scroll_plan, .document_append = .{}, .materialized_without_append = .{}, .semantic_rows = 1, .semantic_progress_rows = 1, .target_flow = &.{}, .target_cache = .empty, .folded_summary_indices = &.{}, .target_layout = .{ .layout_id = 18, .owned_top = 1, .owned_band = .{ .top = 1, .bottom = 8 }, .body_area = .{ .top = 1, .bottom = 4 }, .transcript_area = .{ .top = 1, .bottom = 4 }, .footer_area = .{ .top = 5, .bottom = 8 }, .solved_frame_height = 8, .terminal_rows = 8, .terminal_cols = 80, }, .selection = .{ .top_row = 1, .bottom_row = 4, .start_line = 1, .partial_skip_rows = 0, .line_count = 4, .last_visible_row = 4, }, .visual_offset = 1, .history_visual_offset = 1, .total_visual_rows = 5, .cursor_row = 4, .cursor_col = 1, .replaceable_last_line = false, .replaceable_start = 0, .replaceable_row = 4, .trace_transcript_frame = false, .trace_visible_rows = 4, }; defer transition.deinit(std.testing.allocator); const result = testFrameCommitResult(scroll_plan.terminal_scroll_rows, .committed); runtime.consumeFrameScrollCommit( std.testing.allocator, scroll_plan, result, &transition, ); try std.testing.expect(transition.consumed); try std.testing.expectEqual(@as(u16, 1), runtime.owned_top_row); try std.testing.expectEqual(@as(u16, 1), runtime.viewport_top_row); const committed = runtime.transcriptCommitDiagnostic(); try std.testing.expectEqual(TranscriptCommitDiagnosticState.stable, committed.state); try std.testing.expectEqual(@as(u32, 1), committed.visual_offset); } fn testLayoutWithRows(rows: u16) Layout { return .{ .rows = rows, .cols = 80, .content_bottom = rows, .divider_top_row = rows, .input_row = rows, .divider_bottom_row = rows, .hint_row = rows, }; } test "notification bell writes standalone BEL bytes" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var file = try tmp.dir.createFile(std.testing.io, "notification-bells.log", .{ .read = true }); defer file.close(io_mod.getIo()); var runtime = TranscriptRuntime{ .stdout_file = file }; defer runtime.deinit(std.testing.allocator); var metrics: Metrics = .{}; runtime.writeNotificationBell(&metrics); var bytes: [1]u8 = undefined; try std.testing.expectEqual( @as(usize, 1), try file.readPositionalAll(io_mod.getIo(), &bytes, 0), ); try std.testing.expectEqualSlices(u8, "\x07", &bytes); } test { _ = @import("runtime_tests.zig"); } test "changed stored result retires stale full transcript geometry" { for ([_]bool{ false, true }) |cancelled| { const request = full_transcript_page.Request{ .content_revision = 1, .cols = 80, .anchor = .tail, }; var runtime = TranscriptRuntime{ .layout = testLayoutWithRows(24), .full_transcript = .{ .depth = .full }, .full_transcript_installed_page = .{ .source = .{ .request = request, .range = .{ .start = 0, .end = 0 } }, .projection = .{ .styles = .{}, .measurement_cols = null }, .measured_total_rows = 800, .measurement_snapshot_ready = true, }, }; defer runtime.deinit(std.testing.allocator); const task = try std.heap.c_allocator.create(full_transcript_worker.WindowTask); task.* = .{ .request = full_transcript_worker.preparedWindowRequest(request, 800, 700, 20), .projection = &runtime.full_transcript_installed_page.?.projection, .capability = null, }; task.cancel_requested.store(cancelled, .release); task.done.store(true, .release); runtime.full_transcript_window_load.task = task; try std.testing.expect(try runtime.pollFullTranscriptPageLoad()); try std.testing.expect(runtime.full_transcript_installed_page == null); } } test "prependFullDetailRecords injects records only into the first page" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); try runtime.appendFullDetailRecord(alloc, .{ .topic = "session", .tone = .neutral, .body = "provider: test", }); const request: full_transcript_page.Request = .{ .content_revision = 1, .cols = 80, .anchor = .tail }; var source = full_transcript_worker.Source{ .request = request, .range = .{ .start = 0, .end = 0 }, .styles = .{} }; defer source.deinit(alloc); try runtime.prependFullDetailRecords(alloc, &source); try std.testing.expectEqual(@as(usize, 1), source.entries.items.len); const notice = source.entries.items[0].semantic_notice; try std.testing.expectEqual(@as(u32, 0), notice.id); try std.testing.expectEqualStrings("session", notice.topic); try std.testing.expectEqualStrings("provider: test", notice.body); var later = full_transcript_worker.Source{ .request = request, .range = .{ .start = 1, .end = 1 }, .styles = .{} }; defer later.deinit(alloc); try runtime.prependFullDetailRecords(alloc, &later); try std.testing.expectEqual(@as(usize, 0), later.entries.items.len); } test "prependFullDetailRecords keeps the welcome banner first" { const alloc = std.testing.allocator; var runtime: TranscriptRuntime = .{}; defer runtime.deinit(alloc); try runtime.appendFullDetailRecord(alloc, .{ .topic = "network", .tone = .neutral, .body = "finish: stop", }); const request: full_transcript_page.Request = .{ .content_revision = 1, .cols = 80, .anchor = .tail }; var source = full_transcript_worker.Source{ .request = request, .range = .{ .start = 0, .end = 1 }, .styles = .{} }; defer source.deinit(alloc); try source.entries.append(alloc, .{ .raw_bytes = .{ .id = 1, .bytes = try alloc.dupe(u8, "banner"), .class = .welcome, } }); try runtime.prependFullDetailRecords(alloc, &source); try std.testing.expectEqual(@as(usize, 2), source.entries.items.len); try std.testing.expect(source.entries.items[0] == .raw_bytes); try std.testing.expect(source.entries.items[1] == .semantic_notice); }