@bubblebrain-ai/bubble 0.0.34 → 0.0.36
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/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- 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 +5 -3
- package/dist/config.js +10 -5
- 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 +114 -28
- 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 +92 -52
- 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 +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- 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 +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- 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 +35 -17
- 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.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- 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 +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- 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 +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- 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/plan-confirm.js +1 -1
- 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 +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- 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 +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
2
|
-
import { useState, useEffect, useMemo } from "react";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
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";
|
|
10
|
-
import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
|
|
10
|
+
import { getAvailableThinkingLevels, isThinkingOnlyLevels, isThinkingToggleModel, normalizeThinkingLevel } from "../provider-transform.js";
|
|
11
11
|
export { padVisual, truncateVisual } from "../text-display.js";
|
|
12
12
|
export function resolvePickerKeyAction(input, key) {
|
|
13
13
|
if (key.escape)
|
|
@@ -53,7 +53,8 @@ export function formatSkillPickerRow(skill, options) {
|
|
|
53
53
|
return padVisual(truncateVisual(row, width), width);
|
|
54
54
|
}
|
|
55
55
|
export const MODEL_PICKER_MAX_BODY_ROWS = 10;
|
|
56
|
-
export const MODEL_PICKER_CHROME_ROWS =
|
|
56
|
+
export const MODEL_PICKER_CHROME_ROWS = 14;
|
|
57
|
+
export const MODEL_PICKER_RECENT_FAMILY_LIMIT = 3;
|
|
57
58
|
export function modelPickerBodyRows(termHeight) {
|
|
58
59
|
const rows = Number.isFinite(termHeight) ? Math.floor(termHeight) : 24;
|
|
59
60
|
return Math.max(1, Math.min(MODEL_PICKER_MAX_BODY_ROWS, rows - MODEL_PICKER_CHROME_ROWS));
|
|
@@ -78,25 +79,98 @@ export function padPickerRows(rows, bodyRows, width) {
|
|
|
78
79
|
}
|
|
79
80
|
return padded;
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
export function modelPickerRecentKey(recent) {
|
|
83
|
+
return recent.join("\u0000");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns a provider-scoped family key for model ordering.
|
|
87
|
+
*
|
|
88
|
+
* Keep this deliberately conservative: suffixes such as `mini`, `codex`,
|
|
89
|
+
* `spark`, or `highspeed` often describe distinct products rather than
|
|
90
|
+
* interchangeable siblings. The Codex Sol/Terra/Luna trio is the one family
|
|
91
|
+
* whose relationship is explicitly declared by the provider catalog.
|
|
92
|
+
*/
|
|
93
|
+
export function modelPickerFamilyKey(provider, modelId) {
|
|
94
|
+
const isCodexOAuth = provider.id === "openai-codex"
|
|
95
|
+
|| (provider.id === "openai" && provider.authType === "oauth");
|
|
96
|
+
if (isCodexOAuth) {
|
|
97
|
+
const match = /^(gpt-\d+(?:\.\d+)+)-(sol|terra|luna)$/i.exec(modelId);
|
|
98
|
+
if (match)
|
|
99
|
+
return `${provider.id}:${match[1].toLowerCase()}:sol-terra-luna`;
|
|
100
|
+
}
|
|
101
|
+
return `${provider.id}:${modelId}`;
|
|
102
|
+
}
|
|
103
|
+
export function buildModelPickerDisplayItems(options) {
|
|
104
|
+
const items = [];
|
|
105
|
+
let previousGroup;
|
|
106
|
+
let headerIndex = 0;
|
|
107
|
+
for (const option of options) {
|
|
108
|
+
if (option.group !== previousGroup) {
|
|
109
|
+
previousGroup = option.group;
|
|
110
|
+
items.push({
|
|
111
|
+
kind: "header",
|
|
112
|
+
key: `header:${headerIndex++}:${option.group}`,
|
|
113
|
+
label: option.group,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
items.push({ kind: "model", key: option.id, option });
|
|
117
|
+
}
|
|
118
|
+
return items;
|
|
119
|
+
}
|
|
120
|
+
export function resolveModelPickerDisplayIndex(items, selectedId) {
|
|
121
|
+
if (selectedId) {
|
|
122
|
+
const selectedIndex = items.findIndex((item) => item.kind === "model" && item.option.id === selectedId);
|
|
123
|
+
if (selectedIndex >= 0)
|
|
124
|
+
return selectedIndex;
|
|
125
|
+
}
|
|
126
|
+
const firstModelIndex = items.findIndex((item) => item.kind === "model");
|
|
127
|
+
return firstModelIndex >= 0 ? firstModelIndex : 0;
|
|
128
|
+
}
|
|
129
|
+
export function filterAndRankModelPickerOptions(options, query) {
|
|
130
|
+
const normalized = query.trim().toLowerCase();
|
|
131
|
+
if (!normalized)
|
|
132
|
+
return [...options];
|
|
133
|
+
return options
|
|
134
|
+
.map((option, index) => {
|
|
135
|
+
const { modelId } = decodeModel(option.id);
|
|
136
|
+
const fields = [option.label, modelId, option.id, option.providerBadge]
|
|
137
|
+
.map((value) => value.toLowerCase());
|
|
138
|
+
const rank = fields.some((value) => value === normalized)
|
|
139
|
+
? 0
|
|
140
|
+
: fields.some((value) => value.startsWith(normalized))
|
|
141
|
+
? 1
|
|
142
|
+
: fields.some((value) => value.includes(normalized))
|
|
143
|
+
? 2
|
|
144
|
+
: Number.POSITIVE_INFINITY;
|
|
145
|
+
return { option, index, rank };
|
|
146
|
+
})
|
|
147
|
+
.filter((entry) => Number.isFinite(entry.rank))
|
|
148
|
+
.sort((left, right) => left.rank - right.rank || left.index - right.index)
|
|
149
|
+
.map((entry) => entry.option);
|
|
150
|
+
}
|
|
151
|
+
function isThinkingToggleOption(option) {
|
|
152
|
+
const decoded = decodeModel(option.id);
|
|
153
|
+
return isThinkingToggleModel(decoded.providerId || option.providerBadge, decoded.modelId);
|
|
87
154
|
}
|
|
88
155
|
export function formatReasoningLevelsLabel(levels, asToggle = false) {
|
|
89
|
-
|
|
156
|
+
if (levels.length === 0)
|
|
157
|
+
return "checking";
|
|
158
|
+
if (isThinkingOnlyLevels(levels))
|
|
159
|
+
return "on";
|
|
90
160
|
if (asToggle)
|
|
91
161
|
return "thinking on/off";
|
|
92
|
-
return
|
|
162
|
+
return levels.join("/");
|
|
93
163
|
}
|
|
94
164
|
export function formatModelPickerRow(option, options) {
|
|
95
165
|
const width = Math.max(24, options.width);
|
|
96
166
|
const marker = options.selected ? "> " : " ";
|
|
97
167
|
const label = option.label.replace(/\s+/g, " ").trim();
|
|
98
168
|
const provider = option.providerBadge.replace(/\s+/g, " ").trim();
|
|
99
|
-
const effort =
|
|
169
|
+
const effort = option.pending
|
|
170
|
+
? "checking"
|
|
171
|
+
: (option.available === false || option.reasoningLevels.length === 0)
|
|
172
|
+
? "unavailable"
|
|
173
|
+
: formatReasoningLevelsLabel(option.reasoningLevels, isThinkingToggleOption(option));
|
|
100
174
|
const current = options.current ? " ●" : "";
|
|
101
175
|
const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
|
|
102
176
|
const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
|
|
@@ -128,6 +202,14 @@ export function formatNoModelResultsRow(query, width) {
|
|
|
128
202
|
return padVisual(truncateVisual(row, rowWidth), rowWidth);
|
|
129
203
|
}
|
|
130
204
|
export function preferredEffortIndex(option, currentThinkingLevel) {
|
|
205
|
+
const currentIndex = option.reasoningLevels.indexOf(currentThinkingLevel);
|
|
206
|
+
if (currentIndex >= 0)
|
|
207
|
+
return currentIndex;
|
|
208
|
+
if (option.defaultReasoningLevel) {
|
|
209
|
+
const defaultIndex = option.reasoningLevels.indexOf(option.defaultReasoningLevel);
|
|
210
|
+
if (defaultIndex >= 0)
|
|
211
|
+
return defaultIndex;
|
|
212
|
+
}
|
|
131
213
|
const preferred = normalizeThinkingLevel(currentThinkingLevel, option.reasoningLevels);
|
|
132
214
|
const index = option.reasoningLevels.indexOf(preferred);
|
|
133
215
|
return index >= 0 ? index : 0;
|
|
@@ -135,6 +217,46 @@ export function preferredEffortIndex(option, currentThinkingLevel) {
|
|
|
135
217
|
export function shouldOpenEffortPicker(option) {
|
|
136
218
|
return option.reasoningLevels.length > 1;
|
|
137
219
|
}
|
|
220
|
+
export function isModelOptionSelectable(option) {
|
|
221
|
+
return option.available && !option.pending && option.reasoningLevels.length > 0;
|
|
222
|
+
}
|
|
223
|
+
export function resolveSelectedModelId(options, selectedId) {
|
|
224
|
+
if (selectedId) {
|
|
225
|
+
const selected = options.find((option) => option.id === selectedId);
|
|
226
|
+
if (selected && isModelOptionSelectable(selected))
|
|
227
|
+
return selected.id;
|
|
228
|
+
}
|
|
229
|
+
return options.find(isModelOptionSelectable)?.id;
|
|
230
|
+
}
|
|
231
|
+
export function moveModelSelection(options, selectedId, direction) {
|
|
232
|
+
const selectableIndexes = options
|
|
233
|
+
.map((option, index) => isModelOptionSelectable(option) ? index : -1)
|
|
234
|
+
.filter((index) => index >= 0);
|
|
235
|
+
if (selectableIndexes.length === 0)
|
|
236
|
+
return undefined;
|
|
237
|
+
const currentIndex = options.findIndex((option) => option.id === selectedId);
|
|
238
|
+
if (currentIndex < 0) {
|
|
239
|
+
return options[direction > 0 ? selectableIndexes[0] : selectableIndexes[selectableIndexes.length - 1]]?.id;
|
|
240
|
+
}
|
|
241
|
+
const target = direction > 0
|
|
242
|
+
? selectableIndexes.find((index) => index > currentIndex)
|
|
243
|
+
: [...selectableIndexes].reverse().find((index) => index < currentIndex);
|
|
244
|
+
return options[target ?? currentIndex]?.id ?? selectedId;
|
|
245
|
+
}
|
|
246
|
+
export function reconcileModelPickerPhase(phase, options, currentThinkingLevel) {
|
|
247
|
+
if (phase.kind !== "effort")
|
|
248
|
+
return phase;
|
|
249
|
+
const latest = options.find((option) => option.id === phase.model.id);
|
|
250
|
+
if (!latest || !isModelOptionSelectable(latest))
|
|
251
|
+
return { kind: "model" };
|
|
252
|
+
const selectedLevel = phase.model.reasoningLevels[phase.selectedIndex];
|
|
253
|
+
const selectedIndex = selectedLevel && latest.reasoningLevels.includes(selectedLevel)
|
|
254
|
+
? latest.reasoningLevels.indexOf(selectedLevel)
|
|
255
|
+
: preferredEffortIndex(latest, currentThinkingLevel);
|
|
256
|
+
if (selectedIndex === phase.selectedIndex && sameModelOptionMetadata(phase.model, latest))
|
|
257
|
+
return phase;
|
|
258
|
+
return { kind: "effort", model: latest, selectedIndex };
|
|
259
|
+
}
|
|
138
260
|
function effortDescription(level, asToggle) {
|
|
139
261
|
if (asToggle)
|
|
140
262
|
return level === "off" ? "thinking disabled" : "thinking enabled";
|
|
@@ -153,6 +275,8 @@ function effortDescription(level, asToggle) {
|
|
|
153
275
|
return "extra high reasoning";
|
|
154
276
|
case "max":
|
|
155
277
|
return "maximum provider effort";
|
|
278
|
+
case "ultra":
|
|
279
|
+
return "maximum effort with delegation";
|
|
156
280
|
default:
|
|
157
281
|
return "reasoning effort";
|
|
158
282
|
}
|
|
@@ -170,81 +294,97 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
170
294
|
const terminalColumns = stdout?.columns || 80;
|
|
171
295
|
const bodyRows = modelPickerBodyRows(termHeight);
|
|
172
296
|
const rowWidth = Math.max(36, Math.min(110, terminalColumns - 6));
|
|
173
|
-
const
|
|
297
|
+
const recentKey = modelPickerRecentKey(recent);
|
|
298
|
+
const enabledSnapshot = registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id));
|
|
299
|
+
const providerKey = enabledSnapshot
|
|
300
|
+
.map((provider) => [
|
|
301
|
+
provider.id,
|
|
302
|
+
provider.name,
|
|
303
|
+
provider.baseURL,
|
|
304
|
+
provider.authType ?? "",
|
|
305
|
+
provider.protocol ?? "",
|
|
306
|
+
].join("\u0000"))
|
|
307
|
+
.join("\u0001");
|
|
308
|
+
const enabledProviders = useMemo(() => registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id)), [registry, providerKey]);
|
|
309
|
+
const localModelsByProvider = useMemo(() => new Map(enabledProviders.map((provider) => [
|
|
310
|
+
provider.id,
|
|
311
|
+
localModelsForProvider(registry, provider),
|
|
312
|
+
])), [registry, providerKey]);
|
|
313
|
+
const [discoveries, setDiscoveries] = useState(() => new Map());
|
|
314
|
+
const [refreshGeneration, setRefreshGeneration] = useState(0);
|
|
315
|
+
const requestGenerationRef = useRef(0);
|
|
316
|
+
const rawOptions = useMemo(() => buildMergedModelOptions({
|
|
317
|
+
providers: enabledProviders,
|
|
318
|
+
localModelsByProvider,
|
|
319
|
+
discoveries,
|
|
320
|
+
current,
|
|
321
|
+
recent,
|
|
322
|
+
}), [enabledProviders, localModelsByProvider, discoveries, current, recentKey]);
|
|
174
323
|
const [query, setQuery] = useState("");
|
|
175
|
-
const [
|
|
324
|
+
const [selectedId, setSelectedId] = useState(() => resolveSelectedModelId(buildLocalModelOptions(registry, current, recent), current));
|
|
176
325
|
const [phase, setPhase] = useState({ kind: "model" });
|
|
177
326
|
useEffect(() => {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const { providerId } = decodeModel(m);
|
|
189
|
-
const provider = enabled.find((p) => p.id === providerId);
|
|
190
|
-
appendModelOption(opts, seen, {
|
|
191
|
-
id: m,
|
|
192
|
-
label: displayModel(m),
|
|
193
|
-
group: "Recent",
|
|
194
|
-
providerBadge: provider?.name || providerId || "",
|
|
327
|
+
const generation = ++requestGenerationRef.current;
|
|
328
|
+
setDiscoveries((previous) => {
|
|
329
|
+
const next = new Map();
|
|
330
|
+
for (const provider of enabledProviders) {
|
|
331
|
+
const prior = previous.get(provider.id);
|
|
332
|
+
next.set(provider.id, {
|
|
333
|
+
status: "pending",
|
|
334
|
+
models: prior?.models ?? [],
|
|
335
|
+
source: prior?.source,
|
|
336
|
+
authoritative: prior?.authoritative ?? false,
|
|
195
337
|
});
|
|
196
338
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
appendModelOption(opts, seen, {
|
|
209
|
-
id: encodeModel(m.providerId, m.id),
|
|
210
|
-
label: m.name,
|
|
211
|
-
group: provider.name,
|
|
212
|
-
providerBadge: provider.name,
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (current && !seen.has(current)) {
|
|
217
|
-
const { providerId } = decodeModel(current);
|
|
218
|
-
const provider = enabled.find((p) => p.id === providerId);
|
|
219
|
-
opts.unshift({
|
|
220
|
-
id: current,
|
|
221
|
-
label: displayModel(current),
|
|
222
|
-
group: "Current",
|
|
223
|
-
providerBadge: provider?.name || providerId || "",
|
|
224
|
-
reasoningLevels: reasoningLevelsForModel(current),
|
|
339
|
+
return next;
|
|
340
|
+
});
|
|
341
|
+
for (const provider of enabledProviders) {
|
|
342
|
+
void discoverModelsForPicker(registry, provider, refreshGeneration > 0)
|
|
343
|
+
.then((result) => {
|
|
344
|
+
if (requestGenerationRef.current !== generation)
|
|
345
|
+
return;
|
|
346
|
+
setDiscoveries((previous) => {
|
|
347
|
+
const next = new Map(previous);
|
|
348
|
+
next.set(provider.id, { status: "complete", ...result });
|
|
349
|
+
return next;
|
|
225
350
|
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
:
|
|
351
|
+
})
|
|
352
|
+
.catch((error) => {
|
|
353
|
+
if (requestGenerationRef.current !== generation)
|
|
354
|
+
return;
|
|
355
|
+
setDiscoveries((previous) => {
|
|
356
|
+
const next = new Map(previous);
|
|
357
|
+
next.set(provider.id, {
|
|
358
|
+
status: "complete",
|
|
359
|
+
models: localModelsByProvider.get(provider.id) ?? [],
|
|
360
|
+
source: "fallback",
|
|
361
|
+
authoritative: false,
|
|
362
|
+
error: error instanceof Error ? error.message : String(error),
|
|
363
|
+
});
|
|
364
|
+
return next;
|
|
234
365
|
});
|
|
235
|
-
}
|
|
366
|
+
});
|
|
236
367
|
}
|
|
237
|
-
void refreshRemote();
|
|
238
368
|
return () => {
|
|
239
|
-
|
|
369
|
+
if (requestGenerationRef.current === generation)
|
|
370
|
+
requestGenerationRef.current++;
|
|
240
371
|
};
|
|
241
|
-
}, [registry,
|
|
372
|
+
}, [registry, providerKey, refreshGeneration]);
|
|
242
373
|
const options = useMemo(() => {
|
|
243
|
-
|
|
244
|
-
return rawOptions;
|
|
245
|
-
const q = query.toLowerCase();
|
|
246
|
-
return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
|
|
374
|
+
return filterAndRankModelPickerOptions(rawOptions, query);
|
|
247
375
|
}, [rawOptions, query]);
|
|
376
|
+
const resolvedSelectedId = resolveSelectedModelId(options, selectedId);
|
|
377
|
+
const selectedIndex = resolvedSelectedId
|
|
378
|
+
? options.findIndex((option) => option.id === resolvedSelectedId)
|
|
379
|
+
: -1;
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (selectedId !== resolvedSelectedId)
|
|
382
|
+
setSelectedId(resolvedSelectedId);
|
|
383
|
+
}, [selectedId, resolvedSelectedId]);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
setPhase((currentPhase) => reconcileModelPickerPhase(currentPhase, rawOptions, currentThinkingLevel));
|
|
386
|
+
}, [rawOptions, currentThinkingLevel]);
|
|
387
|
+
const discoveryStatus = formatModelDiscoveryStatus(enabledProviders, discoveries);
|
|
248
388
|
useInput((input, key) => {
|
|
249
389
|
if (isKeyReleaseEvent(key))
|
|
250
390
|
return;
|
|
@@ -256,7 +396,9 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
256
396
|
return;
|
|
257
397
|
}
|
|
258
398
|
if (action === "enter") {
|
|
259
|
-
|
|
399
|
+
const level = levels[clampPickerIndex(phase.selectedIndex, levels.length)];
|
|
400
|
+
if (level)
|
|
401
|
+
onSelect(phase.model.id, level);
|
|
260
402
|
return;
|
|
261
403
|
}
|
|
262
404
|
if (action === "up") {
|
|
@@ -273,13 +415,17 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
273
415
|
}
|
|
274
416
|
return;
|
|
275
417
|
}
|
|
418
|
+
if (key.ctrl && (input.toLowerCase() === "r" || input === "\x12")) {
|
|
419
|
+
setRefreshGeneration((generation) => generation + 1);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
276
422
|
if (action === "escape") {
|
|
277
423
|
onCancel();
|
|
278
424
|
return;
|
|
279
425
|
}
|
|
280
426
|
if (action === "enter") {
|
|
281
|
-
const opt = options[
|
|
282
|
-
if (opt) {
|
|
427
|
+
const opt = selectedIndex >= 0 ? options[selectedIndex] : undefined;
|
|
428
|
+
if (opt && isModelOptionSelectable(opt)) {
|
|
283
429
|
if (shouldOpenEffortPicker(opt)) {
|
|
284
430
|
setPhase({
|
|
285
431
|
kind: "effort",
|
|
@@ -288,23 +434,25 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
288
434
|
});
|
|
289
435
|
}
|
|
290
436
|
else {
|
|
291
|
-
|
|
437
|
+
const level = opt.reasoningLevels[0];
|
|
438
|
+
if (level)
|
|
439
|
+
onSelect(opt.id, level);
|
|
292
440
|
}
|
|
293
441
|
}
|
|
294
442
|
return;
|
|
295
443
|
}
|
|
296
444
|
if (action === "up") {
|
|
297
|
-
|
|
445
|
+
setSelectedId((id) => moveModelSelection(options, id, -1));
|
|
298
446
|
return;
|
|
299
447
|
}
|
|
300
448
|
if (action === "down") {
|
|
301
|
-
|
|
449
|
+
setSelectedId((id) => moveModelSelection(options, id, 1));
|
|
302
450
|
return;
|
|
303
451
|
}
|
|
304
452
|
if (action === "backspace" || action === "delete") {
|
|
305
453
|
setQuery((q) => {
|
|
306
454
|
const next = q.slice(0, -1);
|
|
307
|
-
|
|
455
|
+
setSelectedId(undefined);
|
|
308
456
|
return next;
|
|
309
457
|
});
|
|
310
458
|
return;
|
|
@@ -312,40 +460,60 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
|
|
|
312
460
|
if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
|
|
313
461
|
setQuery((q) => {
|
|
314
462
|
const next = q + input;
|
|
315
|
-
|
|
463
|
+
setSelectedId(undefined);
|
|
316
464
|
return next;
|
|
317
465
|
});
|
|
318
466
|
return;
|
|
319
467
|
}
|
|
320
468
|
});
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
const
|
|
469
|
+
const displayItems = buildModelPickerDisplayItems(options);
|
|
470
|
+
const safeSelectedDisplayIndex = resolveModelPickerDisplayIndex(displayItems, resolvedSelectedId);
|
|
471
|
+
const start = pickerWindowStart(safeSelectedDisplayIndex, displayItems.length, bodyRows);
|
|
472
|
+
const visible = displayItems.slice(start, start + bodyRows);
|
|
324
473
|
const rawModelRows = options.length === 0
|
|
325
474
|
? [{
|
|
326
475
|
key: "no-results",
|
|
327
476
|
row: formatNoModelResultsRow(query, rowWidth),
|
|
328
477
|
selected: false,
|
|
478
|
+
header: false,
|
|
329
479
|
}]
|
|
330
|
-
: visible.map((
|
|
331
|
-
|
|
332
|
-
|
|
480
|
+
: visible.map((item) => {
|
|
481
|
+
if (item.kind === "header") {
|
|
482
|
+
return {
|
|
483
|
+
key: item.key,
|
|
484
|
+
row: padVisual(truncateVisual(` ${item.label}`, rowWidth), rowWidth),
|
|
485
|
+
selected: false,
|
|
486
|
+
header: true,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
const isSelected = item.option.id === resolvedSelectedId;
|
|
333
490
|
return {
|
|
334
|
-
key:
|
|
335
|
-
row: formatModelPickerRow(
|
|
491
|
+
key: item.key,
|
|
492
|
+
row: formatModelPickerRow(item.option, {
|
|
336
493
|
selected: isSelected,
|
|
337
|
-
current:
|
|
494
|
+
current: item.option.id === current,
|
|
338
495
|
width: rowWidth,
|
|
339
496
|
}),
|
|
340
497
|
selected: isSelected,
|
|
498
|
+
header: false,
|
|
341
499
|
};
|
|
342
500
|
});
|
|
343
501
|
const modelRows = padPickerRows(rawModelRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
|
|
344
502
|
key: rawModelRows[index]?.key ?? `blank-${index}`,
|
|
345
503
|
row,
|
|
346
504
|
selected: rawModelRows[index]?.selected ?? false,
|
|
505
|
+
header: rawModelRows[index]?.header ?? false,
|
|
347
506
|
}));
|
|
348
|
-
|
|
507
|
+
const highlightedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;
|
|
508
|
+
const enterAction = highlightedOption
|
|
509
|
+
? (shouldOpenEffortPicker(highlightedOption)
|
|
510
|
+
? (isThinkingToggleOption(highlightedOption) ? "choose mode" : "choose effort")
|
|
511
|
+
: "select")
|
|
512
|
+
: "wait";
|
|
513
|
+
const effortTitle = phase.kind === "effort" && isThinkingToggleOption(phase.model)
|
|
514
|
+
? "Select Thinking Mode"
|
|
515
|
+
: "Select Reasoning Effort";
|
|
516
|
+
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))) })] }));
|
|
349
517
|
}
|
|
350
518
|
function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
|
|
351
519
|
const theme = useTheme();
|
|
@@ -355,7 +523,7 @@ function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
|
|
|
355
523
|
row: formatEffortPickerRow(level, {
|
|
356
524
|
selected: index === safeSelectedIndex,
|
|
357
525
|
width: rowWidth,
|
|
358
|
-
asToggle:
|
|
526
|
+
asToggle: isThinkingToggleOption(model),
|
|
359
527
|
}),
|
|
360
528
|
selected: index === safeSelectedIndex,
|
|
361
529
|
}));
|
|
@@ -382,40 +550,124 @@ function SearchField({ query, placeholder, width }) {
|
|
|
382
550
|
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 })] }));
|
|
383
551
|
}
|
|
384
552
|
export function buildLocalModelOptions(registry, current, recent) {
|
|
385
|
-
const
|
|
553
|
+
const providers = registry.getEnabled().filter((provider) => isUserVisibleProvider(provider.id));
|
|
554
|
+
const localModelsByProvider = new Map(providers.map((provider) => [
|
|
555
|
+
provider.id,
|
|
556
|
+
localModelsForProvider(registry, provider),
|
|
557
|
+
]));
|
|
558
|
+
return buildMergedModelOptions({
|
|
559
|
+
providers,
|
|
560
|
+
localModelsByProvider,
|
|
561
|
+
discoveries: new Map(),
|
|
562
|
+
current,
|
|
563
|
+
recent,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
export function buildMergedModelOptions(input) {
|
|
386
567
|
const opts = [];
|
|
387
568
|
const seen = new Set();
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
569
|
+
const providerById = new Map(input.providers.map((provider) => [provider.id, provider]));
|
|
570
|
+
const effectiveModelsByProvider = new Map(input.providers.map((provider) => {
|
|
571
|
+
const localModels = input.localModelsByProvider.get(provider.id) ?? [];
|
|
572
|
+
const discovery = input.discoveries.get(provider.id);
|
|
573
|
+
return [provider.id, effectiveProviderModels(localModels, discovery)];
|
|
574
|
+
}));
|
|
575
|
+
const promotedFamilies = new Set();
|
|
576
|
+
const promotedRecentProviderIds = [];
|
|
577
|
+
const familyKeyForId = (id) => {
|
|
578
|
+
const { providerId, modelId } = decodeModel(id);
|
|
579
|
+
const provider = providerId ? providerById.get(providerId) : undefined;
|
|
580
|
+
return provider
|
|
581
|
+
? modelPickerFamilyKey(provider, modelId)
|
|
582
|
+
: `unknown:${id}`;
|
|
583
|
+
};
|
|
584
|
+
const appendPinned = (id, group) => {
|
|
585
|
+
appendModelOption(opts, seen, pinnedModelOption({
|
|
586
|
+
id,
|
|
587
|
+
group,
|
|
588
|
+
providers: input.providers,
|
|
589
|
+
localModelsByProvider: input.localModelsByProvider,
|
|
590
|
+
discoveries: input.discoveries,
|
|
591
|
+
}));
|
|
592
|
+
};
|
|
593
|
+
const appendFamily = (seedId, group) => {
|
|
594
|
+
const { providerId, modelId } = decodeModel(seedId);
|
|
595
|
+
const provider = providerId ? providerById.get(providerId) : undefined;
|
|
596
|
+
if (!provider) {
|
|
597
|
+
appendPinned(seedId, group);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const familyKey = modelPickerFamilyKey(provider, modelId);
|
|
601
|
+
const familyModels = (effectiveModelsByProvider.get(provider.id) ?? [])
|
|
602
|
+
.filter((model) => modelPickerFamilyKey(provider, model.id) === familyKey);
|
|
603
|
+
for (const model of familyModels) {
|
|
604
|
+
appendModelOption(opts, seen, {
|
|
605
|
+
...modelOptionMetadata(encodeModel(provider.id, model.id), model, true),
|
|
606
|
+
group,
|
|
607
|
+
providerBadge: provider.name,
|
|
608
|
+
available: true,
|
|
609
|
+
pending: false,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
// Keep an exact Current/Recent pin visible even when an authoritative
|
|
613
|
+
// catalog no longer contains it. It remains unavailable and never causes
|
|
614
|
+
// local siblings to be reintroduced.
|
|
615
|
+
if (!familyModels.some((model) => model.id === modelId))
|
|
616
|
+
appendPinned(seedId, group);
|
|
617
|
+
};
|
|
618
|
+
if (input.current) {
|
|
619
|
+
appendPinned(input.current, "Current");
|
|
620
|
+
const currentFamilyKey = familyKeyForId(input.current);
|
|
621
|
+
promotedFamilies.add(currentFamilyKey);
|
|
622
|
+
appendFamily(input.current, "Current");
|
|
397
623
|
}
|
|
398
|
-
|
|
399
|
-
|
|
624
|
+
let recentFamilyCount = 0;
|
|
625
|
+
for (const recentId of [...new Set(input.recent)]) {
|
|
626
|
+
if (!recentId || recentId === input.current)
|
|
627
|
+
continue;
|
|
628
|
+
const familyKey = familyKeyForId(recentId);
|
|
629
|
+
if (promotedFamilies.has(familyKey)) {
|
|
630
|
+
// A previously promoted family may not contain this exact Recent model
|
|
631
|
+
// in an authoritative catalog. Preserve that pin as unavailable; when
|
|
632
|
+
// it is present, exact-ID deduplication makes this a no-op.
|
|
633
|
+
appendPinned(recentId, "Recent");
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
promotedFamilies.add(familyKey);
|
|
637
|
+
recentFamilyCount += 1;
|
|
638
|
+
const { providerId } = decodeModel(recentId);
|
|
639
|
+
if (providerId && providerById.has(providerId) && !promotedRecentProviderIds.includes(providerId)) {
|
|
640
|
+
promotedRecentProviderIds.push(providerId);
|
|
641
|
+
}
|
|
642
|
+
appendFamily(recentId, "Recent");
|
|
643
|
+
if (recentFamilyCount >= MODEL_PICKER_RECENT_FAMILY_LIMIT)
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
const orderedProviderIds = [];
|
|
647
|
+
const addProviderId = (providerId) => {
|
|
648
|
+
if (providerId && providerById.has(providerId) && !orderedProviderIds.includes(providerId)) {
|
|
649
|
+
orderedProviderIds.push(providerId);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
addProviderId(decodeModel(input.current).providerId);
|
|
653
|
+
for (const providerId of promotedRecentProviderIds)
|
|
654
|
+
addProviderId(providerId);
|
|
655
|
+
for (const provider of input.providers)
|
|
656
|
+
addProviderId(provider.id);
|
|
657
|
+
for (const providerId of orderedProviderIds) {
|
|
658
|
+
const provider = providerById.get(providerId);
|
|
659
|
+
if (!provider)
|
|
660
|
+
continue;
|
|
661
|
+
for (const model of effectiveModelsByProvider.get(provider.id) ?? []) {
|
|
400
662
|
appendModelOption(opts, seen, {
|
|
401
|
-
|
|
402
|
-
label: model.name,
|
|
663
|
+
...modelOptionMetadata(encodeModel(provider.id, model.id), model, true),
|
|
403
664
|
group: provider.name,
|
|
404
665
|
providerBadge: provider.name,
|
|
666
|
+
available: true,
|
|
667
|
+
pending: false,
|
|
405
668
|
});
|
|
406
669
|
}
|
|
407
670
|
}
|
|
408
|
-
if (current && !seen.has(current)) {
|
|
409
|
-
const { providerId } = decodeModel(current);
|
|
410
|
-
const provider = enabled.find((item) => item.id === providerId);
|
|
411
|
-
opts.unshift({
|
|
412
|
-
id: current,
|
|
413
|
-
label: displayModel(current),
|
|
414
|
-
group: "Current",
|
|
415
|
-
providerBadge: provider?.name || providerId || "",
|
|
416
|
-
reasoningLevels: reasoningLevelsForModel(current),
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
671
|
return opts;
|
|
420
672
|
}
|
|
421
673
|
function localModelsForProvider(registry, provider) {
|
|
@@ -429,20 +681,104 @@ function localModelsForProvider(registry, provider) {
|
|
|
429
681
|
id: model.id,
|
|
430
682
|
name: model.name,
|
|
431
683
|
providerId: provider.id,
|
|
684
|
+
reasoningLevels: model.reasoningLevels,
|
|
685
|
+
defaultReasoningLevel: model.defaultReasoningLevel,
|
|
686
|
+
contextWindow: model.contextWindow,
|
|
687
|
+
toolOutputTokenLimit: model.toolOutputTokenLimit,
|
|
432
688
|
}));
|
|
433
689
|
}
|
|
434
690
|
function appendModelOption(options, seen, option) {
|
|
435
691
|
if (seen.has(option.id))
|
|
436
692
|
return;
|
|
437
693
|
seen.add(option.id);
|
|
438
|
-
options.push(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
});
|
|
694
|
+
options.push(option);
|
|
695
|
+
}
|
|
696
|
+
function pinnedModelOption(input) {
|
|
697
|
+
const { providerId, modelId } = decodeModel(input.id);
|
|
698
|
+
const provider = input.providers.find((item) => item.id === providerId);
|
|
699
|
+
if (!provider) {
|
|
700
|
+
return {
|
|
701
|
+
...modelOptionMetadata(input.id, undefined, false),
|
|
702
|
+
group: input.group,
|
|
703
|
+
providerBadge: providerId || "",
|
|
704
|
+
available: false,
|
|
705
|
+
pending: false,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const local = input.localModelsByProvider.get(provider.id) ?? [];
|
|
709
|
+
const discovery = input.discoveries.get(provider.id);
|
|
710
|
+
const discoveredModel = discovery?.models.find((model) => model.id === modelId);
|
|
711
|
+
const localModel = local.find((model) => model.id === modelId);
|
|
712
|
+
const metadata = discoveredModel ?? localModel;
|
|
713
|
+
const hasKnownMetadata = !!metadata;
|
|
714
|
+
let available = hasKnownMetadata;
|
|
715
|
+
let pending = false;
|
|
716
|
+
if (!discovery || discovery.status === "pending") {
|
|
717
|
+
pending = !hasKnownMetadata;
|
|
718
|
+
}
|
|
719
|
+
else if (discovery.authoritative) {
|
|
720
|
+
available = !!discoveredModel;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
...modelOptionMetadata(input.id, metadata, hasKnownMetadata),
|
|
724
|
+
group: input.group,
|
|
725
|
+
providerBadge: provider.name,
|
|
726
|
+
available,
|
|
727
|
+
pending,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function effectiveProviderModels(localModels, discovery) {
|
|
731
|
+
if (!discovery)
|
|
732
|
+
return localModels;
|
|
733
|
+
if (discovery.authoritative)
|
|
734
|
+
return discovery.models;
|
|
735
|
+
return discovery.models.length > 0 ? discovery.models : localModels;
|
|
736
|
+
}
|
|
737
|
+
function modelOptionMetadata(id, rawModel, known) {
|
|
738
|
+
const model = rawModel;
|
|
739
|
+
const reasoningLevels = model?.reasoningLevels
|
|
740
|
+
? [...model.reasoningLevels]
|
|
741
|
+
: (known ? reasoningLevelsForModel(id) : []);
|
|
742
|
+
const defaultReasoningLevel = model?.defaultReasoningLevel
|
|
743
|
+
&& reasoningLevels.includes(model.defaultReasoningLevel)
|
|
744
|
+
? model.defaultReasoningLevel
|
|
745
|
+
: undefined;
|
|
746
|
+
return {
|
|
747
|
+
id,
|
|
748
|
+
label: model?.name || displayModel(id),
|
|
749
|
+
reasoningLevels,
|
|
750
|
+
defaultReasoningLevel,
|
|
751
|
+
contextWindow: model?.contextWindow,
|
|
752
|
+
toolOutputTokenLimit: model?.toolOutputTokenLimit,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
async function discoverModelsForPicker(registry, provider, forceRefresh) {
|
|
756
|
+
return registry.discoverModels(provider, { forceRefresh });
|
|
757
|
+
}
|
|
758
|
+
export function formatModelDiscoveryStatus(providers, discoveries) {
|
|
759
|
+
if (providers.some((provider) => discoveries.get(provider.id)?.status !== "complete")) {
|
|
760
|
+
return { text: "Refreshing model catalog…", hasError: false };
|
|
761
|
+
}
|
|
762
|
+
const failed = providers.filter((provider) => discoveries.get(provider.id)?.error);
|
|
763
|
+
if (failed.length > 0) {
|
|
764
|
+
const names = failed.slice(0, 2).map((provider) => provider.name).join(", ");
|
|
765
|
+
const suffix = failed.length > 2 ? ` +${failed.length - 2}` : "";
|
|
766
|
+
return { text: `Showing fallback models for ${names}${suffix} · Ctrl+R retry`, hasError: true };
|
|
767
|
+
}
|
|
768
|
+
return { text: "", hasError: false };
|
|
442
769
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
770
|
+
function sameModelOptionMetadata(left, right) {
|
|
771
|
+
return left.id === right.id
|
|
772
|
+
&& left.label === right.label
|
|
773
|
+
&& left.group === right.group
|
|
774
|
+
&& left.providerBadge === right.providerBadge
|
|
775
|
+
&& left.defaultReasoningLevel === right.defaultReasoningLevel
|
|
776
|
+
&& left.contextWindow === right.contextWindow
|
|
777
|
+
&& left.toolOutputTokenLimit === right.toolOutputTokenLimit
|
|
778
|
+
&& left.available === right.available
|
|
779
|
+
&& left.pending === right.pending
|
|
780
|
+
&& left.reasoningLevels.length === right.reasoningLevels.length
|
|
781
|
+
&& left.reasoningLevels.every((level, index) => level === right.reasoningLevels[index]);
|
|
446
782
|
}
|
|
447
783
|
function reasoningLevelsForModel(model) {
|
|
448
784
|
const { providerId, modelId } = decodeModel(model);
|