@golba98/codexa 1.0.1 → 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 (234) hide show
  1. package/README.md +391 -94
  2. package/package.json +14 -8
  3. package/src/app.tsx +196 -63
  4. package/src/commands/handler.ts +9 -4
  5. package/src/config/buildInfo.ts +3 -0
  6. package/src/config/launchArgs.ts +10 -0
  7. package/src/config/layeredConfig.ts +0 -0
  8. package/src/config/persistence.ts +51 -0
  9. package/src/config/runtimeConfig.ts +4 -0
  10. package/src/config/settings.ts +6 -13
  11. package/src/config/toml-serialize.ts +0 -0
  12. package/src/config/trustStore.ts +0 -0
  13. package/src/config/updateCheckCache.ts +63 -0
  14. package/src/core/attachments.ts +0 -0
  15. package/src/core/auth/codexAuth.ts +0 -0
  16. package/src/core/channel.ts +23 -0
  17. package/src/core/cleanupFastFail.ts +0 -0
  18. package/src/core/clipboard.ts +0 -0
  19. package/src/core/codex.ts +0 -0
  20. package/src/core/codexExecArgs.ts +0 -0
  21. package/src/core/codexLaunch.ts +0 -0
  22. package/src/core/codexPrompt.ts +0 -0
  23. package/src/core/executables/claudeExecutable.ts +0 -0
  24. package/src/core/executables/codexExecutable.ts +1 -0
  25. package/src/core/executables/executableResolver.ts +61 -43
  26. package/src/core/executables/geminiExecutable.ts +0 -0
  27. package/src/core/githubDiagnostics.ts +0 -0
  28. package/src/core/hollowResponseFormat.ts +0 -0
  29. package/src/core/inputDebug.ts +0 -0
  30. package/src/core/launchContext.ts +33 -33
  31. package/src/core/models/codexCapabilities.ts +0 -0
  32. package/src/core/models/codexModelCapabilities.ts +0 -0
  33. package/src/core/models/modelSpecs.ts +0 -0
  34. package/src/core/perf/profiler.ts +0 -0
  35. package/src/core/perf/renderDebug.ts +0 -0
  36. package/src/core/planStorage.ts +0 -0
  37. package/src/core/process/CommandRunner.ts +0 -0
  38. package/src/core/process/processValidation.ts +9 -5
  39. package/src/core/projectInstructions.ts +0 -0
  40. package/src/core/providerLauncher/launcher.ts +59 -42
  41. package/src/core/providerLauncher/registry.ts +0 -0
  42. package/src/core/providerLauncher/types.ts +0 -0
  43. package/src/core/providerLauncher/workspaceConfig.ts +0 -0
  44. package/src/core/providerRuntime/anthropic.ts +0 -0
  45. package/src/core/providerRuntime/capabilityProfile.ts +0 -0
  46. package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
  47. package/src/core/providerRuntime/contextMetadata.ts +0 -0
  48. package/src/core/providerRuntime/gemini.ts +0 -0
  49. package/src/core/providerRuntime/lmstudio.ts +0 -0
  50. package/src/core/providerRuntime/local.ts +0 -0
  51. package/src/core/providerRuntime/models.ts +0 -0
  52. package/src/core/providerRuntime/reasoning.ts +0 -0
  53. package/src/core/providerRuntime/registry.ts +0 -0
  54. package/src/core/providerRuntime/types.ts +0 -0
  55. package/src/core/providers/codexJsonStream.ts +0 -0
  56. package/src/core/providers/codexSubprocess.ts +0 -0
  57. package/src/core/providers/codexTranscript.ts +0 -0
  58. package/src/core/providers/openaiNative.ts +0 -0
  59. package/src/core/providers/registry.ts +0 -0
  60. package/src/core/providers/types.ts +0 -0
  61. package/src/core/terminal/startupClear.ts +20 -0
  62. package/src/core/terminal/terminalCapabilities.ts +0 -0
  63. package/src/core/terminal/terminalControl.ts +0 -0
  64. package/src/core/terminal/terminalSanitize.ts +0 -0
  65. package/src/core/terminal/terminalSelection.ts +0 -0
  66. package/src/core/terminal/terminalTitle.ts +0 -0
  67. package/src/core/updateCheck.ts +188 -0
  68. package/src/core/workspaceActivity.ts +0 -0
  69. package/src/core/workspaceGuard.ts +0 -0
  70. package/src/core/workspaceRoot.ts +0 -0
  71. package/src/exec.ts +0 -0
  72. package/src/headless/execArgs.ts +1 -0
  73. package/src/headless/execRunner.ts +0 -0
  74. package/src/index.tsx +13 -9
  75. package/src/session/appSession.ts +0 -0
  76. package/src/session/chatLifecycle.ts +0 -0
  77. package/src/session/liveRenderScheduler.ts +0 -0
  78. package/src/session/planFlow.ts +0 -0
  79. package/src/session/planTranscript.ts +0 -0
  80. package/src/session/promptRunSchedule.ts +0 -0
  81. package/src/session/types.ts +2 -1
  82. package/src/test/runtimeTestUtils.ts +0 -0
  83. package/src/types/react-dom.d.ts +0 -0
  84. package/src/ui/ActionRequiredBlock.tsx +5 -5
  85. package/src/ui/ActivityBars.tsx +3 -3
  86. package/src/ui/ActivityIndicator.tsx +6 -6
  87. package/src/ui/AgentBlock.tsx +6 -6
  88. package/src/ui/AnimatedStatusText.tsx +1 -1
  89. package/src/ui/AppShell.tsx +13 -5
  90. package/src/ui/AttachmentImportPanel.tsx +8 -8
  91. package/src/ui/AuthPanel.tsx +20 -20
  92. package/src/ui/BackendPicker.tsx +0 -0
  93. package/src/ui/BottomComposer.tsx +117 -78
  94. package/src/ui/DashCard.tsx +3 -3
  95. package/src/ui/Markdown.tsx +17 -17
  96. package/src/ui/ModePicker.tsx +0 -0
  97. package/src/ui/ModelPicker.tsx +0 -0
  98. package/src/ui/ModelPickerScreen.tsx +10 -10
  99. package/src/ui/ModelReasoningPicker.tsx +14 -14
  100. package/src/ui/Panel.tsx +3 -3
  101. package/src/ui/PermissionsPanel.tsx +0 -0
  102. package/src/ui/PlanActionPicker.tsx +6 -6
  103. package/src/ui/PlanReviewPanel.tsx +8 -8
  104. package/src/ui/ProviderPicker.tsx +18 -18
  105. package/src/ui/ReasoningPicker.tsx +0 -0
  106. package/src/ui/RunFooter.tsx +3 -3
  107. package/src/ui/SelectionPanel.tsx +4 -4
  108. package/src/ui/SettingsPanel.tsx +8 -8
  109. package/src/ui/Spinner.tsx +1 -1
  110. package/src/ui/StaticIntroItem.tsx +0 -0
  111. package/src/ui/StaticTranscriptItem.tsx +0 -0
  112. package/src/ui/TextEntryPanel.tsx +10 -10
  113. package/src/ui/ThemePicker.tsx +0 -0
  114. package/src/ui/ThinkingBlock.tsx +8 -8
  115. package/src/ui/Timeline.tsx +21 -17
  116. package/src/ui/TopHeader.tsx +421 -257
  117. package/src/ui/TurnGroup.tsx +31 -31
  118. package/src/ui/UpdateAvailableCard.tsx +41 -0
  119. package/src/ui/UpdatePromptPanel.tsx +203 -0
  120. package/src/ui/busyStatusAnimation.ts +0 -0
  121. package/src/ui/commandNormalize.ts +0 -0
  122. package/src/ui/diffRenderer.ts +0 -0
  123. package/src/ui/focus.ts +3 -0
  124. package/src/ui/inputBuffer.ts +0 -0
  125. package/src/ui/layout.ts +1 -1
  126. package/src/ui/logoVariants.ts +107 -0
  127. package/src/ui/modeDisplay.ts +12 -12
  128. package/src/ui/outputPipeline.ts +0 -0
  129. package/src/ui/progressEntries.ts +0 -0
  130. package/src/ui/runActivityView.ts +0 -0
  131. package/src/ui/runtimeDisplay.ts +100 -0
  132. package/src/ui/slashCommands.ts +0 -0
  133. package/src/ui/terminalAnswerFormat.ts +0 -0
  134. package/src/ui/textLayout.ts +0 -0
  135. package/src/ui/theme.tsx +274 -395
  136. package/src/ui/themeFlow.ts +0 -0
  137. package/src/ui/timelineMeasure.ts +29 -19
  138. package/src/ui/useThrottledValue.ts +0 -0
  139. package/scripts/audit-codexa-capabilities.mjs +0 -466
  140. package/scripts/smoke-terminal-bench.mjs +0 -35
  141. package/src/appRenderStability.test.ts +0 -131
  142. package/src/commands/handler.test.ts +0 -643
  143. package/src/config/launchArgs.test.ts +0 -158
  144. package/src/config/layeredConfig.test.ts +0 -143
  145. package/src/config/persistence.test.ts +0 -110
  146. package/src/config/runtimeConfig.test.ts +0 -218
  147. package/src/config/settings.test.ts +0 -155
  148. package/src/config/trustStore.test.ts +0 -29
  149. package/src/core/attachments.test.ts +0 -155
  150. package/src/core/auth/codexAuth.test.ts +0 -68
  151. package/src/core/cleanupFastFail.test.ts +0 -76
  152. package/src/core/codexExecArgs.test.ts +0 -195
  153. package/src/core/codexLaunch.test.ts +0 -205
  154. package/src/core/codexPrompt.test.ts +0 -252
  155. package/src/core/executables/codexExecutable.test.ts +0 -212
  156. package/src/core/executables/executableResolver.test.ts +0 -129
  157. package/src/core/executables/geminiExecutable.test.ts +0 -116
  158. package/src/core/executables/pathSanityScan.test.ts +0 -47
  159. package/src/core/githubDiagnostics.test.ts +0 -92
  160. package/src/core/hollowResponseFormat.test.ts +0 -58
  161. package/src/core/launchContext.test.ts +0 -157
  162. package/src/core/models/codexCapabilities.test.ts +0 -45
  163. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  164. package/src/core/models/modelSpecs.test.ts +0 -283
  165. package/src/core/perf/renderDebug.test.ts +0 -230
  166. package/src/core/planStorage.test.ts +0 -143
  167. package/src/core/process/CommandRunner.test.ts +0 -105
  168. package/src/core/projectInstructions.test.ts +0 -50
  169. package/src/core/providerLauncher/launcher.test.ts +0 -238
  170. package/src/core/providerLauncher/registry.test.ts +0 -324
  171. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  172. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  173. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  174. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  175. package/src/core/providerRuntime/gemini.test.ts +0 -437
  176. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  177. package/src/core/providerRuntime/local.test.ts +0 -787
  178. package/src/core/providerRuntime/registry.test.ts +0 -233
  179. package/src/core/providers/codexJsonStream.test.ts +0 -148
  180. package/src/core/providers/codexSubprocess.test.ts +0 -68
  181. package/src/core/providers/codexTranscript.test.ts +0 -284
  182. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  183. package/src/core/terminal/terminalControl.test.ts +0 -75
  184. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  185. package/src/core/terminal/terminalSelection.test.ts +0 -42
  186. package/src/core/terminal/terminalTitle.test.ts +0 -328
  187. package/src/core/workspaceActivity.test.ts +0 -163
  188. package/src/core/workspaceGuard.test.ts +0 -151
  189. package/src/core/workspaceRoot.test.ts +0 -23
  190. package/src/exec.test.ts +0 -13
  191. package/src/headless/execArgs.test.ts +0 -147
  192. package/src/headless/execRunner.test.ts +0 -434
  193. package/src/index.test.tsx +0 -618
  194. package/src/session/appSession.test.ts +0 -897
  195. package/src/session/chatLifecycle.test.ts +0 -64
  196. package/src/session/liveRenderScheduler.test.ts +0 -201
  197. package/src/session/planFlow.test.ts +0 -103
  198. package/src/session/planTranscript.test.ts +0 -65
  199. package/src/session/promptRunSchedule.test.ts +0 -36
  200. package/src/ui/ActivityIndicator.test.tsx +0 -58
  201. package/src/ui/AgentBlock.test.ts +0 -6
  202. package/src/ui/AnimatedStatusText.test.ts +0 -16
  203. package/src/ui/AppShell.test.tsx +0 -1739
  204. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  205. package/src/ui/BottomComposer.test.ts +0 -674
  206. package/src/ui/CodexLogo.tsx +0 -55
  207. package/src/ui/Markdown.test.ts +0 -157
  208. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  209. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  210. package/src/ui/ModelPickerState.test.tsx +0 -151
  211. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  212. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  213. package/src/ui/PromptCardBorder.test.tsx +0 -161
  214. package/src/ui/ProviderPicker.test.tsx +0 -289
  215. package/src/ui/ProviderShortcut.test.tsx +0 -143
  216. package/src/ui/SettingsPanel.test.tsx +0 -233
  217. package/src/ui/Timeline.test.ts +0 -2067
  218. package/src/ui/TimelineNavigation.test.tsx +0 -201
  219. package/src/ui/TopHeader.test.tsx +0 -239
  220. package/src/ui/TurnGroup.test.tsx +0 -365
  221. package/src/ui/busyStatusAnimation.test.ts +0 -30
  222. package/src/ui/commandNormalize.test.ts +0 -142
  223. package/src/ui/diffRenderer.test.ts +0 -102
  224. package/src/ui/focusFlow.test.tsx +0 -1098
  225. package/src/ui/inputBuffer.test.ts +0 -151
  226. package/src/ui/layout.test.ts +0 -145
  227. package/src/ui/modeDisplay.test.ts +0 -42
  228. package/src/ui/runActivityView.test.ts +0 -89
  229. package/src/ui/runLifecycleView.test.tsx +0 -237
  230. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  231. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  232. package/src/ui/textLayout.test.ts +0 -18
  233. package/src/ui/themeFlow.test.ts +0 -53
  234. package/src/ui/timelineMeasureCache.test.ts +0 -986
