open-multi-agent-kit 0.80.5 → 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.
- package/CHANGELOG.md +21 -0
- package/dist/config.d.ts +4 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -15
- package/dist/config.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +6 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +82 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +606 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/harness-control-events.d.ts +79 -0
- package/dist/core/harness-control-events.d.ts.map +1 -0
- package/dist/core/harness-control-events.js +339 -0
- package/dist/core/harness-control-events.js.map +1 -0
- package/dist/core/harness-control-replay.d.ts +26 -0
- package/dist/core/harness-control-replay.d.ts.map +1 -0
- package/dist/core/harness-control-replay.js +155 -0
- package/dist/core/harness-control-replay.js.map +1 -0
- package/dist/core/keybindings.d.ts +12 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +31 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp-inventory.d.ts +40 -0
- package/dist/core/mcp-inventory.d.ts.map +1 -0
- package/dist/core/mcp-inventory.js +104 -0
- package/dist/core/mcp-inventory.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/spec-kit/compiler.d.ts +67 -0
- package/dist/core/spec-kit/compiler.d.ts.map +1 -0
- package/dist/core/spec-kit/compiler.js +268 -0
- package/dist/core/spec-kit/compiler.js.map +1 -0
- package/dist/core/transaction-coordinator.d.ts +30 -0
- package/dist/core/transaction-coordinator.d.ts.map +1 -0
- package/dist/core/transaction-coordinator.js +173 -0
- package/dist/core/transaction-coordinator.js.map +1 -0
- package/dist/migrations.d.ts +35 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +409 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/control-panel.d.ts +29 -0
- package/dist/modes/interactive/components/control-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/control-panel.js +90 -0
- package/dist/modes/interactive/components/control-panel.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +21 -13
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +3 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +3 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/mcp-selector.d.ts +28 -0
- package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/mcp-selector.js +138 -0
- package/dist/modes/interactive/components/mcp-selector.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +12 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +120 -27
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +21 -8
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skills-selector.d.ts +32 -0
- package/dist/modes/interactive/components/skills-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/skills-selector.js +130 -0
- package/dist/modes/interactive/components/skills-selector.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +7 -8
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +7 -48
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +340 -97
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +86 -0
- package/dist/modes/interactive/theme/dracula.json +86 -0
- package/dist/modes/interactive/theme/gruvbox-dark.json +86 -0
- package/dist/modes/interactive/theme/kanagawa-dragon.json +86 -0
- package/dist/modes/interactive/theme/omk-control-panel.json +91 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +41 -16
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/theme/tokyo-night-storm.json +86 -0
- package/docs/compaction.md +21 -8
- package/docs/quickstart.md +1 -1
- package/docs/themes.md +10 -2
- package/docs/usage.md +2 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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,
|
|
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.
|
|
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("
|
|
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
|
-
|
|
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(
|
|
172
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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() {
|