@bubblebrain-ai/bubble 0.0.35 → 0.0.37
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/README.md +27 -7
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +4 -0
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +105 -27
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +87 -41
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +28 -5
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.js +9 -11
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +649 -233
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/message-list.js +5 -5
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +486 -133
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { useState, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { Box, Text, useInput, usePaste, useStdout } from "ink";
|
|
4
4
|
import { isKeyReleaseEvent } from "./key-events.js";
|
|
5
5
|
import { useTheme } from "./theme.js";
|
|
6
|
-
import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
6
|
+
import { encodeModel, decodeModel, displayModel, isUserVisibleProvider, } from "../provider-registry.js";
|
|
7
7
|
import { listBuiltinModels } from "../model-catalog.js";
|
|
8
8
|
import { padVisual, truncateVisual } from "../text-display.js";
|
|
9
9
|
import { hasTerminalMouseSequence } from "./terminal-mouse.js";
|
|
@@ -22,10 +22,17 @@ export function resolvePickerKeyAction(input, key) {
|
|
|
22
22
|
return "backspace";
|
|
23
23
|
if (key.delete)
|
|
24
24
|
return "delete";
|
|
25
|
+
// Batched chunks: slow ptys (web-terminal bridges, SSH, paste) can deliver
|
|
26
|
+
// several keys in ONE input event, in which case ink sets no key flags at
|
|
27
|
+
// all ("\r/quit\r" arrives with return=false). Act on the chunk's leading
|
|
28
|
+
// key instead of swallowing it — exact-match anchors would drop the whole
|
|
29
|
+
// chunk.
|
|
30
|
+
if (input.startsWith("\r") || input.startsWith("\n"))
|
|
31
|
+
return "enter";
|
|
25
32
|
const sequence = normalizeEscapeSequence(input);
|
|
26
|
-
if (/^(?:O|\[[\d;:]*)A
|
|
33
|
+
if (/^(?:O|\[[\d;:]*)A/.test(sequence))
|
|
27
34
|
return "up";
|
|
28
|
-
if (/^(?:O|\[[\d;:]*)B
|
|
35
|
+
if (/^(?:O|\[[\d;:]*)B/.test(sequence))
|
|
29
36
|
return "down";
|
|
30
37
|
return undefined;
|
|
31
38
|
}
|
|
@@ -53,7 +60,8 @@ export function formatSkillPickerRow(skill, options) {
|
|
|
53
60
|
return padVisual(truncateVisual(row, width), width);
|
|
54
61
|
}
|
|
55
62
|
export const MODEL_PICKER_MAX_BODY_ROWS = 10;
|
|
56
|
-
export const MODEL_PICKER_CHROME_ROWS =
|
|
63
|
+
export const MODEL_PICKER_CHROME_ROWS = 14;
|
|
64
|
+
export const MODEL_PICKER_RECENT_FAMILY_LIMIT = 3;
|
|
57
65
|
export function modelPickerBodyRows(termHeight) {
|
|
58
66
|
const rows = Number.isFinite(termHeight) ? Math.floor(termHeight) : 24;
|
|
59
67
|
return Math.max(1, Math.min(MODEL_PICKER_MAX_BODY_ROWS, rows - MODEL_PICKER_CHROME_ROWS));
|
|
@@ -78,24 +86,98 @@ export function padPickerRows(rows, bodyRows, width) {
|
|
|
78
86
|
}
|
|
79
87
|
return padded;
|
|
80
88
|
}
|
|
89
|
+
export function modelPickerRecentKey(recent) {
|
|
90
|
+
return recent.join("\u0000");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns a provider-scoped family key for model ordering.
|
|
94
|
+
*
|
|
95
|
+
* Keep this deliberately conservative: suffixes such as `mini`, `codex`,
|
|
96
|
+
* `spark`, or `highspeed` often describe distinct products rather than
|
|
97
|
+
* interchangeable siblings. The Codex Sol/Terra/Luna trio is the one family
|
|
98
|
+
* whose relationship is explicitly declared by the provider catalog.
|
|
99
|
+
*/
|
|
100
|
+
export function modelPickerFamilyKey(provider, modelId) {
|
|
101
|
+
const isCodexOAuth = provider.id === "openai-codex"
|
|
102
|
+
|| (provider.id === "openai" && provider.authType === "oauth");
|
|
103
|
+
if (isCodexOAuth) {
|
|
104
|
+
const match = /^(gpt-\d+(?:\.\d+)+)-(sol|terra|luna)$/i.exec(modelId);
|
|
105
|
+
if (match)
|
|
106
|
+
return `${provider.id}:${match[1].toLowerCase()}:sol-terra-luna`;
|
|
107
|
+
}
|
|
108
|
+
return `${provider.id}:${modelId}`;
|
|
109
|
+
}
|
|
110
|
+
export function buildModelPickerDisplayItems(options) {
|
|
111
|
+
const items = [];
|
|
112
|
+
let previousGroup;
|
|
113
|
+
let headerIndex = 0;
|
|
114
|
+
for (const option of options) {
|
|
115
|
+
if (option.group !== previousGroup) {
|
|
116
|
+
previousGroup = option.group;
|
|
117
|
+
items.push({
|
|
118
|
+
kind: "header",
|
|
119
|
+
key: `header:${headerIndex++}:${option.group}`,
|
|
120
|
+
label: option.group,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
items.push({ kind: "model", key: option.id, option });
|
|
124
|
+
}
|
|
125
|
+
return items;
|
|
126
|
+
}
|
|
127
|
+
export function resolveModelPickerDisplayIndex(items, selectedId) {
|
|
128
|
+
if (selectedId) {
|
|
129
|
+
const selectedIndex = items.findIndex((item) => item.kind === "model" && item.option.id === selectedId);
|
|
130
|
+
if (selectedIndex >= 0)
|
|
131
|
+
return selectedIndex;
|
|
132
|
+
}
|
|
133
|
+
const firstModelIndex = items.findIndex((item) => item.kind === "model");
|
|
134
|
+
return firstModelIndex >= 0 ? firstModelIndex : 0;
|
|
135
|
+
}
|
|
136
|
+
export function filterAndRankModelPickerOptions(options, query) {
|
|
137
|
+
const normalized = query.trim().toLowerCase();
|
|
138
|
+
if (!normalized)
|
|
139
|
+
return [...options];
|
|
140
|
+
return options
|
|
141
|
+
.map((option, index) => {
|
|
142
|
+
const { modelId } = decodeModel(option.id);
|
|
143
|
+
const fields = [option.label, modelId, option.id, option.providerBadge]
|
|
144
|
+
.map((value) => value.toLowerCase());
|
|
145
|
+
const rank = fields.some((value) => value === normalized)
|
|
146
|
+
? 0
|
|
147
|
+
: fields.some((value) => value.startsWith(normalized))
|
|
148
|
+
? 1
|
|
149
|
+
: fields.some((value) => value.includes(normalized))
|
|
150
|
+
? 2
|
|
151
|
+
: Number.POSITIVE_INFINITY;
|
|
152
|
+
return { option, index, rank };
|
|
153
|
+
})
|
|
154
|
+
.filter((entry) => Number.isFinite(entry.rank))
|
|
155
|
+
.sort((left, right) => left.rank - right.rank || left.index - right.index)
|
|
156
|
+
.map((entry) => entry.option);
|
|
157
|
+
}
|
|
81
158
|
function isThinkingToggleOption(option) {
|
|
82
159
|
const decoded = decodeModel(option.id);
|
|
83
160
|
return isThinkingToggleModel(decoded.providerId || option.providerBadge, decoded.modelId);
|
|
84
161
|
}
|
|
85
162
|
export function formatReasoningLevelsLabel(levels, asToggle = false) {
|
|
163
|
+
if (levels.length === 0)
|
|
164
|
+
return "checking";
|
|
86
165
|
if (isThinkingOnlyLevels(levels))
|
|
87
166
|
return "on";
|
|
88
167
|
if (asToggle)
|
|
89
168
|
return "thinking on/off";
|
|
90
|
-
|
|
91
|
-
return normalized.join("/");
|
|
169
|
+
return levels.join("/");
|
|
92
170
|
}
|
|
93
171
|
export function formatModelPickerRow(option, options) {
|
|
94
172
|
const width = Math.max(24, options.width);
|
|
95
173
|
const marker = options.selected ? "> " : " ";
|
|
96
174
|
const label = option.label.replace(/\s+/g, " ").trim();
|
|
97
175
|
const provider = option.providerBadge.replace(/\s+/g, " ").trim();
|
|
98
|
-
const effort =
|
|
176
|
+
const effort = option.pending
|
|
177
|
+
? "checking"
|
|
178
|
+
: (option.available === false || option.reasoningLevels.length === 0)
|
|
179
|
+
? "unavailable"
|
|
180
|
+
: formatReasoningLevelsLabel(option.reasoningLevels, isThinkingToggleOption(option));
|
|
99
181
|
const current = options.current ? " ●" : "";
|
|
100
182
|
const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
|
|
101
183
|
const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
|
|
@@ -127,6 +209,14 @@ export function formatNoModelResultsRow(query, width) {
|
|
|
127
209
|
return padVisual(truncateVisual(row, rowWidth), rowWidth);
|
|
128
210
|
}
|
|
129
211
|
export function preferredEffortIndex(option, currentThinkingLevel) {
|
|
212
|
+
const currentIndex = option.reasoningLevels.indexOf(currentThinkingLevel);
|
|
213
|
+
if (currentIndex >= 0)
|
|
214
|
+
return currentIndex;
|
|
215
|
+
if (option.defaultReasoningLevel) {
|
|
216
|
+
const defaultIndex = option.reasoningLevels.indexOf(option.defaultReasoningLevel);
|
|
217
|
+
if (defaultIndex >= 0)
|
|
218
|
+
return defaultIndex;
|
|
219
|
+
}
|
|
130
220
|
const preferred = normalizeThinkingLevel(currentThinkingLevel, option.reasoningLevels);
|
|
131
221
|
const index = option.reasoningLevels.indexOf(preferred);
|
|
132
222
|
return index >= 0 ? index : 0;
|
|
@@ -134,6 +224,46 @@ export function preferredEffortIndex(option, currentThinkingLevel) {
|
|
|
134
224
|
export function shouldOpenEffortPicker(option) {
|
|
135
225
|
return option.reasoningLevels.length > 1;
|
|
136
226
|
}
|
|
227
|
+
export function isModelOptionSelectable(option) {
|
|
228
|
+
return option.available && !option.pending && option.reasoningLevels.length > 0;
|
|
229
|
+
}
|
|
230
|
+
export function resolveSelectedModelId(options, selectedId) {
|
|
231
|
+
if (selectedId) {
|
|
232
|
+
const selected = options.find((option) => option.id === selectedId);
|
|
233
|
+
if (selected && isModelOptionSelectable(selected))
|
|
234
|
+
return selected.id;
|
|
235
|
+
}
|
|
236
|
+
return options.find(isModelOptionSelectable)?.id;
|
|
237
|
+
}
|
|
238
|
+
export function moveModelSelection(options, selectedId, direction) {
|
|
239
|
+
const selectableIndexes = options
|
|
240
|
+
.map((option, index) => isModelOptionSelectable(option) ? index : -1)
|
|
241
|
+
.filter((index) => index >= 0);
|
|
242
|
+
if (selectableIndexes.length === 0)
|
|
243
|
+
return undefined;
|
|
244
|
+
const currentIndex = options.findIndex((option) => option.id === selectedId);
|
|
245
|
+
if (currentIndex < 0) {
|
|
246
|
+
return options[direction > 0 ? selectableIndexes[0] : selectableIndexes[selectableIndexes.length - 1]]?.id;
|
|
247
|
+
}
|
|
248
|
+
const target = direction > 0
|
|
249
|
+
? selectableIndexes.find((index) => index > currentIndex)
|
|
250
|
+
: [...selectableIndexes].reverse().find((index) => index < currentIndex);
|
|
251
|
+
return options[target ?? currentIndex]?.id ?? selectedId;
|
|
252
|
+
}
|
|
253
|
+
export function reconcileModelPickerPhase(phase, options, currentThinkingLevel) {
|
|
254
|
+
if (phase.kind !== "effort")
|
|
255
|
+
return phase;
|
|
256
|
+
const latest = options.find((option) => option.id === phase.model.id);
|
|
257
|
+
if (!latest || !isModelOptionSelectable(latest))
|
|
258
|
+
return { kind: "model" };
|
|
259
|
+
const selectedLevel = phase.model.reasoningLevels[phase.selectedIndex];
|
|
260
|
+
const selectedIndex = selectedLevel && latest.reasoningLevels.includes(selectedLevel)
|
|
261
|
+
? latest.reasoningLevels.indexOf(selectedLevel)
|
|
262
|
+
: preferredEffortIndex(latest, currentThinkingLevel);
|
|
263
|
+
if (selectedIndex === phase.selectedIndex && sameModelOptionMetadata(phase.model, latest))
|
|
264
|
+
return phase;
|
|
265
|
+
return { kind: "effort", model: latest, selectedIndex };
|
|
266
|
+
}
|
|
137
267
|
function effortDescription(level, asToggle) {
|
|
138
268
|
if (asToggle)
|
|
139
269
|
return level === "off" ? "thinking disabled" : "thinking enabled";
|
|
@@ -152,6 +282,8 @@ function effortDescription(level, asToggle) {
|
|
|
152
282
|
return "extra high reasoning";
|
|
153
283
|
case "max":
|
|
154
284
|
return "maximum provider effort";
|
|
285
|
+
case "ultra":
|
|
286
|
+
return "maximum effort with delegation";
|
|
155
287
|
default:
|
|
156
288
|
return "reasoning effort";
|
|
157
289
|
}
|
|
@@ -169,81 +301,97 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
169
301
|
const terminalColumns = stdout?.columns || 80;
|
|
170
302
|
const bodyRows = modelPickerBodyRows(termHeight);
|
|
171
303
|
const rowWidth = Math.max(36, Math.min(110, terminalColumns - 6));
|
|
172
|
-
const
|
|
304
|
+
const recentKey = modelPickerRecentKey(recent);
|
|
305
|
+
const enabledSnapshot = registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id));
|
|
306
|
+
const providerKey = enabledSnapshot
|
|
307
|
+
.map((provider) => [
|
|
308
|
+
provider.id,
|
|
309
|
+
provider.name,
|
|
310
|
+
provider.baseURL,
|
|
311
|
+
provider.authType ?? "",
|
|
312
|
+
provider.protocol ?? "",
|
|
313
|
+
].join("\u0000"))
|
|
314
|
+
.join("\u0001");
|
|
315
|
+
const enabledProviders = useMemo(() => registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id)), [registry, providerKey]);
|
|
316
|
+
const localModelsByProvider = useMemo(() => new Map(enabledProviders.map((provider) => [
|
|
317
|
+
provider.id,
|
|
318
|
+
localModelsForProvider(registry, provider),
|
|
319
|
+
])), [registry, providerKey]);
|
|
320
|
+
const [discoveries, setDiscoveries] = useState(() => new Map());
|
|
321
|
+
const [refreshGeneration, setRefreshGeneration] = useState(0);
|
|
322
|
+
const requestGenerationRef = useRef(0);
|
|
323
|
+
const rawOptions = useMemo(() => buildMergedModelOptions({
|
|
324
|
+
providers: enabledProviders,
|
|
325
|
+
localModelsByProvider,
|
|
326
|
+
discoveries,
|
|
327
|
+
current,
|
|
328
|
+
recent,
|
|
329
|
+
}), [enabledProviders, localModelsByProvider, discoveries, current, recentKey]);
|
|
173
330
|
const [query, setQuery] = useState("");
|
|
174
|
-
const [
|
|
331
|
+
const [selectedId, setSelectedId] = useState(() => resolveSelectedModelId(buildLocalModelOptions(registry, current, recent), current));
|
|
175
332
|
const [phase, setPhase] = useState({ kind: "model" });
|
|
176
333
|
useEffect(() => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const { providerId } = decodeModel(m);
|
|
188
|
-
const provider = enabled.find((p) => p.id === providerId);
|
|
189
|
-
appendModelOption(opts, seen, {
|
|
190
|
-
id: m,
|
|
191
|
-
label: displayModel(m),
|
|
192
|
-
group: "Recent",
|
|
193
|
-
providerBadge: provider?.name || providerId || "",
|
|
334
|
+
const generation = ++requestGenerationRef.current;
|
|
335
|
+
setDiscoveries((previous) => {
|
|
336
|
+
const next = new Map();
|
|
337
|
+
for (const provider of enabledProviders) {
|
|
338
|
+
const prior = previous.get(provider.id);
|
|
339
|
+
next.set(provider.id, {
|
|
340
|
+
status: "pending",
|
|
341
|
+
models: prior?.models ?? [],
|
|
342
|
+
source: prior?.source,
|
|
343
|
+
authoritative: prior?.authoritative ?? false,
|
|
194
344
|
});
|
|
195
345
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
appendModelOption(opts, seen, {
|
|
208
|
-
id: encodeModel(m.providerId, m.id),
|
|
209
|
-
label: m.name,
|
|
210
|
-
group: provider.name,
|
|
211
|
-
providerBadge: provider.name,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (current && !seen.has(current)) {
|
|
216
|
-
const { providerId } = decodeModel(current);
|
|
217
|
-
const provider = enabled.find((p) => p.id === providerId);
|
|
218
|
-
opts.unshift({
|
|
219
|
-
id: current,
|
|
220
|
-
label: displayModel(current),
|
|
221
|
-
group: "Current",
|
|
222
|
-
providerBadge: provider?.name || providerId || "",
|
|
223
|
-
reasoningLevels: reasoningLevelsForModel(current),
|
|
346
|
+
return next;
|
|
347
|
+
});
|
|
348
|
+
for (const provider of enabledProviders) {
|
|
349
|
+
void discoverModelsForPicker(registry, provider, refreshGeneration > 0)
|
|
350
|
+
.then((result) => {
|
|
351
|
+
if (requestGenerationRef.current !== generation)
|
|
352
|
+
return;
|
|
353
|
+
setDiscoveries((previous) => {
|
|
354
|
+
const next = new Map(previous);
|
|
355
|
+
next.set(provider.id, { status: "complete", ...result });
|
|
356
|
+
return next;
|
|
224
357
|
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
:
|
|
358
|
+
})
|
|
359
|
+
.catch((error) => {
|
|
360
|
+
if (requestGenerationRef.current !== generation)
|
|
361
|
+
return;
|
|
362
|
+
setDiscoveries((previous) => {
|
|
363
|
+
const next = new Map(previous);
|
|
364
|
+
next.set(provider.id, {
|
|
365
|
+
status: "complete",
|
|
366
|
+
models: localModelsByProvider.get(provider.id) ?? [],
|
|
367
|
+
source: "fallback",
|
|
368
|
+
authoritative: false,
|
|
369
|
+
error: error instanceof Error ? error.message : String(error),
|
|
370
|
+
});
|
|
371
|
+
return next;
|
|
233
372
|
});
|
|
234
|
-
}
|
|
373
|
+
});
|
|
235
374
|
}
|
|
236
|
-
void refreshRemote();
|
|
237
375
|
return () => {
|
|
238
|
-
|
|
376
|
+
if (requestGenerationRef.current === generation)
|
|
377
|
+
requestGenerationRef.current++;
|
|
239
378
|
};
|
|
240
|
-
}, [registry,
|
|
379
|
+
}, [registry, providerKey, refreshGeneration]);
|
|
241
380
|
const options = useMemo(() => {
|
|
242
|
-
|
|
243
|
-
return rawOptions;
|
|
244
|
-
const q = query.toLowerCase();
|
|
245
|
-
return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
|
|
381
|
+
return filterAndRankModelPickerOptions(rawOptions, query);
|
|
246
382
|
}, [rawOptions, query]);
|
|
383
|
+
const resolvedSelectedId = resolveSelectedModelId(options, selectedId);
|
|
384
|
+
const selectedIndex = resolvedSelectedId
|
|
385
|
+
? options.findIndex((option) => option.id === resolvedSelectedId)
|
|
386
|
+
: -1;
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (selectedId !== resolvedSelectedId)
|
|
389
|
+
setSelectedId(resolvedSelectedId);
|
|
390
|
+
}, [selectedId, resolvedSelectedId]);
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
setPhase((currentPhase) => reconcileModelPickerPhase(currentPhase, rawOptions, currentThinkingLevel));
|
|
393
|
+
}, [rawOptions, currentThinkingLevel]);
|
|
394
|
+
const discoveryStatus = formatModelDiscoveryStatus(enabledProviders, discoveries);
|
|
247
395
|
useInput((input, key) => {
|
|
248
396
|
if (isKeyReleaseEvent(key))
|
|
249
397
|
return;
|
|
@@ -255,7 +403,9 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
255
403
|
return;
|
|
256
404
|
}
|
|
257
405
|
if (action === "enter") {
|
|
258
|
-
|
|
406
|
+
const level = levels[clampPickerIndex(phase.selectedIndex, levels.length)];
|
|
407
|
+
if (level)
|
|
408
|
+
onSelect(phase.model.id, level);
|
|
259
409
|
return;
|
|
260
410
|
}
|
|
261
411
|
if (action === "up") {
|
|
@@ -272,13 +422,17 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
272
422
|
}
|
|
273
423
|
return;
|
|
274
424
|
}
|
|
425
|
+
if (key.ctrl && (input.toLowerCase() === "r" || input === "\x12")) {
|
|
426
|
+
setRefreshGeneration((generation) => generation + 1);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
275
429
|
if (action === "escape") {
|
|
276
430
|
onCancel();
|
|
277
431
|
return;
|
|
278
432
|
}
|
|
279
433
|
if (action === "enter") {
|
|
280
|
-
const opt = options[
|
|
281
|
-
if (opt) {
|
|
434
|
+
const opt = selectedIndex >= 0 ? options[selectedIndex] : undefined;
|
|
435
|
+
if (opt && isModelOptionSelectable(opt)) {
|
|
282
436
|
if (shouldOpenEffortPicker(opt)) {
|
|
283
437
|
setPhase({
|
|
284
438
|
kind: "effort",
|
|
@@ -287,23 +441,25 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
287
441
|
});
|
|
288
442
|
}
|
|
289
443
|
else {
|
|
290
|
-
|
|
444
|
+
const level = opt.reasoningLevels[0];
|
|
445
|
+
if (level)
|
|
446
|
+
onSelect(opt.id, level);
|
|
291
447
|
}
|
|
292
448
|
}
|
|
293
449
|
return;
|
|
294
450
|
}
|
|
295
451
|
if (action === "up") {
|
|
296
|
-
|
|
452
|
+
setSelectedId((id) => moveModelSelection(options, id, -1));
|
|
297
453
|
return;
|
|
298
454
|
}
|
|
299
455
|
if (action === "down") {
|
|
300
|
-
|
|
456
|
+
setSelectedId((id) => moveModelSelection(options, id, 1));
|
|
301
457
|
return;
|
|
302
458
|
}
|
|
303
459
|
if (action === "backspace" || action === "delete") {
|
|
304
460
|
setQuery((q) => {
|
|
305
461
|
const next = q.slice(0, -1);
|
|
306
|
-
|
|
462
|
+
setSelectedId(undefined);
|
|
307
463
|
return next;
|
|
308
464
|
});
|
|
309
465
|
return;
|
|
@@ -311,47 +467,60 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
311
467
|
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
312
468
|
setQuery((q) => {
|
|
313
469
|
const next = q + input;
|
|
314
|
-
|
|
470
|
+
setSelectedId(undefined);
|
|
315
471
|
return next;
|
|
316
472
|
});
|
|
317
473
|
return;
|
|
318
474
|
}
|
|
319
475
|
});
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
const
|
|
476
|
+
const displayItems = buildModelPickerDisplayItems(options);
|
|
477
|
+
const safeSelectedDisplayIndex = resolveModelPickerDisplayIndex(displayItems, resolvedSelectedId);
|
|
478
|
+
const start = pickerWindowStart(safeSelectedDisplayIndex, displayItems.length, bodyRows);
|
|
479
|
+
const visible = displayItems.slice(start, start + bodyRows);
|
|
323
480
|
const rawModelRows = options.length === 0
|
|
324
481
|
? [{
|
|
325
482
|
key: "no-results",
|
|
326
483
|
row: formatNoModelResultsRow(query, rowWidth),
|
|
327
484
|
selected: false,
|
|
485
|
+
header: false,
|
|
328
486
|
}]
|
|
329
|
-
: visible.map((
|
|
330
|
-
|
|
331
|
-
|
|
487
|
+
: visible.map((item) => {
|
|
488
|
+
if (item.kind === "header") {
|
|
489
|
+
return {
|
|
490
|
+
key: item.key,
|
|
491
|
+
row: padVisual(truncateVisual(` ${item.label}`, rowWidth), rowWidth),
|
|
492
|
+
selected: false,
|
|
493
|
+
header: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const isSelected = item.option.id === resolvedSelectedId;
|
|
332
497
|
return {
|
|
333
|
-
key:
|
|
334
|
-
row: formatModelPickerRow(
|
|
498
|
+
key: item.key,
|
|
499
|
+
row: formatModelPickerRow(item.option, {
|
|
335
500
|
selected: isSelected,
|
|
336
|
-
current:
|
|
501
|
+
current: item.option.id === current,
|
|
337
502
|
width: rowWidth,
|
|
338
503
|
}),
|
|
339
504
|
selected: isSelected,
|
|
505
|
+
header: false,
|
|
340
506
|
};
|
|
341
507
|
});
|
|
342
508
|
const modelRows = padPickerRows(rawModelRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
|
|
343
509
|
key: rawModelRows[index]?.key ?? `blank-${index}`,
|
|
344
510
|
row,
|
|
345
511
|
selected: rawModelRows[index]?.selected ?? false,
|
|
512
|
+
header: rawModelRows[index]?.header ?? false,
|
|
346
513
|
}));
|
|
347
|
-
const highlightedOption = options[
|
|
348
|
-
const enterAction = highlightedOption
|
|
349
|
-
? (
|
|
350
|
-
|
|
514
|
+
const highlightedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;
|
|
515
|
+
const enterAction = highlightedOption
|
|
516
|
+
? (shouldOpenEffortPicker(highlightedOption)
|
|
517
|
+
? (isThinkingToggleOption(highlightedOption) ? "choose mode" : "choose effort")
|
|
518
|
+
: "select")
|
|
519
|
+
: "wait";
|
|
351
520
|
const effortTitle = phase.kind === "effort" && isThinkingToggleOption(phase.model)
|
|
352
521
|
? "Select Thinking Mode"
|
|
353
522
|
: "Select Reasoning Effort";
|
|
354
|
-
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: phase.kind === "effort" ? effortTitle : "Select Model" }), phase.kind === "effort" ? (_jsx(EffortPickerView, { model: phase.model, selectedIndex: phase.selectedIndex, bodyRows: bodyRows, rowWidth: rowWidth })) : (_jsxs(_Fragment, { children: [_jsx(SearchField, { query: query, placeholder: "Type to search models...", width: rowWidth }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 navigate \u00B7 Enter ", enterAction, " \u00B7 Esc cancel \u00B7
|
|
523
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: phase.kind === "effort" ? effortTitle : "Select Model" }), phase.kind === "effort" ? (_jsx(EffortPickerView, { model: phase.model, selectedIndex: phase.selectedIndex, bodyRows: bodyRows, rowWidth: rowWidth })) : (_jsxs(_Fragment, { children: [_jsx(SearchField, { query: query, placeholder: "Type to search models...", width: rowWidth }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 navigate \u00B7 Enter ", enterAction, " \u00B7 Esc cancel \u00B7 Ctrl+R retry"] }), _jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: discoveryStatus.hasError ? theme.warning : theme.muted, children: discoveryStatus.text || " " }) })] })), phase.kind === "model" && _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", marginTop: 1, children: modelRows.map(({ key, row, selected, header }) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: selected ? theme.accent : (header || key === "no-results" ? theme.muted : undefined), bold: selected || header, children: row }) }, key))) })] }));
|
|
355
524
|
}
|
|
356
525
|
function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
|
|
357
526
|
const theme = useTheme();
|
|
@@ -388,40 +557,124 @@ function SearchField({ query, placeholder, width }) {
|
|
|
388
557
|
return (_jsxs(Box, { height: 1, overflow: "hidden", marginTop: 1, children: [_jsx(Text, { color: theme.accent, children: "❯ " }), _jsx(Text, { children: visibleQuery }), _jsx(Text, { color: theme.accent, inverse: cursorVisible, children: " " }), visiblePlaceholder && _jsx(Text, { color: theme.muted, dimColor: true, children: visiblePlaceholder })] }));
|
|
389
558
|
}
|
|
390
559
|
export function buildLocalModelOptions(registry, current, recent) {
|
|
391
|
-
const
|
|
560
|
+
const providers = registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id));
|
|
561
|
+
const localModelsByProvider = new Map(providers.map((provider) => [
|
|
562
|
+
provider.id,
|
|
563
|
+
localModelsForProvider(registry, provider),
|
|
564
|
+
]));
|
|
565
|
+
return buildMergedModelOptions({
|
|
566
|
+
providers,
|
|
567
|
+
localModelsByProvider,
|
|
568
|
+
discoveries: new Map(),
|
|
569
|
+
current,
|
|
570
|
+
recent,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
export function buildMergedModelOptions(input) {
|
|
392
574
|
const opts = [];
|
|
393
575
|
const seen = new Set();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
576
|
+
const providerById = new Map(input.providers.map((provider) => [provider.id, provider]));
|
|
577
|
+
const effectiveModelsByProvider = new Map(input.providers.map((provider) => {
|
|
578
|
+
const localModels = input.localModelsByProvider.get(provider.id) ?? [];
|
|
579
|
+
const discovery = input.discoveries.get(provider.id);
|
|
580
|
+
return [provider.id, effectiveProviderModels(localModels, discovery)];
|
|
581
|
+
}));
|
|
582
|
+
const promotedFamilies = new Set();
|
|
583
|
+
const promotedRecentProviderIds = [];
|
|
584
|
+
const familyKeyForId = (id) => {
|
|
585
|
+
const { providerId, modelId } = decodeModel(id);
|
|
586
|
+
const provider = providerId ? providerById.get(providerId) : undefined;
|
|
587
|
+
return provider
|
|
588
|
+
? modelPickerFamilyKey(provider, modelId)
|
|
589
|
+
: `unknown:${id}`;
|
|
590
|
+
};
|
|
591
|
+
const appendPinned = (id, group) => {
|
|
592
|
+
appendModelOption(opts, seen, pinnedModelOption({
|
|
593
|
+
id,
|
|
594
|
+
group,
|
|
595
|
+
providers: input.providers,
|
|
596
|
+
localModelsByProvider: input.localModelsByProvider,
|
|
597
|
+
discoveries: input.discoveries,
|
|
598
|
+
}));
|
|
599
|
+
};
|
|
600
|
+
const appendFamily = (seedId, group) => {
|
|
601
|
+
const { providerId, modelId } = decodeModel(seedId);
|
|
602
|
+
const provider = providerId ? providerById.get(providerId) : undefined;
|
|
603
|
+
if (!provider) {
|
|
604
|
+
appendPinned(seedId, group);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const familyKey = modelPickerFamilyKey(provider, modelId);
|
|
608
|
+
const familyModels = (effectiveModelsByProvider.get(provider.id) ?? [])
|
|
609
|
+
.filter((model) => modelPickerFamilyKey(provider, model.id) === familyKey);
|
|
610
|
+
for (const model of familyModels) {
|
|
611
|
+
appendModelOption(opts, seen, {
|
|
612
|
+
...modelOptionMetadata(encodeModel(provider.id, model.id), model, true),
|
|
613
|
+
group,
|
|
614
|
+
providerBadge: provider.name,
|
|
615
|
+
available: true,
|
|
616
|
+
pending: false,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// Keep an exact Current/Recent pin visible even when an authoritative
|
|
620
|
+
// catalog no longer contains it. It remains unavailable and never causes
|
|
621
|
+
// local siblings to be reintroduced.
|
|
622
|
+
if (!familyModels.some((model) => model.id === modelId))
|
|
623
|
+
appendPinned(seedId, group);
|
|
624
|
+
};
|
|
625
|
+
if (input.current) {
|
|
626
|
+
appendPinned(input.current, "Current");
|
|
627
|
+
const currentFamilyKey = familyKeyForId(input.current);
|
|
628
|
+
promotedFamilies.add(currentFamilyKey);
|
|
629
|
+
appendFamily(input.current, "Current");
|
|
630
|
+
}
|
|
631
|
+
let recentFamilyCount = 0;
|
|
632
|
+
for (const recentId of [...new Set(input.recent)]) {
|
|
633
|
+
if (!recentId || recentId === input.current)
|
|
634
|
+
continue;
|
|
635
|
+
const familyKey = familyKeyForId(recentId);
|
|
636
|
+
if (promotedFamilies.has(familyKey)) {
|
|
637
|
+
// A previously promoted family may not contain this exact Recent model
|
|
638
|
+
// in an authoritative catalog. Preserve that pin as unavailable; when
|
|
639
|
+
// it is present, exact-ID deduplication makes this a no-op.
|
|
640
|
+
appendPinned(recentId, "Recent");
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
promotedFamilies.add(familyKey);
|
|
644
|
+
recentFamilyCount += 1;
|
|
645
|
+
const { providerId } = decodeModel(recentId);
|
|
646
|
+
if (providerId && providerById.has(providerId) && !promotedRecentProviderIds.includes(providerId)) {
|
|
647
|
+
promotedRecentProviderIds.push(providerId);
|
|
648
|
+
}
|
|
649
|
+
appendFamily(recentId, "Recent");
|
|
650
|
+
if (recentFamilyCount >= MODEL_PICKER_RECENT_FAMILY_LIMIT)
|
|
651
|
+
break;
|
|
403
652
|
}
|
|
404
|
-
|
|
405
|
-
|
|
653
|
+
const orderedProviderIds = [];
|
|
654
|
+
const addProviderId = (providerId) => {
|
|
655
|
+
if (providerId && providerById.has(providerId) && !orderedProviderIds.includes(providerId)) {
|
|
656
|
+
orderedProviderIds.push(providerId);
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
addProviderId(decodeModel(input.current).providerId);
|
|
660
|
+
for (const providerId of promotedRecentProviderIds)
|
|
661
|
+
addProviderId(providerId);
|
|
662
|
+
for (const provider of input.providers)
|
|
663
|
+
addProviderId(provider.id);
|
|
664
|
+
for (const providerId of orderedProviderIds) {
|
|
665
|
+
const provider = providerById.get(providerId);
|
|
666
|
+
if (!provider)
|
|
667
|
+
continue;
|
|
668
|
+
for (const model of effectiveModelsByProvider.get(provider.id) ?? []) {
|
|
406
669
|
appendModelOption(opts, seen, {
|
|
407
|
-
|
|
408
|
-
label: model.name,
|
|
670
|
+
...modelOptionMetadata(encodeModel(provider.id, model.id), model, true),
|
|
409
671
|
group: provider.name,
|
|
410
672
|
providerBadge: provider.name,
|
|
673
|
+
available: true,
|
|
674
|
+
pending: false,
|
|
411
675
|
});
|
|
412
676
|
}
|
|
413
677
|
}
|
|
414
|
-
if (current && !seen.has(current)) {
|
|
415
|
-
const { providerId } = decodeModel(current);
|
|
416
|
-
const provider = enabled.find((item) => item.id === providerId);
|
|
417
|
-
opts.unshift({
|
|
418
|
-
id: current,
|
|
419
|
-
label: displayModel(current),
|
|
420
|
-
group: "Current",
|
|
421
|
-
providerBadge: provider?.name || providerId || "",
|
|
422
|
-
reasoningLevels: reasoningLevelsForModel(current),
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
678
|
return opts;
|
|
426
679
|
}
|
|
427
680
|
function localModelsForProvider(registry, provider) {
|
|
@@ -435,20 +688,104 @@ function localModelsForProvider(registry, provider) {
|
|
|
435
688
|
id: model.id,
|
|
436
689
|
name: model.name,
|
|
437
690
|
providerId: provider.id,
|
|
691
|
+
reasoningLevels: model.reasoningLevels,
|
|
692
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
693
|
+
contextWindow: model.contextWindow,
|
|
694
|
+
toolOutputTokenLimit: model.toolOutputTokenLimit,
|
|
438
695
|
}));
|
|
439
696
|
}
|
|
440
697
|
function appendModelOption(options, seen, option) {
|
|
441
698
|
if (seen.has(option.id))
|
|
442
699
|
return;
|
|
443
700
|
seen.add(option.id);
|
|
444
|
-
options.push(
|
|
445
|
-
...option,
|
|
446
|
-
reasoningLevels: option.reasoningLevels ?? reasoningLevelsForModel(option.id),
|
|
447
|
-
});
|
|
701
|
+
options.push(option);
|
|
448
702
|
}
|
|
449
|
-
function
|
|
450
|
-
const
|
|
451
|
-
|
|
703
|
+
function pinnedModelOption(input) {
|
|
704
|
+
const { providerId, modelId } = decodeModel(input.id);
|
|
705
|
+
const provider = input.providers.find((item) => item.id === providerId);
|
|
706
|
+
if (!provider) {
|
|
707
|
+
return {
|
|
708
|
+
...modelOptionMetadata(input.id, undefined, false),
|
|
709
|
+
group: input.group,
|
|
710
|
+
providerBadge: providerId || "",
|
|
711
|
+
available: false,
|
|
712
|
+
pending: false,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const local = input.localModelsByProvider.get(provider.id) ?? [];
|
|
716
|
+
const discovery = input.discoveries.get(provider.id);
|
|
717
|
+
const discoveredModel = discovery?.models.find((model) => model.id === modelId);
|
|
718
|
+
const localModel = local.find((model) => model.id === modelId);
|
|
719
|
+
const metadata = discoveredModel ?? localModel;
|
|
720
|
+
const hasKnownMetadata = !!metadata;
|
|
721
|
+
let available = hasKnownMetadata;
|
|
722
|
+
let pending = false;
|
|
723
|
+
if (!discovery || discovery.status === "pending") {
|
|
724
|
+
pending = !hasKnownMetadata;
|
|
725
|
+
}
|
|
726
|
+
else if (discovery.authoritative) {
|
|
727
|
+
available = !!discoveredModel;
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
...modelOptionMetadata(input.id, metadata, hasKnownMetadata),
|
|
731
|
+
group: input.group,
|
|
732
|
+
providerBadge: provider.name,
|
|
733
|
+
available,
|
|
734
|
+
pending,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function effectiveProviderModels(localModels, discovery) {
|
|
738
|
+
if (!discovery)
|
|
739
|
+
return localModels;
|
|
740
|
+
if (discovery.authoritative)
|
|
741
|
+
return discovery.models;
|
|
742
|
+
return discovery.models.length > 0 ? discovery.models : localModels;
|
|
743
|
+
}
|
|
744
|
+
function modelOptionMetadata(id, rawModel, known) {
|
|
745
|
+
const model = rawModel;
|
|
746
|
+
const reasoningLevels = model?.reasoningLevels
|
|
747
|
+
? [...model.reasoningLevels]
|
|
748
|
+
: (known ? reasoningLevelsForModel(id) : []);
|
|
749
|
+
const defaultReasoningLevel = model?.defaultReasoningLevel
|
|
750
|
+
&& reasoningLevels.includes(model.defaultReasoningLevel)
|
|
751
|
+
? model.defaultReasoningLevel
|
|
752
|
+
: undefined;
|
|
753
|
+
return {
|
|
754
|
+
id,
|
|
755
|
+
label: model?.name || displayModel(id),
|
|
756
|
+
reasoningLevels,
|
|
757
|
+
defaultReasoningLevel,
|
|
758
|
+
contextWindow: model?.contextWindow,
|
|
759
|
+
toolOutputTokenLimit: model?.toolOutputTokenLimit,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
async function discoverModelsForPicker(registry, provider, forceRefresh) {
|
|
763
|
+
return registry.discoverModels(provider, { forceRefresh });
|
|
764
|
+
}
|
|
765
|
+
export function formatModelDiscoveryStatus(providers, discoveries) {
|
|
766
|
+
if (providers.some((provider) => discoveries.get(provider.id)?.status !== "complete")) {
|
|
767
|
+
return { text: "Refreshing model catalog…", hasError: false };
|
|
768
|
+
}
|
|
769
|
+
const failed = providers.filter((provider) => discoveries.get(provider.id)?.error);
|
|
770
|
+
if (failed.length > 0) {
|
|
771
|
+
const names = failed.slice(0, 2).map((provider) => provider.name).join(", ");
|
|
772
|
+
const suffix = failed.length > 2 ? ` +${failed.length - 2}` : "";
|
|
773
|
+
return { text: `Showing fallback models for ${names}${suffix} · Ctrl+R retry`, hasError: true };
|
|
774
|
+
}
|
|
775
|
+
return { text: "", hasError: false };
|
|
776
|
+
}
|
|
777
|
+
function sameModelOptionMetadata(left, right) {
|
|
778
|
+
return left.id === right.id
|
|
779
|
+
&& left.label === right.label
|
|
780
|
+
&& left.group === right.group
|
|
781
|
+
&& left.providerBadge === right.providerBadge
|
|
782
|
+
&& left.defaultReasoningLevel === right.defaultReasoningLevel
|
|
783
|
+
&& left.contextWindow === right.contextWindow
|
|
784
|
+
&& left.toolOutputTokenLimit === right.toolOutputTokenLimit
|
|
785
|
+
&& left.available === right.available
|
|
786
|
+
&& left.pending === right.pending
|
|
787
|
+
&& left.reasoningLevels.length === right.reasoningLevels.length
|
|
788
|
+
&& left.reasoningLevels.every((level, index) => level === right.reasoningLevels[index]);
|
|
452
789
|
}
|
|
453
790
|
function reasoningLevelsForModel(model) {
|
|
454
791
|
const { providerId, modelId } = decodeModel(model);
|
|
@@ -463,6 +800,16 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
463
800
|
const idx = providers.findIndex((p) => p.id === current);
|
|
464
801
|
return idx >= 0 ? idx : 0;
|
|
465
802
|
});
|
|
803
|
+
// Authoritative selection lives in a ref, mutated synchronously by the
|
|
804
|
+
// input handler; state only mirrors it for display. Slow ptys (web-terminal
|
|
805
|
+
// bridges) can deliver several key events before React commits a re-render,
|
|
806
|
+
// and a state-only selection would leave the handler closure stale — Enter
|
|
807
|
+
// would then select the pre-batch highlight.
|
|
808
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
809
|
+
const moveSelection = (next) => {
|
|
810
|
+
selectedIndexRef.current = next;
|
|
811
|
+
setSelectedIndex(next);
|
|
812
|
+
};
|
|
466
813
|
useEffect(() => {
|
|
467
814
|
const p = providers[selectedIndex];
|
|
468
815
|
if (p)
|
|
@@ -477,30 +824,31 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
477
824
|
return;
|
|
478
825
|
}
|
|
479
826
|
if (action === "enter") {
|
|
480
|
-
const p = providers[
|
|
827
|
+
const p = providers[selectedIndexRef.current];
|
|
481
828
|
if (p)
|
|
482
829
|
onSelect(p.id);
|
|
483
830
|
return;
|
|
484
831
|
}
|
|
485
832
|
if (action === "up") {
|
|
486
|
-
|
|
833
|
+
moveSelection((selectedIndexRef.current - 1 + providers.length) % providers.length);
|
|
487
834
|
return;
|
|
488
835
|
}
|
|
489
836
|
if (action === "down") {
|
|
490
|
-
|
|
837
|
+
moveSelection((selectedIndexRef.current + 1) % providers.length);
|
|
491
838
|
return;
|
|
492
839
|
}
|
|
493
840
|
if (isPrintablePickerInput(input) && input.length === 1 && /[a-z]/i.test(input)) {
|
|
494
841
|
const char = input.toLowerCase();
|
|
495
|
-
|
|
842
|
+
const from = selectedIndexRef.current;
|
|
843
|
+
for (let i = from + 1; i < providers.length; i++) {
|
|
496
844
|
if (providers[i].name.toLowerCase().startsWith(char)) {
|
|
497
|
-
|
|
845
|
+
moveSelection(i);
|
|
498
846
|
return;
|
|
499
847
|
}
|
|
500
848
|
}
|
|
501
|
-
for (let i = 0; i <=
|
|
849
|
+
for (let i = 0; i <= from; i++) {
|
|
502
850
|
if (providers[i].name.toLowerCase().startsWith(char)) {
|
|
503
|
-
|
|
851
|
+
moveSelection(i);
|
|
504
852
|
return;
|
|
505
853
|
}
|
|
506
854
|
}
|
|
@@ -508,7 +856,12 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title,
|
|
|
508
856
|
});
|
|
509
857
|
const start = Math.max(0, Math.min(selectedIndex, providers.length - maxVisible));
|
|
510
858
|
const visible = providers.slice(start, start + maxVisible);
|
|
511
|
-
return (
|
|
859
|
+
return (
|
|
860
|
+
// backgroundColor lives on the picker's OWN root box, below its state:
|
|
861
|
+
// a canvas toggle (theme preview across a forced-theme mismatch) then
|
|
862
|
+
// reshapes only this stateless output subtree. On an ancestor wrapper it
|
|
863
|
+
// would remount the whole picker and reset selectedIndex mid-navigation.
|
|
864
|
+
_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, backgroundColor: theme.background, children: [_jsx(Text, { bold: true, color: theme.accent, children: title || "Select Provider" }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 type letter to jump" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((p, i) => {
|
|
512
865
|
const actualIndex = start + i;
|
|
513
866
|
const isSelected = actualIndex === selectedIndex;
|
|
514
867
|
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", p.name, p.id === current ? " (current)" : "", !p.enabled ? " [disabled]" : ""] }) }, p.id));
|