From c1b4c0e52e4ad41e2c9e695980e2704beeef6b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Mon, 11 May 2026 18:56:34 +0200 Subject: [PATCH] agent: report recording status and refine verification --- src/agent/Agent.zig | 8 +++++++- src/agent/Recorder.zig | 15 ++++++++++----- src/agent/Terminal.zig | 9 +++++++-- src/agent/Verifier.zig | 4 +++- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/agent/Agent.zig b/src/agent/Agent.zig index 2bf1fbb6..71ae1290 100644 --- a/src/agent/Agent.zig +++ b/src/agent/Agent.zig @@ -230,6 +230,12 @@ pub fn init(allocator: std.mem.Allocator, app: *App, opts: Config.Agent) !*Self self.terminal.attachCompleter(slash_schemas); + if (self.recorder.path) |p| { + self.terminal.printInfoFmt("recording to {s}", .{p}); + } else if (self.recorder.init_error) |reason| { + self.terminal.printErrorFmt("recording disabled: {s}", .{reason}); + } + return self; } @@ -559,7 +565,7 @@ fn runActionEntry(self: *Self, sa: std.mem.Allocator, entry: Command.ScriptItera const verification = if (!result.failed and self.self_heal) self.verifier.verify(ca, entry.command) else - Verifier.VerifyResult{ .result = .passed }; + Verifier.VerifyResult{ .result = .inconclusive }; if (!result.failed and verification.result != .failed) return .ok; diff --git a/src/agent/Recorder.zig b/src/agent/Recorder.zig index ff9653e5..ff4cb3ec 100644 --- a/src/agent/Recorder.zig +++ b/src/agent/Recorder.zig @@ -9,6 +9,9 @@ allocator: std.mem.Allocator, file: ?std.fs.File, /// Path of the active recording, owned by the Recorder. null when disabled. path: ?[]const u8, +/// Set when the user requested recording but init failed. Lets callers +/// surface the reason in the UI instead of burying it in logs. +init_error: ?[]const u8 = null, /// Number of lines successfully appended since init. Bumped only on success /// so callers see the actual file line count, not the attempt count. lines: u32, @@ -23,6 +26,7 @@ pub fn init(allocator: std.mem.Allocator, path: ?[]const u8) Self { .allocator = allocator, .file = opened.file, .path = opened.path, + .init_error = opened.err, .lines = 0, .buf = .init(allocator), }; @@ -31,29 +35,30 @@ pub fn init(allocator: std.mem.Allocator, path: ?[]const u8) Self { const OpenedRecording = struct { file: ?std.fs.File, path: ?[]const u8, + err: ?[]const u8, }; fn openRecording(allocator: std.mem.Allocator, path: ?[]const u8) OpenedRecording { - const p = path orelse return .{ .file = null, .path = null }; + const p = path orelse return .{ .file = null, .path = null, .err = null }; const owned_path = allocator.dupe(u8, p) catch { log.warn(.app, "recording path alloc failed", .{}); - return .{ .file = null, .path = null }; + return .{ .file = null, .path = null, .err = @errorName(error.OutOfMemory) }; }; const f = std.fs.cwd().createFile(p, .{ .truncate = false }) catch |err| { log.warn(.app, "could not open recording file", .{ .err = @errorName(err) }); allocator.free(owned_path); - return .{ .file = null, .path = null }; + return .{ .file = null, .path = null, .err = @errorName(err) }; }; f.seekFromEnd(0) catch |err| { log.warn(.app, "could not seek recording file", .{ .err = @errorName(err) }); f.close(); allocator.free(owned_path); - return .{ .file = null, .path = null }; + return .{ .file = null, .path = null, .err = @errorName(err) }; }; const pos = f.getPos() catch 0; if (pos > 0) f.writeAll("\n") catch {}; - return .{ .file = f, .path = owned_path }; + return .{ .file = f, .path = owned_path, .err = null }; } pub fn deinit(self: *Self) void { diff --git a/src/agent/Terminal.zig b/src/agent/Terminal.zig index f6e0963c..0412e745 100644 --- a/src/agent/Terminal.zig +++ b/src/agent/Terminal.zig @@ -216,6 +216,9 @@ fn analyzeBody(schema: *const SlashCommand.SchemaInfo, body: []const u8, ends_ws a.markUsed(tok[0..eq]); continue; } + // Single-required schemas accept the first arg positionally + // (`/goto https://example.com`); the schema's only required field + // is implicitly bound. if (i == 0 and schema.required.len == 1) { a.markUsed(schema.required[0]); continue; @@ -340,8 +343,10 @@ fn completionCallback(cenv: ?*c.ic_completion_env_t, prefix: [*c]const u8) callc self.addEnvVarCompletions(cenv, &buf, input); } -// File-scope buffer used by `hintsCallback`. Isocline copies the returned -// string into its own stringbuf, so we can safely overwrite this between calls. +// File-scope so the buffer outlives the callback's stack frame. Isocline's +// `sbuf_replace` copies the returned string into its own stringbuf, so it's +// safe to overwrite this on the next invocation. Single-threaded — isocline's +// edit loop runs on the main thread, and we have one Terminal instance. var hint_buf: [completion_buf_len:0]u8 = undefined; fn hintsCallback(input_c: [*c]const u8, arg: ?*anyopaque) callconv(.c) [*c]const u8 { diff --git a/src/agent/Verifier.zig b/src/agent/Verifier.zig index 58c1f0ec..bdf4cf30 100644 --- a/src/agent/Verifier.zig +++ b/src/agent/Verifier.zig @@ -23,12 +23,14 @@ pub const VerifyResult = struct { /// Verify that a command achieved its intent after execution and return /// both the verdict and a human-readable failure reason (if applicable). /// Only called when the command did not hard-fail (ExecResult.failed == false). +/// Commands without a dedicated verifier return `.inconclusive` so callers +/// can distinguish "no verification available" from "explicitly verified". pub fn verify(self: *Self, arena: std.mem.Allocator, cmd: Command.Command) VerifyResult { return switch (cmd) { .type_cmd => |args| self.verifyFill(arena, args.selector, args.value), .check => |args| self.verifyCheck(arena, args.selector, args.checked), .select => |args| self.verifySelect(arena, args.selector, args.value), - else => .{ .result = .passed }, + else => .{ .result = .inconclusive }, }; }