ubume 0.1.0
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 +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Box, Text, useFocus, useInput } from "ink";
|
|
3
|
+
import {
|
|
4
|
+
type CodexModelCapability,
|
|
5
|
+
type ReasoningEffortCapability,
|
|
6
|
+
normalizeReasoningForModelCapabilities,
|
|
7
|
+
} from "../../core/models/codexModelCapabilities.js";
|
|
8
|
+
import { formatReasoningLabel } from "../../config/settings.js";
|
|
9
|
+
import { traceInputDebug } from "../../core/debug/inputDebug.js";
|
|
10
|
+
import { FOCUS_IDS } from "../input/focus.js";
|
|
11
|
+
import {
|
|
12
|
+
clampVisualText,
|
|
13
|
+
getShellWidth,
|
|
14
|
+
getAvailableRowsForPanel,
|
|
15
|
+
useAppLayoutBudget,
|
|
16
|
+
usePanelAvailableRows,
|
|
17
|
+
type Layout,
|
|
18
|
+
useActivePanelLayout,
|
|
19
|
+
type ActivePanelLayout,
|
|
20
|
+
type PanelLayout,
|
|
21
|
+
usePanelLayout,
|
|
22
|
+
} from "../layout.js";
|
|
23
|
+
import { calculateResponsivePickerViewport } from "./responsivePickerViewport.js";
|
|
24
|
+
import { useTheme } from "../theme.js";
|
|
25
|
+
import type { GeminiModelSelection } from "../../core/providerRuntime/types.js";
|
|
26
|
+
|
|
27
|
+
// ─── Types & helpers ─────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
type ModelPickerCloseReason = "escape" | "empty-selection";
|
|
30
|
+
type ModelRenderMode = "full" | "compact" | "windowed";
|
|
31
|
+
|
|
32
|
+
interface ModelPickerScreenProps {
|
|
33
|
+
layout: Layout & {
|
|
34
|
+
contentWidth?: number;
|
|
35
|
+
};
|
|
36
|
+
availableRows?: number;
|
|
37
|
+
activePanelLayout?: ActivePanelLayout;
|
|
38
|
+
panelLayout?: PanelLayout;
|
|
39
|
+
models: readonly CodexModelCapability[];
|
|
40
|
+
currentModel: string;
|
|
41
|
+
currentReasoning: string;
|
|
42
|
+
currentGeminiSelection?: GeminiModelSelection;
|
|
43
|
+
activeProviderLabel?: string;
|
|
44
|
+
isLoading?: boolean;
|
|
45
|
+
emptyMessage?: string;
|
|
46
|
+
routeTextOverride?: string;
|
|
47
|
+
onSelect: (model: string, reasoning: string, geminiSelection?: GeminiModelSelection) => void;
|
|
48
|
+
onCancel: (reason?: ModelPickerCloseReason) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getInitialCursor(models: readonly CodexModelCapability[], currentModel: string, currentGeminiSelection?: GeminiModelSelection): number {
|
|
52
|
+
if (currentGeminiSelection?.kind === "auto") {
|
|
53
|
+
const familyId = currentGeminiSelection.family === "gemini-3" ? "auto-gemini-3" : "auto-gemini-2.5";
|
|
54
|
+
const index = models.findIndex((m) => m.id === familyId);
|
|
55
|
+
if (index >= 0) return index;
|
|
56
|
+
}
|
|
57
|
+
const index = models.findIndex((model) =>
|
|
58
|
+
model.model === currentModel || model.id === currentModel || getVariantModelIds(model).includes(currentModel));
|
|
59
|
+
return Math.max(0, index);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getModelName(model: CodexModelCapability): string {
|
|
63
|
+
return model.label === model.model ? model.model : `${model.label} (${model.model})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getReasoningLevels(model: CodexModelCapability | undefined): readonly ReasoningEffortCapability[] {
|
|
67
|
+
return model?.supportedReasoningLevels ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const GEMINI_EFFORT_IDS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
71
|
+
|
|
72
|
+
function collapseGeminiEffortVariants(models: readonly CodexModelCapability[]): readonly CodexModelCapability[] {
|
|
73
|
+
const groups = new Map<string, CodexModelCapability>();
|
|
74
|
+
const variantIds = new Map<string, string[]>();
|
|
75
|
+
const variantLevels = new Map<string, Set<string>>();
|
|
76
|
+
|
|
77
|
+
for (const model of models) {
|
|
78
|
+
const match = model.model.match(/^(.*?)-(low|medium|high|xhigh|max)$/i);
|
|
79
|
+
if (!match || !GEMINI_EFFORT_IDS.has(match[2]!.toLowerCase())) {
|
|
80
|
+
groups.set(model.id, model);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const familyId = match[1]!;
|
|
85
|
+
const existing = groups.get(familyId);
|
|
86
|
+
const level = match[2]!.toLowerCase();
|
|
87
|
+
const ids = variantIds.get(familyId) ?? [];
|
|
88
|
+
ids.push(model.model);
|
|
89
|
+
variantIds.set(familyId, ids);
|
|
90
|
+
const levels = variantLevels.get(familyId) ?? new Set<string>();
|
|
91
|
+
levels.add(level);
|
|
92
|
+
variantLevels.set(familyId, levels);
|
|
93
|
+
|
|
94
|
+
const label = model.label.replace(/\s*\((?:low|medium|high|xhigh|max)\)\s*$/i, "");
|
|
95
|
+
if (!existing) {
|
|
96
|
+
groups.set(familyId, {
|
|
97
|
+
...model,
|
|
98
|
+
id: familyId,
|
|
99
|
+
model: familyId,
|
|
100
|
+
label,
|
|
101
|
+
description: `Select the intelligence level for ${label}.`,
|
|
102
|
+
defaultReasoningLevel: level,
|
|
103
|
+
supportedReasoningLevels: [{ id: level, label: formatReasoningLabel(level), description: null }],
|
|
104
|
+
reasoningLevelCount: 1,
|
|
105
|
+
raw: { ...(model.raw && typeof model.raw === "object" ? model.raw : {}), variantIds: ids },
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
const orderedLevels = ["low", "medium", "high", "xhigh", "max"].filter((id) => levels.has(id));
|
|
109
|
+
groups.set(familyId, {
|
|
110
|
+
...existing,
|
|
111
|
+
supportedReasoningLevels: orderedLevels.map((id) => ({ id, label: formatReasoningLabel(id), description: null })),
|
|
112
|
+
reasoningLevelCount: orderedLevels.length,
|
|
113
|
+
raw: { ...(existing.raw && typeof existing.raw === "object" ? existing.raw : {}), variantIds: ids },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return [...groups.values()].map((model) => {
|
|
119
|
+
const ids = variantIds.get(model.model);
|
|
120
|
+
if (!ids) return model;
|
|
121
|
+
return {
|
|
122
|
+
...model,
|
|
123
|
+
raw: { ...(model.raw && typeof model.raw === "object" ? model.raw : {}), variantIds: ids },
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getVariantModelIds(model: CodexModelCapability): readonly string[] {
|
|
129
|
+
const raw = model.raw;
|
|
130
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
131
|
+
const ids = (raw as { variantIds?: unknown }).variantIds;
|
|
132
|
+
return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getVariantReasoning(model: CodexModelCapability | undefined, modelId: string): string | null {
|
|
136
|
+
if (!model) return null;
|
|
137
|
+
const variant = getVariantModelIds(model).find((id) => id === modelId);
|
|
138
|
+
const match = variant?.match(/-(low|medium|high|xhigh|max)$/i);
|
|
139
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveVariantModelId(model: CodexModelCapability, reasoning: string): string {
|
|
143
|
+
const variantIds = getVariantModelIds(model);
|
|
144
|
+
return variantIds.find((id) => id.endsWith(`-${reasoning.toLowerCase()}`)) ?? model.model;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getModelSourceMarker(models: readonly CodexModelCapability[], activeProviderLabel: string): string | null {
|
|
148
|
+
if (activeProviderLabel !== "Claude" || models.length === 0) return null;
|
|
149
|
+
const raw = models[0]?.raw as { source?: string; discoveryKind?: string } | null | undefined;
|
|
150
|
+
const source = raw?.source;
|
|
151
|
+
const sourceLabel = source === "claude-code-package"
|
|
152
|
+
? "installed package metadata"
|
|
153
|
+
: source === "claude-code-command"
|
|
154
|
+
? "Claude Code command"
|
|
155
|
+
: source === "claude-code-cache"
|
|
156
|
+
? "Claude Code cache"
|
|
157
|
+
: source === "claude-code-config" || source === "settings" || source === "config"
|
|
158
|
+
? "Claude settings"
|
|
159
|
+
: "Claude Code";
|
|
160
|
+
if (source === "claude-code-package" || source === "claude-code-command" || source === "claude-code-cache" || source === "claude-code-config" || source === "claude-code" || source === "discovered") {
|
|
161
|
+
return raw?.discoveryKind === "aliases"
|
|
162
|
+
? `Claude Code aliases resolved from ${sourceLabel}`
|
|
163
|
+
: `Claude Code models discovered from ${sourceLabel}`;
|
|
164
|
+
}
|
|
165
|
+
if (source === "settings" || source === "config") return "Claude Code models discovered from Claude settings";
|
|
166
|
+
if (source === "fallback") return "Claude Code discovery failed; using fallback aliases";
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeDraftReasoning(
|
|
171
|
+
model: CodexModelCapability | undefined,
|
|
172
|
+
reasoning: string,
|
|
173
|
+
): string {
|
|
174
|
+
if (!model) return reasoning;
|
|
175
|
+
return normalizeReasoningForModelCapabilities(
|
|
176
|
+
model.model,
|
|
177
|
+
reasoning,
|
|
178
|
+
{
|
|
179
|
+
status: "ready",
|
|
180
|
+
source: model.source,
|
|
181
|
+
models: [model],
|
|
182
|
+
discoveredAt: Date.now(),
|
|
183
|
+
executable: null,
|
|
184
|
+
error: null,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getReasoningIndex(levels: readonly ReasoningEffortCapability[], reasoning: string): number {
|
|
190
|
+
return Math.max(0, levels.findIndex((level) => level.id === reasoning));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function describeInputKey(
|
|
194
|
+
input: string,
|
|
195
|
+
key: {
|
|
196
|
+
escape?: boolean;
|
|
197
|
+
return?: boolean;
|
|
198
|
+
upArrow?: boolean;
|
|
199
|
+
downArrow?: boolean;
|
|
200
|
+
leftArrow?: boolean;
|
|
201
|
+
rightArrow?: boolean;
|
|
202
|
+
ctrl?: boolean;
|
|
203
|
+
meta?: boolean;
|
|
204
|
+
},
|
|
205
|
+
) {
|
|
206
|
+
return {
|
|
207
|
+
input,
|
|
208
|
+
escape: Boolean(key.escape),
|
|
209
|
+
return: Boolean(key.return),
|
|
210
|
+
upArrow: Boolean(key.upArrow),
|
|
211
|
+
downArrow: Boolean(key.downArrow),
|
|
212
|
+
leftArrow: Boolean(key.leftArrow),
|
|
213
|
+
rightArrow: Boolean(key.rightArrow),
|
|
214
|
+
ctrl: Boolean(key.ctrl),
|
|
215
|
+
meta: Boolean(key.meta),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export function ModelPickerScreen({
|
|
222
|
+
layout,
|
|
223
|
+
availableRows: propAvailableRows,
|
|
224
|
+
activePanelLayout,
|
|
225
|
+
panelLayout,
|
|
226
|
+
models: baseModels,
|
|
227
|
+
currentModel,
|
|
228
|
+
currentReasoning,
|
|
229
|
+
currentGeminiSelection,
|
|
230
|
+
activeProviderLabel = "OpenAI",
|
|
231
|
+
isLoading = false,
|
|
232
|
+
emptyMessage,
|
|
233
|
+
routeTextOverride,
|
|
234
|
+
onSelect,
|
|
235
|
+
onCancel,
|
|
236
|
+
}: ModelPickerScreenProps) {
|
|
237
|
+
const theme = useTheme();
|
|
238
|
+
const isGoogle = activeProviderLabel === "Google";
|
|
239
|
+
const isAntigravity = activeProviderLabel.toLowerCase().includes("antigravity");
|
|
240
|
+
|
|
241
|
+
const models = useMemo(() => {
|
|
242
|
+
const collapsedModels = isAntigravity ? collapseGeminiEffortVariants(baseModels) : baseModels;
|
|
243
|
+
if (!isGoogle) return collapsedModels;
|
|
244
|
+
|
|
245
|
+
const autoModels: CodexModelCapability[] = [
|
|
246
|
+
{
|
|
247
|
+
id: "auto-gemini-3",
|
|
248
|
+
model: "gemini-3-flash-preview",
|
|
249
|
+
label: "Auto (Gemini 3)",
|
|
250
|
+
description: "Best available verified Gemini 3 model.",
|
|
251
|
+
available: true,
|
|
252
|
+
hidden: false,
|
|
253
|
+
isDefault: false,
|
|
254
|
+
defaultReasoningLevel: "medium",
|
|
255
|
+
supportedReasoningLevels: null,
|
|
256
|
+
reasoningLevelCount: null,
|
|
257
|
+
source: "fallback",
|
|
258
|
+
raw: { kind: "auto", family: "gemini-3" },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "auto-gemini-2.5",
|
|
262
|
+
model: "gemini-2.5-pro",
|
|
263
|
+
label: "Auto (Gemini 2.5)",
|
|
264
|
+
description: "Best available Gemini 2.5 model.",
|
|
265
|
+
available: true,
|
|
266
|
+
hidden: false,
|
|
267
|
+
isDefault: false,
|
|
268
|
+
defaultReasoningLevel: "high",
|
|
269
|
+
supportedReasoningLevels: null,
|
|
270
|
+
reasoningLevelCount: null,
|
|
271
|
+
source: "fallback",
|
|
272
|
+
raw: { kind: "auto", family: "gemini-2.5" },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
const manualModels = collapsedModels.map((m) => ({
|
|
277
|
+
...m,
|
|
278
|
+
label: `Manual: ${m.label}`,
|
|
279
|
+
raw: { kind: "manual", modelId: m.model },
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
return [...autoModels, ...manualModels];
|
|
283
|
+
}, [baseModels, isAntigravity, isGoogle]);
|
|
284
|
+
|
|
285
|
+
const { isFocused } = useFocus({ id: FOCUS_IDS.modelPicker, autoFocus: true });
|
|
286
|
+
const initialModelIndex = getInitialCursor(models, currentModel, currentGeminiSelection);
|
|
287
|
+
const [draftSelectedModel, setDraftSelectedModel] = useState(initialModelIndex);
|
|
288
|
+
const [draftReasoning, setDraftReasoning] = useState(() => normalizeDraftReasoning(
|
|
289
|
+
models[initialModelIndex],
|
|
290
|
+
getVariantReasoning(models[initialModelIndex], currentModel) ?? currentReasoning,
|
|
291
|
+
));
|
|
292
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
293
|
+
|
|
294
|
+
const selectedModel = models[draftSelectedModel];
|
|
295
|
+
const selectedReasoningLevels = getReasoningLevels(selectedModel);
|
|
296
|
+
const reasoningUnavailable = selectedReasoningLevels.length === 0;
|
|
297
|
+
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
traceInputDebug("model_picker_panel_mounted", {
|
|
300
|
+
focusTarget: FOCUS_IDS.modelPicker,
|
|
301
|
+
modelCount: models.length,
|
|
302
|
+
isLoading,
|
|
303
|
+
});
|
|
304
|
+
return () => {
|
|
305
|
+
traceInputDebug("model_picker_panel_unmounted", {
|
|
306
|
+
focusTarget: FOCUS_IDS.modelPicker,
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
}, []);
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (models.length === 0) {
|
|
313
|
+
setDraftSelectedModel(0);
|
|
314
|
+
setDraftReasoning(currentReasoning);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setDraftSelectedModel((current) => {
|
|
319
|
+
const nextCursor = Math.min(Math.max(0, current), models.length - 1);
|
|
320
|
+
const nextModel = models[nextCursor];
|
|
321
|
+
setDraftReasoning((reasoning) => normalizeDraftReasoning(
|
|
322
|
+
nextModel,
|
|
323
|
+
getVariantReasoning(nextModel, currentModel) ?? reasoning,
|
|
324
|
+
));
|
|
325
|
+
return nextCursor;
|
|
326
|
+
});
|
|
327
|
+
}, [currentModel, currentReasoning, models]);
|
|
328
|
+
|
|
329
|
+
const moveModel = (direction: -1 | 1) => {
|
|
330
|
+
setDraftSelectedModel((current) => {
|
|
331
|
+
const next = Math.max(0, Math.min(models.length - 1, current + direction));
|
|
332
|
+
const nextModel = models[next];
|
|
333
|
+
setDraftReasoning((reasoning) => normalizeDraftReasoning(nextModel, reasoning));
|
|
334
|
+
return next;
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const selectModelIndex = (index: number) => {
|
|
339
|
+
setDraftSelectedModel(() => {
|
|
340
|
+
const next = Math.max(0, Math.min(models.length - 1, index));
|
|
341
|
+
setDraftReasoning((reasoning) => normalizeDraftReasoning(models[next], reasoning));
|
|
342
|
+
return next;
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const moveReasoning = (direction: -1 | 1) => {
|
|
347
|
+
const levels = getReasoningLevels(models[draftSelectedModel]);
|
|
348
|
+
if (levels.length <= 1) return;
|
|
349
|
+
|
|
350
|
+
setDraftReasoning((current) => {
|
|
351
|
+
const currentIndex = getReasoningIndex(levels, current);
|
|
352
|
+
const nextIndex = Math.max(0, Math.min(levels.length - 1, currentIndex + direction));
|
|
353
|
+
return levels[nextIndex]?.id ?? current;
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
useInput(
|
|
358
|
+
(input, key) => {
|
|
359
|
+
traceInputDebug("model_picker_panel_input", {
|
|
360
|
+
handler: "ModelPickerScreen.useInput",
|
|
361
|
+
key: describeInputKey(input, key),
|
|
362
|
+
isFocused,
|
|
363
|
+
isLoading,
|
|
364
|
+
modelCount: models.length,
|
|
365
|
+
cursor: draftSelectedModel,
|
|
366
|
+
draftReasoning,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (key.ctrl && (input === "c" || input === "q")) {
|
|
370
|
+
onCancel("escape");
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (key.escape) {
|
|
375
|
+
onCancel("escape");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (key.return) {
|
|
380
|
+
const model = models[draftSelectedModel];
|
|
381
|
+
if (!model) {
|
|
382
|
+
onCancel("empty-selection");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const geminiSelection = isGoogle ? (model.raw as GeminiModelSelection) : undefined;
|
|
386
|
+
const normalizedReasoning = normalizeDraftReasoning(model, draftReasoning);
|
|
387
|
+
onSelect(resolveVariantModelId(model, normalizedReasoning), normalizedReasoning, geminiSelection);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (key.upArrow || input === "k") {
|
|
392
|
+
moveModel(-1);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (key.downArrow || input === "j") {
|
|
397
|
+
moveModel(1);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (key.home) {
|
|
402
|
+
selectModelIndex(0);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (key.end) {
|
|
407
|
+
selectModelIndex(models.length - 1);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (key.pageUp) {
|
|
412
|
+
selectModelIndex(draftSelectedModel - Math.max(1, windowResult.capacity));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (key.pageDown) {
|
|
417
|
+
selectModelIndex(draftSelectedModel + Math.max(1, windowResult.capacity));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (key.leftArrow || input === "h") {
|
|
422
|
+
moveReasoning(-1);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (key.rightArrow || input === "l") {
|
|
427
|
+
moveReasoning(1);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{ isActive: isFocused },
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const contextLayout = useActivePanelLayout();
|
|
434
|
+
const activeLayout = (activePanelLayout ?? contextLayout) as ActivePanelLayout | undefined;
|
|
435
|
+
|
|
436
|
+
const shellWidth = getShellWidth(layout.cols);
|
|
437
|
+
const hookPanelLayout = usePanelLayout();
|
|
438
|
+
const hookAvailableRows = usePanelAvailableRows();
|
|
439
|
+
const resolvedPanelLayout = useMemo<PanelLayout>(() => {
|
|
440
|
+
if (panelLayout) return panelLayout;
|
|
441
|
+
if (hookPanelLayout) return hookPanelLayout;
|
|
442
|
+
|
|
443
|
+
const mode = layout.mode;
|
|
444
|
+
const resolvedRows = activeLayout
|
|
445
|
+
? activeLayout.availableRows
|
|
446
|
+
: getAvailableRowsForPanel(layout, propAvailableRows ?? hookAvailableRows);
|
|
447
|
+
const resolvedCols = activeLayout
|
|
448
|
+
? activeLayout.availableCols
|
|
449
|
+
: Math.max(20, shellWidth - 4);
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
mode: (mode === "compact" || mode === "micro" as any) ? "compact" : mode === "expanded" || mode === "max" as any || mode === "wide" as any ? "expanded" : "regular",
|
|
453
|
+
availableRows: resolvedRows,
|
|
454
|
+
availableCols: resolvedCols,
|
|
455
|
+
};
|
|
456
|
+
}, [panelLayout, hookPanelLayout, layout, activeLayout, propAvailableRows, shellWidth, hookAvailableRows]);
|
|
457
|
+
|
|
458
|
+
const panelWidth = activeLayout
|
|
459
|
+
? activeLayout.width
|
|
460
|
+
: Math.max(38, Math.min((layout as any).contentWidth ?? shellWidth, shellWidth - 2));
|
|
461
|
+
|
|
462
|
+
const availableRows = resolvedPanelLayout.availableRows;
|
|
463
|
+
const innerWidth = Math.max(1, Math.min(resolvedPanelLayout.availableCols, panelWidth - 4));
|
|
464
|
+
const help = resolvedPanelLayout.mode === "compact"
|
|
465
|
+
? "↑↓ model · ←→ intelligence · Enter · Esc"
|
|
466
|
+
: "↑↓ model · ←→ reasoning · Enter select · Esc cancel";
|
|
467
|
+
const aOrAn = /^[aeiou]/i.test(activeProviderLabel) ? "an" : "a";
|
|
468
|
+
const routeText = routeTextOverride ?? `Choose ${aOrAn} ${activeProviderLabel} model to use inside Ubume.`;
|
|
469
|
+
const sourceMarker = getModelSourceMarker(models, activeProviderLabel);
|
|
470
|
+
|
|
471
|
+
const appLayoutBudget = useAppLayoutBudget();
|
|
472
|
+
|
|
473
|
+
const activeModelIndex = models.findIndex((model) =>
|
|
474
|
+
model.model === currentModel || model.id === currentModel || getVariantModelIds(model).includes(currentModel));
|
|
475
|
+
const hasSourceMarker = !!sourceMarker;
|
|
476
|
+
|
|
477
|
+
// ─── Layout & Windowing ───────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
const windowResult = useMemo(() => {
|
|
480
|
+
const allowFull = appLayoutBudget?.showPanelColumnHeaders ?? true;
|
|
481
|
+
|
|
482
|
+
// availableRows is already the shell's border-safe panel body. Only rows
|
|
483
|
+
// rendered inside that body belong in this calculation.
|
|
484
|
+
const fullChrome = 5 + (hasSourceMarker ? 1 : 0);
|
|
485
|
+
if (allowFull && models.length + fullChrome <= availableRows) {
|
|
486
|
+
return {
|
|
487
|
+
...calculateResponsivePickerViewport({ itemCount: models.length, selectedIndex: draftSelectedModel, availableRows, chromeRows: fullChrome, scrollOffset }),
|
|
488
|
+
mode: "full" as const,
|
|
489
|
+
showCurrentLine: false,
|
|
490
|
+
showRouteText: true,
|
|
491
|
+
showReasoningText: true,
|
|
492
|
+
showSourceMarker: hasSourceMarker,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Try fitting without source marker
|
|
497
|
+
if (allowFull && models.length + 5 <= availableRows) {
|
|
498
|
+
return {
|
|
499
|
+
...calculateResponsivePickerViewport({ itemCount: models.length, selectedIndex: draftSelectedModel, availableRows, chromeRows: 3, scrollOffset }),
|
|
500
|
+
mode: "full" as const,
|
|
501
|
+
showCurrentLine: false,
|
|
502
|
+
showRouteText: true,
|
|
503
|
+
showReasoningText: true,
|
|
504
|
+
showSourceMarker: false,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Try compact with the dedicated intelligence control
|
|
509
|
+
if (models.length + 4 <= availableRows) {
|
|
510
|
+
return {
|
|
511
|
+
...calculateResponsivePickerViewport({ itemCount: models.length, selectedIndex: draftSelectedModel, availableRows, chromeRows: 2, scrollOffset }),
|
|
512
|
+
mode: "compact" as const,
|
|
513
|
+
showCurrentLine: false,
|
|
514
|
+
showRouteText: false,
|
|
515
|
+
showReasoningText: true,
|
|
516
|
+
showSourceMarker: false,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Try minimal compact
|
|
521
|
+
if (models.length + 2 <= availableRows) {
|
|
522
|
+
return {
|
|
523
|
+
...calculateResponsivePickerViewport({ itemCount: models.length, selectedIndex: draftSelectedModel, availableRows, chromeRows: 1, scrollOffset }),
|
|
524
|
+
mode: "compact" as const,
|
|
525
|
+
showCurrentLine: false,
|
|
526
|
+
showRouteText: false,
|
|
527
|
+
showReasoningText: false,
|
|
528
|
+
showSourceMarker: false,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Windowed mode
|
|
533
|
+
let window = calculateResponsivePickerViewport({
|
|
534
|
+
itemCount: models.length,
|
|
535
|
+
selectedIndex: draftSelectedModel,
|
|
536
|
+
availableRows,
|
|
537
|
+
chromeRows: 1,
|
|
538
|
+
scrollOffset,
|
|
539
|
+
});
|
|
540
|
+
let showCurrentLine = activeModelIndex >= 0
|
|
541
|
+
&& (activeModelIndex < window.start || activeModelIndex >= window.end);
|
|
542
|
+
if (showCurrentLine) {
|
|
543
|
+
window = calculateResponsivePickerViewport({
|
|
544
|
+
itemCount: models.length,
|
|
545
|
+
selectedIndex: draftSelectedModel,
|
|
546
|
+
availableRows,
|
|
547
|
+
chromeRows: 2,
|
|
548
|
+
scrollOffset,
|
|
549
|
+
});
|
|
550
|
+
showCurrentLine = activeModelIndex < window.start || activeModelIndex >= window.end;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
...window,
|
|
555
|
+
mode: "windowed" as const,
|
|
556
|
+
showCurrentLine,
|
|
557
|
+
showRouteText: false,
|
|
558
|
+
showReasoningText: false,
|
|
559
|
+
showSourceMarker: false,
|
|
560
|
+
};
|
|
561
|
+
}, [models.length, draftSelectedModel, availableRows, hasSourceMarker, appLayoutBudget?.showPanelColumnHeaders, scrollOffset, activeModelIndex]);
|
|
562
|
+
|
|
563
|
+
useEffect(() => {
|
|
564
|
+
setScrollOffset(windowResult.start);
|
|
565
|
+
}, [windowResult.start]);
|
|
566
|
+
|
|
567
|
+
const visibleModels = useMemo(() => {
|
|
568
|
+
return models.slice(windowResult.start, windowResult.end);
|
|
569
|
+
}, [models, windowResult.start, windowResult.end]);
|
|
570
|
+
|
|
571
|
+
const activeModel = models[activeModelIndex];
|
|
572
|
+
|
|
573
|
+
const title = clampVisualText(
|
|
574
|
+
windowResult.mode === "windowed"
|
|
575
|
+
? `Models · ${windowResult.selectedIndex + 1}/${models.length}`
|
|
576
|
+
: `Select model ${help}`,
|
|
577
|
+
innerWidth,
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<Box flexDirection="column" width={panelWidth}>
|
|
583
|
+
<Box
|
|
584
|
+
borderStyle="round"
|
|
585
|
+
borderColor={theme.prompt}
|
|
586
|
+
paddingX={1}
|
|
587
|
+
paddingY={0}
|
|
588
|
+
width={panelWidth}
|
|
589
|
+
flexDirection="column"
|
|
590
|
+
>
|
|
591
|
+
<Box width="100%" overflow="hidden">
|
|
592
|
+
<Text color={theme.accent} bold>{title}</Text>
|
|
593
|
+
</Box>
|
|
594
|
+
{windowResult.showRouteText && (
|
|
595
|
+
<Box width="100%" overflow="hidden">
|
|
596
|
+
<Text color={theme.textMuted}>
|
|
597
|
+
{clampVisualText(routeText, innerWidth)}
|
|
598
|
+
</Text>
|
|
599
|
+
</Box>
|
|
600
|
+
)}
|
|
601
|
+
{windowResult.showReasoningText && (
|
|
602
|
+
<Box width="100%" overflow="hidden">
|
|
603
|
+
<Text color={reasoningUnavailable ? theme.textDim : theme.textMuted}>
|
|
604
|
+
{clampVisualText(
|
|
605
|
+
models.length === 0
|
|
606
|
+
? "Reasoning: current/default"
|
|
607
|
+
: reasoningUnavailable
|
|
608
|
+
? (isAntigravity ? "Uses this model's native AGY configuration" : "Reasoning: unavailable · Intelligence: unavailable")
|
|
609
|
+
: `Reasoning: ${formatReasoningLabel(draftReasoning)} · Intelligence: ${formatReasoningLabel(draftReasoning)}`,
|
|
610
|
+
innerWidth,
|
|
611
|
+
)}
|
|
612
|
+
</Text>
|
|
613
|
+
</Box>
|
|
614
|
+
)}
|
|
615
|
+
{windowResult.showSourceMarker && sourceMarker && (
|
|
616
|
+
<Box width="100%" overflow="hidden">
|
|
617
|
+
<Text color={theme.textDim}>
|
|
618
|
+
{clampVisualText(sourceMarker, innerWidth)}
|
|
619
|
+
</Text>
|
|
620
|
+
</Box>
|
|
621
|
+
)}
|
|
622
|
+
|
|
623
|
+
{windowResult.showCurrentLine && activeModel && (
|
|
624
|
+
<Box height={1} overflow="hidden">
|
|
625
|
+
<Text color={theme.textMuted} wrap="truncate">Current: <Text color={theme.text} bold>{clampVisualText(getModelName(activeModel), Math.max(1, innerWidth - 9))}</Text></Text>
|
|
626
|
+
</Box>
|
|
627
|
+
)}
|
|
628
|
+
|
|
629
|
+
<Box
|
|
630
|
+
flexDirection="column"
|
|
631
|
+
marginTop={0}
|
|
632
|
+
width="100%"
|
|
633
|
+
height={models.length > 0 ? visibleModels.length : undefined}
|
|
634
|
+
overflow={models.length > 0 ? "hidden" : undefined}
|
|
635
|
+
>
|
|
636
|
+
{models.length === 0 ? (
|
|
637
|
+
<Text color={theme.textMuted}>
|
|
638
|
+
{isLoading
|
|
639
|
+
? `Discovering models from ${activeProviderLabel === "OpenAI" ? "the Codex runtime" : activeProviderLabel}...`
|
|
640
|
+
: (emptyMessage ?? "No models available.")}
|
|
641
|
+
</Text>
|
|
642
|
+
) : (
|
|
643
|
+
visibleModels.map((model, index) => {
|
|
644
|
+
const actualIndex = windowResult.start + index;
|
|
645
|
+
return (
|
|
646
|
+
<ModelPickerRow
|
|
647
|
+
key={model.id}
|
|
648
|
+
model={model}
|
|
649
|
+
width={innerWidth}
|
|
650
|
+
currentModel={currentModel}
|
|
651
|
+
currentGeminiSelection={currentGeminiSelection}
|
|
652
|
+
isHighlighted={actualIndex === draftSelectedModel}
|
|
653
|
+
/>
|
|
654
|
+
);
|
|
655
|
+
})
|
|
656
|
+
)}
|
|
657
|
+
</Box>
|
|
658
|
+
|
|
659
|
+
{models.length > 0 && windowResult.mode !== "windowed" && (!isAntigravity || !reasoningUnavailable) && (
|
|
660
|
+
<IntelligenceSlider
|
|
661
|
+
levels={selectedReasoningLevels}
|
|
662
|
+
selected={draftReasoning}
|
|
663
|
+
width={innerWidth}
|
|
664
|
+
unavailable={reasoningUnavailable}
|
|
665
|
+
/>
|
|
666
|
+
)}
|
|
667
|
+
|
|
668
|
+
</Box>
|
|
669
|
+
</Box>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ─── Subcomponents ───────────────────────────────────────────────────────────
|
|
674
|
+
|
|
675
|
+
function ModelPickerRow({
|
|
676
|
+
model,
|
|
677
|
+
width,
|
|
678
|
+
currentModel,
|
|
679
|
+
currentGeminiSelection,
|
|
680
|
+
isHighlighted,
|
|
681
|
+
}: {
|
|
682
|
+
model: CodexModelCapability;
|
|
683
|
+
width: number;
|
|
684
|
+
currentModel: string;
|
|
685
|
+
currentGeminiSelection?: GeminiModelSelection;
|
|
686
|
+
isHighlighted: boolean;
|
|
687
|
+
}) {
|
|
688
|
+
const theme = useTheme();
|
|
689
|
+
|
|
690
|
+
let isCurrent = false;
|
|
691
|
+
if (currentGeminiSelection?.kind === "auto") {
|
|
692
|
+
isCurrent = (model.raw as GeminiModelSelection)?.kind === "auto" && (model.raw as any).family === currentGeminiSelection.family;
|
|
693
|
+
} else if (currentGeminiSelection?.kind === "manual") {
|
|
694
|
+
isCurrent = (model.raw as GeminiModelSelection)?.kind === "manual" && (model.raw as any).modelId === currentGeminiSelection.modelId;
|
|
695
|
+
} else {
|
|
696
|
+
isCurrent = model.model === currentModel || model.id === currentModel || getVariantModelIds(model).includes(currentModel);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const markerWidth = 2;
|
|
700
|
+
const checkWidth = 2;
|
|
701
|
+
const nameWidth = Math.max(8, width - markerWidth - checkWidth);
|
|
702
|
+
const name = clampVisualText(isHighlighted ? getModelName(model) : getCompactModelName(model), nameWidth);
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<Box width="100%" overflow="hidden">
|
|
706
|
+
<Box width={markerWidth} flexShrink={0}>
|
|
707
|
+
<Text color={isHighlighted ? theme.accent : theme.textDim}>{isHighlighted ? ">" : " "}</Text>
|
|
708
|
+
</Box>
|
|
709
|
+
<Box width={nameWidth} flexShrink={0} overflow="hidden">
|
|
710
|
+
<Text color={isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted} wrap="truncate">
|
|
711
|
+
{name}
|
|
712
|
+
</Text>
|
|
713
|
+
</Box>
|
|
714
|
+
<Box width={checkWidth} flexShrink={0}>
|
|
715
|
+
<Text color={theme.textDim}>{isCurrent ? "✓" : " "}</Text>
|
|
716
|
+
</Box>
|
|
717
|
+
</Box>
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function getCompactModelName(model: CodexModelCapability): string {
|
|
722
|
+
return model.label || model.model;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function IntelligenceSlider({
|
|
726
|
+
levels,
|
|
727
|
+
selected,
|
|
728
|
+
width,
|
|
729
|
+
unavailable,
|
|
730
|
+
}: {
|
|
731
|
+
levels: readonly ReasoningEffortCapability[];
|
|
732
|
+
selected: string;
|
|
733
|
+
width: number;
|
|
734
|
+
unavailable: boolean;
|
|
735
|
+
}) {
|
|
736
|
+
const theme = useTheme();
|
|
737
|
+
if (unavailable) {
|
|
738
|
+
return (
|
|
739
|
+
<Box marginTop={1} width="100%" overflow="hidden">
|
|
740
|
+
<Text color={theme.textDim}>Intelligence unavailable for this model</Text>
|
|
741
|
+
</Box>
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const selectedIndex = Math.max(0, levels.findIndex((level) => level.id === selected));
|
|
746
|
+
const trackWidth = Math.max(5, Math.min(24, width - 26));
|
|
747
|
+
const thumbPosition = levels.length <= 1
|
|
748
|
+
? 0
|
|
749
|
+
: Math.round((selectedIndex / (levels.length - 1)) * (trackWidth - 1));
|
|
750
|
+
const track = Array.from({ length: trackWidth }, (_, index) => index === thumbPosition ? "●" : "─").join("");
|
|
751
|
+
const low = formatReasoningLabel(levels[0]?.id ?? "low");
|
|
752
|
+
const high = formatReasoningLabel(levels[levels.length - 1]?.id ?? "high");
|
|
753
|
+
const value = formatReasoningLabel(levels[selectedIndex]?.id ?? selected);
|
|
754
|
+
const sliderText = `Intelligence ${low} ${track} ${high} ${value}`;
|
|
755
|
+
|
|
756
|
+
return (
|
|
757
|
+
<Box marginTop={1} width="100%" overflow="hidden">
|
|
758
|
+
<Text color={theme.textMuted} wrap="truncate">{clampVisualText(sliderText, width)}</Text>
|
|
759
|
+
</Box>
|
|
760
|
+
);
|
|
761
|
+
}
|