@golba98/codexa 1.0.1 → 1.0.3

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 (234) hide show
  1. package/README.md +391 -94
  2. package/package.json +14 -8
  3. package/src/app.tsx +196 -63
  4. package/src/commands/handler.ts +9 -4
  5. package/src/config/buildInfo.ts +3 -0
  6. package/src/config/launchArgs.ts +10 -0
  7. package/src/config/layeredConfig.ts +0 -0
  8. package/src/config/persistence.ts +51 -0
  9. package/src/config/runtimeConfig.ts +4 -0
  10. package/src/config/settings.ts +6 -13
  11. package/src/config/toml-serialize.ts +0 -0
  12. package/src/config/trustStore.ts +0 -0
  13. package/src/config/updateCheckCache.ts +63 -0
  14. package/src/core/attachments.ts +0 -0
  15. package/src/core/auth/codexAuth.ts +0 -0
  16. package/src/core/channel.ts +23 -0
  17. package/src/core/cleanupFastFail.ts +0 -0
  18. package/src/core/clipboard.ts +0 -0
  19. package/src/core/codex.ts +0 -0
  20. package/src/core/codexExecArgs.ts +0 -0
  21. package/src/core/codexLaunch.ts +0 -0
  22. package/src/core/codexPrompt.ts +0 -0
  23. package/src/core/executables/claudeExecutable.ts +0 -0
  24. package/src/core/executables/codexExecutable.ts +1 -0
  25. package/src/core/executables/executableResolver.ts +61 -43
  26. package/src/core/executables/geminiExecutable.ts +0 -0
  27. package/src/core/githubDiagnostics.ts +0 -0
  28. package/src/core/hollowResponseFormat.ts +0 -0
  29. package/src/core/inputDebug.ts +0 -0
  30. package/src/core/launchContext.ts +33 -33
  31. package/src/core/models/codexCapabilities.ts +0 -0
  32. package/src/core/models/codexModelCapabilities.ts +0 -0
  33. package/src/core/models/modelSpecs.ts +0 -0
  34. package/src/core/perf/profiler.ts +0 -0
  35. package/src/core/perf/renderDebug.ts +0 -0
  36. package/src/core/planStorage.ts +0 -0
  37. package/src/core/process/CommandRunner.ts +0 -0
  38. package/src/core/process/processValidation.ts +9 -5
  39. package/src/core/projectInstructions.ts +0 -0
  40. package/src/core/providerLauncher/launcher.ts +59 -42
  41. package/src/core/providerLauncher/registry.ts +0 -0
  42. package/src/core/providerLauncher/types.ts +0 -0
  43. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  44. package/src/core/providerRuntime/anthropic.ts +0 -0
  45. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  46. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  47. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  48. package/src/core/providerRuntime/gemini.ts +0 -0
  49. package/src/core/providerRuntime/lmstudio.ts +0 -0
  50. package/src/core/providerRuntime/local.ts +0 -0
  51. package/src/core/providerRuntime/models.ts +0 -0
  52. package/src/core/providerRuntime/reasoning.ts +0 -0
  53. package/src/core/providerRuntime/registry.ts +0 -0
  54. package/src/core/providerRuntime/types.ts +0 -0
  55. package/src/core/providers/codexJsonStream.ts +0 -0
  56. package/src/core/providers/codexSubprocess.ts +0 -0
  57. package/src/core/providers/codexTranscript.ts +0 -0
  58. package/src/core/providers/openaiNative.ts +0 -0
  59. package/src/core/providers/registry.ts +0 -0
  60. package/src/core/providers/types.ts +0 -0
  61. package/src/core/terminal/startupClear.ts +20 -0
  62. package/src/core/terminal/terminalCapabilities.ts +0 -0
  63. package/src/core/terminal/terminalControl.ts +0 -0
  64. package/src/core/terminal/terminalSanitize.ts +0 -0
  65. package/src/core/terminal/terminalSelection.ts +0 -0
  66. package/src/core/terminal/terminalTitle.ts +0 -0
  67. package/src/core/updateCheck.ts +188 -0
  68. package/src/core/workspaceActivity.ts +0 -0
  69. package/src/core/workspaceGuard.ts +0 -0
  70. package/src/core/workspaceRoot.ts +0 -0
  71. package/src/exec.ts +0 -0
  72. package/src/headless/execArgs.ts +1 -0
  73. package/src/headless/execRunner.ts +0 -0
  74. package/src/index.tsx +13 -9
  75. package/src/session/appSession.ts +0 -0
  76. package/src/session/chatLifecycle.ts +0 -0
  77. package/src/session/liveRenderScheduler.ts +0 -0
  78. package/src/session/planFlow.ts +0 -0
  79. package/src/session/planTranscript.ts +0 -0
  80. package/src/session/promptRunSchedule.ts +0 -0
  81. package/src/session/types.ts +2 -1
  82. package/src/test/runtimeTestUtils.ts +0 -0
  83. package/src/types/react-dom.d.ts +0 -0
  84. package/src/ui/ActionRequiredBlock.tsx +5 -5
  85. package/src/ui/ActivityBars.tsx +3 -3
  86. package/src/ui/ActivityIndicator.tsx +6 -6
  87. package/src/ui/AgentBlock.tsx +6 -6
  88. package/src/ui/AnimatedStatusText.tsx +1 -1
  89. package/src/ui/AppShell.tsx +13 -5
  90. package/src/ui/AttachmentImportPanel.tsx +8 -8
  91. package/src/ui/AuthPanel.tsx +20 -20
  92. package/src/ui/BackendPicker.tsx +0 -0
  93. package/src/ui/BottomComposer.tsx +117 -78
  94. package/src/ui/DashCard.tsx +3 -3
  95. package/src/ui/Markdown.tsx +17 -17
  96. package/src/ui/ModePicker.tsx +0 -0
  97. package/src/ui/ModelPicker.tsx +0 -0
  98. package/src/ui/ModelPickerScreen.tsx +10 -10
  99. package/src/ui/ModelReasoningPicker.tsx +14 -14
  100. package/src/ui/Panel.tsx +3 -3
  101. package/src/ui/PermissionsPanel.tsx +0 -0
  102. package/src/ui/PlanActionPicker.tsx +6 -6
  103. package/src/ui/PlanReviewPanel.tsx +8 -8
  104. package/src/ui/ProviderPicker.tsx +18 -18
  105. package/src/ui/ReasoningPicker.tsx +0 -0
  106. package/src/ui/RunFooter.tsx +3 -3
  107. package/src/ui/SelectionPanel.tsx +4 -4
  108. package/src/ui/SettingsPanel.tsx +8 -8
  109. package/src/ui/Spinner.tsx +1 -1
  110. package/src/ui/StaticIntroItem.tsx +0 -0
  111. package/src/ui/StaticTranscriptItem.tsx +0 -0
  112. package/src/ui/TextEntryPanel.tsx +10 -10
  113. package/src/ui/ThemePicker.tsx +0 -0
  114. package/src/ui/ThinkingBlock.tsx +8 -8
  115. package/src/ui/Timeline.tsx +21 -17
  116. package/src/ui/TopHeader.tsx +421 -257
  117. package/src/ui/TurnGroup.tsx +31 -31
  118. package/src/ui/UpdateAvailableCard.tsx +41 -0
  119. package/src/ui/UpdatePromptPanel.tsx +203 -0
  120. package/src/ui/busyStatusAnimation.ts +0 -0
  121. package/src/ui/commandNormalize.ts +0 -0
  122. package/src/ui/diffRenderer.ts +0 -0
  123. package/src/ui/focus.ts +3 -0
  124. package/src/ui/inputBuffer.ts +0 -0
  125. package/src/ui/layout.ts +1 -1
  126. package/src/ui/logoVariants.ts +107 -0
  127. package/src/ui/modeDisplay.ts +12 -12
  128. package/src/ui/outputPipeline.ts +0 -0
  129. package/src/ui/progressEntries.ts +0 -0
  130. package/src/ui/runActivityView.ts +0 -0
  131. package/src/ui/runtimeDisplay.ts +100 -0
  132. package/src/ui/slashCommands.ts +0 -0
  133. package/src/ui/terminalAnswerFormat.ts +0 -0
  134. package/src/ui/textLayout.ts +0 -0
  135. package/src/ui/theme.tsx +274 -395
  136. package/src/ui/themeFlow.ts +0 -0
  137. package/src/ui/timelineMeasure.ts +29 -19
  138. package/src/ui/useThrottledValue.ts +0 -0
  139. package/scripts/audit-codexa-capabilities.mjs +0 -466
  140. package/scripts/smoke-terminal-bench.mjs +0 -35
  141. package/src/appRenderStability.test.ts +0 -131
  142. package/src/commands/handler.test.ts +0 -643
  143. package/src/config/launchArgs.test.ts +0 -158
  144. package/src/config/layeredConfig.test.ts +0 -143
  145. package/src/config/persistence.test.ts +0 -110
  146. package/src/config/runtimeConfig.test.ts +0 -218
  147. package/src/config/settings.test.ts +0 -155
  148. package/src/config/trustStore.test.ts +0 -29
  149. package/src/core/attachments.test.ts +0 -155
  150. package/src/core/auth/codexAuth.test.ts +0 -68
  151. package/src/core/cleanupFastFail.test.ts +0 -76
  152. package/src/core/codexExecArgs.test.ts +0 -195
  153. package/src/core/codexLaunch.test.ts +0 -205
  154. package/src/core/codexPrompt.test.ts +0 -252
  155. package/src/core/executables/codexExecutable.test.ts +0 -212
  156. package/src/core/executables/executableResolver.test.ts +0 -129
  157. package/src/core/executables/geminiExecutable.test.ts +0 -116
  158. package/src/core/executables/pathSanityScan.test.ts +0 -47
  159. package/src/core/githubDiagnostics.test.ts +0 -92
  160. package/src/core/hollowResponseFormat.test.ts +0 -58
  161. package/src/core/launchContext.test.ts +0 -157
  162. package/src/core/models/codexCapabilities.test.ts +0 -45
  163. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  164. package/src/core/models/modelSpecs.test.ts +0 -283
  165. package/src/core/perf/renderDebug.test.ts +0 -230
  166. package/src/core/planStorage.test.ts +0 -143
  167. package/src/core/process/CommandRunner.test.ts +0 -105
  168. package/src/core/projectInstructions.test.ts +0 -50
  169. package/src/core/providerLauncher/launcher.test.ts +0 -238
  170. package/src/core/providerLauncher/registry.test.ts +0 -324
  171. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  172. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  173. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  174. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  175. package/src/core/providerRuntime/gemini.test.ts +0 -437
  176. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  177. package/src/core/providerRuntime/local.test.ts +0 -787
  178. package/src/core/providerRuntime/registry.test.ts +0 -233
  179. package/src/core/providers/codexJsonStream.test.ts +0 -148
  180. package/src/core/providers/codexSubprocess.test.ts +0 -68
  181. package/src/core/providers/codexTranscript.test.ts +0 -284
  182. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  183. package/src/core/terminal/terminalControl.test.ts +0 -75
  184. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  185. package/src/core/terminal/terminalSelection.test.ts +0 -42
  186. package/src/core/terminal/terminalTitle.test.ts +0 -328
  187. package/src/core/workspaceActivity.test.ts +0 -163
  188. package/src/core/workspaceGuard.test.ts +0 -151
  189. package/src/core/workspaceRoot.test.ts +0 -23
  190. package/src/exec.test.ts +0 -13
  191. package/src/headless/execArgs.test.ts +0 -147
  192. package/src/headless/execRunner.test.ts +0 -434
  193. package/src/index.test.tsx +0 -618
  194. package/src/session/appSession.test.ts +0 -897
  195. package/src/session/chatLifecycle.test.ts +0 -64
  196. package/src/session/liveRenderScheduler.test.ts +0 -201
  197. package/src/session/planFlow.test.ts +0 -103
  198. package/src/session/planTranscript.test.ts +0 -65
  199. package/src/session/promptRunSchedule.test.ts +0 -36
  200. package/src/ui/ActivityIndicator.test.tsx +0 -58
  201. package/src/ui/AgentBlock.test.ts +0 -6
  202. package/src/ui/AnimatedStatusText.test.ts +0 -16
  203. package/src/ui/AppShell.test.tsx +0 -1739
  204. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  205. package/src/ui/BottomComposer.test.ts +0 -674
  206. package/src/ui/CodexLogo.tsx +0 -55
  207. package/src/ui/Markdown.test.ts +0 -157
  208. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  209. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  210. package/src/ui/ModelPickerState.test.tsx +0 -151
  211. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  212. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  213. package/src/ui/PromptCardBorder.test.tsx +0 -161
  214. package/src/ui/ProviderPicker.test.tsx +0 -289
  215. package/src/ui/ProviderShortcut.test.tsx +0 -143
  216. package/src/ui/SettingsPanel.test.tsx +0 -233
  217. package/src/ui/Timeline.test.ts +0 -2067
  218. package/src/ui/TimelineNavigation.test.tsx +0 -201
  219. package/src/ui/TopHeader.test.tsx +0 -239
  220. package/src/ui/TurnGroup.test.tsx +0 -365
  221. package/src/ui/busyStatusAnimation.test.ts +0 -30
  222. package/src/ui/commandNormalize.test.ts +0 -142
  223. package/src/ui/diffRenderer.test.ts +0 -102
  224. package/src/ui/focusFlow.test.tsx +0 -1098
  225. package/src/ui/inputBuffer.test.ts +0 -151
  226. package/src/ui/layout.test.ts +0 -145
  227. package/src/ui/modeDisplay.test.ts +0 -42
  228. package/src/ui/runActivityView.test.ts +0 -89
  229. package/src/ui/runLifecycleView.test.tsx +0 -237
  230. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  231. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  232. package/src/ui/textLayout.test.ts +0 -18
  233. package/src/ui/themeFlow.test.ts +0 -53
  234. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -1,257 +1,421 @@
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
-
24
- export type HeaderHeroMode = "wide" | "stacked" | "compact";
25
-
26
- export interface HeaderHeroLayout {
27
- mode: HeaderHeroMode;
28
- topMarginRows: number;
29
- bottomMarginRows: number;
30
- metadataGapColumns: number;
31
- metadataGapRows: number;
32
- logoRows: number;
33
- metadataRows: number;
34
- totalRows: number;
35
- }
36
-
37
-
38
- interface TopHeaderProps {
39
- authState: CodexAuthState;
40
- workspaceLabel: string;
41
- layout: Layout;
42
- runtimeSummary?: RuntimeSummary | null;
43
- headerConfig?: HeaderConfig;
44
- }
45
-
46
- function getWordmarkWidth(): number {
47
- return HEADER_WORDMARK_LINES.reduce((maxWidth, line) => Math.max(maxWidth, getTextWidth(line)), 0);
48
- }
49
-
50
- function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
51
- if (layout.mode !== "full") {
52
- return {
53
- topMarginRows: 0,
54
- bottomMarginRows: layout.rows > 24 ? 1 : 0,
55
- };
56
- }
57
-
58
- if (layout.rows <= 24) {
59
- return { topMarginRows: 0, bottomMarginRows: 1 };
60
- }
61
-
62
- return {
63
- topMarginRows: 1,
64
- bottomMarginRows: layout.rows >= 36 ? 2 : 1,
65
- };
66
- }
67
-
68
- export function getHeaderHeroLayout(layout: Layout): HeaderHeroLayout {
69
- const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
70
- const metadataRows = 3;
71
-
72
- if (layout.mode !== "full") {
73
- return {
74
- mode: "compact",
75
- topMarginRows,
76
- bottomMarginRows,
77
- metadataGapColumns: 0,
78
- metadataGapRows: 0,
79
- logoRows: 1,
80
- metadataRows: 0,
81
- totalRows: topMarginRows + 1 + bottomMarginRows,
82
- };
83
- }
84
-
85
- const logoRows = HEADER_WORDMARK_LINES.length;
86
- const metadataGapColumns = layout.cols >= 150 ? 6 : layout.cols >= 130 ? 5 : 4;
87
- const canUseWideHero = layout.cols >= getWordmarkWidth() + metadataGapColumns + MIN_METADATA_COLUMN_WIDTH + 2;
88
- const metadataGapRows = canUseWideHero ? 0 : STACKED_METADATA_GAP_ROWS;
89
- const contentRows = canUseWideHero
90
- ? logoRows
91
- : logoRows + metadataGapRows + metadataRows;
92
-
93
- return {
94
- mode: canUseWideHero ? "wide" : "stacked",
95
- topMarginRows,
96
- bottomMarginRows,
97
- metadataGapColumns,
98
- metadataGapRows,
99
- logoRows,
100
- metadataRows,
101
- totalRows: topMarginRows + contentRows + bottomMarginRows,
102
- };
103
- }
104
-
105
- export function measureTopHeaderRows(layout: Layout): number {
106
- return getHeaderHeroLayout(layout).totalRows;
107
- }
108
-
109
- /** Truncate a path to fit within maxWidth, keeping the end and prefixing with "... " if needed */
110
- function truncatePath(path: string, maxWidth: number): string {
111
- if (maxWidth <= 4 || path.length <= maxWidth) return path;
112
- // Prefix with "... " (4 chars) and take the last (maxWidth - 4) chars
113
- return "... " + path.slice(path.length - (maxWidth - 4));
114
- }
115
-
116
- export function TopHeader({
117
- authState,
118
- workspaceLabel,
119
- layout,
120
- headerConfig = HEADER_CONFIG_DEFAULTS,
121
- }: TopHeaderProps) {
122
- renderDebug.useRenderDebug("Header", {
123
- authState,
124
- workspaceLabel,
125
- cols: layout.cols,
126
- rows: layout.rows,
127
- mode: layout.mode,
128
- });
129
- renderDebug.useLifecycleDebug("Header", {
130
- authState,
131
- cols: layout.cols,
132
- rows: layout.rows,
133
- mode: layout.mode,
134
- });
135
-
136
- const { cols, mode } = layout;
137
- const theme = useTheme();
138
-
139
- const authLabelRaw = getAuthStateLabel(authState);
140
- const authLabel = authLabelRaw.length > 0
141
- ? authLabelRaw[0]!.toUpperCase() + authLabelRaw.slice(1)
142
- : authLabelRaw;
143
-
144
- const wsDisplay = truncatePath(workspaceLabel, Math.max(18, cols - 40));
145
- const heroLayout = getHeaderHeroLayout(layout);
146
- const metadataLines = [
147
- headerConfig.showBrand ? { key: "brand", text: `Codexa v${APP_VERSION}`, color: theme.TEXT, bold: true } : null,
148
- headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.TEXT, bold: false } : null,
149
- headerConfig.showWorkspace ? { key: "workspace", text: `Workspace: ${wsDisplay}`, color: theme.MUTED, bold: false } : null,
150
- ].filter((line): line is { key: string; text: string; color: string; bold: boolean } => Boolean(line));
151
-
152
- // Add a 1-row gap between the version line and workspace line in wide mode
153
- // so the two pieces of metadata have breathing room beside the logo.
154
- const hasMetadataGap = heroLayout.mode === "wide"
155
- && metadataLines.some((l) => l.key === "brand")
156
- && metadataLines.some((l) => l.key === "workspace");
157
- const metadataVisualRows = metadataLines.length + (hasMetadataGap ? 1 : 0);
158
-
159
- const metadataColumn = (
160
- <Box flexDirection="column" flexGrow={1} minWidth={Math.min(MIN_METADATA_COLUMN_WIDTH, Math.max(1, cols - getWordmarkWidth() - heroLayout.metadataGapColumns - 2))}>
161
- {metadataLines.map((line) =>
162
- hasMetadataGap && line.key === "workspace" ? (
163
- <Box key={line.key} marginTop={1}>
164
- <Text color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
165
- </Box>
166
- ) : (
167
- <Text key={line.key} color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
168
- )
169
- )}
170
- </Box>
171
- );
172
-
173
- const logoColumn = (
174
- <Box flexDirection="column" flexShrink={0}>
175
- {HEADER_WORDMARK_LINES.map((line, i) => (
176
- <Text key={i} color={theme.ACCENT} bold>{line}</Text>
177
- ))}
178
- </Box>
179
- );
180
-
181
- if (mode === "full") {
182
- const metadataTopOffset = Math.max(0, Math.floor((HEADER_WORDMARK_LINES.length - metadataVisualRows) / 2));
183
-
184
- return (
185
- <Box flexDirection="column" paddingX={1} width="100%">
186
- {heroLayout.topMarginRows > 0 && (
187
- <Box height={heroLayout.topMarginRows} />
188
- )}
189
-
190
- {heroLayout.mode === "wide" ? (
191
- <Box flexDirection="row" width="100%">
192
- {logoColumn}
193
- <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
194
- <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
195
- {metadataColumn}
196
- </Box>
197
- </Box>
198
- ) : (
199
- <Box flexDirection="column" width="100%">
200
- {logoColumn}
201
- {heroLayout.metadataGapRows > 0 && (
202
- <Box height={heroLayout.metadataGapRows} />
203
- )}
204
- {metadataColumn}
205
- </Box>
206
- )}
207
-
208
- {heroLayout.bottomMarginRows > 0 && (
209
- <Box height={heroLayout.bottomMarginRows} />
210
- )}
211
- </Box>
212
- );
213
- }
214
-
215
- // Compact / micro / activity-collapsed: single-line header
216
- const compactParts: React.ReactNode[] = [];
217
- if (headerConfig.showBrand) {
218
- compactParts.push(
219
- <Text key="brand" color={theme.TEXT} bold>{`Codexa v${APP_VERSION}`}</Text>,
220
- );
221
- }
222
- if (headerConfig.showAuthStatus) {
223
- if (compactParts.length > 0) compactParts.push(<Text key="sep-auth" color={theme.DIM}>{" · "}</Text>);
224
- compactParts.push(<Text key="auth" color={theme.TEXT}>{authLabel}</Text>);
225
- }
226
- if (headerConfig.showWorkspace) {
227
- if (compactParts.length > 0) compactParts.push(<Text key="sep-ws" color={theme.DIM}>{" · "}</Text>);
228
- compactParts.push(<Text key="ws" color={theme.MUTED} wrap="truncate">{wsDisplay}</Text>);
229
- }
230
-
231
- return (
232
- <Box flexDirection="column" paddingX={1} width="100%">
233
- {heroLayout.topMarginRows > 0 && (
234
- <Box height={heroLayout.topMarginRows} />
235
- )}
236
- <Box flexDirection="row" width="100%">
237
- {compactParts}
238
- </Box>
239
- {heroLayout.bottomMarginRows > 0 && (
240
- <Box height={heroLayout.bottomMarginRows} />
241
- )}
242
- </Box>
243
- );
244
- }
245
-
246
- // Memoize to prevent re-renders during streaming when props haven't changed
247
- export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
248
- return (
249
- prev.authState === next.authState &&
250
- prev.workspaceLabel === next.workspaceLabel &&
251
- prev.layout.cols === next.layout.cols &&
252
- prev.layout.rows === next.layout.rows &&
253
- prev.layout.mode === next.layout.mode &&
254
- prev.runtimeSummary === next.runtimeSummary &&
255
- prev.headerConfig === next.headerConfig
256
- );
257
- });
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/channel.js";
5
+ import type { RuntimeSummary } from "../config/runtimeConfig.js";
6
+ import type { CodexAuthState } from "../core/auth/codexAuth.js";
7
+ import { getAuthStateLabel } from "../core/auth/codexAuth.js";
8
+ import * as renderDebug from "../core/perf/renderDebug.js";
9
+ import { useTheme } from "./theme.js";
10
+ import { clampVisualText, type Layout } from "./layout.js";
11
+ import { getTextWidth } from "./textLayout.js";
12
+ import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
13
+ import {
14
+ LOGO_LARGE,
15
+ LOGO_COMPACT_MIN_COLS,
16
+ LOGO_LARGE_MIN_COLS,
17
+ LOGO_LARGE_MIN_ROWS,
18
+ LOGO_MEDIUM_MIN_COLS,
19
+ selectLogoVariantForViewport,
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 (= LOGO_MEDIUM_MIN_COLS) for medium side-by-side — both
31
+ // LOGO_LARGE (≥100) and LOGO_MEDIUM (72–99) fit cleanly in side-by-side layout.
32
+ const MEDIUM_HEADER_MIN_COLUMNS = LOGO_MEDIUM_MIN_COLS; // 72
33
+ const MIN_SIDE_BY_SIDE_METADATA_WIDTH = 18;
34
+ const STACKED_METADATA_GAP_ROWS = 1;
35
+ // Gap row between the UpdateAvailableCard and the metadata lines.
36
+ const UPDATE_CARD_GAP_ROWS = 1;
37
+ // Recommended terminal size shown in the compact-mode hint when the viewport is
38
+ // too small to render any logo art.
39
+ const RECOMMENDED_FULL_HEADER_HINT = `Resize to ≥${LOGO_LARGE_MIN_COLS}×${LOGO_LARGE_MIN_ROWS} for the full Codexa header`;
40
+
41
+ export type HeaderHeroMode = "wide" | "medium" | "narrow" | "compact";
42
+
43
+ export interface HeaderHeroLayout {
44
+ mode: HeaderHeroMode;
45
+ topMarginRows: number;
46
+ bottomMarginRows: number;
47
+ metadataGapColumns: number;
48
+ metadataGapRows: number;
49
+ logoRows: number;
50
+ metadataRows: number;
51
+ // Extra rows used by the compact-mode recommended-size hint (0 in non-compact
52
+ // modes). Threaded through so measureTopHeaderRows matches what renders.
53
+ compactHintRows: number;
54
+ totalRows: number;
55
+ }
56
+
57
+ type HeaderMetadataLine = { key: string; text: string; color: string; bold: boolean };
58
+
59
+ export interface UpdateAvailableInfo {
60
+ latestVersion: string;
61
+ currentVersion: string;
62
+ }
63
+
64
+ interface TopHeaderProps {
65
+ authState: CodexAuthState;
66
+ workspaceLabel: string;
67
+ layout: Layout;
68
+ runtimeSummary?: RuntimeSummary | null;
69
+ headerConfig?: HeaderConfig;
70
+ updateAvailable?: UpdateAvailableInfo | null;
71
+ }
72
+
73
+ function getHeaderContentWidth(cols: number): number {
74
+ return Math.max(1, cols - SHELL_GUTTER_COLUMNS - HEADER_PADDING_COLUMNS);
75
+ }
76
+
77
+ function getMetadataRowCount(headerConfig: HeaderConfig): number {
78
+ return [
79
+ headerConfig.showBrand,
80
+ headerConfig.showAuthStatus,
81
+ headerConfig.showWorkspace,
82
+ headerConfig.showProvider,
83
+ ].filter(Boolean).length;
84
+ }
85
+
86
+ function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
87
+ if (layout.mode !== "full") {
88
+ return {
89
+ topMarginRows: 0,
90
+ bottomMarginRows: 0,
91
+ };
92
+ }
93
+
94
+ if (layout.rows <= 24) {
95
+ return { topMarginRows: 0, bottomMarginRows: 0 };
96
+ }
97
+
98
+ return {
99
+ topMarginRows: 1,
100
+ bottomMarginRows: 0,
101
+ };
102
+ }
103
+
104
+ export function getHeaderHeroLayout(
105
+ layout: Layout,
106
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
107
+ hasUpdate = false,
108
+ ): HeaderHeroLayout {
109
+ const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
110
+ const metadataRows = getMetadataRowCount(headerConfig);
111
+ const contentWidth = getHeaderContentWidth(layout.cols);
112
+
113
+ // Pick the largest logo that fits both the columns AND the rows. This degrades
114
+ // large → medium → compact on a short terminal instead of collapsing straight
115
+ // to the flat text-only header.
116
+ const logo = selectLogoVariantForViewport(layout.cols, layout.rows);
117
+ const logoWidth = logo.length > 0 ? getLogoWidth(logo) : 0;
118
+ const canRenderLogo = logo.length > 0;
119
+
120
+ if (!canRenderLogo) {
121
+ // Compact / text-only header. Show a recommended-size hint when the logo was
122
+ // dropped purely for size (not because the user disabled ASCII art).
123
+ const sizeHintRows = process.env["CODEXA_NO_ASCII_LOGO"] === "1" ? 0 : 1;
124
+ return {
125
+ mode: "compact",
126
+ topMarginRows,
127
+ bottomMarginRows,
128
+ metadataGapColumns: 0,
129
+ metadataGapRows: 0,
130
+ logoRows: 1,
131
+ metadataRows: 0,
132
+ compactHintRows: sizeHintRows,
133
+ totalRows: topMarginRows + 1 + sizeHintRows + bottomMarginRows,
134
+ };
135
+ }
136
+
137
+ const logoRowCount = logo.length;
138
+ const metadataGapColumns = layout.cols >= WIDE_HEADER_MIN_COLUMNS ? 4 : 2;
139
+ const metadataColumnWidth = contentWidth - logoWidth - metadataGapColumns;
140
+ const canUseSideBySide = metadataRows === 0
141
+ || metadataColumnWidth >= MIN_SIDE_BY_SIDE_METADATA_WIDTH;
142
+ const mode: HeaderHeroMode = canUseSideBySide && layout.cols >= WIDE_HEADER_MIN_COLUMNS
143
+ ? "wide"
144
+ : canUseSideBySide && layout.cols >= MEDIUM_HEADER_MIN_COLUMNS
145
+ ? "medium"
146
+ : "narrow";
147
+ const metadataGapRows = mode === "narrow" && metadataRows > 0 ? STACKED_METADATA_GAP_ROWS : 0;
148
+
149
+ const isSideBySide = mode === "wide" || mode === "medium";
150
+
151
+ // In side-by-side mode the right column grows to: update card + gap + metadata.
152
+ // In narrow/stacked mode, the update card is a single extra line below metadata.
153
+ const rightColRows = isSideBySide
154
+ ? (hasUpdate ? UPDATE_CARD_ROWS + UPDATE_CARD_GAP_ROWS : 0) + metadataRows
155
+ : 0;
156
+
157
+ const contentRows = isSideBySide
158
+ ? Math.max(logoRowCount, rightColRows)
159
+ : logoRowCount + metadataGapRows + metadataRows + (hasUpdate ? STACKED_METADATA_GAP_ROWS + 1 : 0);
160
+
161
+ return {
162
+ mode,
163
+ topMarginRows,
164
+ bottomMarginRows,
165
+ metadataGapColumns,
166
+ metadataGapRows,
167
+ logoRows: logoRowCount,
168
+ metadataRows,
169
+ compactHintRows: 0,
170
+ totalRows: topMarginRows + contentRows + bottomMarginRows,
171
+ };
172
+ }
173
+
174
+ export function measureTopHeaderRows(
175
+ layout: Layout,
176
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
177
+ hasUpdate = false,
178
+ ): number {
179
+ return getHeaderHeroLayout(layout, headerConfig, hasUpdate).totalRows;
180
+ }
181
+
182
+ function takeVisualSuffix(text: string, maxWidth: number): string {
183
+ if (maxWidth <= 0) return "";
184
+ let output = "";
185
+
186
+ for (const char of Array.from(text).reverse()) {
187
+ if (getTextWidth(char + output) > maxWidth) break;
188
+ output = char + output;
189
+ }
190
+
191
+ return output;
192
+ }
193
+
194
+ export function shortenHeaderWorkspaceLabel(workspaceLabel: string, maxWidth: number): string {
195
+ const trimmed = workspaceLabel.trim();
196
+ if (!trimmed || maxWidth <= 0) return "";
197
+ if (getTextWidth(trimmed) <= maxWidth) return trimmed;
198
+ if (maxWidth <= 1) return "…";
199
+
200
+ const normalized = trimmed.replace(/[\\/]+$/, "");
201
+ const separatorMatch = normalized.match(/[\\/]/g);
202
+ const separator = normalized.includes("\\") && (!separatorMatch || normalized.lastIndexOf("\\") >= normalized.lastIndexOf("/"))
203
+ ? "\\"
204
+ : "/";
205
+ const lastSlash = Math.max(normalized.lastIndexOf("\\"), normalized.lastIndexOf("/"));
206
+ const leaf = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
207
+ const prefix = lastSlash >= 0 ? `…${separator}` : "…";
208
+ const leafWidth = Math.max(1, maxWidth - getTextWidth(prefix));
209
+
210
+ return prefix + takeVisualSuffix(leaf, leafWidth);
211
+ }
212
+
213
+ function clampMetadataText(text: string, maxWidth: number): string {
214
+ return clampVisualText(text, Math.max(1, maxWidth));
215
+ }
216
+
217
+ export function TopHeader({
218
+ authState,
219
+ workspaceLabel,
220
+ layout,
221
+ runtimeSummary = null,
222
+ headerConfig = HEADER_CONFIG_DEFAULTS,
223
+ updateAvailable = null,
224
+ }: TopHeaderProps) {
225
+ renderDebug.useRenderDebug("Header", {
226
+ authState,
227
+ workspaceLabel,
228
+ cols: layout.cols,
229
+ rows: layout.rows,
230
+ mode: layout.mode,
231
+ });
232
+ renderDebug.useLifecycleDebug("Header", {
233
+ authState,
234
+ cols: layout.cols,
235
+ rows: layout.rows,
236
+ mode: layout.mode,
237
+ });
238
+
239
+ const theme = useTheme();
240
+
241
+ const authLabelRaw = getAuthStateLabel(authState);
242
+ const authLabel = authLabelRaw.length > 0
243
+ ? authLabelRaw[0]!.toUpperCase() + authLabelRaw.slice(1)
244
+ : authLabelRaw;
245
+
246
+ const heroLayout = getHeaderHeroLayout(layout, headerConfig, !!updateAvailable);
247
+ const selectedLogo = selectLogoVariantForViewport(layout.cols, layout.rows);
248
+ const selectedLogoWidth = selectedLogo.length > 0 ? getLogoWidth(selectedLogo) : 0;
249
+
250
+ const contentWidth = getHeaderContentWidth(layout.cols);
251
+ const sideBySideMetadataWidth = Math.max(1, contentWidth - selectedLogoWidth - heroLayout.metadataGapColumns);
252
+ const metadataWidth = heroLayout.mode === "wide" || heroLayout.mode === "medium"
253
+ ? sideBySideMetadataWidth
254
+ : contentWidth;
255
+ const workspaceValueWidth = Math.max(1, metadataWidth - getTextWidth("Workspace: "));
256
+ const wsDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, workspaceValueWidth);
257
+ const brandLabel = formatCodexaBrandLabel();
258
+ const metadataLinesRaw = [
259
+ headerConfig.showBrand ? { key: "brand", text: brandLabel, color: theme.text, bold: true } : null,
260
+ headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.text, bold: false } : null,
261
+ headerConfig.showWorkspace ? { key: "workspace", text: `Workspace: ${wsDisplay}`, color: theme.textMuted, bold: false } : null,
262
+ headerConfig.showProvider && runtimeSummary?.providerLabel
263
+ ? { key: "provider", text: `Provider: ${runtimeSummary.providerLabel}`, color: theme.text, bold: false }
264
+ : null,
265
+ ].filter((line): line is HeaderMetadataLine => Boolean(line));
266
+ const metadataLines = metadataLinesRaw.map((line) => ({
267
+ ...line,
268
+ text: clampMetadataText(line.text, metadataWidth),
269
+ }));
270
+
271
+ // Add a 1-row gap between the version line and workspace line in wide mode
272
+ // so the two pieces of metadata have breathing room beside the logo.
273
+ const hasMetadataGap = heroLayout.mode === "wide"
274
+ && metadataLines.length <= 3
275
+ && metadataLines.some((l) => l.key === "brand")
276
+ && metadataLines.some((l) => l.key === "workspace");
277
+ const metadataColumn = (
278
+ <Box flexDirection="column" flexGrow={1} flexShrink={1} width={metadataWidth}>
279
+ {metadataLines.map((line) =>
280
+ hasMetadataGap && line.key === "workspace" ? (
281
+ <Box key={line.key} marginTop={1}>
282
+ <Text color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
283
+ </Box>
284
+ ) : (
285
+ <Text key={line.key} color={line.color} bold={line.bold} wrap="truncate">{line.text}</Text>
286
+ )
287
+ )}
288
+ </Box>
289
+ );
290
+
291
+ // Canonical CODEXA wordmark — uses per-line LOGO palette defined in each theme.
292
+ // No `bold`: bold on Unicode block/box-drawing characters causes per-glyph
293
+ // spacing artifacts in common terminal fonts (Ptyxis, GNOME Terminal).
294
+ // wrap="truncate" keeps each row on exactly one terminal line.
295
+ const logoColumn = (
296
+ <Box flexDirection="column" flexShrink={0}>
297
+ {selectedLogo.map((line, i) => {
298
+ let lineColor = theme.logoPrimary;
299
+ if (selectedLogo.length === 6) {
300
+ if (i === 2 || i === 3) lineColor = theme.logoSecondary;
301
+ else if (i === 4 || i === 5) lineColor = theme.logoShadow;
302
+ }
303
+ return (
304
+ <Text key={i} color={lineColor} wrap="truncate">{line}</Text>
305
+ );
306
+ })}
307
+ </Box>
308
+ );
309
+
310
+ if (heroLayout.mode !== "compact") {
311
+ const isSideBySide = heroLayout.mode === "wide" || heroLayout.mode === "medium";
312
+ const metadataTopOffset = 0;
313
+
314
+ return (
315
+ <Box flexDirection="column" paddingX={1} width="100%">
316
+ {heroLayout.topMarginRows > 0 && (
317
+ <Box height={heroLayout.topMarginRows} />
318
+ )}
319
+
320
+ {isSideBySide ? (
321
+ <Box flexDirection="row" width="100%" alignItems="flex-start">
322
+ {logoColumn}
323
+ <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
324
+ <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
325
+ {updateAvailable && (
326
+ <>
327
+ <UpdateAvailableCard
328
+ latestVersion={updateAvailable.latestVersion}
329
+ currentVersion={updateAvailable.currentVersion}
330
+ width={metadataWidth}
331
+ />
332
+ <Box height={UPDATE_CARD_GAP_ROWS} />
333
+ </>
334
+ )}
335
+ {metadataColumn}
336
+ </Box>
337
+ </Box>
338
+ ) : (
339
+ <Box flexDirection="column" width="100%">
340
+ {logoColumn}
341
+ {heroLayout.metadataGapRows > 0 && (
342
+ <Box height={heroLayout.metadataGapRows} />
343
+ )}
344
+ {metadataColumn}
345
+ {updateAvailable && (
346
+ <Text color={theme.warning} wrap="truncate">{`↑ Update: Codexa ${updateAvailable.latestVersion} is available — run: npm install -g @golba98/codexa@latest`}</Text>
347
+ )}
348
+ </Box>
349
+ )}
350
+
351
+ {heroLayout.bottomMarginRows > 0 && (
352
+ <Box height={heroLayout.bottomMarginRows} />
353
+ )}
354
+ </Box>
355
+ );
356
+ }
357
+
358
+ // Compact / micro / activity-collapsed: single-line header.
359
+ const compactMetadataWidth = contentWidth;
360
+ const compactWorkspaceValueWidth = Math.max(1, compactMetadataWidth - getTextWidth("Workspace: "));
361
+ const compactWorkspaceDisplay = shortenHeaderWorkspaceLabel(workspaceLabel, compactWorkspaceValueWidth);
362
+ // A leading ✦ accent makes the single-line header read as a deliberate
363
+ // compact Codexa header rather than a broken fallback.
364
+ const compactParts: React.ReactNode[] = [
365
+ <Text key="accent" color={theme.accent} bold>{"✦ "}</Text>,
366
+ ];
367
+ if (headerConfig.showBrand) {
368
+ compactParts.push(
369
+ <Text key="brand" color={theme.text} bold>{brandLabel}</Text>,
370
+ );
371
+ }
372
+ if (headerConfig.showAuthStatus) {
373
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-auth" color={theme.textDim}>{" · "}</Text>);
374
+ compactParts.push(<Text key="auth" color={theme.text}>{authLabel}</Text>);
375
+ }
376
+ if (headerConfig.showWorkspace) {
377
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-ws" color={theme.textDim}>{" · "}</Text>);
378
+ compactParts.push(<Text key="ws" color={theme.textMuted} wrap="truncate">{`Workspace: ${compactWorkspaceDisplay}`}</Text>);
379
+ }
380
+ if (headerConfig.showProvider && runtimeSummary?.providerLabel) {
381
+ if (compactParts.length > 0) compactParts.push(<Text key="sep-provider" color={theme.textDim}>{" · "}</Text>);
382
+ compactParts.push(<Text key="provider" color={theme.text} wrap="truncate">{`Provider: ${runtimeSummary.providerLabel}`}</Text>);
383
+ }
384
+
385
+ return (
386
+ <Box flexDirection="column" paddingX={1} width="100%">
387
+ {heroLayout.topMarginRows > 0 && (
388
+ <Box height={heroLayout.topMarginRows} />
389
+ )}
390
+ <Box flexDirection="row" width="100%">
391
+ {compactParts}
392
+ </Box>
393
+ {heroLayout.compactHintRows > 0 && (
394
+ <Text color={theme.textDim} wrap="truncate">{clampMetadataText(RECOMMENDED_FULL_HEADER_HINT, compactMetadataWidth)}</Text>
395
+ )}
396
+ {heroLayout.bottomMarginRows > 0 && (
397
+ <Box height={heroLayout.bottomMarginRows} />
398
+ )}
399
+ </Box>
400
+ );
401
+ }
402
+
403
+ // Memoize to prevent re-renders during streaming when props haven't changed
404
+ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
405
+ return (
406
+ prev.authState === next.authState &&
407
+ prev.workspaceLabel === next.workspaceLabel &&
408
+ prev.layout.cols === next.layout.cols &&
409
+ prev.layout.rows === next.layout.rows &&
410
+ prev.layout.mode === next.layout.mode &&
411
+ prev.runtimeSummary === next.runtimeSummary &&
412
+ prev.headerConfig === next.headerConfig &&
413
+ prev.updateAvailable === next.updateAvailable
414
+ );
415
+ });
416
+
417
+ // Minimum terminal cols to render any logo art (the compact 1-row variant).
418
+ export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
419
+
420
+ // Re-export for consumers that reference these constants directly.
421
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS };