open-multi-agent-kit 0.80.4 → 0.80.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config.d.ts +4 -9
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +46 -15
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  7. package/dist/core/compaction/branch-summarization.js +6 -1
  8. package/dist/core/compaction/branch-summarization.js.map +1 -1
  9. package/dist/core/compaction/compaction.d.ts +82 -1
  10. package/dist/core/compaction/compaction.d.ts.map +1 -1
  11. package/dist/core/compaction/compaction.js +606 -10
  12. package/dist/core/compaction/compaction.js.map +1 -1
  13. package/dist/core/harness-control-events.d.ts +79 -0
  14. package/dist/core/harness-control-events.d.ts.map +1 -0
  15. package/dist/core/harness-control-events.js +339 -0
  16. package/dist/core/harness-control-events.js.map +1 -0
  17. package/dist/core/harness-control-replay.d.ts +26 -0
  18. package/dist/core/harness-control-replay.d.ts.map +1 -0
  19. package/dist/core/harness-control-replay.js +155 -0
  20. package/dist/core/harness-control-replay.js.map +1 -0
  21. package/dist/core/keybindings.d.ts +12 -0
  22. package/dist/core/keybindings.d.ts.map +1 -1
  23. package/dist/core/keybindings.js +31 -1
  24. package/dist/core/keybindings.js.map +1 -1
  25. package/dist/core/mcp-inventory.d.ts +40 -0
  26. package/dist/core/mcp-inventory.d.ts.map +1 -0
  27. package/dist/core/mcp-inventory.js +104 -0
  28. package/dist/core/mcp-inventory.js.map +1 -0
  29. package/dist/core/settings-manager.d.ts +3 -0
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js +4 -0
  32. package/dist/core/settings-manager.js.map +1 -1
  33. package/dist/core/slash-commands.d.ts.map +1 -1
  34. package/dist/core/slash-commands.js +4 -1
  35. package/dist/core/slash-commands.js.map +1 -1
  36. package/dist/core/spec-kit/compiler.d.ts +67 -0
  37. package/dist/core/spec-kit/compiler.d.ts.map +1 -0
  38. package/dist/core/spec-kit/compiler.js +268 -0
  39. package/dist/core/spec-kit/compiler.js.map +1 -0
  40. package/dist/core/transaction-coordinator.d.ts +30 -0
  41. package/dist/core/transaction-coordinator.d.ts.map +1 -0
  42. package/dist/core/transaction-coordinator.js +173 -0
  43. package/dist/core/transaction-coordinator.js.map +1 -0
  44. package/dist/migrations.d.ts +35 -0
  45. package/dist/migrations.d.ts.map +1 -1
  46. package/dist/migrations.js +409 -3
  47. package/dist/migrations.js.map +1 -1
  48. package/dist/modes/interactive/components/control-panel.d.ts +29 -0
  49. package/dist/modes/interactive/components/control-panel.d.ts.map +1 -0
  50. package/dist/modes/interactive/components/control-panel.js +90 -0
  51. package/dist/modes/interactive/components/control-panel.js.map +1 -0
  52. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/custom-editor.js +21 -13
  54. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  55. package/dist/modes/interactive/components/index.d.ts +3 -0
  56. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/index.js +3 -0
  58. package/dist/modes/interactive/components/index.js.map +1 -1
  59. package/dist/modes/interactive/components/mcp-selector.d.ts +28 -0
  60. package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -0
  61. package/dist/modes/interactive/components/mcp-selector.js +138 -0
  62. package/dist/modes/interactive/components/mcp-selector.js.map +1 -0
  63. package/dist/modes/interactive/components/model-selector.d.ts +12 -3
  64. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/model-selector.js +120 -27
  66. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
  68. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +21 -8
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/skills-selector.d.ts +32 -0
  72. package/dist/modes/interactive/components/skills-selector.d.ts.map +1 -0
  73. package/dist/modes/interactive/components/skills-selector.js +130 -0
  74. package/dist/modes/interactive/components/skills-selector.js.map +1 -0
  75. package/dist/modes/interactive/components/thinking-selector.d.ts +7 -8
  76. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/thinking-selector.js +7 -48
  78. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +340 -97
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/interactive/theme/catppuccin-mocha.json +86 -0
  84. package/dist/modes/interactive/theme/dracula.json +86 -0
  85. package/dist/modes/interactive/theme/gruvbox-dark.json +86 -0
  86. package/dist/modes/interactive/theme/kanagawa-dragon.json +86 -0
  87. package/dist/modes/interactive/theme/omk-control-panel.json +91 -0
  88. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  89. package/dist/modes/interactive/theme/theme.js +41 -16
  90. package/dist/modes/interactive/theme/theme.js.map +1 -1
  91. package/dist/modes/interactive/theme/tokyo-night-storm.json +86 -0
  92. package/docs/compaction.md +21 -8
  93. package/docs/quickstart.md +1 -1
  94. package/docs/themes.md +10 -2
  95. package/docs/usage.md +2 -1
  96. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  97. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  98. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  99. package/examples/extensions/gondolin/package-lock.json +2 -2
  100. package/examples/extensions/gondolin/package.json +1 -1
  101. package/examples/extensions/sandbox/package-lock.json +2 -2
  102. package/examples/extensions/sandbox/package.json +1 -1
  103. package/examples/extensions/with-deps/package-lock.json +2 -2
  104. package/examples/extensions/with-deps/package.json +1 -1
  105. package/package.json +4 -4
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP inventory selector.
3
+ *
4
+ * Renders the merged inventory produced by `loadMcpInventory`. Read-only: env
5
+ * values are never displayed, only key names. Adds fuzzy search input + counter.
6
+ */
7
+ import { Container, type Focusable } from "@earendil-works/omk-tui";
8
+ import type { McpInventory } from "../../../core/mcp-inventory.ts";
9
+ export declare class McpSelectorComponent extends Container implements Focusable {
10
+ private allEntries;
11
+ private filteredEntries;
12
+ private selectedIndex;
13
+ private listContainer;
14
+ private detailContainer;
15
+ private counterText;
16
+ private searchInput;
17
+ private onCancelCallback;
18
+ private _focused;
19
+ get focused(): boolean;
20
+ set focused(value: boolean);
21
+ constructor(inventory: McpInventory, onCancel: () => void);
22
+ private getCounterText;
23
+ private applyFilter;
24
+ private updateList;
25
+ private updateDetail;
26
+ handleInput(keyData: string): void;
27
+ }
28
+ //# sourceMappingURL=mcp-selector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/mcp-selector.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAoD,MAAM,yBAAyB,CAAC;AACtH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,gCAAgC,CAAC;AAKnF,qBAAa,oBAAqB,SAAQ,SAAU,YAAW,SAAS;IACvE,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,eAAe,CAAmB;IAC1C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,eAAe,CAAY;IACnC,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,gBAAgB,CAAa;IAErC,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,YAAY,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,IAAI,EAqDxD;IAED,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,UAAU;IAoBlB,OAAO,CAAC,YAAY;IAoBpB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAsBjC;CACD","sourcesContent":["/**\n * MCP inventory selector.\n *\n * Renders the merged inventory produced by `loadMcpInventory`. Read-only: env\n * values are never displayed, only key names. Adds fuzzy search input + counter.\n */\n\nimport { Container, type Focusable, fuzzyFilter, getKeybindings, Input, Spacer, Text } from \"@earendil-works/omk-tui\";\nimport type { McpInventory, McpServerEntry } from \"../../../core/mcp-inventory.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.ts\";\n\nexport class McpSelectorComponent extends Container implements Focusable {\n\tprivate allEntries: McpServerEntry[];\n\tprivate filteredEntries: McpServerEntry[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate detailContainer: Container;\n\tprivate counterText: Text;\n\tprivate searchInput: Input;\n\tprivate onCancelCallback: () => void;\n\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(inventory: McpInventory, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.allEntries = inventory.entries;\n\t\tthis.filteredEntries = inventory.entries;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(\"MCP servers (read-only inventory)\")), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tfor (const src of inventory.sources) {\n\t\t\tconst status = src.exists ? theme.fg(\"muted\", `${src.serverCount} server(s)`) : theme.fg(\"dim\", \"missing\");\n\t\t\tthis.addChild(new Text(` ${theme.fg(\"muted\", src.path)} · ${status}`, 1, 0));\n\t\t}\n\t\tfor (const err of inventory.errors) {\n\t\t\tthis.addChild(new Text(theme.fg(\"error\", ` parse error: ${err.path}: ${err.message}`), 1, 0));\n\t\t}\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.counterText = new Text(this.getCounterText(), 1, 0);\n\t\tthis.addChild(this.counterText);\n\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => this.updateDetail();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.detailContainer = new Container();\n\t\tthis.addChild(this.detailContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(\n\t\t\tnew Text(\n\t\t\t\trawKeyHint(\"↑↓\", \"navigate\") +\n\t\t\t\t\t\" \" +\n\t\t\t\t\tkeyHint(\"tui.select.cancel\", \"close\") +\n\t\t\t\t\ttheme.fg(\"muted\", \" (read-only · env values hidden)\"),\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.updateList();\n\t\tthis.updateDetail();\n\t}\n\n\tprivate getCounterText(): string {\n\t\tconst total = this.allEntries.length;\n\t\tconst matched = this.filteredEntries.length;\n\t\treturn theme.fg(\"muted\", ` matched ${matched} / total ${total}`);\n\t}\n\n\tprivate applyFilter(query: string): void {\n\t\tthis.filteredEntries = query\n\t\t\t? fuzzyFilter(this.allEntries, query, (e) => `${e.name} ${e.commandSummary} ${e.source}`)\n\t\t\t: this.allEntries;\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));\n\t\tthis.counterText.setText(this.getCounterText());\n\t\tthis.updateList();\n\t\tthis.updateDetail();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\t\tif (this.filteredEntries.length === 0) {\n\t\t\tconst msg = this.allEntries.length === 0 ? \" No MCP servers configured.\" : \" No entries match the search.\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", msg), 1, 0));\n\t\t\treturn;\n\t\t}\n\t\tfor (let i = 0; i < this.filteredEntries.length; i++) {\n\t\t\tconst entry = this.filteredEntries[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst name = isSelected ? theme.fg(\"accent\", `→ ${entry.name}`) : ` ${entry.name}`;\n\t\t\tconst meta = theme.fg(\n\t\t\t\t\"muted\",\n\t\t\t\t` · ${entry.commandSummary} · ${entry.envKeys.length} env · ${entry.argsCount} args`,\n\t\t\t);\n\t\t\tconst overridden = entry.overriddenBy ? theme.fg(\"warning\", \" (overrides earlier source)\") : \"\";\n\t\t\tthis.listContainer.addChild(new Text(`${name}${meta}${overridden}`, 1, 0));\n\t\t}\n\t}\n\n\tprivate updateDetail(): void {\n\t\tthis.detailContainer.clear();\n\t\tconst entry = this.filteredEntries[this.selectedIndex];\n\t\tif (!entry) return;\n\n\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` source: ${entry.source}`), 1, 0));\n\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` command: ${entry.commandSummary}`), 1, 0));\n\t\tconst envSummary =\n\t\t\tentry.envKeys.length === 0 ? theme.fg(\"dim\", \"(none)\") : theme.fg(\"muted\", entry.envKeys.join(\", \"));\n\t\tthis.detailContainer.addChild(new Text(` env keys: ${envSummary}`, 1, 0));\n\t\tif (typeof entry.startupTimeoutSec === \"number\") {\n\t\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` timeout: ${entry.startupTimeoutSec}s`), 1, 0));\n\t\t}\n\t\tif (entry.autoApproveCount > 0) {\n\t\t\tthis.detailContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"muted\", ` autoApprove entries: ${entry.autoApproveCount}`), 1, 0),\n\t\t\t);\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredEntries.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredEntries.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t\tthis.updateDetail();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredEntries.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredEntries.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t\tthis.updateDetail();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\t\tthis.searchInput.handleInput(keyData);\n\t\tthis.applyFilter(this.searchInput.getValue());\n\t}\n}\n"]}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * MCP inventory selector.
3
+ *
4
+ * Renders the merged inventory produced by `loadMcpInventory`. Read-only: env
5
+ * values are never displayed, only key names. Adds fuzzy search input + counter.
6
+ */
7
+ import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text } from "@earendil-works/omk-tui";
8
+ import { theme } from "../theme/theme.js";
9
+ import { DynamicBorder } from "./dynamic-border.js";
10
+ import { keyHint, rawKeyHint } from "./keybinding-hints.js";
11
+ export class McpSelectorComponent extends Container {
12
+ allEntries;
13
+ filteredEntries;
14
+ selectedIndex = 0;
15
+ listContainer;
16
+ detailContainer;
17
+ counterText;
18
+ searchInput;
19
+ onCancelCallback;
20
+ _focused = false;
21
+ get focused() {
22
+ return this._focused;
23
+ }
24
+ set focused(value) {
25
+ this._focused = value;
26
+ this.searchInput.focused = value;
27
+ }
28
+ constructor(inventory, onCancel) {
29
+ super();
30
+ this.allEntries = inventory.entries;
31
+ this.filteredEntries = inventory.entries;
32
+ this.onCancelCallback = onCancel;
33
+ this.addChild(new DynamicBorder());
34
+ this.addChild(new Spacer(1));
35
+ this.addChild(new Text(theme.fg("accent", theme.bold("MCP servers (read-only inventory)")), 1, 0));
36
+ this.addChild(new Spacer(1));
37
+ for (const src of inventory.sources) {
38
+ const status = src.exists ? theme.fg("muted", `${src.serverCount} server(s)`) : theme.fg("dim", "missing");
39
+ this.addChild(new Text(` ${theme.fg("muted", src.path)} · ${status}`, 1, 0));
40
+ }
41
+ for (const err of inventory.errors) {
42
+ this.addChild(new Text(theme.fg("error", ` parse error: ${err.path}: ${err.message}`), 1, 0));
43
+ }
44
+ this.addChild(new Spacer(1));
45
+ this.counterText = new Text(this.getCounterText(), 1, 0);
46
+ this.addChild(this.counterText);
47
+ this.searchInput = new Input();
48
+ this.searchInput.onSubmit = () => this.updateDetail();
49
+ this.addChild(this.searchInput);
50
+ this.addChild(new Spacer(1));
51
+ this.listContainer = new Container();
52
+ this.addChild(this.listContainer);
53
+ this.addChild(new Spacer(1));
54
+ this.detailContainer = new Container();
55
+ this.addChild(this.detailContainer);
56
+ this.addChild(new Spacer(1));
57
+ this.addChild(new Text(rawKeyHint("↑↓", "navigate") +
58
+ " " +
59
+ keyHint("tui.select.cancel", "close") +
60
+ theme.fg("muted", " (read-only · env values hidden)"), 1, 0));
61
+ this.addChild(new Spacer(1));
62
+ this.addChild(new DynamicBorder());
63
+ this.updateList();
64
+ this.updateDetail();
65
+ }
66
+ getCounterText() {
67
+ const total = this.allEntries.length;
68
+ const matched = this.filteredEntries.length;
69
+ return theme.fg("muted", ` matched ${matched} / total ${total}`);
70
+ }
71
+ applyFilter(query) {
72
+ this.filteredEntries = query
73
+ ? fuzzyFilter(this.allEntries, query, (e) => `${e.name} ${e.commandSummary} ${e.source}`)
74
+ : this.allEntries;
75
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));
76
+ this.counterText.setText(this.getCounterText());
77
+ this.updateList();
78
+ this.updateDetail();
79
+ }
80
+ updateList() {
81
+ this.listContainer.clear();
82
+ if (this.filteredEntries.length === 0) {
83
+ const msg = this.allEntries.length === 0 ? " No MCP servers configured." : " No entries match the search.";
84
+ this.listContainer.addChild(new Text(theme.fg("muted", msg), 1, 0));
85
+ return;
86
+ }
87
+ for (let i = 0; i < this.filteredEntries.length; i++) {
88
+ const entry = this.filteredEntries[i];
89
+ const isSelected = i === this.selectedIndex;
90
+ const name = isSelected ? theme.fg("accent", `→ ${entry.name}`) : ` ${entry.name}`;
91
+ const meta = theme.fg("muted", ` · ${entry.commandSummary} · ${entry.envKeys.length} env · ${entry.argsCount} args`);
92
+ const overridden = entry.overriddenBy ? theme.fg("warning", " (overrides earlier source)") : "";
93
+ this.listContainer.addChild(new Text(`${name}${meta}${overridden}`, 1, 0));
94
+ }
95
+ }
96
+ updateDetail() {
97
+ this.detailContainer.clear();
98
+ const entry = this.filteredEntries[this.selectedIndex];
99
+ if (!entry)
100
+ return;
101
+ this.detailContainer.addChild(new Text(theme.fg("muted", ` source: ${entry.source}`), 1, 0));
102
+ this.detailContainer.addChild(new Text(theme.fg("muted", ` command: ${entry.commandSummary}`), 1, 0));
103
+ const envSummary = entry.envKeys.length === 0 ? theme.fg("dim", "(none)") : theme.fg("muted", entry.envKeys.join(", "));
104
+ this.detailContainer.addChild(new Text(` env keys: ${envSummary}`, 1, 0));
105
+ if (typeof entry.startupTimeoutSec === "number") {
106
+ this.detailContainer.addChild(new Text(theme.fg("muted", ` timeout: ${entry.startupTimeoutSec}s`), 1, 0));
107
+ }
108
+ if (entry.autoApproveCount > 0) {
109
+ this.detailContainer.addChild(new Text(theme.fg("muted", ` autoApprove entries: ${entry.autoApproveCount}`), 1, 0));
110
+ }
111
+ }
112
+ handleInput(keyData) {
113
+ const kb = getKeybindings();
114
+ if (kb.matches(keyData, "tui.select.up")) {
115
+ if (this.filteredEntries.length === 0)
116
+ return;
117
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredEntries.length - 1 : this.selectedIndex - 1;
118
+ this.updateList();
119
+ this.updateDetail();
120
+ return;
121
+ }
122
+ if (kb.matches(keyData, "tui.select.down")) {
123
+ if (this.filteredEntries.length === 0)
124
+ return;
125
+ this.selectedIndex = this.selectedIndex === this.filteredEntries.length - 1 ? 0 : this.selectedIndex + 1;
126
+ this.updateList();
127
+ this.updateDetail();
128
+ return;
129
+ }
130
+ if (kb.matches(keyData, "tui.select.cancel")) {
131
+ this.onCancelCallback();
132
+ return;
133
+ }
134
+ this.searchInput.handleInput(keyData);
135
+ this.applyFilter(this.searchInput.getValue());
136
+ }
137
+ }
138
+ //# sourceMappingURL=mcp-selector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-selector.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/mcp-selector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MCP inventory selector.\n *\n * Renders the merged inventory produced by `loadMcpInventory`. Read-only: env\n * values are never displayed, only key names. Adds fuzzy search input + counter.\n */\n\nimport { Container, type Focusable, fuzzyFilter, getKeybindings, Input, Spacer, Text } from \"@earendil-works/omk-tui\";\nimport type { McpInventory, McpServerEntry } from \"../../../core/mcp-inventory.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.ts\";\n\nexport class McpSelectorComponent extends Container implements Focusable {\n\tprivate allEntries: McpServerEntry[];\n\tprivate filteredEntries: McpServerEntry[];\n\tprivate selectedIndex = 0;\n\tprivate listContainer: Container;\n\tprivate detailContainer: Container;\n\tprivate counterText: Text;\n\tprivate searchInput: Input;\n\tprivate onCancelCallback: () => void;\n\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\n\tconstructor(inventory: McpInventory, onCancel: () => void) {\n\t\tsuper();\n\n\t\tthis.allEntries = inventory.entries;\n\t\tthis.filteredEntries = inventory.entries;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(\"MCP servers (read-only inventory)\")), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tfor (const src of inventory.sources) {\n\t\t\tconst status = src.exists ? theme.fg(\"muted\", `${src.serverCount} server(s)`) : theme.fg(\"dim\", \"missing\");\n\t\t\tthis.addChild(new Text(` ${theme.fg(\"muted\", src.path)} · ${status}`, 1, 0));\n\t\t}\n\t\tfor (const err of inventory.errors) {\n\t\t\tthis.addChild(new Text(theme.fg(\"error\", ` parse error: ${err.path}: ${err.message}`), 1, 0));\n\t\t}\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.counterText = new Text(this.getCounterText(), 1, 0);\n\t\tthis.addChild(this.counterText);\n\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.onSubmit = () => this.updateDetail();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.detailContainer = new Container();\n\t\tthis.addChild(this.detailContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(\n\t\t\tnew Text(\n\t\t\t\trawKeyHint(\"↑↓\", \"navigate\") +\n\t\t\t\t\t\" \" +\n\t\t\t\t\tkeyHint(\"tui.select.cancel\", \"close\") +\n\t\t\t\t\ttheme.fg(\"muted\", \" (read-only · env values hidden)\"),\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.updateList();\n\t\tthis.updateDetail();\n\t}\n\n\tprivate getCounterText(): string {\n\t\tconst total = this.allEntries.length;\n\t\tconst matched = this.filteredEntries.length;\n\t\treturn theme.fg(\"muted\", ` matched ${matched} / total ${total}`);\n\t}\n\n\tprivate applyFilter(query: string): void {\n\t\tthis.filteredEntries = query\n\t\t\t? fuzzyFilter(this.allEntries, query, (e) => `${e.name} ${e.commandSummary} ${e.source}`)\n\t\t\t: this.allEntries;\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredEntries.length - 1));\n\t\tthis.counterText.setText(this.getCounterText());\n\t\tthis.updateList();\n\t\tthis.updateDetail();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\t\tif (this.filteredEntries.length === 0) {\n\t\t\tconst msg = this.allEntries.length === 0 ? \" No MCP servers configured.\" : \" No entries match the search.\";\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", msg), 1, 0));\n\t\t\treturn;\n\t\t}\n\t\tfor (let i = 0; i < this.filteredEntries.length; i++) {\n\t\t\tconst entry = this.filteredEntries[i];\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst name = isSelected ? theme.fg(\"accent\", `→ ${entry.name}`) : ` ${entry.name}`;\n\t\t\tconst meta = theme.fg(\n\t\t\t\t\"muted\",\n\t\t\t\t` · ${entry.commandSummary} · ${entry.envKeys.length} env · ${entry.argsCount} args`,\n\t\t\t);\n\t\t\tconst overridden = entry.overriddenBy ? theme.fg(\"warning\", \" (overrides earlier source)\") : \"\";\n\t\t\tthis.listContainer.addChild(new Text(`${name}${meta}${overridden}`, 1, 0));\n\t\t}\n\t}\n\n\tprivate updateDetail(): void {\n\t\tthis.detailContainer.clear();\n\t\tconst entry = this.filteredEntries[this.selectedIndex];\n\t\tif (!entry) return;\n\n\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` source: ${entry.source}`), 1, 0));\n\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` command: ${entry.commandSummary}`), 1, 0));\n\t\tconst envSummary =\n\t\t\tentry.envKeys.length === 0 ? theme.fg(\"dim\", \"(none)\") : theme.fg(\"muted\", entry.envKeys.join(\", \"));\n\t\tthis.detailContainer.addChild(new Text(` env keys: ${envSummary}`, 1, 0));\n\t\tif (typeof entry.startupTimeoutSec === \"number\") {\n\t\t\tthis.detailContainer.addChild(new Text(theme.fg(\"muted\", ` timeout: ${entry.startupTimeoutSec}s`), 1, 0));\n\t\t}\n\t\tif (entry.autoApproveCount > 0) {\n\t\t\tthis.detailContainer.addChild(\n\t\t\t\tnew Text(theme.fg(\"muted\", ` autoApprove entries: ${entry.autoApproveCount}`), 1, 0),\n\t\t\t);\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredEntries.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredEntries.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t\tthis.updateDetail();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredEntries.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredEntries.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t\tthis.updateDetail();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\t\tthis.searchInput.handleInput(keyData);\n\t\tthis.applyFilter(this.searchInput.getValue());\n\t}\n}\n"]}
@@ -7,7 +7,7 @@ interface ScopedModelItem {
7
7
  thinkingLevel?: string;
8
8
  }
