parallel-codex-tui 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parallel-codex/config.example.toml +90 -3
- package/README.md +269 -12
- package/dist/bootstrap.js +50 -18
- package/dist/cli-args.js +96 -14
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +40 -0
- package/dist/cli.js +291 -35
- package/dist/core/app-root.js +8 -0
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +191 -23
- package/dist/core/file-store.js +130 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +10 -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 +473 -42
- package/dist/core/session-index.js +225 -30
- package/dist/core/session-manager.js +1182 -44
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +126 -0
- package/dist/doctor.js +384 -30
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1777 -212
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2838 -159
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -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 +46 -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 +318 -11
- package/dist/tui/task-memory.js +15 -0
- 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 +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +147 -8
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +580 -81
- package/dist/workers/registry.js +4 -2
- package/package.json +17 -2
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const TASK_STATE_TRANSITIONS = {
|
|
2
|
+
created: new Set(["routed", "failed", "cancelled"]),
|
|
3
|
+
routed: new Set(["judging", "failed", "cancelled"]),
|
|
4
|
+
judging: new Set(["ready_for_pair", "failed", "cancelled"]),
|
|
5
|
+
ready_for_pair: new Set(["actor_running", "done", "failed", "cancelled"]),
|
|
6
|
+
actor_running: new Set(["critic_running", "verifying", "failed", "cancelled"]),
|
|
7
|
+
critic_running: new Set(["revision_needed", "integrating", "failed", "cancelled"]),
|
|
8
|
+
revision_needed: new Set(["actor_running", "failed", "cancelled"]),
|
|
9
|
+
integrating: new Set(["actor_running", "verifying", "done", "failed", "cancelled"]),
|
|
10
|
+
verifying: new Set(["revision_needed", "integrating", "failed", "cancelled"]),
|
|
11
|
+
done: new Set(["routed", "cancelled"]),
|
|
12
|
+
failed: new Set(["routed", "judging", "ready_for_pair"]),
|
|
13
|
+
cancelled: new Set(["routed", "judging", "ready_for_pair"])
|
|
14
|
+
};
|
|
15
|
+
export function taskStateTransitionAllowed(from, to) {
|
|
16
|
+
return from === to || TASK_STATE_TRANSITIONS[from].has(to);
|
|
17
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sessionsRoot } from "./paths.js";
|
|
4
|
+
const TASK_DIRECTORY_PATTERN = /^task-/;
|
|
5
|
+
const TURN_DIRECTORY_PATTERN = /^turn-(\d{4,})$/;
|
|
6
|
+
const WAVE_DIRECTORY_PATTERN = /^wave-(\d{4,})$/;
|
|
7
|
+
const COMMIT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
|
|
8
|
+
const COMMIT_PROTOCOL = "atomic-claim-v1";
|
|
9
|
+
export async function reconcileWorkspaceCommitIntents(projectRoot, dataDir) {
|
|
10
|
+
const recovery = { cleaned: 0, preserved: 0 };
|
|
11
|
+
const taskEntries = await directoryEntries(sessionsRoot(projectRoot, dataDir));
|
|
12
|
+
for (const taskEntry of taskEntries) {
|
|
13
|
+
if (!taskEntry.isDirectory() || !TASK_DIRECTORY_PATTERN.test(taskEntry.name)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const workspacesDir = join(sessionsRoot(projectRoot, dataDir), taskEntry.name, "workspaces");
|
|
17
|
+
for (const turnEntry of await directoryEntries(workspacesDir)) {
|
|
18
|
+
const turnMatch = turnEntry.isDirectory() ? TURN_DIRECTORY_PATTERN.exec(turnEntry.name) : null;
|
|
19
|
+
if (!turnMatch) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
for (const waveEntry of await directoryEntries(join(workspacesDir, turnEntry.name))) {
|
|
23
|
+
const waveMatch = waveEntry.isDirectory() ? WAVE_DIRECTORY_PATTERN.exec(waveEntry.name) : null;
|
|
24
|
+
if (!waveMatch) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const waveDir = join(workspacesDir, turnEntry.name, waveEntry.name);
|
|
28
|
+
const pendingPath = join(waveDir, "integration.pending.json");
|
|
29
|
+
const pendingRecord = await readJsonRecord(pendingPath);
|
|
30
|
+
if (pendingRecord === undefined) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const pending = parseCommitEvidence(pendingRecord, "committing");
|
|
34
|
+
const integrated = parseCommitEvidence(await readJsonRecord(join(waveDir, "integration.json")), "integrated");
|
|
35
|
+
const directoryTurnId = turnMatch[1];
|
|
36
|
+
const directoryWave = Number(waveMatch[1]);
|
|
37
|
+
if (!pending
|
|
38
|
+
|| !integrated
|
|
39
|
+
|| pending.turnId !== directoryTurnId
|
|
40
|
+
|| pending.wave !== directoryWave
|
|
41
|
+
|| !sameCommit(pending, integrated)) {
|
|
42
|
+
recovery.preserved += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await rm(pendingPath, { force: true });
|
|
47
|
+
recovery.cleaned += 1;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
recovery.preserved += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return recovery;
|
|
56
|
+
}
|
|
57
|
+
async function directoryEntries(path) {
|
|
58
|
+
try {
|
|
59
|
+
return await readdir(path, { withFileTypes: true });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readJsonRecord(path) {
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
68
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
69
|
+
? value
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return error.code === "ENOENT" ? undefined : null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function parseCommitEvidence(record, state) {
|
|
77
|
+
const featureIds = stringArray(record?.feature_ids);
|
|
78
|
+
const changedPaths = stringArray(record?.changed_paths);
|
|
79
|
+
const commitId = record?.commit_id;
|
|
80
|
+
const commitProtocol = record?.commit_protocol;
|
|
81
|
+
if (record?.version !== 1
|
|
82
|
+
|| record.state !== state
|
|
83
|
+
|| typeof record.turn_id !== "string"
|
|
84
|
+
|| !Number.isInteger(record.wave)
|
|
85
|
+
|| record.wave < 1
|
|
86
|
+
|| !featureIds
|
|
87
|
+
|| !changedPaths
|
|
88
|
+
|| (commitId !== undefined && (typeof commitId !== "string" || !COMMIT_ID_PATTERN.test(commitId)))
|
|
89
|
+
|| (commitProtocol !== undefined && commitProtocol !== COMMIT_PROTOCOL)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
version: 1,
|
|
94
|
+
state,
|
|
95
|
+
turnId: record.turn_id,
|
|
96
|
+
wave: record.wave,
|
|
97
|
+
featureIds,
|
|
98
|
+
...(typeof commitId === "string" ? { commitId } : {}),
|
|
99
|
+
...(commitProtocol === COMMIT_PROTOCOL ? { commitProtocol } : {}),
|
|
100
|
+
changedPaths
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function stringArray(value) {
|
|
104
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
105
|
+
? value
|
|
106
|
+
: null;
|
|
107
|
+
}
|
|
108
|
+
function sameCommit(pending, integrated) {
|
|
109
|
+
return pending.turnId === integrated.turnId
|
|
110
|
+
&& pending.wave === integrated.wave
|
|
111
|
+
&& sameStrings(pending.featureIds, integrated.featureIds)
|
|
112
|
+
&& sameStrings(pending.changedPaths, integrated.changedPaths)
|
|
113
|
+
&& (pending.commitId === undefined || pending.commitId === integrated.commitId)
|
|
114
|
+
&& (pending.commitProtocol === undefined || pending.commitProtocol === integrated.commitProtocol);
|
|
115
|
+
}
|
|
116
|
+
function sameStrings(left, right) {
|
|
117
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
118
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { ensureDir, pathExists, pathIsDirectory, readJson, readTextIfExists, writeJson, writeText } from "./file-store.js";
|
|
5
|
+
import { runWithLeaseFinalization } from "./lease-finalization.js";
|
|
6
|
+
import { acquireProcessMutationTurn } from "./process-mutation-turn.js";
|
|
7
|
+
const lastWorkspaceFile = "last-workspace";
|
|
8
|
+
const workspacesFile = "workspaces.json";
|
|
9
|
+
const maxRememberedWorkspaces = 20;
|
|
10
|
+
const workspaceRegistryIntentPrefix = ".workspace-registry-claim-";
|
|
11
|
+
const WorkspaceRegistrySchema = z.object({
|
|
12
|
+
version: z.literal(1).default(1),
|
|
13
|
+
workspaces: z
|
|
14
|
+
.array(z.object({
|
|
15
|
+
path: z.string().min(1),
|
|
16
|
+
last_used_at: z.string().min(1)
|
|
17
|
+
}))
|
|
18
|
+
.default([])
|
|
19
|
+
});
|
|
20
|
+
export async function resolveWorkspaceSelection(input) {
|
|
21
|
+
if (input.explicitWorkspace?.trim()) {
|
|
22
|
+
return resolveWorkspacePath(input.cwd, input.explicitWorkspace);
|
|
23
|
+
}
|
|
24
|
+
const [latest] = await listWorkspaceChoices(input.appRoot);
|
|
25
|
+
return latest ? latest.path : input.cwd;
|
|
26
|
+
}
|
|
27
|
+
export async function prepareWorkspace(appRoot, workspaceRoot) {
|
|
28
|
+
const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
|
|
29
|
+
if ((await pathExists(resolved)) && !(await pathIsDirectory(resolved))) {
|
|
30
|
+
throw new Error(`Workspace path exists but is not a directory: ${resolved}`);
|
|
31
|
+
}
|
|
32
|
+
await ensureDir(resolved);
|
|
33
|
+
await ensureDir(join(resolved, ".parallel-codex"));
|
|
34
|
+
await rememberWorkspace(appRoot, resolved);
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
export async function hasSavedWorkspace(appRoot) {
|
|
38
|
+
return (await listWorkspaceChoices(appRoot)).length > 0;
|
|
39
|
+
}
|
|
40
|
+
export async function listWorkspaceChoices(appRoot) {
|
|
41
|
+
const entries = await readWorkspaceEntries(appRoot);
|
|
42
|
+
const legacy = (await readTextIfExists(lastWorkspacePath(appRoot))).trim();
|
|
43
|
+
if (legacy) {
|
|
44
|
+
entries.push({ path: legacy, last_used_at: "" });
|
|
45
|
+
}
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const unique = entries
|
|
48
|
+
.map((entry, index) => ({
|
|
49
|
+
path: resolveStoredWorkspacePath(appRoot, entry.path),
|
|
50
|
+
lastUsedAt: entry.last_used_at || null,
|
|
51
|
+
order: index
|
|
52
|
+
}))
|
|
53
|
+
.filter((entry) => {
|
|
54
|
+
if (seen.has(entry.path)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
seen.add(entry.path);
|
|
58
|
+
return true;
|
|
59
|
+
})
|
|
60
|
+
.sort((left, right) => {
|
|
61
|
+
const byDate = (right.lastUsedAt ?? "").localeCompare(left.lastUsedAt ?? "");
|
|
62
|
+
return byDate === 0 ? left.order - right.order : byDate;
|
|
63
|
+
});
|
|
64
|
+
const choices = await Promise.all(unique.map(async (entry) => {
|
|
65
|
+
const exists = await pathExists(entry.path);
|
|
66
|
+
const isDirectory = exists && (await pathIsDirectory(entry.path));
|
|
67
|
+
return {
|
|
68
|
+
path: entry.path,
|
|
69
|
+
exists: isDirectory,
|
|
70
|
+
lastUsedAt: entry.lastUsedAt,
|
|
71
|
+
selectable: !exists || isDirectory
|
|
72
|
+
};
|
|
73
|
+
}));
|
|
74
|
+
return choices
|
|
75
|
+
.filter((choice) => choice.selectable)
|
|
76
|
+
.map(({ selectable: _selectable, ...choice }) => choice);
|
|
77
|
+
}
|
|
78
|
+
async function rememberWorkspace(appRoot, workspaceRoot) {
|
|
79
|
+
const resolved = resolveWorkspacePath(process.cwd(), workspaceRoot);
|
|
80
|
+
const mutationTurn = await acquireProcessMutationTurn(join(appRoot, ".parallel-codex"), {
|
|
81
|
+
intentPrefix: workspaceRegistryIntentPrefix,
|
|
82
|
+
timeoutMessage: "Timed out waiting to update the workspace registry."
|
|
83
|
+
});
|
|
84
|
+
await runWithLeaseFinalization("Workspace registry update", mutationTurn, async () => {
|
|
85
|
+
const current = await readWorkspaceEntries(appRoot);
|
|
86
|
+
const next = {
|
|
87
|
+
version: 1,
|
|
88
|
+
workspaces: [
|
|
89
|
+
{ path: resolved, last_used_at: new Date().toISOString() },
|
|
90
|
+
...current.filter((entry) => resolveStoredWorkspacePath(appRoot, entry.path) !== resolved)
|
|
91
|
+
].slice(0, maxRememberedWorkspaces)
|
|
92
|
+
};
|
|
93
|
+
await writeJson(workspacesPath(appRoot), WorkspaceRegistrySchema.parse(next));
|
|
94
|
+
await writeText(lastWorkspacePath(appRoot), `${resolved}\n`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async function readWorkspaceEntries(appRoot) {
|
|
98
|
+
const file = workspacesPath(appRoot);
|
|
99
|
+
if (!(await pathExists(file))) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return (await readJson(file, WorkspaceRegistrySchema)).workspaces;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function lastWorkspacePath(appRoot) {
|
|
110
|
+
return join(appRoot, ".parallel-codex", lastWorkspaceFile);
|
|
111
|
+
}
|
|
112
|
+
function workspacesPath(appRoot) {
|
|
113
|
+
return join(appRoot, ".parallel-codex", workspacesFile);
|
|
114
|
+
}
|
|
115
|
+
export function resolveWorkspacePath(cwd, value) {
|
|
116
|
+
if (value === "~") {
|
|
117
|
+
return homedir();
|
|
118
|
+
}
|
|
119
|
+
if (value.startsWith("~/")) {
|
|
120
|
+
return resolve(homedir(), value.slice(2));
|
|
121
|
+
}
|
|
122
|
+
return resolve(cwd, value);
|
|
123
|
+
}
|
|
124
|
+
function resolveStoredWorkspacePath(appRoot, value) {
|
|
125
|
+
return resolveWorkspacePath(appRoot, value);
|
|
126
|
+
}
|