@golba98/codexa 1.0.3 → 1.0.5

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 (125) 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 +642 -303
  5. package/src/commands/handler.ts +7 -18
  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/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -3,19 +3,41 @@ import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
3
3
 
4
4
  type PathStyle = "windows" | "posix";
5
5
 
6
+ export function normalizeDiagnosticPath(filePath: string): string {
7
+ let pathPart = filePath;
8
+ let drivePrefix = "";
9
+
10
+ if (/^[A-Za-z]:[\\/]/.test(filePath)) {
11
+ drivePrefix = filePath.slice(0, 2);
12
+ pathPart = filePath.slice(2);
13
+ }
14
+
15
+ // Robustly strip trailing colon-separated line/column numbers.
16
+ // Handles:
17
+ // path/to/file.rs:109:12
18
+ // path/to/file.rs:109
19
+ // C:\path\to\file.rs:12:5
20
+ const diagnosticSuffixRegex = /:(\d+)(?::(\d+))?$/;
21
+ pathPart = pathPart.replace(diagnosticSuffixRegex, "");
22
+
23
+ return drivePrefix + pathPart;
24
+ }
25
+
6
26
  export interface WorkspacePathViolation {
7
27
  rawPath: string;
8
28
  normalizedPath: string;
9
29
  }
10
30
 
11
31
  const QUOTED_ABSOLUTE_PATH_PATTERN =
