parallel-codex-tui 0.1.3 → 0.1.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 (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
@@ -1,7 +1,6 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { basename } from "node:path";
3
1
  import { pathExists, pathIsDirectory } from "./core/file-store.js";
4
2
  import { listWorkspaceChoices, resolveWorkspacePath, resolveWorkspaceSelection } from "./core/workspace.js";
3
+ import { promptForWorkspaceTui } from "./cli-workspace-picker.js";
5
4
  export async function selectWorkspaceForCli(input) {
6
5
  if (input.explicitWorkspace?.trim()) {
7
6
  const explicit = resolveWorkspacePath(input.cwd, input.explicitWorkspace);
@@ -10,15 +9,15 @@ export async function selectWorkspaceForCli(input) {
10
9
  const explicitExists = await pathExists(explicit);
11
10
  const explicitIsDirectory = explicitExists && (await pathIsDirectory(explicit));
12
11
  if (!explicitIsDirectory && input.interactive !== false && shouldPromptForWorkspace(stdin, stdout)) {
13
- return promptForWorkspace({
12
+ return promptForWorkspaceTui({
14
13
  cwd: input.cwd,
15
14
  choices: await listWorkspaceChoices(input.appRoot),
16
15
  invalidExplicitWorkspace: {
17
16
  path: explicit,
18
17
  reason: explicitExists ? "file" : "missing"
19
18
  },
20
- stdin: stdin,
21
- stdout: stdout
19
+ stdin,
20
+ stdout
22
21
  });
23
22
  }
24
23
  return resolveWorkspaceSelection(input);
@@ -29,76 +28,13 @@ export async function selectWorkspaceForCli(input) {
29
28
  if (input.interactive === false || !shouldPromptForWorkspace(stdin, stdout)) {
30
29
  return resolveWorkspaceSelection(input);
31
30
  }
32
- return promptForWorkspace({
31
+ return promptForWorkspaceTui({
33
32
  cwd: input.cwd,
34
33
  choices,
35
- stdin: stdin,
36
- stdout: stdout
34
+ stdin,
35
+ stdout
37
36
  });
38
37
  }
39
38
  function shouldPromptForWorkspace(stdin, stdout) {
40
39
  return Boolean(stdin.isTTY && stdout.isTTY);
41
40
  }
42
- async function promptForWorkspace(input) {
43
- if (input.invalidExplicitWorkspace?.reason === "missing") {
44
- input.stdout.write(`Workspace does not exist: ${input.invalidExplicitWorkspace.path}\n`);
45
- }
46
- else if (input.invalidExplicitWorkspace?.reason === "file") {
47
- input.stdout.write(`Workspace is not a directory: ${input.invalidExplicitWorkspace.path}\n`);
48
- }
49
- if (input.choices.length === 0) {
50
- input.stdout.write("No workspace selected yet.\n");
51
- }
52
- else {
53
- input.stdout.write("Select workspace:\n");
54
- for (const [index, choice] of input.choices.entries()) {
55
- input.stdout.write(` ${index + 1}. ${workspaceLabel(choice)}\n`);
56
- }
57
- input.stdout.write(" n. Create/open another folder\n");
58
- }
59
- const rl = createInterface({ input: input.stdin, output: input.stdout });
60
- try {
61
- const defaultWorkspace = createableDefaultWorkspace(input.invalidExplicitWorkspace);
62
- if (input.choices.length === 0) {
63
- return await promptForNewWorkspace(rl, input.cwd, defaultWorkspace);
64
- }
65
- const defaultLabel = defaultWorkspace ? `${defaultWorkspace}, ` : "";
66
- const answer = (await rl.question(`Workspace [${defaultLabel}1/${input.choices.length}, n]: `)).trim();
67
- if (!answer && defaultWorkspace) {
68
- return defaultWorkspace;
69
- }
70
- if (!answer || answer === "1") {
71
- return input.choices[0]?.path ?? input.cwd;
72
- }
73
- const newWorkspaceMatch = answer.match(/^n(?:ew)?\s+(.+)$/i);
74
- if (newWorkspaceMatch?.[1]?.trim()) {
75
- return resolveWorkspacePath(input.cwd, newWorkspaceMatch[1].trim());
76
- }
77
- if (/^n(?:ew)?$/i.test(answer)) {
78
- return await promptForNewWorkspace(rl, input.cwd, defaultWorkspace);
79
- }
80
- const index = Number.parseInt(answer, 10);
81
- if (Number.isInteger(index) && index >= 1 && index <= input.choices.length) {
82
- return input.choices[index - 1]?.path ?? input.cwd;
83
- }
84
- return resolveWorkspacePath(input.cwd, answer);
85
- }
86
- finally {
87
- rl.close();
88
- }
89
- }
90
- async function promptForNewWorkspace(rl, cwd, defaultWorkspace) {
91
- const prompt = defaultWorkspace ? `Workspace path [${defaultWorkspace}]: ` : "Workspace path: ";
92
- const answer = (await rl.question(prompt)).trim();
93
- if (answer) {
94
- return resolveWorkspacePath(cwd, answer);
95
- }
96
- return defaultWorkspace ?? cwd;
97
- }
98
- function createableDefaultWorkspace(invalidExplicitWorkspace) {
99
- return invalidExplicitWorkspace?.reason === "missing" ? invalidExplicitWorkspace.path : undefined;
100
- }
101
- function workspaceLabel(choice) {
102
- const marker = choice.exists ? "" : " (will create)";
103
- return `${basename(choice.path) || choice.path} ${choice.path}${marker}`;
104
- }
package/dist/cli.js CHANGED
@@ -1,29 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { join } from "node:path";
3
4
  import { render } from "ink";
4
5
  import { ZodError } from "zod";
5
6
  import { parseCliArgs, validateCliArgs } from "./cli-args.js";
6
7
  import { selectWorkspaceForCli } from "./cli-workspace.js";
8
+ import { commitWorkspaceTransition } from "./cli-workspace-transition.js";
9
+ import { WorkspaceSelectionCancelledError } from "./cli-workspace-picker.js";
10
+ import { startupRecoveryMessages } from "./cli-startup-recovery.js";
11
+ import { startupPreflightMessages } from "./cli-startup-preflight.js";
7
12
  import { createRuntime } from "./bootstrap.js";
8
13
  import { prepareAppRoot } from "./core/app-root.js";
9
- import { configPath, writeDefaultConfig } from "./core/config.js";
14
+ import { formatConfigErrorMessage } from "./core/config-errors.js";
15
+ import { configPath, loadConfig, withUiThemeOverride, writeDefaultConfig } from "./core/config.js";
10
16
  import { pathExists } from "./core/file-store.js";
11
- import { runDoctor } from "./doctor.js";
17
+ import { readRouterAudit } from "./core/router-audit.js";
18
+ import { loadTaskSessionDetails as loadPersistedTaskSessionDetails } from "./core/task-session-details.js";
19
+ import { listWorkspaceChoices } from "./core/workspace.js";
20
+ import { runDoctor, runRuntimePreflight } from "./doctor.js";
21
+ import { helpText } from "./cli-help.js";
12
22
  import { App } from "./tui/App.js";
23
+ import { formatTuiThemeCatalog } from "./tui/theme-preview.js";
24
+ import { configureTuiTheme } from "./tui/theme.js";
25
+ import { routerDiagnosticsPolicy } from "./tui/RouterDiagnosticsView.js";
13
26
  import { version } from "./version.js";
14
- const helpText = `Usage: parallel-codex-tui [options]
15
-
16
- Options:
17
- -w, --workspace <path> Project workspace for worker sessions and edits
18
- --app-root <path> App root for configuration lookup
19
- -t, --task <id> Open an existing task session
20
- --init Write .parallel-codex/config.toml if missing
21
- --doctor Check local configuration and agent commands
22
- -v, --version Print the current version
23
- -h, --help Print this help message
24
-
25
- Options with values also accept --name=value and -x=value forms.`;
26
27
  main().catch((error) => {
28
+ if (error instanceof WorkspaceSelectionCancelledError) {
29
+ return;
30
+ }
27
31
  process.stderr.write(`${formatStartupError(error)}\n`);
28
32
  process.exit(1);
29
33
  });
@@ -35,7 +39,7 @@ async function main() {
35
39
  process.exit(1);
36
40
  }
37
41
  const cliArgs = parseCliArgs(rawArgs, process.cwd());
38
- if (!cliArgs.help && !cliArgs.version) {
42
+ if (!cliArgs.help && !cliArgs.themes && !cliArgs.version) {
39
43
  await prepareAppRoot(cliArgs.appRoot);
40
44
  }
41
45
  const localConfigPath = configPath(cliArgs.appRoot);
@@ -45,6 +49,9 @@ async function main() {
45
49
  else if (cliArgs.version) {
46
50
  console.log(`parallel-codex-tui ${version}`);
47
51
  }
52
+ else if (cliArgs.themes) {
53
+ console.log(formatTuiThemeCatalog(cliArgs.theme ? [cliArgs.theme] : undefined).join("\n"));
54
+ }
48
55
  else if (cliArgs.doctor) {
49
56
  const workspaceRoot = await selectWorkspaceForCli({
50
57
  appRoot: cliArgs.appRoot,
@@ -52,7 +59,11 @@ async function main() {
52
59
  explicitWorkspace: cliArgs.explicitWorkspace,
53
60
  interactive: false
54
61
  });
55
- const result = await runDoctor(cliArgs.appRoot, workspaceRoot);
62
+ const result = await runDoctor(cliArgs.appRoot, workspaceRoot, process.env, {
63
+ probeAgents: cliArgs.probeAgents,
64
+ probeRouter: cliArgs.probeRouter,
65
+ theme: cliArgs.theme
66
+ });
56
67
  process.stdout.write(result.text);
57
68
  process.exitCode = result.ok ? 0 : 1;
58
69
  }
@@ -66,28 +77,227 @@ async function main() {
66
77
  }
67
78
  }
68
79
  else {
80
+ const startupConfig = await loadConfig(cliArgs.appRoot);
81
+ configureTuiTheme({
82
+ theme: cliArgs.theme ?? startupConfig.ui.theme,
83
+ colors: startupConfig.ui.colors
84
+ });
69
85
  const workspaceRoot = await selectWorkspaceForCli({
70
86
  appRoot: cliArgs.appRoot,
71
87
  cwd: process.cwd(),
72
88
  explicitWorkspace: cliArgs.explicitWorkspace
73
89
  });
74
- const runtime = await createRuntime(cliArgs.appRoot, workspaceRoot);
75
- if (cliArgs.taskId && !(await runtime.sessions.hasTask(cliArgs.taskId))) {
76
- throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${cliArgs.taskId}`);
77
- }
90
+ let current = await loadInteractiveWorkspace(cliArgs.appRoot, workspaceRoot, cliArgs.taskId);
78
91
  if (!canRenderInteractiveTui()) {
92
+ current.runtime.index.close();
79
93
  throw new Error("parallel-codex-tui requires an interactive terminal. Use --help, --version, --init, or --doctor for non-interactive command modes.");
80
94
  }
81
- const latestTask = await runtime.sessions.latestTask();
82
- const initialTaskId = cliArgs.taskId ?? latestTask?.id ?? null;
83
- render(_jsx(App, { config: runtime.config, orchestrator: runtime.orchestrator, cwd: runtime.workspaceRoot, initialTaskId: initialTaskId }), { exitOnCtrlC: false });
95
+ let instance = null;
96
+ const shutdownController = new AbortController();
97
+ const deferredWorkspaceClosures = new Set();
98
+ const appElement = (state) => (_jsx(App, { config: withUiThemeOverride(state.runtime.config, cliArgs.theme), orchestrator: state.runtime.orchestrator, cwd: state.runtime.workspaceRoot, initialTaskId: state.initialTaskId, initialRoute: state.initialRoute, initialWorkers: state.initialWorkers, initialCanRetryTask: state.initialCanRetryTask, initialMessages: state.initialMessages, workspaceChoices: state.workspaceChoices, shutdownSignal: shutdownController.signal, loadRouterDiagnostics: async () => {
99
+ const [records, latestConfig] = await Promise.all([
100
+ readRouterAudit(join(state.runtime.routerCwd, "routes.jsonl"), 100),
101
+ loadConfig(cliArgs.appRoot)
102
+ ]);
103
+ return {
104
+ records,
105
+ policy: routerDiagnosticsPolicy(latestConfig.router)
106
+ };
107
+ }, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), loadTaskSessionDetails: (task) => loadPersistedTaskSessionDetails({
108
+ task,
109
+ taskDir: state.runtime.sessions.taskFromId(task.id).dir,
110
+ modelNames: Object.fromEntries(Object.entries(state.runtime.config.workers).map(([id, worker]) => [id, worker.model.name]))
111
+ }), renameTaskSession: async (taskId, title) => {
112
+ await state.runtime.sessions.renameTask(taskId, title);
113
+ }, setTaskSessionArchived: async (taskId, archived) => {
114
+ await state.runtime.sessions.setTaskArchived(taskId, archived);
115
+ }, deleteTaskSession: async (taskId) => {
116
+ await state.runtime.sessions.deleteTask(taskId);
117
+ }, exportTaskSession: async (taskId) => (await state.runtime.sessions.exportTask(taskId)).path, loadCollaborationTimeline: (taskId) => state.runtime.sessions.readCollaborationTimeline(taskId), activateTaskSession: async (taskId) => {
118
+ if (!taskId) {
119
+ await state.runtime.index.setActiveTaskId(null);
120
+ return null;
121
+ }
122
+ if (!(await state.runtime.sessions.hasTask(taskId))) {
123
+ throw new Error(`Task session not found in workspace ${state.runtime.workspaceRoot}: ${taskId}`);
124
+ }
125
+ const task = state.runtime.sessions.taskFromId(taskId);
126
+ const meta = await state.runtime.sessions.readMeta(task);
127
+ if (meta.archived_at) {
128
+ throw new Error(`Task session is archived: ${taskId}`);
129
+ }
130
+ const [route, workers, canRetry] = await Promise.all([
131
+ state.runtime.sessions.readLatestRoute(task),
132
+ state.runtime.orchestrator.listTaskWorkers(taskId),
133
+ state.runtime.orchestrator.canRetryTask(taskId)
134
+ ]);
135
+ await state.runtime.index.setActiveTaskId(taskId);
136
+ return { taskId, route, workers, canRetry };
137
+ }, switchWorkspace: async (workspace) => {
138
+ if (workspace === current.runtime.workspaceRoot) {
139
+ return;
140
+ }
141
+ retryDeferredWorkspaceClosures(deferredWorkspaceClosures);
142
+ const next = await loadInteractiveWorkspace(cliArgs.appRoot, workspace, null);
143
+ const previous = current;
144
+ current = commitWorkspaceTransition({
145
+ previous,
146
+ next,
147
+ render: (state) => {
148
+ if (!instance) {
149
+ throw new Error("Interactive TUI is not ready to switch workspaces.");
150
+ }
151
+ instance.rerender(appElement(state));
152
+ },
153
+ close: closeInteractiveWorkspace,
154
+ deferClose: (state) => deferredWorkspaceClosures.add(state)
155
+ });
156
+ }, persistChatMessage: (message, taskId) => state.runtime.sessions.appendChatMessage({
157
+ ...message,
158
+ taskId
159
+ }) }, state.runtime.workspaceRoot));
160
+ const removeSigintHandler = installInteractiveSigintExitHandler(() => shutdownController.abort());
161
+ try {
162
+ instance = render(appElement(current), { exitOnCtrlC: false });
163
+ await instance.waitUntilExit();
164
+ }
165
+ finally {
166
+ removeSigintHandler();
167
+ retryDeferredWorkspaceClosures(deferredWorkspaceClosures);
168
+ }
169
+ }
170
+ }
171
+ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId) {
172
+ const runtime = await createRuntime(appRoot, workspaceRoot);
173
+ try {
174
+ const preflightPromise = runRuntimePreflight(runtime.config, runtime.workspaceRoot, process.env).catch((error) => ({
175
+ ok: false,
176
+ lines: [`preflight: failed (${error instanceof Error ? error.message : String(error)})`]
177
+ }));
178
+ if (requestedTaskId) {
179
+ if (!(await runtime.sessions.hasTask(requestedTaskId))) {
180
+ throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
181
+ }
182
+ const requestedMeta = await runtime.sessions.readMeta(runtime.sessions.taskFromId(requestedTaskId));
183
+ if (requestedMeta.archived_at) {
184
+ throw new Error(`Task session is archived in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
185
+ }
186
+ }
187
+ const [latestTask, rememberedTaskId] = await Promise.all([
188
+ runtime.sessions.latestTask(),
189
+ runtime.index.activeTaskId()
190
+ ]);
191
+ const rememberedTaskIsRestorable = typeof rememberedTaskId === "string"
192
+ && await runtime.sessions.hasTask(rememberedTaskId)
193
+ && !(await runtime.sessions.readMeta(runtime.sessions.taskFromId(rememberedTaskId))).archived_at;
194
+ let initialTaskId;
195
+ if (requestedTaskId) {
196
+ initialTaskId = requestedTaskId;
197
+ }
198
+ else if (rememberedTaskId === null) {
199
+ initialTaskId = null;
200
+ }
201
+ else if (rememberedTaskId && rememberedTaskIsRestorable) {
202
+ initialTaskId = rememberedTaskId;
203
+ }
204
+ else {
205
+ initialTaskId = latestTask?.id ?? null;
206
+ }
207
+ if (initialTaskId && initialTaskId !== rememberedTaskId) {
208
+ await runtime.index.setActiveTaskId(initialTaskId);
209
+ }
210
+ else if (!initialTaskId && typeof rememberedTaskId === "string") {
211
+ await runtime.index.setActiveTaskId(null);
212
+ }
213
+ const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices, preflight] = await Promise.all([
214
+ initialTaskId
215
+ ? runtime.sessions.readLatestRoute(runtime.sessions.taskFromId(initialTaskId))
216
+ : null,
217
+ initialTaskId ? runtime.orchestrator.listTaskWorkers(initialTaskId) : [],
218
+ initialTaskId ? runtime.orchestrator.canRetryTask(initialTaskId) : false,
219
+ runtime.sessions.readChatHistory(),
220
+ listWorkspaceChoices(appRoot),
221
+ preflightPromise
222
+ ]);
223
+ const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations, runtime.index.recovery);
224
+ return {
225
+ runtime,
226
+ initialTaskId,
227
+ initialRoute,
228
+ initialWorkers,
229
+ initialCanRetryTask,
230
+ initialMessages: [
231
+ ...initialHistory.map(({ from, text, task_id }) => ({
232
+ from,
233
+ text,
234
+ ...(task_id ? { taskId: task_id } : {})
235
+ })),
236
+ ...recoveryMessages,
237
+ ...startupPreflightMessages(preflight)
238
+ ],
239
+ workspaceChoices
240
+ };
241
+ }
242
+ catch (error) {
243
+ runtime.index.close();
244
+ throw error;
245
+ }
246
+ }
247
+ function closeInteractiveWorkspace(state) {
248
+ state.runtime.index.close();
249
+ }
250
+ function retryDeferredWorkspaceClosures(states) {
251
+ for (const state of states) {
252
+ try {
253
+ closeInteractiveWorkspace(state);
254
+ states.delete(state);
255
+ }
256
+ catch {
257
+ // Process exit remains the final cleanup boundary if an index cannot be closed yet.
258
+ }
84
259
  }
85
260
  }
86
261
  function canRenderInteractiveTui() {
87
262
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
88
263
  }
264
+ function installInteractiveSigintExitHandler(requestGracefulExit) {
265
+ let interrupted = false;
266
+ const onSigint = () => {
267
+ if (!interrupted) {
268
+ interrupted = true;
269
+ try {
270
+ requestGracefulExit();
271
+ return;
272
+ }
273
+ catch {
274
+ // Fall through to the force-exit path when graceful shutdown cannot start.
275
+ }
276
+ }
277
+ restoreInteractiveTerminal();
278
+ process.exit(0);
279
+ };
280
+ process.on("SIGINT", onSigint);
281
+ return () => process.off("SIGINT", onSigint);
282
+ }
283
+ function restoreInteractiveTerminal() {
284
+ if (process.stdin.isTTY && process.stdin.isRaw && typeof process.stdin.setRawMode === "function") {
285
+ try {
286
+ process.stdin.setRawMode(false);
287
+ }
288
+ catch {
289
+ // The active view may already be releasing raw mode.
290
+ }
291
+ }
292
+ process.stdin.pause();
293
+ if (process.stdout.isTTY) {
294
+ process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h");
295
+ }
296
+ }
89
297
  function formatStartupError(error) {
90
- const message = error instanceof Error ? error.message : String(error);
298
+ const message = isConfigStartupError(error)
299
+ ? formatConfigErrorMessage(error)
300
+ : error instanceof Error ? error.message : String(error);
91
301
  if (isConfigStartupError(error)) {
92
302
  return `Config error: ${message}\nRun parallel-codex-tui --doctor for details.`;
93
303
  }