@golba98/codexa 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -0,0 +1,322 @@
1
+ import React, { memo, useEffect, useMemo, useRef } from "react";
2
+ import { Box, Static } from "ink";
3
+ import type { RuntimeSummary } from "../config/runtimeConfig.js";
4
+ import type { CodexAuthState } from "../core/auth/codexAuth.js";
5
+ import * as renderDebug from "../core/perf/renderDebug.js";
6
+ import type { TimelineEvent, UIState } from "../session/types.js";
7
+ import {
8
+ buildActiveRenderItems,
9
+ buildIntroRenderItem,
10
+ buildStaticRenderItems,
11
+ buildTimelineItems,
12
+ TimelineRowView,
13
+ type TimelineItem,
14
+ } from "./Timeline.js";
15
+ import { getShellHeight, getShellWidth, resolveStartupHeaderMode, type TerminalViewport } from "./layout.js";
16
+ import {
17
+ buildNativeTranscriptParts,
18
+ type NativeTranscriptRowItem,
19
+ type TimelineRow,
20
+ } from "./timelineMeasure.js";
21
+ import { LOGO_COMPACT, LOGO_COMPACT_MIN_COLS, LOGO_LARGE, LOGO_MEDIUM, selectLogoVariant } from "./logoVariants.js";
22
+
23
+ type TranscriptStaticItem = NativeTranscriptRowItem & { type: "rows" };
24
+ type StaticRenderItem = TranscriptStaticItem;
25
+
26
+ export interface TranscriptShellProps {
27
+ layout: TerminalViewport;
28
+ authState: CodexAuthState;
29
+ workspaceLabel: string;
30
+ workspaceRoot?: string | null;
31
+ runtimeSummary?: RuntimeSummary | null;
32
+ staticEvents: TimelineEvent[];
33
+ activeEvents: TimelineEvent[];
34
+ uiState: UIState;
35
+ composer: React.ReactNode;
36
+ composerRows?: number;
37
+ verboseMode?: boolean;
38
+ clearCount?: number;
39
+ /**
40
+ * Bumped whenever a width-changing resize forces the terminal's app.tsx-owned
41
+ * clear boundary to physically wipe the screen. Folded into <Static>'s key
42
+ * (not the whole component's) so already-flushed content reprints at the
43
+ * new width — without remounting the composer or anything else.
44
+ */
45
+ repaintGeneration?: number;
46
+ visible?: boolean;
47
+ }
48
+
49
+ function RowsBlock({ rows }: { rows: TimelineRow[] }) {
50
+ return (
51
+ <Box flexDirection="column">
52
+ {rows.map((row) => (
53
+ <TimelineRowView key={row.key} row={row} />
54
+ ))}
55
+ </Box>
56
+ );
57
+ }
58
+
59
+ function isTranscriptEvent(event: TimelineEvent): boolean {
60
+ return event.type === "user" || event.type === "assistant" || event.type === "run" || event.type === "shell";
61
+ }
62
+
63
+ export function isHomeScreenState({
64
+ staticEvents,
65
+ activeEvents,
66
+ uiState,
67
+ }: {
68
+ staticEvents: TimelineEvent[];
69
+ activeEvents: TimelineEvent[];
70
+ uiState: UIState;
71
+ }): boolean {
72
+ return uiState.kind === "IDLE"
73
+ && !staticEvents.some(isTranscriptEvent)
74
+ && !activeEvents.some(isTranscriptEvent);
75
+ }
76
+
77
+ function getLogoVariantName(rows: readonly string[]): "large" | "medium" | "compact" | "wordmark" | "none" {
78
+ if (rows.length === 0) return process.env["CODEXA_NO_ASCII_LOGO"] === "1" ? "none" : "wordmark";
79
+ if (rows === LOGO_LARGE || rows.join("\n") === LOGO_LARGE.join("\n")) return "large";
80
+ if (rows === LOGO_MEDIUM || rows.join("\n") === LOGO_MEDIUM.join("\n")) return "medium";
81
+ if (rows === LOGO_COMPACT || rows.join("\n") === LOGO_COMPACT.join("\n")) return "compact";
82
+ return "wordmark";
83
+ }
84
+
85
+ function getLogoHiddenReason({
86
+ startupHeaderMode,
87
+ logoVariant,
88
+ width,
89
+ }: {
90
+ startupHeaderMode: ReturnType<typeof resolveStartupHeaderMode>;
91
+ logoVariant: ReturnType<typeof getLogoVariantName>;
92
+ width: number;
93
+ }): string | null {
94
+ if (startupHeaderMode === "tiny") return "terminal-too-small";
95
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return "CODEXA_NO_ASCII_LOGO";
96
+ if (logoVariant === "wordmark") return `no-ascii-variant-fits-width-${width}`;
97
+ if (logoVariant === "none") return `no-logo-variant-fits-width-${width}`;
98
+ return null;
99
+ }
100
+
101
+ function buildTranscriptItems({
102
+ layout,
103
+ authState,
104
+ workspaceLabel,
105
+ workspaceRoot,
106
+ runtimeSummary,
107
+ staticEvents,
108
+ activeEvents,
109
+ uiState,
110
+ composerRows,
111
+ verboseMode,
112
+ }: Pick<
113
+ TranscriptShellProps,
114
+ | "layout"
115
+ | "authState"
116
+ | "workspaceLabel"
117
+ | "workspaceRoot"
118
+ | "runtimeSummary"
119
+ | "staticEvents"
120
+ | "activeEvents"
121
+ | "uiState"
122
+ | "composerRows"
123
+ | "verboseMode"
124
+ >): { staticItems: TranscriptStaticItem[]; liveRows: TimelineRow[]; startupHeaderMode: ReturnType<typeof resolveStartupHeaderMode> } {
125
+ const staticItems = buildTimelineItems(staticEvents);
126
+ const activeItems = buildTimelineItems(activeEvents);
127
+ const turnIds = [...staticItems, ...activeItems]
128
+ .filter((item): item is Extract<TimelineItem, { type: "turn" }> => item.type === "turn")
129
+ .map((item) => item.turnId);
130
+ const startupHeaderMode = resolveStartupHeaderMode({
131
+ cols: layout.cols,
132
+ rows: layout.rows,
133
+ introRows: 8,
134
+ composerRows: composerRows ?? 5,
135
+ });
136
+
137
+ const parts = buildNativeTranscriptParts(
138
+ [
139
+ buildIntroRenderItem({
140
+ authState,
141
+ workspaceLabel,
142
+ layout,
143
+ providerLabel: runtimeSummary?.providerLabel ?? null,
144
+ startupHeaderMode,
145
+ }),
146
+ ...buildStaticRenderItems(staticItems, turnIds, null, null, null),
147
+ ...buildActiveRenderItems(activeItems, turnIds, uiState),
148
+ ],
149
+ {
150
+ totalWidth: getShellWidth(layout.cols),
151
+ verboseMode,
152
+ debugLabel: "transcript-shell",
153
+ workspaceRoot,
154
+ },
155
+ );
156
+
157
+ return {
158
+ staticItems: parts.staticItems.map((item) => ({ ...item, type: "rows" as const })),
159
+ liveRows: parts.liveRows,
160
+ startupHeaderMode,
161
+ };
162
+ }
163
+
164
+ function TranscriptShellInner({
165
+ layout,
166
+ authState,
167
+ workspaceLabel,
168
+ workspaceRoot = null,
169
+ runtimeSummary = null,
170
+ staticEvents,
171
+ activeEvents,
172
+ uiState,
173
+ composer,
174
+ composerRows,
175
+ verboseMode = false,
176
+ clearCount = 0,
177
+ visible = true,
178
+ }: TranscriptShellProps) {
179
+ const { staticItems, liveRows, startupHeaderMode } = useMemo(
180
+ () => buildTranscriptItems({
181
+ layout,
182
+ authState,
183
+ workspaceLabel,
184
+ workspaceRoot,
185
+ runtimeSummary,
186
+ staticEvents,
187
+ activeEvents,
188
+ uiState,
189
+ composerRows,
190
+ verboseMode,
191
+ }),
192
+ [activeEvents, authState, composerRows, layout, runtimeSummary, staticEvents, uiState, verboseMode, workspaceLabel, workspaceRoot],
193
+ );
194
+ const homeScreenActive = visible && isHomeScreenState({ staticEvents, activeEvents, uiState });
195
+ const visibleStaticItemsRef = useRef(staticItems);
196
+
197
+ useEffect(() => {
198
+ if (visible) {
199
+ visibleStaticItemsRef.current = staticItems;
200
+ }
201
+ }, [staticItems, visible]);
202
+
203
+ const displayedStaticItems = visible ? staticItems : visibleStaticItemsRef.current;
204
+
205
+ const staticRenderItems = useMemo<StaticRenderItem[]>(() => {
206
+ return displayedStaticItems.map((item) => ({
207
+ ...item,
208
+ key: `clear-${clearCount}-${item.key}`,
209
+ }));
210
+ }, [clearCount, displayedStaticItems]);
211
+
212
+ const displayedLiveRows = visible ? liveRows : [];
213
+ const staticRowCount = useMemo(
214
+ () => displayedStaticItems.reduce((rowCount, item) => rowCount + item.rows.length, 0),
215
+ [displayedStaticItems],
216
+ );
217
+ const liveBottomSpacerRows = visible
218
+ ? Math.max(0, getShellHeight(layout.rows) - staticRowCount - displayedLiveRows.length - (composerRows ?? 0))
219
+ : 0;
220
+ const spacerRows = useMemo<TimelineRow[]>(
221
+ () => Array.from({ length: liveBottomSpacerRows }, (_, index) => ({
222
+ key: `live-bottom-spacer-${clearCount}-${index}`,
223
+ spans: [{ text: " ".repeat(getShellWidth(layout.cols)) }],
224
+ })),
225
+ [clearCount, layout.cols, liveBottomSpacerRows],
226
+ );
227
+ const shellWidth = getShellWidth(layout.cols);
228
+ const introInnerWidth = Math.max(10, shellWidth - 2);
229
+ const selectedLogoRows = startupHeaderMode === "tiny"
230
+ ? []
231
+ : startupHeaderMode === "large"
232
+ ? selectLogoVariant(introInnerWidth)
233
+ : introInnerWidth >= LOGO_COMPACT_MIN_COLS ? LOGO_COMPACT : [];
234
+ const selectedLogoVariant = getLogoVariantName(selectedLogoRows);
235
+ const logoHiddenReason = getLogoHiddenReason({
236
+ startupHeaderMode,
237
+ logoVariant: selectedLogoVariant,
238
+ width: introInnerWidth,
239
+ });
240
+ const startupTraceKeyRef = useRef<string | null>(null);
241
+
242
+ useEffect(() => {
243
+ const nextKey = [
244
+ layout.cols,
245
+ layout.rows,
246
+ layout.mode,
247
+ startupHeaderMode,
248
+ homeScreenActive ? "home" : "transcript",
249
+ staticEvents.length,
250
+ activeEvents.length,
251
+ uiState.kind,
252
+ selectedLogoVariant,
253
+ clearCount,
254
+ ].join("|");
255
+ if (startupTraceKeyRef.current === nextKey) return;
256
+ startupTraceKeyRef.current = nextKey;
257
+ renderDebug.traceEvent("startup", "homeRender", {
258
+ cols: layout.cols,
259
+ rows: layout.rows,
260
+ layoutMode: layout.mode,
261
+ activeRoot: "TranscriptShell",
262
+ messageCount: [...staticEvents, ...activeEvents].filter(isTranscriptEvent).length,
263
+ staticEventsLength: staticEvents.length,
264
+ activeEventsLength: activeEvents.length,
265
+ uiStateKind: uiState.kind,
266
+ selectedLayoutMode: startupHeaderMode,
267
+ selectedLogoVariant,
268
+ logoBranchSelected: selectedLogoVariant !== "none" && selectedLogoVariant !== "wordmark",
269
+ logoHiddenReason,
270
+ composerCount: visible ? 1 : 0,
271
+ footerCount: visible ? 1 : 0,
272
+ homeScreenRendererUsed: homeScreenActive,
273
+ staticItemCount: staticRenderItems.length,
274
+ liveRowCount: displayedLiveRows.length,
275
+ clearCount,
276
+ });
277
+ }, [
278
+ activeEvents,
279
+ clearCount,
280
+ displayedLiveRows.length,
281
+ homeScreenActive,
282
+ layout.cols,
283
+ layout.mode,
284
+ layout.rows,
285
+ logoHiddenReason,
286
+ selectedLogoVariant,
287
+ startupHeaderMode,
288
+ staticEvents,
289
+ staticRenderItems.length,
290
+ uiState.kind,
291
+ visible,
292
+ ]);
293
+
294
+ return (
295
+ <Box flexDirection="column" width="100%" display={visible ? "flex" : "none"}>
296
+ <Static key={`static-${clearCount}`} items={staticRenderItems}>
297
+ {(item) => <RowsBlock key={item.key} rows={item.rows} />}
298
+ </Static>
299
+
300
+ {displayedLiveRows.length > 0 && <RowsBlock rows={displayedLiveRows} />}
301
+ {spacerRows.length > 0 && <RowsBlock rows={spacerRows} />}
302
+
303
+ {visible && composer}
304
+ </Box>
305
+ );
306
+ }
307
+
308
+ export const TranscriptShell = memo(function TranscriptShell(props: TranscriptShellProps) {
309
+ // repaintGeneration is folded in here (not just <Static>'s own key) because
310
+ // Ink only reliably re-flushes <Static> content on a genuine fresh mount of
311
+ // the whole subtree — its "capture before delete" escape hatch
312
+ // (reconciler.js's isStaticDirty/onImmediateRender) does not fire the same
313
+ // way when only the inner <Static> node is keyed away and remounted on its
314
+ // own. This does remount the composer too, but only on the (already
315
+ // disruptive) event of a real terminal resize, not during normal typing.
316
+ return (
317
+ <TranscriptShellInner
318
+ key={`clear-${props.clearCount ?? 0}-repaint-${props.repaintGeneration ?? 0}`}
319
+ {...props}
320
+ />
321
+ );
322
+ });
@@ -18,7 +18,7 @@ import { useTheme } from "./theme.js";
18
18
  import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
