@golba98/codexa 1.0.1

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 (226) hide show
  1. package/README.md +320 -0
  2. package/bin/codexa.js +445 -0
  3. package/package.json +45 -0
  4. package/scripts/audit-codexa-capabilities.mjs +466 -0
  5. package/scripts/smoke-terminal-bench.mjs +35 -0
  6. package/src/app.tsx +4561 -0
  7. package/src/appRenderStability.test.ts +131 -0
  8. package/src/commands/handler.test.ts +643 -0
  9. package/src/commands/handler.ts +875 -0
  10. package/src/config/launchArgs.test.ts +158 -0
  11. package/src/config/launchArgs.ts +186 -0
  12. package/src/config/layeredConfig.test.ts +143 -0
  13. package/src/config/layeredConfig.ts +836 -0
  14. package/src/config/persistence.test.ts +110 -0
  15. package/src/config/persistence.ts +311 -0
  16. package/src/config/runtimeConfig.test.ts +218 -0
  17. package/src/config/runtimeConfig.ts +554 -0
  18. package/src/config/settings.test.ts +155 -0
  19. package/src/config/settings.ts +401 -0
  20. package/src/config/toml-serialize.ts +98 -0
  21. package/src/config/trustStore.test.ts +29 -0
  22. package/src/config/trustStore.ts +68 -0
  23. package/src/core/attachments.test.ts +155 -0
  24. package/src/core/attachments.ts +71 -0
  25. package/src/core/auth/codexAuth.test.ts +68 -0
  26. package/src/core/auth/codexAuth.ts +359 -0
  27. package/src/core/cleanupFastFail.test.ts +76 -0
  28. package/src/core/cleanupFastFail.ts +67 -0
  29. package/src/core/clipboard.ts +24 -0
  30. package/src/core/codex.ts +124 -0
  31. package/src/core/codexExecArgs.test.ts +195 -0
  32. package/src/core/codexExecArgs.ts +152 -0
  33. package/src/core/codexLaunch.test.ts +205 -0
  34. package/src/core/codexLaunch.ts +162 -0
  35. package/src/core/codexPrompt.test.ts +252 -0
  36. package/src/core/codexPrompt.ts +428 -0
  37. package/src/core/executables/claudeExecutable.ts +63 -0
  38. package/src/core/executables/codexExecutable.test.ts +212 -0
  39. package/src/core/executables/codexExecutable.ts +159 -0
  40. package/src/core/executables/executableResolver.test.ts +129 -0
  41. package/src/core/executables/executableResolver.ts +138 -0
  42. package/src/core/executables/geminiExecutable.test.ts +116 -0
  43. package/src/core/executables/geminiExecutable.ts +78 -0
  44. package/src/core/executables/pathSanityScan.test.ts +47 -0
  45. package/src/core/githubDiagnostics.test.ts +92 -0
  46. package/src/core/githubDiagnostics.ts +222 -0
  47. package/src/core/hollowResponseFormat.test.ts +58 -0
  48. package/src/core/hollowResponseFormat.ts +39 -0
  49. package/src/core/inputDebug.ts +51 -0
  50. package/src/core/launchContext.test.ts +157 -0
  51. package/src/core/launchContext.ts +266 -0
  52. package/src/core/models/codexCapabilities.test.ts +45 -0
  53. package/src/core/models/codexCapabilities.ts +95 -0
  54. package/src/core/models/codexModelCapabilities.test.ts +246 -0
  55. package/src/core/models/codexModelCapabilities.ts +571 -0
  56. package/src/core/models/modelSpecs.test.ts +283 -0
  57. package/src/core/models/modelSpecs.ts +300 -0
  58. package/src/core/perf/profiler.ts +125 -0
  59. package/src/core/perf/renderDebug.test.ts +230 -0
  60. package/src/core/perf/renderDebug.ts +373 -0
  61. package/src/core/planStorage.test.ts +143 -0
  62. package/src/core/planStorage.ts +141 -0
  63. package/src/core/process/CommandRunner.test.ts +105 -0
  64. package/src/core/process/CommandRunner.ts +269 -0
  65. package/src/core/process/processValidation.ts +101 -0
  66. package/src/core/projectInstructions.test.ts +50 -0
  67. package/src/core/projectInstructions.ts +54 -0
  68. package/src/core/providerLauncher/launcher.test.ts +238 -0
  69. package/src/core/providerLauncher/launcher.ts +203 -0
  70. package/src/core/providerLauncher/registry.test.ts +324 -0
  71. package/src/core/providerLauncher/registry.ts +253 -0
  72. package/src/core/providerLauncher/types.ts +84 -0
  73. package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
  74. package/src/core/providerLauncher/workspaceConfig.ts +407 -0
  75. package/src/core/providerRuntime/anthropic.test.ts +1120 -0
  76. package/src/core/providerRuntime/anthropic.ts +576 -0
  77. package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
  78. package/src/core/providerRuntime/capabilityProfile.ts +288 -0
  79. package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
  80. package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
  81. package/src/core/providerRuntime/contextMetadata.ts +409 -0
  82. package/src/core/providerRuntime/gemini.test.ts +437 -0
  83. package/src/core/providerRuntime/gemini.ts +784 -0
  84. package/src/core/providerRuntime/lmstudio.test.ts +168 -0
  85. package/src/core/providerRuntime/lmstudio.ts +118 -0
  86. package/src/core/providerRuntime/local.test.ts +787 -0
  87. package/src/core/providerRuntime/local.ts +754 -0
  88. package/src/core/providerRuntime/models.ts +150 -0
  89. package/src/core/providerRuntime/reasoning.ts +17 -0
  90. package/src/core/providerRuntime/registry.test.ts +233 -0
  91. package/src/core/providerRuntime/registry.ts +203 -0
  92. package/src/core/providerRuntime/types.ts +103 -0
  93. package/src/core/providers/codexJsonStream.test.ts +148 -0
  94. package/src/core/providers/codexJsonStream.ts +305 -0
  95. package/src/core/providers/codexSubprocess.test.ts +68 -0
  96. package/src/core/providers/codexSubprocess.ts +372 -0
  97. package/src/core/providers/codexTranscript.test.ts +284 -0
  98. package/src/core/providers/codexTranscript.ts +695 -0
  99. package/src/core/providers/openaiNative.ts +13 -0
  100. package/src/core/providers/registry.ts +21 -0
  101. package/src/core/providers/types.ts +59 -0
  102. package/src/core/terminal/terminalCapabilities.test.ts +93 -0
  103. package/src/core/terminal/terminalCapabilities.ts +100 -0
  104. package/src/core/terminal/terminalControl.test.ts +75 -0
  105. package/src/core/terminal/terminalControl.ts +147 -0
  106. package/src/core/terminal/terminalSanitize.test.ts +22 -0
  107. package/src/core/terminal/terminalSanitize.ts +147 -0
  108. package/src/core/terminal/terminalSelection.test.ts +42 -0
  109. package/src/core/terminal/terminalSelection.ts +66 -0
  110. package/src/core/terminal/terminalTitle.test.ts +328 -0
  111. package/src/core/terminal/terminalTitle.ts +483 -0
  112. package/src/core/workspaceActivity.test.ts +163 -0
  113. package/src/core/workspaceActivity.ts +380 -0
  114. package/src/core/workspaceGuard.test.ts +151 -0
  115. package/src/core/workspaceGuard.ts +288 -0
  116. package/src/core/workspaceRoot.test.ts +23 -0
  117. package/src/core/workspaceRoot.ts +47 -0
  118. package/src/exec.test.ts +13 -0
  119. package/src/exec.ts +72 -0
  120. package/src/headless/execArgs.test.ts +147 -0
  121. package/src/headless/execArgs.ts +294 -0
  122. package/src/headless/execRunner.test.ts +434 -0
  123. package/src/headless/execRunner.ts +304 -0
  124. package/src/index.test.tsx +618 -0
  125. package/src/index.tsx +296 -0
  126. package/src/session/appSession.test.ts +897 -0
  127. package/src/session/appSession.ts +761 -0
  128. package/src/session/chatLifecycle.test.ts +64 -0
  129. package/src/session/chatLifecycle.ts +951 -0
  130. package/src/session/liveRenderScheduler.test.ts +201 -0
  131. package/src/session/liveRenderScheduler.ts +214 -0
  132. package/src/session/planFlow.test.ts +103 -0
  133. package/src/session/planFlow.ts +149 -0
  134. package/src/session/planTranscript.test.ts +65 -0
  135. package/src/session/planTranscript.ts +15 -0
  136. package/src/session/promptRunSchedule.test.ts +36 -0
  137. package/src/session/promptRunSchedule.ts +26 -0
  138. package/src/session/types.ts +228 -0
  139. package/src/test/runtimeTestUtils.ts +14 -0
  140. package/src/types/react-dom.d.ts +3 -0
  141. package/src/ui/ActionRequiredBlock.tsx +38 -0
  142. package/src/ui/ActivityBars.tsx +68 -0
  143. package/src/ui/ActivityIndicator.test.tsx +58 -0
  144. package/src/ui/ActivityIndicator.tsx +58 -0
  145. package/src/ui/AgentBlock.test.ts +6 -0
  146. package/src/ui/AgentBlock.tsx +130 -0
  147. package/src/ui/AnimatedStatusText.test.ts +16 -0
  148. package/src/ui/AnimatedStatusText.tsx +69 -0
  149. package/src/ui/AppShell.test.tsx +1739 -0
  150. package/src/ui/AppShell.tsx +698 -0
  151. package/src/ui/AttachmentImportPanel.test.tsx +204 -0
  152. package/src/ui/AttachmentImportPanel.tsx +98 -0
  153. package/src/ui/AuthPanel.tsx +113 -0
  154. package/src/ui/BackendPicker.tsx +28 -0
  155. package/src/ui/BottomComposer.test.ts +674 -0
  156. package/src/ui/BottomComposer.tsx +1028 -0
  157. package/src/ui/CodexLogo.tsx +55 -0
  158. package/src/ui/DashCard.tsx +82 -0
  159. package/src/ui/Markdown.test.ts +157 -0
  160. package/src/ui/Markdown.tsx +310 -0
  161. package/src/ui/ModePicker.tsx +27 -0
  162. package/src/ui/ModelPicker.tsx +31 -0
  163. package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
  164. package/src/ui/ModelPickerScreen.test.tsx +99 -0
  165. package/src/ui/ModelPickerScreen.tsx +416 -0
  166. package/src/ui/ModelPickerState.test.tsx +151 -0
  167. package/src/ui/ModelReasoningPicker.test.tsx +447 -0
  168. package/src/ui/ModelReasoningPicker.tsx +458 -0
  169. package/src/ui/Panel.tsx +51 -0
  170. package/src/ui/PermissionsPanel.tsx +78 -0
  171. package/src/ui/PlanActionPicker.tsx +119 -0
  172. package/src/ui/PlanReviewPanel.test.tsx +267 -0
  173. package/src/ui/PlanReviewPanel.tsx +212 -0
  174. package/src/ui/PromptCardBorder.test.tsx +161 -0
  175. package/src/ui/ProviderPicker.test.tsx +289 -0
  176. package/src/ui/ProviderPicker.tsx +321 -0
  177. package/src/ui/ProviderShortcut.test.tsx +143 -0
  178. package/src/ui/ReasoningPicker.tsx +46 -0
  179. package/src/ui/RunFooter.tsx +65 -0
  180. package/src/ui/SelectionPanel.tsx +67 -0
  181. package/src/ui/SettingsPanel.test.tsx +233 -0
  182. package/src/ui/SettingsPanel.tsx +156 -0
  183. package/src/ui/Spinner.tsx +25 -0
  184. package/src/ui/StaticIntroItem.tsx +54 -0
  185. package/src/ui/StaticTranscriptItem.tsx +56 -0
  186. package/src/ui/TextEntryPanel.tsx +139 -0
  187. package/src/ui/ThemePicker.tsx +31 -0
  188. package/src/ui/ThinkingBlock.tsx +100 -0
  189. package/src/ui/Timeline.test.ts +2067 -0
  190. package/src/ui/Timeline.tsx +1472 -0
  191. package/src/ui/TimelineNavigation.test.tsx +201 -0
  192. package/src/ui/TopHeader.test.tsx +239 -0
  193. package/src/ui/TopHeader.tsx +257 -0
  194. package/src/ui/TurnGroup.test.tsx +365 -0
  195. package/src/ui/TurnGroup.tsx +657 -0
  196. package/src/ui/busyStatusAnimation.test.ts +30 -0
  197. package/src/ui/busyStatusAnimation.ts +11 -0
  198. package/src/ui/commandNormalize.test.ts +142 -0
  199. package/src/ui/commandNormalize.ts +66 -0
  200. package/src/ui/diffRenderer.test.ts +102 -0
  201. package/src/ui/diffRenderer.ts +116 -0
  202. package/src/ui/focus.ts +61 -0
  203. package/src/ui/focusFlow.test.tsx +1098 -0
  204. package/src/ui/inputBuffer.test.ts +151 -0
  205. package/src/ui/inputBuffer.ts +203 -0
  206. package/src/ui/layout.test.ts +145 -0
  207. package/src/ui/layout.ts +287 -0
  208. package/src/ui/modeDisplay.test.ts +42 -0
  209. package/src/ui/modeDisplay.ts +52 -0
  210. package/src/ui/outputPipeline.ts +64 -0
  211. package/src/ui/progressEntries.ts +156 -0
  212. package/src/ui/runActivityView.test.ts +89 -0
  213. package/src/ui/runActivityView.ts +37 -0
  214. package/src/ui/runLifecycleView.test.tsx +237 -0
  215. package/src/ui/slashCommands.ts +41 -0
  216. package/src/ui/statusRenderIsolation.test.tsx +654 -0
  217. package/src/ui/terminalAnswerFormat.test.ts +19 -0
  218. package/src/ui/terminalAnswerFormat.ts +128 -0
  219. package/src/ui/textLayout.test.ts +18 -0
  220. package/src/ui/textLayout.ts +338 -0
  221. package/src/ui/theme.tsx +395 -0
  222. package/src/ui/themeFlow.test.ts +53 -0
  223. package/src/ui/themeFlow.ts +41 -0
  224. package/src/ui/timelineMeasure.ts +3088 -0
  225. package/src/ui/timelineMeasureCache.test.ts +986 -0
  226. package/src/ui/useThrottledValue.ts +31 -0
