agent: unify REPL result and error formatting

This commit is contained in:
Adrià Arrufat
2026-05-12 12:22:17 +02:00
parent ff03d96f64
commit bfe9f98fb7
2 changed files with 17 additions and 26 deletions

View File

@@ -109,32 +109,14 @@ pub fn stop(self: *Self) void {
self.last_render_len = 0;
}
/// End a turn with no commit (used on hard API errors, where the caller will
/// surface the error itself).
/// End a turn with no commit. The caller is responsible for surfacing the
/// outcome — tool results, error messages, or summaries.
pub fn cancel(self: *Self) void {
if (!self.enabled) return;
self.mu.lock();
defer self.mu.unlock();
if (self.state == .idle) return;
// Manual command success → commit a green `●` line so the user sees a
// permanent "done" confirmation. Errors and agent-side cancels just clear.
if (std.meta.activeTag(self.state) == .tool and self.state.tool.manual and !self.state.tool.failed) {
const tool = self.state.tool;
var buf: [frame_buf_bytes]u8 = undefined;
const line = std.fmt.bufPrint(
&buf,
"\r" ++ ansi.green ++ "" ++ ansi.reset ++ " " ++ ansi.dim ++ "[{s} {s}]" ++ ansi.reset ++ clear_eol ++ "\n",
.{ tool.name_buf[0..tool.name_len], tool.args_buf[0..tool.args_len] },
) catch {
_ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {};
self.state = .idle;
self.last_render_len = 0;
return;
};
_ = std.posix.write(std.posix.STDERR_FILENO, line) catch {};
} else {
_ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {};
}
_ = std.posix.write(std.posix.STDERR_FILENO, "\r" ++ clear_eol) catch {};
self.state = .idle;
self.last_render_len = 0;
}

View File

@@ -599,7 +599,7 @@ pub fn printToolResult(self: *Self, name: []const u8, result: []const u8) void {
if (!self.isRepl() and !atLeast(self.verbosity, .high)) return;
if (self.repl_arena) |*a| {
defer _ = a.reset(.retain_capacity);
const bytes = formatReplResult(a.allocator(), name, result) catch return;
const bytes = formatReplResult(a.allocator(), result) catch return;
if (self.spinner.emitAbove(bytes)) return;
_ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {};
return;
@@ -609,10 +609,10 @@ pub fn printToolResult(self: *Self, name: []const u8, result: []const u8) void {
std.debug.print("{s}{s}[result: {s}]{s} {s}{s}\n", .{ ansi.dim, ansi.green, name, ansi.reset, truncated, ellipsis });
}
/// REPL output: header + body, pretty-print JSON if parseable, raw otherwise.
/// REPL output: green-dot marker followed by the body, pretty-printed if JSON.
/// Builds the entire payload in the arena so callers can route it past the
/// spinner (`emitAbove`) without interleaving with frame writes.
fn formatReplResult(arena: std.mem.Allocator, name: []const u8, result: []const u8) ![]const u8 {
fn formatReplResult(arena: std.mem.Allocator, result: []const u8) ![]const u8 {
var aw: std.Io.Writer.Allocating = .init(arena);
const w = &aw.writer;
@@ -626,7 +626,7 @@ fn formatReplResult(arena: std.mem.Allocator, name: []const u8, result: []const
else
null;
const sep: []const u8 = if (parsed != null) "\n" else " ";
try w.print("{s}{s}[result: {s}]{s}{s}", .{ ansi.dim, ansi.green, name, ansi.reset, sep });
try w.print("{s}{s}{s}", .{ ansi.green, ansi.reset, sep });
if (parsed) |v| {
std.json.Stringify.value(v, .{ .whitespace = .indent_2 }, w) catch {
try w.writeAll(result);
@@ -642,7 +642,16 @@ pub fn printError(self: *Self, msg: []const u8) void {
self.printErrorFmt("{s}", .{msg});
}
pub fn printErrorFmt(_: *Self, comptime fmt: []const u8, args: anytype) void {
pub fn printErrorFmt(self: *Self, comptime fmt: []const u8, args: anytype) void {
if (self.repl_arena) |*a| {
defer _ = a.reset(.retain_capacity);
var aw: std.Io.Writer.Allocating = .init(a.allocator());
aw.writer.print("{s}●{s} " ++ fmt ++ "\n", .{ ansi.red, ansi.reset } ++ args) catch return;
const bytes = aw.written();
if (self.spinner.emitAbove(bytes)) return;
_ = std.posix.write(std.posix.STDERR_FILENO, bytes) catch {};
return;
}
std.debug.print("{s}{s}Error: " ++ fmt ++ "{s}\n", .{ ansi.bold, ansi.red } ++ args ++ .{ansi.reset});
}