From f759f149377942c4e04802c45162cda1c9bfb2b3 Mon Sep 17 00:00:00 2001 From: t Date: Sat, 4 Jul 2026 09:50:12 -0600 Subject: big cli/tui gaps project --- src/config_file.zig | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) (limited to 'src/config_file.zig') diff --git a/src/config_file.zig b/src/config_file.zig index deca908..c606317 100644 --- a/src/config_file.zig +++ b/src/config_file.zig @@ -346,6 +346,15 @@ pub const Config = struct { /// `defaults.model` storage, owned. `default_model_ref` slices into it. default_model: ?[]const u8, default_model_ref: ?ModelRef, + /// `defaults.reasoning`: the session-default reasoning level LABEL (the + /// same names the Ctrl+R picker shows, e.g. "high"). Owned. Applied at + /// boot/model-switch unless the alias sets an explicit per-alias knob in + /// models.toml (`reasoning_explicit`); `--effort` beats both. + default_reasoning: ?[]const u8 = null, + /// `[tui] editor`: overrides `$VISUAL`/`$EDITOR` for Ctrl+G. Owned. + tui_editor: ?[]const u8 = null, + /// `[tui] tools_collapsed`: initial Ctrl+O collapse state (null = default). + tui_tools_collapsed: ?bool = null, /// Extension availability policy (the `[extensions]` section), resolved /// across all config layers. Governs every lua-authored capability. extensions: Policy, @@ -368,6 +377,8 @@ pub const Config = struct { self.allocator.free(self.providers); self.allocator.free(self.auths); if (self.default_model) |m| self.allocator.free(m); + if (self.default_reasoning) |r| self.allocator.free(r); + if (self.tui_editor) |e| self.allocator.free(e); if (self.compaction_model) |m| self.allocator.free(m); self.extensions.deinit(self.allocator); freeLayeredStrs(self.allocator, self.ext_paths); @@ -394,7 +405,8 @@ pub const Config = struct { } /// Choose the model reference to start with. Precedence: - /// 1. an explicit `model_override` (e.g. a future `--model` flag), + /// 1. an explicit `model_override` (`--model`; either "provider:alias" + /// or a bare alias that is unique across providers in `defs`), /// 2. `defaults.model` from config, /// 3. if exactly one provider resolved AND it has exactly one model /// alias in `defs`, use that, @@ -407,7 +419,18 @@ pub const Config = struct { model_override: ?[]const u8, ) ResolveError!ModelRef { if (model_override) |m| { - return parseModelRef(m) catch return error.NoModelSelected; + if (std.mem.indexOfScalar(u8, m, ':') != null) { + return parseModelRef(m) catch return error.NoModelSelected; + } + // Bare alias: accept when exactly one (provider, alias) matches. + var found: ?ModelRef = null; + for (defs.entries.items) |d| { + if (std.mem.eql(u8, d.alias, m)) { + if (found != null) return error.AmbiguousModelAlias; + found = .{ .provider = d.provider, .model = d.alias }; + } + } + return found orelse error.UnknownModelAlias; } if (self.default_model_ref) |ref| return ref; @@ -449,6 +472,10 @@ pub const Error = error{ pub const ResolveError = error{ NoModelSelected, UnknownProvider, + /// A bare `--model ` matched aliases under multiple providers. + AmbiguousModelAlias, + /// A bare `--model ` matched nothing in models.toml. + UnknownModelAlias, }; /// The filesystem errors that can surface while reading a config layer. @@ -715,6 +742,8 @@ fn resolve( var default_model: ?[]const u8 = null; errdefer if (default_model) |m| allocator.free(m); var default_model_ref: ?ModelRef = null; + var default_reasoning: ?[]const u8 = null; + errdefer if (default_reasoning) |r| allocator.free(r); if (root.get("defaults")) |defaults_tbl| { if (defaults_tbl.get("model")) |model_v| { if (model_v.* == .string) { @@ -722,6 +751,26 @@ fn resolve( default_model_ref = try parseModelRef(default_model.?); } } + if (defaults_tbl.get("reasoning")) |r_v| { + if (r_v.* == .string) { + default_reasoning = try allocator.dupe(u8, r_v.string); + } + } + } + + // `[tui]` settings. + var tui_editor: ?[]const u8 = null; + errdefer if (tui_editor) |e| allocator.free(e); + var tui_tools_collapsed: ?bool = null; + if (root.get("tui")) |tui_tbl| { + if (tui_tbl.get("editor")) |e_v| { + if (e_v.* == .string) { + tui_editor = try allocator.dupe(u8, e_v.string); + } + } + if (tui_tbl.get("tools_collapsed")) |tc| { + if (tc.asBool()) |b| tui_tools_collapsed = b; + } } // `[compaction]` settings. @@ -782,6 +831,9 @@ fn resolve( .auth_arena = auth_arena, .default_model = default_model, .default_model_ref = default_model_ref, + .default_reasoning = default_reasoning, + .tui_editor = tui_editor, + .tui_tools_collapsed = tui_tools_collapsed, .extensions = extensions, .ext_paths = ext_paths, .ext_rocks = ext_rocks, @@ -1929,6 +1981,43 @@ test "selectModel: no default and no providers errors" { try testing.expectError(error.NoModelSelected, cfg.selectModel(&models, null)); } +test "selectModel: bare alias override resolves when unique, errors otherwise" { + const a = testing.allocator; + var env = emptyEnv(a); + defer env.deinit(); + const empty = try parseDoc(a, ""); + defer empty.deinit(); + var cfg = try resolveDoc(a, &env, empty.root); + defer cfg.deinit(); + + var models = models_toml.ModelRegistry.init(a); + defer models.deinit(); + for ([_][2][]const u8{ + .{ "openai", "gpt" }, + .{ "anthropic", "sonnet" }, + .{ "openrouter", "sonnet" }, + }) |pair| { + try models.entries.append(a, .{ + .provider = try a.dupe(u8, pair[0]), + .alias = try a.dupe(u8, pair[1]), + .model = try a.dupe(u8, pair[1]), + .reasoning = .default, + .max_tokens = null, + .api_version = null, + .thinking = .disabled, + .effort = .medium, + .thinking_budget_tokens = null, + .thinking_interleaved = false, + }); + } + + const ref = try cfg.selectModel(&models, "gpt"); + try testing.expectEqualStrings("openai", ref.provider); + try testing.expectEqualStrings("gpt", ref.model); + try testing.expectError(error.AmbiguousModelAlias, cfg.selectModel(&models, "sonnet")); + try testing.expectError(error.UnknownModelAlias, cfg.selectModel(&models, "nope")); +} + test "loadFromPaths: missing files are skipped" { const a = testing.allocator; const io = testing.io; @@ -1967,6 +2056,30 @@ test "resolve: [compaction] keep_verbatim and model parse" { try testing.expectEqualStrings("haiku", cfg.compaction_model_ref.?.model); } +test "resolve: [defaults] reasoning and [tui] keys parse" { + const a = testing.allocator; + var env = emptyEnv(a); + defer env.deinit(); + try env.put("ANTHROPIC_API_KEY", "sk-ant-xyz"); + + const src = anthropic_env_src ++ + \\ + \\[defaults] + \\reasoning = "high" + \\ + \\[tui] + \\editor = "code -w" + \\tools_collapsed = false + ; + const doc = try parseDoc(a, src); + defer doc.deinit(); + var cfg = try resolveDoc(a, &env, doc.root); + defer cfg.deinit(); + try testing.expectEqualStrings("high", cfg.default_reasoning.?); + try testing.expectEqualStrings("code -w", cfg.tui_editor.?); + try testing.expectEqual(false, cfg.tui_tools_collapsed.?); +} + test "resolve: [compaction] absent leaves defaults null" { const a = testing.allocator; var env = emptyEnv(a); -- cgit v1.3