agent: report recording status and refine verification

This commit is contained in:
Adrià Arrufat
2026-05-11 18:56:34 +02:00
parent 5ef6afbd06
commit c1b4c0e52e
4 changed files with 27 additions and 9 deletions

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 },
};
}