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.
Files changed (72) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +240 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +7 -71
  10. package/dist/cli.js +221 -23
  11. package/dist/core/collaboration-timeline.js +261 -0
  12. package/dist/core/config-errors.js +14 -0
  13. package/dist/core/config.js +154 -24
  14. package/dist/core/file-store.js +119 -6
  15. package/dist/core/lease-finalization.js +22 -0
  16. package/dist/core/paths.js +7 -0
  17. package/dist/core/process-identity.js +48 -0
  18. package/dist/core/process-mutation-turn.js +128 -0
  19. package/dist/core/process-ownership.js +276 -0
  20. package/dist/core/process-tree.js +90 -0
  21. package/dist/core/router-audit.js +155 -0
  22. package/dist/core/router-redaction.js +31 -0
  23. package/dist/core/router.js +462 -35
  24. package/dist/core/session-index.js +188 -37
  25. package/dist/core/session-manager.js +1086 -40
  26. package/dist/core/task-state-machine.js +17 -0
  27. package/dist/core/workspace-commit-recovery.js +118 -0
  28. package/dist/core/workspace.js +19 -11
  29. package/dist/doctor.js +343 -23
  30. package/dist/domain/schemas.js +127 -6
  31. package/dist/orchestrator/collaboration-channel.js +255 -4
  32. package/dist/orchestrator/feature-plan.js +70 -0
  33. package/dist/orchestrator/judge-artifacts.js +236 -0
  34. package/dist/orchestrator/orchestrator.js +1749 -202
  35. package/dist/orchestrator/prompts.js +126 -2
  36. package/dist/orchestrator/supervisor-summary.js +56 -2
  37. package/dist/orchestrator/workspace-sandbox.js +911 -0
  38. package/dist/tui/App.js +2830 -153
  39. package/dist/tui/AppShell.js +188 -23
  40. package/dist/tui/CollaborationTimelineView.js +327 -0
  41. package/dist/tui/FeatureBoardView.js +227 -0
  42. package/dist/tui/InputBar.js +514 -57
  43. package/dist/tui/RouterDiagnosticsView.js +469 -0
  44. package/dist/tui/StatusBar.js +610 -57
  45. package/dist/tui/TaskSessionsView.js +207 -0
  46. package/dist/tui/TerminalOutput.js +53 -9
  47. package/dist/tui/WorkerOutputView.js +1403 -161
  48. package/dist/tui/WorkerOverviewView.js +250 -0
  49. package/dist/tui/chat-history.js +25 -0
  50. package/dist/tui/chat-input.js +67 -19
  51. package/dist/tui/chat-paste.js +76 -0
  52. package/dist/tui/display-width.js +41 -3
  53. package/dist/tui/incremental-text-file.js +101 -0
  54. package/dist/tui/keyboard.js +46 -0
  55. package/dist/tui/markdown-text.js +14 -0
  56. package/dist/tui/raw-input-decoder.js +3 -0
  57. package/dist/tui/scrolling.js +2 -1
  58. package/dist/tui/status-line.js +318 -11
  59. package/dist/tui/task-memory.js +13 -1
  60. package/dist/tui/task-result.js +105 -0
  61. package/dist/tui/terminal-screen.js +13 -1
  62. package/dist/tui/theme-contrast.js +144 -0
  63. package/dist/tui/theme-preview.js +109 -0
  64. package/dist/tui/theme.js +158 -0
  65. package/dist/version.js +1 -1
  66. package/dist/workers/capabilities.js +212 -0
  67. package/dist/workers/live-probe.js +176 -0
  68. package/dist/workers/mock-adapter.js +39 -6
  69. package/dist/workers/native-attach.js +78 -3
  70. package/dist/workers/process-adapter.js +570 -77
  71. package/dist/workers/registry.js +4 -2
  72. 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
- .run(task.id, task.title, task.created_at, task.cwd, task.mode, task.status);
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("DELETE FROM native_sessions; DELETE FROM workers; DELETE FROM turns; DELETE FROM tasks;");
139
- const sessions = join(this.projectRoot, this.dataDir, "sessions");
140
- if (!(await pathExists(sessions))) {
141
- return;
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
- const taskEntries = await readdir(sessions, { withFileTypes: true });
144
- for (const taskEntry of taskEntries) {
145
- if (!taskEntry.isDirectory() || !taskEntry.name.startsWith("task-")) {
146
- continue;
254
+ catch (error) {
255
+ try {
256
+ this.db.exec("ROLLBACK");
147
257
  }
148
- await this.rebuildTask(join(sessions, taskEntry.name), taskEntry.name);
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
- if (await pathExists(metaPath)) {
157
- const meta = await readJsonIfValid(metaPath, TaskMetaSchema);
158
- if (meta) {
159
- await this.upsertTask(meta);
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
- if (turnEntry.isDirectory() && (await pathExists(turnPath))) {
168
- const turn = await readJsonIfValid(turnPath, TurnMetaSchema);
169
- if (turn) {
170
- await this.upsertTurn(taskId, turn);
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
- const entries = await readdir(taskDir, { withFileTypes: true });
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(taskDir, entry.name);
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, nativePath);
330
+ const status = await this.readRebuildWorkerStatus(statusPath, nativeSession);
185
331
  if (status) {
186
- await this.upsertWorker(taskId, status, {
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 (await pathExists(nativePath)) {
194
- const nativeSession = await readJsonIfValid(nativePath, NativeSessionSchema);
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, nativePath) {
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 && !(await readJsonIfValid(nativePath, NativeSessionSchema))) {
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))) {