12
- /(["'`])((?:[A-Za-z]:[\\/]|\\\\[^\\/\r\n]+[\\/][^\\/\r\n]+[\\/]|\/)[^"'`\r\n]+?)\1/g;
32
+ /(["'`])((?:[A-Za-z]:[\\/]|\\\\[^\\/\r\n]+[\\/][^\\/\r\n]+[\\/]|\/|~\/)[^"'`\r\n]+?)\1/g;
13
33
  const QUOTED_RELATIVE_PATH_PATTERN = /(["'`])((?:\.\.?[\\/])[^"'`\r\n]+?)\1/g;
14
34
  const WINDOWS_DRIVE_PATH_PATTERN = /(?:^|[\s([{\],;=])([A-Za-z]:[\\/][^\s"'`<>|]+)/g;
15
35
  const WINDOWS_UNC_PATH_PATTERN =
16
36
  /(?:^|[\s([{\],;=])(\\\\[^\\/\s"'`<>|]+[\\/][^\\/\s"'`<>|]+(?:[\\/][^\s"'`<>|]+)*)/g;
17
- const POSIX_ABSOLUTE_PATH_PATTERN = /(?:^|[\s([{\],;=])(\/(?:[^\/\s"'`<>|]+\/)+[^\s"'`<>|]+)/g;
37
+ const POSIX_ABSOLUTE_PATH_PATTERN = /(?:^|[\s([{\],;=])((?:\/|~\/)(?:[^\/\s"'`<>|]+\/)+[^\s"'`<>|]+)/g;
18
38
  const RELATIVE_PATH_PATTERN = /(?:^|[\s([{\],;=])((?:\.\.?[\\/])(?:[^\s"'`<>|]+(?:[\\/][^\s"'`<>|]+)*))/g;
39
+ const RUST_RELATIVE_DIAGNOSTIC_PATH_PATTERN =
40
+ /(?:^|[\s([{\],;=])((?:[A-Za-z0-9_.-]+[\\/])+[A-Za-z0-9_.-]+\.rs(?::\d+(?::\d+)?)?)/g;
19
41
  const TRAILING_PUNCTUATION_PATTERN = /[),.;:\]}]+$/;
20
42
  const DIRECTORY_NAVIGATION_PATTERN = /(?:^|[;&\n]|&&|\|\|)\s*(cd|pushd|set-location)\b/i;
21
43
 
@@ -24,7 +46,7 @@ function detectPathStyle(pathValue: string): PathStyle | null {
24
46
  return "windows";
25
47
  }
26
48
 
27
- if (pathValue.startsWith("/")) {
49
+ if (pathValue.startsWith("/") || pathValue.startsWith("~/")) {
28
50
  return "posix";
29
51
  }
30
52
 
@@ -47,6 +69,42 @@ function isExplicitRelativePath(pathValue: string): boolean {
47
69
  return /^(?:\.\.?[\\/])/.test(pathValue);
48
70
  }
49
71
 
72
+ function hasRustRelativeDiagnosticShape(pathValue: string): boolean {
73
+ return /^(?:[A-Za-z0-9_.-]+[\\/])+[A-Za-z0-9_.-]+\.rs(?::\d+(?::\d+)?)?$/.test(pathValue);
74
+ }
75
+
76
+ function normalizedPathSegments(pathValue: string): string[] {
77
+ return normalizeDiagnosticPath(pathValue)
78
+ .replace(/\\/g, "/")
79
+ .split("/")
80
+ .filter(Boolean)
81
+ .map((segment) => segment.toLowerCase());
82
+ }
83
+
84
+ export function isSkippedExternalDependencyPath(pathValue: string): boolean {
85
+ const segments = normalizedPathSegments(pathValue);
86
+
87
+ for (let i = 0; i < segments.length; i++) {
88
+ const segment = segments[i];
89
+ const next = segments[i + 1];
90
+ const afterNext = segments[i + 2];
91
+
92
+ if (segment === ".cargo" && next === "registry") return true;
93
+ if (segment === ".cargo" && next === "git" && afterNext === "checkouts") return true;
94
+ if (
95
+ segment === "target" ||
96
+ segment === "node_modules" ||
97
+ segment === ".pnpm-store" ||
98
+ segment === ".npm" ||
99
+ segment === ".cache"
100
+ ) {
101
+ return true;
102
+ }
103
+ }
104
+
105
+ return false;
106
+ }
107
+
50
108
  function normalizeAbsolutePath(pathValue: string, style: PathStyle): string {
51
109
  const pathApi = getPathApi(style);
52
110
  const resolved = pathApi.normalize(pathApi.resolve(pathValue));
@@ -149,7 +207,7 @@ export function extractExplicitPathReferences(text: string): string[] {
149
207
 
150
208
  const addMatch = (candidate: string) => {
151
209
  const cleaned = stripTrailingPunctuation(stripWrappingQuotes(candidate.trim()));
152
- if (!cleaned || (!detectPathStyle(cleaned) && !isExplicitRelativePath(cleaned))) {
210
+ if (!cleaned || (!detectPathStyle(cleaned) && !isExplicitRelativePath(cleaned) && !hasRustRelativeDiagnosticShape(cleaned))) {
153
211
  return;
154
212
  }
155
213
 
@@ -169,6 +227,7 @@ export function extractExplicitPathReferences(text: string): string[] {
169
227
  WINDOWS_UNC_PATH_PATTERN,
170
228
  POSIX_ABSOLUTE_PATH_PATTERN,
171
229
  RELATIVE_PATH_PATTERN,
230
+ RUST_RELATIVE_DIAGNOSTIC_PATH_PATTERN,
172
231
  ]) {
173
232
  pattern.lastIndex = 0;
174
233
  let match = pattern.exec(text);
@@ -181,24 +240,49 @@ export function extractExplicitPathReferences(text: string): string[] {
181
240
  return matches;
182
241
  }
183
242
 
243
+ export function formatSkippedDependencyPath(pathValue: string): string {
244
+ const parts = pathValue.replace(/\\/g, "/").split("/");
245
+ const registryIndex = parts.indexOf("registry");
246
+ if (registryIndex !== -1 && parts[registryIndex + 1] === "src" && parts[registryIndex + 2]?.includes("index.crates.io")) {
247
+ return parts.slice(registryIndex + 3).join("/");
248
+ }
249
+ const gitIndex = parts.indexOf("git");
250
+ if (gitIndex !== -1 && parts[gitIndex + 1] === "checkouts") {
251
+ return parts.slice(gitIndex + 2).join("/");
252
+ }
253
+ return parts[parts.length - 1] ?? pathValue;
254
+ }
255
+
184
256
  export function findOutsideWorkspacePaths(
185
257
  text: string,
186
258
  workspaceRoot: string,
187
259
  allowedRoots: readonly string[] = [],
188
- ): WorkspacePathViolation[] {
260
+ ): { violations: WorkspacePathViolation[]; skippedExternalPaths: string[] } {
189
261
  const violations: WorkspacePathViolation[] = [];
262
+ const skippedExternalPaths: string[] = [];
190
263
  const seen = new Set<string>();
191
264
  const workspaceStyle = detectPathStyle(workspaceRoot) ?? "windows";
192
265
 
193
266
  for (const rawPath of extractExplicitPathReferences(text)) {
194
- if (isPathInsideAllowedRoots(rawPath, workspaceRoot, allowedRoots)) {
267
+ const cleanPath = normalizeDiagnosticPath(rawPath);
268
+ if (isSkippedExternalDependencyPath(cleanPath)) {
269
+ skippedExternalPaths.push(cleanPath);
270
+ continue;
271
+ }
272
+
273
+ if (isPathInsideAllowedRoots(cleanPath, workspaceRoot, allowedRoots)) {
274
+ continue;
275
+ }
276
+
277
+ const explicitStyle = detectPathStyle(cleanPath) ?? workspaceStyle;
278
+ const normalizedPath = detectPathStyle(cleanPath)
279
+ ? normalizeAbsolutePath(cleanPath, explicitStyle)
280
+ : resolveWorkspacePath(cleanPath, workspaceRoot);
281
+ if (isSkippedExternalDependencyPath(normalizedPath)) {
282
+ skippedExternalPaths.push(normalizedPath);
195
283
  continue;
196
284
  }
197
285
 
198
- const explicitStyle = detectPathStyle(rawPath) ?? workspaceStyle;
199
- const normalizedPath = detectPathStyle(rawPath)
200
- ? normalizeAbsolutePath(rawPath, explicitStyle)
201
- : resolveWorkspacePath(rawPath, workspaceRoot);
202
286
  const comparisonKey = explicitStyle === "windows" ? normalizedPath.toLowerCase() : normalizedPath;
203
287
 
204
288
  if (seen.has(comparisonKey)) {
@@ -209,7 +293,7 @@ export function findOutsideWorkspacePaths(
209
293
  violations.push({ rawPath, normalizedPath });
210
294
  }
211
295
 
212
- return violations;
296
+ return { violations, skippedExternalPaths };
213
297
  }
214
298
 
215
299
  export function containsDirectoryNavigationCommand(command: string): boolean {
@@ -248,7 +332,7 @@ export function getPromptWorkspaceGuardMessage(
248
332
  workspaceRoot: string,
249
333
  allowedRoots: readonly string[] = [],
250
334
  ): string | null {
251
- const violations = findOutsideWorkspacePaths(prompt, workspaceRoot, allowedRoots);
335
+ const { violations } = findOutsideWorkspacePaths(prompt, workspaceRoot, allowedRoots);
252
336
  if (violations.length === 0) {
253
337
  return null;
254
338
  }
@@ -274,7 +358,7 @@ export function getShellWorkspaceGuardMessage(
274
358
  ].join("\n");
275
359
  }
276
360
 
277
- const violations = findOutsideWorkspacePaths(command, workspaceRoot, allowedRoots);
361
+ const { violations } = findOutsideWorkspacePaths(command, workspaceRoot, allowedRoots);
278
362
  if (violations.length === 0) {
279
363
  return null;
280
364
  }
@@ -8,12 +8,12 @@ import {
8
8
  resolveRuntimeConfig,
9
9
  type ResolvedRuntimeConfig,
10
10
  } from "../config/runtimeConfig.js";
11
- import { loadProjectInstructions, type ProjectInstructionsLoadResult } from "../core/projectInstructions.js";
11
+ import { loadProjectInstructions, type ProjectInstructionsLoadResult } from "../core/workspace/projectInstructions.js";
12
12
  import { getBackendProvider } from "../core/providers/registry.js";
13
13
  import type { BackendProvider } from "../core/providers/types.js";
14
14
  import { isNoiseLine } from "../core/providers/codexTranscript.js";
15
15
  import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
16
- import { resolveWorkspaceRoot } from "../core/workspaceRoot.js";
16
+ import { resolveWorkspaceRoot } from "../core/workspace/workspaceRoot.js";
17
17
  import type { RunToolActivity } from "../session/types.js";
18
18
 
19
19
  // ─── Types & constants ────────────────────────────────────────────────────────
package/src/index.tsx CHANGED
@@ -7,55 +7,35 @@ import { APP_NAME, formatTerminalTitlePath } from "./config/settings.js";
7
7
  import { getTerminalCapability } from "./core/terminal/terminalCapabilities.js";
8
8
  import * as renderDebug from "./core/perf/renderDebug.js";
9
9
  import { MIN_VIEWPORT_COLS, MIN_VIEWPORT_ROWS } from "./ui/layout.js";
10
- import {
11
- reassertIntendedTerminalTitle,
12
- setIntendedTerminalTitle,
13
- startTerminalTitleStartupGuard,
14
- } from "./core/terminal/terminalTitle.js";
15
- import { resolveWorkspaceRoot } from "./core/workspaceRoot.js";
16
- import {
17
- createTerminalModeController,
18
- TERMINAL_SEQUENCES,
19
- traceTerminalClear,
20
- writeTerminalControl,
21
- } from "./core/terminal/terminalControl.js";
22
- import { performStartupClear } from "./core/terminal/startupClear.js";
23
-
24
- type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
25
- const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
26
- mode: "auto",
27
- flags: ["disambiguateEscapeCodes"],
28
- };
29
-
30
- /**
31
- * Typed subset of the internal Ink class instance we access to disable Ink's
32
- * built-in resize listener. Post-startup repainting is driven by React layout
33
- * state, not by forcing Ink's private render buffers.
34
- */
35
- interface InkInstance {
36
- unsubscribeResize?: () => void;
37
- }
38
-
39
- /**
40
- * Resolve the real Ink class instance via Ink's internal WeakMap<stdout, Ink>.
41
- * Returns null if resolution fails (e.g. different Ink version, test mocks).
42
- */
43
- function resolveInkInstance(stdout: AppStdout): InkInstance | null {
44
- try {
45
- // Bun doesn't resolve bare subpath imports like "ink/build/instances.js"
46
- // so we resolve ink's main entry first, then derive the sibling path.
47
- const { createRequire } = require("node:module");
48
- const req = createRequire(import.meta.url);
49
- const inkMain = req.resolve("ink");
50
- const instancesPath = inkMain.replace(/index\.js$/, "instances.js");
51
- const instances = req(instancesPath);
52
- const weakMap: WeakMap<object, InkInstance> =
53
- instances.default ?? instances;
54
- return weakMap.get(stdout as object) ?? null;
55
- } catch {
56
- return null;
57
- }
58
- }
10
+ import {
11
+ setIntendedTerminalTitle,
12
+ } from "./core/terminal/terminalTitle.js";
13
+ import { resolveWorkspaceRoot } from "./core/workspace/workspaceRoot.js";
14
+ import {
15
+ createTerminalModeController,
16
+ writeTerminalControl,
17
+ } from "./core/terminal/terminalControl.js";
18
+ import { resolveInkRenderInstance, type InkRenderInstance } from "./core/terminal/inkRenderReset.js";
19
+ import { resetFrameLockForResize, wrapStdoutWithFrameLock } from "./core/terminal/frameLock.js";
20
+
21
+ type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
22
+
23
+ export function resolveKittyKeyboardOptions(
24
+ env: Record<string, string | undefined>,
25
+ ): RenderOptions["kittyKeyboard"] | undefined {
26
+ // Ink's auto mode probes with CSI ? u. Kitty's response can race Ink's
27
+ // regular input listeners and leak into both the first frame and composer.
28
+ // A direct Kitty session is already known to support the protocol, so enable
29
+ // it without probing. Avoid protocol negotiation in all other terminals.
30
+ if ((env.TERM ?? "").toLowerCase() !== "xterm-kitty") {
31
+ return undefined;
32
+ }
33
+
34
+ return {
35
+ mode: "enabled",
36
+ flags: ["disambiguateEscapeCodes"],
37
+ };
38
+ }
59
39
 
60
40
  export interface AppStdout {
61
41
  isTTY: boolean;
@@ -74,7 +54,7 @@ export interface StartAppDependencies {
74
54
  platform: NodeJS.Platform;
75
55
  argv: string[];
76
56
  renderApp: (node: React.ReactElement, options?: RenderOptions) => RenderHandle;
77
- resolveInkInstanceForStdout: (stdout: AppStdout) => InkInstance | null;
57
+ resolveInkInstanceForStdout: (stdout: AppStdout) => InkRenderInstance | null;
78
58
  registerExitHandler: (handler: () => void) => void;
79
59
  }
80
60
 
@@ -112,7 +92,7 @@ export function startApp({
112
92
  platform = process.platform,
113
93
  argv = process.argv.slice(2),
114
94
  renderApp = render,
115
- resolveInkInstanceForStdout = resolveInkInstance,
95
+ resolveInkInstanceForStdout = resolveInkRenderInstance,
116
96
  registerExitHandler = (handler) => {
117
97
  process.on("exit", handler);
118
98
  },
@@ -123,7 +103,11 @@ export function startApp({
123
103
  return { started: true, exitCode: 0 };
124
104
  }
125
105
 
126
- const terminal = createTerminalModeController((chunk) => stdout.write(chunk));
106
+ // Wrap stdout to implement frame locking, deduplication, and width-safe row
107
+ // padding via \x1b[K injection across the entire TUI.
108
+ const wrappedStdout = wrapStdoutWithFrameLock({ stdout, env });
109
+
110
+ const terminal = createTerminalModeController((chunk) => wrappedStdout.write(chunk));
127
111
  const writeStdout = (chunk: string, source: string): boolean => terminal.write(chunk, source);
128
112
 
129
113
  const writeStderr = (chunk: string, source: string): boolean => {
@@ -163,56 +147,38 @@ export function startApp({
163
147
  return { started: false, exitCode: 1 };
164
148
  }
165
149
  const launchArgs: LaunchArgs = parsedLaunchArgs.value;
166
- setIntendedTerminalTitle(env.CODEXA_INITIAL_TERMINAL_TITLE ?? APP_NAME, {
167
- force: true,
168
- reason: "index-safe-fallback-title",
169
- write: (chunk) => writeStdout(chunk, "src/index.tsx:title.fallback"),
170
- });
171
- const stopStartupTitleGuard = startTerminalTitleStartupGuard({
172
- write: (chunk) => writeStdout(chunk, "src/index.tsx:title.startupGuard"),
173
- reason: "index-startup-guard",
174
- intervalMs: 150,
175
- durationMs: 3500,
176
- });
177
-
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
- });
190
- const startupWorkspaceRoot = resolveWorkspaceRoot();
150
+ // Main chat starts in the normal screen buffer so the startup frame and later
151
+ // transcript rows are owned by native terminal scrollback. Overlay screens
152
+ // enter alternate-screen mode from app.tsx only while an overlay is active.
153
+ const startupWorkspaceRoot = resolveWorkspaceRoot();
191
154
  const startupSettings = loadSettings();
192
155
  const startupTitle =
193
156
  formatTerminalTitlePath(startupWorkspaceRoot, startupSettings.ui.terminalTitleMode) || APP_NAME;
194
- setIntendedTerminalTitle(startupTitle, {
195
- force: true,
196
- reason: "startup-title",
197
- write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.title"),
198
- });
199
- reassertIntendedTerminalTitle({
200
- write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.titleReady"),
201
- reason: "startup-title-ready",
202
- });
203
- terminal.setBracketedPaste(true, "src/index.tsx:startup.bracketedPaste");
204
-
157
+ setIntendedTerminalTitle(startupTitle, {
158
+ force: true,
159
+ reason: "startup-title",
160
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.title"),
161
+ });
162
+ terminal.setBracketedPaste(true, "src/index.tsx:startup.bracketedPaste");
163
+
205
164
  let cleanupDone = false;
206
165
  let repaintArmed = false;
207
166
  let renderHandle: RenderHandle | null = null;
208
- let inkInstance: InkInstance | null = null;
209
-
210
- const onResize = () => {
211
- renderDebug.traceEvent("terminal", "resize", {
167
+ let inkInstance: InkRenderInstance | null = null;
168
+ let previousResizeCols = stdout.columns;
169
+ let previousResizeRows = stdout.rows;
170
+
171
+ const onResize = () => {
172
+ resetFrameLockForResize(wrappedStdout);
173
+ renderDebug.traceEvent("terminal", "resize", {
174
+ previousCols: previousResizeCols,
175
+ previousRows: previousResizeRows,
212
176
  cols: stdout.columns,
213
177
  rows: stdout.rows,
214
178
  invalid: hasInvalidRestoreDimensions(stdout),
215
179
  });
180
+ previousResizeCols = stdout.columns;
181
+ previousResizeRows = stdout.rows;
216
182
 
217
183
  if (hasInvalidRestoreDimensions(stdout)) {
218
184
  // Transient invalid dimensions (e.g. during maximize/restore on
@@ -234,12 +200,12 @@ export function startApp({
234
200
  const cleanup = () => {
235
201
  if (cleanupDone) return;
236
202
  cleanupDone = true;
237
- stopStartupTitleGuard();
238
- stdout.off("resize", onResize);
203
+ stdout.off("resize", onResize);
239
204
  renderHandle?.cleanup();
240
205
  // Restore terminal state: disable mouse reporting and bracketed paste.
241
206
  terminal.setMouseReporting(false, "src/index.tsx:cleanup.mouse");
242
207
  terminal.setBracketedPaste(false, "src/index.tsx:cleanup.bracketedPaste");
208
+ terminal.setAlternateScreen(false, "src/index.tsx:cleanup.alternateScreen");
243
209
  terminal.resetModes();
244
210
  activeRoot = null;
245
211
  };
@@ -269,13 +235,15 @@ export function startApp({
269
235
  process.on("uncaughtException", handleFatal);
270
236
  process.on("unhandledRejection", handleFatal);
271
237
 
272
- renderHandle = renderApp(<App launchArgs={launchArgs} />, {
273
- kittyKeyboard: KITTY_KEYBOARD_OPTIONS,
274
- });
238
+ const kittyKeyboard = resolveKittyKeyboardOptions(env);
239
+ renderHandle = renderApp(<App launchArgs={launchArgs} />, {
240
+ ...(kittyKeyboard ? { kittyKeyboard } : {}),
241
+ stdout: wrappedStdout as any,
242
+ });
275
243
 
276
244
  // Resolve the real Ink class instance to get access to lastOutput,
277
245
  // onRender, calculateLayout, etc. Gracefully degrades to null in tests.
278
- inkInstance = resolveInkInstanceForStdout(stdout);
246
+ inkInstance = resolveInkInstanceForStdout(wrappedStdout);
279
247
 
280
248
  // Remove Ink's own resize handler so the app is the sole resize handler.
281
249
  // This eliminates the race where Ink's resized() fires alongside our
@@ -18,7 +18,7 @@ import {
18
18
  upsertRunToolActivity,
19
19
  type UIStateAction,
20
20
  } from "./chatLifecycle.js";
21
- import type { RunFileActivity } from "../core/workspaceActivity.js";
21
+ import type { RunFileActivity } from "../core/workspace/workspaceActivity.js";
22
22
  import type { RunToolActivity } from "./types.js";
23
23
  import type { LiveRenderUpdate } from "./liveRenderScheduler.js";
24
24
  import * as renderDebug from "../core/perf/renderDebug.js";
@@ -47,7 +47,7 @@ export type SessionAction =
47
47
  | { type: "SUBMIT_PROMPT_RUN"; historyValue?: string; events: TimelineEvent[]; turnId: number; runId: number }
48
48
  | { type: "HISTORY_UP" }
49
49
  | { type: "HISTORY_DOWN" }
50
- | { type: "CLEAR_TRANSCRIPT" }
50
+ | { type: "CLEAR_TRANSCRIPT"; seedEvents?: TimelineEvent[] }
51
51
  | { type: "SET_ACTIVE_EVENTS"; events: TimelineEvent[] }
52
52
  | { type: "RUN_APPEND_ACTIVITY"; runId: number; activity: RunFileActivity[] }
53
53
  | { type: "RUN_APPLY_PROGRESS_UPDATES"; runId: number; updates: BackendProgressUpdate[] }
@@ -98,9 +98,9 @@ export type SessionAction =
98
98
 
99
99
  // ─── Helpers ─────────────────────────────────────────────────────────────────
100
100
 
101
- export function createInitialSessionState(): SessionState {
101
+ export function createInitialSessionState(options: { staticEvents?: TimelineEvent[] } = {}): SessionState {
102
102
  return {
103
- staticEvents: [],
103
+ staticEvents: options.staticEvents ?? [],
104
104
  activeEvents: [],
105
105
  uiState: { kind: "IDLE" },
106
106
  externalCliStatus: "idle",
@@ -327,11 +327,12 @@ export function reduceSessionState(state: SessionState, action: SessionAction):
327
327
  renderDebug.traceEvent("transcript", "clear", {
328
328
  previousStaticEventsLength: state.staticEvents.length,
329
329
  previousActiveEventsLength: state.activeEvents.length,
330
+ seedEventsLength: action.seedEvents?.length ?? 0,
330
331
  uiStateKind: state.uiState.kind,
331
332
  });
332
333
  return {
333
334
  ...state,
334
- staticEvents: [],
335
+ staticEvents: action.seedEvents ?? [],
335
336
  activeEvents: [],
336
337
  uiState: { kind: "IDLE" },
337
338
  clearCount: state.clearCount + 1,
@@ -694,8 +695,10 @@ export function reduceSessionState(state: SessionState, action: SessionAction):
694
695
 
695
696
  // ─── Hook ────────────────────────────────────────────────────────────────────
696
697
 
697
- export function useAppSessionState() {
698
- const [state, setState] = useState<SessionState>(createInitialSessionState);
698
+ export function useAppSessionState(initialStaticEvents?: () => TimelineEvent[]) {
699
+ const [state, setState] = useState<SessionState>(() =>
700
+ createInitialSessionState({ staticEvents: initialStaticEvents?.() ?? [] }),
701
+ );
699
702
  const queueRef = useRef<SessionAction[]>([]);
700
703
  const scheduledRef = useRef(false);
701
704
 
@@ -2,7 +2,7 @@ import { MAX_CHAT_LINES } from "../config/settings.js";
2
2
  import type { AvailableBackend } from "../config/settings.js";
3
3
  import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
4
4
  import type { BackendProgressUpdate } from "../core/providers/types.js";
5
- import { summarizeRunActivity, type RunFileActivity } from "../core/workspaceActivity.js";
5
+ import { summarizeRunActivity, type RunFileActivity } from "../core/workspace/workspaceActivity.js";
6
6
  import * as renderDebug from "../core/perf/renderDebug.js";
7
7
  import type {
8
8
  RunEvent,
@@ -1,5 +1,5 @@
1
1
  import type { BackendProgressUpdate } from "../core/providers/types.js";
2
- import type { RunFileActivity } from "../core/workspaceActivity.js";
2
+ import type { RunFileActivity } from "../core/workspace/workspaceActivity.js";
3
3
  import type { RunToolActivity } from "./types.js";
4
4
  import * as renderDebug from "../core/perf/renderDebug.js";
5
5
 
@@ -3,7 +3,7 @@ import type {
3
3
  AvailableBackend,
4
4
  } from "../config/settings.js";
5
5
  import type { ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
6
- import type { RunActivitySummary, RunFileActivity } from "../core/workspaceActivity.js";
6
+ import type { RunActivitySummary, RunFileActivity } from "../core/workspace/workspaceActivity.js";
7
7
 
8
8
  // ─── Screen routing ──────────────────────────────────────────────────────────
9
9
 
@@ -1,4 +1,11 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { resolveRuntimeConfig, type RuntimeConfig, DEFAULT_RUNTIME_CONFIG, type ResolvedRuntimeConfig } from "../config/runtimeConfig.js";
5
+ import type { ProviderId } from "../core/providerLauncher/types.js";
6
+ import { resetAnthropicRouteValidationCacheForTests } from "../core/providerRuntime/anthropic.js";
7
+ import { resetAntigravityRouteValidationCacheForTests } from "../core/providerRuntime/antigravity.js";
8
+ import type { ProviderModel } from "../core/providerRuntime/types.js";
2
9
 
3
10
  export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): ResolvedRuntimeConfig {
4
11
  return resolveRuntimeConfig({
@@ -12,3 +19,86 @@ export function makeResolvedRuntime(overrides: Partial<RuntimeConfig> = {}): Res
12
19
  }
13
20
 
14
21
  export const TEST_RUNTIME = makeResolvedRuntime();
22
+
23
+ function agyModel(id: string, label: string): ProviderModel {
24
+ return {
25
+ id,
26
+ modelId: id,
27
+ label,
28
+ description: null,
29
+ defaultReasoningLevel: null,
30
+ supportedReasoningLevels: null,
31
+ source: "discovered",
32
+ };
33
+ }
34
+
35
+ /**
36
+ * The catalog `agy models` reports on a configured machine. Antigravity resolves
37
+ * its status, backend kind, and model labels from whatever discovery last found,
38
+ * so tests that assert on any of those need a catalog seeded rather than
39
+ * whatever happens to be installed.
40
+ */
41
+ export const ANTIGRAVITY_FIXTURE_MODELS: readonly ProviderModel[] = [
42
+ agyModel("gemini-3.5-flash", "Gemini 3.5 Flash"),
43
+ agyModel("gemini-3.1-pro", "Gemini 3.1 Pro"),
44
+ agyModel("claude-sonnet-4-6", "Claude Sonnet 4.6"),
45
+ agyModel("claude-sonnet-4-6-think", "Claude Sonnet 4.6 (Thinking)"),
46
+ agyModel("gpt-oss-120b", "GPT-OSS 120B"),
47
+ ];
48
+
49
+ /**
50
+ * Drops every provider's in-memory discovery result.
51
+ *
52
+ * Bun shares the module registry across test files, so a catalog discovered by
53
+ * one test (some probe the real `claude` / `agy` CLIs) otherwise survives into
54
+ * the next and takes precedence over both the cache and the fallback list —
55
+ * making assertions depend on file order and on what is installed locally.
56
+ */
57
+ export function resetProviderDiscoveryState(): void {
58
+ resetAntigravityRouteValidationCacheForTests();
59
+ resetAnthropicRouteValidationCacheForTests();
60
+ }
61
+
62
+ /**
63
+ * Runs `fn` with HOME pointed at a throwaway directory holding only the provider
64
+ * model cache built from `seed`.
65
+ *
66
+ * Provider discovery reads `~/.codexa-model-cache.json`, so a test that calls it
67
+ * without this is asserting against the developer's own machine — it passes with
68
+ * a warm cache and fails on a cold one (or in CI). HOME is read per call (see
69
+ * `getProviderModelCacheFile`), so redirecting the env var here is enough.
70
+ *
71
+ * Antigravity's in-memory discovery is reset around the body as well: Bun shares
72
+ * the module registry across test files, so a catalog discovered by one file
73
+ * otherwise leaks into the next and takes precedence over the seeded cache,
74
+ * making results depend on file order. Seed exactly what the assertions need.
75
+ */
76
+ export function withSeededModelCache<T>(
77
+ seed: Partial<Record<ProviderId, readonly ProviderModel[]>>,
78
+ fn: () => T,
79
+ ): T {
80
+ const tempHome = mkdtempSync(join(tmpdir(), "codexa-test-home-"));
81
+ const previousHome = process.env.HOME;
82
+ const previousUserProfile = process.env.USERPROFILE;
83
+ try {
84
+ process.env.HOME = tempHome;
85
+ delete process.env.USERPROFILE;
86
+ const providers = Object.fromEntries(
87
+ Object.entries(seed).map(([providerId, models]) => [providerId, { discoveredAt: 1, models }]),
88
+ );
89
+ writeFileSync(
90
+ join(tempHome, ".codexa-model-cache.json"),
91
+ `${JSON.stringify({ version: 1, providers }, null, 2)}\n`,
92
+ "utf8",
93
+ );
94
+ resetProviderDiscoveryState();
95
+ return fn();
96
+ } finally {
97
+ if (previousHome === undefined) delete process.env.HOME;
98
+ else process.env.HOME = previousHome;
99
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
100
+ else process.env.USERPROFILE = previousUserProfile;
101
+ rmSync(tempHome, { recursive: true, force: true });
102
+ resetProviderDiscoveryState();
103
+ }
104
+ }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  // ─── WaveBar ─────────────────────────────────────────────────────────────────
6
6
  // Audio-visualizer style wave. Renders as a single <Text> node to minimise
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Text } from "ink";
3
- import type { UIState, ExternalCliStatus } from "../session/types.js";
4
- import { useTheme } from "./theme.js";
3
+ import type { UIState, ExternalCliStatus } from "../../session/types.js";
4
+ import { useTheme } from "../theme.js";
5
5
 
6
6
  const SPINNER_FRAMES = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
7
7
  const STREAMING_FRAMES = ["?", "?", "?", "?"];
@@ -36,7 +36,7 @@ export function ActivityIndicator({ uiState, externalCliStatus = "idle" }: Activ
36
36
  let bold = false;
37
37
 
38
38
  if (isError) {
39
- glyph = "�";
39
+ glyph = "�";
40
40
  color = theme.error;
41
41
  bold = true;
42
42
  } else if (isAction) {
@@ -1,8 +1,8 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import * as renderDebug from "../core/perf/renderDebug.js";
4
- import { useTheme } from "./theme.js";
5
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
3
+ import * as renderDebug from "../../core/perf/renderDebug.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
6
6
  import { BUSY_STATUS_FRAME_MS, BUSY_STATUS_FRAMES, getBusyStatusFrame } from "./busyStatusAnimation.js";
7
7
 
8
8
  interface AnimatedStatusTextProps {