diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 70a3f696..959052a9 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -448,7 +448,7 @@ fn runRepl(self: *Agent) void { var arena: std.heap.ArenaAllocator = .init(self.allocator); defer arena.deinit(); const result = self.cmd_runner.executeWithResult(arena.allocator(), cmd); - self.terminal.endTool(!result.is_error); + self.terminal.endTool(); self.cmd_runner.printResult(cmd, result); if (self.recorder) |*r| r.record(cmd); }, @@ -495,14 +495,14 @@ fn handleSlash(self: *Agent, body: []const u8) bool { self.terminal.beginTool(schema.tool_name, rest); if (self.tool_executor.call(aa, schema.tool_name, args_json)) |result| { - self.terminal.endTool(!result.is_error); + self.terminal.endTool(); if (result.is_error) { self.terminal.printErrorFmt("{s}: {s}", .{ schema.tool_name, result.text }); } else { self.terminal.printToolResult(schema.tool_name, result.text); } } else |err| { - self.terminal.endTool(false); + self.terminal.endTool(); self.terminal.printErrorFmt("{s}: {s}", .{ schema.tool_name, @errorName(err) }); } return false; diff --git a/src/agent/Spinner.zig b/src/agent/Spinner.zig index f24475d8..f73fed5d 100644 --- a/src/agent/Spinner.zig +++ b/src/agent/Spinner.zig @@ -23,8 +23,7 @@ const ansi = @import("Terminal.zig").ansi; const Spinner = @This(); -const dots = [_][]const u8{ " ", ". ", ".. ", "..." }; -const braille = [_][]const u8{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }; +const braille = [_][]const u8{ "⡇", "⣆", "⣤", "⣰", "⢸", "⠹", "⠛", "⠏" }; const interval_ns: u64 = 100 * std.time.ns_per_ms; /// Minimum dwell on a tool label so the user can read it. Slow tools exceed /// this naturally; fast ones (getUrl, getCookies) get padded. @@ -44,8 +43,6 @@ const ToolState = struct { /// Worker should flip back to thinking once dwell elapses. Cleared by a /// fresh `setTool` (which overrides the dwell with a new label). dwell_pending: bool = false, - /// Render the label in red — set by `markToolFailed`, cleared by next setTool. - failed: bool = false, /// User-typed REPL commands drop the `agent:` framing since no agent /// is involved — it's lightpanda running the command directly. manual: bool = false, @@ -168,7 +165,6 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { @memcpy(tool.name_buf[0..tool.name_len], name[0..tool.name_len]); tool.args_len = @min(args.len, tool.args_buf.len); @memcpy(tool.args_buf[0..tool.args_len], args[0..tool.args_len]); - self.frame = 0; self.state = .{ .tool = tool }; // Manual paths skip `start()`, so spawn the worker on demand to drive // the braille animation. @@ -177,22 +173,6 @@ pub fn setTool(self: *Spinner, name: []const u8, args: []const u8) void { self.cv.signal(); } -/// Repaint the active tool label in red to flag a failed tool call. Visible -/// for the rest of the dwell window (`min_tool_display_ns`), then the -/// indicator returns to thinking like any other call. -pub fn markToolFailed(self: *Spinner) void { - if (!self.isEnabled()) return; - self.mu.lock(); - defer self.mu.unlock(); - switch (self.state) { - .tool => { - self.state.tool.failed = true; - self.renderLocked(); - }, - else => {}, - } -} - /// Request a transition back to the cycling "thinking" state. The worker /// honors `min_tool_display_ns` — if the current tool label has not been /// up long enough, the flip is deferred until it has. @@ -241,7 +221,6 @@ fn workerLoop(self: *Spinner) void { const delta: i128 = std.time.nanoTimestamp() - self.state.tool.set_ns; if (delta >= @as(i128, min_tool_display_ns)) { self.state = .thinking; - self.frame = 0; } } }, @@ -250,32 +229,27 @@ fn workerLoop(self: *Spinner) void { self.renderLocked(); - if (self.state == .thinking) { - self.frame = (self.frame + 1) % @as(u8, @intCast(dots.len)); - } else if (std.meta.activeTag(self.state) == .tool and self.state.tool.manual) { - self.frame = (self.frame + 1) % @as(u8, @intCast(braille.len)); - } + self.frame = (self.frame + 1) % @as(u8, @intCast(braille.len)); self.cv.timedWait(&self.mu, interval_ns) catch {}; } } fn renderLocked(self: *Spinner) void { var buf: [frame_buf_bytes]u8 = undefined; + const glyph = braille[self.frame % braille.len]; const written = switch (self.state) { .idle => return, .thinking => std.fmt.bufPrint( &buf, - "\r" ++ ansi.yellow ++ "●" ++ ansi.reset ++ " " ++ ansi.dim ++ "[agent: thinking{s}]" ++ ansi.reset ++ clear_eol, - .{dots[self.frame % dots.len]}, + "\r" ++ ansi.yellow ++ "{s}" ++ ansi.reset ++ " " ++ ansi.dim ++ "[agent: thinking]" ++ ansi.reset ++ clear_eol, + .{glyph}, ) catch return, .tool => |tool| blk: { - const color: []const u8 = if (tool.failed) ansi.red else if (tool.manual) ansi.yellow else ansi.green; - const glyph: []const u8 = if (tool.manual) braille[self.frame % braille.len] else "●"; const prefix: []const u8 = if (tool.manual) "" else "agent: "; break :blk std.fmt.bufPrint( &buf, - "\r{s}{s}" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s}{s} {s}]" ++ ansi.reset ++ clear_eol, - .{ color, glyph, prefix, tool.name_buf[0..tool.name_len], tool.args_buf[0..tool.args_len] }, + "\r" ++ ansi.yellow ++ "{s}" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s}{s} {s}]" ++ ansi.reset ++ clear_eol, + .{ glyph, prefix, tool.name_buf[0..tool.name_len], tool.args_buf[0..tool.args_len] }, ) catch return; }, }; diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index 35f4f55f..750906f2 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -135,25 +135,19 @@ pub fn beginTool(self: *Terminal, name: []const u8, args: []const u8) void { self.spinner.setTool(name, args); } -/// Mark the end of a manual REPL tool call. On failure, flashes the spinner -/// label red before clearing it. -pub fn endTool(self: *Terminal, ok: bool) void { - if (!ok) self.spinner.markToolFailed(); +/// Mark the end of a manual REPL tool call. Clears the running spinner; the +/// caller's `printToolResult` / `printError` lays down the colored status dot. +pub fn endTool(self: *Terminal) void { self.spinner.cancel(); } -/// Called after the tool returns. -/// -/// - Spinner mode (TTY REPL): the running label flashes red on failure -/// (handled by `markToolFailed`). At `medium`+, *also* commit a -/// `● [tool: …]` line above the spinner so the run leaves a trace. -/// - No spinner (non-TTY/non-REPL): print the same line directly, -/// gated on `medium`+. In non-TTY contexts ANSI is still emitted — -/// pipes that strip color see plain text via the bullet character. +/// Called after the tool returns. At `medium`+, commits a `● [tool: …]` line +/// above the spinner (green/red bullet for ok/fail) so the run leaves a trace. +/// In non-TTY contexts ANSI is still emitted — pipes that strip color see +/// plain text via the bullet character. pub fn agentToolDone(self: *Terminal, name: []const u8, args: []const u8, ok: bool) void { - const spinner_on = self.spinner.isEnabled(); - if (spinner_on and !ok) self.spinner.markToolFailed(); if (!atLeast(self.verbosity, .medium)) return; + const spinner_on = self.spinner.isEnabled(); if (spinner_on) { const a = if (self.repl_arena) |*ra| ra else return;