@golba98/codexa 1.0.2 → 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 (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Shared utility for windowing large lists in limited terminal rows.
3
+ */
4
+
5
+ export interface ListWindowOptions {
6
+ itemCount: number;
7
+ selectedIndex: number;
8
+ availableRows: number;
9
+ /** Fixed rows consumed by headers, borders, titles, etc. */
10
+ chromeRows: number;
11
+ /** Whether to show "↑ N more" and "↓ N more" indicators. */
12
+ showIndicators?: boolean;
13
+ /** Whether to show "Showing X-Y of N" range line. */
14
+ showRangeLine?: boolean;
15
+ }
16
+
17
+ export interface ListWindowResult {
18
+ start: number;
19
+ end: number;
20
+ visibleCount: number;
21
+ showAbove: boolean;
22
+ showBelow: boolean;
23
+ showRange: boolean;
24
+ hiddenAbove: number;
25
+ hiddenBelow: number;
26
+ /** Total rows consumed including chrome and indicators. */
27
+ totalRows: number;
28
+ }
29
+
30
+ /**
31
+ * Calculates a contiguous window of items that fits within available rows,
32
+ * ensuring the selected item is always visible.
33
+ */
34
+ export function calculateListWindow({
35
+ itemCount,
36
+ selectedIndex,
37
+ availableRows,
38
+ chromeRows,
39
+ showIndicators = true,
40
+ showRangeLine = false,
41
+ }: ListWindowOptions): ListWindowResult {
42
+ if (itemCount === 0) {
43
+ return {
44
+ start: 0,
45
+ end: 0,
46
+ visibleCount: 0,
47
+ showAbove: false,
48
+ showBelow: false,
49
+ showRange: false,
50
+ hiddenAbove: 0,
51
+ hiddenBelow: 0,
52
+ totalRows: chromeRows,
53
+ };
54
+ }
55
+
56
+ const rangeReserved = showRangeLine ? 1 : 0;
57
+
58
+ // If everything fits including the range line, just show everything.
59
+ if (itemCount + chromeRows + rangeReserved <= availableRows) {
60
+ return {
61
+ start: 0,
62
+ end: itemCount,
63
+ visibleCount: itemCount,
64
+ showAbove: false,
65
+ showBelow: false,
66
+ showRange: showRangeLine,
67
+ hiddenAbove: 0,
68
+ hiddenBelow: 0,
69
+ totalRows: itemCount + chromeRows + rangeReserved,
70
+ };
71
+ }
72
+
73
+ // Otherwise we must window.
74
+ // We reserve space for indicators if requested.
75
+ // We MUST fit within availableRows.
76
+ const indicatorReserved = showIndicators ? 2 : 0;
77
+ const baseReserved = chromeRows + indicatorReserved + rangeReserved;
78
+
79
+ let itemRows = Math.max(1, availableRows - baseReserved);
80
+ let start = 0;
81
+ let end = 0;
82
+
83
+ // Iteratively refine to account for dynamic indicators.
84
+ for (let attempt = 0; attempt < 3; attempt++) {
85
+ const visibleCount = Math.max(1, Math.min(itemCount, itemRows));
86
+
87
+ // Center selected item if possible
88
+ start = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
89
+ if (start + visibleCount > itemCount) {
90
+ start = Math.max(0, itemCount - visibleCount);
91
+ }
92
+ end = Math.min(itemCount, start + visibleCount);
93
+
94
+ const actualAbove = start > 0;
95
+ const actualBelow = end < itemCount;
96
+ const dynamicReserved = chromeRows
97
+ + (showRangeLine ? 1 : 0)
98
+ + (showIndicators && actualAbove ? 1 : 0)
99
+ + (showIndicators && actualBelow ? 1 : 0);
100
+
101
+ const nextItemRows = Math.max(1, availableRows - dynamicReserved);
102
+ if (nextItemRows === itemRows) break;
103
+ itemRows = nextItemRows;
104
+ }
105
+
106
+ // Final visibility check for selectedIndex
107
+ if (selectedIndex < start) {
108
+ start = selectedIndex;
109
+ end = Math.min(itemCount, start + itemRows);
110
+ } else if (selectedIndex >= end) {
111
+ end = selectedIndex + 1;
112
+ start = Math.max(0, end - itemRows);
113
+ }
114
+
115
+ // Final clamping to ensure we don't exceed availableRows if indicators are shown
116
+ let finalAbove = showIndicators && start > 0;
117
+ let finalBelow = showIndicators && end < itemCount;
118
+
119
+ while (end - start + chromeRows + (showRangeLine ? 1 : 0) + (finalAbove ? 1 : 0) + (finalBelow ? 1 : 0) > availableRows && end - start > 1) {
120
+ // Shrink window from the side further from selectedIndex
121
+ if (selectedIndex - start > end - 1 - selectedIndex) {
122
+ start++;
123
+ } else {
124
+ end--;
125
+ }
126
+ finalAbove = showIndicators && start > 0;
127
+ finalBelow = showIndicators && end < itemCount;
128
+ }
129
+
130
+ return {
131
+ start,
132
+ end,
133
+ visibleCount: end - start,
134
+ showAbove: finalAbove,
135
+ showBelow: finalBelow,
136
+ showRange: showRangeLine,
137
+ hiddenAbove: start,
138
+ hiddenBelow: itemCount - end,
139
+ totalRows: (end - start)
140
+ + chromeRows
141
+ + (showRangeLine ? 1 : 0)
142
+ + (finalAbove ? 1 : 0)
143
+ + (finalBelow ? 1 : 0),
144
+ };
145
+ }
@@ -0,0 +1,103 @@
1
+ import { getTextWidth } from "./textLayout.js";
2
+
3
+ // ─── Logo constants ───────────────────────────────────────────────────────────
4
+ // Each variant is an array of exact terminal rows.
5
+ //
6
+ // IMPORTANT: Never apply `bold` when rendering these rows. Bold rendering of
7
+ // Unicode full-block (█) and box-drawing (╔═╗╝) characters causes per-glyph
8
+ // width/stroke differences in most terminal fonts (Ptyxis, GNOME Terminal,
9
+ // VS Code), producing visible gaps between characters that should be flush.
10
+ // The companion `wrap="truncate"` rule keeps each row on exactly one terminal
11
+ // line regardless of the surrounding Ink flex layout.
12
+
13
+ // Canonical Codexa brand wordmark — the ██ block art is the authoritative
14
+ // large logo for wide/max layouts.
15
+ export const CODEXA_WORDMARK = [
16
+ " ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ",
17
+ "██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝██╔══██╗",
18
+ "██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ ███████║",
19
+ "██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ ██╔══██║",
20
+ "╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗██║ ██║",
21
+ " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
22
+ ].join("\n");
23
+
24
+ /** 6-row ANSI Shadow block-char logo. Requires cols ≥ LOGO_LARGE_MIN_COLS. */
25
+ export const LOGO_LARGE: readonly string[] = CODEXA_WORDMARK.split("\n");
26
+
27
+ /** 4-row pure-ASCII art logo. Requires cols ≥ LOGO_MEDIUM_MIN_COLS. */
28
+ export const LOGO_MEDIUM: readonly string[] = [
29
+ " ____ ___ ____ _____ _ __ _ ",
30
+ " / ___| / _ \\| _ \\| ____| |/ / / \\ ",
31
+ "| | | | | | | | | _| | ' / / _ \\ ",
32
+ "|_| \\___/|_| |_|_____|_|\\_\\ /_/ \\_\\ ",
33
+ ];
34
+
35
+ /** 1-row compact logo. Requires cols ≥ LOGO_COMPACT_MIN_COLS. */
36
+ export const LOGO_COMPACT: readonly string[] = [
37
+ "✦ CODEXA",
38
+ ];
39
+
40
+ // ─── Breakpoints ──────────────────────────────────────────────────────────────
41
+
42
+ // Aligned with MEDIUM_HEADER_MIN_COLUMNS so any side-by-side-capable terminal
43
+ // shows the canonical block wordmark instead of the thin ASCII fallback.
44
+ export const LOGO_LARGE_MIN_COLS = 72;
45
+ export const LOGO_MEDIUM_MIN_COLS = 72;
46
+ export const LOGO_COMPACT_MIN_COLS = 48;
47
+
48
+ // Minimum terminal rows each variant needs to render without crowding out the
49
+ // metadata + composer. A wide-but-short terminal (e.g. VS Code's bottom panel)
50
+ // must step DOWN to a smaller logo instead of dropping straight to text-only.
51
+ export const LOGO_LARGE_MIN_ROWS = 35;
52
+ export const LOGO_MEDIUM_MIN_ROWS = 16;
53
+ export const LOGO_COMPACT_MIN_ROWS = 12;
54
+
55
+ const LOGO_VARIANTS: readonly { logo: readonly string[]; minCols: number; minRows: number }[] = [
56
+ { logo: LOGO_LARGE, minCols: LOGO_LARGE_MIN_COLS, minRows: LOGO_LARGE_MIN_ROWS },
57
+ { logo: LOGO_MEDIUM, minCols: LOGO_MEDIUM_MIN_COLS, minRows: LOGO_MEDIUM_MIN_ROWS },
58
+ { logo: LOGO_COMPACT, minCols: LOGO_COMPACT_MIN_COLS, minRows: LOGO_COMPACT_MIN_ROWS },
59
+ ];
60
+
61
+ // ─── Selection ────────────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Returns the best logo variant for the given terminal column count.
65
+ *
66
+ * Env overrides:
67
+ * CODEXA_NO_ASCII_LOGO=1 → always text-only (empty array)
68
+ * CODEXA_COMPACT_LOGO=1 → always compact single-line logo
69
+ */
70
+ export function selectLogoVariant(cols: number): readonly string[] {
71
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return [];
72
+ if (process.env["CODEXA_COMPACT_LOGO"] === "1") return LOGO_COMPACT;
73
+ if (cols >= LOGO_LARGE_MIN_COLS) return LOGO_LARGE;
74
+ if (cols >= LOGO_COMPACT_MIN_COLS) return LOGO_COMPACT;
75
+ return [];
76
+ }
77
+
78
+ /**
79
+ * Returns the largest logo variant that fits BOTH the available columns and
80
+ * rows. Unlike {@link selectLogoVariant} (columns-only), this degrades a
81
+ * too-tall logo to a shorter one before falling back to text-only — so a
82
+ * wide-but-short terminal keeps a logo instead of collapsing to a flat line.
83
+ * Returns an empty array only when even the 1-row compact logo cannot fit.
84
+ *
85
+ * Honours the same env overrides as {@link selectLogoVariant}.
86
+ */
87
+ export function selectLogoVariantForViewport(cols: number, rows: number): readonly string[] {
88
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return [];
89
+ if (process.env["CODEXA_COMPACT_LOGO"] === "1") {
90
+ return rows >= LOGO_COMPACT_MIN_ROWS ? LOGO_COMPACT : [];
91
+ }
92
+ for (const variant of LOGO_VARIANTS) {
93
+ if (cols >= variant.minCols && rows >= variant.minRows) {
94
+ return variant.logo;
95
+ }
96
+ }
97
+ return [];
98
+ }
99
+
100
+ /** Returns the visual column width of the widest row in a logo variant. */
101
+ export function getLogoWidth(logo: readonly string[]): number {
102
+ return logo.reduce((max, line) => Math.max(max, getTextWidth(line)), 0);
103
+ }
@@ -18,10 +18,10 @@ export function getModeDisplaySpec(mode: string, theme: Theme): ModeDisplaySpec
18
18
  return {
19
19
  label: formatModeLabel(mode),
20
20
  ringGlyph: "◉",
21
- ringColor: theme.WARNING,
22
- ringFill: theme.BORDER_SUBTLE,
23
- iconColor: theme.WARNING,
24
- labelColor: theme.TEXT,
21
+ ringColor: theme.warning,
22
+ ringFill: theme.border,
23
+ iconColor: theme.warning,
24
+ labelColor: theme.text,
25
25
  labelBold: true,
26
26
  ringBold: true,
27
27
  };
@@ -29,10 +29,10 @@ export function getModeDisplaySpec(mode: string, theme: Theme): ModeDisplaySpec
29
29
  return {
30
30
  label: formatModeLabel(mode),
31
31
  ringGlyph: "◎",
32
- ringColor: theme.BORDER_ACTIVE,
33
- ringFill: theme.PANEL_ALT,
34
- iconColor: theme.PROMPT,
35
- labelColor: theme.TEXT,
32
+ ringColor: theme.borderFocused,
33
+ ringFill: theme.surfaceMuted,
34
+ iconColor: theme.prompt,
35
+ labelColor: theme.text,
36
36
  labelBold: true,
37
37
  ringBold: false,
38
38
  };
@@ -41,10 +41,10 @@ export function getModeDisplaySpec(mode: string, theme: Theme): ModeDisplaySpec
41
41
  return {
42
42
  label: formatModeLabel(mode),
43
43
  ringGlyph: "○",
44
- ringColor: theme.SUCCESS,
45
- ringFill: theme.PANEL_SOFT,
46
- iconColor: theme.SUCCESS,
47
- labelColor: theme.MUTED,
44
+ ringColor: theme.success,
45
+ ringFill: theme.surfaceMuted,
46
+ iconColor: theme.success,
47
+ labelColor: theme.textMuted,
48
48
  labelBold: false,
49
49
  ringBold: false,
50
50
  };
@@ -0,0 +1,112 @@
1
+ import { formatModeLabel, formatReasoningLabel } from "../config/settings.js";
2
+ import type { RuntimeSummary } from "../config/runtimeConfig.js";
3
+ import type { CodexModelCapability } from "../core/models/codexModelCapabilities.js";
4
+ import type { ModelSpec } from "../core/models/modelSpecs.js";
5
+ import type { ModelContextMetadata } from "../core/providerRuntime/contextMetadata.js";
6
+ import { contextMetadataToModelSpec, formatContextCompact } from "../core/providerRuntime/contextMetadata.js";
7
+ import type { ActiveProviderRoute } from "../core/providerRuntime/types.js";
8
+ import { getAntigravityModelLabel } from "../core/providerRuntime/antigravity.js";
9
+
10
+ export interface ActiveRuntimeDisplayInput {
11
+ route: ActiveProviderRoute;
12
+ reasoningLevel: string;
13
+ mode: string;
14
+ tokensUsed: number;
15
+ modelCapability?: CodexModelCapability | null;
16
+ contextMetadata?: ModelContextMetadata | null;
17
+ }
18
+
19
+ export interface ActiveRuntimeDisplay {
20
+ providerLabel: string;
21
+ modelDisplay: string;
22
+ footerModelDisplay: string;
23
+ contextDisplay: string;
24
+ modeLabel: string;
25
+ modelSpec: ModelSpec;
26
+ }
27
+
28
+ const PROVIDER_DISPLAY: Record<string, string> = {
29
+ openai: "OpenAI Codex CLI",
30
+ anthropic: "Claude Code CLI",
31
+ google: "Gemini CLI",
32
+ local: "Local",
33
+ antigravity: "Antigravity CLI",
34
+ };
35
+
36
+ function formatContextLimit(value: number): string {
37
+ if (value >= 1_000_000) {
38
+ const millions = value / 1_000_000;
39
+ return Number.isInteger(millions) || Math.abs(millions - 1.048576) < 0.000001
40
+ ? `${Math.round(millions)}M`
41
+ : `${millions.toFixed(1)}M`;
42
+ }
43
+ return formatContextCompact(value);
44
+ }
45
+
46
+ function formatUsedTokens(value: number): string {
47
+ return formatContextCompact(value).replace(/k$/, "K");
48
+ }
49
+
50
+ function isContextForRoute(metadata: ModelContextMetadata | null | undefined, route: ActiveProviderRoute): metadata is ModelContextMetadata {
51
+ return metadata?.providerId === route.providerId && metadata.modelId === route.modelId;
52
+ }
53
+
54
+ function getModelLabel(route: ActiveProviderRoute, capability?: CodexModelCapability | null): string {
55
+ if (route.providerId === "anthropic") {
56
+ return capability?.label ?? route.modelId;
57
+ }
58
+ if (route.providerId === "antigravity") {
59
+ return getAntigravityModelLabel(route.modelId);
60
+ }
61
+ return route.modelId;
62
+ }
63
+
64
+ export function buildActiveRuntimeDisplay({
65
+ route,
66
+ reasoningLevel,
67
+ mode,
68
+ tokensUsed,
69
+ modelCapability = null,
70
+ contextMetadata = null,
71
+ }: ActiveRuntimeDisplayInput): ActiveRuntimeDisplay {
72
+ const providerLabel = PROVIDER_DISPLAY[route.providerId] ?? route.providerId;
73
+ const rawReasoning = route.providerId === "antigravity"
74
+ ? route.reasoning
75
+ : route.reasoning ?? reasoningLevel;
76
+ const reasoning = rawReasoning ? formatReasoningLabel(rawReasoning) : null;
77
+ const modelLabel = getModelLabel(route, modelCapability);
78
+ const validContextMetadata = isContextForRoute(contextMetadata, route) ? contextMetadata : null;
79
+ const contextDisplay = validContextMetadata?.contextLength != null
80
+ ? `${formatUsedTokens(tokensUsed)} / ${validContextMetadata.confidence === "estimated" ? "~" : ""}${formatContextLimit(validContextMetadata.contextLength)}`
81
+ : "Unknown";
82
+ const modelSpec = contextMetadataToModelSpec(validContextMetadata ?? {
83
+ providerId: route.providerId,
84
+ modelId: route.modelId,
85
+ contextLength: null,
86
+ source: "unknown",
87
+ confidence: "unknown",
88
+ error: "Context length unavailable for this model.",
89
+ });
90
+
91
+ return {
92
+ providerLabel,
93
+ modelDisplay: reasoning
94
+ ? `${providerLabel} / ${modelLabel} / reasoning: ${reasoning}`
95
+ : `${providerLabel} / ${modelLabel}`,
96
+ footerModelDisplay: reasoning
97
+ ? `${providerLabel} / ${modelLabel} (${reasoning})`
98
+ : `${providerLabel} / ${modelLabel}`,
99
+ contextDisplay,
100
+ modeLabel: formatModeLabel(mode),
101
+ modelSpec,
102
+ };
103
+ }
104
+
105
+ export function runtimeDisplayToSummary(display: ActiveRuntimeDisplay, base: RuntimeSummary): RuntimeSummary {
106
+ return {
107
+ ...base,
108
+ providerLabel: display.providerLabel,
109
+ modelLabel: display.modelDisplay,
110
+ contextLabel: display.contextDisplay,
111
+ };
112
+ }
@@ -198,9 +198,14 @@ export function splitTextAtColumn(text: string, column: number): { before: strin
198
198
 
199
199
  // ─── Text wrapping ────────────────────────────────────────────────────────────
200
200
 
201
- export function wrapTextRows(text: string, maxWidth: number): WrappedTextRow[] {
201
+ export function wrapTextRows(
202
+ text: string,
203
+ maxWidth: number,
204
+ firstLineWidth: number = maxWidth,
205
+ ): WrappedTextRow[] {
202
206
  const normalized = normalizeLineBreaks(text);
203
207
  const safeWidth = Math.max(1, maxWidth);
208
+ const safeFirstWidth = Math.max(1, firstLineWidth);
204
209
  const rows: WrappedTextRow[] = [];
205
210
  let rowStart = 0;
206
211
  let rowText = "";
@@ -220,7 +225,9 @@ export function wrapTextRows(text: string, maxWidth: number): WrappedTextRow[] {
220
225
  continue;
221
226
  }
222
227
 
223
- if (rowText.length > 0 && rowWidth + unit.width > safeWidth) {
228
+ // Only the first emitted row honors firstLineWidth; later rows use maxWidth.
229
+ const limit = rows.length === 0 ? safeFirstWidth : safeWidth;
230
+ if (rowText.length > 0 && rowWidth + unit.width > limit) {
224
231
  rows.push({
225
232
  text: rowText,
226
233
  start: rowStart,
@@ -252,8 +259,12 @@ export function wrapTextRows(text: string, maxWidth: number): WrappedTextRow[] {
252
259
  }];
253
260
  }
254
261
 
255
- export function wrapPlainText(text: string, maxWidth: number): string[] {
256
- return wrapTextRows(text, maxWidth).map((row) => row.text);
262
+ export function wrapPlainText(
263
+ text: string,
264
+ maxWidth: number,
265
+ firstLineWidth: number = maxWidth,
266
+ ): string[] {
267
+ return wrapTextRows(text, maxWidth, firstLineWidth).map((row) => row.text);
257
268
  }
258
269
 
259
270
  export function wrapCommandText(text: string, maxWidth: number): string[] {