9
9
  /**
10
- * Component that renders a model selector with search
10
+ * Component that renders a model selector with search and provider tabs.
11
11
  */
12
12
  export declare class ModelSelectorComponent extends Container implements Focusable {
13
13
  private searchInput;
@@ -21,7 +21,6 @@ export declare class ModelSelectorComponent extends Container implements Focusab
21
21
  private filteredModels;
22
22
  private selectedIndex;
23
23
  private currentModel?;
24
- private settingsManager;
25
24
  private modelRegistry;
26
25
  private onSelectCallback;
27
26
  private onCancelCallback;
@@ -29,14 +28,24 @@ export declare class ModelSelectorComponent extends Container implements Focusab
29
28
  private tui;
30
29
  private scopedModels;
31
30
  private scope;
31
+ private providerTabs;
32
+ private activeProviderTab;
33
+ private providerText;
34
+ private providerHintText;
32
35
  private scopeText?;
33
36
  private scopeHintText?;
34
- constructor(tui: TUI, currentModel: Model<any> | undefined, settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: (model: Model<any>) => void, onCancel: () => void, initialSearchInput?: string);
37
+ constructor(tui: TUI, currentModel: Model<any> | undefined, _settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: (model: Model<any>) => void, onCancel: () => void, initialSearchInput?: string);
35
38
  private loadModels;
36
39
  private sortModels;
40
+ private isProviderAuthenticated;
41
+ private getProviderText;
42
+ private getProviderHintText;
37
43
  private getScopeText;
38
44
  private getScopeHintText;
39
45
  private setScope;
46
+ private rebuildProviderTabs;
47
+ private getProviderFilteredModels;
48
+ private cycleProvider;
40
49
  private filterModels;
41
50
  private updateList;
42
51
  handleInput(keyData: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAkB,MAAM,wBAAwB,CAAC;AACpE,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAGd,KAAK,EAGL,KAAK,GAAG,EACR,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAU,YAAW,SAAS;IACzE,OAAO,CAAC,WAAW,CAAQ;IAG3B,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IACD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAC,CAAa;IAClC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,aAAa,CAAC,CAAO;IAE7B,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EACpC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EACpB,kBAAkB,CAAC,EAAE,MAAM,EA+D3B;YAEa,UAAU;IA8CxB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,UAAU;IAyDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAwCjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import { type Model, modelsAreEqual } from \"@earendil-works/omk-ai\";\nimport {\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@earendil-works/omk-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.ts\";\nimport type { SettingsManager } from \"../../../core/settings-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel?: string;\n}\n\ntype ModelScope = \"all\" | \"scoped\";\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel?: Model<any>;\n\tprivate settingsManager: SettingsManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage?: string;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate scope: ModelScope = \"all\";\n\tprivate scopeText?: Text;\n\tprivate scopeHintText?: Text;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | undefined,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint about model filtering\n\t\tif (scopedModels.length > 0) {\n\t\t\tthis.scopeText = new Text(this.getScopeText(), 0, 0);\n\t\t\tthis.addChild(this.scopeText);\n\t\t\tthis.scopeHintText = new Text(this.getScopeHintText(), 0, 0);\n\t\t\tthis.addChild(this.scopeHintText);\n\t\t} else {\n\t\t\tconst hintText = \"Only showing models from configured providers. Use /login to add providers.\";\n\t\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\n\t\t}\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tif (initialSearchInput) {\n\t\t\tthis.searchInput.setValue(initialSearchInput);\n\t\t}\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tif (initialSearchInput) {\n\t\t\t\tthis.filterModels(initialSearchInput);\n\t\t\t} else {\n\t\t\t\tthis.updateList();\n\t\t\t}\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\tlet models: ModelItem[];\n\n\t\t// Refresh to pick up any changes to models.json\n\t\tthis.modelRegistry.refresh();\n\n\t\t// Check for models.json errors\n\t\tconst loadError = this.modelRegistry.getError();\n\t\tif (loadError) {\n\t\t\tthis.errorMessage = loadError;\n\t\t}\n\n\t\t// Load available models (built-in models still work even if models.json failed)\n\t\ttry {\n\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\tprovider: model.provider,\n\t\t\t\tid: model.id,\n\t\t\t\tmodel,\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.scopedModelItems = [];\n\t\t\tthis.activeModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.allModels = this.sortModels(models);\n\t\tthis.scopedModels = this.scopedModels.map((scoped) => {\n\t\t\tconst refreshed = this.modelRegistry.find(scoped.model.provider, scoped.model.id);\n\t\t\treturn refreshed ? { ...scoped, model: refreshed } : scoped;\n\t\t});\n\t\tthis.scopedModelItems = this.scopedModels.map((scoped) => ({\n\t\t\tprovider: scoped.model.provider,\n\t\t\tid: scoped.model.id,\n\t\t\tmodel: scoped.model,\n\t\t}));\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.filteredModels = this.activeModels;\n\t\tconst currentIndex = this.filteredModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex =\n\t\t\tcurrentIndex >= 0 ? currentIndex : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t}\n\n\tprivate sortModels(models: ModelItem[]): ModelItem[] {\n\t\tconst sorted = [...models];\n\t\t// Sort: current model first, then by provider\n\t\tsorted.sort((a, b) => {\n\t\t\tconst aIsCurrent = modelsAreEqual(this.currentModel, a.model);\n\t\t\tconst bIsCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\t\treturn sorted;\n\t}\n\n\tprivate getScopeText(): string {\n\t\tconst allText = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scopedText = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\treturn `${theme.fg(\"muted\", \"Scope: \")}${allText}${theme.fg(\"muted\", \" | \")}${scopedText}`;\n\t}\n\n\tprivate getScopeHintText(): string {\n\t\treturn keyHint(\"tui.input.tab\", \"scope\") + theme.fg(\"muted\", \" (all/scoped)\");\n\t}\n\n\tprivate setScope(scope: ModelScope): void {\n\t\tif (this.scope === scope) return;\n\t\tthis.scope = scope;\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tconst currentIndex = this.activeModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex = currentIndex >= 0 ? currentIndex : 0;\n\t\tthis.filterModels(this.searchInput.getValue());\n\t\tif (this.scopeText) {\n\t\t\tthis.scopeText.setText(this.getScopeText());\n\t\t}\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = query\n\t\t\t? fuzzyFilter(\n\t\t\t\t\tthis.activeModels,\n\t\t\t\t\tquery,\n\t\t\t\t\t({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`,\n\t\t\t\t)\n\t\t\t: this.activeModels;\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = modelsAreEqual(this.currentModel, item.model);\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${prefix + theme.fg(\"accent\", modelText)} ${providerBadge}${checkmark}`;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${modelText} ${providerBadge}${checkmark}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t} else {\n\t\t\tconst selected = this.filteredModels[this.selectedIndex];\n\t\t\tthis.listContainer.addChild(new Spacer(1));\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` Model Name: ${selected.model.name}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tif (this.scopedModelItems.length > 0) {\n\t\t\t\tconst nextScope: ModelScope = this.scope === \"all\" ? \"scoped\" : \"all\";\n\t\t\t\tthis.setScope(nextScope);\n\t\t\t\tif (this.scopeHintText) {\n\t\t\t\t\tthis.scopeHintText.setText(this.getScopeHintText());\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredModels.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredModels.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tthis.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,KAAK,EAAkB,MAAM,wBAAwB,CAAC;AAChG,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAGd,KAAK,EAGL,KAAK,GAAG,EACR,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAuBzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AA8BD;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAU,YAAW,SAAS;IACzE,OAAO,CAAC,WAAW,CAAQ;IAG3B,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IACD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAC,CAAa;IAClC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,YAAY,CAAqC;IACzD,OAAO,CAAC,iBAAiB,CAAiC;IAC1D,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,gBAAgB,CAAO;IAC/B,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,aAAa,CAAC,CAAO;IAE7B,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EACpC,gBAAgB,EAAE,eAAe,EACjC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EACpB,kBAAkB,CAAC,EAAE,MAAM,EAmE3B;YAEa,UAAU;IA+CxB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,uBAAuB;IAM/B,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,yBAAyB;IAOjC,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,UAAU;IAwDlB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA8CjC;IAED,OAAO,CAAC,YAAY;IAIpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import { getSupportedThinkingLevels, type Model, modelsAreEqual } from \"@earendil-works/omk-ai\";\nimport {\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@earendil-works/omk-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.ts\";\nimport type { SettingsManager } from \"../../../core/settings-manager.ts\";\n\nfunction formatThinkingBadge(model: Model<any>): string {\n\tconst levels = getSupportedThinkingLevels(model) as string[];\n\tif (!levels || levels.length === 0) return \"think:-\";\n\tif (levels.length === 1) return `think:${levels[0]}`;\n\treturn `think:${levels[0]}…${levels[levels.length - 1]}`;\n}\n\nfunction providerAuthGlyph(authConfigured: boolean): string {\n\treturn authConfigured ? \"✓\" : \"✗\";\n}\n\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel?: string;\n}\n\ntype ModelScope = \"all\" | \"scoped\";\ntype ProviderTab = \"all\" | string;\n\nconst ALL_PROVIDER_TAB = \"all\" as const;\nconst PROVIDER_TAB_ORDER = [\n\t\"anthropic\",\n\t\"openai\",\n\t\"google\",\n\t\"deepseek\",\n\t\"kimi\",\n\t\"openrouter\",\n\t\"mistral\",\n\t\"xai\",\n\t\"groq\",\n\t\"zai\",\n\t\"together\",\n\t\"fireworks\",\n] as const;\n\nfunction buildProviderTabs(models: readonly ModelItem[]): ProviderTab[] {\n\tconst providerIds = Array.from(new Set(models.map((model) => model.provider)));\n\tconst ordered = PROVIDER_TAB_ORDER.filter((provider) => providerIds.includes(provider));\n\tconst extras = providerIds\n\t\t.filter((provider) => !PROVIDER_TAB_ORDER.includes(provider as (typeof PROVIDER_TAB_ORDER)[number]))\n\t\t.sort((a, b) => a.localeCompare(b));\n\treturn [ALL_PROVIDER_TAB, ...ordered, ...extras];\n}\n\n/**\n * Component that renders a model selector with search and provider tabs.\n */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel?: Model<any>;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage?: string;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate scope: ModelScope = \"all\";\n\tprivate providerTabs: ProviderTab[] = [ALL_PROVIDER_TAB];\n\tprivate activeProviderTab: ProviderTab = ALL_PROVIDER_TAB;\n\tprivate providerText: Text;\n\tprivate providerHintText: Text;\n\tprivate scopeText?: Text;\n\tprivate scopeHintText?: Text;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | undefined,\n\t\t_settingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.providerText = new Text(this.getProviderText(), 0, 0);\n\t\tthis.addChild(this.providerText);\n\t\tthis.providerHintText = new Text(this.getProviderHintText(), 0, 0);\n\t\tthis.addChild(this.providerHintText);\n\n\t\t// Add hint about model filtering\n\t\tif (scopedModels.length > 0) {\n\t\t\tthis.scopeText = new Text(this.getScopeText(), 0, 0);\n\t\t\tthis.addChild(this.scopeText);\n\t\t\tthis.scopeHintText = new Text(this.getScopeHintText(), 0, 0);\n\t\t\tthis.addChild(this.scopeHintText);\n\t\t} else {\n\t\t\tconst hintText = \"Only showing models from configured providers. Use /login to add providers.\";\n\t\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\n\t\t}\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tif (initialSearchInput) {\n\t\t\tthis.searchInput.setValue(initialSearchInput);\n\t\t}\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tif (initialSearchInput) {\n\t\t\t\tthis.filterModels(initialSearchInput);\n\t\t\t} else {\n\t\t\t\tthis.updateList();\n\t\t\t}\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\tlet models: ModelItem[];\n\n\t\t// Refresh to pick up any changes to models.json\n\t\tthis.modelRegistry.refresh();\n\n\t\t// Check for models.json errors\n\t\tconst loadError = this.modelRegistry.getError();\n\t\tif (loadError) {\n\t\t\tthis.errorMessage = loadError;\n\t\t}\n\n\t\t// Load available models (built-in models still work even if models.json failed)\n\t\ttry {\n\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\tprovider: model.provider,\n\t\t\t\tid: model.id,\n\t\t\t\tmodel,\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.scopedModelItems = [];\n\t\t\tthis.activeModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.allModels = this.sortModels(models);\n\t\tthis.scopedModels = this.scopedModels.map((scoped) => {\n\t\t\tconst refreshed = this.modelRegistry.find(scoped.model.provider, scoped.model.id);\n\t\t\treturn refreshed ? { ...scoped, model: refreshed } : scoped;\n\t\t});\n\t\tthis.scopedModelItems = this.scopedModels.map((scoped) => ({\n\t\t\tprovider: scoped.model.provider,\n\t\t\tid: scoped.model.id,\n\t\t\tmodel: scoped.model,\n\t\t}));\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.rebuildProviderTabs();\n\t\tthis.filteredModels = this.getProviderFilteredModels();\n\t\tconst currentIndex = this.filteredModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex =\n\t\t\tcurrentIndex >= 0 ? currentIndex : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t}\n\n\tprivate sortModels(models: ModelItem[]): ModelItem[] {\n\t\tconst sorted = [...models];\n\t\t// Sort: current model first, then by provider\n\t\tsorted.sort((a, b) => {\n\t\t\tconst aIsCurrent = modelsAreEqual(this.currentModel, a.model);\n\t\t\tconst bIsCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\t\treturn sorted;\n\t}\n\n\tprivate isProviderAuthenticated(provider: string): boolean {\n\t\tif (provider === ALL_PROVIDER_TAB) return true;\n\t\tconst status = this.modelRegistry.authStorage.getAuthStatus(provider);\n\t\treturn status.configured || status.source !== undefined;\n\t}\n\n\tprivate getProviderText(): string {\n\t\tconst tabs = this.providerTabs.map((provider) => {\n\t\t\tconst auth = this.isProviderAuthenticated(provider);\n\t\t\tconst glyph = provider === ALL_PROVIDER_TAB ? \"\" : ` ${providerAuthGlyph(auth)}`;\n\t\t\tconst label = `${provider}${glyph}`;\n\t\t\tif (provider === this.activeProviderTab) return theme.fg(\"accent\", label);\n\t\t\treturn auth ? theme.fg(\"muted\", label) : theme.fg(\"dim\", label);\n\t\t});\n\t\treturn `${theme.fg(\"muted\", \"Provider: \")}${tabs.join(theme.fg(\"dim\", \" | \"))}`;\n\t}\n\n\tprivate getProviderHintText(): string {\n\t\treturn (\n\t\t\tkeyHint(\"tui.input.tab\", \"provider\") +\n\t\t\ttheme.fg(\"muted\", \" · \") +\n\t\t\tkeyHint(\"app.model.providerPrevious\", \"previous provider\")\n\t\t);\n\t}\n\n\tprivate getScopeText(): string {\n\t\tconst allText = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scopedText = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\treturn `${theme.fg(\"muted\", \"Scope: \")}${allText}${theme.fg(\"muted\", \" | \")}${scopedText}`;\n\t}\n\n\tprivate getScopeHintText(): string {\n\t\treturn keyHint(\"app.model.cycleForward\", \"scope\") + theme.fg(\"muted\", \" (all/scoped)\");\n\t}\n\n\tprivate setScope(scope: ModelScope): void {\n\t\tif (this.scope === scope) return;\n\t\tthis.scope = scope;\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.rebuildProviderTabs();\n\t\tconst currentIndex = this.getProviderFilteredModels().findIndex((item) =>\n\t\t\tmodelsAreEqual(this.currentModel, item.model),\n\t\t);\n\t\tthis.selectedIndex = currentIndex >= 0 ? currentIndex : 0;\n\t\tthis.filterModels(this.searchInput.getValue());\n\t\tif (this.scopeText) {\n\t\t\tthis.scopeText.setText(this.getScopeText());\n\t\t}\n\t\tif (this.scopeHintText) {\n\t\t\tthis.scopeHintText.setText(this.getScopeHintText());\n\t\t}\n\t}\n\n\tprivate rebuildProviderTabs(): void {\n\t\tthis.providerTabs = buildProviderTabs(this.activeModels);\n\t\tif (!this.providerTabs.includes(this.activeProviderTab)) {\n\t\t\tthis.activeProviderTab = ALL_PROVIDER_TAB;\n\t\t}\n\t\tthis.providerText.setText(this.getProviderText());\n\t\tthis.providerHintText.setText(this.getProviderHintText());\n\t}\n\n\tprivate getProviderFilteredModels(): ModelItem[] {\n\t\tif (this.activeProviderTab === ALL_PROVIDER_TAB) {\n\t\t\treturn this.activeModels;\n\t\t}\n\t\treturn this.activeModels.filter((item) => item.provider === this.activeProviderTab);\n\t}\n\n\tprivate cycleProvider(direction: 1 | -1): void {\n\t\tif (this.providerTabs.length === 0) return;\n\t\tconst currentIndex = Math.max(0, this.providerTabs.indexOf(this.activeProviderTab));\n\t\tconst nextIndex = (currentIndex + direction + this.providerTabs.length) % this.providerTabs.length;\n\t\tthis.activeProviderTab = this.providerTabs[nextIndex] ?? ALL_PROVIDER_TAB;\n\t\tthis.providerText.setText(this.getProviderText());\n\t\tthis.selectedIndex = 0;\n\t\tthis.filterModels(this.searchInput.getValue());\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tconst providerModels = this.getProviderFilteredModels();\n\t\tthis.filteredModels = query\n\t\t\t? fuzzyFilter(\n\t\t\t\t\tproviderModels,\n\t\t\t\t\tquery,\n\t\t\t\t\t({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`,\n\t\t\t\t)\n\t\t\t: providerModels;\n\t\tthis.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = modelsAreEqual(this.currentModel, item.model);\n\t\t\tconst providerAuthed = this.isProviderAuthenticated(item.provider);\n\t\t\tconst providerLabel = `[${item.provider}${providerAuthed ? \"\" : \" no-auth\"}]`;\n\t\t\tconst providerBadge = providerAuthed ? theme.fg(\"muted\", providerLabel) : theme.fg(\"warning\", providerLabel);\n\t\t\tconst thinkingBadge = theme.fg(\"dim\", ` · ${formatThinkingBadge(item.model)}`);\n\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tline = `${prefix + theme.fg(\"accent\", item.id)} ${providerBadge}${thinkingBadge}${checkmark}`;\n\t\t\t} else {\n\t\t\t\tline = ` ${item.id} ${providerBadge}${thinkingBadge}${checkmark}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tif (this.errorMessage) {\n\t\t\t// Show error in red\n\t\t\tconst errorLines = this.errorMessage.split(\"\\n\");\n\t\t\tfor (const line of errorLines) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t} else if (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t} else {\n\t\t\tconst selected = this.filteredModels[this.selectedIndex];\n\t\t\tthis.listContainer.addChild(new Spacer(1));\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` Model Name: ${selected.model.name}`), 0, 0));\n\t\t}\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tthis.cycleProvider(1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"app.model.providerPrevious\")) {\n\t\t\tthis.cycleProvider(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (\n\t\t\t(kb.matches(keyData, \"app.model.cycleForward\") || kb.matches(keyData, \"app.model.cycleBackward\")) &&\n\t\t\tthis.scopedModelItems.length > 0\n\t\t) {\n\t\t\tconst nextScope: ModelScope = this.scope === \"all\" ? \"scoped\" : \"all\";\n\t\t\tthis.setScope(nextScope);\n\t\t\treturn;\n\t\t}\n\t\t// Up arrow - wrap to bottom when at top\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filteredModels.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Down arrow - wrap to top when at bottom\n\t\telse if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filteredModels.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t}\n\t\t// Enter\n\t\telse if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\t\tif (selectedModel) {\n\t\t\t\tthis.handleSelect(selectedModel.model);\n\t\t\t}\n\t\t}\n\t\t// Escape or Ctrl+C\n\t\telse if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t}\n\t\t// Pass everything else to search input\n\t\telse {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.filterModels(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
@@ -1,10 +1,44 @@
1
- import { modelsAreEqual } from "@earendil-works/omk-ai";
1
+ import { getSupportedThinkingLevels, modelsAreEqual } from "@earendil-works/omk-ai";
2
2
  import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, } from "@earendil-works/omk-tui";
3
+ function formatThinkingBadge(model) {
4
+ const levels = getSupportedThinkingLevels(model);
5
+ if (!levels || levels.length === 0)
6
+ return "think:-";
7
+ if (levels.length === 1)
8
+ return `think:${levels[0]}`;
9
+ return `think:${levels[0]}…${levels[levels.length - 1]}`;
10
+ }
11
+ function providerAuthGlyph(authConfigured) {
12
+ return authConfigured ? "✓" : "✗";
13
+ }
3
14
  import { theme } from "../theme/theme.js";
4
15
  import { DynamicBorder } from "./dynamic-border.js";
5
16
  import { keyHint } from "./keybinding-hints.js";
17
+ const ALL_PROVIDER_TAB = "all";
18
+ const PROVIDER_TAB_ORDER = [
19
+ "anthropic",
20
+ "openai",
21
+ "google",
22
+ "deepseek",
23
+ "kimi",
24
+ "openrouter",
25
+ "mistral",
26
+ "xai",
27
+ "groq",
28
+ "zai",
29
+ "together",
30
+ "fireworks",
31
+ ];
32
+ function buildProviderTabs(models) {
33
+ const providerIds = Array.from(new Set(models.map((model) => model.provider)));
34
+ const ordered = PROVIDER_TAB_ORDER.filter((provider) => providerIds.includes(provider));
35
+ const extras = providerIds
36
+ .filter((provider) => !PROVIDER_TAB_ORDER.includes(provider))
37
+ .sort((a, b) => a.localeCompare(b));
38
+ return [ALL_PROVIDER_TAB, ...ordered, ...extras];
39
+ }
6
40
  /**
7
- * Component that renders a model selector with search
41
+ * Component that renders a model selector with search and provider tabs.
8
42
  */
9
43
  export class ModelSelectorComponent extends Container {
10
44
  searchInput;
@@ -24,7 +58,6 @@ export class ModelSelectorComponent extends Container {
24
58
  filteredModels = [];
25
59
  selectedIndex = 0;
26
60
  currentModel;
27
- settingsManager;
28
61
  modelRegistry;
29
62
  onSelectCallback;
30
63
  onCancelCallback;
@@ -32,13 +65,16 @@ export class ModelSelectorComponent extends Container {
32
65
  tui;
33
66
  scopedModels;
34
67
  scope = "all";
68
+ providerTabs = [ALL_PROVIDER_TAB];
69
+ activeProviderTab = ALL_PROVIDER_TAB;
70
+ providerText;
71
+ providerHintText;
35
72
  scopeText;
36
73
  scopeHintText;
37
- constructor(tui, currentModel, settingsManager, modelRegistry, scopedModels, onSelect, onCancel, initialSearchInput) {
74
+ constructor(tui, currentModel, _settingsManager, modelRegistry, scopedModels, onSelect, onCancel, initialSearchInput) {
38
75
  super();
39
76
  this.tui = tui;
40
77
  this.currentModel = currentModel;
41
- this.settingsManager = settingsManager;
42
78
  this.modelRegistry = modelRegistry;
43
79
  this.scopedModels = scopedModels;
44
80
  this.scope = scopedModels.length > 0 ? "scoped" : "all";
@@ -47,6 +83,10 @@ export class ModelSelectorComponent extends Container {
47
83
  // Add top border
48
84
  this.addChild(new DynamicBorder());
49
85
  this.addChild(new Spacer(1));
86
+ this.providerText = new Text(this.getProviderText(), 0, 0);
87
+ this.addChild(this.providerText);
88
+ this.providerHintText = new Text(this.getProviderHintText(), 0, 0);
89
+ this.addChild(this.providerHintText);
50
90
  // Add hint about model filtering
51
91
  if (scopedModels.length > 0) {
52
92
  this.scopeText = new Text(this.getScopeText(), 0, 0);
@@ -127,7 +167,8 @@ export class ModelSelectorComponent extends Container {
127
167
  model: scoped.model,
128
168
  }));
129
169
  this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
130
- this.filteredModels = this.activeModels;
170
+ this.rebuildProviderTabs();
171
+ this.filteredModels = this.getProviderFilteredModels();
131
172
  const currentIndex = this.filteredModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));
132
173
  this.selectedIndex =
133
174
  currentIndex >= 0 ? currentIndex : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
@@ -146,30 +187,81 @@ export class ModelSelectorComponent extends Container {
146
187
  });
147
188
  return sorted;
148
189
  }
190
+ isProviderAuthenticated(provider) {
191
+ if (provider === ALL_PROVIDER_TAB)
192
+ return true;
193
+ const status = this.modelRegistry.authStorage.getAuthStatus(provider);
194
+ return status.configured || status.source !== undefined;
195
+ }
196
+ getProviderText() {
197
+ const tabs = this.providerTabs.map((provider) => {
198
+ const auth = this.isProviderAuthenticated(provider);
199
+ const glyph = provider === ALL_PROVIDER_TAB ? "" : ` ${providerAuthGlyph(auth)}`;
200
+ const label = `${provider}${glyph}`;
201
+ if (provider === this.activeProviderTab)
202
+ return theme.fg("accent", label);
203
+ return auth ? theme.fg("muted", label) : theme.fg("dim", label);
204
+ });
205
+ return `${theme.fg("muted", "Provider: ")}${tabs.join(theme.fg("dim", " | "))}`;
206
+ }
207
+ getProviderHintText() {
208
+ return (keyHint("tui.input.tab", "provider") +
209
+ theme.fg("muted", " · ") +
210
+ keyHint("app.model.providerPrevious", "previous provider"));
211
+ }
149
212
  getScopeText() {
150
213
  const allText = this.scope === "all" ? theme.fg("accent", "all") : theme.fg("muted", "all");
151
214
  const scopedText = this.scope === "scoped" ? theme.fg("accent", "scoped") : theme.fg("muted", "scoped");
152
215
  return `${theme.fg("muted", "Scope: ")}${allText}${theme.fg("muted", " | ")}${scopedText}`;
153
216
  }
154
217
  getScopeHintText() {
155
- return keyHint("tui.input.tab", "scope") + theme.fg("muted", " (all/scoped)");
218
+ return keyHint("app.model.cycleForward", "scope") + theme.fg("muted", " (all/scoped)");
156
219
  }
157
220
  setScope(scope) {
158
221
  if (this.scope === scope)
159
222
  return;
160
223
  this.scope = scope;
161
224
  this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
162
- const currentIndex = this.activeModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));
225
+ this.rebuildProviderTabs();
226
+ const currentIndex = this.getProviderFilteredModels().findIndex((item) => modelsAreEqual(this.currentModel, item.model));
163
227
  this.selectedIndex = currentIndex >= 0 ? currentIndex : 0;
164
228
  this.filterModels(this.searchInput.getValue());
165
229
  if (this.scopeText) {
166
230
  this.scopeText.setText(this.getScopeText());
167
231
  }
232
+ if (this.scopeHintText) {
233
+ this.scopeHintText.setText(this.getScopeHintText());
234
+ }
235
+ }
236
+ rebuildProviderTabs() {
237
+ this.providerTabs = buildProviderTabs(this.activeModels);
238
+ if (!this.providerTabs.includes(this.activeProviderTab)) {
239
+ this.activeProviderTab = ALL_PROVIDER_TAB;
240
+ }
241
+ this.providerText.setText(this.getProviderText());
242
+ this.providerHintText.setText(this.getProviderHintText());
243
+ }
244
+ getProviderFilteredModels() {
245
+ if (this.activeProviderTab === ALL_PROVIDER_TAB) {
246
+ return this.activeModels;
247
+ }
248
+ return this.activeModels.filter((item) => item.provider === this.activeProviderTab);
249
+ }
250
+ cycleProvider(direction) {
251
+ if (this.providerTabs.length === 0)
252
+ return;
253
+ const currentIndex = Math.max(0, this.providerTabs.indexOf(this.activeProviderTab));
254
+ const nextIndex = (currentIndex + direction + this.providerTabs.length) % this.providerTabs.length;
255
+ this.activeProviderTab = this.providerTabs[nextIndex] ?? ALL_PROVIDER_TAB;
256
+ this.providerText.setText(this.getProviderText());
257
+ this.selectedIndex = 0;
258
+ this.filterModels(this.searchInput.getValue());
168
259
  }
