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.
Files changed (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,753 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+ import type { LocalBackendId, ProviderConfig, ProviderId, ProviderPickerAction } from "../../core/providerLauncher/types.js";
4
+ import { traceInputDebug } from "../../core/debug/inputDebug.js";
5
+ import { FOCUS_IDS } from "../input/focus.js";
6
+ import {
7
+ clampVisualText,
8
+ getShellWidth,
9
+ type Layout,
10
+ usePanelAvailableRows,
11
+ getAvailableRowsForPanel,
12
+ useAppLayoutBudget,
13
+ useActivePanelLayout,
14
+ type ActivePanelLayout,
15
+ type PanelLayout,
16
+ usePanelLayout,
17
+ } from "../layout.js";
18
+ import { calculateResponsivePickerViewport } from "./responsivePickerViewport.js";
19
+ import { useTheme } from "../theme.js";
20
+
21
+ // ─── Types & helpers ─────────────────────────────────────────────────────────
22
+
23
+ export interface LocalBackendStatus {
24
+ state: "idle" | "checking" | "ready" | "no-model" | "not-running" | "auth-required";
25
+ label: string;
26
+ }
27
+
28
+ interface ProviderPickerProps {
29
+ layout?: Layout;
30
+ providers: readonly ProviderConfig[];
31
+ onAction: (providerId: ProviderId, action: ProviderPickerAction, localBackend?: LocalBackendId) => void;
32
+ onCancel: () => void;
33
+ /** When set, the picker mounts directly at this provider's action panel. */
34
+ initialProviderId?: ProviderId;
35
+ activeLocalBackend?: LocalBackendId;
36
+ localBackendStatuses?: Partial<Record<LocalBackendId, LocalBackendStatus>>;
37
+ onLocalBackendsOpen?: () => void;
38
+ availableRows?: number;
39
+ activePanelLayout?: ActivePanelLayout;
40
+ panelLayout?: PanelLayout;
41
+ }
42
+
43
+ interface ProviderActionItem {
44
+ value: ProviderPickerAction;
45
+ label: string;
46
+ disabledReason?: string | null;
47
+ }
48
+
49
+ type ProviderPickerMode = "providers" | "codexa-native-models" | "local-backends";
50
+
51
+ const CODEXA_NATIVE_PROVIDER_IDS = new Set<ProviderId>(["codexa-native", "codexa-cupy"]);
52
+
53
+ function isCodexaNativeProvider(provider: ProviderConfig): boolean {
54
+ return CODEXA_NATIVE_PROVIDER_IDS.has(provider.id);
55
+ }
56
+
57
+ export function groupCodexaNativeProviders(providers: readonly ProviderConfig[]): ProviderConfig[] {
58
+ const nativeProviders = providers.filter(isCodexaNativeProvider);
59
+ if (nativeProviders.length === 0) return [...providers];
60
+
61
+ const primary = nativeProviders.find((provider) => provider.id === "codexa-native") ?? nativeProviders[0]!;
62
+ const active = nativeProviders.find((provider) => provider.isActiveRoute);
63
+ const defaultProvider = nativeProviders.find((provider) => provider.isDefault);
64
+ const grouped: ProviderConfig = {
65
+ ...primary,
66
+ displayName: "Codexa Native",
67
+ currentModel: (active ?? defaultProvider ?? primary).currentModel,
68
+ enabled: nativeProviders.some((provider) => provider.enabled),
69
+ isDefault: nativeProviders.some((provider) => provider.isDefault),
70
+ isActiveRoute: nativeProviders.some((provider) => provider.isActiveRoute),
71
+ routeUnavailableReason: nativeProviders.every((provider) => provider.routeUnavailableReason)
72
+ ? nativeProviders[0]?.routeUnavailableReason ?? null
73
+ : null,
74
+ };
75
+
76
+ const firstNativeIndex = providers.findIndex(isCodexaNativeProvider);
77
+ return providers.filter((provider, index) => {
78
+ if (!isCodexaNativeProvider(provider)) return true;
79
+ return index === firstNativeIndex;
80
+ }).map((provider) => isCodexaNativeProvider(provider) ? grouped : provider);
81
+ }
82
+
83
+ export function getCodexaNativeModelProviders(providers: readonly ProviderConfig[]): ProviderConfig[] {
84
+ return providers.filter(isCodexaNativeProvider).map((provider) => ({
85
+ ...provider,
86
+ displayName: provider.id === "codexa-native" ? "Codexa PyTorch" : "Codexa CuPy",
87
+ }));
88
+ }
89
+
90
+ function clampIndex(index: number, length: number): number {
91
+ if (length <= 0) return 0;
92
+ return Math.max(0, Math.min(length - 1, index));
93
+ }
94
+
95
+ export function getTableLayout(innerWidth: number, isMicro = false) {
96
+ if (innerWidth < 70) {
97
+ const columnWidthBudget = Math.max(26, innerWidth - 5 - 4);
98
+ const status = Math.min(8, Math.max(6, columnWidthBudget - 20));
99
+ const tool = innerWidth < 65 ? 0 : 4;
100
+ const stream = innerWidth < 65 ? 0 : 4;
101
+ const context = innerWidth < 45 ? 0 : Math.min(11, Math.max(10, columnWidthBudget - status - (tool || 4) - (stream || 4) - 16));
102
+ const provider = Math.min(isMicro ? 10 : 14, Math.max(8, columnWidthBudget - status - tool - stream - context - 8));
103
+ const model = Math.max(5, columnWidthBudget - provider - context - tool - stream - status);
104
+ const trailingPadding = 0;
105
+ return { provider, model, context, tool, stream, status, trailingPadding };
106
+ }
107
+
108
+ const provider = Math.min(22, Math.max(12, Math.floor(innerWidth * 0.18)));
109
+ const status = Math.min(20, Math.max(12, Math.floor(innerWidth * 0.15)));
110
+ const context = Math.min(14, Math.max(11, Math.floor(innerWidth * 0.12)));
111
+ const tool = 4;
112
+ const stream = 4;
113
+ const fixed = 5 + provider + 1 + context + 1 + tool + 1 + stream + 1 + status;
114
+ const model = Math.max(10, innerWidth - fixed - 3);
115
+ const trailingPadding = Math.max(0, innerWidth - (fixed + 1 + model + 1));
116
+
117
+ return {
118
+ provider,
119
+ model,
120
+ context,
121
+ tool,
122
+ stream,
123
+ status,
124
+ trailingPadding
125
+ };
126
+ }
127
+
128
+ // ─── Component ────────────────────────────────────────────────────────────────
129
+
130
+ export function ProviderPicker({
131
+ layout,
132
+ providers,
133
+ onAction,
134
+ onCancel,
135
+ initialProviderId,
136
+ activeLocalBackend = "lm-studio",
137
+ localBackendStatuses = {},
138
+ onLocalBackendsOpen = () => undefined,
139
+ availableRows: propAvailableRows,
140
+ activePanelLayout,
141
+ panelLayout,
142
+ }: ProviderPickerProps) {
143
+ const theme = useTheme();
144
+ const budget = useAppLayoutBudget();
145
+ const { isFocused } = useFocus({ id: FOCUS_IDS.providerPicker, autoFocus: true });
146
+ const groupedProviders = useMemo(() => groupCodexaNativeProviders(providers), [providers]);
147
+ const codexaNativeModels = useMemo(() => getCodexaNativeModelProviders(providers), [providers]);
148
+ const localBackends = useMemo<ProviderConfig[]>(() => {
149
+ const local = providers.find((provider) => provider.id === "local");
150
+ if (!local) return [];
151
+ return [
152
+ { ...local, displayName: "LM Studio", currentModel: activeLocalBackend === "lm-studio" ? local.currentModel : "Local server" },
153
+ { ...local, displayName: "Unsloth", currentModel: activeLocalBackend === "unsloth" ? local.currentModel : "Studio server" },
154
+ ];
155
+ }, [activeLocalBackend, providers]);
156
+ const initialIndex = initialProviderId
157
+ ? Math.max(0, groupedProviders.findIndex((provider) =>
158
+ initialProviderId === "codexa-cupy"
159
+ ? provider.id === "codexa-native"
160
+ : provider.id === initialProviderId))
161
+ : 0;
162
+ const [providerIndex, setProviderIndex] = useState(initialIndex);
163
+ const [mode, setMode] = useState<ProviderPickerMode>("providers");
164
+ const [scrollOffset, setScrollOffset] = useState(0);
165
+
166
+ const pickerProviders = mode === "codexa-native-models"
167
+ ? codexaNativeModels
168
+ : mode === "local-backends" ? localBackends : groupedProviders;
169
+ const selectedLocalBackend: LocalBackendId = providerIndex === 1 ? "unsloth" : "lm-studio";
170
+
171
+ const contextLayout = useActivePanelLayout();
172
+ const activeLayout = (activePanelLayout ?? contextLayout) as ActivePanelLayout | undefined;
173
+
174
+ const selectedProvider = pickerProviders[clampIndex(providerIndex, pickerProviders.length)];
175
+ const shellWidth = getShellWidth(layout?.cols ?? 120);
176
+ const panelWidth = activeLayout
177
+ ? activeLayout.width
178
+ : Math.max(
179
+ 42,
180
+ Math.min((layout as any)?.contentWidth ?? (shellWidth - 2), shellWidth - 2)
181
+ );
182
+
183
+ const hookPanelLayout = usePanelLayout();
184
+ const hookAvailableRows = usePanelAvailableRows();
185
+ const resolvedPanelLayout = useMemo<PanelLayout>(() => {
186
+ if (panelLayout) return panelLayout;
187
+ if (hookPanelLayout) return hookPanelLayout;
188
+
189
+ const mode = layout?.mode ?? "regular";
190
+ const resolvedRows = activeLayout
191
+ ? activeLayout.availableRows
192
+ : getAvailableRowsForPanel(layout || { cols: 120, rows: 24, mode: "regular" }, propAvailableRows ?? hookAvailableRows);
193
+ const resolvedCols = activeLayout
194
+ ? activeLayout.availableCols
195
+ : Math.max(30, shellWidth - 4);
196
+
197
+ return {
198
+ mode: (mode === "compact" || mode === "micro" as any) ? "compact" : mode === "expanded" || mode === "max" as any || mode === "wide" as any ? "expanded" : "regular",
199
+ availableRows: resolvedRows,
200
+ availableCols: resolvedCols,
201
+ };
202
+ }, [panelLayout, hookPanelLayout, layout, activeLayout, propAvailableRows, shellWidth, hookAvailableRows]);
203
+
204
+ const availableRows = resolvedPanelLayout.availableRows;
205
+ const innerWidth = Math.max(1, Math.min(resolvedPanelLayout.availableCols, panelWidth - 4));
206
+
207
+ const isCompactLayout = resolvedPanelLayout.mode === "compact";
208
+
209
+ // Compact columns widths:
210
+ const markerWidth = 5;
211
+ const compactContextWidth = innerWidth >= 90 ? 6 : 5;
212
+ const compactStatusWidth = innerWidth >= 90 ? 8 : 6;
213
+ const compactProviderWidth = Math.max(11, Math.min(14, Math.floor(innerWidth * 0.15)));
214
+ const spacingWidth = 4;
215
+ const fixedWidth = markerWidth + compactProviderWidth + compactContextWidth + compactStatusWidth + spacingWidth;
216
+ const compactModelWidth = Math.max(12, innerWidth - fixedWidth);
217
+ const compactWidths = {
218
+ markerWidth,
219
+ providerWidth: compactProviderWidth,
220
+ modelWidth: compactModelWidth,
221
+ contextWidth: compactContextWidth,
222
+ statusWidth: compactStatusWidth
223
+ };
224
+
225
+ // Regular columns:
226
+ const cols = getTableLayout(innerWidth, false);
227
+ const providerNameWidth = cols.provider;
228
+ const modelWidth = cols.model;
229
+ const contextWidth = cols.context;
230
+ const toolsWidth = cols.tool;
231
+ const streamWidth = cols.stream;
232
+ const statusWidth = cols.status;
233
+
234
+ const helpText = mode === "codexa-native-models"
235
+ ? "Enter use · Esc back"
236
+ : mode === "local-backends" ? "Enter use · Esc back"
237
+ : "Enter use · Esc close";
238
+
239
+ const openCodexaNativeModels = () => {
240
+ const selectedNativeIndex = codexaNativeModels.findIndex((provider) => provider.isActiveRoute || provider.isDefault);
241
+ setMode("codexa-native-models");
242
+ setProviderIndex(Math.max(0, selectedNativeIndex));
243
+ setScrollOffset(0);
244
+ };
245
+
246
+ const openLocalBackends = () => {
247
+ setMode("local-backends");
248
+ setProviderIndex(activeLocalBackend === "unsloth" ? 1 : 0);
249
+ setScrollOffset(0);
250
+ onLocalBackendsOpen();
251
+ };
252
+
253
+ const actions = useMemo<ProviderActionItem[]>(() => {
254
+ const routeUnavailable = selectedProvider?.routeMode === "in-ubume";
255
+ const disabledReason = routeUnavailable
256
+ ? null
257
+ : selectedProvider?.routeUnavailableReason ?? "In-Ubume routing is not configured yet.";
258
+
259
+ return [
260
+ { value: "use-in-ubume", label: "Use in Ubume", disabledReason },
261
+ { value: "select-model", label: "Select model", disabledReason },
262
+ { value: "refresh-models", label: selectedProvider?.id === "anthropic" ? "Refresh Claude capabilities" : selectedProvider?.id === "local" ? "Refresh LM Studio metadata" : "Refresh models", disabledReason },
263
+ ...(selectedProvider?.id === "google" || selectedProvider?.id === "local"
264
+ ? [{ value: "run-diagnostics" as const, label: selectedProvider.id === "local" ? "Run Local diagnostics" : "Run Gemini diagnostics" }]
265
+ : []),
266
+ { value: "launch", label: "Launch external CLI" },
267
+ { value: "set-default", label: "Set as workspace default" },
268
+ { value: "cancel", label: "Cancel" },
269
+ ];
270
+ }, [selectedProvider]);
271
+
272
+ useInput((input, key) => {
273
+ traceInputDebug("provider_picker_input", {
274
+ handler: "ProviderPicker.useInput",
275
+ input,
276
+ return: Boolean(key.return),
277
+ escape: Boolean(key.escape),
278
+ upArrow: Boolean(key.upArrow),
279
+ downArrow: Boolean(key.downArrow),
280
+ mode,
281
+ providerIndex,
282
+ actionIndex: null,
283
+ });
284
+
285
+ if (key.ctrl && (input === "c" || input === "q")) {
286
+ onCancel();
287
+ return;
288
+ }
289
+
290
+ if (key.escape) {
291
+ if (mode === "codexa-native-models" || mode === "local-backends") {
292
+ const groupedIndex = groupedProviders.findIndex((provider) => provider.id === (mode === "local-backends" ? "local" : "codexa-native"));
293
+ setMode("providers");
294
+ setProviderIndex(Math.max(0, groupedIndex));
295
+ setScrollOffset(0);
296
+ return;
297
+ }
298
+ onCancel();
299
+ return;
300
+ }
301
+
302
+ if (mode === "providers" || mode === "codexa-native-models" || mode === "local-backends") {
303
+ if (key.home) {
304
+ setProviderIndex(0);
305
+ return;
306
+ }
307
+ if (key.end) {
308
+ setProviderIndex(Math.max(0, pickerProviders.length - 1));
309
+ return;
310
+ }
311
+ if (key.pageUp) {
312
+ setProviderIndex((current) => clampIndex(current - Math.max(1, windowResult?.capacity ?? 1), pickerProviders.length));
313
+ return;
314
+ }
315
+ if (key.pageDown) {
316
+ setProviderIndex((current) => clampIndex(current + Math.max(1, windowResult?.capacity ?? 1), pickerProviders.length));
317
+ return;
318
+ }
319
+ if (key.upArrow || input === "k") {
320
+ setProviderIndex((current) => clampIndex(current - 1, pickerProviders.length));
321
+ return;
322
+ }
323
+ if (key.downArrow || input === "j") {
324
+ setProviderIndex((current) => clampIndex(current + 1, pickerProviders.length));
325
+ return;
326
+ }
327
+ if (input.toLowerCase() === "u" && selectedProvider) {
328
+ if (mode === "providers" && selectedProvider.id === "codexa-native" && codexaNativeModels.length > 0) {
329
+ openCodexaNativeModels();
330
+ return;
331
+ }
332
+ if (mode === "providers" && selectedProvider.id === "local" && localBackends.length > 0) {
333
+ openLocalBackends();
334
+ return;
335
+ }
336
+ onAction(selectedProvider.id, "use-in-ubume", mode === "local-backends" ? selectedLocalBackend : undefined);
337
+ return;
338
+ }
339
+ if (input.toLowerCase() === "s" && selectedProvider) {
340
+ if (mode === "providers" && selectedProvider.id === "codexa-native" && codexaNativeModels.length > 0) {
341
+ openCodexaNativeModels();
342
+ return;
343
+ }
344
+ onAction(selectedProvider.id, "set-default");
345
+ return;
346
+ }
347
+ if (key.return && selectedProvider) {
348
+ if (mode === "providers" && selectedProvider.id === "codexa-native" && codexaNativeModels.length > 0) {
349
+ openCodexaNativeModels();
350
+ return;
351
+ }
352
+ if (mode === "providers" && selectedProvider.id === "local" && localBackends.length > 0) {
353
+ openLocalBackends();
354
+ return;
355
+ }
356
+ onAction(selectedProvider.id, "use-in-ubume", mode === "local-backends" ? selectedLocalBackend : undefined);
357
+ }
358
+ return;
359
+ }
360
+ }, { isActive: isFocused });
361
+
362
+ // ─── Layout & Windowing ───────────────────────────────────────────────────
363
+
364
+ const activeRouteIndex = pickerProviders.findIndex((provider) => provider.isActiveRoute);
365
+
366
+ const windowResult = useMemo(() => {
367
+ const showBorder = availableRows >= 3;
368
+ const showTitle = availableRows >= 2;
369
+ const useCompactRows = true;
370
+ const showHeaders = false;
371
+ const chromeRows = (showTitle ? 1 : 0) + (showHeaders ? 1 : 0);
372
+ let viewport = calculateResponsivePickerViewport({
373
+ itemCount: pickerProviders.length,
374
+ selectedIndex: providerIndex,
375
+ availableRows,
376
+ chromeRows,
377
+ scrollOffset,
378
+ });
379
+ let reserveCurrent = viewport.hasOverflow
380
+ && activeRouteIndex >= 0
381
+ && (activeRouteIndex < viewport.start || activeRouteIndex >= viewport.end);
382
+ if (reserveCurrent) {
383
+ viewport = calculateResponsivePickerViewport({
384
+ itemCount: pickerProviders.length,
385
+ selectedIndex: providerIndex,
386
+ availableRows,
387
+ chromeRows: chromeRows + 1,
388
+ scrollOffset,
389
+ });
390
+ reserveCurrent = activeRouteIndex < viewport.start || activeRouteIndex >= viewport.end;
391
+ }
392
+ return {
393
+ ...viewport,
394
+ visibleCount: viewport.end - viewport.start,
395
+ showAbove: false,
396
+ showBelow: false,
397
+ showRange: viewport.hasOverflow,
398
+ showBorder,
399
+ showTitle,
400
+ showHeaders,
401
+ reserveCurrent,
402
+ renderMode: useCompactRows ? ("compact" as const) : viewport.hasOverflow ? ("windowed" as const) : ("full" as const),
403
+ };
404
+ }, [pickerProviders.length, providerIndex, availableRows, resolvedPanelLayout.mode, scrollOffset, activeRouteIndex]);
405
+
406
+ useEffect(() => {
407
+ setProviderIndex((current) => clampIndex(current, pickerProviders.length));
408
+ }, [pickerProviders.length]);
409
+
410
+ useEffect(() => {
411
+ if (windowResult) setScrollOffset(windowResult.start);
412
+ }, [windowResult?.start]);
413
+
414
+ const visibleProviders = useMemo(() => {
415
+ if (!windowResult) return pickerProviders;
416
+ return pickerProviders.slice(windowResult.start, windowResult.end);
417
+ }, [pickerProviders, windowResult]);
418
+
419
+ const titleText = (windowResult?.showRange)
420
+ ? `${mode === "codexa-native-models" ? "Codexa Native Models" : mode === "local-backends" ? "Local Backends" : "Providers"} · ${windowResult.selectedIndex + 1}/${pickerProviders.length}`
421
+ : mode === "codexa-native-models" ? "Codexa Native Models" : mode === "local-backends" ? "Local Backends" : "Providers";
422
+ const showCurrent = false;
423
+
424
+ const body = useMemo(() => {
425
+ return visibleProviders.map((provider, index) => (
426
+ <ProviderRowSimple
427
+ key={`${mode}-${provider.id}-${provider.displayName}`}
428
+ provider={provider}
429
+ isHighlighted={(windowResult!.start + index) === providerIndex}
430
+ width={innerWidth}
431
+ secondaryText={mode === "local-backends"
432
+ ? localBackendStatuses[windowResult!.start + index === 1 ? "unsloth" : "lm-studio"]?.label ?? "Checking…"
433
+ : undefined}
434
+ secondaryState={mode === "local-backends"
435
+ ? localBackendStatuses[windowResult!.start + index === 1 ? "unsloth" : "lm-studio"]?.state
436
+ : undefined}
437
+ />
438
+ ));
439
+ }, [innerWidth, localBackendStatuses, mode, providerIndex, visibleProviders, windowResult?.start]);
440
+
441
+ const showBorder = windowResult?.showBorder ?? true;
442
+
443
+ return (
444
+ <Box flexDirection="column" width={panelWidth} flexShrink={0}>
445
+ <Box
446
+ borderStyle={showBorder ? "round" : undefined}
447
+ borderColor={theme.prompt}
448
+ paddingX={showBorder ? 1 : 0}
449
+ paddingY={0}
450
+ width={panelWidth}
451
+ flexDirection="column"
452
+ flexShrink={0}
453
+ >
454
+ {(windowResult?.showTitle ?? true) && (
455
+ <Box width="100%" overflow="hidden" flexShrink={0}>
456
+ <Text color={theme.accent} bold>
457
+ {clampVisualText(showBorder ? `${titleText} ${helpText}` : titleText, innerWidth)}
458
+ </Text>
459
+ </Box>
460
+ )}
461
+
462
+ {!(windowResult?.showTitle ?? true) && windowResult?.showRange && (
463
+ <Box width="100%" overflow="hidden" flexShrink={0}>
464
+ <Text color={theme.accent}>
465
+ {mode === "codexa-native-models" ? "Codexa Native Models" : mode === "local-backends" ? "Local Backends" : "Providers"} · {windowResult.selectedIndex + 1}/{pickerProviders.length}
466
+ </Text>
467
+ </Box>
468
+ )}
469
+
470
+ {windowResult?.showAbove && (
471
+ <Box height={1} overflow="hidden" flexShrink={0}>
472
+ <Text color={theme.accent}>{isCompactLayout ? "↑ more" : `↑ ${windowResult.start} more`}</Text>
473
+ </Box>
474
+ )}
475
+
476
+ {(windowResult?.showHeaders ?? true) && (
477
+ <Box width="100%" overflow="hidden" flexShrink={0}>
478
+ <Text color={theme.textDim}>
479
+ {" "}
480
+ {clampVisualText("Provider", providerNameWidth)}
481
+ {" "}
482
+ {clampVisualText("Model", modelWidth)}
483
+ {contextWidth > 0 && " " + clampVisualText("Context", contextWidth)}
484
+ {toolsWidth > 0 && " " + clampVisualText("Tool", toolsWidth)}
485
+ {streamWidth > 0 && " " + clampVisualText("Strm", streamWidth)}
486
+ {" "}
487
+ {clampVisualText("Status", statusWidth)}
488
+ </Text>
489
+ </Box>
490
+ )}
491
+
492
+ <Box flexDirection="column" marginTop={0} width="100%" flexShrink={0}>
493
+ {body}
494
+ </Box>
495
+
496
+ {showCurrent && (
497
+ <Box height={1} overflow="hidden" flexShrink={0}>
498
+ <Text color={theme.textDim}>
499
+ Current: <Text color={theme.text}>{clampVisualText(`${pickerProviders[activeRouteIndex]!.displayName} / ${pickerProviders[activeRouteIndex]!.currentModel}`, Math.max(1, innerWidth - 9))}</Text>
500
+ </Text>
501
+ </Box>
502
+ )}
503
+
504
+ {windowResult?.showBelow && (
505
+ <Box height={1} overflow="hidden" flexShrink={0}>
506
+ <Text color={theme.accent}>{isCompactLayout ? "↓ more" : `↓ ${pickerProviders.length - windowResult.end} more`}</Text>
507
+ </Box>
508
+ )}
509
+ </Box>
510
+
511
+ {process.env.UBUME_DEBUG_LAYOUT === "1" && (
512
+ <Box flexDirection="column" marginTop={1} flexShrink={0}>
513
+ <Text color="red">
514
+ DEBUG layout: rows={layout?.rows} cols={layout?.cols} mode={layout?.mode} headerRows={budget?.headerRows ?? 6} panelRows={availableRows} bottomChromeRows={budget?.bottomChromeBudget.totalRows ?? 4} composerRows={budget?.composerRows ?? 3} providerRows={visibleProviders.length} renderMode={windowResult?.renderMode}
515
+ </Text>
516
+ </Box>
517
+ )}
518
+ </Box>
519
+ );
520
+ }
521
+
522
+ // ─── Subcomponents ───────────────────────────────────────────────────────────
523
+
524
+ function capabilityFlag(value: boolean | null | undefined): string {
525
+ if (value === true) return "Y";
526
+ if (value === false) return "N";
527
+ return "?";
528
+ }
529
+
530
+ function formatCompactStatus(status: string | undefined): string {
531
+ switch (status) {
532
+ case "Active":
533
+ return "Active";
534
+ case "Enabled":
535
+ return "Ready";
536
+ case "Needs config":
537
+ return "Config";
538
+ case "Disabled":
539
+ return "Off";
540
+ case "Unknown":
541
+ default:
542
+ return "?";
543
+ }
544
+ }
545
+
546
+ function formatCompactContext(value: string | number | undefined): string {
547
+ if (value === undefined || value === null) return "?";
548
+ const raw = String(value).trim();
549
+ if (!raw || raw === "Unknown" || raw === "?") return "?";
550
+ const numeric = Number(raw.replace(/,/g, ""));
551
+ if (!Number.isFinite(numeric)) return raw;
552
+ if (numeric >= 1_000_000) {
553
+ return `${(numeric / 1_000_000).toFixed(1)}M`;
554
+ }
555
+ if (numeric >= 1_000) {
556
+ return `${Math.round(numeric / 1_000)}K`;
557
+ }
558
+ return String(numeric);
559
+ }
560
+
561
+ function ProviderRowCompact({
562
+ provider,
563
+ isHighlighted,
564
+ widths,
565
+ }: {
566
+ provider: ProviderConfig;
567
+ isHighlighted: boolean;
568
+ widths: {
569
+ markerWidth: number;
570
+ providerWidth: number;
571
+ modelWidth: number;
572
+ contextWidth: number;
573
+ statusWidth: number;
574
+ };
575
+ }) {
576
+ const theme = useTheme();
577
+ const marker = isHighlighted ? ">" : " ";
578
+ const defaultMark = provider.isDefault ? "*" : " ";
579
+ const activeMark = provider.isActiveRoute ? "@" : " ";
580
+ const statusText = provider.isActiveRoute ? "Active" : provider.statusLabel;
581
+
582
+ const compactStatus = formatCompactStatus(statusText);
583
+ const compactContext = formatCompactContext(provider.contextLengthLabel);
584
+
585
+ const providerColor = isHighlighted ? theme.text : theme.textMuted;
586
+ const modelColor = theme.textDim;
587
+ const statusColor = provider.isActiveRoute
588
+ ? theme.success
589
+ : provider.enabled && !provider.routeUnavailableReason
590
+ ? theme.success
591
+ : theme.warning;
592
+
593
+ return (
594
+ <Box width="100%" overflow="hidden" flexDirection="row" flexShrink={0}>
595
+ <Box width={widths.markerWidth} flexShrink={0}>
596
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>
597
+ {marker} {defaultMark} {activeMark}
598
+ </Text>
599
+ </Box>
600
+ <Box width={widths.providerWidth} flexShrink={0} overflow="hidden">
601
+ <Text color={providerColor} bold={isHighlighted}>
602
+ {clampVisualText(provider.displayName, widths.providerWidth)}
603
+ </Text>
604
+ </Box>
605
+ <Text> </Text>
606
+ <Box width={widths.modelWidth} flexShrink={0} overflow="hidden">
607
+ <Text color={modelColor}>{clampVisualText(provider.currentModel, widths.modelWidth)}</Text>
608
+ </Box>
609
+ <Text> </Text>
610
+ <Box width={widths.contextWidth} flexShrink={0} overflow="hidden">
611
+ <Text color={theme.textDim}>{clampVisualText(compactContext, widths.contextWidth)}</Text>
612
+ </Box>
613
+ <Text> </Text>
614
+ <Box width={widths.statusWidth} flexShrink={0} overflow="hidden">
615
+ <Text color={statusColor}>{clampVisualText(compactStatus, widths.statusWidth)}</Text>
616
+ </Box>
617
+ </Box>
618
+ );
619
+ }
620
+
621
+ function ProviderRowSimple({
622
+ provider,
623
+ isHighlighted,
624
+ secondaryText,
625
+ secondaryState,
626
+ }: {
627
+ provider: ProviderConfig;
628
+ isHighlighted: boolean;
629
+ width: number;
630
+ secondaryText?: string;
631
+ secondaryState?: LocalBackendStatus["state"];
632
+ }) {
633
+ const theme = useTheme();
634
+ const markerWidth = 2;
635
+
636
+ return (
637
+ <Box width="100%" overflow="hidden" flexDirection="row" flexShrink={0}>
638
+ <Box width={markerWidth} flexShrink={0}>
639
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>{isHighlighted ? ">" : " "}</Text>
640
+ </Box>
641
+ <Box flexGrow={1} overflow="hidden">
642
+ <Text color={isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted}>
643
+ {provider.displayName}
644
+ </Text>
645
+ {secondaryText && (
646
+ <Text color={secondaryState === "ready" ? theme.success : secondaryState === "checking" ? theme.accent : theme.textDim}>
647
+ {` — ${secondaryText}`}
648
+ </Text>
649
+ )}
650
+ </Box>
651
+ </Box>
652
+ );
653
+ }
654
+
655
+ function ProviderRow({
656
+ provider,
657
+ isHighlighted,
658
+ widths,
659
+ }: {
660
+ provider: ProviderConfig;
661
+ isHighlighted: boolean;
662
+ widths: {
663
+ providerNameWidth: number;
664
+ modelWidth: number;
665
+ contextWidth: number;
666
+ toolsWidth: number;
667
+ streamWidth: number;
668
+ statusWidth: number;
669
+ };
670
+ }) {
671
+ const theme = useTheme();
672
+ const statusColor = provider.isActiveRoute
673
+ ? theme.success
674
+ : provider.enabled && !provider.routeUnavailableReason
675
+ ? theme.success
676
+ : theme.warning;
677
+ const marker = isHighlighted ? ">" : " ";
678
+ const defaultMark = provider.isDefault ? "*" : " ";
679
+ const activeMark = provider.isActiveRoute ? "@" : " ";
680
+ const statusText = provider.isActiveRoute ? "Active" : provider.statusLabel;
681
+
682
+ return (
683
+ <Box width="100%" overflow="hidden" flexDirection="row" flexShrink={0}>
684
+ <Box width={5} flexShrink={0}>
685
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>{marker} {defaultMark} {activeMark}</Text>
686
+ </Box>
687
+ <Box width={widths.providerNameWidth} flexShrink={0} overflow="hidden">
688
+ <Text color={isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted}>
689
+ {clampVisualText(provider.displayName, widths.providerNameWidth)}
690
+ </Text>
691
+ </Box>
692
+ <Text> </Text>
693
+ <Box width={widths.modelWidth} flexShrink={0} overflow="hidden">
694
+ <Text color={theme.textMuted}>{clampVisualText(provider.currentModel, widths.modelWidth)}</Text>
695
+ </Box>
696
+ {widths.contextWidth > 0 && (
697
+ <>
698
+ <Text> </Text>
699
+ <Box width={widths.contextWidth} flexShrink={0} overflow="hidden">
700
+ <Text color={theme.textMuted}>{clampVisualText(provider.contextLengthLabel ?? "Unknown", widths.contextWidth)}</Text>
701
+ </Box>
702
+ </>
703
+ )}
704
+ {widths.toolsWidth > 0 && (
705
+ <>
706
+ <Text> </Text>
707
+ <Box width={widths.toolsWidth} flexShrink={0} overflow="hidden">
708
+ <Text color={theme.textMuted}>{clampVisualText(capabilityFlag(provider.capabilityProfile?.supportsToolCalls), widths.toolsWidth)}</Text>
709
+ </Box>
710
+ </>
711
+ )}
712
+ {widths.streamWidth > 0 && (
713
+ <>
714
+ <Text> </Text>
715
+ <Box width={widths.streamWidth} flexShrink={0} overflow="hidden">
716
+ <Text color={theme.textMuted}>{clampVisualText(capabilityFlag(provider.capabilityProfile?.supportsStreaming), widths.streamWidth)}</Text>
717
+ </Box>
718
+ </>
719
+ )}
720
+ <Text> </Text>
721
+ <Box width={widths.statusWidth} flexShrink={0} overflow="hidden">
722
+ <Text color={statusColor}>{clampVisualText(statusText, widths.statusWidth)}</Text>
723
+ </Box>
724
+ </Box>
725
+ );
726
+ }
727
+
728
+ function ActionRow({
729
+ label,
730
+ disabledReason,
731
+ isHighlighted,
732
+ width,
733
+ }: {
734
+ label: string;
735
+ disabledReason?: string | null;
736
+ isHighlighted: boolean;
737
+ width: number;
738
+ }) {
739
+ const theme = useTheme();
740
+ const text = disabledReason ? `${label} unavailable` : label;
741
+ return (
742
+ <Box width="100%" overflow="hidden">
743
+ <Box width={2} flexShrink={0}>
744
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>{isHighlighted ? ">" : " "}</Text>
745
+ </Box>
746
+ <Box width={Math.max(10, width - 2)} flexShrink={0} overflow="hidden">
747
+ <Text color={disabledReason ? theme.textDim : isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted && !disabledReason}>
748
+ {clampVisualText(text, Math.max(10, width - 2))}
749
+ </Text>
750
+ </Box>
751
+ </Box>
752
+ );
753
+ }