const std = @import("std");const build_checkpoint = @import("../render_engine/build_checkpoint.zig");const transcript_blocks = @import("../render_engine/transcript_blocks.zig");const types = @import("../../core/shared/types.zig");const display_width = @import("../../core/shared/display_width.zig");const sort_utils = @import("../../core/shared/sort_utils.zig"); const TranscriptEntry = transcript_blocks.TranscriptEntry;const ToolDetailRecord = transcript_blocks.ToolDetailRecord;const cancellation_follow_up = " · What can fx do differently?"; pub const Projection = struct { entry_actions: std.ArrayList(transcript_blocks.EntryRenderAction) = .empty, owned_overrides: std.ArrayList(OwnedOverride) = .empty, pub fn deinit(self: *Projection, alloc: std.mem.Allocator) void { for (self.owned_overrides.items) |owned| alloc.free(owned.bytes); self.owned_overrides.deinit(alloc); self.entry_actions.deinit(alloc); self.* = undefined; } fn setOwnedOverride( self: *Projection, alloc: std.mem.Allocator, entry_index: usize, kind: transcript_blocks.TranscriptBlockKind, bytes: []u8, ) !void { errdefer alloc.free(bytes); try self.owned_overrides.append(alloc, .{ .entry_index = entry_index, .bytes = bytes, }); self.entry_actions.items[entry_index] = .{ .override = .{ .kind = kind, .bytes = bytes, } }; } fn appendOwnedOverride( self: *Projection, alloc: std.mem.Allocator, kind: transcript_blocks.TranscriptBlockKind, bytes: []u8, ) !void { const entry_index = self.entry_actions.items.len; errdefer alloc.free(bytes); try self.entry_actions.ensureUnusedCapacity(alloc, 1); try self.owned_overrides.append(alloc, .{ .entry_index = entry_index, .bytes = bytes, }); self.entry_actions.appendAssumeCapacity(.{ .override = .{ .kind = kind, .bytes = bytes, } }); } pub fn replaceSuffix( self: *Projection, alloc: std.mem.Allocator, start_index: usize, suffix: *Projection, ) !void { std.debug.assert(start_index <= self.entry_actions.items.len); try self.entry_actions.ensureTotalCapacity( alloc, start_index + suffix.entry_actions.items.len, ); var retained_owned_count: usize = 0; for (self.owned_overrides.items) |owned| { if (owned.entry_index < start_index) retained_owned_count += 1; } try self.owned_overrides.ensureTotalCapacity( alloc, retained_owned_count + suffix.owned_overrides.items.len, ); var retained_index: usize = 0; for (self.owned_overrides.items) |owned| { if (owned.entry_index < start_index) { self.owned_overrides.items[retained_index] = owned; retained_index += 1; } else { alloc.free(owned.bytes); } } self.owned_overrides.items.len = retained_index; self.entry_actions.items.len = start_index; for (suffix.entry_actions.items) |action| { self.entry_actions.appendAssumeCapacity(action); } for (suffix.owned_overrides.items) |owned| { self.owned_overrides.appendAssumeCapacity(.{ .entry_index = start_index + owned.entry_index, .bytes = owned.bytes, }); } suffix.entry_actions.items.len = 0; suffix.owned_overrides.items.len = 0; }}; const OwnedOverride = struct { entry_index: usize, bytes: []u8,}; pub const SummaryStyle = struct { marker_style: []const u8 = "", text_style: []const u8 = "", reset_style: []const u8 = "",}; const BuildStats = struct { detail_lookups: usize = 0,}; const ProjectionMode = enum { compact, expanded }; const category_labels = [_][]const u8{ "read", "list", "write", "edit", "open", "command", "subagent", "browser",};const command_category_index = 5; const Summary = struct { total: usize = 0, categories: [category_labels.len]usize = @splat(0), failed: usize = 0, denied: usize = 0, cancelled: usize = 0, deferred: usize = 0, completion_unreported: usize = 0, not_executed: usize = 0,}; const PresentationGroup = struct { anchor_index: usize, status_indices: std.ArrayList(usize) = .empty, summary: Summary = .{}, fn deinit(self: *PresentationGroup, alloc: std.mem.Allocator) void { self.status_indices.deinit(alloc); self.* = undefined; }}; fn categoryIndex(kind: types.ToolActivityKind) ?usize { return switch (kind) { .read => 0, .list => 1, .write => 2, .edit => 3, .open => 4, .command => command_category_index, .subagent => 6, .ask => null, };} fn detailForEntry( details: []const ToolDetailRecord, detail_indices: *const std.AutoHashMapUnmanaged(u32, usize), entry_id: u32, stats: ?*BuildStats,) ?*const ToolDetailRecord { if (stats) |value| value.detail_lookups += 1; const index = detail_indices.get(entry_id) orelse return null; return &details[index];} fn toolStatusEntryId(entry: TranscriptEntry) ?u32 { return switch (entry) { .raw_bytes => |raw| if (raw.class == .tool_status) raw.id else null, else => null, };} fn skipSgrSequence(text: []const u8, index: *usize) bool { if (index.* + 2 > text.len or text[index.*] != 0x1b or text[index.* + 1] != '[') { return false; } 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') return false; index.* = end + 1; return true;} fn rawStatusNamesAskTool(text: []const u8) bool { var index: usize = 0; while (skipSgrSequence(text, &index)) {} const label = "● ask_user_question"; if (!std.mem.startsWith(u8, text[index..], label)) return false; index += label.len; while (skipSgrSequence(text, &index)) {} return std.mem.eql(u8, text[index..], "") or std.mem.eql(u8, text[index..], "\n") or std.mem.eql(u8, text[index..], "\r\n");} fn statusNamesAsk(entry: TranscriptEntry, detail: ?*const ToolDetailRecord) bool { if (detail) |record| { return record.activity_kind == .ask or std.mem.eql(u8, record.tool_name, "ask_user_question"); } return switch (entry) { .raw_bytes => |raw| rawStatusNamesAskTool(raw.bytes), else => false, };} fn isAttachedEntry(entry: TranscriptEntry) bool { return switch (entry) { .raw_bytes => |raw| raw.class == .command_output or raw.class == .diff_block, else => false, };} fn isTransparentCompactEntry(entry: TranscriptEntry) bool { if (!transcript_blocks.isEntryVisibleInCompactPresentation(entry)) return true; return switch (entry) { .assistant_turn => |assistant| assistant.segments.text.items.len == 0, else => false, };} fn commandProcessFailed(record: *const ToolDetailRecord) bool { if (record.activity_kind != .command) return false; const presentation = record.command_process_presentation orelse return false; return switch (presentation) { .exit_code => |code| code != 0, .signal => true, };} fn observeTool(summary: *Summary, detail: ?*const ToolDetailRecord) void { summary.total += 1; const record = detail orelse return; if (record.activity_kind) |kind| { if (categoryIndex(kind)) |index| summary.categories[index] += 1; } if (record.fallback_disposition) |disposition| { switch (disposition) { .completion_unreported => summary.completion_unreported += 1, .not_executed => summary.not_executed += 1, } return; } const process_failed = commandProcessFailed(record); if (record.outcome) |outcome| { switch (outcome) { .completed => { if (process_failed) summary.failed += 1; }, .failed => summary.failed += 1, .denied => summary.denied += 1, .cancelled => summary.cancelled += 1, .deferred => summary.deferred += 1, } }} fn appendSegment(writer: *std.Io.Writer, count: usize, label: []const u8) !void { if (count == 0) return; try writer.print(" · {d} {s}", .{ count, label });} fn normalizeCanonicalStatus( scratch: std.mem.Allocator, text: []const u8,) !?[]const u8 { var index: usize = 0; while (skipSgrSequence(text, &index)) {} const marker = if (std.mem.startsWith(u8, text[index..], "●")) "●" else if (std.mem.startsWith(u8, text[index..], "■")) "■" else return null; index += marker.len; while (skipSgrSequence(text, &index)) {} while (index < text.len and (text[index] == ' ' or text[index] == '\t')) : (index += 1) {} var out: std.Io.Writer.Allocating = .init(scratch); errdefer out.deinit(); while (index < text.len) { if (skipSgrSequence(text, &index)) continue; const byte = text[index]; if (byte == '\n' or byte == '\r') break; if (byte >= 0x20) try out.writer.writeByte(byte); index += 1; } const owned = try out.toOwnedSlice(); var phrase = std.mem.trim(u8, owned, " \t"); if (std.mem.endsWith(u8, phrase, cancellation_follow_up)) { phrase = std.mem.trimEnd(u8, phrase[0 .. phrase.len - cancellation_follow_up.len], " \t"); } return if (phrase.len == 0) null else phrase;} fn normalizeStatusPhrase( scratch: std.mem.Allocator, text: []const u8,) !?[]const u8 { if (try normalizeCanonicalStatus(scratch, text)) |phrase| return phrase; var out: std.Io.Writer.Allocating = .init(scratch); errdefer out.deinit(); var index: usize = 0; while (index < text.len) { if (skipSgrSequence(text, &index)) continue; const byte = text[index]; if (byte == '\n' or byte == '\r') break; if (byte >= 0x20) try out.writer.writeByte(byte); index += 1; } const owned = try out.toOwnedSlice(); const phrase = std.mem.trim(u8, owned, " \t"); return if (phrase.len == 0) null else phrase;} fn clipSummary( alloc: std.mem.Allocator, text: []const u8, cols: u16,) ![]u8 { if (display_width.visibleWidth(text) <= cols) return try alloc.dupe(u8, text); if (cols == 0) return try alloc.dupe(u8, ""); if (cols == 1) return try alloc.dupe(u8, "…"); const prefix = display_width.prefixByWidth(text, cols - 1); return try std.fmt.allocPrint(alloc, "{s}…", .{prefix});} fn applySummaryStyle( alloc: std.mem.Allocator, text: []const u8, style: SummaryStyle,) ![]u8 { if (style.marker_style.len == 0 and style.text_style.len == 0 and style.reset_style.len == 0) { return try alloc.dupe(u8, text); } const marker = "●"; if (!std.mem.startsWith(u8, text, marker)) return try alloc.dupe(u8, text); var content_start = marker.len; const has_separator = content_start < text.len and text[content_start] == ' '; if (has_separator) content_start += 1; var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.writeAll(style.marker_style); try out.writer.writeAll(marker); try out.writer.writeAll(style.reset_style); if (content_start < text.len) { if (has_separator) try out.writer.writeByte(' '); try out.writer.writeAll(style.text_style); try out.writer.writeAll(text[content_start..]); try out.writer.writeAll(style.reset_style); } return try out.toOwnedSlice();} fn formatGroupHeader( alloc: std.mem.Allocator, summary: Summary, cols: u16, style: SummaryStyle,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.print("● {d} tool call{s}", .{ summary.total, if (summary.total == 1) "" else "s", }); var emitted: [category_labels.len]bool = @splat(false); for (0..category_labels.len) |_| { var next_index: ?usize = null; for (summary.categories, 0..) |count, index| { if (count == 0 or emitted[index]) continue; if (next_index == null or count > summary.categories[next_index.?]) { next_index = index; } } const index = next_index orelse break; emitted[index] = true; const count = summary.categories[index]; const label = if (index == command_category_index and count != 1) "commands" else category_labels[index]; try appendSegment(&out.writer, count, label); } try appendSegment(&out.writer, summary.completion_unreported, "unreported"); try appendSegment(&out.writer, summary.not_executed, "not executed"); try appendSegment(&out.writer, summary.failed, "failed"); try appendSegment(&out.writer, summary.denied, "denied"); try appendSegment(&out.writer, summary.cancelled, "cancelled"); try appendSegment(&out.writer, summary.deferred, "deferred"); const plain = try out.toOwnedSlice(); defer alloc.free(plain); const clipped = try clipSummary(alloc, plain, cols); defer alloc.free(clipped); return applySummaryStyle(alloc, clipped, style);} fn formatGroupBlock( alloc: std.mem.Allocator, entries: []const TranscriptEntry, status_indices: []const usize, details: []const ToolDetailRecord, detail_indices: *const std.AutoHashMapUnmanaged(u32, usize), summary: Summary, focused_entry_id: ?u32, cols: u16, style: SummaryStyle, styles: transcript_blocks.Styles,) ![]u8 { const header = try formatGroupHeader(alloc, summary, cols, style); defer alloc.free(header); var focused_in_group = false; var static_count: usize = 0; for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null); if (statusNamesAsk(entry, detail)) continue; if (focused_entry_id == entry_id) { focused_in_group = true; } else { static_count += 1; } } var scratch_state = std.heap.ArenaAllocator.init(alloc); defer scratch_state.deinit(); const scratch = scratch_state.allocator(); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.writeAll(header); var static_index: usize = 0; for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null); if (statusNamesAsk(entry, detail) or focused_entry_id == entry_id) continue; const phrase = switch (entry) { .raw_bytes => |raw| try normalizeCanonicalStatus(scratch, raw.bytes), else => null, } orelse if (detail) |record| record.tool_name else "tool activity"; static_index += 1; const connector = if (!focused_in_group and static_index == static_count) "└" else "├"; const child = try std.fmt.allocPrint(scratch, "{s} {s}", .{ connector, phrase }); const clipped = try clipSummary(scratch, child, cols); try out.writer.writeByte('\n'); if (style.text_style.len > 0) try out.writer.writeAll(style.text_style); try out.writer.writeAll(clipped); if (style.text_style.len > 0) try out.writer.writeAll(style.reset_style); } for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null) orelse continue; if (detail.outcome != .cancelled) continue; const terminal = try transcript_blocks.renderEntryToBlock(scratch, entry, cols, styles); if (terminal.bytes.len > 0) { try out.writer.writeAll("\n\n"); try out.writer.writeAll(terminal.bytes); } terminal.deinit(scratch); } return out.toOwnedSlice();} fn formatExpandedChild( alloc: std.mem.Allocator, entry: TranscriptEntry, detail: ?*const ToolDetailRecord, connector: []const u8, cols: u16,) ![]u8 { var scratch_state = std.heap.ArenaAllocator.init(alloc); defer scratch_state.deinit();Lines 1–500 of 1976. Load more
const std = @import("std"); const build_checkpoint = @import("../render_engine/build_checkpoint.zig"); const transcript_blocks = @import("../render_engine/transcript_blocks.zig"); const types = @import("../../core/shared/types.zig"); const display_width = @import("../../core/shared/display_width.zig"); const sort_utils = @import("../../core/shared/sort_utils.zig"); const TranscriptEntry = transcript_blocks.TranscriptEntry; const ToolDetailRecord = transcript_blocks.ToolDetailRecord; const cancellation_follow_up = " · What can fx do differently?"; pub const Projection = struct { entry_actions: std.ArrayList(transcript_blocks.EntryRenderAction) = .empty, owned_overrides: std.ArrayList(OwnedOverride) = .empty, pub fn deinit(self: *Projection, alloc: std.mem.Allocator) void { for (self.owned_overrides.items) |owned| alloc.free(owned.bytes); self.owned_overrides.deinit(alloc); self.entry_actions.deinit(alloc); self.* = undefined; } fn setOwnedOverride( self: *Projection, alloc: std.mem.Allocator, entry_index: usize, kind: transcript_blocks.TranscriptBlockKind, bytes: []u8, ) !void { errdefer alloc.free(bytes); try self.owned_overrides.append(alloc, .{ .entry_index = entry_index, .bytes = bytes, }); self.entry_actions.items[entry_index] = .{ .override = .{ .kind = kind, .bytes = bytes, } }; } fn appendOwnedOverride( self: *Projection, alloc: std.mem.Allocator, kind: transcript_blocks.TranscriptBlockKind, bytes: []u8, ) !void { const entry_index = self.entry_actions.items.len; errdefer alloc.free(bytes); try self.entry_actions.ensureUnusedCapacity(alloc, 1); try self.owned_overrides.append(alloc, .{ .entry_index = entry_index, .bytes = bytes, }); self.entry_actions.appendAssumeCapacity(.{ .override = .{ .kind = kind, .bytes = bytes, } }); } pub fn replaceSuffix( self: *Projection, alloc: std.mem.Allocator, start_index: usize, suffix: *Projection, ) !void { std.debug.assert(start_index <= self.entry_actions.items.len); try self.entry_actions.ensureTotalCapacity( alloc, start_index + suffix.entry_actions.items.len, ); var retained_owned_count: usize = 0; for (self.owned_overrides.items) |owned| { if (owned.entry_index < start_index) retained_owned_count += 1; } try self.owned_overrides.ensureTotalCapacity( alloc, retained_owned_count + suffix.owned_overrides.items.len, ); var retained_index: usize = 0; for (self.owned_overrides.items) |owned| { if (owned.entry_index < start_index) { self.owned_overrides.items[retained_index] = owned; retained_index += 1; } else { alloc.free(owned.bytes); } } self.owned_overrides.items.len = retained_index; self.entry_actions.items.len = start_index; for (suffix.entry_actions.items) |action| { self.entry_actions.appendAssumeCapacity(action); } for (suffix.owned_overrides.items) |owned| { self.owned_overrides.appendAssumeCapacity(.{ .entry_index = start_index + owned.entry_index, .bytes = owned.bytes, }); } suffix.entry_actions.items.len = 0; suffix.owned_overrides.items.len = 0; } }; const OwnedOverride = struct { entry_index: usize, bytes: []u8, }; pub const SummaryStyle = struct { marker_style: []const u8 = "", text_style: []const u8 = "", reset_style: []const u8 = "", }; const BuildStats = struct { detail_lookups: usize = 0, }; const ProjectionMode = enum { compact, expanded }; const category_labels = [_][]const u8{ "read", "list", "write", "edit", "open", "command", "subagent", "browser", }; const command_category_index = 5; const Summary = struct { total: usize = 0, categories: [category_labels.len]usize = @splat(0), failed: usize = 0, denied: usize = 0, cancelled: usize = 0, deferred: usize = 0, completion_unreported: usize = 0, not_executed: usize = 0, }; const PresentationGroup = struct { anchor_index: usize, status_indices: std.ArrayList(usize) = .empty, summary: Summary = .{}, fn deinit(self: *PresentationGroup, alloc: std.mem.Allocator) void { self.status_indices.deinit(alloc); self.* = undefined; } }; fn categoryIndex(kind: types.ToolActivityKind) ?usize { return switch (kind) { .read => 0, .list => 1, .write => 2, .edit => 3, .open => 4, .command => command_category_index, .subagent => 6, .ask => null, }; } fn detailForEntry( details: []const ToolDetailRecord, detail_indices: *const std.AutoHashMapUnmanaged(u32, usize), entry_id: u32, stats: ?*BuildStats, ) ?*const ToolDetailRecord { if (stats) |value| value.detail_lookups += 1; const index = detail_indices.get(entry_id) orelse return null; return &details[index]; } fn toolStatusEntryId(entry: TranscriptEntry) ?u32 { return switch (entry) { .raw_bytes => |raw| if (raw.class == .tool_status) raw.id else null, else => null, }; } fn skipSgrSequence(text: []const u8, index: *usize) bool { if (index.* + 2 > text.len or text[index.*] != 0x1b or text[index.* + 1] != '[') { return false; } 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') return false; index.* = end + 1; return true; } fn rawStatusNamesAskTool(text: []const u8) bool { var index: usize = 0; while (skipSgrSequence(text, &index)) {} const label = "● ask_user_question"; if (!std.mem.startsWith(u8, text[index..], label)) return false; index += label.len; while (skipSgrSequence(text, &index)) {} return std.mem.eql(u8, text[index..], "") or std.mem.eql(u8, text[index..], "\n") or std.mem.eql(u8, text[index..], "\r\n"); } fn statusNamesAsk(entry: TranscriptEntry, detail: ?*const ToolDetailRecord) bool { if (detail) |record| { return record.activity_kind == .ask or std.mem.eql(u8, record.tool_name, "ask_user_question"); } return switch (entry) { .raw_bytes => |raw| rawStatusNamesAskTool(raw.bytes), else => false, }; } fn isAttachedEntry(entry: TranscriptEntry) bool { return switch (entry) { .raw_bytes => |raw| raw.class == .command_output or raw.class == .diff_block, else => false, }; } fn isTransparentCompactEntry(entry: TranscriptEntry) bool { if (!transcript_blocks.isEntryVisibleInCompactPresentation(entry)) return true; return switch (entry) { .assistant_turn => |assistant| assistant.segments.text.items.len == 0, else => false, }; } fn commandProcessFailed(record: *const ToolDetailRecord) bool { if (record.activity_kind != .command) return false; const presentation = record.command_process_presentation orelse return false; return switch (presentation) { .exit_code => |code| code != 0, .signal => true, }; } fn observeTool(summary: *Summary, detail: ?*const ToolDetailRecord) void { summary.total += 1; const record = detail orelse return; if (record.activity_kind) |kind| { if (categoryIndex(kind)) |index| summary.categories[index] += 1; } if (record.fallback_disposition) |disposition| { switch (disposition) { .completion_unreported => summary.completion_unreported += 1, .not_executed => summary.not_executed += 1, } return; } const process_failed = commandProcessFailed(record); if (record.outcome) |outcome| { switch (outcome) { .completed => { if (process_failed) summary.failed += 1; }, .failed => summary.failed += 1, .denied => summary.denied += 1, .cancelled => summary.cancelled += 1, .deferred => summary.deferred += 1, } } } fn appendSegment(writer: *std.Io.Writer, count: usize, label: []const u8) !void { if (count == 0) return; try writer.print(" · {d} {s}", .{ count, label }); } fn normalizeCanonicalStatus( scratch: std.mem.Allocator, text: []const u8, ) !?[]const u8 { var index: usize = 0; while (skipSgrSequence(text, &index)) {} const marker = if (std.mem.startsWith(u8, text[index..], "●")) "●" else if (std.mem.startsWith(u8, text[index..], "■")) "■" else return null; index += marker.len; while (skipSgrSequence(text, &index)) {} while (index < text.len and (text[index] == ' ' or text[index] == '\t')) : (index += 1) {} var out: std.Io.Writer.Allocating = .init(scratch); errdefer out.deinit(); while (index < text.len) { if (skipSgrSequence(text, &index)) continue; const byte = text[index]; if (byte == '\n' or byte == '\r') break; if (byte >= 0x20) try out.writer.writeByte(byte); index += 1; } const owned = try out.toOwnedSlice(); var phrase = std.mem.trim(u8, owned, " \t"); if (std.mem.endsWith(u8, phrase, cancellation_follow_up)) { phrase = std.mem.trimEnd(u8, phrase[0 .. phrase.len - cancellation_follow_up.len], " \t"); } return if (phrase.len == 0) null else phrase; } fn normalizeStatusPhrase( scratch: std.mem.Allocator, text: []const u8, ) !?[]const u8 { if (try normalizeCanonicalStatus(scratch, text)) |phrase| return phrase; var out: std.Io.Writer.Allocating = .init(scratch); errdefer out.deinit(); var index: usize = 0; while (index < text.len) { if (skipSgrSequence(text, &index)) continue; const byte = text[index]; if (byte == '\n' or byte == '\r') break; if (byte >= 0x20) try out.writer.writeByte(byte); index += 1; } const owned = try out.toOwnedSlice(); const phrase = std.mem.trim(u8, owned, " \t"); return if (phrase.len == 0) null else phrase; } fn clipSummary( alloc: std.mem.Allocator, text: []const u8, cols: u16, ) ![]u8 { if (display_width.visibleWidth(text) <= cols) return try alloc.dupe(u8, text); if (cols == 0) return try alloc.dupe(u8, ""); if (cols == 1) return try alloc.dupe(u8, "…"); const prefix = display_width.prefixByWidth(text, cols - 1); return try std.fmt.allocPrint(alloc, "{s}…", .{prefix}); } fn applySummaryStyle( alloc: std.mem.Allocator, text: []const u8, style: SummaryStyle, ) ![]u8 { if (style.marker_style.len == 0 and style.text_style.len == 0 and style.reset_style.len == 0) { return try alloc.dupe(u8, text); } const marker = "●"; if (!std.mem.startsWith(u8, text, marker)) return try alloc.dupe(u8, text); var content_start = marker.len; const has_separator = content_start < text.len and text[content_start] == ' '; if (has_separator) content_start += 1; var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.writeAll(style.marker_style); try out.writer.writeAll(marker); try out.writer.writeAll(style.reset_style); if (content_start < text.len) { if (has_separator) try out.writer.writeByte(' '); try out.writer.writeAll(style.text_style); try out.writer.writeAll(text[content_start..]); try out.writer.writeAll(style.reset_style); } return try out.toOwnedSlice(); } fn formatGroupHeader( alloc: std.mem.Allocator, summary: Summary, cols: u16, style: SummaryStyle, ) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.print("● {d} tool call{s}", .{ summary.total, if (summary.total == 1) "" else "s", }); var emitted: [category_labels.len]bool = @splat(false); for (0..category_labels.len) |_| { var next_index: ?usize = null; for (summary.categories, 0..) |count, index| { if (count == 0 or emitted[index]) continue; if (next_index == null or count > summary.categories[next_index.?]) { next_index = index; } } const index = next_index orelse break; emitted[index] = true; const count = summary.categories[index]; const label = if (index == command_category_index and count != 1) "commands" else category_labels[index]; try appendSegment(&out.writer, count, label); } try appendSegment(&out.writer, summary.completion_unreported, "unreported"); try appendSegment(&out.writer, summary.not_executed, "not executed"); try appendSegment(&out.writer, summary.failed, "failed"); try appendSegment(&out.writer, summary.denied, "denied"); try appendSegment(&out.writer, summary.cancelled, "cancelled"); try appendSegment(&out.writer, summary.deferred, "deferred"); const plain = try out.toOwnedSlice(); defer alloc.free(plain); const clipped = try clipSummary(alloc, plain, cols); defer alloc.free(clipped); return applySummaryStyle(alloc, clipped, style); } fn formatGroupBlock( alloc: std.mem.Allocator, entries: []const TranscriptEntry, status_indices: []const usize, details: []const ToolDetailRecord, detail_indices: *const std.AutoHashMapUnmanaged(u32, usize), summary: Summary, focused_entry_id: ?u32, cols: u16, style: SummaryStyle, styles: transcript_blocks.Styles, ) ![]u8 { const header = try formatGroupHeader(alloc, summary, cols, style); defer alloc.free(header); var focused_in_group = false; var static_count: usize = 0; for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null); if (statusNamesAsk(entry, detail)) continue; if (focused_entry_id == entry_id) { focused_in_group = true; } else { static_count += 1; } } var scratch_state = std.heap.ArenaAllocator.init(alloc); defer scratch_state.deinit(); const scratch = scratch_state.allocator(); var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try out.writer.writeAll(header); var static_index: usize = 0; for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null); if (statusNamesAsk(entry, detail) or focused_entry_id == entry_id) continue; const phrase = switch (entry) { .raw_bytes => |raw| try normalizeCanonicalStatus(scratch, raw.bytes), else => null, } orelse if (detail) |record| record.tool_name else "tool activity"; static_index += 1; const connector = if (!focused_in_group and static_index == static_count) "└" else "├"; const child = try std.fmt.allocPrint(scratch, "{s} {s}", .{ connector, phrase }); const clipped = try clipSummary(scratch, child, cols); try out.writer.writeByte('\n'); if (style.text_style.len > 0) try out.writer.writeAll(style.text_style); try out.writer.writeAll(clipped); if (style.text_style.len > 0) try out.writer.writeAll(style.reset_style); } for (status_indices) |status_index| { const entry = entries[status_index]; const entry_id = toolStatusEntryId(entry) orelse continue; const detail = detailForEntry(details, detail_indices, entry_id, null) orelse continue; if (detail.outcome != .cancelled) continue; const terminal = try transcript_blocks.renderEntryToBlock(scratch, entry, cols, styles); if (terminal.bytes.len > 0) { try out.writer.writeAll("\n\n"); try out.writer.writeAll(terminal.bytes); } terminal.deinit(scratch); } return out.toOwnedSlice(); } fn formatExpandedChild( alloc: std.mem.Allocator, entry: TranscriptEntry, detail: ?*const ToolDetailRecord, connector: []const u8, cols: u16, ) ![]u8 { var scratch_state = std.heap.ArenaAllocator.init(alloc); defer scratch_state.deinit();