@golba98/codexa 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +391 -94
  2. package/package.json +12 -7
  3. package/src/app.tsx +149 -117
  4. package/src/commands/handler.ts +4 -4
  5. package/src/config/buildInfo.ts +2 -2
  6. package/src/config/persistence.ts +10 -0
  7. package/src/config/settings.ts +7 -15
  8. package/src/config/updateCheckCache.ts +19 -1
  9. package/src/core/channel.ts +23 -0
  10. package/src/core/executables/codexExecutable.ts +1 -0
  11. package/src/core/executables/executableResolver.ts +61 -43
  12. package/src/core/launchContext.ts +33 -33
  13. package/src/core/process/processValidation.ts +9 -5
  14. package/src/core/providerLauncher/launcher.ts +59 -42
  15. package/src/core/updateCheck.ts +111 -95
  16. package/src/session/types.ts +2 -1
  17. package/src/ui/ActionRequiredBlock.tsx +5 -5
  18. package/src/ui/ActivityBars.tsx +3 -3
  19. package/src/ui/ActivityIndicator.tsx +6 -6
  20. package/src/ui/AgentBlock.tsx +6 -6
  21. package/src/ui/AnimatedStatusText.tsx +1 -1
  22. package/src/ui/AppShell.tsx +9 -24
  23. package/src/ui/AttachmentImportPanel.tsx +8 -8
  24. package/src/ui/AuthPanel.tsx +20 -20
  25. package/src/ui/BottomComposer.tsx +117 -78
  26. package/src/ui/DashCard.tsx +3 -3
  27. package/src/ui/Markdown.tsx +17 -17
  28. package/src/ui/ModelPickerScreen.tsx +10 -10
  29. package/src/ui/ModelReasoningPicker.tsx +14 -14
  30. package/src/ui/Panel.tsx +3 -3
  31. package/src/ui/PlanActionPicker.tsx +6 -6
  32. package/src/ui/PlanReviewPanel.tsx +8 -8
  33. package/src/ui/ProviderPicker.tsx +18 -18
  34. package/src/ui/RunFooter.tsx +3 -3
  35. package/src/ui/SelectionPanel.tsx +4 -4
  36. package/src/ui/SettingsPanel.tsx +8 -8
  37. package/src/ui/Spinner.tsx +1 -1
  38. package/src/ui/TextEntryPanel.tsx +10 -10
  39. package/src/ui/ThinkingBlock.tsx +8 -8
  40. package/src/ui/Timeline.tsx +21 -17
  41. package/src/ui/TopHeader.tsx +421 -293
  42. package/src/ui/TurnGroup.tsx +31 -31
  43. package/src/ui/UpdateAvailableCard.tsx +41 -0
  44. package/src/ui/UpdatePromptPanel.tsx +203 -0
  45. package/src/ui/focus.ts +3 -0
  46. package/src/ui/layout.ts +1 -1
  47. package/src/ui/logoVariants.ts +107 -0
  48. package/src/ui/modeDisplay.ts +12 -12
  49. package/src/ui/runtimeDisplay.ts +100 -0
  50. package/src/ui/theme.tsx +274 -395
  51. package/src/ui/timelineMeasure.ts +29 -19
  52. package/scripts/audit-codexa-capabilities.mjs +0 -466
  53. package/scripts/gen-build-info.mjs +0 -33
  54. package/scripts/smoke-terminal-bench.mjs +0 -35
  55. package/src/appRenderStability.test.ts +0 -131
  56. package/src/commands/handler.test.ts +0 -655
  57. package/src/config/launchArgs.test.ts +0 -189
  58. package/src/config/layeredConfig.test.ts +0 -143
  59. package/src/config/persistence.test.ts +0 -114
  60. package/src/config/runtimeConfig.test.ts +0 -218
  61. package/src/config/settings.test.ts +0 -155
  62. package/src/config/trustStore.test.ts +0 -29
  63. package/src/core/attachments.test.ts +0 -155
  64. package/src/core/auth/codexAuth.test.ts +0 -68
  65. package/src/core/cleanupFastFail.test.ts +0 -76
  66. package/src/core/codexExecArgs.test.ts +0 -195
  67. package/src/core/codexLaunch.test.ts +0 -205
  68. package/src/core/codexPrompt.test.ts +0 -252
  69. package/src/core/executables/codexExecutable.test.ts +0 -212
  70. package/src/core/executables/executableResolver.test.ts +0 -129
  71. package/src/core/executables/geminiExecutable.test.ts +0 -116
  72. package/src/core/executables/pathSanityScan.test.ts +0 -47
  73. package/src/core/githubDiagnostics.test.ts +0 -92
  74. package/src/core/hollowResponseFormat.test.ts +0 -58
  75. package/src/core/launchContext.test.ts +0 -157
  76. package/src/core/models/codexCapabilities.test.ts +0 -45
  77. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  78. package/src/core/models/modelSpecs.test.ts +0 -283
  79. package/src/core/perf/renderDebug.test.ts +0 -230
  80. package/src/core/planStorage.test.ts +0 -143
  81. package/src/core/process/CommandRunner.test.ts +0 -105
  82. package/src/core/projectInstructions.test.ts +0 -50
  83. package/src/core/providerLauncher/launcher.test.ts +0 -238
  84. package/src/core/providerLauncher/registry.test.ts +0 -324
  85. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  86. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  87. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  88. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  89. package/src/core/providerRuntime/gemini.test.ts +0 -437
  90. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  91. package/src/core/providerRuntime/local.test.ts +0 -787
  92. package/src/core/providerRuntime/registry.test.ts +0 -233
  93. package/src/core/providers/codexJsonStream.test.ts +0 -148
  94. package/src/core/providers/codexSubprocess.test.ts +0 -68
  95. package/src/core/providers/codexTranscript.test.ts +0 -284
  96. package/src/core/terminal/startupClear.test.ts +0 -55
  97. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  98. package/src/core/terminal/terminalControl.test.ts +0 -75
  99. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  100. package/src/core/terminal/terminalSelection.test.ts +0 -42
  101. package/src/core/terminal/terminalTitle.test.ts +0 -328
  102. package/src/core/updateCheck.test.ts +0 -194
  103. package/src/core/workspaceActivity.test.ts +0 -163
  104. package/src/core/workspaceGuard.test.ts +0 -151
  105. package/src/core/workspaceRoot.test.ts +0 -23
  106. package/src/exec.test.ts +0 -13
  107. package/src/headless/execArgs.test.ts +0 -147
  108. package/src/headless/execRunner.test.ts +0 -436
  109. package/src/index.test.tsx +0 -620
  110. package/src/session/appSession.test.ts +0 -897
  111. package/src/session/chatLifecycle.test.ts +0 -64
  112. package/src/session/liveRenderScheduler.test.ts +0 -201
  113. package/src/session/planFlow.test.ts +0 -103
  114. package/src/session/planTranscript.test.ts +0 -65
  115. package/src/session/promptRunSchedule.test.ts +0 -36
  116. package/src/ui/ActivityIndicator.test.tsx +0 -58
  117. package/src/ui/AgentBlock.test.ts +0 -6
  118. package/src/ui/AnimatedStatusText.test.ts +0 -16
  119. package/src/ui/AppShell.test.tsx +0 -1776
  120. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  121. package/src/ui/BottomComposer.test.ts +0 -674
  122. package/src/ui/CodexLogo.tsx +0 -55
  123. package/src/ui/Markdown.test.ts +0 -157
  124. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  125. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  126. package/src/ui/ModelPickerState.test.tsx +0 -151
  127. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  128. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  129. package/src/ui/PromptCardBorder.test.tsx +0 -161
  130. package/src/ui/ProviderPicker.test.tsx +0 -289
  131. package/src/ui/ProviderShortcut.test.tsx +0 -143
  132. package/src/ui/SettingsPanel.test.tsx +0 -233
  133. package/src/ui/Timeline.test.ts +0 -2067
  134. package/src/ui/TimelineNavigation.test.tsx +0 -201
  135. package/src/ui/TopHeader.test.tsx +0 -254
  136. package/src/ui/TurnGroup.test.tsx +0 -365
  137. package/src/ui/busyStatusAnimation.test.ts +0 -30
  138. package/src/ui/commandNormalize.test.ts +0 -142
  139. package/src/ui/diffRenderer.test.ts +0 -102
  140. package/src/ui/focusFlow.test.tsx +0 -1098
  141. package/src/ui/inputBuffer.test.ts +0 -151
  142. package/src/ui/layout.test.ts +0 -146
  143. package/src/ui/modeDisplay.test.ts +0 -42
  144. package/src/ui/runActivityView.test.ts +0 -89
  145. package/src/ui/runLifecycleView.test.tsx +0 -237
  146. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  147. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  148. package/src/ui/textLayout.test.ts +0 -18
  149. package/src/ui/themeFlow.test.ts +0 -53
  150. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -1,117 +1,96 @@
