parallel-codex-tui 0.1.4 → 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 +46 -0
- package/README.md +76 -17
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +13 -1
- package/dist/cli.js +18 -6
- package/dist/core/collaboration-timeline.js +9 -2
- package/dist/core/config.js +161 -103
- package/dist/core/router.js +1 -1
- package/dist/core/session-index.js +234 -61
- package/dist/core/session-manager.js +25 -1
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +10 -9
- package/dist/doctor.js +58 -39
- package/dist/domain/schemas.js +15 -1
- package/dist/orchestrator/collaboration-channel.js +35 -3
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/orchestrator.js +405 -69
- package/dist/orchestrator/prompts.js +42 -3
- package/dist/orchestrator/workspace-sandbox.js +16 -0
- package/dist/tui/App.js +401 -52
- package/dist/tui/AppShell.js +9 -3
- package/dist/tui/FeatureBoardView.js +7 -2
- package/dist/tui/InputBar.js +82 -15
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +5 -2
- package/dist/tui/WorkerOutputView.js +1 -0
- package/dist/tui/WorkerOverviewView.js +23 -4
- package/dist/tui/keyboard.js +3 -0
- package/dist/tui/status-line.js +42 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +4 -3
- package/dist/workers/live-probe.js +4 -3
- package/dist/workers/mock-adapter.js +37 -5
- package/dist/workers/native-attach.js +32 -17
- package/dist/workers/process-adapter.js +2 -0
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -22
- package/package.json +7 -1
|
@@ -105,7 +105,7 @@ function taskSessionSummary(tasks, width) {
|
|
|
105
105
|
const group = taskSessionStatusGroup(task.status);
|
|
106
106
|
counts.set(group, (counts.get(group) ?? 0) + 1);
|
|
107
107
|
}
|
|
108
|
-
const parts = ["running", "done", "failed", "cancelled"].flatMap((group) => {
|
|
108
|
+
const parts = ["running", "paused", "done", "failed", "cancelled"].flatMap((group) => {
|
|
109
109
|
const count = counts.get(group) ?? 0;
|
|
110
110
|
return count > 0 ? [`${count} ${group}`] : [];
|
|
111
111
|
});
|
|
@@ -149,7 +149,7 @@ function clampTaskIndex(index, count) {
|
|
|
149
149
|
return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
|
|
150
150
|
}
|
|
151
151
|
function taskSessionStatusGroup(status) {
|
|
152
|
-
if (status === "done" || status === "failed" || status === "cancelled") {
|
|
152
|
+
if (status === "paused" || status === "done" || status === "failed" || status === "cancelled") {
|
|
153
153
|
return status;
|
|
154
154
|
}
|
|
155
155
|
return "running";
|
|
@@ -165,6 +165,9 @@ function taskSessionStatusTone(task) {
|
|
|
165
165
|
if (status === "done") {
|
|
166
166
|
return "success";
|
|
167
167
|
}
|
|
168
|
+
if (status === "paused") {
|
|
169
|
+
return "warning";
|
|
170
|
+
}
|
|
168
171
|
if (status === "failed") {
|
|
169
172
|
return "danger";
|
|
170
173
|
}
|
|
@@ -2882,6 +2882,7 @@ function shouldDropNoisyProcessLine(trimmed, line) {
|
|
|
2882
2882
|
/^202\d-\d\d-\d\dT.*codex_models_manager::manager/.test(trimmed) ||
|
|
2883
2883
|
/^reasoning summaries:/i.test(trimmed) ||
|
|
2884
2884
|
/^session id:/i.test(trimmed) ||
|
|
2885
|
+
/^collab:\s*(?:wait|waiting)$/i.test(trimmed) ||
|
|
2885
2886
|
/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]$/.test(trimmed) ||
|
|
2886
2887
|
line === "--------");
|
|
2887
2888
|
}
|
|
@@ -158,18 +158,37 @@ function workerOverviewWorkerText(worker, selected, width) {
|
|
|
158
158
|
const marker = selected ? "> " : " ";
|
|
159
159
|
const label = safeWorkerOverviewText(worker.label);
|
|
160
160
|
const state = worker.runtimeStatus?.state ?? "waiting";
|
|
161
|
+
const turn = workerOverviewTurnLabel(worker);
|
|
162
|
+
const model = workerOverviewModelLabel(worker);
|
|
161
163
|
const phase = humanizeWorkerOverviewPhase(worker.runtimeStatus?.phase ?? "status pending");
|
|
162
164
|
const session = worker.runtimeStatus?.native_session_id ? "session" : "";
|
|
163
165
|
const summary = safeWorkerOverviewText(worker.runtimeStatus?.summary ?? "");
|
|
164
166
|
const identity = `${worker.role}/${worker.engine}`;
|
|
165
167
|
return fitWorkerOverviewCandidates([
|
|
166
|
-
[marker + label, state, phase, session, summary].filter(Boolean).join(" · "),
|
|
167
|
-
[marker + label, state, phase, session].filter(Boolean).join(" · "),
|
|
168
|
-
[marker + label, state, session].filter(Boolean).join(" · "),
|
|
169
|
-
[marker + identity, state].join(" · "),
|
|
168
|
+
[marker + label, turn, state, model, phase, session, summary].filter(Boolean).join(" · "),
|
|
169
|
+
[marker + label, turn, state, model, phase, session].filter(Boolean).join(" · "),
|
|
170
|
+
[marker + label, turn, state, model, session].filter(Boolean).join(" · "),
|
|
171
|
+
[marker + identity, turn, state, model].filter(Boolean).join(" · "),
|
|
172
|
+
[marker + identity, turn, state].filter(Boolean).join(" · "),
|
|
170
173
|
marker.trimEnd()
|
|
171
174
|
], width);
|
|
172
175
|
}
|
|
176
|
+
export function workerOverviewTurnLabel(worker) {
|
|
177
|
+
if (worker.role === "main") {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
const suffix = worker.id.startsWith(`${worker.role}-${worker.engine}`)
|
|
181
|
+
? worker.id.slice(`${worker.role}-${worker.engine}`.length).replace(/^-/, "")
|
|
182
|
+
: "";
|
|
183
|
+
const turn = suffix.match(/^(?:final-|wave-)?(\d{4})(?:-|$)/)?.[1];
|
|
184
|
+
return `turn ${Number(turn ?? "0001")}`;
|
|
185
|
+
}
|
|
186
|
+
function workerOverviewModelLabel(worker) {
|
|
187
|
+
const provider = safeWorkerOverviewText(worker.runtimeStatus?.model_provider ?? "");
|
|
188
|
+
const model = safeWorkerOverviewText(worker.runtimeStatus?.model_name ?? "");
|
|
189
|
+
const selection = [provider, model].filter(Boolean).join("/");
|
|
190
|
+
return selection ? `model ${selection}` : "";
|
|
191
|
+
}
|
|
173
192
|
function workerOverviewWindowStart(selected, count, visibleCount) {
|
|
174
193
|
if (visibleCount <= 0 || count <= visibleCount) {
|
|
175
194
|
return 0;
|
package/dist/tui/keyboard.js
CHANGED
|
@@ -25,6 +25,9 @@ export function isTaskSessionsShortcut(input, key) {
|
|
|
25
25
|
export function isTaskResultShortcut(input, key) {
|
|
26
26
|
return (key.ctrl === true && input.toLowerCase() === "d") || input === "\u0004";
|
|
27
27
|
}
|
|
28
|
+
export function isStatusDetailsShortcut(input, key) {
|
|
29
|
+
return (key.ctrl === true && input.toLowerCase() === "s") || input === "\u0013";
|
|
30
|
+
}
|
|
28
31
|
export function isWorkerSearchShortcut(input, key) {
|
|
29
32
|
return (key.ctrl === true && input.toLowerCase() === "f") || input === "\u0006";
|
|
30
33
|
}
|
package/dist/tui/status-line.js
CHANGED
|
@@ -129,6 +129,45 @@ export function formatRouteStatus(route) {
|
|
|
129
129
|
}
|
|
130
130
|
return `route ${details.join(" · ")}`;
|
|
131
131
|
}
|
|
132
|
+
export function formatRouteSummaryStatus(route) {
|
|
133
|
+
if (!route) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
const details = [route.mode];
|
|
137
|
+
if (route.source === "forced") {
|
|
138
|
+
details.push("forced");
|
|
139
|
+
}
|
|
140
|
+
else if (route.source === "fallback") {
|
|
141
|
+
details.push("fallback");
|
|
142
|
+
const cause = route.router_failure_kind && route.router_failure_kind !== "unknown"
|
|
143
|
+
? route.router_failure_kind
|
|
144
|
+
: classifyRouterFailure(route.reason) ?? "unknown";
|
|
145
|
+
details.push(cause === "timeout" ? "timeout" : cause.replaceAll("-", " "));
|
|
146
|
+
}
|
|
147
|
+
else if (route.router_recovered_from) {
|
|
148
|
+
details.push("recovered");
|
|
149
|
+
}
|
|
150
|
+
return `route ${details.join(" · ")}`;
|
|
151
|
+
}
|
|
152
|
+
export function formatRoutePendingSummaryStatus(state, elapsedMs) {
|
|
153
|
+
if (!state) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
if (state.mode !== "auto") {
|
|
157
|
+
return `route ${state.mode} · forced`;
|
|
158
|
+
}
|
|
159
|
+
if (state.phase === "retrying") {
|
|
160
|
+
return `route retry ${state.attempt}/${state.maxAttempts}`;
|
|
161
|
+
}
|
|
162
|
+
const details = [routePendingPhaseLabel(state)];
|
|
163
|
+
if (state.attempt > 1) {
|
|
164
|
+
details.push(`try ${state.attempt}`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof elapsedMs === "number") {
|
|
167
|
+
details.push(formatRouteElapsed(Math.max(0, elapsedMs)));
|
|
168
|
+
}
|
|
169
|
+
return `route ${details.join(" · ")}`;
|
|
170
|
+
}
|
|
132
171
|
function routeRecoveryLabel(route) {
|
|
133
172
|
if (!route.router_recovered_from) {
|
|
134
173
|
return null;
|
|
@@ -378,6 +417,9 @@ function compactStatus(status) {
|
|
|
378
417
|
if (state === "cancelled" || state === "canceled") {
|
|
379
418
|
return "stop";
|
|
380
419
|
}
|
|
420
|
+
if (state === "paused") {
|
|
421
|
+
return "wait";
|
|
422
|
+
}
|
|
381
423
|
if (state === "waiting" || state === "queued") {
|
|
382
424
|
return "wait";
|
|
383
425
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.1.
|
|
1
|
+
export const version = "0.1.5";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
+
import { workerProvider } from "./provider.js";
|
|
3
4
|
export async function diagnoseAgentCapabilities(config, env, options) {
|
|
4
5
|
const targets = capabilityTargets(config, options);
|
|
5
6
|
const runner = options.runner ?? runCapabilityCommand;
|
|
@@ -33,7 +34,7 @@ function capabilityTargets(config, options) {
|
|
|
33
34
|
}, "automated");
|
|
34
35
|
}
|
|
35
36
|
for (const engine of [...new Set(options.workerEngines)]) {
|
|
36
|
-
const worker = config.
|
|
37
|
+
const worker = workerProvider(config, engine).config;
|
|
37
38
|
addTarget(engine, worker.command, worker.capabilities, "automated");
|
|
38
39
|
if (worker.nativeSession.enabled) {
|
|
39
40
|
addTarget(engine, worker.command, worker.capabilities, "resume");
|
|
@@ -79,7 +80,7 @@ async function diagnoseCapabilityTarget(target, config, env, runner, timeoutMs)
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
if (target.capabilities.profile === "codex" && target.surfaces.has("native")) {
|
|
82
|
-
const sandbox = configuredCodexSandbox(config.
|
|
83
|
+
const sandbox = configuredCodexSandbox(workerProvider(config, target.engine).config.interactive.args);
|
|
83
84
|
if (sandbox === "read-only") {
|
|
84
85
|
incompatible.push("native resume uses read-only but feature attach requires writable --add-dir roots");
|
|
85
86
|
}
|
|
@@ -139,7 +140,7 @@ function capabilityProbeSpecs(target) {
|
|
|
139
140
|
return specs;
|
|
140
141
|
}
|
|
141
142
|
function configuredCapabilityIssues(target, config) {
|
|
142
|
-
const worker = config
|
|
143
|
+
const worker = workerProvider(config, target.engine).config;
|
|
143
144
|
const issues = [];
|
|
144
145
|
if (target.surfaces.has("resume")
|
|
145
146
|
&& worker.nativeSession.enabled
|
|
@@ -3,12 +3,13 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { ensureDir, readTextIfExists, writeText } from "../core/file-store.js";
|
|
5
5
|
import { createWorkerRegistry, getAdapter } from "./registry.js";
|
|
6
|
+
import { workerProvider } from "./provider.js";
|
|
6
7
|
export async function runLiveAgentProbes(config, workspaceRoot, engines, options = {}) {
|
|
7
8
|
const activeEngines = [...new Set(engines)];
|
|
8
9
|
if (activeEngines.length === 0) {
|
|
9
10
|
return {
|
|
10
11
|
ok: true,
|
|
11
|
-
lines: ["agent live probe: skipped (no
|
|
12
|
+
lines: ["agent live probe: skipped (no process Worker providers active)"]
|
|
12
13
|
};
|
|
13
14
|
}
|
|
14
15
|
const probesRoot = join(workspaceRoot, config.dataDir, "probes");
|
|
@@ -39,7 +40,7 @@ export async function runLiveAgentProbes(config, workspaceRoot, engines, options
|
|
|
39
40
|
}
|
|
40
41
|
async function probeEngine(config, registry, engine, runRoot, nonce, timeoutOverride) {
|
|
41
42
|
const startedAt = Date.now();
|
|
42
|
-
const worker = config.
|
|
43
|
+
const worker = workerProvider(config, engine).config;
|
|
43
44
|
const adapter = getAdapter(registry, engine);
|
|
44
45
|
const cwd = join(runRoot, `${engine}-workspace`);
|
|
45
46
|
await mkdir(cwd, { recursive: true });
|
|
@@ -106,7 +107,7 @@ async function probeEngine(config, registry, engine, runRoot, nonce, timeoutOver
|
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
async function probeSpec(input) {
|
|
109
|
-
const worker = input.config
|
|
110
|
+
const worker = workerProvider(input.config, input.engine).config;
|
|
110
111
|
await mkdir(input.dir, { recursive: true });
|
|
111
112
|
const promptPath = join(input.dir, "prompt.md");
|
|
112
113
|
await writeText(promptPath, `${input.challenge.prompt}\n`);
|
|
@@ -11,11 +11,28 @@ export class MockWorkerAdapter {
|
|
|
11
11
|
await setStatus(spec, "running", "mock-running", `${spec.role} mock worker running`, nativeSessionId);
|
|
12
12
|
await appendText(spec.outputLogPath, `[mock:${spec.role}] started\n`);
|
|
13
13
|
if (spec.role === "judge") {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if (spec.prompt.includes("# Role:") && spec.prompt.includes("· Final acceptance")) {
|
|
15
|
+
const criterionIds = promptJsonArray(spec.prompt, "Required acceptance criterion ids");
|
|
16
|
+
const changedPaths = promptJsonArray(spec.prompt, "Authoritative changed paths");
|
|
17
|
+
await writeJson(join(spec.filesDir, "final-acceptance.json"), {
|
|
18
|
+
version: 1,
|
|
19
|
+
decision: "approved",
|
|
20
|
+
summary: "Mock Final Judge verified every acceptance criterion.",
|
|
21
|
+
acceptance: criterionIds.map((criterionId) => ({
|
|
22
|
+
criterion_id: criterionId,
|
|
23
|
+
status: "passed",
|
|
24
|
+
evidence: "Mock verification completed."
|
|
25
|
+
})),
|
|
26
|
+
changed_paths: changedPaths
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
await writeText(join(spec.filesDir, "requirements.md"), "# Requirements\n\n- [R-001] Mock requirements derived from the user request.\n");
|
|
31
|
+
await writeText(join(spec.filesDir, "plan.md"), "# Plan\n\n1. [P-001] Implement the scoped change.\n2. [P-002] Run focused verification.\n");
|
|
32
|
+
await writeText(join(spec.filesDir, "acceptance.md"), "# Acceptance\n\n- [A-001] [R-001] Focused tests pass for the requested behavior.\n");
|
|
33
|
+
await writeText(join(spec.filesDir, "actor-brief.md"), "# Actor Brief\n\nImplement the requested change and write a worklog.\n");
|
|
34
|
+
await writeText(join(spec.filesDir, "critic-brief.md"), "# Critic Brief\n\nReview the Actor output against acceptance criteria.\n");
|
|
35
|
+
}
|
|
19
36
|
}
|
|
20
37
|
if (spec.role === "actor") {
|
|
21
38
|
await writeText(join(spec.filesDir, "worklog.md"), "# Worklog\n\n- Mock actor completed the implementation.\n");
|
|
@@ -69,6 +86,19 @@ function featureDirFromPrompt(prompt) {
|
|
|
69
86
|
const line = prompt.split("\n").find((item) => item.startsWith("Feature directory: "));
|
|
70
87
|
return line ? line.replace("Feature directory: ", "").trim() : null;
|
|
71
88
|
}
|
|
89
|
+
function promptJsonArray(prompt, label) {
|
|
90
|
+
const line = prompt.split("\n").find((item) => item.startsWith(`${label}: `));
|
|
91
|
+
if (!line) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const value = JSON.parse(line.slice(label.length + 2));
|
|
96
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
72
102
|
async function setStatus(spec, state, phase, summary, nativeSessionId) {
|
|
73
103
|
const status = {
|
|
74
104
|
worker_id: spec.workerId,
|
|
@@ -76,6 +106,8 @@ async function setStatus(spec, state, phase, summary, nativeSessionId) {
|
|
|
76
106
|
...(spec.featureTitle ? { feature_title: spec.featureTitle } : {}),
|
|
77
107
|
role: spec.role,
|
|
78
108
|
engine: spec.engine,
|
|
109
|
+
...(spec.modelConfig?.name.trim() ? { model_name: spec.modelConfig.name.trim() } : {}),
|
|
110
|
+
...(spec.modelConfig?.provider.trim() ? { model_provider: spec.modelConfig.provider.trim() } : {}),
|
|
79
111
|
state,
|
|
80
112
|
phase,
|
|
81
113
|
last_event_at: new Date().toISOString(),
|
|
@@ -7,12 +7,26 @@ import { spawn } from "node-pty";
|
|
|
7
7
|
import { pathExists, pathIsDirectory, readJson, readTextIfExists, removeIfExists, writeJson } from "../core/file-store.js";
|
|
8
8
|
import { NativeSessionSchema, TaskMetaSchema } from "../domain/schemas.js";
|
|
9
9
|
import { detectResumeSessionId } from "./native-session-detection.js";
|
|
10
|
+
import { workerProvider } from "./provider.js";
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
export async function buildNativeAttachLaunch(input) {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
return buildNativeSessionLaunch(input, "resume");
|
|
14
|
+
}
|
|
15
|
+
export async function buildNativeForkLaunch(input) {
|
|
16
|
+
return buildNativeSessionLaunch(input, "fork");
|
|
17
|
+
}
|
|
18
|
+
async function buildNativeSessionLaunch(input, mode) {
|
|
19
|
+
const workerConfig = workerProvider(input.config, input.worker.engine).config;
|
|
20
|
+
const nativeSession = await readWorkerNativeSession(input.worker, workerConfig.capabilities.profile);
|
|
14
21
|
const modelConfig = workerConfig.model;
|
|
15
22
|
const env = modelEnvironment(modelConfig);
|
|
23
|
+
const interactiveArgs = mode === "fork"
|
|
24
|
+
? workerConfig.interactive.forkArgs
|
|
25
|
+
: workerConfig.interactive.args;
|
|
26
|
+
if (mode === "fork" && interactiveArgs.length === 0) {
|
|
27
|
+
throw new Error(`Native session fork is not configured for ${input.worker.engine}. `
|
|
28
|
+
+ `Set workers.${input.worker.engine}.interactive.forkArgs with a {sessionId} template.`);
|
|
29
|
+
}
|
|
16
30
|
const workerDir = dirname(input.worker.statusPath);
|
|
17
31
|
const taskDir = dirname(workerDir);
|
|
18
32
|
if (!(await pathExists(nativeSession.cwd))) {
|
|
@@ -34,16 +48,17 @@ export async function buildNativeAttachLaunch(input) {
|
|
|
34
48
|
command: workerConfig.interactive.command,
|
|
35
49
|
args: nativeAttachArgs({
|
|
36
50
|
args: [
|
|
37
|
-
...
|
|
51
|
+
...interactiveArgs,
|
|
38
52
|
...modelConfig.args
|
|
39
53
|
].map((arg) => renderTemplate(arg, nativeSession.session_id, modelConfig)),
|
|
54
|
+
providerId: input.worker.engine,
|
|
40
55
|
capabilities: workerConfig.capabilities,
|
|
41
56
|
additionalDirs
|
|
42
57
|
}),
|
|
43
58
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
|
44
59
|
cwd: nativeSession.cwd,
|
|
45
60
|
sessionId: nativeSession.session_id,
|
|
46
|
-
label: input.worker.label
|
|
61
|
+
label: mode === "fork" ? `${input.worker.label} · fork` : input.worker.label
|
|
47
62
|
};
|
|
48
63
|
}
|
|
49
64
|
function nativeAttachArgs(input) {
|
|
@@ -51,18 +66,18 @@ function nativeAttachArgs(input) {
|
|
|
51
66
|
return input.args;
|
|
52
67
|
}
|
|
53
68
|
const args = input.capabilities.profile === "codex"
|
|
54
|
-
? withCodexWritableSandbox(input.args)
|
|
69
|
+
? withCodexWritableSandbox(input.args, input.providerId)
|
|
55
70
|
: input.args;
|
|
56
71
|
return [
|
|
57
72
|
...args,
|
|
58
73
|
...[...new Set(input.additionalDirs)].flatMap((directory) => (input.capabilities.writableDirArgs.map((arg) => arg.replaceAll("{dir}", directory))))
|
|
59
74
|
];
|
|
60
75
|
}
|
|
61
|
-
function withCodexWritableSandbox(args) {
|
|
76
|
+
function withCodexWritableSandbox(args, providerId) {
|
|
62
77
|
const sandbox = codexSandboxSelection(args);
|
|
63
78
|
if (sandbox === "read-only") {
|
|
64
79
|
throw new Error("Codex native attach cannot use recorded worker directories with a read-only sandbox. "
|
|
65
|
-
+
|
|
80
|
+
+ `Set workers.${providerId}.interactive.args to workspace-write or danger-full-access.`);
|
|
66
81
|
}
|
|
67
82
|
return sandbox ? args : [...args, "--sandbox", "workspace-write"];
|
|
68
83
|
}
|
|
@@ -144,17 +159,17 @@ function ensureNodePtySpawnHelperExecutable() {
|
|
|
144
159
|
chmodSync(helperPath, 0o755);
|
|
145
160
|
}
|
|
146
161
|
}
|
|
147
|
-
async function readWorkerNativeSession(worker) {
|
|
162
|
+
async function readWorkerNativeSession(worker, profile) {
|
|
148
163
|
const workerDir = dirname(worker.statusPath);
|
|
149
164
|
const nativePath = join(workerDir, "native-session.json");
|
|
150
165
|
const record = await readAttachNativeSessionIfValid(nativePath);
|
|
151
166
|
if (!record) {
|
|
152
|
-
const recoveredCodex = await recoverCodexNativeSession(worker, workerDir);
|
|
167
|
+
const recoveredCodex = await recoverCodexNativeSession(worker, workerDir, profile);
|
|
153
168
|
if (recoveredCodex) {
|
|
154
169
|
await writeJson(nativePath, NativeSessionSchema.parse(recoveredCodex));
|
|
155
170
|
return recoveredCodex;
|
|
156
171
|
}
|
|
157
|
-
const recovered = await recoverClaudeNativeSession(worker, workerDir);
|
|
172
|
+
const recovered = await recoverClaudeNativeSession(worker, workerDir, profile);
|
|
158
173
|
if (recovered) {
|
|
159
174
|
await writeJson(nativePath, NativeSessionSchema.parse(recovered));
|
|
160
175
|
return recovered;
|
|
@@ -181,8 +196,8 @@ async function readAttachNativeSessionIfValid(nativePath) {
|
|
|
181
196
|
return null;
|
|
182
197
|
}
|
|
183
198
|
}
|
|
184
|
-
async function recoverCodexNativeSession(worker, workerDir) {
|
|
185
|
-
if (
|
|
199
|
+
async function recoverCodexNativeSession(worker, workerDir, profile) {
|
|
200
|
+
if (profile !== "codex") {
|
|
186
201
|
return null;
|
|
187
202
|
}
|
|
188
203
|
const taskDir = dirname(workerDir);
|
|
@@ -197,7 +212,7 @@ async function recoverCodexNativeSession(worker, workerDir) {
|
|
|
197
212
|
}
|
|
198
213
|
const now = new Date().toISOString();
|
|
199
214
|
return {
|
|
200
|
-
engine:
|
|
215
|
+
engine: worker.engine,
|
|
201
216
|
role: worker.role,
|
|
202
217
|
worker_id: worker.id,
|
|
203
218
|
session_id: sessionId,
|
|
@@ -208,8 +223,8 @@ async function recoverCodexNativeSession(worker, workerDir) {
|
|
|
208
223
|
source: "output-detected"
|
|
209
224
|
};
|
|
210
225
|
}
|
|
211
|
-
async function recoverClaudeNativeSession(worker, workerDir) {
|
|
212
|
-
if (
|
|
226
|
+
async function recoverClaudeNativeSession(worker, workerDir, profile) {
|
|
227
|
+
if (profile !== "claude") {
|
|
213
228
|
return null;
|
|
214
229
|
}
|
|
215
230
|
const prompt = await readTextIfExists(join(workerDir, "prompt.md"));
|
|
@@ -229,7 +244,7 @@ async function recoverClaudeNativeSession(worker, workerDir) {
|
|
|
229
244
|
return null;
|
|
230
245
|
}
|
|
231
246
|
return {
|
|
232
|
-
engine:
|
|
247
|
+
engine: worker.engine,
|
|
233
248
|
role: worker.role,
|
|
234
249
|
worker_id: worker.id,
|
|
235
250
|
session_id: match.sessionId,
|
|
@@ -324,5 +339,5 @@ function renderTemplate(value, sessionId, modelConfig) {
|
|
|
324
339
|
.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => process.env[name] ?? "");
|
|
325
340
|
}
|
|
326
341
|
export function supportsNativeAttach(engine) {
|
|
327
|
-
return engine
|
|
342
|
+
return Boolean(engine);
|
|
328
343
|
}
|
|
@@ -745,6 +745,8 @@ async function setStatus(spec, state, phase, summary, nativeSessionId) {
|
|
|
745
745
|
...(spec.featureTitle ? { feature_title: spec.featureTitle } : {}),
|
|
746
746
|
role: spec.role,
|
|
747
747
|
engine: spec.engine,
|
|
748
|
+
...(spec.modelConfig?.name.trim() ? { model_name: spec.modelConfig.name.trim() } : {}),
|
|
749
|
+
...(spec.modelConfig?.provider.trim() ? { model_provider: spec.modelConfig.provider.trim() } : {}),
|
|
748
750
|
state,
|
|
749
751
|
phase,
|
|
750
752
|
last_event_at: new Date().toISOString(),
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function workerProvider(config, id) {
|
|
2
|
+
const provider = config.workers[id];
|
|
3
|
+
if (!provider) {
|
|
4
|
+
throw new Error(`Worker provider is not configured: ${id}`);
|
|
5
|
+
}
|
|
6
|
+
return { id, config: provider };
|
|
7
|
+
}
|
|
8
|
+
export function workerProviders(config) {
|
|
9
|
+
return Object.entries(config.workers)
|
|
10
|
+
.map(([id, provider]) => ({ id, config: provider }))
|
|
11
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
12
|
+
}
|
|
13
|
+
export function assignableWorkerProviderIds(config) {
|
|
14
|
+
return workerProviders(config)
|
|
15
|
+
.filter((provider) => provider.config.assignable)
|
|
16
|
+
.map((provider) => provider.id);
|
|
17
|
+
}
|
|
18
|
+
export function workerProviderLabel(config, id) {
|
|
19
|
+
const provider = config.workers[id];
|
|
20
|
+
if (!provider) {
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
const model = provider.model.name.trim();
|
|
24
|
+
const remote = provider.model.provider.trim();
|
|
25
|
+
return [id, model, remote].filter(Boolean).join("/");
|
|
26
|
+
}
|
package/dist/workers/registry.js
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import { MockWorkerAdapter } from "./mock-adapter.js";
|
|
2
2
|
import { ProcessWorkerAdapter } from "./process-adapter.js";
|
|
3
|
+
import { workerProviders } from "./provider.js";
|
|
3
4
|
export function createWorkerRegistry(config) {
|
|
4
|
-
return new Map([
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
new ProcessWorkerAdapter(
|
|
9
|
-
timeoutMs:
|
|
10
|
-
idleTimeoutMs:
|
|
11
|
-
firstOutputTimeoutMs:
|
|
12
|
-
model:
|
|
13
|
-
capabilities:
|
|
5
|
+
return new Map(workerProviders(config).map(({ id, config: provider }) => [
|
|
6
|
+
id,
|
|
7
|
+
id === "mock"
|
|
8
|
+
? new MockWorkerAdapter()
|
|
9
|
+
: new ProcessWorkerAdapter(provider.command, provider.args, id, {
|
|
10
|
+
timeoutMs: provider.timeoutMs,
|
|
11
|
+
idleTimeoutMs: provider.idleTimeoutMs,
|
|
12
|
+
firstOutputTimeoutMs: provider.firstOutputTimeoutMs,
|
|
13
|
+
model: provider.model,
|
|
14
|
+
capabilities: provider.capabilities
|
|
14
15
|
})
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
"claude",
|
|
18
|
-
new ProcessWorkerAdapter(config.workers.claude.command, config.workers.claude.args, "claude", {
|
|
19
|
-
timeoutMs: config.workers.claude.timeoutMs,
|
|
20
|
-
idleTimeoutMs: config.workers.claude.idleTimeoutMs,
|
|
21
|
-
firstOutputTimeoutMs: config.workers.claude.firstOutputTimeoutMs,
|
|
22
|
-
model: config.workers.claude.model,
|
|
23
|
-
capabilities: config.workers.claude.capabilities
|
|
24
|
-
})
|
|
25
|
-
]
|
|
26
|
-
]);
|
|
16
|
+
]));
|
|
27
17
|
}
|
|
28
18
|
export function getAdapter(registry, engine) {
|
|
29
19
|
const adapter = registry.get(engine);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parallel-codex-tui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A TypeScript TUI wrapper for routed parallel coding with Codex and Claude workers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"build": "tsc -p tsconfig.json",
|
|
41
41
|
"postbuild": "node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
|
|
42
42
|
"prepack": "npm run build",
|
|
43
|
+
"pretest": "node scripts/fix-node-pty-permissions.mjs",
|
|
43
44
|
"verify:package": "node scripts/verify-package.mjs",
|
|
44
45
|
"test": "vitest run",
|
|
45
46
|
"test:watch": "vitest",
|
|
@@ -64,5 +65,10 @@
|
|
|
64
65
|
"tsx": "^4.19.0",
|
|
65
66
|
"typescript": "^5.8.0",
|
|
66
67
|
"vitest": "^3.2.0"
|
|
68
|
+
},
|
|
69
|
+
"allowScripts": {
|
|
70
|
+
"esbuild@0.28.1": true,
|
|
71
|
+
"fsevents@2.3.3": true,
|
|
72
|
+
"node-pty@1.1.0": true
|
|
67
73
|
}
|
|
68
74
|
}
|