@golba98/codexa 1.0.3 → 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 (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -1,22 +1,22 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
- import { formatCodexaBrandLabel } from "../core/channel.js";
4
+ import { formatCodexaBrandLabel } from "../core/version/channel.js";
5
+ import { formatVersionLabel } from "../core/version/updateCheck.js";
5
6
  import type { RuntimeSummary } from "../config/runtimeConfig.js";
6
7
  import type { CodexAuthState } from "../core/auth/codexAuth.js";
7
8
  import { getAuthStateLabel } from "../core/auth/codexAuth.js";
8
9
  import * as renderDebug from "../core/perf/renderDebug.js";
9
10
  import { useTheme } from "./theme.js";
10
- import { clampVisualText, type Layout } from "./layout.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "./layout.js";
11
12
  import { getTextWidth } from "./textLayout.js";
12
13
  import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
13
14
  import {
14
15
  LOGO_LARGE,
16
+ LOGO_COMPACT,
15
17
  LOGO_COMPACT_MIN_COLS,
16
18
  LOGO_LARGE_MIN_COLS,
17
19
  LOGO_LARGE_MIN_ROWS,
18
- LOGO_MEDIUM_MIN_COLS,
19
- selectLogoVariantForViewport,
20
20
  getLogoWidth,
21
21
  } from "./logoVariants.js";
22
22
 
@@ -27,9 +27,8 @@ const HEADER_PADDING_COLUMNS = 2;
27
27
  const SHELL_GUTTER_COLUMNS = 1;
28
28
  // Require 130+ cols for wide side-by-side so the UpdateAvailableCard has room.
29
29
  const WIDE_HEADER_MIN_COLUMNS = 130;
30
- // Require 72+ cols (= LOGO_MEDIUM_MIN_COLS) for medium side-by-side both
31
- // LOGO_LARGE (≥100) and LOGO_MEDIUM (72–99) fit cleanly in side-by-side layout.
32
- const MEDIUM_HEADER_MIN_COLUMNS = LOGO_MEDIUM_MIN_COLS; // 72
30
+ // Require 72+ cols for medium side-by-side canonical-logo layout.
31
+ const MEDIUM_HEADER_MIN_COLUMNS = LOGO_LARGE_MIN_COLS;
33
32
  const MIN_SIDE_BY_SIDE_METADATA_WIDTH = 18;
34
33
  const STACKED_METADATA_GAP_ROWS = 1;
35
34
  // Gap row between the UpdateAvailableCard and the metadata lines.
@@ -84,7 +83,7 @@ function getMetadataRowCount(headerConfig: HeaderConfig): number {
84
83
  }
85
84
 
86
85
  function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
87
- if (layout.mode !== "full") {
86
+ if (!isDecorativeLayoutMode(layout.mode)) {
88
87
  return {
89
88
  topMarginRows: 0,
90
89
  bottomMarginRows: 0,
@@ -101,6 +100,19 @@ function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bott
101
100
  };
102
101
  }
103
102
 
103
+ export function selectHeaderLogo(layout: Layout): readonly string[] {
104
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return [];
105
+ const showNormalLogo =
106
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
107
+ layout.mode === "regular" ||
108
+ layout.mode === "expanded" ||
109
+ (layout.mode === "compact" && layout.cols >= 72)
110
+ );
111
+ if (!showNormalLogo) return [];
112
+ if (process.env["CODEXA_COMPACT_LOGO"] === "1") return LOGO_COMPACT;
113
+ return LOGO_LARGE;
114
+ }
115
+
104
116
  export function getHeaderHeroLayout(
105
117
  layout: Layout,
106
118
  headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
@@ -110,17 +122,29 @@ export function getHeaderHeroLayout(
110
122
  const metadataRows = getMetadataRowCount(headerConfig);
111
123
  const contentWidth = getHeaderContentWidth(layout.cols);
112
124
 
113
- // Pick the largest logo that fits both the columns AND the rows. This degrades
114
- // large medium → compact on a short terminal instead of collapsing straight
115
- // to the flat text-only header.
116
- const logo = selectLogoVariantForViewport(layout.cols, layout.rows);
125
+ const showNormalLogo =
126
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
127
+ layout.mode === "regular" ||
128
+ layout.mode === "expanded" ||
129
+ (layout.mode === "compact" && layout.cols >= 72)
130
+ );
131
+
132
+ const placeMetadataBesideLogo = showNormalLogo && layout.cols >= 95;
133
+
134
+ let mode: HeaderHeroMode;
135
+ if (!showNormalLogo) {
136
+ mode = "compact";
137
+ } else if (placeMetadataBesideLogo) {
138
+ mode = layout.cols >= 130 ? "wide" : "medium";
139
+ } else {
140
+ mode = "narrow";
141
+ }
142
+
143
+ const logo = selectHeaderLogo(layout);
117
144
  const logoWidth = logo.length > 0 ? getLogoWidth(logo) : 0;
118
- const canRenderLogo = logo.length > 0;
119
145
 
120
- if (!canRenderLogo) {
121
- // Compact / text-only header. Show a recommended-size hint when the logo was
122
- // dropped purely for size (not because the user disabled ASCII art).
123
- const sizeHintRows = process.env["CODEXA_NO_ASCII_LOGO"] === "1" ? 0 : 1;
146
+ if (mode === "compact") {
147
+ // Compact / micro text-only header.
124
148
  return {
125
149
  mode: "compact",
126
150
  topMarginRows,
@@ -129,21 +153,13 @@ export function getHeaderHeroLayout(
129
153
  metadataGapRows: 0,
130
154
  logoRows: 1,
131
155
  metadataRows: 0,
132
- compactHintRows: sizeHintRows,
133
- totalRows: topMarginRows + 1 + sizeHintRows + bottomMarginRows,
156
+ compactHintRows: 0,
157
+ totalRows: topMarginRows + 1 + bottomMarginRows,
134
158
  };
135
159
  }
136
160
 
137
161
  const logoRowCount = logo.length;
138
162
  const metadataGapColumns = layout.cols >= WIDE_HEADER_MIN_COLUMNS ? 4 : 2;
139
- const metadataColumnWidth = contentWidth - logoWidth - metadataGapColumns;
140
- const canUseSideBySide = metadataRows === 0
141
- || metadataColumnWidth >= MIN_SIDE_BY_SIDE_METADATA_WIDTH;
142
- const mode: HeaderHeroMode = canUseSideBySide && layout.cols >= WIDE_HEADER_MIN_COLUMNS
143
- ? "wide"
144
- : canUseSideBySide && layout.cols >= MEDIUM_HEADER_MIN_COLUMNS
145
- ? "medium"
146
- : "narrow";
147
163
  const metadataGapRows = mode === "narrow" && metadataRows > 0 ? STACKED_METADATA_GAP_ROWS : 0;
148
164
 
149
165
  const isSideBySide = mode === "wide" || mode === "medium";
@@ -244,7 +260,7 @@ export function TopHeader({
244
260
  : authLabelRaw;
245
261
 
246
262
  const heroLayout = getHeaderHeroLayout(layout, headerConfig, !!updateAvailable);
247
- const selectedLogo = selectLogoVariantForViewport(layout.cols, layout.rows);
263
+ const selectedLogo = selectHeaderLogo(layout);
248
264
  const selectedLogoWidth = selectedLogo.length > 0 ? getLogoWidth(selectedLogo) : 0;
249
265
 
250
266
  const contentWidth = getHeaderContentWidth(layout.cols);
@@ -343,7 +359,7 @@ export function TopHeader({
343
359
  )}
344
360
  {metadataColumn}
345
361
  {updateAvailable && (
346
- <Text color={theme.warning} wrap="truncate">{`↑ Update: Codexa ${updateAvailable.latestVersion} is available run: npm install -g @golba98/codexa@latest`}</Text>
362
+ <Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: npm install -g @golba98/codexa@latest`}</Text>
347
363
  )}
348
364
  </Box>
349
365
  )}
@@ -418,4 +434,4 @@ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
418
434
  export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
419
435
 
420
436
  // Re-export for consumers that reference these constants directly.
421
- export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS };
437
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "./logoVariants.js";
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { CODEXA_UPDATE_COMMAND } from "../core/updateCheck.js";
3
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
4
4
  import { clampVisualText } from "./layout.js";
5
5
  import { useTheme } from "./theme.js";
6
6
 
@@ -33,8 +33,8 @@ export function UpdateAvailableCard({ latestVersion, currentVersion, width }: Up
33
33
  flexShrink={0}
34
34
  >
35
35
  <Text color={theme.text} bold>{clamp("Update available")}</Text>
36
- <Text color={theme.textMuted}>{clamp(`Codexa ${latestVersion} is available`)}</Text>
37
- <Text color={theme.textMuted}>{clamp(`You are using ${currentVersion}`)}</Text>
36
+ <Text color={theme.textMuted}>{clamp(`Codexa ${formatVersionLabel(latestVersion)}`)}</Text>
37
+ <Text color={theme.textMuted}>{clamp(`Using ${formatVersionLabel(currentVersion)}`)}</Text>
38
38
  <Text color={theme.textDim}>{clamp(command)}</Text>
39
39
  </Box>
40
40
  );
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
3
  import { spawn } from "child_process";
4
4
  import { useTheme } from "./theme.js";
5
- import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND } from "../core/updateCheck.js";
5
+ import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
6
6
 
7
7
  type Phase = "menu" | "running" | "done" | "error";
8
8
 
@@ -38,6 +38,10 @@ export function UpdatePromptPanel({
38
38
  const spawnStartedRef = useRef(false);
39
39
 
40
40
  useInput((input, key) => {
41
+ if (key.escape) {
42
+ onSkip();
43
+ return;
44
+ }
41
45
  if (phase === "menu") {
42
46
  if (key.upArrow || input === "k") {
43
47
  setSelectedIndex((i) => Math.max(0, i - 1));
@@ -57,12 +61,8 @@ export function UpdatePromptPanel({
57
61
  }
58
62
  return;
59
63
  }
60
- if (key.escape) {
61
- onSkip();
62
- return;
63
- }
64
64
  } else if (phase === "done" || phase === "error") {
65
- if (key.return || key.escape) {
65
+ if (key.return) {
66
66
  onSkip();
67
67
  }
68
68
  }
@@ -107,9 +107,9 @@ export function UpdatePromptPanel({
107
107
  };
108
108
  }, [phase]);
109
109
 
110
- const hintText = phase === "menu"
111
- ? "↑↓/jk select Enter confirm Esc skip"
112
- : "Press Enter to close";
110
+ const footerText = phase === "menu"
111
+ ? "Esc to close · Enter to confirm"
112
+ : "Esc to close";
113
113
 
114
114
  return (
115
115
  <Box flexDirection="column" width="100%" marginTop={1}>
@@ -122,11 +122,10 @@ export function UpdatePromptPanel({
122
122
  flexDirection="column"
123
123
  >
124
124
  <Box>
125
- <Text color={theme.accent} bold>Update available </Text>
126
- <Text color={theme.textMuted}>{hintText}</Text>
125
+ <Text color={theme.accent} bold>{`Update available: Codexa ${formatVersionLabel(latestVersion)}`}</Text>
127
126
  </Box>
128
127
  <Box marginTop={1}>
129
- <Text color={theme.text}>{`✨ ${currentVersion} -> ${latestVersion}`}</Text>
128
+ <Text color={theme.text}>{`${formatVersionLabel(currentVersion)} -> ${formatVersionLabel(latestVersion)}`}</Text>
130
129
  </Box>
131
130
  <Box>
132
131
  <Text color={theme.textMuted}>{`Package: ${CODEXA_NPM_PACKAGE}`}</Text>
@@ -138,7 +137,7 @@ export function UpdatePromptPanel({
138
137
 
139
138
  <Box
140
139
  borderStyle="round"
141
- borderColor={theme.borderFocused}
140
+ borderColor={phase === "menu" ? theme.borderFocused : theme.border}
142
141
  paddingX={2}
143
142
  paddingY={1}
144
143
  marginTop={1}
@@ -160,9 +159,6 @@ export function UpdatePromptPanel({
160
159
  </Text>
161
160
  </Box>
162
161
  ))}
163
- <Box marginTop={1}>
164
- <Text color={theme.textDim}>Press enter to continue</Text>
165
- </Box>
166
162
  </>
167
163
  )}
168
164
 
@@ -179,9 +175,6 @@ export function UpdatePromptPanel({
179
175
  <>
180
176
  <Text color={theme.success}>{"Codexa was updated successfully."}</Text>
181
177
  <Text color={theme.textMuted}>{"Restart Codexa to use the new version."}</Text>
182
- <Box marginTop={1}>
183
- <Text color={theme.textDim}>Press Enter to close</Text>
184
- </Box>
185
178
  </>
186
179
  )}
187
180
 
@@ -192,11 +185,12 @@ export function UpdatePromptPanel({
192
185
  {outputLines.slice(-5).map((line, i) => (
193
186
  <Text key={i} color={theme.textDim}>{line}</Text>
194
187
  ))}
195
- <Box marginTop={1}>
196
- <Text color={theme.textDim}>Press Enter to close</Text>
197
- </Box>
198
188
  </>
199
189
  )}
190
+
191
+ <Box marginTop={1}>
192
+ <Text color={theme.textDim}>{footerText}</Text>
193
+ </Box>
200
194
  </Box>
201
195
  </Box>
202
196
  );