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.
- package/.parallel-codex/config.example.toml +136 -3
- package/README.md +299 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +82 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +234 -24
- package/dist/core/collaboration-timeline.js +268 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +297 -109
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +412 -88
- package/dist/core/session-manager.js +1110 -40
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +18 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +373 -34
- package/dist/domain/schemas.js +142 -7
- package/dist/orchestrator/collaboration-channel.js +289 -6
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +2086 -203
- package/dist/orchestrator/prompts.js +168 -5
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +927 -0
- package/dist/tui/App.js +3187 -161
- package/dist/tui/AppShell.js +196 -25
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +232 -0
- package/dist/tui/InputBar.js +581 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +210 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1404 -161
- package/dist/tui/WorkerOverviewView.js +269 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +49 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +360 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +213 -0
- package/dist/workers/live-probe.js +177 -0
- package/dist/workers/mock-adapter.js +73 -8
- package/dist/workers/native-attach.js +106 -16
- package/dist/workers/process-adapter.js +572 -77
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -20
- package/package.json +11 -2
package/dist/cli-workspace.js
CHANGED
|
@@ -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
|
|
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
|
|
21
|
-
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
|
|
31
|
+
return promptForWorkspaceTui({
|
|
33
32
|
cwd: input.cwd,
|
|
34
33
|
choices,
|
|
35
|
-
stdin
|
|
36
|
-
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
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 =
|
|
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
|
}
|