@golba98/codexa 1.0.1 → 1.0.2

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 (232) hide show
  1. package/README.md +0 -0
  2. package/package.json +3 -2
  3. package/scripts/audit-codexa-capabilities.mjs +0 -0
  4. package/scripts/gen-build-info.mjs +33 -0
  5. package/scripts/smoke-terminal-bench.mjs +0 -0
  6. package/src/app.tsx +103 -2
  7. package/src/appRenderStability.test.ts +0 -0
  8. package/src/commands/handler.test.ts +12 -0
  9. package/src/commands/handler.ts +5 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.test.ts +31 -0
  12. package/src/config/launchArgs.ts +10 -0
  13. package/src/config/layeredConfig.test.ts +0 -0
  14. package/src/config/layeredConfig.ts +0 -0
  15. package/src/config/persistence.test.ts +6 -2
  16. package/src/config/persistence.ts +41 -0
  17. package/src/config/runtimeConfig.test.ts +0 -0
  18. package/src/config/runtimeConfig.ts +4 -0
  19. package/src/config/settings.test.ts +0 -0
  20. package/src/config/settings.ts +7 -6
  21. package/src/config/toml-serialize.ts +0 -0
  22. package/src/config/trustStore.test.ts +0 -0
  23. package/src/config/trustStore.ts +0 -0
  24. package/src/config/updateCheckCache.ts +45 -0
  25. package/src/core/attachments.test.ts +0 -0
  26. package/src/core/attachments.ts +0 -0
  27. package/src/core/auth/codexAuth.test.ts +0 -0
  28. package/src/core/auth/codexAuth.ts +0 -0
  29. package/src/core/cleanupFastFail.test.ts +0 -0
  30. package/src/core/cleanupFastFail.ts +0 -0
  31. package/src/core/clipboard.ts +0 -0
  32. package/src/core/codex.ts +0 -0
  33. package/src/core/codexExecArgs.test.ts +0 -0
  34. package/src/core/codexExecArgs.ts +0 -0
  35. package/src/core/codexLaunch.test.ts +0 -0
  36. package/src/core/codexLaunch.ts +0 -0
  37. package/src/core/codexPrompt.test.ts +0 -0
  38. package/src/core/codexPrompt.ts +0 -0
  39. package/src/core/executables/claudeExecutable.ts +0 -0
  40. package/src/core/executables/codexExecutable.test.ts +0 -0
  41. package/src/core/executables/codexExecutable.ts +0 -0
  42. package/src/core/executables/executableResolver.test.ts +0 -0
  43. package/src/core/executables/executableResolver.ts +0 -0
  44. package/src/core/executables/geminiExecutable.test.ts +0 -0
  45. package/src/core/executables/geminiExecutable.ts +0 -0
  46. package/src/core/executables/pathSanityScan.test.ts +0 -0
  47. package/src/core/githubDiagnostics.test.ts +0 -0
  48. package/src/core/githubDiagnostics.ts +0 -0
  49. package/src/core/hollowResponseFormat.test.ts +0 -0
  50. package/src/core/hollowResponseFormat.ts +0 -0
  51. package/src/core/inputDebug.ts +0 -0
  52. package/src/core/launchContext.test.ts +0 -0
  53. package/src/core/launchContext.ts +0 -0
  54. package/src/core/models/codexCapabilities.test.ts +0 -0
  55. package/src/core/models/codexCapabilities.ts +0 -0
  56. package/src/core/models/codexModelCapabilities.test.ts +0 -0
  57. package/src/core/models/codexModelCapabilities.ts +0 -0
  58. package/src/core/models/modelSpecs.test.ts +0 -0
  59. package/src/core/models/modelSpecs.ts +0 -0
  60. package/src/core/perf/profiler.ts +0 -0
  61. package/src/core/perf/renderDebug.test.ts +0 -0
  62. package/src/core/perf/renderDebug.ts +0 -0
  63. package/src/core/planStorage.test.ts +0 -0
  64. package/src/core/planStorage.ts +0 -0
  65. package/src/core/process/CommandRunner.test.ts +0 -0
  66. package/src/core/process/CommandRunner.ts +0 -0
  67. package/src/core/process/processValidation.ts +0 -0
  68. package/src/core/projectInstructions.test.ts +0 -0
  69. package/src/core/projectInstructions.ts +0 -0
  70. package/src/core/providerLauncher/launcher.test.ts +0 -0
  71. package/src/core/providerLauncher/launcher.ts +0 -0
  72. package/src/core/providerLauncher/registry.test.ts +0 -0
  73. package/src/core/providerLauncher/registry.ts +0 -0
  74. package/src/core/providerLauncher/types.ts +0 -0
  75. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -0
  76. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  77. package/src/core/providerRuntime/anthropic.test.ts +0 -0
  78. package/src/core/providerRuntime/anthropic.ts +0 -0
  79. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -0
  80. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  81. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  82. package/src/core/providerRuntime/contextMetadata.test.ts +0 -0
  83. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  84. package/src/core/providerRuntime/gemini.test.ts +0 -0
  85. package/src/core/providerRuntime/gemini.ts +0 -0
  86. package/src/core/providerRuntime/lmstudio.test.ts +0 -0
  87. package/src/core/providerRuntime/lmstudio.ts +0 -0
  88. package/src/core/providerRuntime/local.test.ts +0 -0
  89. package/src/core/providerRuntime/local.ts +0 -0
  90. package/src/core/providerRuntime/models.ts +0 -0
  91. package/src/core/providerRuntime/reasoning.ts +0 -0
  92. package/src/core/providerRuntime/registry.test.ts +0 -0
  93. package/src/core/providerRuntime/registry.ts +0 -0
  94. package/src/core/providerRuntime/types.ts +0 -0
  95. package/src/core/providers/codexJsonStream.test.ts +0 -0
  96. package/src/core/providers/codexJsonStream.ts +0 -0
  97. package/src/core/providers/codexSubprocess.test.ts +0 -0
  98. package/src/core/providers/codexSubprocess.ts +0 -0
  99. package/src/core/providers/codexTranscript.test.ts +0 -0
  100. package/src/core/providers/codexTranscript.ts +0 -0
  101. package/src/core/providers/openaiNative.ts +0 -0
  102. package/src/core/providers/registry.ts +0 -0
  103. package/src/core/providers/types.ts +0 -0
  104. package/src/core/terminal/startupClear.test.ts +55 -0
  105. package/src/core/terminal/startupClear.ts +20 -0
  106. package/src/core/terminal/terminalCapabilities.test.ts +0 -0
  107. package/src/core/terminal/terminalCapabilities.ts +0 -0
  108. package/src/core/terminal/terminalControl.test.ts +0 -0
  109. package/src/core/terminal/terminalControl.ts +0 -0
  110. package/src/core/terminal/terminalSanitize.test.ts +0 -0
  111. package/src/core/terminal/terminalSanitize.ts +0 -0
  112. package/src/core/terminal/terminalSelection.test.ts +0 -0
  113. package/src/core/terminal/terminalSelection.ts +0 -0
  114. package/src/core/terminal/terminalTitle.test.ts +0 -0
  115. package/src/core/terminal/terminalTitle.ts +0 -0
  116. package/src/core/updateCheck.test.ts +194 -0
  117. package/src/core/updateCheck.ts +172 -0
  118. package/src/core/workspaceActivity.test.ts +0 -0
  119. package/src/core/workspaceActivity.ts +0 -0
  120. package/src/core/workspaceGuard.test.ts +0 -0
  121. package/src/core/workspaceGuard.ts +0 -0
  122. package/src/core/workspaceRoot.test.ts +0 -0
  123. package/src/core/workspaceRoot.ts +0 -0
  124. package/src/exec.test.ts +0 -0
  125. package/src/exec.ts +0 -0
  126. package/src/headless/execArgs.test.ts +0 -0
  127. package/src/headless/execArgs.ts +1 -0
  128. package/src/headless/execRunner.test.ts +8 -6
  129. package/src/headless/execRunner.ts +0 -0
  130. package/src/index.test.tsx +5 -3
  131. package/src/index.tsx +13 -9
  132. package/src/session/appSession.test.ts +0 -0
  133. package/src/session/appSession.ts +0 -0
  134. package/src/session/chatLifecycle.test.ts +0 -0
  135. package/src/session/chatLifecycle.ts +0 -0
  136. package/src/session/liveRenderScheduler.test.ts +0 -0
  137. package/src/session/liveRenderScheduler.ts +0 -0
  138. package/src/session/planFlow.test.ts +0 -0
  139. package/src/session/planFlow.ts +0 -0
  140. package/src/session/planTranscript.test.ts +0 -0
  141. package/src/session/planTranscript.ts +0 -0
  142. package/src/session/promptRunSchedule.test.ts +0 -0
  143. package/src/session/promptRunSchedule.ts +0 -0
  144. package/src/session/types.ts +0 -0
  145. package/src/test/runtimeTestUtils.ts +0 -0
  146. package/src/types/react-dom.d.ts +0 -0
  147. package/src/ui/ActionRequiredBlock.tsx +0 -0
  148. package/src/ui/ActivityBars.tsx +0 -0
  149. package/src/ui/ActivityIndicator.test.tsx +0 -0
  150. package/src/ui/ActivityIndicator.tsx +0 -0
  151. package/src/ui/AgentBlock.test.ts +0 -0
  152. package/src/ui/AgentBlock.tsx +0 -0
  153. package/src/ui/AnimatedStatusText.test.ts +0 -0
  154. package/src/ui/AnimatedStatusText.tsx +0 -0
  155. package/src/ui/AppShell.test.tsx +60 -23
  156. package/src/ui/AppShell.tsx +28 -5
  157. package/src/ui/AttachmentImportPanel.test.tsx +0 -0
  158. package/src/ui/AttachmentImportPanel.tsx +0 -0
  159. package/src/ui/AuthPanel.tsx +0 -0
  160. package/src/ui/BackendPicker.tsx +0 -0
  161. package/src/ui/BottomComposer.test.ts +0 -0
  162. package/src/ui/BottomComposer.tsx +0 -0
  163. package/src/ui/CodexLogo.tsx +0 -0
  164. package/src/ui/DashCard.tsx +0 -0
  165. package/src/ui/Markdown.test.ts +0 -0
  166. package/src/ui/Markdown.tsx +0 -0
  167. package/src/ui/ModePicker.tsx +0 -0
  168. package/src/ui/ModelPicker.tsx +0 -0
  169. package/src/ui/ModelPickerProviderScope.test.tsx +0 -0
  170. package/src/ui/ModelPickerScreen.test.tsx +0 -0
  171. package/src/ui/ModelPickerScreen.tsx +0 -0
  172. package/src/ui/ModelPickerState.test.tsx +0 -0
  173. package/src/ui/ModelReasoningPicker.test.tsx +0 -0
  174. package/src/ui/ModelReasoningPicker.tsx +0 -0
  175. package/src/ui/Panel.tsx +0 -0
  176. package/src/ui/PermissionsPanel.tsx +0 -0
  177. package/src/ui/PlanActionPicker.tsx +0 -0
  178. package/src/ui/PlanReviewPanel.test.tsx +0 -0
  179. package/src/ui/PlanReviewPanel.tsx +0 -0
  180. package/src/ui/PromptCardBorder.test.tsx +0 -0
  181. package/src/ui/ProviderPicker.test.tsx +0 -0
  182. package/src/ui/ProviderPicker.tsx +0 -0
  183. package/src/ui/ProviderShortcut.test.tsx +0 -0
  184. package/src/ui/ReasoningPicker.tsx +0 -0
  185. package/src/ui/RunFooter.tsx +0 -0
  186. package/src/ui/SelectionPanel.tsx +0 -0
  187. package/src/ui/SettingsPanel.test.tsx +0 -0
  188. package/src/ui/SettingsPanel.tsx +0 -0
  189. package/src/ui/Spinner.tsx +0 -0
  190. package/src/ui/StaticIntroItem.tsx +0 -0
  191. package/src/ui/StaticTranscriptItem.tsx +0 -0
  192. package/src/ui/TextEntryPanel.tsx +0 -0
  193. package/src/ui/ThemePicker.tsx +0 -0
  194. package/src/ui/ThinkingBlock.tsx +0 -0
  195. package/src/ui/Timeline.test.ts +0 -0
  196. package/src/ui/Timeline.tsx +0 -0
  197. package/src/ui/TimelineNavigation.test.tsx +0 -0
  198. package/src/ui/TopHeader.test.tsx +53 -38
  199. package/src/ui/TopHeader.tsx +51 -15
  200. package/src/ui/TurnGroup.test.tsx +0 -0
  201. package/src/ui/TurnGroup.tsx +0 -0
  202. package/src/ui/busyStatusAnimation.test.ts +0 -0
  203. package/src/ui/busyStatusAnimation.ts +0 -0
  204. package/src/ui/commandNormalize.test.ts +0 -0
  205. package/src/ui/commandNormalize.ts +0 -0
  206. package/src/ui/diffRenderer.test.ts +0 -0
  207. package/src/ui/diffRenderer.ts +0 -0
  208. package/src/ui/focus.ts +0 -0
  209. package/src/ui/focusFlow.test.tsx +0 -0
  210. package/src/ui/inputBuffer.test.ts +0 -0
  211. package/src/ui/inputBuffer.ts +0 -0
  212. package/src/ui/layout.test.ts +3 -2
  213. package/src/ui/layout.ts +0 -0
  214. package/src/ui/modeDisplay.test.ts +0 -0
  215. package/src/ui/modeDisplay.ts +0 -0
  216. package/src/ui/outputPipeline.ts +0 -0
  217. package/src/ui/progressEntries.ts +0 -0
  218. package/src/ui/runActivityView.test.ts +0 -0
  219. package/src/ui/runActivityView.ts +0 -0
  220. package/src/ui/runLifecycleView.test.tsx +0 -0
  221. package/src/ui/slashCommands.ts +0 -0
  222. package/src/ui/statusRenderIsolation.test.tsx +0 -0
  223. package/src/ui/terminalAnswerFormat.test.ts +0 -0
  224. package/src/ui/terminalAnswerFormat.ts +0 -0
  225. package/src/ui/textLayout.test.ts +0 -0
  226. package/src/ui/textLayout.ts +0 -0
  227. package/src/ui/theme.tsx +0 -0
  228. package/src/ui/themeFlow.test.ts +0 -0
  229. package/src/ui/themeFlow.ts +0 -0
  230. package/src/ui/timelineMeasure.ts +0 -0
  231. package/src/ui/timelineMeasureCache.test.ts +0 -0
  232. package/src/ui/useThrottledValue.ts +0 -0
