@golba98/codexa 1.0.3 → 1.0.5

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 (125) 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 +642 -303
  5. package/src/commands/handler.ts +7 -18
  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/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -0,0 +1,737 @@
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 "../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 { calculateListWindow } from "../timeline/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
+ const action = actions[actionIndex];
251
+ if (action?.disabledReason) return;
252
+ onAction(selectedProvider.id, action?.value ?? "cancel");
253
+ }
254
+ }, { isActive: isFocused });
255
+
256
+ const activeRouteIndex = providers.findIndex((p) => p.isActiveRoute);
257
+
258
+ // ─── Layout & Windowing ───────────────────────────────────────────────────
259
+
260
+ const windowResult = useMemo(() => {
261
+ if (mode !== "providers") return null;
262
+
263
+ if (resolvedPanelLayout.mode === "compact") {
264
+ const titleRows = 1;
265
+ const hasActiveRoute = activeRouteIndex >= 0;
266
+
267
+ let footerRows = providers.length > resolvedPanelLayout.availableRows - titleRows ? 1 : 0;
268
+ let visibleRows = Math.max(1, resolvedPanelLayout.availableRows - titleRows - footerRows);
269
+
270
+ let start = clampIndex(
271
+ providerIndex - Math.floor(visibleRows / 2),
272
+ providers.length
273
+ );
274
+ let adjustedStart = Math.max(0, Math.min(start, providers.length - visibleRows));
275
+ let end = Math.min(providers.length, adjustedStart + visibleRows);
276
+
277
+ let reserveCurrent = false;
278
+ if (hasActiveRoute && visibleRows < providers.length) {
279
+ const isOutside = activeRouteIndex < adjustedStart || activeRouteIndex >= end;
280
+ if (isOutside) {
281
+ reserveCurrent = true;
282
+ footerRows = providers.length > resolvedPanelLayout.availableRows - titleRows - 1 ? 1 : 0;
283
+ visibleRows = Math.max(1, resolvedPanelLayout.availableRows - titleRows - footerRows - 1);
284
+ start = clampIndex(
285
+ providerIndex - Math.floor(visibleRows / 2),
286
+ providers.length
287
+ );
288
+ adjustedStart = Math.max(0, Math.min(start, providers.length - visibleRows));
289
+ end = Math.min(providers.length, adjustedStart + visibleRows);
290
+ }
291
+ }
292
+
293
+ return {
294
+ start: adjustedStart,
295
+ end,
296
+ showAbove: adjustedStart > 0,
297
+ showBelow: end < providers.length,
298
+ showRange: providers.length > visibleRows,
299
+ showHeaders: false,
300
+ showBorder: true,
301
+ showTitle: true,
302
+ reserveCurrent,
303
+ renderMode: "compact" as const,
304
+ visibleCount: visibleRows,
305
+ };
306
+ }
307
+
308
+ if (resolvedPanelLayout.mode === "expanded" && providers.length <= 6) {
309
+ return {
310
+ start: 0,
311
+ end: providers.length,
312
+ showAbove: false,
313
+ showBelow: false,
314
+ showRange: false,
315
+ showHeaders: false,
316
+ showBorder: true,
317
+ showTitle: true,
318
+ reserveCurrent: false,
319
+ renderMode: "compact" as const,
320
+ visibleCount: providers.length,
321
+ };
322
+ }
323
+
324
+ // Determine chrome rows based on available space
325
+ let showBorder = true;
326
+ let showTitle = true;
327
+ let showHeaders = true;
328
+
329
+ if (availableRows < 7) {
330
+ showBorder = false;
331
+ showTitle = false;
332
+ showHeaders = false;
333
+ } else if (availableRows < 9) {
334
+ showHeaders = false;
335
+ }
336
+
337
+ const baseChrome = (showBorder ? 2 : 0) + (showTitle ? 1 : 0) + (showHeaders ? 1 : 0);
338
+ const hasActiveRoute = activeRouteIndex >= 0;
339
+
340
+ let window = calculateListWindow({
341
+ itemCount: providers.length,
342
+ selectedIndex: providerIndex,
343
+ availableRows,
344
+ chromeRows: baseChrome,
345
+ showRangeLine: true,
346
+ });
347
+
348
+ let reserveCurrent = false;
349
+ if (hasActiveRoute && window.visibleCount < providers.length) {
350
+ const isOutside = activeRouteIndex < window.start || activeRouteIndex >= window.end;
351
+ if (isOutside) {
352
+ reserveCurrent = true;
353
+ window = calculateListWindow({
354
+ itemCount: providers.length,
355
+ selectedIndex: providerIndex,
356
+ availableRows,
357
+ chromeRows: baseChrome + 1,
358
+ showRangeLine: true,
359
+ });
360
+ }
361
+ }
362
+
363
+ return {
364
+ ...window,
365
+ showBorder,
366
+ showTitle,
367
+ showHeaders,
368
+ reserveCurrent,
369
+ renderMode: window.visibleCount < providers.length ? ("windowed" as const) : ("full" as const),
370
+ };
371
+ }, [mode, providers.length, providerIndex, availableRows, resolvedPanelLayout, activeRouteIndex]);
372
+
373
+ const visibleProviders = useMemo(() => {
374
+ if (mode !== "providers") return [];
375
+ if (!windowResult) return providers;
376
+ return providers.slice(windowResult.start, windowResult.end);
377
+ }, [mode, providers, windowResult]);
378
+
379
+ const showCurrent = mode === "providers" && windowResult?.reserveCurrent &&
380
+ activeRouteIndex >= 0 && (activeRouteIndex < windowResult.start || activeRouteIndex >= windowResult.end);
381
+
382
+ const titleText = mode === "actions" && selectedProvider
383
+ ? `Provider action: ${selectedProvider.displayName}`
384
+ : (windowResult?.showRange)
385
+ ? `Providers · Showing ${windowResult.start + 1}-${windowResult.end} of ${providers.length}`
386
+ : "Providers";
387
+
388
+ const body = useMemo(() => {
389
+ if (mode === "actions" && selectedProvider) {
390
+ const inCodexaAvailable = selectedProvider.routeMode === "in-codexa";
391
+ const isConfigured = inCodexaAvailable && !selectedProvider.routeUnavailableReason;
392
+ const inCodexaStatusText = !inCodexaAvailable ? "Unavailable" : isConfigured ? "Available" : "Needs configuration";
393
+ const inCodexaStatusColor = !inCodexaAvailable ? theme.error : isConfigured ? theme.success : theme.warning;
394
+
395
+ if (isCompactLayout) {
396
+ const statusText = selectedProvider.routeUnavailableReason ?? "Ready";
397
+ const metadataLine = `Status: ${statusText} · Backend: ${selectedProvider.backendType} · Codexa: ${inCodexaStatusText}`;
398
+ return (
399
+ <Box flexDirection="column">
400
+ <Box marginBottom={1} flexDirection="row" paddingX={2} overflow="hidden">
401
+ <Text color={theme.textDim} wrap="truncate">
402
+ {clampVisualText(metadataLine, innerWidth - 4)}
403
+ </Text>
404
+ </Box>
405
+ {actions.map((action, index) => (
406
+ <ActionRow
407
+ key={action.value}
408
+ label={action.label}
409
+ disabledReason={action.disabledReason}
410
+ isHighlighted={index === actionIndex}
411
+ width={innerWidth}
412
+ />
413
+ ))}
414
+ </Box>
415
+ );
416
+ }
417
+
418
+ return (
419
+ <Box flexDirection="column">
420
+ <Box marginBottom={1} flexDirection="column" paddingX={2}>
421
+ <Text color={theme.textDim}>Status: <Text color={theme.text}>{selectedProvider.routeUnavailableReason ?? "Ready"}</Text></Text>
422
+ <Text color={theme.textDim}>Backend: <Text color={theme.text}>{selectedProvider.backendType}</Text></Text>
423
+ <Text color={theme.textDim}>Use in Codexa: <Text color={inCodexaStatusColor}>{inCodexaStatusText}</Text></Text>
424
+ </Box>
425
+ {actions.map((action, index) => (
426
+ <ActionRow
427
+ key={action.value}
428
+ label={action.label}
429
+ disabledReason={action.disabledReason}
430
+ isHighlighted={index === actionIndex}
431
+ width={innerWidth}
432
+ />
433
+ ))}
434
+ </Box>
435
+ );
436
+ }
437
+
438
+ if (isCompactLayout) {
439
+ return visibleProviders.map((provider, index) => (
440
+ <ProviderRowCompact
441
+ key={provider.id}
442
+ provider={provider}
443
+ isHighlighted={(windowResult!.start + index) === providerIndex}
444
+ widths={compactWidths}
445
+ />
446
+ ));
447
+ }
448
+
449
+ return visibleProviders.map((provider, index) => (
450
+ <ProviderRow
451
+ key={provider.id}
452
+ provider={provider}
453
+ isHighlighted={(windowResult!.start + index) === providerIndex}
454
+ widths={{ providerNameWidth, modelWidth, contextWidth, toolsWidth, streamWidth, statusWidth }}
455
+ />
456
+ ));
457
+ }, [actionIndex, actions, contextWidth, innerWidth, mode, modelWidth, providerIndex, providerNameWidth, visibleProviders, windowResult?.start, streamWidth, toolsWidth, statusWidth, theme, selectedProvider, isCompactLayout, compactWidths]);
458
+
459
+ const showBorder = mode === "actions" ? (availableRows >= actions.length + 7) : (windowResult?.showBorder ?? true);
460
+
461
+ return (
462
+ <Box flexDirection="column" width={panelWidth} flexShrink={0}>
463
+ <Box
464
+ borderStyle={showBorder ? "round" : undefined}
465
+ borderColor={theme.prompt}
466
+ paddingX={showBorder ? 1 : 0}
467
+ paddingY={0}
468
+ width={panelWidth}
469
+ flexDirection="column"
470
+ flexShrink={0}
471
+ >
472
+ {(windowResult?.showTitle ?? true) && (
473
+ <Box width="100%" overflow="hidden" flexShrink={0}>
474
+ <Text color={theme.accent} bold>
475
+ {clampVisualText(showBorder ? `${titleText} ${helpText}` : titleText, innerWidth)}
476
+ </Text>
477
+ </Box>
478
+ )}
479
+
480
+ {!(windowResult?.showTitle ?? true) && windowResult?.showRange && (
481
+ <Box width="100%" overflow="hidden" flexShrink={0}>
482
+ <Text color={theme.accent}>
483
+ Showing {windowResult.start + 1}-{windowResult.end} of {providers.length}
484
+ </Text>
485
+ </Box>
486
+ )}
487
+
488
+ {windowResult?.showAbove && (
489
+ <Box height={1} overflow="hidden" flexShrink={0}>
490
+ <Text color={theme.accent}>{isCompactLayout ? "↑ more" : `↑ ${windowResult.start} more`}</Text>
491
+ </Box>
492
+ )}
493
+
494
+ {mode === "providers" && (windowResult?.showHeaders ?? true) && (
495
+ <Box width="100%" overflow="hidden" flexShrink={0}>
496
+ <Text color={theme.textDim}>
497
+ {" "}
498
+ {clampVisualText("Provider", providerNameWidth)}
499
+ {" "}
500
+ {clampVisualText("Model", modelWidth)}
501
+ {contextWidth > 0 && " " + clampVisualText("Context", contextWidth)}
502
+ {toolsWidth > 0 && " " + clampVisualText("Tool", toolsWidth)}
503
+ {streamWidth > 0 && " " + clampVisualText("Strm", streamWidth)}
504
+ {" "}
505
+ {clampVisualText("Status", statusWidth)}
506
+ </Text>
507
+ </Box>
508
+ )}
509
+
510
+ <Box flexDirection="column" marginTop={0} width="100%" flexShrink={0}>
511
+ {body}
512
+ </Box>
513
+
514
+ {showCurrent && activeRouteIndex >= 0 && (
515
+ <Box height={1} overflow="hidden" flexShrink={0} marginTop={0}>
516
+ <Text color={theme.textDim}>
517
+ Current: <Text color={theme.text}>{providers[activeRouteIndex].displayName} / {providers[activeRouteIndex].currentModel}</Text>
518
+ </Text>
519
+ </Box>
520
+ )}
521
+
522
+ {windowResult?.showBelow && (
523
+ <Box height={1} overflow="hidden" flexShrink={0}>
524
+ <Text color={theme.accent}>{isCompactLayout ? "↓ more" : `↓ ${providers.length - windowResult.end} more`}</Text>
525
+ </Box>
526
+ )}
527
+ </Box>
528
+
529
+ {process.env.CODEXA_DEBUG_LAYOUT === "1" && (
530
+ <Box flexDirection="column" marginTop={1} flexShrink={0}>
531
+ <Text color="red">
532
+ 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}
533
+ </Text>
534
+ </Box>
535
+ )}
536
+ </Box>
537
+ );
538
+ }
539
+
540
+ // ─── Subcomponents ───────────────────────────────────────────────────────────
541
+
542
+ function capabilityFlag(value: boolean | null | undefined): string {
543
+ if (value === true) return "Y";
544
+ if (value === false) return "N";
545
+ return "?";
546
+ }
547
+
548
+ function formatCompactStatus(status: string | undefined): string {
549
+ switch (status) {
550
+ case "Active":
551
+ return "Active";
552
+ case "Enabled":
553
+ return "Ready";
554
+ case "Needs config":
555
+ return "Config";
556
+ case "Disabled":
557
+ return "Off";
558
+ case "Unknown":
559
+ default:
560
+ return "?";
561
+ }
562
+ }
563
+
564
+ function formatCompactContext(value: string | number | undefined): string {
565
+ if (value === undefined || value === null) return "?";
566
+ const raw = String(value).trim();
567
+ if (!raw || raw === "Unknown" || raw === "?") return "?";
568
+ const numeric = Number(raw.replace(/,/g, ""));
569
+ if (!Number.isFinite(numeric)) return raw;
570
+ if (numeric >= 1_000_000) {
571
+ return `${(numeric / 1_000_000).toFixed(1)}M`;
572
+ }
573
+ if (numeric >= 1_000) {
574
+ return `${Math.round(numeric / 1_000)}K`;
575
+ }
576
+ return String(numeric);
577
+ }
578
+
579
+ function ProviderRowCompact({
580
+ provider,
581
+ isHighlighted,
582
+ widths,
583
+ }: {
584
+ provider: ProviderConfig;
585
+ isHighlighted: boolean;
586
+ widths: {
587
+ markerWidth: number;
588
+ providerWidth: number;
589
+ modelWidth: number;
590
+ contextWidth: number;
591
+ statusWidth: number;
592
+ };
593
+ }) {
594
+ const theme = useTheme();
595
+ const marker = isHighlighted ? ">" : " ";
596
+ const defaultMark = provider.isDefault ? "*" : " ";
597
+ const activeMark = provider.isActiveRoute ? "@" : " ";
598
+ const statusText = provider.isActiveRoute ? "Active" : provider.statusLabel;
599
+
600
+ const compactStatus = formatCompactStatus(statusText);
601
+ const compactContext = formatCompactContext(provider.contextLengthLabel);
602
+
603
+ const providerColor = isHighlighted ? theme.text : theme.textMuted;
604
+ const modelColor = theme.textDim;
605
+ const statusColor = provider.isActiveRoute
606
+ ? theme.success
607
+ : provider.enabled && !provider.routeUnavailableReason
608
+ ? theme.success
609
+ : theme.warning;
610
+
611
+ return (
612
+ <Box width="100%" overflow="hidden" flexDirection="row" flexShrink={0}>
613
+ <Box width={widths.markerWidth} flexShrink={0}>
614
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>
615
+ {marker} {defaultMark} {activeMark}
616
+ </Text>
617
+ </Box>
618
+ <Box width={widths.providerWidth} flexShrink={0} overflow="hidden">
619
+ <Text color={providerColor} bold={isHighlighted}>
620
+ {clampVisualText(provider.displayName, widths.providerWidth)}
621
+ </Text>
622
+ </Box>
623
+ <Text> </Text>
624
+ <Box width={widths.modelWidth} flexShrink={0} overflow="hidden">
625
+ <Text color={modelColor}>{clampVisualText(provider.currentModel, widths.modelWidth)}</Text>
626
+ </Box>
627
+ <Text> </Text>
628
+ <Box width={widths.contextWidth} flexShrink={0} overflow="hidden">
629
+ <Text color={theme.textDim}>{clampVisualText(compactContext, widths.contextWidth)}</Text>
630
+ </Box>
631
+ <Text> </Text>
632
+ <Box width={widths.statusWidth} flexShrink={0} overflow="hidden">
633
+ <Text color={statusColor}>{clampVisualText(compactStatus, widths.statusWidth)}</Text>
634
+ </Box>
635
+ </Box>
636
+ );
637
+ }
638
+
639
+ function ProviderRow({
640
+ provider,
641
+ isHighlighted,
642
+ widths,
643
+ }: {
644
+ provider: ProviderConfig;
645
+ isHighlighted: boolean;
646
+ widths: {
647
+ providerNameWidth: number;
648
+ modelWidth: number;
649
+ contextWidth: number;
650
+ toolsWidth: number;
651
+ streamWidth: number;
652
+ statusWidth: number;
653
+ };
654
+ }) {
655
+ const theme = useTheme();
656
+ const statusColor = provider.isActiveRoute
657
+ ? theme.success
658
+ : provider.enabled && !provider.routeUnavailableReason
659
+ ? theme.success
660
+ : theme.warning;
661
+ const marker = isHighlighted ? ">" : " ";
662
+ const defaultMark = provider.isDefault ? "*" : " ";
663
+ const activeMark = provider.isActiveRoute ? "@" : " ";
664
+ const statusText = provider.isActiveRoute ? "Active" : provider.statusLabel;
665
+
666
+ return (
667
+ <Box width="100%" overflow="hidden" flexDirection="row" flexShrink={0}>
668
+ <Box width={5} flexShrink={0}>
669
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>{marker} {defaultMark} {activeMark}</Text>
670
+ </Box>
671
+ <Box width={widths.providerNameWidth} flexShrink={0} overflow="hidden">
672
+ <Text color={isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted}>
673
+ {clampVisualText(provider.displayName, widths.providerNameWidth)}
674
+ </Text>
675
+ </Box>
676
+ <Text> </Text>
677
+ <Box width={widths.modelWidth} flexShrink={0} overflow="hidden">
678
+ <Text color={theme.textMuted}>{clampVisualText(provider.currentModel, widths.modelWidth)}</Text>
679
+ </Box>
680
+ {widths.contextWidth > 0 && (
681
+ <>
682
+ <Text> </Text>
683
+ <Box width={widths.contextWidth} flexShrink={0} overflow="hidden">
684
+ <Text color={theme.textMuted}>{clampVisualText(provider.contextLengthLabel ?? "Unknown", widths.contextWidth)}</Text>
685
+ </Box>
686
+ </>
687
+ )}
688
+ {widths.toolsWidth > 0 && (
689
+ <>
690
+ <Text> </Text>
691
+ <Box width={widths.toolsWidth} flexShrink={0} overflow="hidden">
692
+ <Text color={theme.textMuted}>{clampVisualText(capabilityFlag(provider.capabilityProfile?.supportsToolCalls), widths.toolsWidth)}</Text>
693
+ </Box>
694
+ </>
695
+ )}
696
+ {widths.streamWidth > 0 && (
697
+ <>
698
+ <Text> </Text>
699
+ <Box width={widths.streamWidth} flexShrink={0} overflow="hidden">
700
+ <Text color={theme.textMuted}>{clampVisualText(capabilityFlag(provider.capabilityProfile?.supportsStreaming), widths.streamWidth)}</Text>
701
+ </Box>
702
+ </>
703
+ )}
704
+ <Text> </Text>
705
+ <Box width={widths.statusWidth} flexShrink={0} overflow="hidden">
706
+ <Text color={statusColor}>{clampVisualText(statusText, widths.statusWidth)}</Text>
707
+ </Box>
708
+ </Box>
709
+ );
710
+ }
711
+
712
+ function ActionRow({
713
+ label,
714
+ disabledReason,
715
+ isHighlighted,
716
+ width,
717
+ }: {
718
+ label: string;
719
+ disabledReason?: string | null;
720
+ isHighlighted: boolean;
721
+ width: number;
722
+ }) {
723
+ const theme = useTheme();
724
+ const text = disabledReason ? `${label} unavailable` : label;
725
+ return (
726
+ <Box width="100%" overflow="hidden">
727
+ <Box width={2} flexShrink={0}>
728
+ <Text color={isHighlighted ? theme.accent : theme.textDim}>{isHighlighted ? ">" : " "}</Text>
729
+ </Box>
730
+ <Box width={Math.max(10, width - 2)} flexShrink={0} overflow="hidden">
731
+ <Text color={disabledReason ? theme.textDim : isHighlighted ? theme.text : theme.textMuted} bold={isHighlighted && !disabledReason}>
732
+ {clampVisualText(text, Math.max(10, width - 2))}
733
+ </Text>
734
+ </Box>
735
+ </Box>
736
+ );
737
+ }