@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
@@ -1,293 +1,437 @@
1
- import React, { memo } from "react";
2
- import { Box, Text } from "ink";
3
- import { APP_VERSION, HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
5
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
6
- import { getAuthStateLabel } from "../core/auth/codexAuth.js";
7
- import * as renderDebug from "../core/perf/renderDebug.js";
8
- import { useTheme } from "./theme.js";
9
- import type { Layout } from "./layout.js";
10
- import { getTextWidth } from "./textLayout.js";
11
-
12
- export const HEADER_WORDMARK_LINES = [
13
- " ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ",
14
- "██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝██╔══██╗",
15
- "██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ ███████║",
16
- "██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ ██╔══██║",
17
- "╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗██║ ██║",
18
- " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
19
- ];
20
-
21
- const MIN_METADATA_COLUMN_WIDTH = 60;
22
- const STACKED_METADATA_GAP_ROWS = 1;
23
- const MIN_LOGO_TERMINAL_WIDTH = getWordmarkWidth() + 2;
24
- const MIN_LOGO_TERMINAL_ROWS = 24;
25
-
26
- export type HeaderHeroMode = "wide" | "stacked" | "compact";
27
-
28
- export interface HeaderHeroLayout {
29
- mode: HeaderHeroMode;
30
- topMarginRows: number;
31
- bottomMarginRows: number;
32
- metadataGapColumns: number;
33
- metadataGapRows: number;
34
- logoRows: number;
35
- metadataRows: number;
36
- totalRows: number;
37
- }
38
-
39
- type HeaderMetadataLine = { key: string; text: string; color: string; bold: boolean };
40
-
41
- interface TopHeaderProps {
42
- authState: CodexAuthState;
43
- workspaceLabel: string;
44
- layout: Layout;
45
- runtimeSummary?: RuntimeSummary | null;
46
- headerConfig?: HeaderConfig;
47
- }
48
-
49
- function getWordmarkWidth(): number {
50
- return HEADER_WORDMARK_LINES.reduce((maxWidth, line) => Math.max(maxWidth, getTextWidth(line)), 0);
51
- }
52
-
53
- function getMetadataRowCount(headerConfig: HeaderConfig): number {
54
- return [
55
- headerConfig.showBrand,
56
- headerConfig.showAuthStatus,
57
- headerConfig.showWorkspace,
58
- headerConfig.showProvider,
59
- headerConfig.showContext,
60
- ].filter(Boolean).length;
61
- }
62
-
63
- function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
64
- if (layout.mode !== "full") {
65
- return {
66
- topMarginRows: 0,
67
- bottomMarginRows: 0,
68
- };
69
- }
70
-
71
- if (layout.rows <= 24) {
72
- return { topMarginRows: 0, bottomMarginRows: 0 };
73
- }
74
-
75
- return {
76
- topMarginRows: 1,
77
- bottomMarginRows: 0,
78
- };
79
- }
80
-
81
- export function getHeaderHeroLayout(
82
- layout: Layout,
83
- headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
84
- ): HeaderHeroLayout {
85
- const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
86
- const metadataRows = getMetadataRowCount(headerConfig);
87
- const canRenderLogo = layout.cols >= MIN_LOGO_TERMINAL_WIDTH && layout.rows >= MIN_LOGO_TERMINAL_ROWS;
88
-
89
- if (!canRenderLogo) {
90
- return {
91
- mode: "compact",
92
- topMarginRows,
93
- bottomMarginRows,
94
- metadataGapColumns: 0,
95
- metadataGapRows: 0,
96
- logoRows: 1,
97
- metadataRows: 0,
98
- totalRows: topMarginRows + 1 + bottomMarginRows,
99
- };
100
- }
101
-
102
- const logoRows = HEADER_WORDMARK_LINES.length;
103
- const metadataGapColumns = layout.cols >= 150 ? 6 : layout.cols >= 130 ? 5 : 4;
104
- const canUseWideHero = layout.cols >= getWordmarkWidth() + metadataGapColumns + MIN_METADATA_COLUMN_WIDTH + 2;
105
- const metadataGapRows = canUseWideHero || metadataRows === 0 ? 0 : STACKED_METADATA_GAP_ROWS;
106
- const contentRows = canUseWideHero
107
- ? logoRows
108
- : logoRows + metadataGapRows + metadataRows;
109
-
110
- return {
111
- mode: canUseWideHero ? "wide" : "stacked",
112
- topMarginRows,
113
- bottomMarginRows,
114
- metadataGapColumns,
115
- metadataGapRows,
116
- logoRows,
117
- metadataRows,
118
- totalRows: topMarginRows + contentRows + bottomMarginRows,
119
- };
120
- }
121
-
122
- export function measureTopHeaderRows(
123
- layout: Layout,
124
- headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
125
- ): number {
126
- return getHeaderHeroLayout(layout, headerConfig).totalRows;
127
- }
128
-
129
- /** Truncate a path to fit within maxWidth, keeping the end and prefixing with "... " if needed */
130
- function truncatePath(path: string, maxWidth: number): string {
131
- if (maxWidth <= 4 || path.length <= maxWidth) return path;
132
- // Prefix with "... " (4 chars) and take the last (maxWidth - 4) chars
133
- return "... " + path.slice(path.length - (maxWidth - 4));
134
- }
135
-
136
- export function TopHeader({
137
- authState,
138
- workspaceLabel,
139
- layout,
140
- runtimeSummary = null,
141
- headerConfig = HEADER_CONFIG_DEFAULTS,
142
- }: TopHeaderProps) {
143
- renderDebug.useRenderDebug("Header", {
144
- authState,
145
- workspaceLabel,
146
- cols: layout.cols,
147
- rows: layout.rows,
148
- mode: layout.mode,
149
- });
150
- renderDebug.useLifecycleDebug("Header", {
151
- authState,
152
- cols: layout.cols,
153
- rows: layout.rows,
154
- mode: layout.mode,
155
- });
156
-
157
- const { cols } = layout;
158
- const theme = useTheme();
159
-
160
- const authLabelRaw = getAuthStateLabel(authState);
161
- const authLabel = authLabelRaw.length > 0
162
- ? authLabelRaw[0]!.toUpperCase() + authLabelRaw.slice(1)
163
- : authLabelRaw;
164
-
165
- const wsDisplay = truncatePath(workspaceLabel, Math.max(18, cols - 40));
166
- const heroLayout = getHeaderHeroLayout(layout, headerConfig);
167
- const metadataLines = [
168
- headerConfig.showBrand ? { key: "brand", text: `Codexa v${APP_VERSION}`, color: theme.TEXT, bold: true } : null,
169
- headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.TEXT, bold: false } : null,
170
- headerConfig.showWorkspace ? { key: "workspace", text: `Workspace: ${wsDisplay}`, color: theme.MUTED, bold: false } : null,
171
- headerConfig.showProvider && runtimeSummary?.providerLabel
172
- ? { key: "provider", text: `Provider: ${runtimeSummary.providerLabel}`, color: theme.TEXT, bold: false }
173
- : null,
174
- headerConfig.showContext
175
- ? { key: "context", text: `Context: ${runtimeSummary?.contextLabel ?? "Unknown"}`, color: theme.MUTED, bold: false }
176
- : null,
177
- ].filter((line): line is HeaderMetadataLine => Boolean(line));
178
-
179
- // Add a 1-row gap between the version line and workspace line in wide mode
180
- // so the two pieces of metadata have breathing room beside the logo.
181
- const hasMetadataGap = heroLayout.mode === "wide"
182
- && metadataLines.length <= 3
183
- && metadataLines.some((l) => l.key === "brand")
184
- && metadataLines.some((l) => l.key === "workspace");
185
- const metadataVisualRows = metadataLines.length + (hasMetadataGap ? 1 : 0);
186
-
187
- const metadataColumn = (
188
- <Box flexDirection="column" flexGrow={1} minWidth={Math.min(MIN_METADATA_COLUMN_WIDTH, Math.max(1, cols - getWordmarkWidth() - heroLayout.metadataGapColumns - 2))}>
189
- {metadataLines.map((line) =>
190
- hasMetadataGap && line.key === "workspace" ? (
191
- <Box key={line.key} marginTop={1}>
192
- <Text color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
193
- </Box>
194
- ) : (
195
- <Text key={line.key} color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
196
- )
197
- )}
198
- </Box>
199
- );
200
-
201
- const logoColumn = (
202
- <Box flexDirection="column" flexShrink={0}>
203
- {HEADER_WORDMARK_LINES.map((line, i) => (
204
- <Text key={i} color={theme.ACCENT} bold>{line}</Text>
205
- ))}
206
- </Box>
207
- );
208
-
209
- if (heroLayout.mode !== "compact") {
210
- const metadataTopOffset = Math.max(0, Math.floor((HEADER_WORDMARK_LINES.length - metadataVisualRows) / 2));
211
-
212
- return (
213
- <Box flexDirection="column" paddingX={1} width="100%">
214
- {heroLayout.topMarginRows > 0 && (
215
- <Box height={heroLayout.topMarginRows} />
216
- )}
217
-
218
- {heroLayout.mode === "wide" ? (
219
- <Box flexDirection="row" width="100%" alignItems="flex-start">
220
- {logoColumn}
221
- <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
222
- <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
223
- {metadataColumn}
224
- </Box>
225
- </Box>
226
- ) : (
227
- <Box flexDirection="column" width="100%">
228
- {logoColumn}
229
- {heroLayout.metadataGapRows > 0 && (
230
- <Box height={heroLayout.metadataGapRows} />
231
- )}
232
- {metadataColumn}
233
- </Box>
234
- )}
235
-
236
- {heroLayout.bottomMarginRows > 0 && (
237
- <Box height={heroLayout.bottomMarginRows} />
238
- )}
239
- </Box>
240
- );
241
- }
242
-
243
- // Compact / micro / activity-collapsed: single-line header
244
- const compactParts: React.ReactNode[] = [];
245
- if (headerConfig.showBrand) {
246
- compactParts.push(
247
- <Text key="brand" color={theme.TEXT} bold>{`Codexa v${APP_VERSION}`}</Text>,
248
- );
249
- }
250
- if (headerConfig.showAuthStatus) {
251
- if (compactParts.length > 0) compactParts.push(<Text key="sep-auth" color={theme.DIM}>{" · "}</Text>);
252
- compactParts.push(<Text key="auth" color={theme.TEXT}>{authLabel}</Text>);
253
- }
254
- if (headerConfig.showWorkspace) {
255
- if (compactParts.length > 0) compactParts.push(<Text key="sep-ws" color={theme.DIM}>{" · "}</Text>);
256
- compactParts.push(<Text key="ws" color={theme.MUTED} wrap="truncate">{`Workspace: ${wsDisplay}`}</Text>);
257
- }
258
- if (headerConfig.showProvider && runtimeSummary?.providerLabel) {
259
- if (compactParts.length > 0) compactParts.push(<Text key="sep-provider" color={theme.DIM}>{" · "}</Text>);
260
- compactParts.push(<Text key="provider" color={theme.TEXT} wrap="truncate">{`Provider: ${runtimeSummary.providerLabel}`}</Text>);
261
- }
262
- if (headerConfig.showContext) {
263
- if (compactParts.length > 0) compactParts.push(<Text key="sep-context" color={theme.DIM}>{" · "}</Text>);
264
- compactParts.push(<Text key="context" color={theme.MUTED} wrap="truncate">{`Context: ${runtimeSummary?.contextLabel ?? "Unknown"}`}</Text>);
265
- }
266
-
267
- return (
268
- <Box flexDirection="column" paddingX={1} width="100%">
269
- {heroLayout.topMarginRows > 0 && (
270
- <Box height={heroLayout.topMarginRows} />
271
- )}
272
- <Box flexDirection="row" width="100%">
273
- {compactParts}
274
- </Box>
275
- {heroLayout.bottomMarginRows > 0 && (
276
- <Box height={heroLayout.bottomMarginRows} />
277
- )}
278
- </Box>
279
- );
280
- }
281
-
282
- // Memoize to prevent re-renders during streaming when props haven't changed
283
- export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
284
- return (
285
- prev.authState === next.authState &&
286
- prev.workspaceLabel === next.workspaceLabel &&
287
- prev.layout.cols === next.layout.cols &&
288
- prev.layout.rows === next.layout.rows &&
289
- prev.layout.mode === next.layout.mode &&
290
- prev.runtimeSummary === next.runtimeSummary &&
291
- prev.headerConfig === next.headerConfig
292
- );
293
- });
1
+ import React, { memo } from "react";
2
+ import { Box, Text } from "ink";
3
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
+ import { formatCodexaBrandLabel } from "../core/version/channel.js";
5
+ import { formatVersionLabel } from "../core/version/updateCheck.js";
6
+ import type { RuntimeSummary } from "../config/runtimeConfig.js";
7
+ import type { CodexAuthState } from "../core/auth/codexAuth.js";
8
+ import { getAuthStateLabel } from "../core/auth/codexAuth.js";
9
+ import * as renderDebug from "../core/perf/renderDebug.js";
10
+ import { useTheme } from "./theme.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "./layout.js";
12
+ import { getTextWidth } from "./textLayout.js";
13
+ import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
14
+ import {
15
+ LOGO_LARGE,
16
+ LOGO_COMPACT,
17
+ LOGO_COMPACT_MIN_COLS,
18
+ LOGO_LARGE_MIN_COLS,
19
+ LOGO_LARGE_MIN_ROWS,
20
+ getLogoWidth,
21
+ } from "./logoVariants.js";
22
+
23
+ // Re-exported for backward compatibility with existing tests.
24
+ export const HEADER_WORDMARK_LINES = LOGO_LARGE;
25
+
26
+ const HEADER_PADDING_COLUMNS = 2;
27
+ const SHELL_GUTTER_COLUMNS = 1;
28
+ // Require 130+ cols for wide side-by-side so the UpdateAvailableCard has room.
29
+ const WIDE_HEADER_MIN_COLUMNS = 130;
30
+ // Require 72+ cols for medium side-by-side canonical-logo layout.
31
+ const MEDIUM_HEADER_MIN_COLUMNS = LOGO_LARGE_MIN_COLS;
32
+ const MIN_SIDE_BY_SIDE_METADATA_WIDTH = 18;
33
+ const STACKED_METADATA_GAP_ROWS = 1;
34
+ // Gap row between the UpdateAvailableCard and the metadata lines.
35
+ const UPDATE_CARD_GAP_ROWS = 1;
36
+ // Recommended terminal size shown in the compact-mode hint when the viewport is
37
+ // too small to render any logo art.
38
+ const RECOMMENDED_FULL_HEADER_HINT = `Resize to ≥${LOGO_LARGE_MIN_COLS}×${LOGO_LARGE_MIN_ROWS} for the full Codexa header`;
39
+
40
+ export type HeaderHeroMode = "wide" | "medium" | "narrow" | "compact";
41
+
42
+ export interface HeaderHeroLayout {
43
+ mode: HeaderHeroMode;
44
+ topMarginRows: number;
45
+ bottomMarginRows: number;
46
+ metadataGapColumns: number;
47
+ metadataGapRows: number;
48
+ logoRows: number;
49
+ metadataRows: number;
50
+ // Extra rows used by the compact-mode recommended-size hint (0 in non-compact
51
+ // modes). Threaded through so measureTopHeaderRows matches what renders.
52
+ compactHintRows: number;
53
+ totalRows: number;
54
+ }
55
+
56
+ type HeaderMetadataLine = { key: string; text: string; color: string; bold: boolean };
57
+
58
+ export interface UpdateAvailableInfo {
59
+ latestVersion: string;
60
+ currentVersion: string;
61
+ }
62
+
63
+ interface TopHeaderProps {
64
+ authState: CodexAuthState;
65
+ workspaceLabel: string;
66
+ layout: Layout;
67
+ runtimeSummary?: RuntimeSummary | null;
68
+ headerConfig?: HeaderConfig;
69
+ updateAvailable?: UpdateAvailableInfo | null;
70
+ }
71
+
72
+ function getHeaderContentWidth(cols: number): number {
73
+ return Math.max(1, cols - SHELL_GUTTER_COLUMNS - HEADER_PADDING_COLUMNS);
74
+ }
75
+
76
+ function getMetadataRowCount(headerConfig: HeaderConfig): number {
77
+ return [
78
+ headerConfig.showBrand,
79
+ headerConfig.showAuthStatus,
80
+ headerConfig.showWorkspace,
81
+ headerConfig.showProvider,
82
+ ].filter(Boolean).length;
83
+ }
84
+
85
+ function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
86
+ if (!isDecorativeLayoutMode(layout.mode)) {
87
+ return {
88
+ topMarginRows: 0,
89
+ bottomMarginRows: 0,
90
+ };
91
+ }
92
+
93
+ if (layout.rows <= 24) {
94
+ return { topMarginRows: 0, bottomMarginRows: 0 };
95
+ }
96
+
97
+ return {
98
+ topMarginRows: 1,
99
+ bottomMarginRows: 0,
100
+ };
101
+ }
102
+
103
+ export function selectHeaderLogo(layout: Layout): readonly string[] {
104
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return [];
105
+ const showNormalLogo =
106
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
107
+ layout.mode === "regular" ||
108
+ layout.mode === "expanded" ||
109
+ (layout.mode === "compact" && layout.cols >= 72)
110
+ );
111
+ if (!showNormalLogo) return [];
112
+ if (process.env["CODEXA_COMPACT_LOGO"] === "1") return LOGO_COMPACT;
113
+ return LOGO_LARGE;
114
+ }
115
+
116
+ export function getHeaderHeroLayout(
117
+ layout: Layout,
118
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
119
+ hasUpdate = false,
120
+ ): HeaderHeroLayout {
121
+ const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
122
+ const metadataRows = getMetadataRowCount(headerConfig);
123
+ const contentWidth = getHeaderContentWidth(layout.cols);
124
+
125
+ const showNormalLogo =
126
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
127
+ layout.mode === "regular" ||
128
+ layout.mode === "expanded" ||
129
+ (layout.mode === "compact" && layout.cols >= 72)
130
+ );
131
+
132
+ const placeMetadataBesideLogo = showNormalLogo && layout.cols >= 95;
133
+
134
+ let mode: HeaderHeroMode;
135
+ if (!showNormalLogo) {
136
+ mode = "compact";
137
+ } else if (placeMetadataBesideLogo) {
138
+ mode = layout.cols >= 130 ? "wide" : "medium";
139
+ } else {
140
+ mode = "narrow";
141
+ }
142
+
143
+ const logo = selectHeaderLogo(layout);
144
+ const logoWidth = logo.length > 0 ? getLogoWidth(logo) : 0;
145
+
146
+ if (mode === "compact") {
147
+ // Compact / micro text-only header.
148
+ return {
149
+ mode: "compact",
150
+ topMarginRows,
151
+ bottomMarginRows,
152
+ metadataGapColumns: 0,
153
+ metadataGapRows: 0,
154
+ logoRows: 1,
155
+ metadataRows: 0,
156
+ compactHintRows: 0,
157
+ totalRows: topMarginRows + 1 + bottomMarginRows,
158
+ };
159
+ }
160
+
161
+ const logoRowCount = logo.length;
162
+ const metadataGapColumns = layout.cols >= WIDE_HEADER_MIN_COLUMNS ? 4 : 2;
163
+ const metadataGapRows = mode === "narrow" && metadataRows > 0 ? STACKED_METADATA_GAP_ROWS : 0;
164
+
165
+ const isSideBySide = mode === "wide" || mode === "medium";
166
+
167
+ // In side-by-side mode the right column grows to: update card + gap + metadata.
168
+ // In narrow/stacked mode, the update card is a single extra line below metadata.
169
+ const rightColRows = isSideBySide
170
+ ? (hasUpdate ? UPDATE_CARD_ROWS + UPDATE_CARD_GAP_ROWS : 0) + metadataRows
171
+ : 0;
172
+
173
+ const contentRows = isSideBySide
174
+ ? Math.max(logoRowCount, rightColRows)
175
+ : logoRowCount + metadataGapRows + metadataRows + (hasUpdate ? STACKED_METADATA_GAP_ROWS + 1 : 0);
176
+
177
+ return {
178
+ mode,
179
+ topMarginRows,
180
+ bottomMarginRows,
181
+ metadataGapColumns,
182
+ metadataGapRows,
183
+ logoRows: logoRowCount,
184
+ metadataRows,
185
+ compactHintRows: 0,
186
+ totalRows: topMarginRows + contentRows + bottomMarginRows,
187
+ };
188
+ }
189
+
190
+ export function measureTopHeaderRows(
191
+ layout: Layout,
192
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
193
+ hasUpdate = false,
194
+ ): number {
195
+ return getHeaderHeroLayout(layout, headerConfig, hasUpdate).totalRows;
196
+ }
197
+
198
+ function takeVisualSuffix(text: string, maxWidth: number): string {
199
+ if (maxWidth <= 0) return "";
200
+ let output = "";
201
+
202
+ for (const char of Array.from(text).reverse()) {
203
+ if (getTextWidth(char + output) > maxWidth) break;
204
+ output = char + output;
205
+ }
206
+
207
+ return output;
208
+ }
209
+
210
+ export function shortenHeaderWorkspaceLabel(workspaceLabel: string, maxWidth: number): string {
211
+ const trimmed = workspaceLabel.trim();
212
+ if (!trimmed || maxWidth <= 0) return "";
213
+ if (getTextWidth(trimmed) <= maxWidth) return trimmed;
214
+ if (maxWidth <= 1) return "…";
215
+
216
+ const normalized = trimmed.replace(/[\\/]+$/, "");
217
+ const separatorMatch = normalized.match(/[\\/]/g);
218
+ const separator = normalized.includes("\\") && (!separatorMatch || normalized.lastIndexOf("\\") >= normalized.lastIndexOf("/"))
219
+ ? "\\"
220
+ : "/";
221
+ const lastSlash = Math.max(normalized.lastIndexOf("\\"), normalized.lastIndexOf("/"));
222
+ const leaf = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
223
+ const prefix = lastSlash >= 0 ? `…${separator}` : "…";
224
+ const leafWidth = Math.max(1, maxWidth - getTextWidth(prefix));
225
+
226
+ return prefix + takeVisualSuffix(leaf, leafWidth);
227
+ }
228
+
229
+ function clampMetadataText(text: string, maxWidth: number): string {
230
+ return clampVisualText(text, Math.max(1, maxWidth));
231
+ }
232
+
233
+ export function TopHeader({
234
+ authState,
235
+ workspaceLabel,
236
+ layout,
237
+ runtimeSummary = null,
238
+ headerConfig = HEADER_CONFIG_DEFAULTS,
239
+ updateAvailable = null,
240
+ }: TopHeaderProps) {
241
+ renderDebug.useRenderDebug("Header", {
242
+ authState,
243
+ workspaceLabel,
244
+ cols: layout.cols,
245
+ rows: layout.rows,
246
+ mode: layout.mode,
247
+ });
248
+ renderDebug.useLifecycleDebug("Header", {
249
+ authState,
250
+ cols: layout.cols,
251
+ rows: layout.rows,
252
+ mode: layout.mode,
253
+ });
254
+
255
+ const theme = useTheme();
256
+
257
+ const authLabelRaw = getAuthStateLabel(authState);
258
+ const authLabel = authLabelRaw.length > 0
259
+ ? authLabelRaw[0]!.toUpperCase() + authLabelRaw.slice(1)
260
+ : authLabelRaw;
261
+
262
+ const heroLayout = getHeaderHeroLayout(layout, headerConfig, !!updateAvailable);
263
+ const selectedLogo = selectHeaderLogo(layout);
264
+ const selectedLogoWidth = selectedLogo.length > 0 ? getLogoWidth(selectedLogo) : 0;
265
+
266
+ const contentWidth = getHeaderContentWidth(layout.cols);
267
+ const sideBySideMetadataWidth = Math.max(1, contentWidth - selectedLogoWidth - heroLayout.metadataGapColumns);
268
+ const metadataWidth = heroLayout.mode === "wide" || heroLayout.mode === "medium"
269
+ ? sideBySideMetadataWidth
270
+ : contentWidth;
271
+ const workspaceValueWidth = Math.max(1, metadataWidth - getTextWidth("Workspace: "));
272
+ const wsDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, workspaceValueWidth);
273
+ const brandLabel = formatCodexaBrandLabel();
274
+ const metadataLinesRaw = [
275
+ headerConfig.showBrand ? { key: "brand", text: brandLabel, color: theme.text, bold: true } : null,
276
+ headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.text, bold: false } : null,
277
+ headerConfig.showWorkspace ? { key: "workspace", text: `Workspace: ${wsDisplay}`, color: theme.textMuted, bold: false } : null,
278
+ headerConfig.showProvider && runtimeSummary?.providerLabel
279
+ ? { key: "provider", text: `Provider: ${runtimeSummary.providerLabel}`, color: theme.text, bold: false }
280
+ : null,
281
+ ].filter((line): line is HeaderMetadataLine => Boolean(line));
282
+ const metadataLines = metadataLinesRaw.map((line) => ({
283
+ ...line,
284
+ text: clampMetadataText(line.text, metadataWidth),
285
+ }));
286
+
287
+ // Add a 1-row gap between the version line and workspace line in wide mode
288
+ // so the two pieces of metadata have breathing room beside the logo.
289
+ const hasMetadataGap = heroLayout.mode === "wide"
290
+ && metadataLines.length <= 3
291
+ && metadataLines.some((l) => l.key === "brand")
292
+ && metadataLines.some((l) => l.key === "workspace");
293
+ const metadataColumn = (
294
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} width={metadataWidth}>
295
+ {metadataLines.map((line) =>
296
+ hasMetadataGap && line.key === "workspace" ? (
297
+ <Box key={line.key} marginTop={1}>
298
+ <Text color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
299
+ </Box>
300
+ ) : (
301
+ <Text key={line.key} color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
302
+ )
303
+ )}
304
+ </Box>
305
+ );
306
+
307
+ // Canonical CODEXA wordmark — uses per-line LOGO palette defined in each theme.
308
+ // No `bold`: bold on Unicode block/box-drawing characters causes per-glyph
309
+ // spacing artifacts in common terminal fonts (Ptyxis, GNOME Terminal).
310
+ // wrap="truncate" keeps each row on exactly one terminal line.
311
+ const logoColumn = (
312
+ <Box flexDirection="column" flexShrink={0}>
313
+ {selectedLogo.map((line, i) => {
314
+ let lineColor = theme.logoPrimary;
315
+ if (selectedLogo.length === 6) {
316
+ if (i === 2 || i === 3) lineColor = theme.logoSecondary;
317
+ else if (i === 4 || i === 5) lineColor = theme.logoShadow;
318
+ }
319
+ return (
320
+ <Text key={i} color={lineColor} wrap="truncate">{line}</Text>
321
+ );
322
+ })}
323
+ </Box>
324
+ );
325
+
326
+ if (heroLayout.mode !== "compact") {
327
+ const isSideBySide = heroLayout.mode === "wide" || heroLayout.mode === "medium";
328
+ const metadataTopOffset = 0;
329
+
330
+ return (
331
+ <Box flexDirection="column" paddingX={1} width="100%">
332
+ {heroLayout.topMarginRows > 0 && (
333
+ <Box height={heroLayout.topMarginRows} />
334
+ )}
335
+
336
+ {isSideBySide ? (
337
+ <Box flexDirection="row" width="100%" alignItems="flex-start">
338
+ {logoColumn}
339
+ <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
340
+ <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
341
+ {updateAvailable && (
342
+ <>
343
+ <UpdateAvailableCard
344
+ latestVersion={updateAvailable.latestVersion}
345
+ currentVersion={updateAvailable.currentVersion}
346
+ width={metadataWidth}
347
+ />
348
+ <Box height={UPDATE_CARD_GAP_ROWS} />
349
+ </>
350
+ )}
351
+ {metadataColumn}
352
+ </Box>
353
+ </Box>
354
+ ) : (
355
+ <Box flexDirection="column" width="100%">
356
+ {logoColumn}
357
+ {heroLayout.metadataGapRows > 0 && (
358
+ <Box height={heroLayout.metadataGapRows} />
359
+ )}
360
+ {metadataColumn}
361
+ {updateAvailable && (
362
+ <Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: npm install -g @golba98/codexa@latest`}</Text>
363
+ )}
364
+ </Box>
365
+ )}
366
+
367
+ {heroLayout.bottomMarginRows > 0 && (
368
+ <Box height={heroLayout.bottomMarginRows} />
369
+ )}
370
+ </Box>
371
+ );
372
+ }
373
+
374
+ // Compact / micro / activity-collapsed: single-line header.
375
+ const compactMetadataWidth = contentWidth;
376
+ const compactWorkspaceValueWidth = Math.max(1, compactMetadataWidth - getTextWidth("Workspace: "));
377
+ const compactWorkspaceDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, compactWorkspaceValueWidth);
378
+ // A leading ✦ accent makes the single-line header read as a deliberate
379
+ // compact Codexa header rather than a broken fallback.
380
+ const compactParts: React.ReactNode[] = [
381
+ <Text key="accent" color={theme.accent} bold>{"✦ "}</Text>,
382
+ ];
383
+ if (headerConfig.showBrand) {
384
+ compactParts.push(
385
+ <Text key="brand" color={theme.text} bold>{brandLabel}</Text>,
386
+ );
387
+ }
388
+ if (headerConfig.showAuthStatus) {
389
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-auth" color={theme.textDim}>{" · "}</Text>);
390
+ compactParts.push(<Text key="auth" color={theme.text}>{authLabel}</Text>);
391
+ }
392
+ if (headerConfig.showWorkspace) {
393
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-ws" color={theme.textDim}>{" · "}</Text>);
394
+ compactParts.push(<Text key="ws" color={theme.textMuted} wrap="truncate">{`Workspace: ${compactWorkspaceDisplay}`}</Text>);
395
+ }
396
+ if (headerConfig.showProvider && runtimeSummary?.providerLabel) {
397
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-provider" color={theme.textDim}>{" · "}</Text>);
398
+ compactParts.push(<Text key="provider" color={theme.text} wrap="truncate">{`Provider: ${runtimeSummary.providerLabel}`}</Text>);
399
+ }
400
+
401
+ return (
402
+ <Box flexDirection="column" paddingX={1} width="100%">
403
+ {heroLayout.topMarginRows > 0 && (
404
+ <Box height={heroLayout.topMarginRows} />
405
+ )}
406
+ <Box flexDirection="row" width="100%">
407
+ {compactParts}
408
+ </Box>
409
+ {heroLayout.compactHintRows > 0 && (
410
+ <Text color={theme.textDim} wrap="truncate">{clampMetadataText(RECOMMENDED_FULL_HEADER_HINT, compactMetadataWidth)}</Text>
411
+ )}
412
+ {heroLayout.bottomMarginRows > 0 && (
413
+ <Box height={heroLayout.bottomMarginRows} />
414
+ )}
415
+ </Box>
416
+ );
417
+ }
418
+
419
+ // Memoize to prevent re-renders during streaming when props haven't changed
420
+ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
421
+ return (
422
+ prev.authState === next.authState &&
423
+ prev.workspaceLabel === next.workspaceLabel &&
424
+ prev.layout.cols === next.layout.cols &&
425
+ prev.layout.rows === next.layout.rows &&
426
+ prev.layout.mode === next.layout.mode &&
427
+ prev.runtimeSummary === next.runtimeSummary &&
428
+ prev.headerConfig === next.headerConfig &&
429
+ prev.updateAvailable === next.updateAvailable
430
+ );
431
+ });
432
+
433
+ // Minimum terminal cols to render any logo art (the compact 1-row variant).
434
+ export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
435
+
436
+ // Re-export for consumers that reference these constants directly.
437
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "./logoVariants.js";