@@ -50,7 +50,11 @@ function sleep(ms = 50): Promise<void> {
50
50
  return new Promise((resolve) => setTimeout(resolve, ms));
51
51
  }
52
52
 
53
- const HEADER_CONFIG_WITH_AUTH: HeaderConfig = { ...HEADER_CONFIG_DEFAULTS, showAuthStatus: true };
53
+ const HEADER_CONFIG_WITH_AUTH: HeaderConfig = {
54
+ ...HEADER_CONFIG_DEFAULTS,
55
+ showAuthStatus: true,
56
+ showContext: true,
57
+ };
54
58
 
55
59
  async function renderHeader(cols: number, authState: CodexAuthState, headerConfig?: HeaderConfig): Promise<string> {
56
60
  return renderHeaderWithWorkspace(cols, authState, "C:\\Development\\1-JavaScript\\13-Custom CLI", headerConfig);
@@ -105,9 +109,12 @@ test("full mode renders wordmark at wide terminal", async () => {
105
109
  assert.match(output, /[█╔╗╚╝═║]/);
106
110
  assert.match(output, new RegExp(`Codexa v${APP_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
107
111
  assert.match(output, /Authenticated/);
108
- assert.match(output, /Workspace:\s*C:\\Development\\1-JavaScript\\13-Custom CLI/);
112
+ assert.match(output, /Workspace:\s*C:\\Development\\1-JavaScript\\13-Custom CLI/);
113
+ assert.match(output, /Provider:\s*Codexa Core/);
114
+ assert.match(output, /Context:\s*Unknown/);
115
+ assert.doesNotMatch(output, /Model:/);
116
+ assert.doesNotMatch(output, /Reasoning:/);
109
117
  assert.doesNotMatch(output, /Runtime:/);
110
- assert.doesNotMatch(output, /gpt-5\.4/i);
111
118
  assert.doesNotMatch(output, /Net:\s*off/i);
112
119
  assert.doesNotMatch(output, /Roots:\s*0/i);
113
120
  assert.doesNotMatch(output, /FULL AUTO/i);
@@ -123,15 +130,17 @@ test("wide header centers metadata beside the logo with a clear column gap", asy
123
130
  const workspaceRow = rows.findIndex((row) => row.includes("Workspace:"));
124
131
 
125
132
  assert.ok(firstLogoRow >= 0, "logo should render");
126
- assert.equal(brandRow, firstLogoRow + 1, "metadata should be vertically centered within the logo block");
127
- // With auth shown: brand → auth → [1-row gap] → workspace so workspace is 3 rows from brand
128
- assert.equal(workspaceRow, brandRow + 3, "workspace should sit below auth with a 1-row gap between version and workspace");
129
- assert.ok((rows[brandRow]?.indexOf(`Codexa v${APP_VERSION}`) ?? -1) >= 55, "metadata should have a visible left gap from the logo");
130
- });
133
+ assert.ok(brandRow >= firstLogoRow && brandRow <= firstLogoRow + 2, "metadata should be vertically centered within the logo block");
134
+ assert.equal(workspaceRow, brandRow + 2, "workspace should sit below auth in the metadata block");
135
+ assert.ok((rows[brandRow]?.indexOf(`Codexa v${APP_VERSION}`) ?? -1) >= 55, "metadata should have a visible left gap from the logo");
136
+ });
131
137
 
132
138
  test("version and workspace metadata rows have a visible gap between them", async () => {
133
- // Default config: brand + workspace only (no auth row between them)
134
- const output = await renderHeader(130, "authenticated");
139
+ const output = await renderHeader(130, "authenticated", {
140
+ ...HEADER_CONFIG_DEFAULTS,
141
+ showProvider: false,
142
+ showModel: false,
143
+ });
135
144
  const rows = output.split("\n");
136
145
  const brandRow = rows.findIndex((row) => row.includes(`Codexa v${APP_VERSION}`));
137
146
  const workspaceRow = rows.findIndex((row) => row.includes("Workspace:"));
@@ -161,28 +170,33 @@ test("header wordmark lines never contain metadata text", () => {
161
170
  assert.doesNotMatch(wordmarkText, /Auth:/);
162
171
  });
163
172
 
164
- test("compact mode renders version and auth", async () => {
165
- const output = await renderHeader(105, "authenticated", HEADER_CONFIG_WITH_AUTH);
166
-
167
- assert.match(output, new RegExp(`v${APP_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
168
- assert.match(output, /Authenticated/);
169
- assert.match(output, /C:\\Development\\1-JavaScript\\13-Custom CLI/);
173
+ test("compact mode renders version and auth", async () => {
174
+ const output = await renderHeader(105, "authenticated", HEADER_CONFIG_WITH_AUTH);
175
+
176
+ assert.match(output, /[█╔╗╚╝═║]/);
177
+ assert.match(output, new RegExp(`v${APP_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
178
+ assert.match(output, /Authenticated/);
179
+ assert.match(output, /Workspace:\s*C:\\Development\\1-JavaScript\\13-Custom CLI/);
180
+ assert.match(output, /Provider:\s*Codexa Core/);
181
+ assert.match(output, /Context:\s*Unknown/);
182
+ assert.doesNotMatch(output, /Model:/);
183
+ assert.doesNotMatch(output, /Reasoning:/);
170
184
  assert.doesNotMatch(output, /Runtime:/);
171
- assert.doesNotMatch(output, /gpt-5\.4/i);
172
185
  assert.doesNotMatch(output, /Net:\s*off/i);
173
186
  assert.doesNotMatch(output, /Roots:\s*0/i);
174
187
  assert.doesNotMatch(output, /FULL AUTO/i);
175
- assert.doesNotMatch(output, /[█╔╗╚╝═║]/);
176
188
  });
177
189
 
178
- test("micro mode renders version and auth", async () => {
179
- const output = await renderHeader(50, "authenticated");
180
-
181
- assert.match(output, /Codex/);
182
- assert.match(output, new RegExp(`v${APP_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
183
- assert.doesNotMatch(output, /[█╔╗╚╝═║]/);
184
- assert.doesNotMatch(output, /gpt-5\.4/i);
185
- });
190
+ test("micro mode renders version and auth", async () => {
191
+ const output = await renderHeader(50, "authenticated", HEADER_CONFIG_WITH_AUTH);
192
+
193
+ assert.match(output, /Codex/);
194
+ assert.match(output, new RegExp(`v${APP_VERSION.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
195
+ assert.doesNotMatch(output, /[█╔╗╚╝═║]/);
196
+ assert.match(output, /Provider/);
197
+ assert.match(output, /Contex/);
198
+ assert.doesNotMatch(output, /Model:/);
199
+ });
186
200
 
187
201
  test("full mode always shows wordmark regardless of activity", async () => {
188
202
  const output = await renderHeader(180, "authenticated", HEADER_CONFIG_WITH_AUTH);
@@ -195,23 +209,24 @@ test("full mode always shows wordmark regardless of activity", async () => {
195
209
  });
196
210
 
197
211
  test("full mode renders Checking for checking auth state", async () => {
198
- const output = await renderHeader(130, "checking", HEADER_CONFIG_WITH_AUTH);
199
- assert.match(output, /Checking/);
200
- assert.doesNotMatch(output, /Unknown/);
201
- });
212
+ const output = await renderHeader(130, "checking", HEADER_CONFIG_WITH_AUTH);
213
+ assert.match(output, /Checking/);
214
+ assert.doesNotMatch(output, /Auth:\s*Unknown/);
215
+ });
202
216
 
203
- test("compact mode preserves workspace truncation without runtime text", async () => {
217
+ test("compact mode preserves workspace truncation with runtime text", async () => {
204
218
  const output = await renderHeaderWithWorkspace(
205
219
  60,
206
220
  "authenticated",
207
221
  "C:\\Development\\1-JavaScript\\13-Custom CLI\\packages\\really-long-subfolder\\nested\\workspace",
208
222
  );
209
223
 
210
- assert.match(output, /\.\.\. /);
211
- assert.match(output, /nested\\workspace/);
212
- assert.doesNotMatch(output, /packages\\really-long-subfolder/);
213
- assert.doesNotMatch(output, /gpt-5\.4/i);
214
- });
224
+ assert.match(output, /\.\.\. /);
225
+ assert.match(output, /nested\\workspace/);
226
+ assert.doesNotMatch(output, /packages\\really-long-subfolder/);
227
+ assert.match(output, /Provider:\s*Codexa Core/);
228
+ assert.doesNotMatch(output, /Model:/);
229
+ });
215
230
 
216
231
  test("renders configured workspace display labels", async () => {
217
232
  const workspaceRoot = "C:\\Development\\1-JavaScript\\13-Custom-CLI-Normal";
@@ -221,7 +236,7 @@ test("renders configured workspace display labels", async () => {
221
236
  "authenticated",
222
237
  formatWorkspaceDisplayPath(workspaceRoot, "dir"),
223
238
  );
224
- assert.match(dirOutput, /Workspace:\s*13-Custom-CLI-Normal/);
239
+ assert.match(dirOutput, /Workspace:\s*(?:.*\\)?13-Custom-CLI-Normal/);
225
240
 
226
241
  const nameOutput = await renderHeaderWithWorkspace(
227
242
  130,
@@ -235,5 +250,5 @@ test("renders configured workspace display labels", async () => {
235
250
  "authenticated",
236
251
  formatWorkspaceDisplayPath(workspaceRoot, "simple"),
237
252
  );
238
- assert.match(simpleOutput, /Workspace:\s*13-Custom-CLI-Normal/);
253
+ assert.match(simpleOutput, /Workspace:\s*(?:.*\\)?13-Custom-CLI-Normal/);
239
254
  });
@@ -20,6 +20,8 @@ export const HEADER_WORDMARK_LINES = [
20
20
 
21
21
  const MIN_METADATA_COLUMN_WIDTH = 60;
22
22
  const STACKED_METADATA_GAP_ROWS = 1;
23
+ const MIN_LOGO_TERMINAL_WIDTH = getWordmarkWidth() + 2;
24
+ const MIN_LOGO_TERMINAL_ROWS = 24;
23
25
 
24
26
  export type HeaderHeroMode = "wide" | "stacked" | "compact";
25
27
 
@@ -34,6 +36,7 @@ export interface HeaderHeroLayout {
34
36
  totalRows: number;
35
37
  }
36
38
 
39
+ type HeaderMetadataLine = { key: string; text: string; color: string; bold: boolean };
37
40
 
38
41
  interface TopHeaderProps {
39
42
  authState: CodexAuthState;
@@ -47,29 +50,43 @@ function getWordmarkWidth(): number {
47
50
  return HEADER_WORDMARK_LINES.reduce((maxWidth, line) => Math.max(maxWidth, getTextWidth(line)), 0);
48
51
  }
49
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
+
50
63
  function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
51
64
  if (layout.mode !== "full") {
52
65
  return {
53
66
  topMarginRows: 0,
54
- bottomMarginRows: layout.rows > 24 ? 1 : 0,
67
+ bottomMarginRows: 0,
55
68
  };
56
69
  }
57
70
 
58
71
  if (layout.rows <= 24) {
59
- return { topMarginRows: 0, bottomMarginRows: 1 };
72
+ return { topMarginRows: 0, bottomMarginRows: 0 };
60
73
  }
61
74
 
62
75
  return {
63
76
  topMarginRows: 1,
64
- bottomMarginRows: layout.rows >= 36 ? 2 : 1,
77
+ bottomMarginRows: 0,
65
78
  };
66
79
  }
67
80
 
68
- export function getHeaderHeroLayout(layout: Layout): HeaderHeroLayout {
81
+ export function getHeaderHeroLayout(
82
+ layout: Layout,
83
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
84
+ ): HeaderHeroLayout {
69
85
  const { topMarginRows, bottomMarginRows } = getHeaderVerticalMargins(layout);
70
- const metadataRows = 3;
86
+ const metadataRows = getMetadataRowCount(headerConfig);
87
+ const canRenderLogo = layout.cols >= MIN_LOGO_TERMINAL_WIDTH && layout.rows >= MIN_LOGO_TERMINAL_ROWS;
71
88
 
72
- if (layout.mode !== "full") {
89
+ if (!canRenderLogo) {
73
90
  return {
74
91
  mode: "compact",
75
92
  topMarginRows,
@@ -85,7 +102,7 @@ export function getHeaderHeroLayout(layout: Layout): HeaderHeroLayout {
85
102
  const logoRows = HEADER_WORDMARK_LINES.length;
86
103
  const metadataGapColumns = layout.cols >= 150 ? 6 : layout.cols >= 130 ? 5 : 4;
87
104
  const canUseWideHero = layout.cols >= getWordmarkWidth() + metadataGapColumns + MIN_METADATA_COLUMN_WIDTH + 2;
88
- const metadataGapRows = canUseWideHero ? 0 : STACKED_METADATA_GAP_ROWS;
105
+ const metadataGapRows = canUseWideHero || metadataRows === 0 ? 0 : STACKED_METADATA_GAP_ROWS;
89
106
  const contentRows = canUseWideHero
90
107
  ? logoRows
91
108
  : logoRows + metadataGapRows + metadataRows;
@@ -102,8 +119,11 @@ export function getHeaderHeroLayout(layout: Layout): HeaderHeroLayout {
102
119
  };
103
120
  }
104
121
 
105
- export function measureTopHeaderRows(layout: Layout): number {
106
- return getHeaderHeroLayout(layout).totalRows;
122
+ export function measureTopHeaderRows(
123
+ layout: Layout,
124
+ headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
125
+ ): number {
126
+ return getHeaderHeroLayout(layout, headerConfig).totalRows;
107
127
  }
108
128
 
109
129
  /** Truncate a path to fit within maxWidth, keeping the end and prefixing with "... " if needed */
@@ -117,6 +137,7 @@ export function TopHeader({
117
137
  authState,
118
138
  workspaceLabel,
119
139
  layout,
140
+ runtimeSummary = null,
120
141
  headerConfig = HEADER_CONFIG_DEFAULTS,
121
142
  }: TopHeaderProps) {
122
143
  renderDebug.useRenderDebug("Header", {
@@ -133,7 +154,7 @@ export function TopHeader({
133
154
  mode: layout.mode,
134
155
  });
135
156
 
136
- const { cols, mode } = layout;
157
+ const { cols } = layout;
137
158
  const theme = useTheme();
138
159
 
139
160
  const authLabelRaw = getAuthStateLabel(authState);
@@ -142,16 +163,23 @@ export function TopHeader({
142
163
  : authLabelRaw;
143
164
 
144
165
  const wsDisplay = truncatePath(workspaceLabel, Math.max(18, cols - 40));
145
- const heroLayout = getHeaderHeroLayout(layout);
166
+ const heroLayout = getHeaderHeroLayout(layout, headerConfig);
146
167
  const metadataLines = [
147
168
  headerConfig.showBrand ? { key: "brand", text: `Codexa v${APP_VERSION}`, color: theme.TEXT, bold: true } : null,
148
169
  headerConfig.showAuthStatus ? { key: "auth", text: `Auth: ${authLabel}`, color: theme.TEXT, bold: false } : null,
149
170
  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));
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));
151
178
 
152
179
  // Add a 1-row gap between the version line and workspace line in wide mode
153
180
  // so the two pieces of metadata have breathing room beside the logo.
154
181
  const hasMetadataGap = heroLayout.mode === "wide"
182
+ && metadataLines.length <= 3
155
183
  && metadataLines.some((l) => l.key === "brand")
156
184
  && metadataLines.some((l) => l.key === "workspace");
157
185
  const metadataVisualRows = metadataLines.length + (hasMetadataGap ? 1 : 0);
@@ -178,7 +206,7 @@ export function TopHeader({
178
206
  </Box>
179
207
  );
180
208
 
181
- if (mode === "full") {
209
+ if (heroLayout.mode !== "compact") {
182
210
  const metadataTopOffset = Math.max(0, Math.floor((HEADER_WORDMARK_LINES.length - metadataVisualRows) / 2));
183
211
 
184
212
  return (
@@ -188,7 +216,7 @@ export function TopHeader({
188
216
  )}
189
217
 
190
218
  {heroLayout.mode === "wide" ? (
191
- <Box flexDirection="row" width="100%">
219
+ <Box flexDirection="row" width="100%" alignItems="flex-start">
192
220
  {logoColumn}
193
221
  <Box width={heroLayout.metadataGapColumns} flexShrink={0} />
194
222
  <Box flexDirection="column" flexGrow={1} paddingTop={metadataTopOffset}>
@@ -225,7 +253,15 @@ export function TopHeader({
225
253
  }
226
254
  if (headerConfig.showWorkspace) {
227
255
  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>);
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>);
229
265
  }
230
266
 
231
267
  return (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/ui/focus.ts CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -85,8 +85,9 @@ test("chooses startup header mode from measured row budget", () => {
85
85
  });
86
86
 
87
87
  test("measures the header rows for full and compact layouts", () => {
88
- assert.equal(measureTopHeaderRows(createLayoutSnapshot(120, 30)), 8);
89
- assert.equal(measureTopHeaderRows(createLayoutSnapshot(80, 24)), 1);
88
+ assert.equal(measureTopHeaderRows(createLayoutSnapshot(120, 30)), 7);
89
+ assert.equal(measureTopHeaderRows(createLayoutSnapshot(80, 24)), 10);
90
+ assert.equal(measureTopHeaderRows(createLayoutSnapshot(50, 24)), 1);
90
91
  });
91
92
 
92
93
  test("header hero switches from stacked to wide only when metadata has room", () => {
package/src/ui/layout.ts CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/ui/theme.tsx CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes