fix(session): keep a tab working while Claude waits for its own workers

When Claude hands work to an ultracode workflow or background agents, it
ends its own turn and closes it with `✻ Waiting for 1 dynamic workflow to
finish` instead of `✻ Brewed for 1m 18s`, then resumes by itself when the
workers report back. The pane sits quiet with the composer up, so the idle
probe called the session idle for the whole wait. At phone width the
workflow's progress row also drops its ticking timer, so nothing on screen
changes for minutes.

A new optional registry field, `capabilities.workDetect.awaitingLine`,
names that closing row, and `_probePaneWorking()` counts it as work.
Claude renders the row once from a snapshot and never redraws it, so the
same words stay on screen after the workers finish. `isAwaitingWorkers()`
therefore tests only the newest column-0 row directly above the composer,
never the whole pane and never the PTY stream; a follow-up turn always
puts rows of its own there. The column-0 anchor also keeps an agent from
holding its own tab busy by printing the sentence.

Verified against the live Mac mini pane that reported the bug (2.1.283),
and end to end on an isolated instance: an ultracode session running a
90 s workflow at 46 columns stayed busy through the wait and the
follow-up turn, then went idle 6 s after that turn closed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-28 16:18:27 +02:00
parent 45ea2e1d32
commit 7659ca8b44
10 changed files with 381 additions and 6 deletions
+9
View File
@@ -338,6 +338,15 @@ const capabilitiesSchema = z
// Bounded hard: this is how far up the screen a config file may push the search,
// and every row it adds is one more row the agent itself may be able to write.
watchingLines: z.number().int().min(1).max(8).optional(),
// Same guard again: tested against a pane row every time a session settles.
awaitingLine: z
.string()
.min(1)
.refine(
(src) => compileVersionRegex(src) !== null,
'awaitingLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers'
)
.optional(),
})
.strict()
// A window with nothing to search is a typo, not a configuration. Refused at LOAD
+8
View File
@@ -238,6 +238,14 @@ const CLAUDE: CliEntry = {
// watching rather than open that door. See `watchingLabel()` in
// `session-activity.ts`.
watchingLine: String.raw`·\s*(\d+ (?:monitors?|shells?|teams?|local agents?|cloud sessions?|MCP tasks?|background tasks?|(?:background|remote) dynamic workflows?|Artifact comment monitors?))`,
// When a turn ends while background agents or an ultracode workflow are still
// running, Claude swaps its `✻ Brewed for 1m 18s` closing row for
// `✻ Waiting for 2 background agents and 1 dynamic workflow to finish` and resumes
// by itself when they report back. Read from the 2.1.283 bundle (the turn-duration
// renderer) and a live pane on 2026-09-28. The row is a snapshot taken at turn end
// and never redrawn, which is why only the newest row above the composer counts.
// Anchored on column 0: Claude's own rows start there, the agent's prose never does.
awaitingLine: String.raw`^✻ Waiting for \d+ (?:background agents?|dynamic workflows?)\b`,
},
requiresMux: false,
// Claude installs Codeman's own hooks block into every workspace it runs in, so its
+13
View File
@@ -362,6 +362,19 @@ export interface CliCapabilities {
* alert. See `watchingLabel()` in `session-activity.ts`.
*/
watchingLines?: number;
/**
* Source of a regex matching the row this CLI closes a turn with when it ended that
* turn to WAIT for workers it started and will resume on its own once they finish,
* e.g. Claude's `✻ Waiting for 1 dynamic workflow to finish`. A pane showing it counts
* as working, not idle: nothing is being asked of the user, and the next turn starts
* without them.
*
* Unlike `workingLine` this is never searched across the pane. The CLI prints the row
* once and never updates it, so the copy from an earlier turn is still on screen after
* the workers are done. Only the newest transcript row directly above the composer is
* tested. See `isAwaitingWorkers()` in `session-activity.ts`.
*/
awaitingLine?: string;
};
/**
* How many columns this CLI indents its transcript body by, so a copy taken from its