summaryrefslogtreecommitdiff
path: root/src/config_file.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/config_file.zig')
-rw-r--r--src/config_file.zig117
1 files changed, 115 insertions, 2 deletions
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 <alias>` matched aliases under multiple providers.
+ AmbiguousModelAlias,
+ /// A bare `--model <alias>` 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);