1
1
  import { APP_VERSION } from "../config/settings.js";
2
+ import { isLocalDevChannel } from "./channel.js";
3
+
4
+ export const CODEXA_NPM_PACKAGE = "@golba98/codexa";
5
+ export const CODEXA_NPM_REGISTRY_URL = "https://registry.npmjs.org/@golba98%2Fcodexa";
6
+ export const CODEXA_UPDATE_COMMAND = `npm install -g ${CODEXA_NPM_PACKAGE}@latest`;
2
7
 
3
8
  export type UpdateStatus = "up-to-date" | "update-available" | "unknown" | "error";
4
9
 
10
+ export interface NpmRegistryMetadata {
11
+ "dist-tags": { latest?: string };
12
+ }
13
+
5
14
  export interface UpdateCheckResult {
6
15
  status: UpdateStatus;
7
16
  currentVersion: string;
8
17
  latestVersion: string | null;
9
18
  errorMessage?: string;
10
19
  checkedAt: number;
20
+ source?: "npm" | "cache";
11
21
  }
12
22
 
13
- export interface NpmRegistryMetadata {
14
- "dist-tags"?: {
15
- latest?: unknown;
16
- };
17
- }
23
+ const FETCH_TIMEOUT_MS = 5000;
18
24
 
19
- export interface UpdateCheckOverrides {
20
- fetchNpmMetadataFn?: (url: string, init: RequestInit) => Promise<NpmRegistryMetadata>;
21
- currentVersion?: string;
22
- now?: () => number;
25
+ /** Strip a leading "v" so "v1.0.2" and "1.0.2" are treated as equal. */
26
+ export function normalizeVersion(v: string): string {
27
+ return v.startsWith("v") ? v.slice(1) : v;
23
28
  }
24
29
 
25
- export const CODEXA_NPM_PACKAGE = "@golba98/codexa";
26
- export const CODEXA_NPM_REGISTRY_URL = "https://registry.npmjs.org/@golba98%2Fcodexa";
27
- export const CODEXA_UPDATE_COMMAND = "npm install -g @golba98/codexa@latest";
30
+ const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
28
31
 
29
- const REMOTE_TIMEOUT_MS = 3000;
30
-
31
- interface ParsedSemver {
32
- major: number;
33
- minor: number;
34
- patch: number;
35
- prerelease: string[];
32
+ /** Returns true for valid semver strings with or without a leading "v". */
33
+ export function isValidSemver(v: string): boolean {
34
+ return SEMVER_RE.test(normalizeVersion(v));
36
35
  }
37
36
 
38
- function parseSemver(version: string): ParsedSemver | null {
39
- const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
40
- if (!match) return null;
41
-
42
- return {
43
- major: Number(match[1]),
44
- minor: Number(match[2]),
45
- patch: Number(match[3]),
46
- prerelease: match[4]?.split(".") ?? [],
47
- };
37
+ export function shouldRunStartupUpdateCheck(
38
+ env: NodeJS.ProcessEnv = process.env,
39
+ enabled = true,
40
+ ): boolean {
41
+ return enabled && !isLocalDevChannel(env);
48
42
  }
49
43
 
50
- function comparePrerelease(left: string[], right: string[]): number {
51
- if (left.length === 0 && right.length === 0) return 0;
52
- if (left.length === 0) return 1;
53
- if (right.length === 0) return -1;
54
-
55
- const max = Math.max(left.length, right.length);
56
- for (let index = 0; index < max; index += 1) {
57
- const a = left[index];
58
- const b = right[index];
59
- if (a === undefined) return -1;
60
- if (b === undefined) return 1;
61
- if (a === b) continue;
62
-
63
- const aNumeric = /^\d+$/.test(a);
64
- const bNumeric = /^\d+$/.test(b);
65
- if (aNumeric && bNumeric) return Number(a) > Number(b) ? 1 : -1;
66
- if (aNumeric) return -1;
67
- if (bNumeric) return 1;
68
- return a > b ? 1 : -1;
69
- }
44
+ // Compares two semver strings numerically. Returns negative if a < b, 0 if equal, positive if a > b.
45
+ // Pre-release versions (e.g. 1.0.2-beta.1) sort below their release counterpart (1.0.2 > 1.0.2-beta.1).
46
+ // Leading "v" is stripped before comparison.
47
+ export function compareSemver(a: string, b: string): number {
48
+ const parseParts = (v: string): { numeric: number[]; prerelease: string | null } => {
49
+ const norm = normalizeVersion(v);
50
+ const dashIdx = norm.indexOf("-");
51
+ const base = dashIdx === -1 ? norm : norm.slice(0, dashIdx);
52
+ const prerelease = dashIdx === -1 ? null : norm.slice(dashIdx + 1);
53
+ const numeric = base.split(".").map((p) => parseInt(p, 10) || 0);
54
+ return { numeric, prerelease };
55
+ };
70
56
 
71
- return 0;
72
- }
57
+ const pa = parseParts(a);
58
+ const pb = parseParts(b);
59
+ const len = Math.max(pa.numeric.length, pb.numeric.length);
73
60
 
74
- export function compareSemver(leftVersion: string, rightVersion: string): number {
75
- const left = parseSemver(leftVersion);
76
- const right = parseSemver(rightVersion);
77
- if (!left || !right) {
78
- throw new Error(`Invalid semver version: ${!left ? leftVersion : rightVersion}`);
61
+ for (let i = 0; i < len; i++) {
62
+ const diff = (pa.numeric[i] ?? 0) - (pb.numeric[i] ?? 0);
63
+ if (diff !== 0) return diff;
79
64
  }
80
65
 
81
- for (const key of ["major", "minor", "patch"] as const) {
82
- if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1;
66
+ // Same numeric version: no pre-release > has pre-release
67
+ if (pa.prerelease === null && pb.prerelease !== null) return 1;
68
+ if (pa.prerelease !== null && pb.prerelease === null) return -1;
69
+ if (pa.prerelease !== null && pb.prerelease !== null) {
70
+ return pa.prerelease < pb.prerelease ? -1 : pa.prerelease > pb.prerelease ? 1 : 0;
83
71
  }
84
-
85
- return comparePrerelease(left.prerelease, right.prerelease);
72
+ return 0;
86
73
  }
87
74
 
88
- export function isNewerVersion(latestVersion: string, currentVersion: string): boolean {
89
- return compareSemver(latestVersion, currentVersion) > 0;
75
+ export function isNewerVersion(candidate: string, current: string): boolean {
76
+ return compareSemver(candidate, current) > 0;
90
77
  }
91
78
 
92
- async function fetchNpmMetadata(url: string, init: RequestInit): Promise<NpmRegistryMetadata> {
93
- const res = await fetch(url, init);
94
- if (!res.ok) {
95
- throw new Error(`npm registry responded with HTTP ${res.status}`);
96
- }
97
- return await res.json() as NpmRegistryMetadata;
79
+ export interface UpdateCheckOverrides {
80
+ currentVersion?: string;
81
+ fetchNpmMetadataFn?: (url: string) => Promise<NpmRegistryMetadata>;
98
82
  }
99
83
 
100
- export async function getNpmLatestVersion(
101
- fetchMetadata: (url: string, init: RequestInit) => Promise<NpmRegistryMetadata> = fetchNpmMetadata,
102
- ): Promise<string> {
84
+ async function defaultFetchNpmMetadata(url: string): Promise<NpmRegistryMetadata> {
103
85
  const controller = new AbortController();
104
- const timer = setTimeout(() => controller.abort(), REMOTE_TIMEOUT_MS);
86
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
105
87
  try {
106
- const data = await fetchMetadata(CODEXA_NPM_REGISTRY_URL, {
88
+ const res = await fetch(url, {
107
89
  signal: controller.signal,
108
- headers: { "User-Agent": "Codexa-Update-Checker/1.0" },
90
+ headers: { "User-Agent": `${CODEXA_NPM_PACKAGE}-update-checker/1.0` },
109
91
  });
110
- const latest = data?.["dist-tags"]?.latest;
111
- if (typeof latest !== "string" || !latest.trim()) {
112
- throw new Error("npm registry response did not include dist-tags.latest");
113
- }
114
- return latest.trim();
92
+ if (!res.ok) throw new Error(`npm registry returned HTTP ${res.status}`);
93
+ return await res.json() as NpmRegistryMetadata;
115
94
  } finally {
116
95
  clearTimeout(timer);
117
96
  }
@@ -121,28 +100,51 @@ export async function checkForUpdates(
121
100
  opts?: { enabled?: boolean },
122
101
  overrides?: UpdateCheckOverrides,
123
102
  ): Promise<UpdateCheckResult> {
124
- const now = overrides?.now ?? Date.now;
125
- const currentVersion = overrides?.currentVersion ?? APP_VERSION;
103
+ const currentVersion = normalizeVersion(overrides?.currentVersion ?? APP_VERSION);
126
104
 
127
105
  if (opts?.enabled === false) {
128
- return { status: "unknown", currentVersion, latestVersion: null, checkedAt: now() };
106
+ return { status: "unknown", currentVersion, latestVersion: null, checkedAt: Date.now(), source: "npm" };
129
107
  }
130
108
 
131
109
  try {
132
- const latestVersion = await getNpmLatestVersion(overrides?.fetchNpmMetadataFn);
133
- return {
134
- status: isNewerVersion(latestVersion, currentVersion) ? "update-available" : "up-to-date",
135
- currentVersion,
136
- latestVersion,
137
- checkedAt: now(),
138
- };
110
+ const fetchFn = overrides?.fetchNpmMetadataFn ?? defaultFetchNpmMetadata;
111
+ const metadata = await fetchFn(CODEXA_NPM_REGISTRY_URL);
112
+ const rawLatest = metadata["dist-tags"]?.latest;
113
+
114
+ if (!rawLatest) {
115
+ return {
116
+ status: "error",
117
+ currentVersion,
118
+ latestVersion: null,
119
+ errorMessage: "npm registry response did not include dist-tags.latest",
120
+ checkedAt: Date.now(),
121
+ source: "npm",
122
+ };
123
+ }
124
+
125
+ const latestVersion = normalizeVersion(rawLatest);
126
+
127
+ if (!isValidSemver(latestVersion) || !isValidSemver(currentVersion)) {
128
+ return {
129
+ status: "unknown",
130
+ currentVersion,
131
+ latestVersion: rawLatest,
132
+ errorMessage: `Invalid semver — current: "${currentVersion}", latest: "${rawLatest}"`,
133
+ checkedAt: Date.now(),
134
+ source: "npm",
135
+ };
136
+ }
137
+
138
+ const status = isNewerVersion(latestVersion, currentVersion) ? "update-available" : "up-to-date";
139
+ return { status, currentVersion, latestVersion, checkedAt: Date.now(), source: "npm" };
139
140
  } catch (err) {
140
141
  return {
141
142
  status: "error",
142
143
  currentVersion,
143
144
  latestVersion: null,
144
145
  errorMessage: err instanceof Error ? err.message : String(err),
145
- checkedAt: now(),
146
+ checkedAt: Date.now(),
147
+ source: "npm",
146
148
  };
147
149
  }
148
150
  }
@@ -151,22 +153,36 @@ export function formatUpdateInstructions(result: UpdateCheckResult | null): stri
151
153
  const current = result?.currentVersion ?? APP_VERSION;
152
154
  const latest = result?.latestVersion ?? "unknown";
153
155
 
156
+ if (result?.status === "error") {
157
+ return [
158
+ `Current installed version: ${current}`,
159
+ `npm latest version: ${latest}`,
160
+ `Error checking npm update status: ${result.errorMessage ?? "unknown error"}`,
161
+ ].join("\n");
162
+ }
163
+
154
164
  let statusLine: string;
155
165
  if (result?.status === "update-available" && result.latestVersion) {
156
166
  statusLine = `Update available: Codexa ${result.latestVersion} is available. You are using ${current}.`;
157
167
  } else if (result?.status === "up-to-date") {
158
168
  statusLine = "Already up to date.";
159
- } else if (result?.status === "error") {
160
- statusLine = `Error checking npm update status: ${result.errorMessage || "unknown error"}`;
161
169
  } else {
162
- statusLine = "Status unknown - could not read npm latest version.";
170
+ statusLine = "Status unknown could not reach npm registry.";
163
171
  }
164
172
 
165
173
  return [
166
174
  `Current installed version: ${current}`,
167
- `npm latest version: ${latest}`,
168
- `Status: ${statusLine}`,
175
+ `npm latest version: ${latest}`,
176
+ `Status: ${statusLine}`,
169
177
  "",
170
178
  `Run: ${CODEXA_UPDATE_COMMAND}`,
171
179
  ].join("\n");
172
180
  }
181
+
182
+ export function formatLocalDevUpdateStatus(): string {
183
+ return [
184
+ "Running local-dev Codexa.",
185
+ "Automatic published npm update prompts are disabled for this channel.",
186
+ "Run /update check to explicitly check the published npm package.",
187
+ ].join("\n");
188
+ }
@@ -23,7 +23,8 @@ export type Screen =
23
23
  | "permissions-network-picker"
24
24
  | "permissions-add-writable-root"
25
25
  | "permissions-remove-writable-root"
26
- | "import-confirmation";
26
+ | "import-confirmation"
27
+ | "update-prompt";
27
28
 
28
29
  // ─── Provider readiness ───────────────────────────────────────────────────────
29
30
 
@@ -22,15 +22,15 @@ export function ActionRequiredBlock({ cols, turnIndex, question }: ActionRequire
22
22
  });
23
23
 
24
24
  return (
25
- <Box borderStyle="single" borderColor={theme.BORDER_ACTIVE} flexDirection="column" marginBottom={1} width="100%">
25
+ <Box borderStyle="single" borderColor={theme.borderFocused} flexDirection="column" marginBottom={1} width="100%">
26
26
  <Box width="100%" justifyContent="space-between" overflow="hidden" paddingX={1}>
27
- <Text color={theme.TEXT} bold>{`[${turnIndex}] ACTION REQUIRED`}</Text>
28
- <Text color={theme.TEXT} bold>{"⚡"}</Text>
27
+ <Text color={theme.text} bold>{`[${turnIndex}] ACTION REQUIRED`}</Text>
28
+ <Text color={theme.text} bold>{"⚡"}</Text>
29
29
  </Box>
30
30
  <Box flexDirection="column" paddingX={2} marginTop={1} marginBottom={1} width="100%">
31
- <Text color={theme.TEXT} bold>{"Verification Question"}</Text>
31
+ <Text color={theme.text} bold>{"Verification Question"}</Text>
32
32
  {wrappedContent.map((row, index) => (
33
- <Text key={`content-${index}`} color={theme.TEXT}>{row || " "}</Text>
33
+ <Text key={`content-${index}`} color={theme.text}>{row || " "}</Text>
34
34
  ))}
35
35
  </Box>
36
36
  </Box>
@@ -18,7 +18,7 @@ interface WaveBarProps {
18
18
 
19
19
  export function WaveBar({ tick, color }: WaveBarProps) {
20
20
  const theme = useTheme();
21
- const activeColor = color ?? theme.ACCENT;
21
+ const activeColor = color ?? theme.accent;
22
22
 
23
23
  let str = "";
24
24
  for (let c = 0; c < WAVE_COLS; c++) {
@@ -48,8 +48,8 @@ interface PulseBarProps {
48
48
 
49
49
  export function PulseBar({ tick, color, dimColor }: PulseBarProps) {
50
50
  const theme = useTheme();
51
- const activeColor = color ?? theme.ACCENT;
52
- const inactiveColor = dimColor ?? theme.DIM;
51
+ const activeColor = color ?? theme.accent;
52
+ const inactiveColor = dimColor ?? theme.textDim;
53
53
 
54
54
  const raw = Math.floor(tick / PULSE_SPEED) % PULSE_PERIOD;
55
55
  const pos = raw < PULSE_MAX_POS ? raw : PULSE_PERIOD - raw;
@@ -32,26 +32,26 @@ export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: Activ
32
32
  }, [isAnimated]);
33
33
 
34
34
  let glyph = "?";
35
- let color = theme.DIM;
35
+ let color = theme.textDim;
36
36
  let bold = false;
37
37
 
38
38
  if (isError) {
39
39
  glyph = "�";
40
- color = theme.ERROR;
40
+ color = theme.error;
41
41
  bold = true;
42
42
  } else if (isAction) {
43
43
  glyph = "?";
44
- color = theme.WARNING;
44
+ color = theme.warning;
45
45
  bold = true;
46
46
  } else if (isStarting || isThinking) {
47
47
  glyph = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]!;
48
- color = theme.TEXT;
48
+ color = theme.text;
49
49
  } else if (isStreaming) {
50
50
  glyph = STREAMING_FRAMES[frameIndex % STREAMING_FRAMES.length]!;
51
- color = theme.SUCCESS;
51
+ color = theme.success;
52
52
  } else {
53
53
  glyph = "?";
54
- color = theme.DIM;
54
+ color = theme.textDim;
55
55
  }
56
56
 
57
57
  return <Text color={color} bold={bold}>{glyph}</Text>;
@@ -49,7 +49,7 @@ const StreamingCursor = memo(function StreamingCursor() {
49
49
  const theme = useTheme();
50
50
  return (
51
51
  <Box width="100%" paddingLeft={2}>
52
- <Text color={theme.ACCENT}>{"▌"}</Text>
52
+ <Text color={theme.accent}>{"▌"}</Text>
53
53
  </Box>
54
54
  );
55
55
  });
@@ -93,14 +93,14 @@ export function AgentBlock({
93
93
  : runStatus;
94
94
  const heading = run?.runtime.model ? run.runtime.model.toUpperCase().replace(/-/g, " ") : "Codex";
95
95
 
96
- const borderColor = dim ? theme.BORDER_SUBTLE : (runPhase === "streaming" ? theme.BORDER_ACTIVE : theme.BORDER_SUBTLE);
96
+ const borderColor = dim ? theme.border : (runPhase === "streaming" ? theme.borderFocused : theme.border);
97
97
 
98
98
  return (
99
99
  <DashCard cols={cols} title={heading} rightBadge={rightBadge} borderColor={borderColor}>
100
100
  {!streaming && failureMessage && (
101
101
  <Box flexDirection="column" width="100%">
102
102
  {wrapPlainText(failureMessage, contentWidth).map((row, index) => (
103
- <Text key={index} color={theme.ERROR}>{index === 0 ? `✕ ${row || " "}` : row || " "}</Text>
103
+ <Text key={index} color={theme.error}>{index === 0 ? `✕ ${row || " "}` : row || " "}</Text>
104
104
  ))}
105
105
  </Box>
106
106
  )}
@@ -116,12 +116,12 @@ export function AgentBlock({
116
116
  {!streaming && run && run.status !== "running" && (
117
117
  <Box flexDirection="column" width="100%">
118
118
  {run.status === "canceled" && cancelMessage ? (
119
- <Text color={theme.WARNING}>{cancelMessage}</Text>
119
+ <Text color={theme.warning}>{cancelMessage}</Text>
120
120
  ) : run.status === "completed" && pipelineState.length === 0 ? (
121
- <Text color={theme.DIM}>{"(no output)"}</Text>
121
+ <Text color={theme.textDim}>{"(no output)"}</Text>
122
122
  ) : null}
123
123
  {run.truncatedOutput && (
124
- <Text color={theme.DIM}>{RUN_OUTPUT_TRUNCATION_NOTICE}</Text>
124
+ <Text color={theme.textDim}>{RUN_OUTPUT_TRUNCATION_NOTICE}</Text>
125
125
  )}
126
126
  </Box>
127
127
  )}
@@ -62,7 +62,7 @@ export function AnimatedStatusText({ baseText, isActive, isError = false, animat
62
62
  const suffix = isActive ? animationFrame ?? localFrame : "";
63
63
 
64
64
  return (
65
- <Text color={isError ? theme.ERROR : theme.INFO} wrap="truncate">
65
+ <Text color={isError ? theme.error : theme.info} wrap="truncate">
66
66
  {renderedText}{suffix}
67
67
  </Text>
68
68
  );
@@ -1,7 +1,6 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useStdin } from "ink";
3
3
  import { useTheme } from "./theme.js";
4
- import type { UpdateCheckResult } from "../core/updateCheck.js";
5
4
  import type { RuntimeSummary } from "../config/runtimeConfig.js";
6
5
  import type { CodexAuthState } from "../core/auth/codexAuth.js";
7
6
  import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
@@ -25,7 +24,7 @@ import {
25
24
  } from "./Timeline.js";
26
25
  import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
27
26
  import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
28
- import { MemoizedTopHeader, measureTopHeaderRows } from "./TopHeader.js";
27
+ import { MemoizedTopHeader, measureTopHeaderRows, type UpdateAvailableInfo } from "./TopHeader.js";
29
28
 
30
29
  const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 1;
31
30
  const MEDIUM_HEADER_TO_COMPOSER_GAP_ROWS = 1;
@@ -59,7 +58,7 @@ export interface AppShellProps {
59
58
  selectionProfile?: TerminalSelectionProfile;
60
59
  clearCount?: number;
61
60
  headerConfig?: HeaderConfig;
62
- updateCheckResult?: UpdateCheckResult | null;
61
+ updateAvailable?: UpdateAvailableInfo | null;
63
62
  }
64
63
 
65
64
  // ─── Helpers & subcomponents ─────────────────────────────────────────────────
@@ -164,7 +163,7 @@ function AppShellInner({
164
163
  selectionProfile,
165
164
  clearCount = 0,
166
165
  headerConfig = HEADER_CONFIG_DEFAULTS,
167
- updateCheckResult = null,
166
+ updateAvailable = null,
168
167
  }: AppShellProps) {
169
168
  const theme = useTheme();
170
169
  renderDebug.useRenderDebug("AppShell", {
@@ -193,7 +192,7 @@ function AppShellInner({
193
192
 
194
193
  const shellWidth = getShellWidth(layout.cols);
195
194
  const shellHeight = getShellHeight(layout.rows);
196
- const headerRows = measureTopHeaderRows(layout, headerConfig);
195
+ const headerRows = measureTopHeaderRows(layout, headerConfig, !!updateAvailable);
197
196
  const headerToContentGapRows = calculateHeaderToContentGapRows(layout);
198
197
  const showComposer = screen === "main";
199
198
  const showMainPanel = screen === "main" && mainPanel !== undefined && mainPanel !== null;
@@ -505,17 +504,6 @@ function AppShellInner({
505
504
  ? React.cloneElement(composer as React.ReactElement<{ selectionProfile?: TerminalSelectionProfile }>, { selectionProfile })
506
505
  : composer;
507
506
 
508
- const updateNotice = updateCheckResult?.status === "update-available" ? (
509
- <Box paddingX={1} flexDirection="column">
510
- <Text color={theme.WARNING}>
511
- {`Update available: Codexa ${updateCheckResult.latestVersion ?? "unknown"} is available. You are using ${updateCheckResult.currentVersion}.`}
512
- </Text>
513
- <Text color={theme.WARNING}>
514
- {"Run: npm install -g @golba98/codexa@latest"}
515
- </Text>
516
- </Box>
517
- ) : null;
518
-
519
507
  if (showMainPanelFullOutput) {
520
508
  return (
521
509
  <Box flexDirection="column" width="100%">
@@ -526,10 +514,9 @@ function AppShellInner({
526
514
  layout={layout}
527
515
  runtimeSummary={runtimeSummary}
528
516
  headerConfig={headerConfig}
517
+ updateAvailable={updateAvailable}
529
518
  />
530
519
 
531
- {updateNotice}
532
-
533
520
  {headerToContentGapRows > 0 && (
534
521
  <Box height={headerToContentGapRows} />
535
522
  )}
@@ -558,10 +545,9 @@ function AppShellInner({
558
545
  layout={layout}
559
546
  runtimeSummary={runtimeSummary}
560
547
  headerConfig={headerConfig}
548
+ updateAvailable={updateAvailable}
561
549
  />
562
550
 
563
- {updateNotice}
564
-
565
551
  {headerToContentGapRows > 0 && (
566
552
  <Box height={headerToContentGapRows} />
567
553
  )}
@@ -614,10 +600,9 @@ function AppShellInner({
614
600
  layout={layout}
615
601
  runtimeSummary={runtimeSummary}
616
602
  headerConfig={headerConfig}
603
+ updateAvailable={updateAvailable}
617
604
  />
618
605
 
619
- {updateNotice}
620
-
621
606
  {headerToContentGapRows > 0 && (
622
607
  <Box height={headerToContentGapRows} />
623
608
  )}
@@ -714,8 +699,8 @@ export const AppShell = memo(AppShellInner, (prev, next) => {
714
699
  prev.verboseMode === next.verboseMode &&
715
700
  prev.mouseCapture === next.mouseCapture &&
716
701
  prev.onMouseActivity === next.onMouseActivity &&
717
- prev.clearCount === next.clearCount &&
718
- prev.updateCheckResult === next.updateCheckResult &&
702
+ prev.clearCount === next.clearCount &&
703
+ prev.updateAvailable === next.updateAvailable &&
719
704
  panelPropsEqual
720
705
  );
721
706
  });
@@ -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.borderFocused}
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}>Enter copy and continue Esc cancel</Text>
94
94
  </Box>
95
95
  </Box>
96
96
  </Box>