parallel-codex-tui 0.2.4 → 0.2.5
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 +6 -5
- package/README.md +12 -11
- package/dist/cli.js +1 -1
- package/dist/core/config.js +19 -4
- package/dist/core/main-response.js +78 -0
- package/dist/core/session-manager.js +6 -1
- package/dist/core/worker-output-policy.js +19 -0
- package/dist/orchestrator/orchestrator.js +1 -8
- package/dist/orchestrator/workspace-sandbox.js +10 -0
- package/dist/tui/App.js +42 -3
- package/dist/tui/StatusBar.js +8 -2
- package/dist/tui/StatusDetailView.js +19 -0
- package/dist/tui/WorkerOverviewView.js +6 -0
- package/dist/tui/runtime-config-state.js +21 -0
- package/dist/tui/status-line.js +3 -0
- package/dist/version.js +1 -1
- package/dist/workers/process-adapter.js +20 -9
- package/package.json +1 -1
|
@@ -77,12 +77,13 @@ fallback = "new"
|
|
|
77
77
|
|
|
78
78
|
[workers.claude]
|
|
79
79
|
command = "claude"
|
|
80
|
-
#
|
|
80
|
+
# Claude auto mode lets safe non-interactive tools run while retaining permission checks.
|
|
81
81
|
args = ["--print", "--permission-mode", "auto", "--output-format", "text"]
|
|
82
|
-
#
|
|
82
|
+
# Claude text output is buffered until completion, so timeoutMs is the effective startup ceiling.
|
|
83
|
+
# A shorter firstOutputTimeoutMs is used only with --output-format stream-json.
|
|
83
84
|
# timeoutMs = 2700000
|
|
84
85
|
# idleTimeoutMs = 300000
|
|
85
|
-
firstOutputTimeoutMs =
|
|
86
|
+
# firstOutputTimeoutMs = 2700000
|
|
86
87
|
|
|
87
88
|
# [workers.claude.model]
|
|
88
89
|
# name = "claude-sonnet"
|
|
@@ -157,10 +158,10 @@ freshSessionArgs = []
|
|
|
157
158
|
# args = ["run", "--stdin"]
|
|
158
159
|
|
|
159
160
|
[pairing]
|
|
160
|
-
main = "
|
|
161
|
+
main = "codex"
|
|
161
162
|
judge = "codex"
|
|
162
163
|
actor = "codex"
|
|
163
|
-
critic = "
|
|
164
|
+
critic = "claude"
|
|
164
165
|
|
|
165
166
|
[roles.main]
|
|
166
167
|
title = "Main"
|
package/README.md
CHANGED
|
@@ -6,21 +6,22 @@ Built with Codex-assisted development.
|
|
|
6
6
|
|
|
7
7
|
## Current Release
|
|
8
8
|
|
|
9
|
-
`v0.2.
|
|
9
|
+
`v0.2.5` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.2.5) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.2.5). Main chat now extracts only the final Codex answer while retaining the complete CLI transcript in `output.log`; legacy transcript-shaped chat records are cleaned when displayed without rewriting their file-backed evidence. Workspace integration ignores known host metadata such as `.DS_Store` and AppleDouble files while continuing to reject real concurrent project edits.
|
|
10
10
|
|
|
11
|
-
The
|
|
11
|
+
The default role map is Main/Judge/Actor on Codex and Critic on Claude. Status details show the complete active role map separately from each historical Worker's persisted Provider/model. Config monitoring labels Router-only edits as active on the next request and marks role, Worker, model, permission, or UI changes as requiring restart. Claude text/JSON print runs are recognized as buffered work, use the total deadline instead of a false first-output deadline, and run safe isolated tools through non-interactive `auto` permissions. The `v0.2.4` non-interactive Codex permission placement remains included. The release keeps terminal scrolling and copying available at the same time without requiring Shift and preserves the embedded native Agent scrollback across status-detail round trips and real PTY resizes.
|
|
12
12
|
|
|
13
13
|
Highlights:
|
|
14
14
|
|
|
15
15
|
- Judge produces validated requirements, acceptance criteria, and a dependency-aware Feature DAG. Actor/Critic pairs exchange checked JSONL findings and replies, followed by Wave Critic and final Judge acceptance.
|
|
16
16
|
- A conflicting parallel wave is archived with its merge evidence and returned to the same Judge native session for a bounded DAG replan. The replacement plan must retain the same Feature IDs and serialize every conflicting pair; a deterministic serialized fallback is used when the Judge response is invalid.
|
|
17
17
|
- Tasks retain every Turn, historical Worker log, native session binding, model snapshot, and collaboration artifact across follow-ups and restarts. A 20-turn soak verifies 80 ordered Workers before and after restart.
|
|
18
|
+
- Main responses, raw process evidence, active role configuration, and historical Worker identity now have separate display and persistence paths.
|
|
18
19
|
- `--diagnostics` and `Ctrl+X` export bounded, redacted support evidence without prompts, role instructions, command arguments, source files, or environment-variable values.
|
|
19
20
|
- Runtime preflight checks Codex, Claude, named third-party Providers, proxy reachability, workspace access, CLI capabilities, and native attach policy before work starts.
|
|
20
21
|
- 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.
|
|
21
22
|
- Worker overview, Feature board, collaboration timeline, Task center, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
|
|
22
23
|
|
|
23
|
-
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and
|
|
24
|
+
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence and preserving the native output tail across status-detail round trips. The deterministic repository suite contains 1,281 tests across 127 files: 1,280 pass by default, while one quota-consuming real-Agent test is skipped and passes through `npm run test:real-agents`.
|
|
24
25
|
|
|
25
26
|
Real Provider probes 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.
|
|
26
27
|
|
|
@@ -204,7 +205,7 @@ The doctor output includes `preview:` and `semantic:` ANSI swatch rows so you ca
|
|
|
204
205
|
- Router classification only receives the user request; workspace selection and session files are kept out of the router prompt.
|
|
205
206
|
- Simple requests stay in the main TUI flow and do not create Judge, Actor, or Critic workers.
|
|
206
207
|
- Consecutive simple requests reuse the main worker's native session across app restarts when the CLI exposes a session id.
|
|
207
|
-
- The workspace chat transcript is appended to `.parallel-codex/sessions/main/chat.jsonl`; startup restores the latest 200 valid messages and skips isolated corrupt rows. The shared tail reader reads JSONL backward in bounded chunks, so a long-running chat does not require loading its lifetime transcript and a partial or oversized final row cannot hide earlier valid messages.
|
|
208
|
+
- The workspace chat transcript is appended to `.parallel-codex/sessions/main/chat.jsonl`; startup restores the latest 200 valid messages and skips isolated corrupt rows. Main stores only the extracted final answer in new chat records while retaining the raw Codex/Claude process transcript in the Main Worker's `output.log`. Legacy Codex transcript records are reduced to their final answer at read time without changing the original JSONL. The shared tail reader reads JSONL backward in bounded chunks, so a long-running chat does not require loading its lifetime transcript and a partial or oversized final row cannot hide earlier valid messages.
|
|
208
209
|
- Chat drafts support Unicode-safe Left/Right, Home/End, Backspace, and Delete editing. A single Up/Down recalls persisted user requests and returns to the exact unsent draft; Ctrl+Up/Down remains available, while repeated alternate-scroll arrow bursts continue to scroll chat. Long input stays on one row with the visible window centered around the logical cursor.
|
|
209
210
|
- Bracketed multiline paste stays in one draft instead of submitting the first line; logical line breaks and tabs appear as `↵` and `⇥` in the single-row input until the complete request is submitted.
|
|
210
211
|
- Complex requests create a session under `.parallel-codex/sessions/`.
|
|
@@ -223,7 +224,7 @@ The doctor output includes `preview:` and `semantic:` ANSI swatch rows so you ca
|
|
|
223
224
|
- Parallel Actor, Critic, and revision batches honor `[orchestration].maxParallelFeatures` (default `3`).
|
|
224
225
|
- Each planned feature gets an isolated Actor implementation workspace, a disposable Critic review clone, worker directories, logs, status, native session ids, and a mailbox under `features/<turn>-<feature>`; the shared dialogue remains in `dialogue/actor-critic.jsonl`. Concurrent text and JSONL appends inside one TUI are serialized per resolved file path, preserving call order without blocking independent feature files; one failed append does not poison later writes.
|
|
225
226
|
- Feature revision mailboxes are a checked protocol rather than prompt-only convention. A `REVISION_REQUIRED` Critic must write one `{"id":"C-001","severity":"blocker","summary":"..."}` row per blocker to `critic-findings.jsonl`; the revision Actor must write matching `{"finding_id":"C-001","status":"fixed","notes":"..."}` rows to `actor-replies.jsonl`. Missing, malformed, unacknowledged, or contradictory findings stop the task before live integration. The Supervisor snapshots pending, inconsistent, and approved fixed/open state in `finding-resolution.json`; the collaboration timeline retains the original findings and replies as history and uses resolution evidence instead of counting every reply as a fix.
|
|
226
|
-
- Parallel workspace isolation works for both Git and non-Git projects. A wave records `baseline`, per-feature `features/` workspaces, refreshable `reviews/` copies, `staging`, and conflict evidence under `sessions/<task>/workspaces/turn-<turn>/wave-<wave>/` while excluding `.git
|
|
227
|
+
- Parallel workspace isolation works for both Git and non-Git projects. A wave records `baseline`, per-feature `features/` workspaces, refreshable `reviews/` copies, `staging`, and conflict evidence under `sessions/<task>/workspaces/turn-<turn>/wave-<wave>/` while excluding `.git`, the configured runtime data directory, and known host metadata (`.DS_Store`, AppleDouble, Spotlight/Trash indexes, `Thumbs.db`, and `desktop.ini`). Source files outside that narrow list still trigger the live-mutation guard.
|
|
227
228
|
- Feature Critic implementation writes are discarded with the review clone. After an Actor revision, the same Critic native session rechecks a freshly cloned review path; only Actor workspace changes can enter staging.
|
|
228
229
|
- Approved feature changes are three-way merged into a task-owned integration workspace first; independent edits to the same text file are combined automatically. The live workspace remains unchanged during this stage.
|
|
229
230
|
- Every staged wave gets a combined Wave Critic run in a disposable verification copy. It checks the full Judge acceptance criteria, cross-feature behavior, tests, and builds. A missing `APPROVED`/`REVISION_REQUIRED` decision fails safely instead of being treated as approval.
|
|
@@ -236,7 +237,7 @@ The doctor output includes `preview:` and `semantic:` ANSI swatch rows so you ca
|
|
|
236
237
|
- Every numbered turn keeps its own immutable Judge, Actor, and Critic worker directories. Later turns use suffixed ids such as `judge-codex-0002`; the log viewer retains earlier workers, cycles them in turn order, and restores the same order after restart. The new worker record may reuse the prior native session id without overwriting the prior turn's prompt, status, log, or native-session metadata.
|
|
237
238
|
- `Ctrl+N` leaves the active complex task intact on disk while clearing its live context, worker selection, retry state, and status so the next complex request creates an independent task from turn `0001`.
|
|
238
239
|
- Single-feature follow-ups reuse the same Actor/Critic native sessions when available while moving them to the new turn's isolated workspace; prompts include up to five prior turn summaries as file-backed memory. A failed follow-up retry reuses a complete saved Judge plan, or restores Judge first when the earlier Judge run never produced one.
|
|
239
|
-
- Automated Judge, Actor, Critic, Wave Actor, and Wave Critic runs enforce process-level isolation: Codex is clamped to root-level `-a never` plus `workspace-write`, and Claude is clamped to `
|
|
240
|
+
- Automated Judge, Actor, Critic, Wave Actor, and Wave Critic runs enforce process-level isolation: Codex is clamped to root-level `-a never` plus `workspace-write`, and Claude is clamped to non-interactive `auto`, even when private command arguments request manual approval or contain a broader bypass mode. Native attach remains an explicit interactive path.
|
|
240
241
|
- Pressing `Esc` while a request is running stops the router or active worker and records an interrupted complex task as `cancelled`; exiting the outer TUI also terminates the active run.
|
|
241
242
|
- Failed and cancelled tasks expose `Ctrl+R` retry. Retry keeps the same task and turn, reuses recorded native worker sessions, preserves prior output behind a retry separator, does not route the request again, and reuses the persisted feature dependency plan. A complete Judge snapshot and fully integrated waves are skipped; an unchanged in-progress wave reuses successful Actor and Critic checkpoints and runs only unfinished workers. If the live workspace no longer matches the saved baseline, the stale wave checkpoint is rejected and rebuilt from the current project before workers continue.
|
|
242
243
|
- The shared Main chat holds its own `sessions/main/run-owner.json` lease across prompt initialization, native-session reuse, and Worker completion. A second TUI is rejected before it can clear or overwrite the active Main prompt, log, status, or native session, and startup native-session reconciliation skips a Main session owned by another live TUI. After a hard exit, startup atomically claims the stale Main lease, terminates every verifiable orphan process group, marks active Main Workers `cancelled`, preserves native session ids, and records recovery before exposing the runtime. An unverifiable or still-running Main process blocks startup without changing its status or checkpoints.
|
|
@@ -363,7 +364,7 @@ args = ["--resume", "{sessionId}"]
|
|
|
363
364
|
forkArgs = ["--resume", "{sessionId}", "--fork-session"]
|
|
364
365
|
```
|
|
365
366
|
|
|
366
|
-
`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`. Codex Router and non-interactive Codex Workers use the root option `-a never` before `exec`, so they cannot stall on an approval UI the outer TUI cannot deliver; Router remains `read-only`, while coding Workers stay inside `workspace-write`. Isolated Codex roles remove broader bypass flags and replace any configured approval mode with this bounded policy. The dangerous `--dangerously-bypass-approvals-and-sandbox` mode is never required. Native Codex attach remains interactive. Claude
|
|
367
|
+
`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`. Codex Router and non-interactive Codex Workers use the root option `-a never` before `exec`, so they cannot stall on an approval UI the outer TUI cannot deliver; Router remains `read-only`, while coding Workers stay inside `workspace-write`. Isolated Codex roles remove broader bypass flags and replace any configured approval mode with this bounded policy. The dangerous `--dangerously-bypass-approvals-and-sandbox` mode is never required. Native Codex attach remains interactive. Claude automated roles use `auto`, allowing safe test and edit commands while retaining Claude's permission checks; commands that still require interactive approval must be continued through `Ctrl+O` instead of being falsely approved in chat.
|
|
367
368
|
|
|
368
369
|
`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.
|
|
369
370
|
|
|
@@ -450,9 +451,9 @@ instructions = ["Implement small verified changes.", "Record decisions in worklo
|
|
|
450
451
|
|
|
451
452
|
Keep `[router.codex]` on `read-only`; routing only classifies requests and does not need project writes. Automated Judge/Actor/Critic runs also clamp unsafe Codex or Claude permission flags to their isolated workspace policy, so a private `danger-full-access` or bypass flag cannot silently defeat live-workspace isolation. Use native attach for deliberate interactive host-level work such as Docker or OrbStack access.
|
|
452
453
|
|
|
453
|
-
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.
|
|
454
|
+
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. Claude `--print` with `text` or `json` emits only its final result, so those launches display `working · buffered` and use `timeoutMs` without arming a separate first-output watchdog. Claude `stream-json` remains streaming and retains the configured first-output deadline.
|
|
454
455
|
|
|
455
|
-
In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. A single Up/Down recalls persisted request history, and Ctrl+Up/Down remains an alternative. The outer TUI never enables application mouse tracking, so ordinary left-drag selection and system copy work without Shift while alternate-scroll keeps the wheel active in chat and Worker logs. `Ctrl+Y` also copies the current visible chat, rendered Worker log, native Agent screen, or structured overview without changing terminal modes. 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
|
|
456
|
+
In chat, scroll long conversation history with the mouse wheel or PageUp/PageDown; sending a new message returns to the latest reply. A single Up/Down recalls persisted request history, and Ctrl+Up/Down remains an alternative. The outer TUI never enables application mouse tracking, so ordinary left-drag selection and system copy work without Shift while alternate-scroll keeps the wheel active in chat and Worker logs. `Ctrl+Y` also copies the current visible chat, rendered Worker log, native Agent screen, or structured overview without changing terminal modes. 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, active Main/Judge/Actor/Critic Provider map, selected historical Provider/model, phase, activity timestamp, native session, and config effective/restart state. 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, Up/Down, or PageUp/PageDown, press `Tab` to cycle workers, `Ctrl+N` to start a new task, and `Esc` to return to chat.
|
|
456
457
|
|
|
457
458
|
`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.
|
|
458
459
|
|
|
@@ -516,12 +517,12 @@ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the pr
|
|
|
516
517
|
To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
|
|
517
518
|
|
|
518
519
|
```bash
|
|
519
|
-
VERSION=0.2.
|
|
520
|
+
VERSION=0.2.5
|
|
520
521
|
git tag "v$VERSION"
|
|
521
522
|
git push origin "v$VERSION"
|
|
522
523
|
```
|
|
523
524
|
|
|
524
|
-
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.2.
|
|
525
|
+
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.2.5` requires tag `v0.2.5`. Published tags such as `v0.2.4` are immutable and must not be moved or reused.
|
|
525
526
|
|
|
526
527
|
## Publishing Hygiene
|
|
527
528
|
|
package/dist/cli.js
CHANGED
|
@@ -179,7 +179,7 @@ async function main() {
|
|
|
179
179
|
}, persistChatMessage: (message, taskId) => state.runtime.sessions.appendChatMessage({
|
|
180
180
|
...message,
|
|
181
181
|
taskId
|
|
182
|
-
}) }, state.runtime.workspaceRoot));
|
|
182
|
+
}), reloadConfig: async () => withUiThemeOverride(await loadConfig(cliArgs.appRoot), cliArgs.theme) }, state.runtime.workspaceRoot));
|
|
183
183
|
const removeSigintHandler = installInteractiveSigintExitHandler(() => shutdownController.abort());
|
|
184
184
|
enterInteractiveTerminal();
|
|
185
185
|
try {
|
package/dist/core/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { EngineNameSchema } from "../domain/schemas.js";
|
|
6
6
|
import { pathExists, readTextIfExists, writeText } from "./file-store.js";
|
|
7
|
+
import { workerBuffersOutputUntilCompletion } from "./worker-output-policy.js";
|
|
7
8
|
import { normalizeTuiThemeColorValue, normalizeTuiThemeName, TUI_THEME_FIELDS, TUI_THEME_NAMES } from "../tui/theme.js";
|
|
8
9
|
const NativeSessionConfigSchema = z.object({
|
|
9
10
|
enabled: z.boolean().default(true),
|
|
@@ -220,7 +221,7 @@ export function defaultConfig(projectRoot) {
|
|
|
220
221
|
assignable: true,
|
|
221
222
|
timeoutMs: 45 * 60 * 1000,
|
|
222
223
|
idleTimeoutMs: 5 * 60 * 1000,
|
|
223
|
-
firstOutputTimeoutMs:
|
|
224
|
+
firstOutputTimeoutMs: 45 * 60 * 1000,
|
|
224
225
|
model: {
|
|
225
226
|
name: "",
|
|
226
227
|
provider: "",
|
|
@@ -273,10 +274,10 @@ export function defaultConfig(projectRoot) {
|
|
|
273
274
|
}
|
|
274
275
|
},
|
|
275
276
|
pairing: {
|
|
276
|
-
main: "
|
|
277
|
+
main: "codex",
|
|
277
278
|
judge: "codex",
|
|
278
279
|
actor: "codex",
|
|
279
|
-
critic: "
|
|
280
|
+
critic: "claude"
|
|
280
281
|
},
|
|
281
282
|
roles: {
|
|
282
283
|
main: {
|
|
@@ -404,7 +405,7 @@ function resolveWorkerConfigs(builtins, configured) {
|
|
|
404
405
|
parent = resolve(parentId);
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
|
-
const worker = mergeWorkerConfig(parent, override);
|
|
408
|
+
const worker = normalizeBufferedWorkerTimeout(mergeWorkerConfig(parent, override));
|
|
408
409
|
resolved.set(id, worker);
|
|
409
410
|
return worker;
|
|
410
411
|
}
|
|
@@ -476,6 +477,20 @@ function mergeWorkerConfig(base, override) {
|
|
|
476
477
|
}
|
|
477
478
|
};
|
|
478
479
|
}
|
|
480
|
+
function normalizeBufferedWorkerTimeout(worker) {
|
|
481
|
+
const launchArgs = [
|
|
482
|
+
worker.args,
|
|
483
|
+
...(worker.nativeSession.enabled ? [worker.nativeSession.resumeArgs] : [])
|
|
484
|
+
];
|
|
485
|
+
const buffersOutput = launchArgs.some((args) => (workerBuffersOutputUntilCompletion(worker.capabilities.profile, args)));
|
|
486
|
+
if (!buffersOutput || !worker.timeoutMs || worker.firstOutputTimeoutMs === worker.timeoutMs) {
|
|
487
|
+
return worker;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
...worker,
|
|
491
|
+
firstOutputTimeoutMs: worker.timeoutMs
|
|
492
|
+
};
|
|
493
|
+
}
|
|
479
494
|
export function withUiThemeOverride(config, theme) {
|
|
480
495
|
if (!theme) {
|
|
481
496
|
return config;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const ANSI_CSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
+
const ANSI_OSC_PATTERN = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
3
|
+
const CODEX_HEADER_PATTERN = /^OpenAI Codex v\S+/;
|
|
4
|
+
const CODEX_COMMAND_PATTERN = /^\$\s+.*\bcodex(?:\s|$)/i;
|
|
5
|
+
const TOKEN_COUNT_PATTERN = /^[\d][\d,._ ]*$/;
|
|
6
|
+
/**
|
|
7
|
+
* Keeps process transcripts in output.log while returning only the response that
|
|
8
|
+
* belongs in chat. Codex writes its UI transcript to stderr and repeats the final
|
|
9
|
+
* response on stdout, so the cleanest answer normally follows "tokens used".
|
|
10
|
+
*/
|
|
11
|
+
export function extractMainResponse(outputLog) {
|
|
12
|
+
const normalized = stripTerminalControls(outputLog).replace(/\r\n?/g, "\n");
|
|
13
|
+
const lines = normalized.split("\n");
|
|
14
|
+
const isCodexTranscript = linesAreCodexTranscript(lines);
|
|
15
|
+
if (isCodexTranscript) {
|
|
16
|
+
const extracted = extractCodexFinalResponse(lines);
|
|
17
|
+
if (extracted) {
|
|
18
|
+
return extracted;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return lines
|
|
22
|
+
.filter((line) => !line.startsWith("$ "))
|
|
23
|
+
.filter((line) => !line.startsWith("[mock:main]"))
|
|
24
|
+
.join("\n")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
export function sanitizePersistedMainMessage(from, text) {
|
|
28
|
+
if (from !== "system" || !linesAreCodexTranscript(stripTerminalControls(text).split(/\r?\n/))) {
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
return extractMainResponse(text);
|
|
32
|
+
}
|
|
33
|
+
function linesAreCodexTranscript(lines) {
|
|
34
|
+
return lines.some((line) => CODEX_HEADER_PATTERN.test(line.trim()))
|
|
35
|
+
|| lines.some((line) => CODEX_COMMAND_PATTERN.test(line.trim()));
|
|
36
|
+
}
|
|
37
|
+
function extractCodexFinalResponse(lines) {
|
|
38
|
+
const tokensUsedIndex = findLastLine(lines, (line) => line.trim().toLowerCase() === "tokens used");
|
|
39
|
+
if (tokensUsedIndex >= 0) {
|
|
40
|
+
const trailing = trimBlankLines(lines.slice(tokensUsedIndex + 1));
|
|
41
|
+
if (trailing[0] && TOKEN_COUNT_PATTERN.test(trailing[0].trim())) {
|
|
42
|
+
trailing.shift();
|
|
43
|
+
}
|
|
44
|
+
const stdoutResponse = trimBlankLines(trailing).join("\n").trim();
|
|
45
|
+
if (stdoutResponse) {
|
|
46
|
+
return stdoutResponse;
|
|
47
|
+
}
|
|
48
|
+
const assistantIndex = findLastLine(lines.slice(0, tokensUsedIndex), (line) => line.trim().toLowerCase() === "codex");
|
|
49
|
+
if (assistantIndex >= 0) {
|
|
50
|
+
return trimBlankLines(lines.slice(assistantIndex + 1, tokensUsedIndex)).join("\n").trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function findLastLine(lines, predicate) {
|
|
56
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
57
|
+
if (predicate(lines[index] ?? "")) {
|
|
58
|
+
return index;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
|
63
|
+
function trimBlankLines(lines) {
|
|
64
|
+
let start = 0;
|
|
65
|
+
let end = lines.length;
|
|
66
|
+
while (start < end && !(lines[start] ?? "").trim()) {
|
|
67
|
+
start += 1;
|
|
68
|
+
}
|
|
69
|
+
while (end > start && !(lines[end - 1] ?? "").trim()) {
|
|
70
|
+
end -= 1;
|
|
71
|
+
}
|
|
72
|
+
return lines.slice(start, end);
|
|
73
|
+
}
|
|
74
|
+
function stripTerminalControls(text) {
|
|
75
|
+
return text
|
|
76
|
+
.replace(ANSI_CSI_PATTERN, "")
|
|
77
|
+
.replace(ANSI_OSC_PATTERN, "");
|
|
78
|
+
}
|
|
@@ -4,6 +4,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { appendJsonLine, appendText, ensureDir, pathExists, readJson, readRecentJsonLines, readTextIfExists, removeIfExists, writeJson, writeText } from "./file-store.js";
|
|
6
6
|
import { runWithLeaseFinalization } from "./lease-finalization.js";
|
|
7
|
+
import { sanitizePersistedMainMessage } from "./main-response.js";
|
|
7
8
|
import { formatTaskTimestamp, taskDir, taskSessionIdIsValid } from "./paths.js";
|
|
8
9
|
import { sessionsRoot } from "./paths.js";
|
|
9
10
|
import { loadCollaborationTimeline } from "./collaboration-timeline.js";
|
|
@@ -212,7 +213,11 @@ export class SessionManager {
|
|
|
212
213
|
const boundedLimit = Number.isFinite(limit)
|
|
213
214
|
? Math.min(1000, Math.max(0, Math.trunc(limit)))
|
|
214
215
|
: 200;
|
|
215
|
-
|
|
216
|
+
const records = await readRecentJsonLines(join(this.mainSessionDir(), "chat.jsonl"), ChatRecordSchema, boundedLimit);
|
|
217
|
+
return records.map((record) => ({
|
|
218
|
+
...record,
|
|
219
|
+
text: sanitizePersistedMainMessage(record.from, record.text)
|
|
220
|
+
}));
|
|
216
221
|
}
|
|
217
222
|
taskFromId(taskId) {
|
|
218
223
|
const dir = taskDir(this.projectRoot, this.dataDir, taskId);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function workerBuffersOutputUntilCompletion(profile, args) {
|
|
2
|
+
if (profile !== "claude" || !args.some((arg) => arg === "--print" || arg === "-p")) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
const outputFormat = readOptionValue(args, "--output-format");
|
|
6
|
+
return outputFormat === undefined || outputFormat === "text" || outputFormat === "json";
|
|
7
|
+
}
|
|
8
|
+
function readOptionValue(args, option) {
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === option) {
|
|
12
|
+
return args[index + 1]?.trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
if (arg?.startsWith(`${option}=`)) {
|
|
15
|
+
return arg.slice(option.length + 1).trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
@@ -2,6 +2,7 @@ import { cp, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, removeIfExists, writeJson, writeText } from "../core/file-store.js";
|
|
4
4
|
import { runWithLeaseFinalization } from "../core/lease-finalization.js";
|
|
5
|
+
import { extractMainResponse } from "../core/main-response.js";
|
|
5
6
|
import { claimTaskRunLease, TaskRunLeaseConflictError } from "../core/process-ownership.js";
|
|
6
7
|
import { routerRuntimeDir } from "../core/paths.js";
|
|
7
8
|
import { classifyRouterFailure, routerFallbackIsTransient } from "../core/router-audit.js";
|
|
@@ -2235,14 +2236,6 @@ async function recoverWorkerFileFromWorkspace(sourcePath, targetPath) {
|
|
|
2235
2236
|
}
|
|
2236
2237
|
await mirrorWorkerFileToFeature(sourcePath, targetPath);
|
|
2237
2238
|
}
|
|
2238
|
-
function extractMainResponse(outputLog) {
|
|
2239
|
-
return outputLog
|
|
2240
|
-
.split("\n")
|
|
2241
|
-
.filter((line) => !line.startsWith("$ "))
|
|
2242
|
-
.filter((line) => !line.startsWith("[mock:main]"))
|
|
2243
|
-
.join("\n")
|
|
2244
|
-
.trim();
|
|
2245
|
-
}
|
|
2246
2239
|
function emptyMainResponseSummary() {
|
|
2247
2240
|
return "简单对话通道没有收到可显示回复。";
|
|
2248
2241
|
}
|
|
@@ -7,6 +7,8 @@ const MAX_TEXT_MERGE_BYTES = 10 * 1024 * 1024;
|
|
|
7
7
|
const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
8
8
|
const COMMIT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/;
|
|
9
9
|
const LIVE_COMMIT_PROTOCOL = "atomic-claim-v1";
|
|
10
|
+
const HOST_METADATA_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
11
|
+
const HOST_METADATA_DIRECTORIES = new Set([".Spotlight-V100", ".Trashes", ".fseventsd"]);
|
|
10
12
|
export class WorkspaceMergeConflictError extends Error {
|
|
11
13
|
paths;
|
|
12
14
|
conflictDir;
|
|
@@ -405,10 +407,18 @@ export class ParallelWorkspaceManager {
|
|
|
405
407
|
if (segments.includes(".git")) {
|
|
406
408
|
return true;
|
|
407
409
|
}
|
|
410
|
+
if (isHostMetadataPath(segments)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
408
413
|
return Boolean(this.dataRelativePath
|
|
409
414
|
&& (path === this.dataRelativePath || path.startsWith(`${this.dataRelativePath}${sep}`)));
|
|
410
415
|
}
|
|
411
416
|
}
|
|
417
|
+
function isHostMetadataPath(segments) {
|
|
418
|
+
return segments.some((segment) => (HOST_METADATA_FILES.has(segment)
|
|
419
|
+
|| HOST_METADATA_DIRECTORIES.has(segment)
|
|
420
|
+
|| segment.startsWith("._")));
|
|
421
|
+
}
|
|
412
422
|
async function readJsonRecord(path) {
|
|
413
423
|
try {
|
|
414
424
|
const value = JSON.parse(await readFile(path, "utf8"));
|
package/dist/tui/App.js
CHANGED
|
@@ -34,6 +34,7 @@ import { latestTaskResultMessageIndex, parseTaskResultSummary } from "./task-res
|
|
|
34
34
|
import { buildNativeAttachLaunch, buildNativeForkLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
|
|
35
35
|
import { assignableWorkerProviderIds, workerProviderLabel, workerProviders } from "../workers/provider.js";
|
|
36
36
|
import { StatusDetailView } from "./StatusDetailView.js";
|
|
37
|
+
import { runtimeConfigChange } from "./runtime-config-state.js";
|
|
37
38
|
export function routeDecisionChatMessage(route) {
|
|
38
39
|
return `route · ${route.mode} · ${route.source ?? "router"}\n${route.reason.trim()}`;
|
|
39
40
|
}
|
|
@@ -48,7 +49,7 @@ const EMPTY_WORKER_NAVIGATION_TARGETS = {
|
|
|
48
49
|
errorOffsets: [],
|
|
49
50
|
diffOffsets: []
|
|
50
51
|
};
|
|
51
|
-
export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, loadTaskSessionDetails, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, exportDiagnostics, loadCollaborationTimeline, activateTaskSession, prepareNativeAttach, prepareNativeFork, startNativeAttach, copyToClipboard = copyTextToClipboard, shutdownSignal }) {
|
|
52
|
+
export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, reloadConfig, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, loadTaskSessionDetails, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, exportDiagnostics, loadCollaborationTimeline, activateTaskSession, prepareNativeAttach, prepareNativeFork, startNativeAttach, copyToClipboard = copyTextToClipboard, shutdownSignal }) {
|
|
52
53
|
configureTuiTheme({
|
|
53
54
|
theme: config.ui.theme,
|
|
54
55
|
colors: config.ui.colors
|
|
@@ -74,6 +75,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
74
75
|
const [activeMode, setActiveMode] = useState(initialTaskId ? "complex" : null);
|
|
75
76
|
const [canRetryTask, setCanRetryTask] = useState(initialCanRetryTask);
|
|
76
77
|
const [attachError, setAttachError] = useState(null);
|
|
78
|
+
const [configChange, setConfigChange] = useState(null);
|
|
77
79
|
const [clipboardNotice, setClipboardNotice] = useState(null);
|
|
78
80
|
const [nativeInput, setNativeInput] = useState("");
|
|
79
81
|
const [workerScrollOffset, setWorkerScrollOffset] = useState(0);
|
|
@@ -250,6 +252,43 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
250
252
|
process.stdout.off("resize", updateTerminalSize);
|
|
251
253
|
};
|
|
252
254
|
}, []);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (!reloadConfig) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let active = true;
|
|
260
|
+
let loading = false;
|
|
261
|
+
const refresh = async () => {
|
|
262
|
+
if (loading) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
loading = true;
|
|
266
|
+
try {
|
|
267
|
+
const latest = await reloadConfig();
|
|
268
|
+
if (active) {
|
|
269
|
+
setConfigChange(runtimeConfigChange(config, latest));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (active) {
|
|
274
|
+
setConfigChange({
|
|
275
|
+
kind: "restart",
|
|
276
|
+
detail: `config · invalid · ${error instanceof Error ? error.message : String(error)}`,
|
|
277
|
+
compact: "config invalid"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
loading = false;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
void refresh();
|
|
286
|
+
const interval = setInterval(() => void refresh(), 1500);
|
|
287
|
+
return () => {
|
|
288
|
+
active = false;
|
|
289
|
+
clearInterval(interval);
|
|
290
|
+
};
|
|
291
|
+
}, [config, reloadConfig]);
|
|
253
292
|
useEffect(() => {
|
|
254
293
|
inputCursorRef.current = inputCursor;
|
|
255
294
|
}, [inputCursor]);
|
|
@@ -2689,7 +2728,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2689
2728
|
if (view === "workspace") {
|
|
2690
2729
|
return (_jsx(Box, { flexDirection: "column", height: terminalSize.rows, children: _jsx(WorkspacePicker, { cwd: cwd, choices: workspaceChoices, terminalHeight: terminalSize.rows, terminalWidth: terminalWidth, onCancel: closeWorkspacePicker, onSelect: (workspace) => void selectWorkspace(workspace) }) }));
|
|
2691
2730
|
}
|
|
2692
|
-
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction: taskSessionAction?.type === "rename"
|
|
2731
|
+
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction: taskSessionAction?.type === "rename"
|
|
2693
2732
|
? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
|
|
2694
2733
|
: taskSessionAction?.type === "delete"
|
|
2695
2734
|
? { type: "delete", title: taskSessionAction.title }
|
|
@@ -2697,7 +2736,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2697
2736
|
? workerSearch.query
|
|
2698
2737
|
: view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" ? "" : input, cursor: workerSearch.open && view === "worker"
|
|
2699
2738
|
? workerSearch.cursor
|
|
2700
|
-
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
|
|
2739
|
+
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
|
|
2701
2740
|
? { type: "rename", title: taskSessionAction.title }
|
|
2702
2741
|
: taskSessionAction?.type === "delete"
|
|
2703
2742
|
? { type: "delete", title: taskSessionAction.title }
|
package/dist/tui/StatusBar.js
CHANGED
|
@@ -275,18 +275,24 @@ function currentStatusValueCandidates(text) {
|
|
|
275
275
|
].filter(Boolean)));
|
|
276
276
|
}
|
|
277
277
|
function compactCurrentStatusDetail(detail) {
|
|
278
|
+
if (/^working\s+·\s+buffered$/i.test(detail)) {
|
|
279
|
+
return "run buffered";
|
|
280
|
+
}
|
|
278
281
|
const progress = detail.match(/^(waiting output|responding)\s+·\s+(\d+(?:\.\d+)?s)\s*\/\s*(\d+(?:\.\d+)?(?:ms|s|m))\s+(?:first|idle)$/i);
|
|
279
282
|
if (progress) {
|
|
280
283
|
const state = progress[1]?.toLowerCase() === "responding" ? "reply" : "wait";
|
|
281
284
|
return `${state} ${progress[2]}/${progress[3]}`;
|
|
282
285
|
}
|
|
283
|
-
const state = detail.match(/^(starting|running|run|done|failed|fail|waiting|wait|queued|stopping|stop|idle|cancelled|canceled)\b/i)?.[1]?.toLowerCase();
|
|
286
|
+
const state = detail.match(/^(starting|working|running|run|done|failed|fail|waiting|wait|queued|stopping|stop|idle|cancelled|canceled)\b/i)?.[1]?.toLowerCase();
|
|
284
287
|
if (state === "starting") {
|
|
285
288
|
return "start";
|
|
286
289
|
}
|
|
287
290
|
if (state === "running") {
|
|
288
291
|
return "run";
|
|
289
292
|
}
|
|
293
|
+
if (state === "working") {
|
|
294
|
+
return "run";
|
|
295
|
+
}
|
|
290
296
|
if (state === "failed") {
|
|
291
297
|
return "fail";
|
|
292
298
|
}
|
|
@@ -763,7 +769,7 @@ function parseMainEngineStatus(part) {
|
|
|
763
769
|
};
|
|
764
770
|
}
|
|
765
771
|
function runtimeStatusTone(status) {
|
|
766
|
-
if (status === "run" || status === "running" || status === "starting" || status === "responding") {
|
|
772
|
+
if (status === "run" || status === "running" || status === "working" || status === "starting" || status === "responding") {
|
|
767
773
|
return "run";
|
|
768
774
|
}
|
|
769
775
|
if (status === "done") {
|
|
@@ -23,8 +23,18 @@ export function statusDetailDisplayLines(input, width, height) {
|
|
|
23
23
|
{
|
|
24
24
|
text: fitStatusDetailText(`workspace · ${basename(input.cwd) || input.cwd} · ${input.cwd}`, safeWidth),
|
|
25
25
|
tone: "muted"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
text: fitStatusDetailText(rolePairingDetail(input.pairing), safeWidth),
|
|
29
|
+
tone: "text"
|
|
26
30
|
}
|
|
27
31
|
];
|
|
32
|
+
if (input.configStatus?.trim()) {
|
|
33
|
+
lines.push({
|
|
34
|
+
text: fitStatusDetailText(input.configStatus, safeWidth),
|
|
35
|
+
tone: input.configRestartRequired ? "warning" : "success"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
28
38
|
if (input.routeStatus.trim()) {
|
|
29
39
|
lines.push({
|
|
30
40
|
text: fitStatusDetailText(input.routeStatus.trim(), safeWidth),
|
|
@@ -49,6 +59,15 @@ export function statusDetailDisplayLines(input, width, height) {
|
|
|
49
59
|
}
|
|
50
60
|
return lines.slice(0, maxLines);
|
|
51
61
|
}
|
|
62
|
+
function rolePairingDetail(pairing) {
|
|
63
|
+
return [
|
|
64
|
+
"active roles",
|
|
65
|
+
`main/${pairing.main}`,
|
|
66
|
+
`judge/${pairing.judge}`,
|
|
67
|
+
`actor/${pairing.actor}`,
|
|
68
|
+
`critic/${pairing.critic}`
|
|
69
|
+
].join(" · ");
|
|
70
|
+
}
|
|
52
71
|
function taskDetail(input) {
|
|
53
72
|
if (!input.taskId) {
|
|
54
73
|
return "task · none";
|
|
@@ -80,6 +80,12 @@ export function workerOverviewActivityLine(worker, nowMs, policy) {
|
|
|
80
80
|
tone: "warning"
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
if (status.phase === "process-buffered") {
|
|
84
|
+
return {
|
|
85
|
+
text: `activity · working ${formatWorkerActivityDuration(ageMs)} · output buffered until completion`,
|
|
86
|
+
tone: "muted"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
83
89
|
const starting = status.state === "starting";
|
|
84
90
|
const limitMs = effectiveWorkerWatchdog(starting ? policy.firstOutputTimeoutMs : policy.idleTimeoutMs, policy.timeoutMs);
|
|
85
91
|
const activity = starting ? "started" : "output";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function runtimeConfigChange(active, latest) {
|
|
2
|
+
if (runtimeConfigFingerprint(active) !== runtimeConfigFingerprint(latest)) {
|
|
3
|
+
return {
|
|
4
|
+
kind: "restart",
|
|
5
|
+
detail: "config · roles/workers changed · restart required",
|
|
6
|
+
compact: "config restart"
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
if (JSON.stringify(active.router) !== JSON.stringify(latest.router)) {
|
|
10
|
+
return {
|
|
11
|
+
kind: "router",
|
|
12
|
+
detail: "config · router changed · active on next request",
|
|
13
|
+
compact: "config router live"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function runtimeConfigFingerprint(config) {
|
|
19
|
+
const { router: _router, ...runtime } = config;
|
|
20
|
+
return JSON.stringify(runtime);
|
|
21
|
+
}
|
package/dist/tui/status-line.js
CHANGED
|
@@ -45,6 +45,9 @@ function formatMainStatus(status, progress) {
|
|
|
45
45
|
if (progress.phase === "initialized") {
|
|
46
46
|
return "starting";
|
|
47
47
|
}
|
|
48
|
+
if (progress.phase === "process-buffered") {
|
|
49
|
+
return "working · buffered";
|
|
50
|
+
}
|
|
48
51
|
if (progress.phase === "process-starting" || progress.phase === "native-resume-fallback") {
|
|
49
52
|
return formatMainWaitProgress("waiting output", progress.elapsedMs, progress.firstOutputTimeoutMs, "first");
|
|
50
53
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.2.
|
|
1
|
+
export const version = "0.2.5";
|
|
@@ -4,6 +4,7 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
4
4
|
import { appendText, writeJson } from "../core/file-store.js";
|
|
5
5
|
import { clearWorkerProcessRecord, writeWorkerProcessRecord } from "../core/process-ownership.js";
|
|
6
6
|
import { terminateProcessTree } from "../core/process-tree.js";
|
|
7
|
+
import { workerBuffersOutputUntilCompletion } from "../core/worker-output-policy.js";
|
|
7
8
|
import { detectNativeSessionId } from "./native-session-detection.js";
|
|
8
9
|
const WORKER_DIAGNOSTIC_TAIL_CHARS = 64 * 1024;
|
|
9
10
|
const RESUME_DETECTION_OVERLAP_CHARS = 512;
|
|
@@ -32,7 +33,8 @@ export class ProcessWorkerAdapter {
|
|
|
32
33
|
modelConfig: model
|
|
33
34
|
};
|
|
34
35
|
const first = await this.runAttempt(runSpec, launch, {
|
|
35
|
-
initialNativeSessionId: launch.initialNativeSessionId
|
|
36
|
+
initialNativeSessionId: launch.initialNativeSessionId,
|
|
37
|
+
outputBufferedUntilCompletion: workerBuffersOutputUntilCompletion(capabilities.profile, launch.args)
|
|
36
38
|
});
|
|
37
39
|
if (!shouldFallbackToNewNativeSession(first, runSpec.nativeSessionConfig)) {
|
|
38
40
|
return first.result;
|
|
@@ -49,7 +51,8 @@ export class ProcessWorkerAdapter {
|
|
|
49
51
|
}, freshLaunch, {
|
|
50
52
|
initialNativeSessionId: freshLaunch.initialNativeSessionId,
|
|
51
53
|
startPhase: "native-resume-fallback",
|
|
52
|
-
startSummary: `${this.command} starting fresh session after unrecoverable native resume
|
|
54
|
+
startSummary: `${this.command} starting fresh session after unrecoverable native resume`,
|
|
55
|
+
outputBufferedUntilCompletion: workerBuffersOutputUntilCompletion(capabilities.profile, freshLaunch.args)
|
|
53
56
|
})).result;
|
|
54
57
|
}
|
|
55
58
|
async runAttempt(runSpec, launch, options = {}) {
|
|
@@ -64,7 +67,13 @@ export class ProcessWorkerAdapter {
|
|
|
64
67
|
await appendText(runSpec.outputLogPath, "Process cancelled by user before start\n");
|
|
65
68
|
return { result, output: "", launch };
|
|
66
69
|
}
|
|
67
|
-
|
|
70
|
+
const startPhase = options.outputBufferedUntilCompletion
|
|
71
|
+
? "process-buffered"
|
|
72
|
+
: options.startPhase ?? "process-starting";
|
|
73
|
+
const startSummary = options.startSummary ?? `Starting ${this.command}`;
|
|
74
|
+
await setStatus(runSpec, "starting", startPhase, options.outputBufferedUntilCompletion
|
|
75
|
+
? `${startSummary}; text output is buffered until completion`
|
|
76
|
+
: startSummary);
|
|
68
77
|
await appendText(runSpec.outputLogPath, `$ ${formatShellCommand(this.command, launch.args)}\n`);
|
|
69
78
|
return new Promise((resolve, reject) => {
|
|
70
79
|
const detached = process.platform !== "win32";
|
|
@@ -456,7 +465,9 @@ export class ProcessWorkerAdapter {
|
|
|
456
465
|
failAndTerminate("process-timeout", `${this.command} exceeded ${runSpec.timeoutMs}ms`, `\nProcess timed out after ${runSpec.timeoutMs}ms\n`);
|
|
457
466
|
}, runSpec.timeoutMs);
|
|
458
467
|
}
|
|
459
|
-
if (
|
|
468
|
+
if (!options.outputBufferedUntilCompletion
|
|
469
|
+
&&
|
|
470
|
+
runSpec.firstOutputTimeoutMs
|
|
460
471
|
&& runSpec.firstOutputTimeoutMs > 0
|
|
461
472
|
&& (!runSpec.timeoutMs || runSpec.timeoutMs <= 0 || runSpec.firstOutputTimeoutMs < runSpec.timeoutMs)) {
|
|
462
473
|
firstOutputTimeout = setTimeout(() => {
|
|
@@ -540,7 +551,7 @@ function enforceWorkerIsolationArgs(args, profile, enforce) {
|
|
|
540
551
|
return enforceCodexWorkspaceSandbox(args);
|
|
541
552
|
}
|
|
542
553
|
if (profile === "claude") {
|
|
543
|
-
return
|
|
554
|
+
return enforceClaudeAutoPermissions(args);
|
|
544
555
|
}
|
|
545
556
|
return args;
|
|
546
557
|
}
|
|
@@ -586,7 +597,7 @@ function enforceCodexWorkspaceSandbox(args) {
|
|
|
586
597
|
}
|
|
587
598
|
return result;
|
|
588
599
|
}
|
|
589
|
-
function
|
|
600
|
+
function enforceClaudeAutoPermissions(args) {
|
|
590
601
|
const result = [];
|
|
591
602
|
let hasPermissionMode = false;
|
|
592
603
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -595,20 +606,20 @@ function enforceClaudeEditPermissions(args) {
|
|
|
595
606
|
continue;
|
|
596
607
|
}
|
|
597
608
|
if (arg === "--permission-mode") {
|
|
598
|
-
result.push(arg, "
|
|
609
|
+
result.push(arg, "auto");
|
|
599
610
|
hasPermissionMode = true;
|
|
600
611
|
index += 1;
|
|
601
612
|
continue;
|
|
602
613
|
}
|
|
603
614
|
if (arg.startsWith("--permission-mode=")) {
|
|
604
|
-
result.push("--permission-mode=
|
|
615
|
+
result.push("--permission-mode=auto");
|
|
605
616
|
hasPermissionMode = true;
|
|
606
617
|
continue;
|
|
607
618
|
}
|
|
608
619
|
result.push(arg);
|
|
609
620
|
}
|
|
610
621
|
if (!hasPermissionMode) {
|
|
611
|
-
result.push("--permission-mode", "
|
|
622
|
+
result.push("--permission-mode", "auto");
|
|
612
623
|
}
|
|
613
624
|
return result;
|
|
614
625
|
}
|