169
260
  filterModels(query) {
261
+ const providerModels = this.getProviderFilteredModels();
170
262
  this.filteredModels = query
171
- ? fuzzyFilter(this.activeModels, query, ({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`)
172
- : this.activeModels;
263
+ ? fuzzyFilter(providerModels, query, ({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`)
264
+ : providerModels;
173
265
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
174
266
  this.updateList();
175
267
  }
@@ -185,19 +277,18 @@ export class ModelSelectorComponent extends Container {
185
277
  continue;
186
278
  const isSelected = i === this.selectedIndex;
187
279
  const isCurrent = modelsAreEqual(this.currentModel, item.model);
280
+ const providerAuthed = this.isProviderAuthenticated(item.provider);
281
+ const providerLabel = `[${item.provider}${providerAuthed ? "" : " no-auth"}]`;
282
+ const providerBadge = providerAuthed ? theme.fg("muted", providerLabel) : theme.fg("warning", providerLabel);
283
+ const thinkingBadge = theme.fg("dim", ` · ${formatThinkingBadge(item.model)}`);
284
+ const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
188
285
  let line = "";
189
286
  if (isSelected) {
190
287
  const prefix = theme.fg("accent", "→ ");
191
- const modelText = `${item.id}`;
192
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
193
- const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
194
- line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${checkmark}`;
288
+ line = `${prefix + theme.fg("accent", item.id)} ${providerBadge}${thinkingBadge}${checkmark}`;
195
289
  }
196
290
  else {
197
- const modelText = ` ${item.id}`;
198
- const providerBadge = theme.fg("muted", `[${item.provider}]`);
199
- const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
200
- line = `${modelText} ${providerBadge}${checkmark}`;
291
+ line = ` ${item.id} ${providerBadge}${thinkingBadge}${checkmark}`;
201
292
  }
202
293
  this.listContainer.addChild(new Text(line, 0, 0));
203
294
  }
@@ -226,13 +317,17 @@ export class ModelSelectorComponent extends Container {
226
317
  handleInput(keyData) {
227
318
  const kb = getKeybindings();
228
319
  if (kb.matches(keyData, "tui.input.tab")) {
229
- if (this.scopedModelItems.length > 0) {
230
- const nextScope = this.scope === "all" ? "scoped" : "all";
231
- this.setScope(nextScope);
232
- if (this.scopeHintText) {
233
- this.scopeHintText.setText(this.getScopeHintText());
234
- }
235
- }
320
+ this.cycleProvider(1);
321
+ return;
322
+ }
323
+ if (kb.matches(keyData, "app.model.providerPrevious")) {
324
+ this.cycleProvider(-1);
325
+ return;
326
+ }
327
+ if ((kb.matches(keyData, "app.model.cycleForward") || kb.matches(keyData, "app.model.cycleBackward")) &&
328
+ this.scopedModelItems.length > 0) {
329
+ const nextScope = this.scope === "all" ? "scoped" : "all";
330
+ this.setScope(nextScope);
236
331
  return;
237
332
  }
238
333
  // Up arrow - wrap to bottom when at top
@@ -267,8 +362,6 @@ export class ModelSelectorComponent extends Container {
267
362
  }
268
363
  }
269
364
  handleSelect(model) {
270
- // Save as new default
271
- this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
272
365
  this.onSelectCallback(model);
273
366
  }
274
367
  getSearchInput() {