// Host contract:// - fields: command_output_blocks, folded_command_blocks,// command_output_display, command_output_render,// entries, transcript, transcript_band_dirty, replaceable_last_line,// replaceable_start, footer_viewport, viewport_top_row, cursor_row,// cursor_col, layout// - methods: assertCanMutateTranscript, appendRawBytesEntryClassified,// attachCommandOutputEntryForLifecycle,// writeRecordedCommandOutputChunkAtomic,// flushRecordedCommandOutputSummaryAtomic,// rebuildTranscriptCacheFromEntries,// rebuildTranscriptCacheAfterStructuredRewrite,// recomputeCursorFromTranscript, enforceStructuredRetention,// enforceStructuredRetentionAndReport const std = @import("std");const debug_trace = @import("../../core/shared/debug_trace.zig");const display_width = @import("../../core/shared/display_width.zig");const io_mod = @import("../../core/shared/io.zig");const types = @import("../../core/shared/types.zig");const command_output_content = @import("../../core/tooling/command_output_content.zig");const render_engine = @import("../render_engine.zig");const assistant_wrap = @import("../render_engine/assistant_wrap.zig"); const Allocator = std.mem.Allocator;const Metrics = types.Metrics;const transcript_blocks = render_engine.transcript_blocks; const Styles = transcript_blocks.Styles; fn requestTranscriptPaint(shell: anytype, entry_id: ?u32) void { const Runtime = @TypeOf(shell.*); if (entry_id) |dirty_entry_id| { if (comptime @hasDecl(Runtime, "markTranscriptCommandOutputDirtyFrom")) { shell.markTranscriptCommandOutputDirtyFrom(dirty_entry_id); return; } } if (comptime @hasDecl(Runtime, "markTranscriptCommandOutputDirty")) { shell.markTranscriptCommandOutputDirty(); } else if (comptime @hasDecl(Runtime, "markTranscriptContentDirty")) { shell.markTranscriptContentDirty(); } else if (comptime @hasDecl(Runtime, "markTranscriptDirty")) { shell.markTranscriptDirty(); } else { shell.transcript_band_dirty = true; }} fn commandBlockDirtyEntryId(block: CommandOutputBlock) ?u32 { 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;} pub fn openCommandOutputDirtyEntryId(shell: anytype) ?u32 { const index = shell.command_output_display.open_command_block orelse return null; if (index >= shell.command_output_blocks.items.len) return null; return commandBlockDirtyEntryId(shell.command_output_blocks.items[index]);} pub const CommandOutputLine = struct { stream: command_output_content.Stream, text: []u8, record_ordinal: usize = 0, entry_id: ?u32 = null, terminated: bool = false, visible: bool = true,}; pub const CommandOutputPrunedRange = struct { anchor_entry_id: u32, start_record: usize, end_record: usize,}; const command_output_line_descriptor_bytes = @sizeOf(CommandOutputLine);const command_output_entry_descriptor_bytes = @sizeOf(transcript_blocks.TranscriptEntry);const command_output_source_id_descriptor_bytes = @sizeOf(u32);const command_output_pruned_range_descriptor_bytes = @sizeOf(CommandOutputPrunedRange); pub const CommandOutputBlock = struct { entry_id: ?u32 = null, lifecycle_id: ?types.ToolLifecycleId = null, total_lines: usize = 0, retained_text_bytes: usize = 0, lines: std.ArrayList(CommandOutputLine) = .empty, decoders: [2]command_output_content.Decoder = .{ .{}, .{} }, open_line_indices: [2]?usize = .{ null, null }, overflow_open_records: [2]bool = .{ false, false }, overflow_record_visible: [2]bool = .{ false, false }, retention_overflow: bool = false, /// First retained record whose text became incomplete at the structured /// cap. Records before this index are stable for active Ctrl-O projection. overflow_line_index: ?usize = null, pruned_ranges: std.ArrayList(CommandOutputPrunedRange) = .empty, live_entry_ids: std.ArrayList(u32) = .empty, source_entry_ids: std.ArrayList(u32) = .empty, pub fn deinit(self: *CommandOutputBlock, alloc: Allocator) void { if (self.lifecycle_id) |id| alloc.free(@constCast(id.call_id)); for (self.lines.items) |line| alloc.free(line.text); self.lines.deinit(alloc); self.pruned_ranges.deinit(alloc); self.live_entry_ids.deinit(alloc); self.source_entry_ids.deinit(alloc); } pub fn canReconstructEntries(self: CommandOutputBlock) bool { return self.pruned_ranges.items.len == 0 and self.lines.items.len >= self.live_entry_ids.items.len and self.lines.items.len >= self.source_entry_ids.items.len; }}; pub const FoldedCommandBlock = struct { summary_transcript_index: usize = 0, summary_entry_id: ?u32 = null, lines: std.ArrayList(CommandOutputLine) = .empty, pub fn deinit(self: *FoldedCommandBlock, alloc: Allocator) void { for (self.lines.items) |line| alloc.free(line.text); self.lines.deinit(alloc); }}; pub const CommandOutputRenderPolicy = struct { styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", },}; test "command output render policy has no presentation mode" { try std.testing.expect(!@hasField(CommandOutputRenderPolicy, "output_level"));} pub const compact_output_row_limit: usize = 5; pub const CommandOutputEntryProjection = struct { entry_id: u32, byte_start: usize, byte_end: usize,}; pub const CommandOutputProjection = struct { bytes: std.ArrayList(u8) = .empty, entries: std.ArrayList(CommandOutputEntryProjection) = .empty, pub fn deinit(self: *CommandOutputProjection, alloc: Allocator) void { self.bytes.deinit(alloc); self.entries.deinit(alloc); self.* = undefined; } pub fn bytesForEntry(self: *const CommandOutputProjection, entry_id: u32) ?[]const u8 { for (self.entries.items) |entry| { if (entry.entry_id != entry_id) continue; return self.bytes.items[entry.byte_start..entry.byte_end]; } return null; }}; pub fn foldedBlockRetainedBytes(block: FoldedCommandBlock) usize { var total: usize = 0; for (block.lines.items) |line| total += line.text.len; return total;} pub fn commandOutputBlockRetainedBytes(block: CommandOutputBlock) usize { var total = block.retained_text_bytes; for (block.lines.items) |line| { total +|= command_output_line_descriptor_bytes; if (line.entry_id != null) total +|= command_output_entry_descriptor_bytes; } total +|= block.pruned_ranges.items.len *| (command_output_pruned_range_descriptor_bytes +| command_output_entry_descriptor_bytes); total +|= (block.live_entry_ids.items.len +| block.source_entry_ids.items.len) *| command_output_source_id_descriptor_bytes; return total;} pub fn commandOutputLineRetainedBytes(text_bytes: usize) usize { return text_bytes +| command_output_line_descriptor_bytes +| command_output_entry_descriptor_bytes +| command_output_source_id_descriptor_bytes;} pub fn commandOutputAdmissionBytes(raw: []const u8) usize { var decoder: command_output_content.Decoder = .{}; var sink: CommandOutputAdmissionSink = .{}; if (raw.len > 0) sink.beginRecord() catch return std.math.maxInt(usize); decoder.append(raw, &sink) catch return std.math.maxInt(usize); return sink.text_bytes +| sink.record_count *| commandOutputLineRetainedBytes(0);} const CommandOutputAdmissionSink = struct { text_bytes: usize = 0, current_record_bytes: usize = 0, record_count: usize = 0, open: bool = false, pub fn beginRecord(self: *CommandOutputAdmissionSink) error{}!void { if (self.open) return; self.open = true; self.current_record_bytes = 0; self.record_count +|= 1; } pub fn appendText(self: *CommandOutputAdmissionSink, bytes: []const u8) error{}!void { try self.beginRecord(); self.text_bytes +|= bytes.len; self.current_record_bytes +|= bytes.len; } pub fn finishLine(self: *CommandOutputAdmissionSink) error{}!void { try self.beginRecord(); self.open = false; self.current_record_bytes = 0; } pub fn replaceLine(self: *CommandOutputAdmissionSink) error{}!void { try self.beginRecord(); self.text_bytes -|= self.current_record_bytes; self.current_record_bytes = 0; }}; pub fn commandArtifactHandleFromResult(result: []const u8) ?[]const u8 { const prefix = "output_file="; var truncated = false; var has_stdout_bytes = false; var has_stderr_bytes = false; var lines = std.mem.splitScalar(u8, result, '\n'); while (lines.next()) |raw_line| { const line = std.mem.trimEnd(u8, raw_line, "\r"); if (std.mem.eql(u8, line, "<stdout>") or std.mem.eql(u8, line, "<stderr>")) return null; if (std.mem.eql(u8, line, "truncated=true")) { truncated = true; continue; } if (std.mem.startsWith(u8, line, "stdout_bytes=")) { has_stdout_bytes = true; continue; } if (std.mem.startsWith(u8, line, "stderr_bytes=")) { has_stderr_bytes = true; continue; } if (!std.mem.startsWith(u8, line, prefix)) continue; if (!truncated or !has_stdout_bytes or !has_stderr_bytes) return null; const path = line[prefix.len..]; const handle = std.fs.path.basename(path); if (std.mem.startsWith(u8, handle, "fx-command-") and std.mem.endsWith(u8, handle, ".log") and !std.mem.endsWith(u8, handle, ".stdout.log") and !std.mem.endsWith(u8, handle, ".stderr.log")) { return handle; } return null; } return null;} pub fn removeFoldedCommandBlock(shell: anytype, alloc: Allocator, index: usize) void { var block = shell.folded_command_blocks.orderedRemove(index); block.deinit(alloc); if (shell.command_output_display.open_block) |open| { if (open == index) { shell.command_output_display.open_block = null; } else if (open > index) { shell.command_output_display.open_block = open - 1; } }} pub fn removeCommandOutputBlock(shell: anytype, alloc: Allocator, index: usize) void { var block = shell.command_output_blocks.orderedRemove(index); block.deinit(alloc); if (shell.command_output_display.open_command_block) |open| { if (open == index) { shell.command_output_display.open_command_block = null; } else if (open > index) { shell.command_output_display.open_command_block = open - 1; } }} pub fn resetCommandOutputDisplay(shell: anytype, alloc: Allocator, reason: []const u8) void { if (shell.command_output_display.open_command_block) |index| { if (index < shell.command_output_blocks.items.len and shell.command_output_blocks.items[index].entry_id == null) { debug_trace.logf("command_output", "dropping buffered command output lines={d} reason={s}", .{ shell.command_output_blocks.items[index].total_lines, reason }); removeCommandOutputBlock(shell, alloc, index); } } shell.command_output_display = .{};} pub fn setCommandOutputRenderPolicy(shell: anytype, styles: Styles) void { shell.command_output_render = .{ .styles = styles, };} const PreparedCanonicalRecord = struct { text: []u8, placeholder: []u8,}; const PreparedCanonicalBatch = struct { block_index: usize, stream: command_output_content.Stream, total_record_count: usize, retained_bytes_after: usize, records: std.ArrayList(PreparedCanonicalRecord) = .empty, fn deinit(self: *PreparedCanonicalBatch, alloc: Allocator) void { for (self.records.items) |record| { alloc.free(record.text); alloc.free(record.placeholder); } self.records.deinit(alloc); self.* = undefined; }}; /// One owner-level decision for both direct and recorded command-output writes./// Prepared canonical buffers remain owned by this value until apply transfers/// them into the transcript block.pub const PreparedCommandOutputMutation = union(enum) { decode, overflow_fragment: usize, count_only: struct { block_index: usize, record_count: usize, }, canonical_batch: PreparedCanonicalBatch, pub fn deinit(self: *PreparedCommandOutputMutation, alloc: Allocator) void { switch (self.*) { .canonical_batch => |*batch| batch.deinit(alloc), .decode, .overflow_fragment, .count_only => {}, } self.* = undefined; } pub fn requiresRecordedShadow(self: PreparedCommandOutputMutation) bool { return switch (self) { .decode => true, .overflow_fragment, .count_only, .canonical_batch => false, }; }}; pub fn prepareCommandOutputMutation( shell: anytype, alloc: Allocator, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, record: bool,) !PreparedCommandOutputMutation { const block_index = shell.command_output_display.open_command_block orelse return .decode; const block = &shell.command_output_blocks.items[block_index]; if (!sameLifecycleId(block.lifecycle_id, lifecycle_id)) { return error.CommandOutputLifecycleMismatch; } const stream_index = commandStreamIndex(stream); if (record and block.overflow_open_records[stream_index] and block.decoders[stream_index].isIdle() and std.mem.indexOfAny(u8, text, "\r\n\x1b") == null) { return .{ .overflow_fragment = block_index }; } if (record and block.retention_overflow and block.open_line_indices[stream_index] == null and !block.overflow_open_records[stream_index] and text.len > 0 and text[text.len - 1] == '\n' and isCountOnlyCommandOutput(text)) { const record_count = std.mem.count(u8, text, "\n"); if (record_count > 0) { return .{ .count_only = .{ .block_index = block_index, .record_count = record_count, } }; } } if (!record or block.total_lines < compact_output_row_limit + 1 or block.open_line_indices[stream_index] != null or block.overflow_open_records[stream_index]) { return .decode; } var canonical: command_output_content.CanonicalOutput = .{}; defer canonical.deinit(alloc); try canonical.append( alloc, switch (stream) { .stdout => .stdout, .stderr => .stderr, }, text, ); if (canonical.records.items.len == 0) return .decode; for (canonical.records.items) |canonical_record| { if (!canonical_record.terminated) return .decode; } const cap = commandOutputRetentionCap(shell); var retained = retainedCommandOutputBytes(shell, block_index); var retained_count: usize = 0; if (!block.retention_overflow) { for (canonical.records.items) |canonical_record| { const footprint = commandOutputLineRetainedBytes( canonical_record.text.items.len, ); if (retained > cap or footprint > cap - retained) break; retained += footprint; retained_count += 1; } } var batch = PreparedCanonicalBatch{ .block_index = block_index, .stream = stream, .total_record_count = canonical.records.items.len, .retained_bytes_after = retained, }; errdefer batch.deinit(alloc); try batch.records.ensureTotalCapacityPrecise(alloc, retained_count); for (canonical.records.items[0..retained_count]) |canonical_record| { const owned_text = try alloc.dupe(u8, canonical_record.text.items); errdefer alloc.free(owned_text); const placeholder = try alloc.alloc(u8, 0); batch.records.appendAssumeCapacity(.{ .text = owned_text, .placeholder = placeholder, }); } return .{ .canonical_batch = batch };} fn isCountOnlyCommandOutput(text: []const u8) bool { for (text) |byte| { if (byte == '\n' or byte == '\t' or (byte >= 0x20 and byte < 0x7f)) continue; return false; } return true;} pub fn applyPreparedCommandOutputMutation( shell: anytype, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, record: bool, mutation: *PreparedCommandOutputMutation,) !?u32 { return applyPreparedCommandOutputMutationAt( shell, alloc, metrics, styles, lifecycle_id, stream, text, record, mutation, null, );} fn applyPreparedCommandOutputMutationAt( shell: anytype, alloc: Allocator,Lines 1–500 of 2335. Load more
// Host contract: // - fields: command_output_blocks, folded_command_blocks, // command_output_display, command_output_render, // entries, transcript, transcript_band_dirty, replaceable_last_line, // replaceable_start, footer_viewport, viewport_top_row, cursor_row, // cursor_col, layout // - methods: assertCanMutateTranscript, appendRawBytesEntryClassified, // attachCommandOutputEntryForLifecycle, // writeRecordedCommandOutputChunkAtomic, // flushRecordedCommandOutputSummaryAtomic, // rebuildTranscriptCacheFromEntries, // rebuildTranscriptCacheAfterStructuredRewrite, // recomputeCursorFromTranscript, enforceStructuredRetention, // enforceStructuredRetentionAndReport const std = @import("std"); const debug_trace = @import("../../core/shared/debug_trace.zig"); const display_width = @import("../../core/shared/display_width.zig"); const io_mod = @import("../../core/shared/io.zig"); const types = @import("../../core/shared/types.zig"); const command_output_content = @import("../../core/tooling/command_output_content.zig"); const render_engine = @import("../render_engine.zig"); const assistant_wrap = @import("../render_engine/assistant_wrap.zig"); const Allocator = std.mem.Allocator; const Metrics = types.Metrics; const transcript_blocks = render_engine.transcript_blocks; const Styles = transcript_blocks.Styles; fn requestTranscriptPaint(shell: anytype, entry_id: ?u32) void { const Runtime = @TypeOf(shell.*); if (entry_id) |dirty_entry_id| { if (comptime @hasDecl(Runtime, "markTranscriptCommandOutputDirtyFrom")) { shell.markTranscriptCommandOutputDirtyFrom(dirty_entry_id); return; } } if (comptime @hasDecl(Runtime, "markTranscriptCommandOutputDirty")) { shell.markTranscriptCommandOutputDirty(); } else if (comptime @hasDecl(Runtime, "markTranscriptContentDirty")) { shell.markTranscriptContentDirty(); } else if (comptime @hasDecl(Runtime, "markTranscriptDirty")) { shell.markTranscriptDirty(); } else { shell.transcript_band_dirty = true; } } fn commandBlockDirtyEntryId(block: CommandOutputBlock) ?u32 { 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; } pub fn openCommandOutputDirtyEntryId(shell: anytype) ?u32 { const index = shell.command_output_display.open_command_block orelse return null; if (index >= shell.command_output_blocks.items.len) return null; return commandBlockDirtyEntryId(shell.command_output_blocks.items[index]); } pub const CommandOutputLine = struct { stream: command_output_content.Stream, text: []u8, record_ordinal: usize = 0, entry_id: ?u32 = null, terminated: bool = false, visible: bool = true, }; pub const CommandOutputPrunedRange = struct { anchor_entry_id: u32, start_record: usize, end_record: usize, }; const command_output_line_descriptor_bytes = @sizeOf(CommandOutputLine); const command_output_entry_descriptor_bytes = @sizeOf(transcript_blocks.TranscriptEntry); const command_output_source_id_descriptor_bytes = @sizeOf(u32); const command_output_pruned_range_descriptor_bytes = @sizeOf(CommandOutputPrunedRange); pub const CommandOutputBlock = struct { entry_id: ?u32 = null, lifecycle_id: ?types.ToolLifecycleId = null, total_lines: usize = 0, retained_text_bytes: usize = 0, lines: std.ArrayList(CommandOutputLine) = .empty, decoders: [2]command_output_content.Decoder = .{ .{}, .{} }, open_line_indices: [2]?usize = .{ null, null }, overflow_open_records: [2]bool = .{ false, false }, overflow_record_visible: [2]bool = .{ false, false }, retention_overflow: bool = false, /// First retained record whose text became incomplete at the structured /// cap. Records before this index are stable for active Ctrl-O projection. overflow_line_index: ?usize = null, pruned_ranges: std.ArrayList(CommandOutputPrunedRange) = .empty, live_entry_ids: std.ArrayList(u32) = .empty, source_entry_ids: std.ArrayList(u32) = .empty, pub fn deinit(self: *CommandOutputBlock, alloc: Allocator) void { if (self.lifecycle_id) |id| alloc.free(@constCast(id.call_id)); for (self.lines.items) |line| alloc.free(line.text); self.lines.deinit(alloc); self.pruned_ranges.deinit(alloc); self.live_entry_ids.deinit(alloc); self.source_entry_ids.deinit(alloc); } pub fn canReconstructEntries(self: CommandOutputBlock) bool { return self.pruned_ranges.items.len == 0 and self.lines.items.len >= self.live_entry_ids.items.len and self.lines.items.len >= self.source_entry_ids.items.len; } }; pub const FoldedCommandBlock = struct { summary_transcript_index: usize = 0, summary_entry_id: ?u32 = null, lines: std.ArrayList(CommandOutputLine) = .empty, pub fn deinit(self: *FoldedCommandBlock, alloc: Allocator) void { for (self.lines.items) |line| alloc.free(line.text); self.lines.deinit(alloc); } }; pub const CommandOutputRenderPolicy = struct { styles: Styles = .{ .system_notice_label_style = "", .system_notice_text_style = "", .reset_style = "", .dim_style = "", .red_style = "", }, }; test "command output render policy has no presentation mode" { try std.testing.expect(!@hasField(CommandOutputRenderPolicy, "output_level")); } pub const compact_output_row_limit: usize = 5; pub const CommandOutputEntryProjection = struct { entry_id: u32, byte_start: usize, byte_end: usize, }; pub const CommandOutputProjection = struct { bytes: std.ArrayList(u8) = .empty, entries: std.ArrayList(CommandOutputEntryProjection) = .empty, pub fn deinit(self: *CommandOutputProjection, alloc: Allocator) void { self.bytes.deinit(alloc); self.entries.deinit(alloc); self.* = undefined; } pub fn bytesForEntry(self: *const CommandOutputProjection, entry_id: u32) ?[]const u8 { for (self.entries.items) |entry| { if (entry.entry_id != entry_id) continue; return self.bytes.items[entry.byte_start..entry.byte_end]; } return null; } }; pub fn foldedBlockRetainedBytes(block: FoldedCommandBlock) usize { var total: usize = 0; for (block.lines.items) |line| total += line.text.len; return total; } pub fn commandOutputBlockRetainedBytes(block: CommandOutputBlock) usize { var total = block.retained_text_bytes; for (block.lines.items) |line| { total +|= command_output_line_descriptor_bytes; if (line.entry_id != null) total +|= command_output_entry_descriptor_bytes; } total +|= block.pruned_ranges.items.len *| (command_output_pruned_range_descriptor_bytes +| command_output_entry_descriptor_bytes); total +|= (block.live_entry_ids.items.len +| block.source_entry_ids.items.len) *| command_output_source_id_descriptor_bytes; return total; } pub fn commandOutputLineRetainedBytes(text_bytes: usize) usize { return text_bytes +| command_output_line_descriptor_bytes +| command_output_entry_descriptor_bytes +| command_output_source_id_descriptor_bytes; } pub fn commandOutputAdmissionBytes(raw: []const u8) usize { var decoder: command_output_content.Decoder = .{}; var sink: CommandOutputAdmissionSink = .{}; if (raw.len > 0) sink.beginRecord() catch return std.math.maxInt(usize); decoder.append(raw, &sink) catch return std.math.maxInt(usize); return sink.text_bytes +| sink.record_count *| commandOutputLineRetainedBytes(0); } const CommandOutputAdmissionSink = struct { text_bytes: usize = 0, current_record_bytes: usize = 0, record_count: usize = 0, open: bool = false, pub fn beginRecord(self: *CommandOutputAdmissionSink) error{}!void { if (self.open) return; self.open = true; self.current_record_bytes = 0; self.record_count +|= 1; } pub fn appendText(self: *CommandOutputAdmissionSink, bytes: []const u8) error{}!void { try self.beginRecord(); self.text_bytes +|= bytes.len; self.current_record_bytes +|= bytes.len; } pub fn finishLine(self: *CommandOutputAdmissionSink) error{}!void { try self.beginRecord(); self.open = false; self.current_record_bytes = 0; } pub fn replaceLine(self: *CommandOutputAdmissionSink) error{}!void { try self.beginRecord(); self.text_bytes -|= self.current_record_bytes; self.current_record_bytes = 0; } }; pub fn commandArtifactHandleFromResult(result: []const u8) ?[]const u8 { const prefix = "output_file="; var truncated = false; var has_stdout_bytes = false; var has_stderr_bytes = false; var lines = std.mem.splitScalar(u8, result, '\n'); while (lines.next()) |raw_line| { const line = std.mem.trimEnd(u8, raw_line, "\r"); if (std.mem.eql(u8, line, "<stdout>") or std.mem.eql(u8, line, "<stderr>")) return null; if (std.mem.eql(u8, line, "truncated=true")) { truncated = true; continue; } if (std.mem.startsWith(u8, line, "stdout_bytes=")) { has_stdout_bytes = true; continue; } if (std.mem.startsWith(u8, line, "stderr_bytes=")) { has_stderr_bytes = true; continue; } if (!std.mem.startsWith(u8, line, prefix)) continue; if (!truncated or !has_stdout_bytes or !has_stderr_bytes) return null; const path = line[prefix.len..]; const handle = std.fs.path.basename(path); if (std.mem.startsWith(u8, handle, "fx-command-") and std.mem.endsWith(u8, handle, ".log") and !std.mem.endsWith(u8, handle, ".stdout.log") and !std.mem.endsWith(u8, handle, ".stderr.log")) { return handle; } return null; } return null; } pub fn removeFoldedCommandBlock(shell: anytype, alloc: Allocator, index: usize) void { var block = shell.folded_command_blocks.orderedRemove(index); block.deinit(alloc); if (shell.command_output_display.open_block) |open| { if (open == index) { shell.command_output_display.open_block = null; } else if (open > index) { shell.command_output_display.open_block = open - 1; } } } pub fn removeCommandOutputBlock(shell: anytype, alloc: Allocator, index: usize) void { var block = shell.command_output_blocks.orderedRemove(index); block.deinit(alloc); if (shell.command_output_display.open_command_block) |open| { if (open == index) { shell.command_output_display.open_command_block = null; } else if (open > index) { shell.command_output_display.open_command_block = open - 1; } } } pub fn resetCommandOutputDisplay(shell: anytype, alloc: Allocator, reason: []const u8) void { if (shell.command_output_display.open_command_block) |index| { if (index < shell.command_output_blocks.items.len and shell.command_output_blocks.items[index].entry_id == null) { debug_trace.logf("command_output", "dropping buffered command output lines={d} reason={s}", .{ shell.command_output_blocks.items[index].total_lines, reason }); removeCommandOutputBlock(shell, alloc, index); } } shell.command_output_display = .{}; } pub fn setCommandOutputRenderPolicy(shell: anytype, styles: Styles) void { shell.command_output_render = .{ .styles = styles, }; } const PreparedCanonicalRecord = struct { text: []u8, placeholder: []u8, }; const PreparedCanonicalBatch = struct { block_index: usize, stream: command_output_content.Stream, total_record_count: usize, retained_bytes_after: usize, records: std.ArrayList(PreparedCanonicalRecord) = .empty, fn deinit(self: *PreparedCanonicalBatch, alloc: Allocator) void { for (self.records.items) |record| { alloc.free(record.text); alloc.free(record.placeholder); } self.records.deinit(alloc); self.* = undefined; } }; /// One owner-level decision for both direct and recorded command-output writes. /// Prepared canonical buffers remain owned by this value until apply transfers /// them into the transcript block. pub const PreparedCommandOutputMutation = union(enum) { decode, overflow_fragment: usize, count_only: struct { block_index: usize, record_count: usize, }, canonical_batch: PreparedCanonicalBatch, pub fn deinit(self: *PreparedCommandOutputMutation, alloc: Allocator) void { switch (self.*) { .canonical_batch => |*batch| batch.deinit(alloc), .decode, .overflow_fragment, .count_only => {}, } self.* = undefined; } pub fn requiresRecordedShadow(self: PreparedCommandOutputMutation) bool { return switch (self) { .decode => true, .overflow_fragment, .count_only, .canonical_batch => false, }; } }; pub fn prepareCommandOutputMutation( shell: anytype, alloc: Allocator, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, record: bool, ) !PreparedCommandOutputMutation { const block_index = shell.command_output_display.open_command_block orelse return .decode; const block = &shell.command_output_blocks.items[block_index]; if (!sameLifecycleId(block.lifecycle_id, lifecycle_id)) { return error.CommandOutputLifecycleMismatch; } const stream_index = commandStreamIndex(stream); if (record and block.overflow_open_records[stream_index] and block.decoders[stream_index].isIdle() and std.mem.indexOfAny(u8, text, "\r\n\x1b") == null) { return .{ .overflow_fragment = block_index }; } if (record and block.retention_overflow and block.open_line_indices[stream_index] == null and !block.overflow_open_records[stream_index] and text.len > 0 and text[text.len - 1] == '\n' and isCountOnlyCommandOutput(text)) { const record_count = std.mem.count(u8, text, "\n"); if (record_count > 0) { return .{ .count_only = .{ .block_index = block_index, .record_count = record_count, } }; } } if (!record or block.total_lines < compact_output_row_limit + 1 or block.open_line_indices[stream_index] != null or block.overflow_open_records[stream_index]) { return .decode; } var canonical: command_output_content.CanonicalOutput = .{}; defer canonical.deinit(alloc); try canonical.append( alloc, switch (stream) { .stdout => .stdout, .stderr => .stderr, }, text, ); if (canonical.records.items.len == 0) return .decode; for (canonical.records.items) |canonical_record| { if (!canonical_record.terminated) return .decode; } const cap = commandOutputRetentionCap(shell); var retained = retainedCommandOutputBytes(shell, block_index); var retained_count: usize = 0; if (!block.retention_overflow) { for (canonical.records.items) |canonical_record| { const footprint = commandOutputLineRetainedBytes( canonical_record.text.items.len, ); if (retained > cap or footprint > cap - retained) break; retained += footprint; retained_count += 1; } } var batch = PreparedCanonicalBatch{ .block_index = block_index, .stream = stream, .total_record_count = canonical.records.items.len, .retained_bytes_after = retained, }; errdefer batch.deinit(alloc); try batch.records.ensureTotalCapacityPrecise(alloc, retained_count); for (canonical.records.items[0..retained_count]) |canonical_record| { const owned_text = try alloc.dupe(u8, canonical_record.text.items); errdefer alloc.free(owned_text); const placeholder = try alloc.alloc(u8, 0); batch.records.appendAssumeCapacity(.{ .text = owned_text, .placeholder = placeholder, }); } return .{ .canonical_batch = batch }; } fn isCountOnlyCommandOutput(text: []const u8) bool { for (text) |byte| { if (byte == '\n' or byte == '\t' or (byte >= 0x20 and byte < 0x7f)) continue; return false; } return true; } pub fn applyPreparedCommandOutputMutation( shell: anytype, alloc: Allocator, metrics: *Metrics, styles: Styles, lifecycle_id: ?types.ToolLifecycleId, stream: command_output_content.Stream, text: []const u8, record: bool, mutation: *PreparedCommandOutputMutation, ) !?u32 { return applyPreparedCommandOutputMutationAt( shell, alloc, metrics, styles, lifecycle_id, stream, text, record, mutation, null, ); } fn applyPreparedCommandOutputMutationAt( shell: anytype, alloc: Allocator,