summaryrefslogtreecommitdiff
path: root/src/tui_components.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui_components.zig')
-rw-r--r--src/tui_components.zig75
1 files changed, 63 insertions, 12 deletions
diff --git a/src/tui_components.zig b/src/tui_components.zig
index f85c294..545ccce 100644
--- a/src/tui_components.zig
+++ b/src/tui_components.zig
@@ -446,8 +446,8 @@ fn renderBlock(
@memset(right_spaces, ' ');
const composed = try std.fmt.allocPrint(alloc, "{s}{s}{s}{s}{s}{s}", .{
bg_style.open(), fg_style.open(),
- indent, vis, right_spaces,
- ansi_reset,
+ indent, vis,
+ right_spaces, ansi_reset,
});
try out.append(alloc, composed);
}
@@ -2392,8 +2392,12 @@ pub const ToolUse = struct {
/// Resolve the tool name (from `tool_details` or the completed block).
pub fn setName(self: *ToolUse, name: []const u8) !void {
- if (self.name == null) self.name = .empty;
- self.name.?.clearRetainingCapacity();
+ if (self.name) |existing| {
+ if (std.mem.eql(u8, existing.items, name)) return;
+ self.name.?.clearRetainingCapacity();
+ } else {
+ self.name = .empty;
+ }
try self.name.?.appendSlice(self.alloc, name);
self.cache.markDirty();
}
@@ -2401,11 +2405,17 @@ pub const ToolUse = struct {
/// Append a streaming args delta (verbatim JSON bytes).
pub fn appendInput(self: *ToolUse, delta: []const u8) !void {
try self.input.appendSlice(self.alloc, delta);
- self.cache.markDirty();
+ // Input arrives as an append-only stream. Keep the previous render as
+ // the diff baseline so the engine can roll back only to the first
+ // header line that actually changed instead of conservatively cutting
+ // from the top of the tool (and reprinting everything below it) on
+ // every args chunk.
+ self.cache.markDirtyAppend();
}
/// Replace the input verbatim (e.g. from the completed block's `input`).
pub fn setInput(self: *ToolUse, value: []const u8) !void {
+ if (std.mem.eql(u8, self.input.items, value)) return;
self.input.clearRetainingCapacity();
try self.input.appendSlice(self.alloc, value);
self.cache.markDirty();
@@ -2448,8 +2458,8 @@ pub const ToolUse = struct {
// false -> error (dark red)
const bg = theme.default.fg(.tool_pending_bg);
const header_fg = theme.default.fg(.tool_header);
- const dim_fg = theme.default.fg(.dim);
- const plain_fg = theme.default.fg(.assistant);
+ const dim_fg = theme.default.fg(.dim);
+ const plain_fg = theme.default.fg(.assistant);
// Padding within the block (1 col each side matches pi).
const pad: usize = 1;
@@ -2468,8 +2478,7 @@ pub const ToolUse = struct {
const rp = try ctx.a.alloc(u8, right_pad_n);
defer ctx.a.free(rp);
@memset(rp, ' ');
- return std.fmt.allocPrint(ctx.a, "{s}{s}{s}{s}{s}{s}",
- .{ ctx.bg.open(), fg.open(), indent, text, rp, ansi_reset });
+ return std.fmt.allocPrint(ctx.a, "{s}{s}{s}{s}{s}{s}", .{ ctx.bg.open(), fg.open(), indent, text, rp, ansi_reset });
}
fn blank(ctx: @This()) ![]u8 {
return ctx.line(.{ .open_seq = "", .is_plain = true }, "");
@@ -3490,7 +3499,10 @@ test "CompactionSummary: header + wrapped summary within width" {
try testing.expect(lines.len >= 4);
// Content starts at lines[1] (lines[0] is the top margin).
var found_compacted = false;
- for (lines) |l| if (std.mem.indexOf(u8, l, "compacted") != null) { found_compacted = true; break; };
+ for (lines) |l| if (std.mem.indexOf(u8, l, "compacted") != null) {
+ found_compacted = true;
+ break;
+ };
try testing.expect(found_compacted);
for (lines) |l| try testing.expect(vw(l) <= 20);
}
@@ -3502,7 +3514,10 @@ test "ToolUse: stage 1 renders tool (?) before the name resolves" {
// margin + pad + header + pad + margin = 5
try testing.expectEqual(@as(usize, 5), lines.len);
var found = false;
- for (lines) |l| if (std.mem.indexOf(u8, l, "tool (?)") != null) { found = true; break; };
+ for (lines) |l| if (std.mem.indexOf(u8, l, "tool (?)") != null) {
+ found = true;
+ break;
+ };
try testing.expect(found);
}
@@ -3648,6 +3663,43 @@ test "ToolUse: collapse/expand is a length change with a cache-derived firstLine
try testing.expectEqual(@as(usize, 15), recollapsed.len);
}
+test "ToolUse: streaming args keep the render baseline for differential updates" {
+ var t = ToolUse.init(testing.allocator);
+ defer t.deinit();
+ try t.setName("search");
+ try t.appendInput("{\"q\":");
+
+ _ = try t.comp().render(40, testing.allocator);
+ try testing.expectEqual(@as(?usize, null), t.comp().firstLineChanged());
+
+ const old_len = t.cache.lines.?.len;
+ try t.appendInput("\"term\"}");
+
+ // Args deltas are append-only, so the component must retain its old lines
+ // as the diff baseline. Dropping the cache here made the engine cut from
+ // the top of the tool on every streamed args chunk, repainting all content
+ // below it and causing visible flicker in long transcripts.
+ try testing.expect(t.cache.lines != null);
+ try testing.expectEqual(old_len, t.cache.lines.?.len);
+ try testing.expectEqual(@as(?usize, old_len - 1), t.comp().firstLineChanged());
+
+ _ = try t.comp().render(40, testing.allocator);
+ try testing.expectEqual(@as(?usize, null), t.comp().firstLineChanged());
+}
+
+test "ToolUse: final identical name/input does not dirty a clean tool" {
+ var t = ToolUse.init(testing.allocator);
+ defer t.deinit();
+ try t.setName("search");
+ try t.setInput("{\"q\":\"term\"}");
+
+ _ = try t.comp().render(40, testing.allocator);
+ try testing.expectEqual(@as(?usize, null), t.comp().firstLineChanged());
+
+ try t.setName("search");
+ try t.setInput("{\"q\":\"term\"}");
+ try testing.expectEqual(@as(?usize, null), t.comp().firstLineChanged());
+}
test "ToolUse: args are rendered VERBATIM (no pretty-print) and within width" {
// The input JSON must pass through byte-for-byte (no reflow of the JSON
// structure), only terminal-wrapped. We use a compact object with no spaces
@@ -3681,4 +3733,3 @@ test "ToolUse: long output lines honor the width contract" {
const lines = try t.comp().render(10, testing.allocator);
for (lines) |l| try testing.expect(vw(l) <= 10);
}
-