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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
import { NativeSessionSchema, TaskMetaSchema, TurnMetaSchema, WorkerStatusSchema } from "../domain/schemas.js";
|
|
5
|
-
import { ensureDir, pathExists, readJson } from "./file-store.js";
|
|
4
|
+
import { NativeSessionSchema, RetiredNativeSessionSchema, RouteDecisionSchema, TaskIdSchema, TaskMetaSchema, TurnMetaSchema, WorkerStatusSchema } from "../domain/schemas.js";
|
|
5
|
+
import { ensureDir, pathExists, readJson, readTextIfExists } from "./file-store.js";
|
|
6
6
|
export class SessionIndex {
|
|
7
7
|
db;
|
|
8
8
|
projectRoot;
|
|
@@ -20,13 +20,16 @@ export class SessionIndex {
|
|
|
20
20
|
}
|
|
21
21
|
initialize() {
|
|
22
22
|
this.db.exec(`
|
|
23
|
+
PRAGMA busy_timeout = 5000;
|
|
24
|
+
|
|
23
25
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
24
26
|
id TEXT PRIMARY KEY,
|
|
25
27
|
title TEXT NOT NULL,
|
|
26
28
|
created_at TEXT NOT NULL,
|
|
27
29
|
cwd TEXT NOT NULL,
|
|
28
30
|
mode TEXT NOT NULL,
|
|
29
|
-
status TEXT NOT NULL
|
|
31
|
+
status TEXT NOT NULL,
|
|
32
|
+
archived_at TEXT
|
|
30
33
|
);
|
|
31
34
|
|
|
32
35
|
CREATE TABLE IF NOT EXISTS turns (
|
|
@@ -64,19 +67,26 @@ export class SessionIndex {
|
|
|
64
67
|
source TEXT NOT NULL,
|
|
65
68
|
PRIMARY KEY (task_id, worker_id)
|
|
66
69
|
);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS workspace_state (
|
|
72
|
+
key TEXT PRIMARY KEY,
|
|
73
|
+
value TEXT NOT NULL
|
|
74
|
+
);
|
|
67
75
|
`);
|
|
76
|
+
this.ensureColumn("tasks", "archived_at", "TEXT");
|
|
68
77
|
}
|
|
69
78
|
async upsertTask(task) {
|
|
70
79
|
this.db
|
|
71
|
-
.prepare(`INSERT INTO tasks (id, title, created_at, cwd, mode, status)
|
|
72
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
80
|
+
.prepare(`INSERT INTO tasks (id, title, created_at, cwd, mode, status, archived_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
73
82
|
ON CONFLICT(id) DO UPDATE SET
|
|
74
83
|
title=excluded.title,
|
|
75
84
|
created_at=excluded.created_at,
|
|
76
85
|
cwd=excluded.cwd,
|
|
77
86
|
mode=excluded.mode,
|
|
78
|
-
status=excluded.status
|
|
79
|
-
|
|
87
|
+
status=excluded.status,
|
|
88
|
+
archived_at=excluded.archived_at`)
|
|
89
|
+
.run(task.id, task.title, task.created_at, task.cwd, task.mode, task.status, task.archived_at ?? null);
|
|
80
90
|
}
|
|
81
91
|
async upsertTurn(taskId, turn) {
|
|
82
92
|
this.db
|
|
@@ -121,60 +131,245 @@ export class SessionIndex {
|
|
|
121
131
|
source=excluded.source`)
|
|
122
132
|
.run(taskId, record.worker_id, record.engine, record.role, record.session_id, record.cwd, record.created_at, record.last_used_at, record.source);
|
|
123
133
|
}
|
|
134
|
+
async deleteNativeSession(taskId, workerId) {
|
|
135
|
+
this.db.prepare("DELETE FROM native_sessions WHERE task_id = ? AND worker_id = ?").run(taskId, workerId);
|
|
136
|
+
}
|
|
137
|
+
async deleteTask(taskId) {
|
|
138
|
+
const id = TaskIdSchema.parse(taskId);
|
|
139
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
140
|
+
try {
|
|
141
|
+
this.db.prepare("DELETE FROM native_sessions WHERE task_id = ?").run(id);
|
|
142
|
+
this.db.prepare("DELETE FROM workers WHERE task_id = ?").run(id);
|
|
143
|
+
this.db.prepare("DELETE FROM turns WHERE task_id = ?").run(id);
|
|
144
|
+
this.db.prepare("DELETE FROM tasks WHERE id = ?").run(id);
|
|
145
|
+
this.db.prepare("UPDATE workspace_state SET value = '' WHERE key = 'active_task_id' AND value = ?").run(id);
|
|
146
|
+
this.db.exec("COMMIT");
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.db.exec("ROLLBACK");
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
124
153
|
async countRows(table) {
|
|
125
154
|
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
126
155
|
return row.count;
|
|
127
156
|
}
|
|
157
|
+
async listTasks(limit = 50, options = {}) {
|
|
158
|
+
const boundedLimit = Number.isFinite(limit)
|
|
159
|
+
? Math.min(500, Math.max(0, Math.trunc(limit)))
|
|
160
|
+
: 50;
|
|
161
|
+
if (boundedLimit === 0) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
const archivedFilter = options.includeArchived ? "" : "WHERE tasks.archived_at IS NULL";
|
|
165
|
+
const rows = this.db.prepare(`SELECT
|
|
166
|
+
tasks.id,
|
|
167
|
+
tasks.title,
|
|
168
|
+
tasks.created_at,
|
|
169
|
+
tasks.cwd,
|
|
170
|
+
tasks.mode,
|
|
171
|
+
tasks.status,
|
|
172
|
+
tasks.archived_at,
|
|
173
|
+
(SELECT COUNT(*) FROM turns WHERE turns.task_id = tasks.id) AS turn_count,
|
|
174
|
+
(SELECT COUNT(*) FROM workers WHERE workers.task_id = tasks.id) AS worker_count,
|
|
175
|
+
(SELECT COUNT(DISTINCT engine || char(31) || session_id)
|
|
176
|
+
FROM native_sessions
|
|
177
|
+
WHERE native_sessions.task_id = tasks.id) AS native_session_count
|
|
178
|
+
FROM tasks
|
|
179
|
+
${archivedFilter}
|
|
180
|
+
ORDER BY tasks.created_at DESC, tasks.id DESC
|
|
181
|
+
LIMIT ?`).all(boundedLimit);
|
|
182
|
+
return rows.flatMap((row) => {
|
|
183
|
+
const task = TaskMetaSchema.safeParse({
|
|
184
|
+
id: row.id,
|
|
185
|
+
title: row.title,
|
|
186
|
+
created_at: row.created_at,
|
|
187
|
+
cwd: row.cwd,
|
|
188
|
+
mode: row.mode,
|
|
189
|
+
status: row.status,
|
|
190
|
+
archived_at: row.archived_at ?? undefined
|
|
191
|
+
});
|
|
192
|
+
if (!task.success) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
return [{
|
|
196
|
+
...task.data,
|
|
197
|
+
turnCount: Number(row.turn_count) || 0,
|
|
198
|
+
workerCount: Number(row.worker_count) || 0,
|
|
199
|
+
nativeSessionCount: Number(row.native_session_count) || 0
|
|
200
|
+
}];
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async activeTaskId() {
|
|
204
|
+
const row = this.db
|
|
205
|
+
.prepare("SELECT value FROM workspace_state WHERE key = 'active_task_id'")
|
|
206
|
+
.get();
|
|
207
|
+
if (!row) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const value = row.value.trim();
|
|
211
|
+
if (!value) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return TaskIdSchema.safeParse(value).success ? value : undefined;
|
|
215
|
+
}
|
|
216
|
+
async setActiveTaskId(taskId) {
|
|
217
|
+
const value = taskId?.trim() ?? "";
|
|
218
|
+
if (value && !TaskIdSchema.safeParse(value).success) {
|
|
219
|
+
throw new Error(`Invalid active task id: ${JSON.stringify(taskId)}`);
|
|
220
|
+
}
|
|
221
|
+
this.db.prepare(`INSERT INTO workspace_state (key, value)
|
|
222
|
+
VALUES ('active_task_id', ?)
|
|
223
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(value);
|
|
224
|
+
}
|
|
225
|
+
async workerNativeSessionId(taskId, workerId) {
|
|
226
|
+
const row = this.db
|
|
227
|
+
.prepare("SELECT native_session_id FROM workers WHERE task_id = ? AND worker_id = ?")
|
|
228
|
+
.get(taskId, workerId);
|
|
229
|
+
return row?.native_session_id ?? null;
|
|
230
|
+
}
|
|
128
231
|
async rebuildFromFiles() {
|
|
129
|
-
this.db.exec("
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
232
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
233
|
+
try {
|
|
234
|
+
this.db.exec("DELETE FROM native_sessions; DELETE FROM workers; DELETE FROM turns; DELETE FROM tasks;");
|
|
235
|
+
const sessions = join(this.projectRoot, this.dataDir, "sessions");
|
|
236
|
+
if (await pathExists(sessions)) {
|
|
237
|
+
const taskEntries = await readdir(sessions, { withFileTypes: true });
|
|
238
|
+
for (const taskEntry of taskEntries) {
|
|
239
|
+
if (!taskEntry.isDirectory()) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (taskEntry.name === "main") {
|
|
243
|
+
await this.rebuildWorkers(join(sessions, taskEntry.name), "main");
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (!TaskIdSchema.safeParse(taskEntry.name).success) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
await this.rebuildTask(join(sessions, taskEntry.name), taskEntry.name);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
this.db.exec("COMMIT");
|
|
133
253
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
continue;
|
|
254
|
+
catch (error) {
|
|
255
|
+
try {
|
|
256
|
+
this.db.exec("ROLLBACK");
|
|
138
257
|
}
|
|
139
|
-
|
|
258
|
+
catch {
|
|
259
|
+
// Preserve the filesystem or SQLite failure that interrupted rebuilding.
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
140
262
|
}
|
|
141
263
|
}
|
|
142
264
|
close() {
|
|
143
265
|
this.db.close();
|
|
144
266
|
}
|
|
267
|
+
ensureColumn(table, column, declaration) {
|
|
268
|
+
if (this.tableHasColumn(table, column)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${declaration}`);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (!this.tableHasColumn(table, column)) {
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
tableHasColumn(table, column) {
|
|
281
|
+
const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
|
|
282
|
+
return columns.some((entry) => entry.name === column);
|
|
283
|
+
}
|
|
145
284
|
async rebuildTask(taskDir, taskId) {
|
|
146
285
|
const metaPath = join(taskDir, "meta.json");
|
|
147
|
-
|
|
148
|
-
|
|
286
|
+
const meta = await pathExists(metaPath)
|
|
287
|
+
? await readJsonIfValid(metaPath, TaskMetaSchema)
|
|
288
|
+
: null;
|
|
289
|
+
if (!meta || meta.id !== taskId) {
|
|
290
|
+
return;
|
|
149
291
|
}
|
|
292
|
+
await this.upsertTask(meta);
|
|
150
293
|
const turnsDir = join(taskDir, "turns");
|
|
151
294
|
if (await pathExists(turnsDir)) {
|
|
152
295
|
const turnEntries = await readdir(turnsDir, { withFileTypes: true });
|
|
153
296
|
for (const turnEntry of turnEntries) {
|
|
297
|
+
if (!turnEntry.isDirectory() || !/^\d{4}$/.test(turnEntry.name)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
154
300
|
const turnPath = join(turnsDir, turnEntry.name, "turn.json");
|
|
155
|
-
|
|
156
|
-
|
|
301
|
+
const [turn, route, request] = await Promise.all([
|
|
302
|
+
readJsonIfValid(turnPath, TurnMetaSchema),
|
|
303
|
+
readJsonIfValid(join(turnsDir, turnEntry.name, "route.json"), RouteDecisionSchema),
|
|
304
|
+
readTextIfExists(join(turnsDir, turnEntry.name, "user.md"))
|
|
305
|
+
]);
|
|
306
|
+
if (turn
|
|
307
|
+
&& route
|
|
308
|
+
&& request.trim()
|
|
309
|
+
&& turn.task_id === taskId
|
|
310
|
+
&& turn.turn_id === turnEntry.name
|
|
311
|
+
&& turn.request_path === `turns/${turnEntry.name}/user.md`) {
|
|
312
|
+
await this.upsertTurn(taskId, turn);
|
|
157
313
|
}
|
|
158
314
|
}
|
|
159
315
|
}
|
|
160
|
-
|
|
316
|
+
await this.rebuildWorkers(taskDir, taskId);
|
|
317
|
+
}
|
|
318
|
+
async rebuildWorkers(sessionDir, sessionId) {
|
|
319
|
+
const entries = await readdir(sessionDir, { withFileTypes: true });
|
|
161
320
|
for (const entry of entries) {
|
|
162
321
|
if (!entry.isDirectory() || entry.name === "turns") {
|
|
163
322
|
continue;
|
|
164
323
|
}
|
|
165
|
-
const workerDir = join(
|
|
324
|
+
const workerDir = join(sessionDir, entry.name);
|
|
166
325
|
const statusPath = join(workerDir, "status.json");
|
|
326
|
+
const nativePath = join(workerDir, "native-session.json");
|
|
327
|
+
const retiredNativePath = join(workerDir, "native-session.retired.json");
|
|
328
|
+
const nativeSession = await this.readRebuildNativeSession(nativePath, retiredNativePath);
|
|
167
329
|
if (await pathExists(statusPath)) {
|
|
168
|
-
await this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
330
|
+
const status = await this.readRebuildWorkerStatus(statusPath, nativeSession);
|
|
331
|
+
if (status) {
|
|
332
|
+
await this.upsertWorker(sessionId, status, {
|
|
333
|
+
dir: workerDir,
|
|
334
|
+
statusPath,
|
|
335
|
+
outputLogPath: join(workerDir, "output.log")
|
|
336
|
+
});
|
|
337
|
+
}
|
|
173
338
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await this.upsertNativeSession(taskId, await readJson(nativePath, NativeSessionSchema));
|
|
339
|
+
if (nativeSession) {
|
|
340
|
+
await this.upsertNativeSession(sessionId, nativeSession);
|
|
177
341
|
}
|
|
178
342
|
}
|
|
179
343
|
}
|
|
344
|
+
async readRebuildWorkerStatus(statusPath, nativeSession) {
|
|
345
|
+
const status = await readJsonIfValid(statusPath, WorkerStatusSchema);
|
|
346
|
+
if (!status) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
if (status.native_session_id && status.native_session_id !== nativeSession?.session_id) {
|
|
350
|
+
const nextStatus = { ...status };
|
|
351
|
+
delete nextStatus.native_session_id;
|
|
352
|
+
return WorkerStatusSchema.parse(nextStatus);
|
|
353
|
+
}
|
|
354
|
+
return status;
|
|
355
|
+
}
|
|
356
|
+
async readRebuildNativeSession(nativePath, retiredNativePath) {
|
|
357
|
+
const active = await readJsonIfValid(nativePath, NativeSessionSchema);
|
|
358
|
+
if (!active) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const retired = await readJsonIfValid(retiredNativePath, RetiredNativeSessionSchema);
|
|
362
|
+
return retired?.session_id === active.session_id ? null : active;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function readJsonIfValid(path, schema) {
|
|
366
|
+
if (!(await pathExists(path))) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
return await readJson(path, schema);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
180
375
|
}
|