summaryrefslogtreecommitdiff
path: root/src/tui_app.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/tui_app.zig')
-rw-r--r--src/tui_app.zig50
1 files changed, 44 insertions, 6 deletions
diff --git a/src/tui_app.zig b/src/tui_app.zig
index 69eb8fa..f9c58fe 100644
--- a/src/tui_app.zig
+++ b/src/tui_app.zig
@@ -1008,6 +1008,17 @@ pub const App = struct {
/// `ModelRegistry`, and `panto.Agent` (all owned by `main`). The model labels
/// and provider/model wire strings the rebuilt config borrows therefore stay
/// valid for the whole session.
+const ModelPickerRow = struct {
+ def_index: usize,
+ item: SelectorItem,
+};
+
+fn modelPickerRowLessThan(_: void, a: ModelPickerRow, b: ModelPickerRow) bool {
+ const by_label = std.mem.order(u8, a.item.label, b.item.label);
+ if (by_label != .eq) return by_label == .lt;
+ return std.mem.lessThan(u8, a.item.detail, b.item.detail);
+}
+
pub const SelectorController = struct {
alloc: std.mem.Allocator,
app: *App,
@@ -1024,6 +1035,8 @@ pub const SelectorController = struct {
/// Built selector item list for the model picker (label = "provider:alias",
/// detail = "<wire> <knobs>"). Owned: labels/details are allocated here.
model_items: []SelectorItem,
+ /// Parallel to `model_items`: source registry index for each SORTED row.
+ model_entry_indices: []usize = &.{},
/// Backing storage for the model item label/detail strings.
model_strings: std.ArrayList([]u8) = .empty,
/// Reasoning picker items for the CURRENTLY OPEN reasoning picker, rebuilt
@@ -1089,6 +1102,7 @@ pub const SelectorController = struct {
for (self.model_strings.items) |s| self.alloc.free(s);
self.model_strings.deinit(self.alloc);
self.alloc.free(self.model_items);
+ self.alloc.free(self.model_entry_indices);
self.alloc.free(self.reasoning_items);
self.alloc.free(self.model_label);
self.alloc.destroy(self);
@@ -1097,16 +1111,22 @@ pub const SelectorController = struct {
/// Build the model picker items from every `models.toml` definition.
fn buildModelItems(self: *SelectorController) !void {
const a = self.alloc;
- var items: std.ArrayList(SelectorItem) = .empty;
- defer items.deinit(a);
- for (self.defs.entries.items) |d| {
+ var rows: std.ArrayList(ModelPickerRow) = .empty;
+ defer rows.deinit(a);
+ for (self.defs.entries.items, 0..) |d, def_index| {
const label = try std.fmt.allocPrint(a, "{s}:{s}", .{ d.provider, d.alias });
try self.model_strings.append(a, label);
const detail = try formatModelDetail(a, d);
try self.model_strings.append(a, detail);
- try items.append(a, .{ .label = label, .detail = detail });
+ try rows.append(a, .{ .def_index = def_index, .item = .{ .label = label, .detail = detail } });
+ }
+ std.mem.sort(ModelPickerRow, rows.items, {}, modelPickerRowLessThan);
+ self.model_items = try a.alloc(SelectorItem, rows.items.len);
+ self.model_entry_indices = try a.alloc(usize, rows.items.len);
+ for (rows.items, 0..) |row, i| {
+ self.model_items[i] = row.item;
+ self.model_entry_indices[i] = row.def_index;
}
- self.model_items = try items.toOwnedSlice(a);
}
/// Open the model picker, preselecting the current model.
@@ -1191,7 +1211,8 @@ pub const SelectorController = struct {
}
fn applyModel(self: *SelectorController, index: usize) !void {
- const d = self.defs.entries.items[index];
+ if (index >= self.model_entry_indices.len) return;
+ const d = self.defs.entries.items[self.model_entry_indices[index]];
const ref: config_file.ModelRef = .{ .provider = d.provider, .model = d.alias };
const new_provider = config_file.buildProviderConfig(self.file_cfg, self.defs, ref) catch |err| {
try self.statusf("[model switch failed: {s}]", .{@errorName(err)});
@@ -2314,6 +2335,23 @@ test "formatModelDetail: shows wire model + non-default knobs" {
}
}
+test "model picker rows sort by provider:alias and keep mapping" {
+ var rows = [_]ModelPickerRow{
+ .{ .def_index = 2, .item = .{ .label = "openai:gpt-4o", .detail = "gpt-4o" } },
+ .{ .def_index = 0, .item = .{ .label = "anthropic:sonnet", .detail = "claude-sonnet-4" } },
+ .{ .def_index = 1, .item = .{ .label = "anthropic:haiku", .detail = "claude-haiku-4" } },
+ };
+
+ std.mem.sort(ModelPickerRow, rows[0..], {}, modelPickerRowLessThan);
+
+ try testing.expectEqualStrings("anthropic:haiku", rows[0].item.label);
+ try testing.expectEqual(@as(usize, 1), rows[0].def_index);
+ try testing.expectEqualStrings("anthropic:sonnet", rows[1].item.label);
+ try testing.expectEqual(@as(usize, 0), rows[1].def_index);
+ try testing.expectEqualStrings("openai:gpt-4o", rows[2].item.label);
+ try testing.expectEqual(@as(usize, 2), rows[2].def_index);
+}
+
test "toggleToolCollapse flips every tool component globally" {
const alloc = testing.allocator;
const h = try Harness.make(alloc);