parallel-codex-tui 0.1.3 → 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 +240 -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-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 +7 -71
- package/dist/cli.js +221 -23
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +154 -24
- 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 +188 -37
- package/dist/core/session-manager.js +1086 -40
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +343 -23
- 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 +1749 -202
- 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 +2830 -153
- 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 +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 +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +78 -3
- package/dist/workers/process-adapter.js +570 -77
- package/dist/workers/registry.js +4 -2
- package/package.json +5 -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
|
|
@@ -124,10 +134,94 @@ export class SessionIndex {
|
|
|
124
134
|
async deleteNativeSession(taskId, workerId) {
|
|
125
135
|
this.db.prepare("DELETE FROM native_sessions WHERE task_id = ? AND worker_id = ?").run(taskId, workerId);
|
|
126
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
|
+
}
|
|
127
153
|
async countRows(table) {
|
|
128
154
|
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
129
155
|
return row.count;
|
|
130
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
|
+
}
|
|
131
225
|
async workerNativeSessionId(taskId, workerId) {
|
|
132
226
|
const row = this.db
|
|
133
227
|
.prepare("SELECT native_session_id FROM workers WHERE task_id = ? AND worker_id = ?")
|
|
@@ -135,81 +229,138 @@ export class SessionIndex {
|
|
|
135
229
|
return row?.native_session_id ?? null;
|
|
136
230
|
}
|
|
137
231
|
async rebuildFromFiles() {
|
|
138
|
-
this.db.exec("
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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");
|
|
142
253
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
continue;
|
|
254
|
+
catch (error) {
|
|
255
|
+
try {
|
|
256
|
+
this.db.exec("ROLLBACK");
|
|
147
257
|
}
|
|
148
|
-
|
|
258
|
+
catch {
|
|
259
|
+
// Preserve the filesystem or SQLite failure that interrupted rebuilding.
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
149
262
|
}
|
|
150
263
|
}
|
|
151
264
|
close() {
|
|
152
265
|
this.db.close();
|
|
153
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
|
+
}
|
|
154
284
|
async rebuildTask(taskDir, taskId) {
|
|
155
285
|
const metaPath = join(taskDir, "meta.json");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
286
|
+
const meta = await pathExists(metaPath)
|
|
287
|
+
? await readJsonIfValid(metaPath, TaskMetaSchema)
|
|
288
|
+
: null;
|
|
289
|
+
if (!meta || meta.id !== taskId) {
|
|
290
|
+
return;
|
|
161
291
|
}
|
|
292
|
+
await this.upsertTask(meta);
|
|
162
293
|
const turnsDir = join(taskDir, "turns");
|
|
163
294
|
if (await pathExists(turnsDir)) {
|
|
164
295
|
const turnEntries = await readdir(turnsDir, { withFileTypes: true });
|
|
165
296
|
for (const turnEntry of turnEntries) {
|
|
297
|
+
if (!turnEntry.isDirectory() || !/^\d{4}$/.test(turnEntry.name)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
166
300
|
const turnPath = join(turnsDir, turnEntry.name, "turn.json");
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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);
|
|
172
313
|
}
|
|
173
314
|
}
|
|
174
315
|
}
|
|
175
|
-
|
|
316
|
+
await this.rebuildWorkers(taskDir, taskId);
|
|
317
|
+
}
|
|
318
|
+
async rebuildWorkers(sessionDir, sessionId) {
|
|
319
|
+
const entries = await readdir(sessionDir, { withFileTypes: true });
|
|
176
320
|
for (const entry of entries) {
|
|
177
321
|
if (!entry.isDirectory() || entry.name === "turns") {
|
|
178
322
|
continue;
|
|
179
323
|
}
|
|
180
|
-
const workerDir = join(
|
|
324
|
+
const workerDir = join(sessionDir, entry.name);
|
|
181
325
|
const statusPath = join(workerDir, "status.json");
|
|
182
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);
|
|
183
329
|
if (await pathExists(statusPath)) {
|
|
184
|
-
const status = await this.readRebuildWorkerStatus(statusPath,
|
|
330
|
+
const status = await this.readRebuildWorkerStatus(statusPath, nativeSession);
|
|
185
331
|
if (status) {
|
|
186
|
-
await this.upsertWorker(
|
|
332
|
+
await this.upsertWorker(sessionId, status, {
|
|
187
333
|
dir: workerDir,
|
|
188
334
|
statusPath,
|
|
189
335
|
outputLogPath: join(workerDir, "output.log")
|
|
190
336
|
});
|
|
191
337
|
}
|
|
192
338
|
}
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
if (nativeSession) {
|
|
196
|
-
await this.upsertNativeSession(taskId, nativeSession);
|
|
197
|
-
}
|
|
339
|
+
if (nativeSession) {
|
|
340
|
+
await this.upsertNativeSession(sessionId, nativeSession);
|
|
198
341
|
}
|
|
199
342
|
}
|
|
200
343
|
}
|
|
201
|
-
async readRebuildWorkerStatus(statusPath,
|
|
344
|
+
async readRebuildWorkerStatus(statusPath, nativeSession) {
|
|
202
345
|
const status = await readJsonIfValid(statusPath, WorkerStatusSchema);
|
|
203
346
|
if (!status) {
|
|
204
347
|
return null;
|
|
205
348
|
}
|
|
206
|
-
if (status.native_session_id &&
|
|
349
|
+
if (status.native_session_id && status.native_session_id !== nativeSession?.session_id) {
|
|
207
350
|
const nextStatus = { ...status };
|
|
208
351
|
delete nextStatus.native_session_id;
|
|
209
352
|
return WorkerStatusSchema.parse(nextStatus);
|
|
210
353
|
}
|
|
211
354
|
return status;
|
|
212
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
|
+
}
|
|
213
364
|
}
|
|
214
365
|
async function readJsonIfValid(path, schema) {
|
|
215
366
|
if (!(await pathExists(path))) {
|