parallel-codex-tui 0.1.4 → 0.1.6
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 +46 -0
- package/README.md +96 -19
- package/dist/cli-startup-preflight.js +18 -0
- package/dist/cli-startup-recovery.js +13 -1
- package/dist/cli.js +19 -7
- package/dist/core/clipboard.js +97 -0
- package/dist/core/collaboration-timeline.js +9 -2
- package/dist/core/config.js +161 -103
- package/dist/core/router.js +1 -1
- package/dist/core/session-index.js +234 -61
- package/dist/core/session-manager.js +25 -1
- package/dist/core/task-session-details.js +175 -0
- package/dist/core/task-state-machine.js +10 -9
- package/dist/doctor.js +58 -39
- package/dist/domain/schemas.js +15 -1
- package/dist/orchestrator/collaboration-channel.js +35 -3
- package/dist/orchestrator/final-acceptance.js +86 -0
- package/dist/orchestrator/orchestrator.js +405 -69
- package/dist/orchestrator/prompts.js +42 -3
- package/dist/orchestrator/workspace-sandbox.js +16 -0
- package/dist/tui/App.js +514 -56
- package/dist/tui/AppShell.js +9 -3
- package/dist/tui/FeatureBoardView.js +7 -2
- package/dist/tui/InputBar.js +87 -15
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusDetailView.js +164 -0
- package/dist/tui/TaskSessionDetailView.js +222 -0
- package/dist/tui/TaskSessionsView.js +5 -2
- package/dist/tui/WorkerOutputView.js +6 -1
- package/dist/tui/WorkerOverviewView.js +23 -4
- package/dist/tui/keyboard.js +6 -0
- package/dist/tui/status-line.js +42 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +4 -3
- package/dist/workers/live-probe.js +4 -3
- package/dist/workers/mock-adapter.js +37 -5
- package/dist/workers/native-attach.js +32 -17
- package/dist/workers/process-adapter.js +2 -0
- package/dist/workers/provider.js +26 -0
- package/dist/workers/registry.js +12 -22
- package/package.json +7 -1
|
@@ -30,6 +30,8 @@ fallback = "simple"
|
|
|
30
30
|
# Maximum isolated Actor or Critic feature workers running at the same time (1..8).
|
|
31
31
|
# Each feature receives its own snapshot; a combined Wave Critic approves staged changes before atomic live commit.
|
|
32
32
|
maxParallelFeatures = 3
|
|
33
|
+
# Maximum Actor/Critic revision rounds before the task is stopped for manual intervention (1..10).
|
|
34
|
+
maxRevisionRounds = 3
|
|
33
35
|
|
|
34
36
|
[workers.codex]
|
|
35
37
|
command = "codex"
|
|
@@ -63,6 +65,8 @@ freshSessionArgs = []
|
|
|
63
65
|
[workers.codex.interactive]
|
|
64
66
|
command = "codex"
|
|
65
67
|
args = ["resume", "{sessionId}"]
|
|
68
|
+
# Used by the Task session detail view when branching from a recorded native session.
|
|
69
|
+
forkArgs = ["fork", "{sessionId}"]
|
|
66
70
|
|
|
67
71
|
[workers.codex.nativeSession]
|
|
68
72
|
fallback = "new"
|
|
@@ -93,6 +97,7 @@ freshSessionArgs = ["--session-id", "{sessionId}"]
|
|
|
93
97
|
[workers.claude.interactive]
|
|
94
98
|
command = "claude"
|
|
95
99
|
args = ["--resume", "{sessionId}"]
|
|
100
|
+
forkArgs = ["--resume", "{sessionId}", "--fork-session"]
|
|
96
101
|
|
|
97
102
|
[workers.claude.nativeSession]
|
|
98
103
|
fallback = "new"
|
|
@@ -106,6 +111,47 @@ profile = "generic"
|
|
|
106
111
|
writableDirArgs = []
|
|
107
112
|
freshSessionArgs = []
|
|
108
113
|
|
|
114
|
+
# Add any number of named Worker providers. IDs use lowercase letters, digits, or _.
|
|
115
|
+
# A profile can inherit the Codex/Claude command protocol and override only role-specific values.
|
|
116
|
+
# [workers.openai_compat]
|
|
117
|
+
# extends = "codex"
|
|
118
|
+
# command = "openai-compatible-coder"
|
|
119
|
+
# assignable = true
|
|
120
|
+
#
|
|
121
|
+
# [workers.openai_compat.model]
|
|
122
|
+
# name = "third-party-model"
|
|
123
|
+
# provider = "openai-compatible"
|
|
124
|
+
# args = ["--model", "{model}", "--provider", "{provider}"]
|
|
125
|
+
#
|
|
126
|
+
# [workers.openai_compat.model.env]
|
|
127
|
+
# OPENAI_API_KEY = "{env:OPENAI_COMPAT_API_KEY}"
|
|
128
|
+
# OPENAI_BASE_URL = "{env:OPENAI_COMPAT_BASE_URL}"
|
|
129
|
+
#
|
|
130
|
+
# [workers.openai_compat.interactive]
|
|
131
|
+
# command = "openai-compatible-coder"
|
|
132
|
+
#
|
|
133
|
+
# [workers.anthropic_compat]
|
|
134
|
+
# extends = "claude"
|
|
135
|
+
# command = "anthropic-compatible-coder"
|
|
136
|
+
#
|
|
137
|
+
# [workers.anthropic_compat.model]
|
|
138
|
+
# name = "third-party-claude-model"
|
|
139
|
+
# provider = "anthropic-compatible"
|
|
140
|
+
#
|
|
141
|
+
# [workers.anthropic_compat.model.env]
|
|
142
|
+
# ANTHROPIC_API_KEY = "{env:ANTHROPIC_COMPAT_API_KEY}"
|
|
143
|
+
# ANTHROPIC_BASE_URL = "{env:ANTHROPIC_COMPAT_BASE_URL}"
|
|
144
|
+
#
|
|
145
|
+
# [workers.anthropic_compat.interactive]
|
|
146
|
+
# command = "anthropic-compatible-coder"
|
|
147
|
+
|
|
148
|
+
# A custom CLI starts from conservative generic defaults: isolated cwd, no injected
|
|
149
|
+
# vendor flags, and native resume disabled until explicitly configured.
|
|
150
|
+
# [workers.vendor]
|
|
151
|
+
# extends = "generic"
|
|
152
|
+
# command = "vendor-coder"
|
|
153
|
+
# args = ["run", "--stdin"]
|
|
154
|
+
|
|
109
155
|
[pairing]
|
|
110
156
|
main = "claude"
|
|
111
157
|
judge = "codex"
|
package/README.md
CHANGED
|
@@ -4,6 +4,23 @@ A standalone TypeScript TUI wrapper for routed parallel coding workflows. It kee
|
|
|
4
4
|
|
|
5
5
|
Built with Codex-assisted development.
|
|
6
6
|
|
|
7
|
+
## Current Release
|
|
8
|
+
|
|
9
|
+
`v0.1.6` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.1.6) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.1.6). It keeps terminal scrolling and copying available at the same time: mouse reporting now matches Claude Code's click/wheel protocol (`1000` + `1006`) without enabling drag-event tracking (`1002`), so normal terminal selection and system copy remain available while the app receives wheel input. `Ctrl+Y` is an additional fallback that copies the visible chat, rendered Worker log, native Agent screen, or structured overview without changing mouse mode. Unicode text and Diff indentation are preserved.
|
|
10
|
+
|
|
11
|
+
The release also includes the stability, collaboration, session-management, Provider, TUI, and release work completed in `v0.1.5`:
|
|
12
|
+
|
|
13
|
+
- Runtime preflight checks Codex, Claude, named third-party Providers, proxy reachability, workspace access, CLI capabilities, and native attach policy before work starts.
|
|
14
|
+
- Judge produces validated requirements, acceptance criteria, and a dependency-aware Feature plan. Actor/Critic pairs exchange checked JSONL findings and replies across revision rounds, followed by Wave Critic and final Judge acceptance.
|
|
15
|
+
- Tasks retain every Turn, Worker log, native session binding, model snapshot, and collaboration artifact across follow-ups and restarts. Sessions can be inspected, continued, forked, renamed, archived, exported, restored, or deleted from the TUI.
|
|
16
|
+
- Named Worker Providers support Codex-compatible, Claude-compatible, OpenAI-compatible, Anthropic-compatible, and custom generic commands with independent role, model, environment, permission, resume, and interactive settings.
|
|
17
|
+
- Worker overview, Feature board, collaboration timeline, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
|
|
18
|
+
- SQLite migrations and recovery backups, owned-process cleanup, ten-turn Worker-history recovery, packaged CLI installation, and cross-platform CI are covered by automated tests.
|
|
19
|
+
|
|
20
|
+
Release acceptance included a real three-Feature Tetris task with parallel Actor/Critic waves, final integration review, 43 project tests, and a clean build. The repository suite passed 1,240 tests across 119 files, including PTY coverage proving wheel delivery without drag-event tracking. The npm package was independently installed and returned `parallel-codex-tui 0.1.6`.
|
|
21
|
+
|
|
22
|
+
Real Provider probes still depend on valid local CLI credentials. In particular, authenticate the Claude CLI before selecting a Claude-compatible Worker, then run `parallel-codex-tui --doctor --probe-agents` to prove fresh and resumed calls on that machine.
|
|
23
|
+
|
|
7
24
|
## Requirements
|
|
8
25
|
|
|
9
26
|
- Node.js 24.15+.
|
|
@@ -39,6 +56,7 @@ Startup resolves the worker project before routing:
|
|
|
39
56
|
- Without `--workspace`, an interactive terminal shows remembered projects from `.parallel-codex/workspaces.json`; use Up/Down and Enter, a displayed number, or `N` to enter a new folder path. The picker clears before the main chat opens.
|
|
40
57
|
- In non-interactive startup, the CLI reuses the last remembered workspace, falls back to the current directory if none was saved, and creates an explicit `--workspace` path if needed.
|
|
41
58
|
- The selected workspace is prepared before any router or worker process starts.
|
|
59
|
+
- Every interactive open and in-place workspace switch runs a quota-free startup preflight before the TUI accepts work. It checks workspace read/write/search access, every CLI required by the active route and role configuration, referenced model environment variables, each named Worker's Codex, Claude, or declared generic CLI capabilities, configured proxy endpoint reachability, and native workspace trust policy. Healthy checks stay quiet; warnings and failures appear in chat with the `parallel-codex-tui --doctor` action. Live model requests remain opt-in through `--probe-agents` and `--probe-router`.
|
|
42
60
|
- While the TUI is idle, press `Ctrl+P` to open the same project picker without exiting. `Esc` returns with the unsent draft intact; selecting or creating a folder locks duplicate picker input, shows the project being opened, rebuilds the runtime in place, and restores that workspace's latest task, route, workers, and chat. A failed open returns to the original view without replacing its runtime or draft.
|
|
43
61
|
- Workspace chat, task files, and `session-index.sqlite` stay isolated under each project. Router configuration and `router/routes.jsonl` remain shared under the app root, so classifications across projects use one global Router audit.
|
|
44
62
|
|
|
@@ -68,7 +86,7 @@ parallel-codex-tui --doctor --probe-router
|
|
|
68
86
|
parallel-codex-tui --version
|
|
69
87
|
```
|
|
70
88
|
|
|
71
|
-
`--doctor` checks configured automated and interactive commands, `{env:NAME}` references, and the CLI help surfaces required for Codex exec/resume sandboxing and Claude print/resume permissions before workers start. Recognized standard CLI help that lacks a required option fails the check; an opaque wrapper keeps the compatible built-in profile's unverified warning, while an explicit `generic` capability contract is validated without guessing vendor-specific help. Doctor
|
|
89
|
+
`--doctor` checks configured automated and interactive commands, `{env:NAME}` references, and the CLI help surfaces required for Codex exec/resume sandboxing and Claude print/resume permissions before workers start. Recognized standard CLI help that lacks a required option fails the check; an opaque wrapper keeps the compatible built-in profile's unverified warning, while an explicit `generic` capability contract is validated without guessing vendor-specific help. Doctor performs these checks for every named Worker Provider used by the active route, rejects an explicitly read-only Codex-compatible interactive sandbox when feature attach needs writable roots, and reminds you that native workspace trust remains an interactive decision. It reports proxy host/port reachability as a local-endpoint check, not as proof that the proxy upstream or model API is healthy. Add `--probe-agents` to make one minimal fresh request and, when configured, one same-session resume request through every active process Worker Provider. This explicit probe uses model quota, removes successful probe artifacts, preserves failed artifacts under `.parallel-codex/probes/`, and exits non-zero when a fresh or resumed request cannot be proven. Fresh sessions use the configured `freshSessionArgs`; Claude-compatible profiles default to a generated native `--session-id`, persisted only after the process emits output, so a silent failed launch cannot create a false resumable session. Add `--probe-router` to run one real classification through the configured Codex Router; the command exits non-zero when that live request falls back or fails. Doctor also reports the loaded TUI theme, core palette values, ANSI swatch previews, and color override values, including any temporary `--theme` override.
|
|
72
90
|
|
|
73
91
|
## Quick Start
|
|
74
92
|
|
|
@@ -107,10 +125,13 @@ Limit how many feature-level Actor or Critic agents may run at once:
|
|
|
107
125
|
```toml
|
|
108
126
|
[orchestration]
|
|
109
127
|
maxParallelFeatures = 3 # 1..8
|
|
128
|
+
maxRevisionRounds = 3 # 1..10
|
|
110
129
|
```
|
|
111
130
|
|
|
112
131
|
Judge may plan up to eight dependency-aware features. The orchestrator keeps dependency order while running at most this many agents concurrently. When one worker fails, already-running peers are allowed to finish cleanly and queued features are not started.
|
|
113
132
|
|
|
133
|
+
Actor and Critic reuse their native sessions across revision rounds. Blocking findings and Actor replies remain in the Feature JSONL mailbox; the task stops with an explicit error when `maxRevisionRounds` is exhausted.
|
|
134
|
+
|
|
114
135
|
Queued Feature workers remain `queued` until they acquire a concurrency slot. Their persisted state changes to `actor_running` or `critic_running` only after the Worker is registered as cancellable, then to `actor_done` or `critic_done` before that active registration is removed. As a result, only `actor_running` and `critic_running` expose Feature cancellation; queued and role-complete Features cannot present a cancel action that has no live process behind it.
|
|
115
136
|
|
|
116
137
|
## Theme
|
|
@@ -262,7 +283,7 @@ ALL_PROXY = "socks5h://127.0.0.1:7890"
|
|
|
262
283
|
NO_PROXY = "localhost,127.0.0.1"
|
|
263
284
|
```
|
|
264
285
|
|
|
265
|
-
`router.codex.env` applies only to semantic classification. `workers
|
|
286
|
+
`router.codex.env` applies only to semantic classification. `workers.<id>.model.env` applies to fresh/resumed runs and embedded native attach for that named Worker Provider. Keep these values in local `config.toml`, which is ignored by Git.
|
|
266
287
|
|
|
267
288
|
Run `parallel-codex-tui --doctor` after changing proxy settings. Doctor checks referenced environment variables, reports when a macOS system proxy is not inherited by Codex subprocesses, and labels configured proxy host/port checks as local-endpoint reachability without printing credentials. Then run `parallel-codex-tui --doctor --probe-router` when you also need to verify the real Codex Router path through that proxy; this explicit probe can take up to `router.codex.timeoutMs`. A timed-out request is identified as `first output timeout`, `idle timeout after stdout/stderr`, or `total timeout`; `via <proxy-host:port>` remains context, not a claim that the proxy caused the failure. The audit never exposes the proxy URL or credentials.
|
|
268
289
|
|
|
@@ -302,6 +323,7 @@ freshSessionArgs = []
|
|
|
302
323
|
[workers.codex.interactive]
|
|
303
324
|
command = "codex"
|
|
304
325
|
args = ["resume", "{sessionId}"]
|
|
326
|
+
forkArgs = ["fork", "{sessionId}"]
|
|
305
327
|
|
|
306
328
|
[workers.claude]
|
|
307
329
|
command = "claude"
|
|
@@ -315,31 +337,81 @@ freshSessionArgs = ["--session-id", "{sessionId}"]
|
|
|
315
337
|
[workers.claude.interactive]
|
|
316
338
|
command = "claude"
|
|
317
339
|
args = ["--resume", "{sessionId}"]
|
|
340
|
+
forkArgs = ["--resume", "{sessionId}", "--fork-session"]
|
|
318
341
|
```
|
|
319
342
|
|
|
320
343
|
`model.args` and `model.env` apply to both automated worker runs and embedded native attach sessions. Native attach appends the rendered model arguments after `interactive.args`, so third-party `{model}` and `{provider}` selections remain active when you press `Ctrl+O`.
|
|
321
344
|
|
|
322
|
-
`model.provider` names the remote model service; `capabilities.profile` describes the local CLI protocol.
|
|
345
|
+
`model.provider` names the remote model service; `capabilities.profile` describes the local CLI protocol. Worker Provider IDs are lowercase names using letters, digits, or `_`; excluding `-` keeps generated Worker directory names unambiguous. Existing `codex`, `claude`, and `mock` IDs remain compatible, while additional profiles can inherit `codex`, `claude`, another named profile, or conservative `generic` defaults.
|
|
346
|
+
|
|
347
|
+
Each role can select an independent command, model, environment, and permission contract through `[pairing]`. This example uses an OpenAI-compatible coding CLI for Judge/Actor and an Anthropic-compatible coding CLI for Main/Critic:
|
|
323
348
|
|
|
324
349
|
```toml
|
|
325
|
-
[workers.
|
|
350
|
+
[workers.openai_compat]
|
|
351
|
+
extends = "codex"
|
|
352
|
+
command = "openai-compatible-coder"
|
|
353
|
+
args = ["exec", "--sandbox", "workspace-write", "-"]
|
|
354
|
+
|
|
355
|
+
[workers.openai_compat.model]
|
|
356
|
+
name = "third-party-code-model"
|
|
357
|
+
provider = "openai-compatible"
|
|
358
|
+
args = ["--model", "{model}", "--provider", "{provider}"]
|
|
359
|
+
|
|
360
|
+
[workers.openai_compat.model.env]
|
|
361
|
+
OPENAI_API_KEY = "{env:OPENAI_COMPAT_API_KEY}"
|
|
362
|
+
OPENAI_BASE_URL = "{env:OPENAI_COMPAT_BASE_URL}"
|
|
363
|
+
|
|
364
|
+
[workers.openai_compat.interactive]
|
|
365
|
+
command = "openai-compatible-coder"
|
|
366
|
+
|
|
367
|
+
[workers.anthropic_compat]
|
|
368
|
+
extends = "claude"
|
|
369
|
+
command = "anthropic-compatible-coder"
|
|
370
|
+
|
|
371
|
+
[workers.anthropic_compat.model]
|
|
372
|
+
name = "third-party-claude-model"
|
|
373
|
+
provider = "anthropic-compatible"
|
|
374
|
+
|
|
375
|
+
[workers.anthropic_compat.model.env]
|
|
376
|
+
ANTHROPIC_API_KEY = "{env:ANTHROPIC_COMPAT_API_KEY}"
|
|
377
|
+
ANTHROPIC_BASE_URL = "{env:ANTHROPIC_COMPAT_BASE_URL}"
|
|
378
|
+
|
|
379
|
+
[workers.anthropic_compat.interactive]
|
|
380
|
+
command = "anthropic-compatible-coder"
|
|
381
|
+
|
|
382
|
+
[pairing]
|
|
383
|
+
main = "anthropic_compat"
|
|
384
|
+
judge = "openai_compat"
|
|
385
|
+
actor = "openai_compat"
|
|
386
|
+
critic = "anthropic_compat"
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
`extends` inherits the complete command protocol, including capabilities, native-session behavior, watchdogs, and interactive arguments. Override both `command` and `interactive.command` when a wrapper replaces both executables. `assignable = true` exposes a profile to Feature-level Provider cycling; custom profiles default to true, while the reserved deterministic `mock` profile defaults to false. Pairing an unknown profile, unsafe profile IDs, and inheritance cycles fail config validation before startup.
|
|
390
|
+
|
|
391
|
+
For a third-party command with its own syntax, inherit `generic` so parallel-codex-tui does not inject `--sandbox`, `--permission-mode`, or other built-in-only flags:
|
|
392
|
+
|
|
393
|
+
```toml
|
|
394
|
+
[workers.vendor]
|
|
395
|
+
extends = "generic"
|
|
326
396
|
command = "vendor-coder"
|
|
327
397
|
args = ["run", "--stdin"]
|
|
328
398
|
|
|
329
|
-
[workers.
|
|
399
|
+
[workers.vendor.capabilities]
|
|
330
400
|
profile = "generic"
|
|
331
401
|
writableDirArgs = ["--allow-root", "{dir}"]
|
|
332
402
|
freshSessionArgs = ["--new-session", "{sessionId}"]
|
|
333
403
|
|
|
334
|
-
[workers.
|
|
404
|
+
[workers.vendor.nativeSession]
|
|
405
|
+
enabled = true
|
|
335
406
|
resumeArgs = ["run", "--resume", "{sessionId}", "--stdin"]
|
|
336
407
|
|
|
337
|
-
[workers.
|
|
408
|
+
[workers.vendor.interactive]
|
|
338
409
|
command = "vendor-coder"
|
|
339
410
|
args = ["resume", "{sessionId}"]
|
|
411
|
+
forkArgs = ["branch", "{sessionId}"]
|
|
340
412
|
```
|
|
341
413
|
|
|
342
|
-
Set either capability argument list to `[]` when the wrapper needs no extra argument. A non-empty `writableDirArgs` must include `{dir}`,
|
|
414
|
+
Generic profiles start with native resume disabled, no vendor flags, and the isolated Worker cwd. Set either capability argument list to `[]` when the wrapper needs no extra argument. A non-empty `writableDirArgs` must include `{dir}`, while non-empty `freshSessionArgs` and `interactive.forkArgs` must include `{sessionId}`. Set `forkArgs = []` when a generic CLI cannot fork a native conversation. Doctor validates the declared capability contract without guessing the wrapper's `--help` format; `--doctor --probe-agents` remains the end-to-end fresh/resume check. Every new Worker status records the actual profile ID plus its rendered model/provider snapshot, so later config changes do not relabel historical runs.
|
|
343
415
|
|
|
344
416
|
Customize each role independently; the main role is applied to simple chat, while Judge, Actor, and Critic receive their configured instructions during complex work:
|
|
345
417
|
|
|
@@ -357,46 +429,51 @@ Keep `[router.codex]` on `read-only`; routing only classifies requests and does
|
|
|
357
429
|
|
|
358
430
|
The process adapter sends each role prompt to stdin and records stdout/stderr in `output.log`. Worker success is separate from the operating-system exit code: a total, first-output, idle, or stdin watchdog failure remains failed even when the terminated CLI handles `SIGTERM` and exits with code `0`, so the next role never starts from a timed-out checkpoint. `firstOutputTimeoutMs` owns silent startup, `idleTimeoutMs` starts only after real stdout/stderr activity, and `timeoutMs` remains the hard ceiling; equal or longer secondary watchdogs do not race the total deadline.
|
|
359
431
|
|
|
360
|
-
In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. After a complex task completes, use the same controls to move through its structured result and press `Ctrl+D` to toggle details. Press `Ctrl+N` to preserve the current session and start a new task, `Ctrl+W` to open worker logs, and `Tab` to cycle the selected worker when workers exist. While a request is running, press `Esc` to stop it. After a failed or cancelled task, press `Ctrl+R` to retry the same turn or `Ctrl+N` to start independently. In worker-log views, scroll with the mouse wheel or PageUp/PageDown, press `Tab` to cycle workers, `Ctrl+N` to start a new task, and `Esc` to return to chat.
|
|
432
|
+
In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. Mouse reporting matches Claude Code's click/wheel protocol and does not enable drag-event tracking, so ordinary terminal drag selection and system copy remain available while wheel scrolling stays active. `Ctrl+Y` also copies the current visible chat, rendered Worker log, native Agent screen, or structured overview without changing mouse mode. macOS uses `pbcopy`; Linux uses `wl-copy`, `xclip`, or `xsel` when available and falls back to OSC 52. After a complex task completes, use the same controls to move through its structured result and press `Ctrl+D` to toggle details. Press `Ctrl+N` to preserve the current session and start a new task, `Ctrl+W` to open worker logs, and `Tab` to cycle the selected worker when workers exist. `Ctrl+S` opens the same status detail view from chat, logs, Worker overview, or an embedded native session; it keeps the footer compact while exposing the complete route, reason, selected Provider/model, phase, activity timestamp, and native session. Press `Ctrl+S` or `Esc` to return without interrupting the underlying Worker. While a request is running, press `Esc` to stop it. After a failed or cancelled task, press `Ctrl+R` to retry the same turn or `Ctrl+N` to start independently. In worker-log views, scroll with the mouse wheel or PageUp/PageDown, press `Tab` to cycle workers, `Ctrl+N` to start a new task, and `Esc` to return to chat.
|
|
433
|
+
|
|
434
|
+
`Ctrl+B` opens a live Worker overview without replacing the `Ctrl+W` log shortcut. It summarizes every Judge, Actor, and Critic by turn, role, named Provider, persisted model, state, phase, latest summary, and native-session availability. The selected active worker gets a live activity line showing its first-output deadline while starting and its idle deadline after output begins. The line stays muted while healthy, changes to warning for the final 20% and danger once overdue. Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` changes the selected worker; Enter or `Ctrl+W` opens its rendered log, `Ctrl+O` attaches to its native session, and `Esc` returns to the originating chat or log view. Router diagnostics and the project picker also return to the overview when opened from there.
|
|
361
435
|
|
|
362
|
-
|
|
436
|
+
From Worker overview, press `F` to open the file-backed Feature board. It summarizes every planned feature by turn, state, blocked dependencies and open Critic findings while preserving descriptions and Actor replies from the same collaboration files. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select a feature; Enter opens the selected feature's collaboration timeline; `R` refreshes immediately; and `Esc` returns to Worker overview. When a timeline was opened from this board, `Esc` returns to the Feature board instead of skipping back to Workers. While the selected feature is running, press `P` twice to pause only its active Actor or Critic process, or press `X` twice to cancel it; the first press opens a confirmation and `Esc` keeps it running. A paused feature preserves completed peers and same-wave checkpoints; select it and press `Ctrl+R` to resume the same task, turn, and native Worker session. After cancellation, already-running peers finish, queued workers stop, and integration remains blocked, so a partial wave never reaches the live workspace. Once the task settles as cancelled, `Ctrl+R` retries the cancelled task with persisted worker sessions; completed peers are loaded from their same-wave checkpoints instead of running again. Checkpoint load, reuse, and recovery events appear in the collaboration timeline.
|
|
363
437
|
|
|
364
|
-
|
|
438
|
+
For a failed, cancelled, or paused task, select an unfinished Feature and press `M` to edit its Provider assignment. `A` cycles that Feature's Actor through every `assignable` Worker Provider, `C` cycles its Critic, and `M` or `Esc` closes the assignment prompt. The choice is persisted in `features/<turn>-<feature>/assignment.json`, appears on the Feature board and collaboration artifacts, and takes effect on the next `Ctrl+R` retry. A changed role starts a new Provider-specific Worker while every previous Worker log and native-session record remains available. Assignment writes hold the same task lease as execution, so another live TUI cannot change a Feature while it is resuming.
|
|
365
439
|
|
|
366
440
|
From Worker overview, press `C` to open the file-backed Actor/Critic collaboration timeline. It merges `dialogue/actor-critic.jsonl`, feature status, Critic findings, Actor replies, finding resolution, and Wave events into one chronological view. The timeline follows new file evidence automatically and shows verified `fixed`/`open` counts when resolution evidence exists. Up/Down selects a collaboration event; `Enter` opens its complete event detail, including artifact paths from dialogue, status, Critic findings, Actor replies, and finding resolution; `U` filters to unresolved feature evidence; `Tab` cycles all features and each individual feature; `R` refreshes immediately; the mouse wheel or PageUp/PageDown scrolls history; and `Esc` returns to Worker overview.
|
|
367
441
|
|
|
368
|
-
`Ctrl+T` opens the workspace's persisted Task sessions without exiting the outer TUI. The list shows status, creation time, turn and worker counts, plus the number of distinct native sessions after deduplicating reused `engine + session_id` bindings; `>` marks the selected row and `*` marks the active task. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select, Enter to restore, `R` to rename with Unicode-safe cursor editing, `A` to archive or unarchive, `D` twice to confirm deletion, `E` to export, `H` to show or hide archived sessions, `Ctrl+N` to start with an empty task context, and `Esc` to return without losing the chat draft. Archived sessions are hidden by default, cannot be restored until unarchived, and are excluded from automatic startup selection. Archive, delete, and export require a terminal task with no live task lease; the active task cannot be archived or deleted. Exports are complete file-backed snapshots under `.parallel-codex/exports/<task>-<timestamp>/` with a versioned manifest and without transient lease files. Restoring a task reloads its route, workers, retry state, and recorded native session ids. The selected active task is stored in `session-index.sqlite` and survives process and workspace switches; `Ctrl+N` persists an intentionally empty active-task context instead of reopening an older task on restart. Existing
|
|
442
|
+
`Ctrl+T` opens the workspace's persisted Task sessions without exiting the outer TUI. The list shows status, creation time, turn and worker counts, plus the number of distinct native sessions after deduplicating reused `engine + session_id` bindings; `>` marks the selected row and `*` marks the active task. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select, Enter to restore, `I` to inspect the complete session hierarchy, `R` to rename with Unicode-safe cursor editing, `A` to archive or unarchive, `D` twice to confirm deletion, `E` to export, `H` to show or hide archived sessions, `Ctrl+N` to start with an empty task context, and `Esc` to return without losing the chat draft. Archived sessions are hidden by default, cannot be restored until unarchived, and are excluded from automatic startup selection. Archive, delete, and export require a terminal task with no live task lease; the active task cannot be archived or deleted. Exports are complete file-backed snapshots under `.parallel-codex/exports/<task>-<timestamp>/` with a versioned manifest and without transient lease files. Restoring a task reloads its route, workers, retry state, and recorded native session ids. The selected active task is stored in `session-index.sqlite` and survives process and workspace switches; `Ctrl+N` persists an intentionally empty active-task context instead of reopening an older task on restart. SQLite schema changes run as ordered, transactional migrations. Existing catalogs receive a pre-migration snapshot, each successful startup/reindex refreshes a healthy backup, and an integrity failure restores that backup or rebuilds the catalog from authoritative task files while preserving the corrupt copy for inspection. `meta.json` and the Worker files remain the rebuild authority.
|
|
443
|
+
|
|
444
|
+
The Task detail view reads authoritative task files and presents `Project -> Task -> Turn -> Worker -> Native session`, including each request, role, engine, configured model, Worker state, native session id, working directory, and last activity time. Historical Workers from earlier turns remain selectable after same-task follow-ups. Use Up/Down or `Tab` to select a Worker, Enter to open its retained log, `C` or `Ctrl+O` to continue the original native session, `B` to ask the native CLI to fork it, `R` to refresh, and `Esc` to return to the Task list. Continue and fork restore the selected Worker's recorded cwd and writable roots while the outer TUI stays open. Codex and Claude have built-in fork commands; a generic CLI exposes `B` only when `interactive.forkArgs` is configured. The child fork is owned and persisted by the native CLI; the indexed parent session remains unchanged.
|
|
369
445
|
|
|
370
446
|
In a Worker log, `Ctrl+F` searches the final rendered Worker log rather than raw file offsets. Type Unicode text normally; Enter moves to the next match and Up/Down moves backward or forward. `Esc` closes search. The current match is marked with `>` without shifting Diff or source line-number columns. With search closed, press `E` to cycle rendered error lines or `D` to cycle Diff files and hunks.
|
|
371
447
|
|
|
372
|
-
In chat or worker-log views, press `Ctrl+O` to attach to the selected worker's native session inside `parallel-codex-tui`. Native attach runs through a PTY, so full-screen CLIs such as `codex resume {sessionId}` receive a real terminal instead of pipe stdin. Native attach follows outer terminal resize, updating both the rendered screen and child PTY so Codex or Claude reflows at the current dimensions. Input is forwarded to the configured interactive command and output is shown in the native attach panel. When a Codex feature session needs its recorded worker, turn, or feature directories and the interactive command does not already choose a sandbox, native attach adds `--sandbox workspace-write` before its `--add-dir` arguments; an explicitly configured native sandbox remains unchanged. Overlapping attach preparations keep only the latest request. Press `Ctrl+]` to return to worker logs; this and closing the outer App terminate the active PTY. An attach preparation that finishes after App shutdown is discarded instead of starting a detached agent. `Ctrl+C` is forwarded to the native agent while attached. In chat and worker-log views, press `Ctrl+C` to exit the outer TUI. An operating-system SIGINT first asks the App to abort its Router or Worker and clean up any PTY; a second SIGINT restores terminal modes and forces exit.
|
|
448
|
+
In chat or worker-log views, press `Ctrl+O` to attach to the selected worker's native session inside `parallel-codex-tui`. Native attach runs through a PTY, so full-screen CLIs such as `codex resume {sessionId}` receive a real terminal instead of pipe stdin. Native attach follows outer terminal resize, updating both the rendered screen and child PTY so Codex or Claude reflows at the current dimensions. Input is forwarded to the configured interactive command and output is shown in the native attach panel. When a Codex feature session needs its recorded worker, turn, or feature directories and the interactive command does not already choose a sandbox, native attach adds `--sandbox workspace-write` before its `--add-dir` arguments; an explicitly configured native sandbox remains unchanged. A non-zero native exit recognizes read-only sandbox/add-dir conflicts, untrusted directories, misplaced `--skip-git-repo-check`, missing PTY input, and host permission failures, then appends the exact configuration or terminal action before the exit line scrolls away. Overlapping attach preparations keep only the latest request. Press `Ctrl+]` to return to worker logs; this and closing the outer App terminate the active PTY. An attach preparation that finishes after App shutdown is discarded instead of starting a detached agent. `Ctrl+C` is forwarded to the native agent while attached. In chat and worker-log views, press `Ctrl+C` to exit the outer TUI. An operating-system SIGINT first asks the App to abort its Router or Worker and clean up any PTY; a second SIGINT restores terminal modes and forces exit.
|
|
373
449
|
|
|
374
450
|
If a native resume fails because the underlying CLI reports that its context window is full, configure `fallback = "new"` under `[workers.<engine>.nativeSession]`. The old native session is archived as `native-session.retired.json`, removed from active use, and the worker is retried once with the normal fresh-session command. A valid retirement tombstone is also a cross-turn inheritance barrier: a retry or later turn cannot resurrect the same session from an older worker copy, while a newer replacement session remains reusable. A `process-cleanup-error` or `process-ownership-error` always blocks native-session fallback, even when the CLI output also mentions a full context window, so a fresh Worker cannot overlap an untracked or still-running resume process.
|
|
375
451
|
|
|
376
452
|
## Release
|
|
377
453
|
|
|
378
|
-
GitHub Actions runs CI on Ubuntu with Node.js 24.15 and 26, plus macOS with Node.js
|
|
454
|
+
GitHub Actions runs CI on Ubuntu with Node.js 24.15 and 26, plus macOS with Node.js 24.15, for pushes and pull requests to `main`. Every job also packs the release artifact, installs that tarball into a clean global prefix, and executes the installed `--version` and `--help` entrypoints.
|
|
379
455
|
|
|
380
456
|
Releases publish to npm through npm Trusted Publishing with GitHub OIDC. Do not configure `NPM_TOKEN` for the release workflow. In npm, configure Trusted Publishing for organization/user `allendred`, repository `parallel-codex-tui`, workflow filename `release.yml`, and allowed action `npm publish`. The package already exists on npm, so future releases can use Trusted Publishing directly. You can also configure the trust relationship from an authenticated npm CLI session:
|
|
381
457
|
|
|
382
458
|
```bash
|
|
383
|
-
npm install -g npm@^11.
|
|
459
|
+
npm install -g npm@^11.5.1
|
|
384
460
|
npm trust github parallel-codex-tui --repo allendred/parallel-codex-tui --file release.yml --allow-publish --dry-run
|
|
385
461
|
npm trust github parallel-codex-tui --repo allendred/parallel-codex-tui --file release.yml --allow-publish --yes
|
|
386
462
|
```
|
|
387
463
|
|
|
388
464
|
Creating or listing trusted publishers may require npm two-factor authentication in the browser.
|
|
389
465
|
|
|
390
|
-
The release job installs npm `^11.5.1`, runs on Node `
|
|
466
|
+
The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the prepared tarball through OIDC, waits for the package to become visible on npm, installs it globally in a temporary prefix, and checks `parallel-codex-tui --version` before creating the GitHub Release. If npm returns `ENEEDAUTH` or `E401`, fix the npm Trusted Publishing package settings rather than adding a token fallback.
|
|
391
467
|
|
|
392
468
|
To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
|
|
393
469
|
|
|
394
470
|
```bash
|
|
395
|
-
|
|
396
|
-
git
|
|
471
|
+
VERSION=0.1.6
|
|
472
|
+
git tag "v$VERSION"
|
|
473
|
+
git push origin "v$VERSION"
|
|
397
474
|
```
|
|
398
475
|
|
|
399
|
-
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.1.
|
|
476
|
+
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.1.6` requires tag `v0.1.6`. Published tags such as `v0.1.5` are immutable and must not be moved or reused.
|
|
400
477
|
|
|
401
478
|
## Publishing Hygiene
|
|
402
479
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function startupPreflightMessages(preflight) {
|
|
2
|
+
if (preflight.ok) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const issues = preflight.lines.filter(isStartupPreflightIssue);
|
|
6
|
+
if (issues.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const summary = issues.slice(0, 4).join(" · ");
|
|
10
|
+
const remainder = issues.length > 4 ? ` · ${issues.length - 4} more` : "";
|
|
11
|
+
return [{
|
|
12
|
+
from: "system",
|
|
13
|
+
text: `Startup preflight needs attention · ${summary}${remainder} · run parallel-codex-tui --doctor before starting workers`
|
|
14
|
+
}];
|
|
15
|
+
}
|
|
16
|
+
function isStartupPreflightIssue(line) {
|
|
17
|
+
return /(?:^|: )(?:warning\b|missing\b|incompatible\b|unreachable\b|invalid\b|denied\b|failed\b)/i.test(line);
|
|
18
|
+
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
export function startupRecoveryMessages(recoveredTasks, activeTaskId, pendingTaskCreations) {
|
|
1
|
+
export function startupRecoveryMessages(recoveredTasks, activeTaskId, pendingTaskCreations, sessionIndexRecovery) {
|
|
2
2
|
return [
|
|
3
|
+
...sessionIndexRecoveryMessages(sessionIndexRecovery),
|
|
3
4
|
...interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId),
|
|
4
5
|
...pendingTaskCreationMessages(pendingTaskCreations)
|
|
5
6
|
];
|
|
6
7
|
}
|
|
8
|
+
function sessionIndexRecoveryMessages(recovery) {
|
|
9
|
+
if (!recovery) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return [{
|
|
13
|
+
from: "system",
|
|
14
|
+
text: recovery.source === "backup"
|
|
15
|
+
? `Recovered session catalog from the last healthy SQLite backup · task files reindexed · corrupt copy kept at ${recovery.quarantinedPath}`
|
|
16
|
+
: `Rebuilt session catalog from task files after SQLite integrity failure · corrupt copy kept at ${recovery.quarantinedPath}`
|
|
17
|
+
}];
|
|
18
|
+
}
|
|
7
19
|
function interruptedTaskRecoveryMessages(recoveredTasks, activeTaskId) {
|
|
8
20
|
if (recoveredTasks.length === 0) {
|
|
9
21
|
return [];
|
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,16 @@ import { selectWorkspaceForCli } from "./cli-workspace.js";
|
|
|
8
8
|
import { commitWorkspaceTransition } from "./cli-workspace-transition.js";
|
|
9
9
|
import { WorkspaceSelectionCancelledError } from "./cli-workspace-picker.js";
|
|
10
10
|
import { startupRecoveryMessages } from "./cli-startup-recovery.js";
|
|
11
|
+
import { startupPreflightMessages } from "./cli-startup-preflight.js";
|
|
11
12
|
import { createRuntime } from "./bootstrap.js";
|
|
12
13
|
import { prepareAppRoot } from "./core/app-root.js";
|
|
13
14
|
import { formatConfigErrorMessage } from "./core/config-errors.js";
|
|
14
15
|
import { configPath, loadConfig, withUiThemeOverride, writeDefaultConfig } from "./core/config.js";
|
|
15
16
|
import { pathExists } from "./core/file-store.js";
|
|
16
17
|
import { readRouterAudit } from "./core/router-audit.js";
|
|
18
|
+
import { loadTaskSessionDetails as loadPersistedTaskSessionDetails } from "./core/task-session-details.js";
|
|
17
19
|
import { listWorkspaceChoices } from "./core/workspace.js";
|
|
18
|
-
import { runDoctor } from "./doctor.js";
|
|
20
|
+
import { runDoctor, runRuntimePreflight } from "./doctor.js";
|
|
19
21
|
import { helpText } from "./cli-help.js";
|
|
20
22
|
import { App } from "./tui/App.js";
|
|
21
23
|
import { formatTuiThemeCatalog } from "./tui/theme-preview.js";
|
|
@@ -102,7 +104,11 @@ async function main() {
|
|
|
102
104
|
records,
|
|
103
105
|
policy: routerDiagnosticsPolicy(latestConfig.router)
|
|
104
106
|
};
|
|
105
|
-
}, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options),
|
|
107
|
+
}, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), loadTaskSessionDetails: (task) => loadPersistedTaskSessionDetails({
|
|
108
|
+
task,
|
|
109
|
+
taskDir: state.runtime.sessions.taskFromId(task.id).dir,
|
|
110
|
+
modelNames: Object.fromEntries(Object.entries(state.runtime.config.workers).map(([id, worker]) => [id, worker.model.name]))
|
|
111
|
+
}), renameTaskSession: async (taskId, title) => {
|
|
106
112
|
await state.runtime.sessions.renameTask(taskId, title);
|
|
107
113
|
}, setTaskSessionArchived: async (taskId, archived) => {
|
|
108
114
|
await state.runtime.sessions.setTaskArchived(taskId, archived);
|
|
@@ -165,6 +171,10 @@ async function main() {
|
|
|
165
171
|
async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId) {
|
|
166
172
|
const runtime = await createRuntime(appRoot, workspaceRoot);
|
|
167
173
|
try {
|
|
174
|
+
const preflightPromise = runRuntimePreflight(runtime.config, runtime.workspaceRoot, process.env).catch((error) => ({
|
|
175
|
+
ok: false,
|
|
176
|
+
lines: [`preflight: failed (${error instanceof Error ? error.message : String(error)})`]
|
|
177
|
+
}));
|
|
168
178
|
if (requestedTaskId) {
|
|
169
179
|
if (!(await runtime.sessions.hasTask(requestedTaskId))) {
|
|
170
180
|
throw new Error(`Task session not found in workspace ${runtime.workspaceRoot}: ${requestedTaskId}`);
|
|
@@ -200,16 +210,17 @@ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId)
|
|
|
200
210
|
else if (!initialTaskId && typeof rememberedTaskId === "string") {
|
|
201
211
|
await runtime.index.setActiveTaskId(null);
|
|
202
212
|
}
|
|
203
|
-
const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices] = await Promise.all([
|
|
213
|
+
const [initialRoute, initialWorkers, initialCanRetryTask, initialHistory, workspaceChoices, preflight] = await Promise.all([
|
|
204
214
|
initialTaskId
|
|
205
215
|
? runtime.sessions.readLatestRoute(runtime.sessions.taskFromId(initialTaskId))
|
|
206
216
|
: null,
|
|
207
217
|
initialTaskId ? runtime.orchestrator.listTaskWorkers(initialTaskId) : [],
|
|
208
218
|
initialTaskId ? runtime.orchestrator.canRetryTask(initialTaskId) : false,
|
|
209
219
|
runtime.sessions.readChatHistory(),
|
|
210
|
-
listWorkspaceChoices(appRoot)
|
|
220
|
+
listWorkspaceChoices(appRoot),
|
|
221
|
+
preflightPromise
|
|
211
222
|
]);
|
|
212
|
-
const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations);
|
|
223
|
+
const recoveryMessages = startupRecoveryMessages(runtime.recoveredTasks, initialTaskId, runtime.pendingTaskCreations, runtime.index.recovery);
|
|
213
224
|
return {
|
|
214
225
|
runtime,
|
|
215
226
|
initialTaskId,
|
|
@@ -222,7 +233,8 @@ async function loadInteractiveWorkspace(appRoot, workspaceRoot, requestedTaskId)
|
|
|
222
233
|
text,
|
|
223
234
|
...(task_id ? { taskId: task_id } : {})
|
|
224
235
|
})),
|
|
225
|
-
...recoveryMessages
|
|
236
|
+
...recoveryMessages,
|
|
237
|
+
...startupPreflightMessages(preflight)
|
|
226
238
|
],
|
|
227
239
|
workspaceChoices
|
|
228
240
|
};
|
|
@@ -279,7 +291,7 @@ function restoreInteractiveTerminal() {
|
|
|
279
291
|
}
|
|
280
292
|
process.stdin.pause();
|
|
281
293
|
if (process.stdout.isTTY) {
|
|
282
|
-
process.stdout.write("\x1b[?1006l\x1b[?
|
|
294
|
+
process.stdout.write("\x1b[?1006l\x1b[?1000l\x1b[?2004l\x1b[?25h");
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
297
|
function formatStartupError(error) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const MAX_CLIPBOARD_CHARACTERS = 200_000;
|
|
3
|
+
export async function copyTextToClipboard(text, options = {}) {
|
|
4
|
+
const value = normalizeClipboardText(text);
|
|
5
|
+
if (!value) {
|
|
6
|
+
throw new Error("No visible text to copy");
|
|
7
|
+
}
|
|
8
|
+
const platform = options.platform ?? process.platform;
|
|
9
|
+
const env = options.env ?? process.env;
|
|
10
|
+
const runCommand = options.runCommand ?? runClipboardCommand;
|
|
11
|
+
for (const candidate of clipboardCommands(platform, env)) {
|
|
12
|
+
try {
|
|
13
|
+
await runCommand(candidate.command, candidate.args, value);
|
|
14
|
+
return { method: candidate.method, characters: Array.from(value).length };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Try the next local clipboard integration before falling back to OSC 52.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const writeTerminal = options.writeTerminal ?? ((sequence) => {
|
|
21
|
+
process.stdout.write(sequence);
|
|
22
|
+
});
|
|
23
|
+
writeTerminal(osc52ClipboardSequence(value));
|
|
24
|
+
return { method: "osc52", characters: Array.from(value).length };
|
|
25
|
+
}
|
|
26
|
+
export function normalizeClipboardText(text) {
|
|
27
|
+
const lines = text
|
|
28
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
29
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
30
|
+
.replace(/\r\n?/g, "\n")
|
|
31
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
32
|
+
.split("\n")
|
|
33
|
+
.map((line) => line.trimEnd());
|
|
34
|
+
while (lines[0] === "") {
|
|
35
|
+
lines.shift();
|
|
36
|
+
}
|
|
37
|
+
while (lines.at(-1) === "") {
|
|
38
|
+
lines.pop();
|
|
39
|
+
}
|
|
40
|
+
const clean = lines.join("\n");
|
|
41
|
+
return Array.from(clean).slice(0, MAX_CLIPBOARD_CHARACTERS).join("");
|
|
42
|
+
}
|
|
43
|
+
export function osc52ClipboardSequence(text) {
|
|
44
|
+
return `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
|
|
45
|
+
}
|
|
46
|
+
function clipboardCommands(platform, env) {
|
|
47
|
+
if (platform === "darwin") {
|
|
48
|
+
return [{ method: "pbcopy", command: "/usr/bin/pbcopy", args: [] }];
|
|
49
|
+
}
|
|
50
|
+
if (platform !== "linux") {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
...(env.WAYLAND_DISPLAY
|
|
55
|
+
? [{ method: "wl-copy", command: "wl-copy", args: [] }]
|
|
56
|
+
: []),
|
|
57
|
+
...(env.DISPLAY
|
|
58
|
+
? [
|
|
59
|
+
{ method: "xclip", command: "xclip", args: ["-selection", "clipboard"] },
|
|
60
|
+
{ method: "xsel", command: "xsel", args: ["--clipboard", "--input"] }
|
|
61
|
+
]
|
|
62
|
+
: [])
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function runClipboardCommand(command, args, text) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const child = spawn(command, args, {
|
|
68
|
+
env: process.env,
|
|
69
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
70
|
+
});
|
|
71
|
+
let stderr = "";
|
|
72
|
+
let settled = false;
|
|
73
|
+
const finish = (error) => {
|
|
74
|
+
if (settled) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
settled = true;
|
|
78
|
+
if (error) {
|
|
79
|
+
reject(error);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
resolve();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
child.stderr?.on("data", (chunk) => {
|
|
86
|
+
stderr += String(chunk);
|
|
87
|
+
});
|
|
88
|
+
child.on("error", (error) => finish(error));
|
|
89
|
+
child.on("close", (code) => {
|
|
90
|
+
finish(code === 0
|
|
91
|
+
? undefined
|
|
92
|
+
: new Error(`${command} exited ${code ?? "without a code"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
|
|
93
|
+
});
|
|
94
|
+
child.stdin?.on("error", (error) => finish(error));
|
|
95
|
+
child.stdin?.end(text);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { EventRecordSchema, FeatureStatusSchema } from "../domain/schemas.js";
|
|
4
|
+
import { EventRecordSchema, FeatureAssignmentSchema, FeatureStatusSchema } from "../domain/schemas.js";
|
|
5
5
|
import { readTextIfExists } from "./file-store.js";
|
|
6
6
|
const FeatureDialogueSchema = z.object({
|
|
7
7
|
time: z.string().datetime(),
|
|
@@ -107,8 +107,9 @@ async function readCollaborationFeatures(taskDir) {
|
|
|
107
107
|
.filter((entry) => entry.isDirectory())
|
|
108
108
|
.map(async (entry) => {
|
|
109
109
|
const dir = join(root, entry.name);
|
|
110
|
-
const [statusText, spec, findings, replies, resolutionText] = await Promise.all([
|
|
110
|
+
const [statusText, assignmentText, spec, findings, replies, resolutionText] = await Promise.all([
|
|
111
111
|
readTextIfExists(join(dir, "status.json")),
|
|
112
|
+
readTextIfExists(join(dir, "assignment.json")),
|
|
112
113
|
readTextIfExists(join(dir, "spec.md")),
|
|
113
114
|
readTextIfExists(join(dir, "critic-findings.jsonl")),
|
|
114
115
|
readTextIfExists(join(dir, "actor-replies.jsonl")),
|
|
@@ -121,6 +122,7 @@ async function readCollaborationFeatures(taskDir) {
|
|
|
121
122
|
const findingEvidence = readMailboxEvidence(findings);
|
|
122
123
|
const replyEvidence = readMailboxEvidence(replies);
|
|
123
124
|
const resolution = parseJsonValue(resolutionText, FindingResolutionSchema);
|
|
125
|
+
const assignment = parseJsonValue(assignmentText, FeatureAssignmentSchema);
|
|
124
126
|
return {
|
|
125
127
|
id: status.feature_id,
|
|
126
128
|
title: status.title?.trim() || featureSpecTitle(spec) || status.feature_id,
|
|
@@ -129,11 +131,16 @@ async function readCollaborationFeatures(taskDir) {
|
|
|
129
131
|
turnId: status.turn_id,
|
|
130
132
|
state: status.state,
|
|
131
133
|
updatedAt: status.updated_at,
|
|
134
|
+
...(assignment ? {
|
|
135
|
+
actorEngine: assignment.actor_engine,
|
|
136
|
+
criticEngine: assignment.critic_engine
|
|
137
|
+
} : {}),
|
|
132
138
|
findings: findingEvidence.count,
|
|
133
139
|
replies: replyEvidence.count,
|
|
134
140
|
artifactRefs: [
|
|
135
141
|
{ label: "status", path: join(dir, "status.json") },
|
|
136
142
|
{ label: "spec", path: join(dir, "spec.md") },
|
|
143
|
+
...(assignment ? [{ label: "assignment", path: join(dir, "assignment.json") }] : []),
|
|
137
144
|
{ label: "critic findings", path: join(dir, "critic-findings.jsonl") },
|
|
138
145
|
{ label: "actor replies", path: join(dir, "actor-replies.jsonl") },
|
|
139
146
|
...(resolution ? [{ label: "finding resolution", path: join(dir, "finding-resolution.json") }] : [])
|