@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
@@ -52,20 +52,20 @@ export function AttachmentImportPanel({
52
52
  <Box flexDirection="column" width="100%" marginTop={1}>
53
53
  <Box
54
54
  borderStyle="round"
55
- borderColor={theme.BORDER_SUBTLE}
55
+ borderColor={theme.border}
56
56
  paddingX={2}
57
57
  paddingY={1}
58
58
  width="100%"
59
59
  >
60
- <Text color={theme.ACCENT} bold>IMPORT FILE </Text>
61
- <Text color={theme.MUTED}>
60
+ <Text color={theme.accent} bold>IMPORT FILE </Text>
61
+ <Text color={theme.textMuted}>
62
62
  Copy {files.length} outside-workspace {fileLabel} into .codexa/attachments?
63
63
  </Text>
64
64
  </Box>
65
65
 
66
66
  <Box
67
67
  borderStyle="round"
68
- borderColor={theme.BORDER_ACTIVE}
68
+ borderColor={theme.border}
69
69
  paddingX={2}
70
70
  paddingY={1}
71
71
  marginTop={1}
@@ -74,8 +74,8 @@ export function AttachmentImportPanel({
74
74
  >
75
75
  {files.map((file, i) => (
76
76
  <Box key={i} flexDirection="column" marginBottom={i < files.length - 1 ? 1 : 0}>
77
- <Text color={theme.TEXT}>{path.basename(file.srcPath)}</Text>
78
- <Text color={theme.DIM}>
77
+ <Text color={theme.text}>{path.basename(file.srcPath)}</Text>
78
+ <Text color={theme.textDim}>
79
79
  {"→ "}{relativeAttachmentsDir}/{file.destFilename}
80
80
  </Text>
81
81
  </Box>
@@ -83,14 +83,14 @@ export function AttachmentImportPanel({
83
83
 
84
84
  {showVisionWarning && (
85
85
  <Box marginTop={1}>
86
- <Text color={theme.WARNING}>
86
+ <Text color={theme.warning}>
87
87
  Note: active model may not support images.
88
88
  </Text>
89
89
  </Box>
90
90
  )}
91
91
 
92
92
  <Box marginTop={1}>
93
- <Text color={theme.DIM}>Enter copy and continue Esc cancel</Text>
93
+ <Text color={theme.textDim}>Esc to close · Enter to confirm</Text>
94
94
  </Box>
95
95
  </Box>
96
96
  </Box>
@@ -49,10 +49,10 @@ export function AuthPanel({
49
49
  const authStateLabel = getAuthStateLabel(authStatus.state);
50
50
  const authStateColor =
51
51
  authStatus.state === "authenticated"
52
- ? theme.SUCCESS
52
+ ? theme.success
53
53
  : authStatus.state === "unauthenticated"
54
- ? theme.ERROR
55
- : theme.WARNING;
54
+ ? theme.error
55
+ : theme.warning;
56
56
  const checkedAtLabel = authStatus.checkedAt > 0
57
57
  ? new Date(authStatus.checkedAt).toLocaleTimeString()
58
58
  : "not checked yet";
@@ -60,52 +60,52 @@ export function AuthPanel({
60
60
  return (
61
61
  <Box
62
62
  borderStyle="round"
63
- borderColor={theme.BORDER_ACTIVE}
63
+ borderColor={theme.borderFocused}
64
64
  flexDirection="column"
65
65
  paddingX={2}
66
66
  paddingY={1}
67
67
  marginTop={1}
68
68
  width="100%"
69
69
  >
70
- <Text color={theme.ACCENT} bold>
70
+ <Text color={theme.accent} bold>
71
71
  Auth and subscription guidance
72
72
  </Text>
73
- <Text color={theme.MUTED}>Current backend: {provider.label}</Text>
74
- <Text color={theme.MUTED}>Current preference: {formatAuthPreferenceLabel(authPreference)}</Text>
75
- <Text color={theme.INFO}>Backend auth: {provider.authLabel}</Text>
73
+ <Text color={theme.textMuted}>Current backend: {provider.label}</Text>
74
+ <Text color={theme.textMuted}>Current preference: {formatAuthPreferenceLabel(authPreference)}</Text>
75
+ <Text color={theme.info}>Backend auth: {provider.authLabel}</Text>
76
76
  <Text color={authStateColor}>Runtime auth state: {authStateLabel}</Text>
77
- <Text color={theme.DIM}>Last checked: {checkedAtLabel}</Text>
78
- <Text color={theme.DIM}>Probe summary: {authStatus.rawSummary || "No probe output yet"}</Text>
79
- <Text color={theme.TEXT}>
77
+ <Text color={theme.textDim}>Last checked: {checkedAtLabel}</Text>
78
+ <Text color={theme.textDim}>Probe summary: {authStatus.rawSummary || "No probe output yet"}</Text>
79
+ <Text color={theme.text}>
80
80
  This UI securely bridges to the Codexa neural network. It does not collect or store your ChatGPT credentials.
81
81
  </Text>
82
- <Text color={theme.TEXT}>{provider.statusMessage}</Text>
82
+ <Text color={theme.text}>{provider.statusMessage}</Text>
83
83
  <Box flexDirection="column" marginTop={1}>
84
- <Text color={theme.INFO}>Commands:</Text>
85
- <Text color={theme.TEXT}> /login guided ChatGPT sign-in steps</Text>
86
- <Text color={theme.TEXT}> /logout guided sign-out steps</Text>
87
- <Text color={theme.TEXT}> /auth status refresh Codexa authentication</Text>
84
+ <Text color={theme.info}>Commands:</Text>
85
+ <Text color={theme.text}> /login guided ChatGPT sign-in steps</Text>
86
+ <Text color={theme.text}> /logout guided sign-out steps</Text>
87
+ <Text color={theme.text}> /auth status refresh Codexa authentication</Text>
88
88
  </Box>
89
89
  <Box flexDirection="column" marginTop={1}>
90
90
  {AUTH_PREFERENCES.map((item, index) => (
91
- <Text key={item.id} color={item.id === authPreference ? theme.SUCCESS : theme.TEXT}>
91
+ <Text key={item.id} color={item.id === authPreference ? theme.success : theme.text}>
92
92
  {index + 1}. {item.label} {item.id === authPreference ? "✓" : ""}
93
93
  </Text>
94
94
  ))}
95
95
  </Box>
96
96
  <Box marginTop={1}>
97
- <Text color={theme.DIM}>
97
+ <Text color={theme.textDim}>
98
98
  Press 1-{AUTH_PREFERENCES.length} to change preference, R to refresh status, Esc to close.
99
99
  </Text>
100
100
  </Box>
101
101
  {authStatusBusy && (
102
102
  <Box marginTop={1}>
103
- <Text color={theme.WARNING}>Checking auth status...</Text>
103
+ <Text color={theme.warning}>Checking auth status...</Text>
104
104
  </Box>
105
105
  )}
106
106
  {authStatus.recommendedAction && (
107
107
  <Box marginTop={1}>
108
- <Text color={theme.DIM}>Next step: {authStatus.recommendedAction}</Text>
108
+ <Text color={theme.textDim}>Next step: {authStatus.recommendedAction}</Text>
109
109
  </Box>
110
110
  )}
111
111
  </Box>
@@ -23,7 +23,7 @@ import { clampVisualText, getShellWidth, type Layout } from "./layout.js";
23
23
  import { getTextWidth, splitTextAtColumn } from "./textLayout.js";
24
24
  import { useThrottledValue } from "./useThrottledValue.js";
25
25
  import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
26
- import { getStdinDebugState, traceInputDebug } from "../core/inputDebug.js";
26
+ import { getStdinDebugState, traceInputDebug } from "../core/debug/inputDebug.js";
27
27
  import * as renderDebug from "../core/perf/renderDebug.js";
28
28
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
29
29
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
@@ -100,9 +100,11 @@ interface BottomComposerProps {
100
100
  uiState: UIState;
101
101
  themeName?: string;
102
102
  mode?: string;
103
- model?: string;
104
- reasoningLevel?: string;
105
- planMode?: boolean;
103
+ model?: string;
104
+ footerModelDisplay?: string;
105
+ reasoningLevel?: string;
106
+ contextDisplay?: string;
107
+ planMode?: boolean;
106
108
  showBusyLoader?: boolean;
107
109
  tokensUsed?: number;
108
110
  modelSpec?: ModelSpec;
@@ -136,11 +138,11 @@ export interface BottomComposerMeasureParams {
136
138
  mode?: string;
137
139
  model?: string;
138
140
  reasoningLevel?: string;
139
- tokensUsed?: number;
140
- modelSpec?: ModelSpec;
141
- value: string;
142
- cursor: number;
143
- }
141
+ tokensUsed?: number;
142
+ modelSpec?: ModelSpec;
143
+ value: string;
144
+ cursor: number;
145
+ }
144
146
 
145
147
  export interface CommandSuggestionState {
146
148
  showSuggestions: boolean;
@@ -174,9 +176,9 @@ export function shouldRenderBusyFooter(layout: Layout, uiState: UIState): boolea
174
176
  return false;
175
177
  }
176
178
 
177
- export function getComposerToFooterGapRows(layout: Layout): number {
178
- return layout.mode === "micro" || layout.rows <= 24 ? 0 : 1;
179
- }
179
+ export function getComposerToFooterGapRows(layout: Layout): number {
180
+ return 0;
181
+ }
180
182
 
181
183
  export function getCommandSuggestionState({
182
184
  value,
@@ -204,11 +206,11 @@ export function getCommandSuggestionState({
204
206
  }
205
207
 
206
208
  export function measureBottomComposerRows({
207
- layout,
208
- uiState,
209
- value,
210
- cursor,
211
- }: BottomComposerMeasureParams): number {
209
+ layout,
210
+ uiState,
211
+ value,
212
+ cursor,
213
+ }: BottomComposerMeasureParams): number {
212
214
  if (shouldRenderBusyFooter(layout, uiState)) {
213
215
  return measureRunFooterRows();
214
216
  }
@@ -234,24 +236,27 @@ export function measureBottomComposerRows({
234
236
  inputLocked,
235
237
  });
236
238
 
237
- // ALWAYS reserve 1 row for the status line to prevent height shifting between idle and busy states.
238
- const statusLineReservedRows = 1;
239
- const showMetadata = layout.mode !== "micro" && layout.rows > 24;
240
- const bottomPadding = layout.mode === "micro" || layout.rows <= 24 ? 0 : 1;
241
- const footerGapRows = getComposerToFooterGapRows(layout);
242
-
243
- const visiblePromptRows = inputLocked ? 1 : promptViewport.visibleRows.length;
244
-
239
+ const bottomPadding = layout.mode === "compact" ? 0 : 1;
240
+ const footerGapRows = getComposerToFooterGapRows(layout);
241
+ const visibleStatusLine = getVisibleComposerStatusLine({
242
+ uiState,
243
+ value: normalizedValue,
244
+ allowCommands,
245
+ });
246
+ const transientStatusRows = visibleStatusLine.length > 0 ? 1 : 0;
247
+
248
+ const visiblePromptRows = inputLocked ? 1 : promptViewport.visibleRows.length;
249
+
245
250
  return (
246
251
  visiblePromptRows
247
- + 2
248
- + (commandSuggestionState.reserveSuggestionRow ? 1 : 0)
249
- + footerGapRows
250
- + statusLineReservedRows
251
- + (showMetadata ? 1 : 0)
252
- + bottomPadding
253
- );
254
- }
252
+ + 2
253
+ + (commandSuggestionState.reserveSuggestionRow ? 1 : 0)
254
+ + footerGapRows
255
+ + transientStatusRows
256
+ + 1
257
+ + bottomPadding
258
+ );
259
+ }
255
260
 
256
261
  function getExternalCliLabel(providerId: string): string | null {
257
262
  if (providerId === "google") return "Gemini CLI";
@@ -338,14 +343,54 @@ function getPlaceholder(persona: ComposerPersona): string {
338
343
 
339
344
  // ─── Component ────────────────────────────────────────────────────────────────
340
345
 
346
+ function renderFooterRuntime(displayStr: string, theme: any) {
347
+ // e.g. "Claude Code CLI / Sonnet 4.6 (Low)"
348
+ const slashIndex = displayStr.indexOf("/");
349
+ if (slashIndex === -1) {
350
+ return <Text color={theme.model} wrap="truncate">{displayStr}</Text>;
351
+ }
352
+ const providerPart = displayStr.substring(0, slashIndex).trim();
353
+ let remaining = displayStr.substring(slashIndex + 1).trim();
354
+
355
+ const parenIndex = remaining.indexOf("(");
356
+ if (parenIndex === -1) {
357
+ return (
358
+ <Box flexDirection="row" overflow="hidden">
359
+ <Text color={theme.provider}>{providerPart}</Text>
360
+ <Text color={theme.textMuted}>{" / "}</Text>
361
+ <Text color={theme.model}>{remaining}</Text>
362
+ </Box>
363
+ );
364
+ }
365
+
366
+ const modelPart = remaining.substring(0, parenIndex).trim();
367
+ let reasoningPart = remaining.substring(parenIndex + 1).trim();
368
+ if (reasoningPart.endsWith(")")) {
369
+ reasoningPart = reasoningPart.substring(0, reasoningPart.length - 1).trim();
370
+ }
371
+
372
+ return (
373
+ <Box flexDirection="row" overflow="hidden">
374
+ <Text color={theme.provider}>{providerPart}</Text>
375
+ <Text color={theme.textMuted}>{" / "}</Text>
376
+ <Text color={theme.model}>{modelPart}</Text>
377
+ <Text color={theme.textMuted}>{" ("}</Text>
378
+ <Text color={theme.accentMuted}>{reasoningPart}</Text>
379
+ <Text color={theme.textMuted}>{")"}</Text>
380
+ </Box>
381
+ );
382
+ }
383
+
341
384
  export function BottomComposer({
342
385
  layout,
343
386
  uiState,
344
387
  themeName = "purple",
345
388
  mode = "",
346
- model = "",
347
- reasoningLevel = "",
348
- planMode = false,
389
+ model = "",
390
+ footerModelDisplay,
391
+ reasoningLevel = "",
392
+ contextDisplay,
393
+ planMode = false,
349
394
  showBusyLoader = true,
350
395
  tokensUsed = 0,
351
396
  modelSpec = FALLBACK_MODEL_SPEC,
@@ -522,9 +567,10 @@ export function BottomComposer({
522
567
  .map((suggestion, index) => `${index === selectedIndex ? "›" : "·"} ${suggestion.cmd}`)
523
568
  .join(" ");
524
569
 
525
- const rawStatusLine = getVisibleComposerStatusLine({ uiState, value, allowCommands, activeProviderId, runElapsedSeconds, externalCliStatus });
526
- const showStatusLine = rawStatusLine.length > 0;
527
- const footerGapRows = getComposerToFooterGapRows(layout);
570
+ const rawStatusLine = getVisibleComposerStatusLine({ uiState, value, allowCommands, activeProviderId, runElapsedSeconds, externalCliStatus });
571
+ const showStatusLine = rawStatusLine.length > 0;
572
+ const showTransientStatusRow = showStatusLine || inputLocked || !!selectionProfile;
573
+ const footerGapRows = getComposerToFooterGapRows(layout);
528
574
 
529
575
  const promptViewport = useMemo(
530
576
  () => createInputViewport({
@@ -807,16 +853,12 @@ export function BottomComposer({
807
853
  }
808
854
  }, { isActive: isFocused });
809
855
 
810
- const modeDisplay = getModeDisplaySpec(mode, theme);
811
856
  const tokenDisplay = getTokenBarDisplay(tokensUsed, modelSpec);
812
- const tokenColor = tokenDisplay.percentage === null ? theme.DIM
813
- : tokenDisplay.percentage >= 90 ? theme.ERROR
814
- : tokenDisplay.percentage >= 70 ? theme.WARNING
815
- : theme.SUCCESS;
816
857
  const reasoningSuffix = reasoningLevel ? ` (${reasoningLevel})` : "";
858
+ const footerRuntimeDisplay = footerModelDisplay ?? `${model}${reasoningSuffix}`;
817
859
  const isAnswerMode = persona === "answer";
818
860
  const showBusyFooter = shouldRenderBusyFooter(layout, uiState);
819
- const promptPrefixColor = inputLocked ? theme.DIM : theme.TEXT;
861
+ const promptPrefixColor = inputLocked ? theme.textDim : theme.text;
820
862
  const lockedInputText = promptViewport.visibleRows[0]?.text ?? " ";
821
863
 
822
864
  // The prompt line is shared between bordered and non-bordered layouts.
@@ -826,12 +868,12 @@ export function BottomComposer({
826
868
  <Box flexDirection="column" flexGrow={1}>
827
869
  {value.length === 0 && !inputLocked ? (
828
870
  <Box width="100%" overflow="hidden">
829
- <Text backgroundColor={cursorVisible ? theme.TEXT : undefined} color={cursorVisible ? theme.PANEL : undefined}>{" "}</Text>
830
- <Text color={theme.DIM}>{placeholderText}</Text>
871
+ <Text backgroundColor={cursorVisible ? theme.text : undefined} color={cursorVisible ? theme.surface : undefined}>{" "}</Text>
872
+ <Text color={theme.textDim}>{placeholderText}</Text>
831
873
  </Box>
832
874
  ) : inputLocked ? (
833
875
  <Box key="busy-locked-input" width="100%" overflow="hidden">
834
- <Text color={theme.DIM}>{lockedInputText || " "}</Text>
876
+ <Text color={theme.textDim}>{lockedInputText || " "}</Text>
835
877
  </Box>
836
878
  ) : (
837
879
  promptViewport.visibleRows.map((row, index) => {
@@ -845,14 +887,14 @@ export function BottomComposer({
845
887
  <Box key={`${row.start}-${row.end}-${index}`} width="100%" overflow="hidden">
846
888
  {isCursorRow && segments ? (
847
889
  <>
848
- <Text color={theme.TEXT}>{segments.before}</Text>
849
- <Text backgroundColor={cursorVisible ? theme.TEXT : undefined} color={cursorVisible ? theme.PANEL : undefined}>
890
+ <Text color={theme.text}>{segments.before}</Text>
891
+ <Text backgroundColor={cursorVisible ? theme.text : undefined} color={cursorVisible ? theme.surface : undefined}>
850
892
  {segments.current || " "}
851
893
  </Text>
852
- <Text color={theme.TEXT}>{segments.after}</Text>
894
+ <Text color={theme.text}>{segments.after}</Text>
853
895
  </>
854
896
  ) : (
855
- <Text color={theme.TEXT}>{row.text || " "}</Text>
897
+ <Text color={theme.text}>{row.text || " "}</Text>
856
898
  )}
857
899
  </Box>
858
900
  );
@@ -866,17 +908,17 @@ export function BottomComposer({
866
908
  return <MemoizedRunFooter uiState={uiState} showBusyLoader={showBusyLoader} onCancel={onCancel} onQuit={onQuit} />;
867
909
  }
868
910
 
869
- return (
870
- <Box flexDirection="column" paddingBottom={layoutMode === "micro" || crampedViewport ? 0 : 1} width="100%">
871
- {isAnswerMode ? (
872
- // Answer mode: Highlighted prompt
873
- <Box
911
+ return (
912
+ <Box flexDirection="column" paddingBottom={layoutMode === "compact" ? 0 : 1} width="100%">
913
+ {isAnswerMode ? (
914
+ // Answer mode: Highlighted prompt
915
+ <Box
874
916
  flexDirection="column"
875
917
  width="100%"
876
918
  paddingX={1}
877
919
  paddingY={0}
878
920
  borderStyle="round"
879
- borderColor={theme.WARNING}
921
+ borderColor={theme.warning}
880
922
  >
881
923
  {promptLine}
882
924
  </Box>
@@ -888,15 +930,15 @@ export function BottomComposer({
888
930
  paddingX={1}
889
931
  paddingY={0}
890
932
  borderStyle="round"
891
- borderColor={theme.BORDER_SUBTLE}
933
+ borderColor={theme.border}
892
934
  >
893
935
  {promptLine}
894
- </Box>
895
- )}
896
-
897
- {commandSuggestionState.reserveSuggestionRow && (
898
- <Box paddingLeft={1} marginTop={0} width="100%" overflow="hidden">
899
- <Text color={theme.DIM} wrap="truncate">{suggestionText || " "}</Text>
936
+ </Box>
937
+ )}
938
+
939
+ {commandSuggestionState.reserveSuggestionRow && (
940
+ <Box paddingLeft={1} marginTop={0} width="100%" overflow="hidden">
941
+ <Text color={theme.textDim} wrap="truncate">{suggestionText || " "}</Text>
900
942
  </Box>
901
943
  )}
902
944
 
@@ -904,70 +946,66 @@ export function BottomComposer({
904
946
  <Box height={footerGapRows} />
905
947
  )}
906
948
 
907
- {/* Always reserve the status line height */}
908
- <Box paddingX={1} marginTop={0} height={1} width="100%" justifyContent="space-between" overflow="hidden">
909
- {showStatusLine && (
910
- <>
911
- <Box flexShrink={1} flexGrow={1} overflow="hidden" flexDirection="row">
912
- {!!getExternalCliLabel(activeProviderId ?? "") && uiState.kind === "THINKING" && (
949
+ {showTransientStatusRow && (
950
+ <Box paddingX={1} marginTop={0} height={1} width="100%" justifyContent="space-between" overflow="hidden">
951
+ <>
952
+ <Box flexShrink={1} flexGrow={1} overflow="hidden" flexDirection="row">
953
+ {!!getExternalCliLabel(activeProviderId ?? "") && uiState.kind === "THINKING" && (
913
954
  <>
914
- <Spinner color={theme.ACCENT} />
955
+ <Spinner color={theme.accent} />
915
956
  <Text>{" "}</Text>
916
957
  </>
917
958
  )}
918
- <AnimatedStatusText
919
- baseText={rawStatusLine}
920
- isActive={!getExternalCliLabel(activeProviderId ?? "") && inputLocked && showBusyLoader}
921
- isError={persona === "error"}
922
- />
959
+ <AnimatedStatusText
960
+ baseText={rawStatusLine}
961
+ isActive={!getExternalCliLabel(activeProviderId ?? "") && inputLocked && showBusyLoader}
962
+ isError={persona === "error"}
963
+ />
923
964
  </Box>
924
965
  {inputLocked && (
925
966
  <Box flexShrink={0}>
926
- <Text color={theme.DIM}>Esc cancel Ctrl+C quit</Text>
967
+ <Text color={theme.textDim}>Esc cancel Ctrl+C quit</Text>
927
968
  </Box>
928
969
  )}
929
970
  {!inputLocked && selectionProfile && (
930
971
  <Box flexShrink={0}>
931
- <Text color={theme.DIM}>{selectionProfile.shortHint}</Text>
932
- </Box>
933
- )}
934
- </>
935
- )}
936
- </Box>
937
-
938
- {layoutMode !== "micro" && !crampedViewport && (
939
- <Box paddingLeft={1} paddingRight={1} marginTop={0} width="100%" justifyContent="space-between">
940
- <Box flexGrow={1} flexShrink={1} overflow="hidden">
941
- <Text
942
- color={modeDisplay.ringColor}
943
- backgroundColor={modeDisplay.ringFill}
944
- bold={modeDisplay.ringBold}
945
- >
946
- {modeDisplay.ringGlyph}
947
- </Text>
948
- <Text color={theme.DIM}>{" "}</Text>
949
- <Text color={modeDisplay.labelColor} bold={modeDisplay.labelBold}>{modeDisplay.label}</Text>
950
- <Text color={theme.DIM}>{" "}{model}{reasoningSuffix}{" Ctrl+O Ctrl+Alt+P"}</Text>
951
- {planMode && <Text color={theme.ACCENT}>{" Plan"}</Text>}
952
- </Box>
953
- <Box flexShrink={0}>
954
- {tokenDisplay.hasKnownLimit ? (
955
- <>
956
- <Text color={theme.DIM}>Context: </Text>
957
- <Text color={theme.TEXT}>{tokenDisplay.usedText}</Text>
958
- <Text color={theme.DIM}>
959
- {" / "}{tokenDisplay.limitText}
960
- {tokenDisplay.percentage !== null ? ` · ${tokenDisplay.isEstimatedLimit ? "~" : ""}${tokenDisplay.percentage}%` : ""}
961
- </Text>
962
- </>
963
- ) : (
964
- <Text color={theme.DIM}>Context: Unknown</Text>
965
- )}
966
- </Box>
967
- </Box>
968
- )}
969
- </Box>
970
- );
972
+ <Text color={theme.textDim}>{selectionProfile.shortHint}</Text>
973
+ </Box>
974
+ )}
975
+ </>
976
+ </Box>
977
+ )}
978
+
979
+ <Box paddingLeft={1} paddingRight={1} marginTop={0} width="100%" justifyContent="space-between">
980
+ <Box flexGrow={1} flexShrink={1} overflow="hidden">
981
+ {renderFooterRuntime(footerRuntimeDisplay, theme)}
982
+ </Box>
983
+ <Box flexShrink={0}>
984
+ {contextDisplay ? (
985
+ <Box flexDirection="row">
986
+ <Text color={theme.textMuted}>Context: </Text>
987
+ <Text color={theme.context}>{contextDisplay}</Text>
988
+ </Box>
989
+ ) : tokenDisplay.hasKnownLimit ? (
990
+ <Box flexDirection="row">
991
+ <Text color={theme.textMuted}>Context: </Text>
992
+ <Text color={theme.context}>{tokenDisplay.usedText}</Text>
993
+ <Text color={theme.textDim}>
994
+ {" / "}{tokenDisplay.limitText}
995
+ {tokenDisplay.percentage !== null ? ` · ${tokenDisplay.isEstimatedLimit ? "~" : ""}${tokenDisplay.percentage}%` : ""}
996
+ </Text>
997
+ </Box>
998
+ ) : (
999
+ <Box flexDirection="row">
1000
+ <Text color={theme.textMuted}>Context: </Text>
1001
+ <Text color={theme.textDim}>Unknown</Text>
1002
+ </Box>
1003
+ )}
1004
+ </Box>
1005
+ </Box>
1006
+
1007
+ </Box>
1008
+ );
971
1009
  }
972
1010
 
973
1011
  // Helper to extract the relevant uiState kind for comparison
@@ -1003,7 +1041,9 @@ export const MemoizedBottomComposer = memo(BottomComposer, (prev, next) => {
1003
1041
  // Re-render if display props change
1004
1042
  if (prev.mode !== next.mode) return false;
1005
1043
  if (prev.model !== next.model) return false;
1044
+ if (prev.footerModelDisplay !== next.footerModelDisplay) return false;
1006
1045
  if (prev.reasoningLevel !== next.reasoningLevel) return false;
1046
+ if (prev.contextDisplay !== next.contextDisplay) return false;
1007
1047
  if (prev.planMode !== next.planMode) return false;
1008
1048
  if (prev.showBusyLoader !== next.showBusyLoader) return false;
1009
1049
  if (prev.tokensUsed !== next.tokensUsed) return false;
@@ -24,9 +24,9 @@ export function DashCard({
24
24
  children,
25
25
  }: DashCardProps) {
26
26
  const theme = useTheme();
27
- const border = borderColor ?? theme.BORDER_SUBTLE;
28
- const tColor = titleColor ?? theme.MUTED;
29
- const bColor = badgeColor ?? theme.DIM;
27
+ const border = borderColor ?? theme.border;
28
+ const tColor = titleColor ?? theme.textMuted;
29
+ const bColor = badgeColor ?? theme.textDim;
30
30
 
31
31
  // Account for AppShell gutter (-1) and Timeline paddingX={1} (-2)
32
32
  const w = cols - 3;