Merge pull request #634 from JDProfresh/fix/idle-not-on-trust-dialog

fix(idle): hold idle while the workspace trust dialog is on screen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-16 01:59:55 +02:00
commit 9aa3083280
7 files changed
+154 -16

No files matched your search

+8 -8
View File
@@ -250,9 +250,9 @@ the wait promptly instead of burning the caller's whole timeout on something tha
can no longer happen). On a `claude` worker, prefer an explicit `until=stop,exit`
once the session is up: the default set's `idle` also resolves on a spinner pause,
and on a fresh session the **startup** `idle` (emitted when the CLI first comes up)
can land inside your first wait window and report a turn that never ran. Measured:
a session parked on the trust dialog emits no *further* `idle`, so it is the
startup transition, not the dialog, that produces the false success below.
can land inside your first wait window and report a turn that never ran. That
startup `idle` is held while Claude's workspace-trust dialog is on screen (inside
the 90 s startup window), because the dialog's own ❯ is not a composer.
⚠️ **`exit` means "nothing is running", which includes "not started yet".** The
server answers from `pid === null` plus a mux-layer pane-death probe, and that
@@ -290,11 +290,11 @@ instead. The same caution applies to the external CLIs.
Nothing here reports "the agent is ready for a prompt", and no combination of
`until`/`fresh` synthesizes one. A freshly created session reads as `exit` (above),
and a `claude` worker in a brand-new case comes up on the CLI's **trust dialog**,
which contains a ❯ prompt of its own. Send-and-wait posted at that moment types the
prompt into the dialog, where the `\r` never gets past it, while the session's
startup `idle` lands inside the wait window: the wait resolves on `idle` in a
couple of seconds with `timedOut: false`, which looks exactly like a completed
turn.
which contains a ❯ of its own. A prompt posted at that moment is typed into the
dialog, and its `\r` confirms the highlighted `No, exit`: the CLI quits and the
pane dies, so send-and-wait resolves on `exit`. Codeman holds the session's `idle`
while the dialog is on screen, so waiting for `idle` before prompting no longer
clears too early, but that still does not make `idle` a readiness signal.
The reliable sequence is: poll `GET /api/v1/sessions/:id` until `.data.pid` is
non-null, then `wait-output` for the composer's own marker (`bypass`, the status
+2
View File
@@ -270,6 +270,8 @@ Scoping needs no check of its own here: `setPaneExit()` already forces `paneExit
So: `_confirmIdle()` (session.ts) requires the pane to go quiet, and then asks the SCREEN via `capturePaneText()` + `CLAUDE_WORKING_LINE_PATTERN` before believing it; a sustained run of repaints (`session-activity.ts`, pure + unit tested) is what marks a turn as started, with the same screen probe vetoing keystroke echo. Idle now lands ~3-5s after a turn ends instead of 2s into one.
⚠️ **The workspace-trust dialog is not a composer.** Its `❯` sits on `No, exit`, so it arms the confirmation, and the dialog is quiet with no working line, so a fresh case used to be concluded idle about 4 s after launch while the dialog was still up (measured on Claude Code 2.1.294: `GET .../wait?until=idle` answered `idle` at 3.8 s in 6 of 12 fresh-dir launches). An agent that waited for idle and then prompted typed into the dialog, and the prompt's Enter confirmed `No, exit`: the pane died and the wait reported `exit`. `_probePaneWorking()` therefore also records whether its capture shows `isTrustDialogScreen()` (a failed capture keeps the last answer, since it proves nothing), and `_confirmIdle()` re-checks on `PANE_PROBE_RECHECK_MS` instead of concluding while it does, but only inside the auto-accept's own `TRUST_DIALOG_WINDOW_MS`: a later screen that merely quotes the dialog must not hold a session busy. The status is left alone rather than marked working, since nothing is running. Tests: `test/session-trust-dialog.test.ts`.
⚠️ **The composer glyph and the working line are per-CLI registry DATA** (`capabilities.workDetect`, #385), not Claude constants: claude declares `❯` plus the pattern above, codex declares `›` plus `[Ee]sc to interrupt`, pi declares its composer rule `─` plus the spinner it embeds there, opencode declares its composer bar `┃` plus its 8-cell footer spinner `[⬝■]{8}`, omp declares its input row `╰─` plus the spinner and elapsed time that replace the `π` in its status bar, gemini declares its composer's bottom bar `▀` plus its `(esc to cancel, <n>s)` spinner line, and a CLI that declares neither falls back to Claude's pair, which is what every session used before the registry carried one. Before that, this whole mechanism was gated Claude-mode-only on the reasoning that an external CLI has no `❯`, which was true and still left every Codex session reporting `idle` for its entire life.
⚠️ `workingLine` is config-supplied (a user `clis.json` can set it) and the compiled pattern runs on the PTY hot path, so it goes through `compileVersionRegex()` in BOTH the schema refine and `_workingLinePattern()`: a nested quantifier there is a ReDoS against the event loop, and the helper returns null rather than throwing so the fallback is structural.
+4 -4
View File
@@ -227,10 +227,10 @@ whether your integration works, and the last one is what actually bites:
⚠️ **There is no readiness signal, and skipping readiness is the failure that looks
like success.** A session reports `idle` before its CLI has spawned, and a `claude`
worker in a brand-new case comes up on the CLI's **trust dialog**, which has a ❯
prompt of its own. Prompt it at that moment and the text lands in the dialog, the
`\r` does not get past it, and the session's startup `idle` lands inside the wait
window: the wait resolves on `idle` in a couple of seconds with `timedOut: false`,
indistinguishable from a finished turn. Wait for the pid, then wait for the
prompt of its own. Prompt it at that moment and the text lands in the dialog,
whose highlighted option is `No, exit`, so the prompt's `\r` quits the CLI. Codeman
holds the session's `idle` while the dialog is on screen, which makes waiting for
`idle` safe but still not a readiness signal. Wait for the pid, then wait for the
composer, answering the dialog only as the bounded fallback.
⚠️ **Answering it is not "press Enter".** Claude Code 2.1.252 dropped the options'
+3 -3
View File
@@ -149,9 +149,9 @@ Read these before writing any code. Each one has cost somebody an afternoon.
output marker instead.
7. **Nothing reports "ready", so wait for it explicitly.** A new session answers
`{"signal":"exit","immediate":true}` until its PID exists, and that means *not started*,
not *crashed*. A Claude worker in a fresh case then sits on the CLI's trust dialog; prompt
it there and the wait resolves on idle in about two seconds looking exactly like a finished
turn, while your text sits stuck in the dialog.
not *crashed*. A Claude worker in a fresh case then sits on the CLI's trust dialog, whose
highlighted option is `No, exit`: prompt it there and your Enter quits the CLI. Codeman
holds `idle` while the dialog is on screen, but that is not a readiness signal either.
### Recipes