package/src/index.tsx ADDED
@@ -0,0 +1,296 @@
1
+ import React from "react";
2
+ import { render, type Instance, type RenderOptions } from "ink";
3
+ import { App } from "./app.js";
4
+ import { parseLaunchArgs, type LaunchArgs } from "./config/launchArgs.js";
5
+ import { loadSettings } from "./config/persistence.js";
6
+ import { APP_NAME, formatTerminalTitlePath } from "./config/settings.js";
7
+ import { getTerminalCapability } from "./core/terminal/terminalCapabilities.js";
8
+ import * as renderDebug from "./core/perf/renderDebug.js";
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
+
23
+ type RenderHandle = Pick<Instance, "clear" | "cleanup" | "waitUntilExit">;
24
+ const KITTY_KEYBOARD_OPTIONS: RenderOptions["kittyKeyboard"] = {
25
+ mode: "auto",
26
+ flags: ["disambiguateEscapeCodes"],
27
+ };
28
+
29
+ /**
30
+ * Typed subset of the internal Ink class instance we access to disable Ink's
31
+ * built-in resize listener. Post-startup repainting is driven by React layout
32
+ * state, not by forcing Ink's private render buffers.
33
+ */
34
+ interface InkInstance {
35
+ unsubscribeResize?: () => void;
36
+ }
37
+
38
+ /**
39
+ * Resolve the real Ink class instance via Ink's internal WeakMap<stdout, Ink>.
40
+ * Returns null if resolution fails (e.g. different Ink version, test mocks).
41
+ */
42
+ function resolveInkInstance(stdout: AppStdout): InkInstance | null {
43
+ try {
44
+ // Bun doesn't resolve bare subpath imports like "ink/build/instances.js"
45
+ // so we resolve ink's main entry first, then derive the sibling path.
46
+ const { createRequire } = require("node:module");
47
+ const req = createRequire(import.meta.url);
48
+ const inkMain = req.resolve("ink");
49
+ const instancesPath = inkMain.replace(/index\.js$/, "instances.js");
50
+ const instances = req(instancesPath);
51
+ const weakMap: WeakMap<object, InkInstance> =
52
+ instances.default ?? instances;
53
+ return weakMap.get(stdout as object) ?? null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export interface AppStdout {
60
+ isTTY: boolean;
61
+ write: (chunk: string) => boolean;
62
+ on: (event: "resize" | string, listener: (...args: unknown[]) => void) => void;
63
+ off: (event: "resize" | string, listener: (...args: unknown[]) => void) => void;
64
+ columns?: number;
65
+ rows?: number;
66
+ }
67
+
68
+ export interface StartAppDependencies {
69
+ stdin: Pick<NodeJS.ReadStream, "isTTY">;
70
+ stdout: AppStdout;
71
+ stderr: Pick<NodeJS.WriteStream, "write"> & Partial<Pick<NodeJS.WriteStream, "isTTY">>;
72
+ env: Record<string, string | undefined>;
73
+ platform: NodeJS.Platform;
74
+ argv: string[];
75
+ renderApp: (node: React.ReactElement, options?: RenderOptions) => RenderHandle;
76
+ resolveInkInstanceForStdout: (stdout: AppStdout) => InkInstance | null;
77
+ registerExitHandler: (handler: () => void) => void;
78
+ }
79
+
80
+ export interface StartAppResult {
81
+ started: boolean;
82
+ exitCode: number;
83
+ }
84
+
85
+ interface ActiveRootState {
86
+ cleanup: () => void;
87
+ }
88
+
89
+ let activeRoot: ActiveRootState | null = null;
90
+
91
+ function debugLaunch(env: Record<string, string | undefined>, write: (chunk: string, source: string) => boolean, fields: Record<string, unknown>): void {
92
+ if (env.CODEXA_DEBUG_LAUNCH !== "1") {
93
+ return;
94
+ }
95
+
96
+ write(`[codexa:launch] ${JSON.stringify(fields)}\n`, "src/index.tsx:launchDebug");
97
+ }
98
+
99
+ function hasInvalidRestoreDimensions(stdout: Pick<AppStdout, "columns" | "rows">): boolean {
100
+ const cols = stdout.columns;
101
+ const rows = stdout.rows;
102
+ return !Number.isFinite(cols) || !Number.isFinite(rows)
103
+ || (cols ?? 0) < MIN_VIEWPORT_COLS || (rows ?? 0) < MIN_VIEWPORT_ROWS;
104
+ }
105
+
106
+ export function startApp({
107
+ stdin = process.stdin,
108
+ stdout = process.stdout,
109
+ stderr = process.stderr,
110
+ env = process.env,
111
+ platform = process.platform,
112
+ argv = process.argv.slice(2),
113
+ renderApp = render,
114
+ resolveInkInstanceForStdout = resolveInkInstance,
115
+ registerExitHandler = (handler) => {
116
+ process.on("exit", handler);
117
+ },
118
+ }: Partial<StartAppDependencies> = {}): StartAppResult {
119
+ renderDebug.configureRenderDebug(env);
120
+
121
+ if (activeRoot) {
122
+ return { started: true, exitCode: 0 };
123
+ }
124
+
125
+ const terminal = createTerminalModeController((chunk) => stdout.write(chunk));
126
+ const writeStdout = (chunk: string, source: string): boolean => terminal.write(chunk, source);
127
+
128
+ const writeStderr = (chunk: string, source: string): boolean => {
129
+ return writeTerminalControl((value) => stderr.write(value), "stderr", source, chunk);
130
+ };
131
+
132
+ debugLaunch(env, writeStderr, {
133
+ phase: "startApp",
134
+ resolvedLaunchMode: "interactive-ui",
135
+ stdinIsTTY: Boolean(stdin.isTTY),
136
+ stdoutIsTTY: Boolean(stdout.isTTY),
137
+ stderrIsTTY: Boolean(stderr.isTTY),
138
+ TERM: env.TERM,
139
+ WT_SESSION: env.WT_SESSION,
140
+ TERM_PROGRAM: env.TERM_PROGRAM,
141
+ argv,
142
+ });
143
+
144
+ const capability = getTerminalCapability({
145
+ stdinIsTTY: Boolean(stdin.isTTY),
146
+ stdoutIsTTY: Boolean(stdout.isTTY),
147
+ platform,
148
+ env,
149
+ });
150
+
151
+ if (!capability.supported) {
152
+ writeStderr(`${capability.message}\n`, "src/index.tsx:unsupportedTerminal");
153
+ return { started: false, exitCode: 1 };
154
+ }
155
+ if (capability.warning) {
156
+ writeStderr(`${capability.warning}\n`, "src/index.tsx:terminalCapabilityWarning");
157
+ }
158
+
159
+ const parsedLaunchArgs = parseLaunchArgs(argv);
160
+ if (!parsedLaunchArgs.ok) {
161
+ writeStderr(`${parsedLaunchArgs.error}\n`, "src/index.tsx:launchArgs");
162
+ return { started: false, exitCode: 1 };
163
+ }
164
+ const launchArgs: LaunchArgs = parsedLaunchArgs.value;
165
+ setIntendedTerminalTitle(env.CODEXA_INITIAL_TERMINAL_TITLE ?? APP_NAME, {
166
+ force: true,
167
+ reason: "index-safe-fallback-title",
168
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:title.fallback"),
169
+ });
170
+ const stopStartupTitleGuard = startTerminalTitleStartupGuard({
171
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:title.startupGuard"),
172
+ reason: "index-startup-guard",
173
+ intervalMs: 150,
174
+ durationMs: 3500,
175
+ });
176
+
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");
186
+ const startupWorkspaceRoot = resolveWorkspaceRoot();
187
+ const startupSettings = loadSettings();
188
+ const startupTitle =
189
+ formatTerminalTitlePath(startupWorkspaceRoot, startupSettings.ui.terminalTitleMode) || APP_NAME;
190
+ setIntendedTerminalTitle(startupTitle, {
191
+ force: true,
192
+ reason: "startup-title",
193
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.title"),
194
+ });
195
+ reassertIntendedTerminalTitle({
196
+ write: (chunk) => writeStdout(chunk, "src/index.tsx:startup.titleReady"),
197
+ reason: "startup-title-ready",
198
+ });
199
+ terminal.setBracketedPaste(true, "src/index.tsx:startup.bracketedPaste");
200
+
201
+ let cleanupDone = false;
202
+ let repaintArmed = false;
203
+ let renderHandle: RenderHandle | null = null;
204
+ let inkInstance: InkInstance | null = null;
205
+
206
+ const onResize = () => {
207
+ renderDebug.traceEvent("terminal", "resize", {
208
+ cols: stdout.columns,
209
+ rows: stdout.rows,
210
+ invalid: hasInvalidRestoreDimensions(stdout),
211
+ });
212
+
213
+ if (hasInvalidRestoreDimensions(stdout)) {
214
+ // Transient invalid dimensions (e.g. during maximize/restore on
215
+ // Windows). Keep the previous frame visible and let useTerminalViewport
216
+ // preserve the last renderable layout until valid dimensions return.
217
+ repaintArmed = true;
218
+ return;
219
+ }
220
+
221
+ if (repaintArmed) {
222
+ repaintArmed = false;
223
+ }
224
+
225
+ // useTerminalViewport owns resize-driven React state. Do not clear the
226
+ // terminal, reset Ink output caches, or force Ink renders here; those
227
+ // imperative paths can produce a transient empty frame during streaming.
228
+ };
229
+
230
+ const cleanup = () => {
231
+ if (cleanupDone) return;
232
+ cleanupDone = true;
233
+ stopStartupTitleGuard();
234
+ stdout.off("resize", onResize);
235
+ renderHandle?.cleanup();
236
+ // Restore terminal state: disable mouse reporting and bracketed paste.
237
+ terminal.setMouseReporting(false, "src/index.tsx:cleanup.mouse");
238
+ terminal.setBracketedPaste(false, "src/index.tsx:cleanup.bracketedPaste");
239
+ terminal.resetModes();
240
+ activeRoot = null;
241
+ };
242
+
243
+ const handleFatal = (error: unknown) => {
244
+ cleanup();
245
+ if (error instanceof Error) {
246
+ writeStderr(`${error.stack || error.message}\n`, "src/index.tsx:fatalError");
247
+ } else if (error) {
248
+ writeStderr(`${String(error)}\n`, "src/index.tsx:fatalUnknown");
249
+ }
250
+ process.exit(1);
251
+ };
252
+
253
+ const handleSignal = () => {
254
+ cleanup();
255
+ process.exit(0);
256
+ };
257
+
258
+ stdout.on("resize", onResize);
259
+ registerExitHandler(cleanup);
260
+
261
+ // Ensure clean teardown on signals and unhandled failures to prevent
262
+ // leaving the terminal in mouse-reporting mode.
263
+ process.on("SIGINT", handleSignal);
264
+ process.on("SIGTERM", handleSignal);
265
+ process.on("uncaughtException", handleFatal);
266
+ process.on("unhandledRejection", handleFatal);
267
+
268
+ renderHandle = renderApp(<App launchArgs={launchArgs} />, {
269
+ kittyKeyboard: KITTY_KEYBOARD_OPTIONS,
270
+ });
271
+
272
+ // Resolve the real Ink class instance to get access to lastOutput,
273
+ // onRender, calculateLayout, etc. Gracefully degrades to null in tests.
274
+ inkInstance = resolveInkInstanceForStdout(stdout);
275
+
276
+ // Remove Ink's own resize handler so the app is the sole resize handler.
277
+ // This eliminates the race where Ink's resized() fires alongside our
278
+ // onResize, causing interleaved renders that desync logUpdate.
279
+ if (inkInstance?.unsubscribeResize) {
280
+ inkInstance.unsubscribeResize();
281
+ }
282
+
283
+ void renderHandle.waitUntilExit().finally(cleanup);
284
+
285
+ activeRoot = { cleanup };
286
+ return { started: true, exitCode: 0 };
287
+ }
288
+
289
+ const isMainModule = Boolean((import.meta as ImportMeta & { main?: boolean }).main);
290
+
291
+ if (isMainModule) {
292
+ const result = startApp();
293
+ if (!result.started) {
294
+ process.exitCode = result.exitCode;
295
+ }
296
+ }