19
19
  import { wrapPlainText, wrapCommandText } from "./textLayout.js";
20
20
  import { selectVisibleRunActivity } from "./runActivityView.js";
21
- import type { RunFileActivity } from "../core/workspaceActivity.js";
21
+ import type { RunFileActivity } from "../core/workspace/workspaceActivity.js";
22
22
  import { RUN_OUTPUT_TRUNCATION_NOTICE } from "../session/chatLifecycle.js";
23
23
  import { formatProgressBlockBodyLines } from "./progressEntries.js";
24
24
  import { getUsableShellWidth, transcriptContentIndent } from "./layout.js";
@@ -32,7 +32,7 @@ import {
32
32
  } from "./outputPipeline.js";
33
33
  import { normalizeCommand, getFriendlyActionLabel } from "./commandNormalize.js";
34
34
  import * as renderDebug from "../core/perf/renderDebug.js";
35
- import { normalizePlanReviewMarkdown } from "../core/planStorage.js";
35
+ import { normalizePlanReviewMarkdown } from "../core/workspace/planStorage.js";
36
36
 
37
37
  export type TurnOpacity = "active" | "recent" | "dim";
38
38
 
@@ -69,14 +69,14 @@ function UserInputCard({
69
69
  dim: boolean;
70
70
  }) {
71
71
  const theme = useTheme();
72
- const borderColor = theme.BORDER_SUBTLE;
72
+ const borderColor = theme.border;
73
73
  const contentWidth = Math.max(1, cols - 7);
74
74
  const lines = wrapPlainText(sanitizeTerminalOutput(prompt), contentWidth);
75
75
 
76
76
  return (
77
77
  <DashCard cols={cols} title="PROMPT" borderColor={borderColor}>
78
78
  {lines.map((line, i) => (
79
- <Text key={i} color={dim ? theme.DIM : theme.TEXT}>
79
+ <Text key={i} color={dim ? theme.textDim : theme.text}>
80
80
  {i === 0 ? "❯ " : " "}{line}
81
81
  </Text>
82
82
  ))}
@@ -122,38 +122,38 @@ function ImpactSummary({
122
122
 
123
123
  const opColor = (op: string) => {
124
124
  switch (op) {
125
- case "created": return theme.SUCCESS;
126
- case "deleted": return theme.ERROR;
127
- default: return theme.INFO;
125
+ case "created": return theme.success;
126
+ case "deleted": return theme.error;
127
+ default: return theme.info;
128
128
  }
129
129
  };
130
130
 
131
131
  return (
132
132
  <Box flexDirection="column" width="100%" paddingX={1} marginTop={0}>
133
133
  {hasDeletes && (
134
- <Text color={theme.WARNING}>{"⚠ Destructive changes detected:"}</Text>
134
+ <Text color={theme.warning}>{"⚠ Destructive changes detected:"}</Text>
135
135
  )}
136
136
  {hasFiles && (
137
137
  <>
138
- <Text color={theme.DIM}>{" Changes:"}</Text>
138
+ <Text color={theme.textDim}>{" Changes:"}</Text>
139
139
  {recentFiles.map((file: RunFileActivity, i: number) => {
140
140
  const diffInfo = file.addedLines != null || file.removedLines != null
141
141
  ? ` (+${file.addedLines ?? 0} -${file.removedLines ?? 0})`
142
142
  : "";
143
143
  return (
144
144
  <Text key={i}>
145
- <Text color={theme.DIM}>{" "}</Text>
145
+ <Text color={theme.textDim}>{" "}</Text>
146
146
  <Text color={opColor(file.operation)}>{opLabel(file.operation)}</Text>
147
- <Text color={theme.TEXT}>{" "}{file.path}</Text>
148
- <Text color={theme.DIM}>{diffInfo}</Text>
147
+ <Text color={theme.text}>{" "}{file.path}</Text>
148
+ <Text color={theme.textDim}>{diffInfo}</Text>
149
149
  </Text>
150
150
  );
151
151
  })}
152
152
  </>
153
153
  )}
154
- <Text color={theme.DIM}>
154
+ <Text color={theme.textDim}>
155
155
  {" "}
156
- <Text color={theme.SUCCESS}>{"✔ "}</Text>
156
+ <Text color={theme.success}>{"✔ "}</Text>
157
157
  {run.touchedFileCount > 0 && `${run.touchedFileCount} file${run.touchedFileCount === 1 ? "" : "s"}`}
158
158
  {hasTools && `${hasFiles ? " • " : ""}${run.toolActivities.length} action${run.toolActivities.length === 1 ? "" : "s"}`}
159
159
  {run.durationMs != null && ` • ${formatDuration(run.durationMs)}`}
@@ -172,11 +172,11 @@ function FileScanCard({ run, cols }: { run: RunEvent; cols: number }) {
172
172
  return (
173
173
  <DashCard cols={cols} title="Scanning workspace ..." rightBadge={badge}>
174
174
  {hiddenCount > 0 && (
175
- <Text color={theme.DIM}>{`... ${hiddenCount} more`}</Text>
175
+ <Text color={theme.textDim}>{`... ${hiddenCount} more`}</Text>
176
176
  )}
177
177
  {visible.map((file, i) => (
178
- <Text key={i} color={theme.SUCCESS}>
179
- {"● "}<Text color={theme.TEXT}>{file.path}</Text>
178
+ <Text key={i} color={theme.success}>
179
+ {"● "}<Text color={theme.text}>{file.path}</Text>
180
180
  </Text>
181
181
  ))}
182
182
  </DashCard>
@@ -307,9 +307,9 @@ function PlanPanel({
307
307
  cols={cols}
308
308
  title="Plan"
309
309
  rightBadge={approved ? "approved" : undefined}
310
- borderColor={theme.ACCENT}
311
- titleColor={theme.TEXT}
312
- badgeColor={theme.SUCCESS}
310
+ borderColor={theme.accent}
311
+ titleColor={theme.text}
312
+ badgeColor={theme.success}
313
313
  >
314
314
  <MemoizedRenderMessage segments={formatted} width={contentWidth} brightHeadings />
315
315
  </DashCard>
@@ -337,12 +337,12 @@ function ActionEventCard({
337
337
  const actionLabel = getFriendlyActionLabel(actionNormalized);
338
338
 
339
339
  const statusIcon = tool.status === "failed" ? "✕" : tool.status === "completed" ? "✔" : "▸";
340
- const statusColor = tool.status === "failed" ? theme.ERROR : tool.status === "completed" ? theme.SUCCESS : theme.INFO;
341
- const borderColor = dim ? theme.BORDER_SUBTLE : tool.status === "running" ? theme.BORDER_ACTIVE : theme.BORDER_SUBTLE;
340
+ const statusColor = tool.status === "failed" ? theme.error : tool.status === "completed" ? theme.success : theme.info;
341
+ const borderColor = dim ? theme.border : tool.status === "running" ? theme.borderFocused : theme.border;
342
342
  const detailText = isLiveCursorTarget && tool.status === "running"
343
343
  ? "▌"
344
344
  : tool.summary?.trim() ? tool.summary : " ";
345
- const detailColor = isLiveCursorTarget && tool.status === "running" ? theme.ACCENT : theme.MUTED;
345
+ const detailColor = isLiveCursorTarget && tool.status === "running" ? theme.accent : theme.textMuted;
346
346
  const duration = tool.completedAt != null
347
347
  ? formatDuration(tool.completedAt - tool.startedAt)
348
348
  : null;
@@ -356,10 +356,10 @@ function ActionEventCard({
356
356
  <>
357
357
  <Box>
358
358
  <Text color={statusColor}>{statusIcon + " "}</Text>
359
- <Text color={dim ? theme.DIM : theme.TEXT}>{actionLabel}</Text>
359
+ <Text color={dim ? theme.textDim : theme.text}>{actionLabel}</Text>
360
360
  </Box>
361
361
  {commandLines.map((line, i) => (
362
- <Text key={i} color={theme.MUTED}>{" "}{line || " "}</Text>
362
+ <Text key={i} color={theme.textMuted}>{" "}{line || " "}</Text>
363
363
  ))}
364
364
  </>
365
365
  ) : (
@@ -367,10 +367,10 @@ function ActionEventCard({
367
367
  {commandLines.map((line, i) => (
368
368
  <Box key={i}>
369
369
  <Text color={i === 0 ? statusColor : undefined}>{i === 0 ? statusIcon + " " : " "}</Text>
370
- <Text color={dim ? theme.DIM : theme.TEXT}>{line || " "}</Text>
370
+ <Text color={dim ? theme.textDim : theme.text}>{line || " "}</Text>
371
371
  </Box>
372
372
  ))}
373
- <Text color={theme.MUTED}>{" "}</Text>
373
+ <Text color={theme.textMuted}>{" "}</Text>
374
374
  </>
375
375
  )}
376
376
  <Text color={detailColor}>{" "}{detailText}</Text>
@@ -405,14 +405,14 @@ function CodexThinkingBlock({
405
405
 
406
406
  return (
407
407
  <Box flexDirection="column" width="100%" paddingLeft={transcriptContentIndent} paddingRight={1}>
408
- <Text color={theme.MUTED} bold>Codexa</Text>
408
+ <Text color={theme.textMuted} bold>Codexa</Text>
409
409
  {formatProgressBlockBodyLines(block.text, contentWidth)
410
410
  .slice(0, verboseMode ? undefined : COMPACT_PROCESSING_BODY_LINE_CAP)
411
411
  .map((line, i) => (
412
- <Text key={i} color={theme.DIM}>{line || " "}</Text>
412
+ <Text key={i} color={theme.textDim}>{line || " "}</Text>
413
413
  ))}
414
414
  {isLiveCursorTarget && block.status === "active" && (
415
- <Text color={theme.ACCENT}>▌</Text>
415
+ <Text color={theme.accent}>▌</Text>
416
416
  )}
417
417
  </Box>
418
418
  );
@@ -453,11 +453,11 @@ function CodexResponseBlock({
453
453
 
454
454
  return (
455
455
  <Box flexDirection="column" width="100%" paddingLeft={transcriptContentIndent} paddingRight={1}>
456
- <Text color={theme.MUTED} bold>Codexa</Text>
456
+ <Text color={theme.textMuted} bold>Codexa</Text>
457
457
  {run.status === "failed" && !streaming && isLast && (
458
458
  <Box flexDirection="column">
459
459
  {wrapPlainText(sanitizeTerminalOutput(run.errorMessage ?? run.summary), contentWidth).map((row, i) => (
460
- <Text key={i} color={theme.ERROR}>{i === 0 ? `✕ ${row}` : ` ${row}`}</Text>
460
+ <Text key={i} color={theme.error}>{i === 0 ? `✕ ${row}` : ` ${row}`}</Text>
461
461
  ))}
462
462
  </Box>
463
463
  )}
@@ -466,7 +466,7 @@ function CodexResponseBlock({
466
466
  width={contentWidth}
467
467
  />
468
468
  {isLiveCursorTarget && segmentStreaming && (
469
- <Text color={theme.ACCENT}>▌</Text>
469
+ <Text color={theme.accent}>▌</Text>
470
470
  )}
471
471
  </Box>
472
472
  );
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
4
+ import { clampVisualText } from "./layout.js";
5
+ import { useTheme } from "./theme.js";
6
+
7
+ export const UPDATE_CARD_CONTENT_ROWS = 4; // title + available + using + command
8
+ export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bottom border rows
9
+
10
+ export interface UpdateAvailableCardProps {
11
+ latestVersion: string;
12
+ currentVersion: string;
13
+ /** Total box width including borders. Long lines are truncated to fit. */
14
+ width?: number;
15
+ }
16
+
17
+ export function UpdateAvailableCard({ latestVersion, currentVersion, width }: UpdateAvailableCardProps) {
18
+ const theme = useTheme();
19
+ const command = `Run: ${CODEXA_UPDATE_COMMAND}`;
20
+ // Inner content width = boxWidth - 2 (left/right border cols)
21
+ const innerWidth = width !== undefined ? Math.max(8, width - 2) : undefined;
22
+
23
+ function clamp(text: string): string {
24
+ return innerWidth !== undefined ? clampVisualText(text, innerWidth) : text;
25
+ }
26
+
27
+ return (
28
+ <Box
29
+ borderStyle="round"
30
+ borderColor={theme.accent}
31
+ flexDirection="column"
32
+ width={width}
33
+ flexShrink={0}
34
+ >
35
+ <Text color={theme.text} bold>{clamp("Update available")}</Text>
36
+ <Text color={theme.textMuted}>{clamp(`Codexa ${formatVersionLabel(latestVersion)}`)}</Text>
37
+ <Text color={theme.textMuted}>{clamp(`Using ${formatVersionLabel(currentVersion)}`)}</Text>
38
+ <Text color={theme.textDim}>{clamp(command)}</Text>
39
+ </Box>
40
+ );
41
+ }