mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
agent: simplify spinner and remove tool failure state
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user