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.
Files changed (74) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +269 -12
  3. package/dist/bootstrap.js +50 -18
  4. package/dist/cli-args.js +96 -14
  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 +40 -0
  10. package/dist/cli.js +291 -35
  11. package/dist/core/app-root.js +8 -0
  12. package/dist/core/collaboration-timeline.js +261 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +191 -23
  15. package/dist/core/file-store.js +130 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +10 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +473 -42
  25. package/dist/core/session-index.js +225 -30
  26. package/dist/core/session-manager.js +1182 -44
  27. package/dist/core/task-state-machine.js +17 -0
  28. package/dist/core/workspace-commit-recovery.js +118 -0
  29. package/dist/core/workspace.js +126 -0
  30. package/dist/doctor.js +384 -30
  31. package/dist/domain/schemas.js +127 -6
  32. package/dist/orchestrator/collaboration-channel.js +255 -4
  33. package/dist/orchestrator/feature-plan.js +70 -0
  34. package/dist/orchestrator/judge-artifacts.js +236 -0
  35. package/dist/orchestrator/orchestrator.js +1777 -212
  36. package/dist/orchestrator/prompts.js +126 -2
  37. package/dist/orchestrator/supervisor-summary.js +56 -2
  38. package/dist/orchestrator/workspace-sandbox.js +911 -0
  39. package/dist/tui/App.js +2838 -159
  40. package/dist/tui/AppShell.js +188 -23
  41. package/dist/tui/CollaborationTimelineView.js +327 -0
  42. package/dist/tui/FeatureBoardView.js +227 -0
  43. package/dist/tui/InputBar.js +514 -57
  44. package/dist/tui/RouterDiagnosticsView.js +469 -0
  45. package/dist/tui/StatusBar.js +610 -57
  46. package/dist/tui/TaskSessionsView.js +207 -0
  47. package/dist/tui/TerminalOutput.js +53 -9
  48. package/dist/tui/WorkerOutputView.js +1403 -161
  49. package/dist/tui/WorkerOverviewView.js +250 -0
  50. package/dist/tui/chat-history.js +25 -0
  51. package/dist/tui/chat-input.js +67 -19
  52. package/dist/tui/chat-paste.js +76 -0
  53. package/dist/tui/display-width.js +41 -3
  54. package/dist/tui/incremental-text-file.js +101 -0
  55. package/dist/tui/keyboard.js +46 -0
  56. package/dist/tui/markdown-text.js +14 -0
  57. package/dist/tui/raw-input-decoder.js +3 -0
  58. package/dist/tui/scrolling.js +2 -1
  59. package/dist/tui/status-line.js +318 -11
  60. package/dist/tui/task-memory.js +15 -0
  61. package/dist/tui/task-result.js +105 -0
  62. package/dist/tui/terminal-screen.js +13 -1
  63. package/dist/tui/theme-contrast.js +144 -0
  64. package/dist/tui/theme-preview.js +109 -0
  65. package/dist/tui/theme.js +158 -0
  66. package/dist/version.js +1 -1
  67. package/dist/workers/capabilities.js +212 -0
  68. package/dist/workers/live-probe.js +176 -0
  69. package/dist/workers/mock-adapter.js +39 -6
  70. package/dist/workers/native-attach.js +147 -8
  71. package/dist/workers/native-session-detection.js +17 -0
  72. package/dist/workers/process-adapter.js +580 -81
  73. package/dist/workers/registry.js +4 -2
  74. 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
- .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
@@ -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("DELETE FROM native_sessions; DELETE FROM workers; DELETE FROM turns; DELETE FROM tasks;");
130
- const sessions = join(this.projectRoot, this.dataDir, "sessions");
131
- if (!(await pathExists(sessions))) {
132
- 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");
133
253
  }
134
- const taskEntries = await readdir(sessions, { withFileTypes: true });
135
- for (const taskEntry of taskEntries) {
136
- if (!taskEntry.isDirectory() || !taskEntry.name.startsWith("task-")) {
137
- continue;
254
+ catch (error) {
255
+ try {
256
+ this.db.exec("ROLLBACK");
138
257
  }
139
- 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;
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
- if (await pathExists(metaPath)) {
148
- await this.upsertTask(await readJson(metaPath, TaskMetaSchema));
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
- if (turnEntry.isDirectory() && (await pathExists(turnPath))) {
156
- await this.upsertTurn(taskId, await readJson(turnPath, TurnMetaSchema));
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
- 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 });
161
320
  for (const entry of entries) {
162
321
  if (!entry.isDirectory() || entry.name === "turns") {
163
322
  continue;
164
323
  }
165
- const workerDir = join(taskDir, entry.name);
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.upsertWorker(taskId, await readJson(statusPath, WorkerStatusSchema), {
169
- dir: workerDir,
170
- statusPath,
171
- outputLogPath: join(workerDir, "output.log")
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
- const nativePath = join(workerDir, "native-session.json");
175
- if (await pathExists(nativePath)) {
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
  }