1976 lines · 76.6 KB · Zig
RawDownload
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.