@golba98/codexa 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +533 -275
  5. package/src/commands/handler.ts +2 -2
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -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,19 +7,16 @@ 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";
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";
23
20
 
24
21
  type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
25
22
  const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
@@ -27,36 +24,6 @@ const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
27
24
  flags: ["disambiguateEscapeCodes"],
28
25
  };
29
26
 
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
- }
59
-
60
27
  export interface AppStdout {
61
28
  isTTY: boolean;
62
29
  write: (chunk: string) => boolean;
@@ -74,7 +41,7 @@ export interface StartAppDependencies {
74
41
  platform: NodeJS.Platform;
75
42
  argv: string[];
76
43
  renderApp: (node: React.ReactElement, options?: RenderOptions) => RenderHandle;
77
- resolveInkInstanceForStdout: (stdout: AppStdout) => InkInstance | null;
44
+ resolveInkInstanceForStdout: (stdout: AppStdout) => InkRenderInstance | null;
78
45
  registerExitHandler: (handler: () => void) => void;
79
46
  }
80
47
 
@@ -112,7 +79,7 @@ export function startApp({
112
79
  platform = process.platform,
113
80
  argv = process.argv.slice(2),
114
81
  renderApp = render,
115
- resolveInkInstanceForStdout = resolveInkInstance,
82
+ resolveInkInstanceForStdout = resolveInkRenderInstance,
116
83
  registerExitHandler = (handler) => {
117
84
  process.on("exit", handler);
118
85
  },
@@ -123,7 +90,11 @@ export function startApp({
123
90
  return { started: true, exitCode: 0 };
124
91
  }
125
92
 
126
- const terminal = createTerminalModeController((chunk) => stdout.write(chunk));
93
+ // Wrap stdout to implement frame locking, deduplication, and width-safe row
94
+ // padding via \x1b[K injection across the entire TUI.
95
+ const wrappedStdout = wrapStdoutWithFrameLock({ stdout, env });
96
+
97
+ const terminal = createTerminalModeController((chunk) => wrappedStdout.write(chunk));
127
98
  const writeStdout = (chunk: string, source: string): boolean => terminal.write(chunk, source);
128
99
 
129
100
  const writeStderr = (chunk: string, source: string): boolean => {
@@ -163,56 +134,38 @@ export function startApp({
163
134
  return { started: false, exitCode: 1 };
164
135
  }
165
136
  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();
137
+ // Main chat starts in the normal screen buffer so the startup frame and later
138
+ // transcript rows are owned by native terminal scrollback. Overlay screens
139
+ // enter alternate-screen mode from app.tsx only while an overlay is active.
140
+ const startupWorkspaceRoot = resolveWorkspaceRoot();
191
141
  const startupSettings = loadSettings();
192
142
  const startupTitle =
193
143
  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
-
144
+ setIntendedTerminalTitle(startupTitle, {
145
+ force: true,
146
+ reason: "startup-title",
147
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.title"),
148
+ });
149
+ terminal.setBracketedPaste(true, "src/index.tsx:startup.bracketedPaste");
150
+
205
151
  let cleanupDone = false;
206
152
  let repaintArmed = false;
207
153
  let renderHandle: RenderHandle | null = null;
208
- let inkInstance: InkInstance | null = null;
209
-
210
- const onResize = () => {
211
- renderDebug.traceEvent("terminal", "resize", {
154
+ let inkInstance: InkRenderInstance | null = null;
155
+ let previousResizeCols = stdout.columns;
156
+ let previousResizeRows = stdout.rows;
157
+
158
+ const onResize = () => {
159
+ resetFrameLockForResize(wrappedStdout);
160
+ renderDebug.traceEvent("terminal", "resize", {
161
+ previousCols: previousResizeCols,
162
+ previousRows: previousResizeRows,
212
163
  cols: stdout.columns,
213
164
  rows: stdout.rows,
214
165
  invalid: hasInvalidRestoreDimensions(stdout),
215
166
  });
167
+ previousResizeCols = stdout.columns;
168
+ previousResizeRows = stdout.rows;
216
169
 
217
170
  if (hasInvalidRestoreDimensions(stdout)) {
218
171
  // Transient invalid dimensions (e.g. during maximize/restore on
@@ -234,12 +187,12 @@ export function startApp({
234
187
  const cleanup = () => {
235
188
  if (cleanupDone) return;
236
189
  cleanupDone = true;
237
- stopStartupTitleGuard();
238
- stdout.off("resize", onResize);
190
+ stdout.off("resize", onResize);
239
191
  renderHandle?.cleanup();
240
192
  // Restore terminal state: disable mouse reporting and bracketed paste.
241
193
  terminal.setMouseReporting(false, "src/index.tsx:cleanup.mouse");
242
194
  terminal.setBracketedPaste(false, "src/index.tsx:cleanup.bracketedPaste");
195
+ terminal.setAlternateScreen(false, "src/index.tsx:cleanup.alternateScreen");
243
196
  terminal.resetModes();
244
197
  activeRoot = null;
245
198
  };
@@ -271,11 +224,12 @@ export function startApp({
271
224
 
272
225
  renderHandle = renderApp(<App launchArgs={launchArgs} />, {
273
226
  kittyKeyboard: KITTY_KEYBOARD_OPTIONS,
227
+ stdout: wrappedStdout as any,
274
228
  });
275
229
 
276
230
  // Resolve the real Ink class instance to get access to lastOutput,
277
231
  // onRender, calculateLayout, etc. Gracefully degrades to null in tests.
278
- inkInstance = resolveInkInstanceForStdout(stdout);
232
+ inkInstance = resolveInkInstanceForStdout(wrappedStdout);
279
233
 
280
234
  // Remove Ink's own resize handler so the app is the sole resize handler.
281
235
  // 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