agent: clean up save mode and terminal prompts

- Use `std.meta.fieldNames` for the save mode prompt.
- Update terminal prompt functions to accept null-terminated strings.
- Respect the resolved save mode in `synthesizeSave`.
This commit is contained in:
Adrià Arrufat
2026-06-03 17:13:52 +02:00
parent efb0041c83
commit 560897ea89
3 changed files with 12 additions and 19 deletions

View File

@@ -677,7 +677,7 @@ fn setProvider(self: *Agent, credentials: Credentials) !void {
_ = completionModels(self, self.allocator);
}
const SaveMode = enum { replace, append };
const SaveMode = enum { append, replace };
const PathAndMode = struct { path: []const u8, mode: SaveMode };
@@ -818,12 +818,11 @@ fn promptSaveMode(self: *Agent, path: []const u8) ?SaveMode {
var header_buf: [256]u8 = undefined;
const header = std.fmt.bufPrint(&header_buf, "{s} already exists. Pick save mode:", .{path}) catch
"File already exists. Pick save mode:";
const labels: []const []const u8 = &.{ "replace", "append" };
const idx = Terminal.promptNumberedChoice(header, labels, null) catch {
const idx = Terminal.promptNumberedChoice(header, std.meta.fieldNames(SaveMode), 0) catch {
self.terminal.printInfo("Save cancelled.", .{});
return null;
};
return if (idx == 0) .replace else .append;
return @enumFromInt(idx);
}
fn writeSaveFile(self: *Agent, path: []const u8, mode: SaveMode) !void {
@@ -848,18 +847,12 @@ fn failSave(self: *Agent, reason: []const u8) void {
/// LLM-synthesized `/save`: hand the model the builtin catalog, the full
/// conversation, and the deterministic record of what ran, then write the
/// idiomatic script it returns. Always replaces the target file.
/// idiomatic script it returns.
fn synthesizeSave(self: *Agent, arena: std.mem.Allocator, filename: ?[]const u8, prompt: ?[]const u8) void {
const provider_client = self.ai_client.?;
const path: []const u8 = blk: {
if (filename) |f| break :blk f;
if (self.save_path) |p| break :blk p;
break :blk randomSaveFilename(arena) catch |err| {
self.terminal.printError("failed to choose save filename: {s}", .{@errorName(err)});
return;
};
};
const resolved = self.resolveSavePathAndMode(arena, filename) orelse return;
const path = resolved.path;
self.ensureSystemPrompt() catch return self.failSave("out of memory");
@@ -922,7 +915,7 @@ fn synthesizeSave(self: *Agent, arena: std.mem.Allocator, filename: ?[]const u8,
// The save turn is a meta-action; keep it out of the ongoing conversation.
self.rollbackMessages(baseline);
writeContentFile(path, script, .replace) catch |err| {
writeContentFile(path, script, resolved.mode) catch |err| {
self.terminal.printError("failed to save {s}: {s}", .{ path, @errorName(err) });
return;
};

View File

@@ -905,7 +905,7 @@ pub fn interactiveTty() bool {
/// Numbered TTY picker. `default` (if set) marks that row "(default)" and
/// makes Enter start on that index. Up/Down moves the active row; Enter
/// selects it. Numbered input still works for users who prefer typing.
pub fn promptNumberedChoice(header: []const u8, items: []const []const u8, default: ?usize) !usize {
pub fn promptNumberedChoice(header: []const u8, items: []const [:0]const u8, default: ?usize) !usize {
if (items.len == 0) return error.NoChoice;
const valid_default: ?usize = if (default) |d| if (d < items.len) d else null else null;
if (interactiveTty()) {
@@ -918,7 +918,7 @@ pub fn promptNumberedChoice(header: []const u8, items: []const []const u8, defau
}
/// Line-oriented fallback. Errors with NoChoice after 3 invalid attempts.
fn promptNumberedChoiceLine(header: []const u8, items: []const []const u8, default: ?usize) !usize {
fn promptNumberedChoiceLine(header: []const u8, items: []const [:0]const u8, default: ?usize) !usize {
var stdin_buf: [128]u8 = undefined;
var stdin = std.fs.File.stdin().reader(&stdin_buf);
@@ -1005,7 +1005,7 @@ const RawTerminal = struct {
}
};
fn promptInteractiveChoice(header: []const u8, items: []const []const u8, default: ?usize) !usize {
fn promptInteractiveChoice(header: []const u8, items: []const [:0]const u8, default: ?usize) !usize {
var raw = try RawTerminal.enable();
defer raw.restore();
@@ -1046,7 +1046,7 @@ fn moveChoiceRenderStart(line_count: usize) void {
}
}
fn renderChoice(header: []const u8, items: []const []const u8, default: ?usize, selected: usize, first_render: bool) void {
fn renderChoice(header: []const u8, items: []const [:0]const u8, default: ?usize, selected: usize, first_render: bool) void {
if (!first_render) moveChoiceRenderStart(items.len + 2);
std.debug.print(ansi.clear_line ++ "{s}\r\n", .{header});
for (items, 0..) |item, idx| {

View File

@@ -82,7 +82,7 @@ pub fn resolveCredentials(opts: Config.Agent, remembered: ?Remembered, allow_pic
return .{ .credentials = found[0], .source = .detected };
}
var names: [zenai.provider.default_candidates.len][]const u8 = undefined;
var names: [zenai.provider.default_candidates.len][:0]const u8 = undefined;
for (found, 0..) |cred, i| names[i] = @tagName(cred.provider);
std.debug.print("\n", .{});
const idx = Terminal.promptNumberedChoice(" Select a provider:", names[0..found.len], 0) catch {