@@ -0,0 +1,188 @@
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`;
7
+
8
+ export type UpdateStatus = "up-to-date" | "update-available" | "unknown" | "error";
9
+
10
+ export interface NpmRegistryMetadata {
11
+ "dist-tags": { latest?: string };
12
+ }
13
+
14
+ export interface UpdateCheckResult {
15
+ status: UpdateStatus;
16
+ currentVersion: string;
17
+ latestVersion: string | null;
18
+ errorMessage?: string;
19
+ checkedAt: number;
20
+ source?: "npm" | "cache";
21
+ }
22
+
23
+ const FETCH_TIMEOUT_MS = 5000;
24
+
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;
28
+ }
29
+
30
+ const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
31
+
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));
35
+ }
36
+
37
+ export function shouldRunStartupUpdateCheck(
38
+ env: NodeJS.ProcessEnv = process.env,
39
+ enabled = true,
40
+ ): boolean {
41
+ return enabled && !isLocalDevChannel(env);
42
+ }
43
+
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
+ };
56
+
57
+ const pa = parseParts(a);
58
+ const pb = parseParts(b);
59
+ const len = Math.max(pa.numeric.length, pb.numeric.length);
60
+
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;
64
+ }
65
+
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;
71
+ }
72
+ return 0;
73
+ }
74
+
75
+ export function isNewerVersion(candidate: string, current: string): boolean {
76
+ return compareSemver(candidate, current) > 0;
77
+ }
78
+
79
+ export interface UpdateCheckOverrides {
80
+ currentVersion?: string;
81
+ fetchNpmMetadataFn?: (url: string) => Promise<NpmRegistryMetadata>;
82
+ }
83
+
84
+ async function defaultFetchNpmMetadata(url: string): Promise<NpmRegistryMetadata> {
85
+ const controller = new AbortController();
86
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
87
+ try {
88
+ const res = await fetch(url, {
89
+ signal: controller.signal,
90
+ headers: { "User-Agent": `${CODEXA_NPM_PACKAGE}-update-checker/1.0` },
91
+ });
92
+ if (!res.ok) throw new Error(`npm registry returned HTTP ${res.status}`);
93
+ return await res.json() as NpmRegistryMetadata;
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+
99
+ export async function checkForUpdates(
100
+ opts?: { enabled?: boolean },
101
+ overrides?: UpdateCheckOverrides,
102
+ ): Promise<UpdateCheckResult> {
103
+ const currentVersion = normalizeVersion(overrides?.currentVersion ?? APP_VERSION);
104
+
105
+ if (opts?.enabled === false) {
106
+ return { status: "unknown", currentVersion, latestVersion: null, checkedAt: Date.now(), source: "npm" };
107
+ }
108
+
109
+ try {
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" };
140
+ } catch (err) {
141
+ return {
142
+ status: "error",
143
+ currentVersion,
144
+ latestVersion: null,
145
+ errorMessage: err instanceof Error ? err.message : String(err),
146
+ checkedAt: Date.now(),
147
+ source: "npm",
148
+ };
149
+ }
150
+ }
151
+
152
+ export function formatUpdateInstructions(result: UpdateCheckResult | null): string {
153
+ const current = result?.currentVersion ?? APP_VERSION;
154
+ const latest = result?.latestVersion ?? "unknown";
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
+
164
+ let statusLine: string;
165
+ if (result?.status === "update-available" && result.latestVersion) {
166
+ statusLine = `Update available: Codexa ${result.latestVersion} is available. You are using ${current}.`;
167
+ } else if (result?.status === "up-to-date") {
168
+ statusLine = "Already up to date.";
169
+ } else {
170
+ statusLine = "Status unknown — could not reach npm registry.";
171
+ }
172
+
173
+ return [
174
+ `Current installed version: ${current}`,
175
+ `npm latest version: ${latest}`,
176
+ `Status: ${statusLine}`,
177
+ "",
178
+ `Run: ${CODEXA_UPDATE_COMMAND}`,
179
+ ].join("\n");
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
+ }
File without changes
File without changes
File without changes
package/src/exec.ts CHANGED
File without changes
@@ -60,6 +60,7 @@ function buildLaunchArgs(params: {
60
60
  configOverrides: params.configOverrides,
61
61
  passthroughArgs: params.passthroughArgs,
62
62
  modelOverride: params.modelOverride,
63
+ noClear: false,
63
64
  };
64
65
  }
65
66
 
File without changes
package/src/index.tsx CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  traceTerminalClear,
20
20
  writeTerminalControl,
21
21
  } from "./core/terminal/terminalControl.js";
22
+ import { performStartupClear } from "./core/terminal/startupClear.js";
22
23
 
23
24
  type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
24
25
  const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
@@ -174,15 +175,18 @@ export function startApp({
174
175
  durationMs: 3500,
175
176
  });
176
177
 
177
- // Clear the screen and move cursor to home before rendering so no stale
178
- // content from a previous process (e.g. bun --watch restart) ghosts above
179
- // the new frame. We stay in the normal screen buffer (no \x1b[?1049h) to
180
- // preserve terminal scrollback and allow mouse text selection.
181
- // NOTE: Mouse reporting (\x1b[?1000h / \x1b[?1006h) is NOT enabled here.
182
- // It is managed exclusively by the React app (app.tsx) and defaults to OFF
183
- // so native terminal drag-selection and copy work without any special steps.
184
- traceTerminalClear("src/index.tsx:startup", { mode: "hard" });
185
- writeStdout(TERMINAL_SEQUENCES.hardRepaint, "src/index.tsx:startup");
178
+ // Clear the screen (viewport + scrollback) and move cursor home before Ink
179
+ // renders the first frame. This removes any previous terminal content so
180
+ // the app opens into a clean screen. We stay in the normal screen buffer
181
+ // (no \x1b[?1049h) to preserve mouse text selection after exit.
182
+ // Skipped when --no-clear or CODEXA_NO_CLEAR=1 is set.
183
+ // NOTE: Mouse reporting is NOT enabled here managed solely by app.tsx.
184
+ traceTerminalClear("src/index.tsx:startup", { mode: "transcript" });
185
+ performStartupClear({
186
+ write: (c) => writeStdout(c, "src/index.tsx:startup"),
187
+ noClear: launchArgs.noClear,
188
+ env,
189
+ });
186
190
  const startupWorkspaceRoot = resolveWorkspaceRoot();
187
191
  const startupSettings = loadSettings();
188
192
  const startupTitle =
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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
 
File without changes
File without changes
@@ -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,5 +1,6 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useStdin } from "ink";
3
+ import { useTheme } from "./theme.js";
3
4
  import type { RuntimeSummary } from "../config/runtimeConfig.js";
4
5
  import type { CodexAuthState } from "../core/auth/codexAuth.js";
5
6
  import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
@@ -23,11 +24,11 @@ import {
23
24
  } from "./Timeline.js";
24
25
  import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
25
26
  import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
26
- import { MemoizedTopHeader, measureTopHeaderRows } from "./TopHeader.js";
27
+ import { MemoizedTopHeader, measureTopHeaderRows, type UpdateAvailableInfo } from "./TopHeader.js";
27
28
 
28
- const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 2;
29
- const MEDIUM_HEADER_TO_COMPOSER_GAP_ROWS = 4;
30
- const TALL_HEADER_TO_COMPOSER_GAP_ROWS = 6;
29
+ const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 1;
30
+ const MEDIUM_HEADER_TO_COMPOSER_GAP_ROWS = 1;
31
+ const TALL_HEADER_TO_COMPOSER_GAP_ROWS = 1;
31
32
 
32
33
  // ─── Types & constants ────────────────────────────────────────────────────────
33
34
 
@@ -57,6 +58,7 @@ export interface AppShellProps {
57
58
  selectionProfile?: TerminalSelectionProfile;
58
59
  clearCount?: number;
59
60
  headerConfig?: HeaderConfig;
61
+ updateAvailable?: UpdateAvailableInfo | null;
60
62
  }
61
63
 
62
64
  // ─── Helpers & subcomponents ─────────────────────────────────────────────────
@@ -161,7 +163,9 @@ function AppShellInner({
161
163
  selectionProfile,
162
164
  clearCount = 0,
163
165
  headerConfig = HEADER_CONFIG_DEFAULTS,
166
+ updateAvailable = null,
164
167
  }: AppShellProps) {
168
+ const theme = useTheme();
165
169
  renderDebug.useRenderDebug("AppShell", {
166
170
  cols: layout.cols,
167
171
  rows: layout.rows,
@@ -188,7 +192,7 @@ function AppShellInner({
188
192
 
189
193
  const shellWidth = getShellWidth(layout.cols);
190
194
  const shellHeight = getShellHeight(layout.rows);
191
- const headerRows = measureTopHeaderRows(layout);
195
+ const headerRows = measureTopHeaderRows(layout, headerConfig, !!updateAvailable);
192
196
  const headerToContentGapRows = calculateHeaderToContentGapRows(layout);
193
197
  const showComposer = screen === "main";
194
198
  const showMainPanel = screen === "main" && mainPanel !== undefined && mainPanel !== null;
@@ -510,6 +514,7 @@ function AppShellInner({
510
514
  layout={layout}
511
515
  runtimeSummary={runtimeSummary}
512
516
  headerConfig={headerConfig}
517
+ updateAvailable={updateAvailable}
513
518
  />
514
519
 
515
520
  {headerToContentGapRows > 0 && (
@@ -540,6 +545,7 @@ function AppShellInner({
540
545
  layout={layout}
541
546
  runtimeSummary={runtimeSummary}
542
547
  headerConfig={headerConfig}
548
+ updateAvailable={updateAvailable}
543
549
  />
544
550
 
545
551
  {headerToContentGapRows > 0 && (
@@ -594,6 +600,7 @@ function AppShellInner({
594
600
  layout={layout}
595
601
  runtimeSummary={runtimeSummary}
596
602
  headerConfig={headerConfig}
603
+ updateAvailable={updateAvailable}
597
604
  />
598
605
 
599
606
  {headerToContentGapRows > 0 && (
@@ -693,6 +700,7 @@ export const AppShell = memo(AppShellInner, (prev, next) => {
693
700
  prev.mouseCapture === next.mouseCapture &&
694
701
  prev.onMouseActivity === next.onMouseActivity &&
695
702
  prev.clearCount === next.clearCount &&
703
+ prev.updateAvailable === next.updateAvailable &&
696
704
  panelPropsEqual
697
705
  );
698
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>
@@ -49,10 +49,10 @@ export function AuthPanel({
49
49
  const authStateLabel = getAuthStateLabel(authStatus.state);
50
50
  const authStateColor =
51
51
  authStatus.state === "authenticated"
52
- ? theme.SUCCESS
52
+ ? theme.success
53
53
  : authStatus.state === "unauthenticated"
54
- ? theme.ERROR
55
- : theme.WARNING;
54
+ ? theme.error
55
+ : theme.warning;
56
56
  const checkedAtLabel = authStatus.checkedAt > 0
57
57
  ? new Date(authStatus.checkedAt).toLocaleTimeString()
58
58
  : "not checked yet";
@@ -60,52 +60,52 @@ export function AuthPanel({
60
60
  return (
61
61
  <Box
62
62
  borderStyle="round"
63
- borderColor={theme.BORDER_ACTIVE}
63
+ borderColor={theme.borderFocused}
64
64
  flexDirection="column"
65
65
  paddingX={2}
66
66
  paddingY={1}
67
67
  marginTop={1}
68
68
  width="100%"
69
69
  >
70
- <Text color={theme.ACCENT} bold>
70
+ <Text color={theme.accent} bold>
71
71
  Auth and subscription guidance
72
72
  </Text>
73
- <Text color={theme.MUTED}>Current backend: {provider.label}</Text>
74
- <Text color={theme.MUTED}>Current preference: {formatAuthPreferenceLabel(authPreference)}</Text>
75
- <Text color={theme.INFO}>Backend auth: {provider.authLabel}</Text>
73
+ <Text color={theme.textMuted}>Current backend: {provider.label}</Text>
74
+ <Text color={theme.textMuted}>Current preference: {formatAuthPreferenceLabel(authPreference)}</Text>
75
+ <Text color={theme.info}>Backend auth: {provider.authLabel}</Text>
76
76
  <Text color={authStateColor}>Runtime auth state: {authStateLabel}</Text>
77
- <Text color={theme.DIM}>Last checked: {checkedAtLabel}</Text>
78
- <Text color={theme.DIM}>Probe summary: {authStatus.rawSummary || "No probe output yet"}</Text>
79
- <Text color={theme.TEXT}>
77
+ <Text color={theme.textDim}>Last checked: {checkedAtLabel}</Text>
78
+ <Text color={theme.textDim}>Probe summary: {authStatus.rawSummary || "No probe output yet"}</Text>
79
+ <Text color={theme.text}>
80
80
  This UI securely bridges to the Codexa neural network. It does not collect or store your ChatGPT credentials.
81
81
  </Text>
82
- <Text color={theme.TEXT}>{provider.statusMessage}</Text>
82
+ <Text color={theme.text}>{provider.statusMessage}</Text>
83
83
  <Box flexDirection="column" marginTop={1}>
84
- <Text color={theme.INFO}>Commands:</Text>
85
- <Text color={theme.TEXT}> /login guided ChatGPT sign-in steps</Text>
86
- <Text color={theme.TEXT}> /logout guided sign-out steps</Text>
87
- <Text color={theme.TEXT}> /auth status refresh Codexa authentication</Text>
84
+ <Text color={theme.info}>Commands:</Text>
85
+ <Text color={theme.text}> /login guided ChatGPT sign-in steps</Text>
86
+ <Text color={theme.text}> /logout guided sign-out steps</Text>
87
+ <Text color={theme.text}> /auth status refresh Codexa authentication</Text>
88
88
  </Box>
89
89
  <Box flexDirection="column" marginTop={1}>
90
90
  {AUTH_PREFERENCES.map((item, index) => (
91
- <Text key={item.id} color={item.id === authPreference ? theme.SUCCESS : theme.TEXT}>
91
+ <Text key={item.id} color={item.id === authPreference ? theme.success : theme.text}>
92
92
  {index + 1}. {item.label} {item.id === authPreference ? "✓" : ""}
93
93
  </Text>
94
94
  ))}
95
95
  </Box>
96
96
  <Box marginTop={1}>
97
- <Text color={theme.DIM}>
97
+ <Text color={theme.textDim}>
98
98
  Press 1-{AUTH_PREFERENCES.length} to change preference, R to refresh status, Esc to close.
99
99
  </Text>
100
100
  </Box>
101
101
  {authStatusBusy && (
102
102
  <Box marginTop={1}>
103
- <Text color={theme.WARNING}>Checking auth status...</Text>
103
+ <Text color={theme.warning}>Checking auth status...</Text>
104
104
  </Box>
105
105
  )}
106
106
  {authStatus.recommendedAction && (
107
107
  <Box marginTop={1}>
108
- <Text color={theme.DIM}>Next step: {authStatus.recommendedAction}</Text>
108
+ <Text color={theme.textDim}>Next step: {authStatus.recommendedAction}</Text>
109
109
  </Box>
110
110
  )}
111
111
  </Box>
File without changes