@golba98/codexa 1.0.3 → 1.0.4

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