Commit Graph
10 Commits
Author SHA1 Message Date
Codeman maintainer 7d8c188f83 fix(gemini): read Gemini CLI's composer bar and spinner line so a turn can end
A gemini session stayed "working" for good after its first turn. Its braille
spinner trips the generic SPINNER_PATTERN and marks the pane working, but only
a composer glyph arms the idle confirmation and gemini declared none, so it
fell back to Claude's `❯`, which gemini never draws.

Measured on live Gemini CLI 0.63.0 panes (capture-pane every 300 ms through
real turns with a shell call at 40, 120 and 200 columns, YOLO and default
approval mode, plus the raw PTY stream). The turns ran against a local
stand-in for the Gemini API (GOOGLE_GEMINI_BASE_URL, which Codeman's custom
endpoint support already sets), since the CLI's TUI does not depend on the
backend and no account is needed for it:

- The TUI repaints its whole bottom region every frame, composer included,
  and the composer sits between a `▄` bar and a `▀` bar; the submitted prompt
  is echoed between the same bars. The `▀` bar arms the idle check: every
  repaint carries it, tmux's reattach repaint too. The composer's prompt
  character is no good: it follows the approval mode (`*` in YOLO), and its
  `>` also starts the echoed prompt, which would make the submit verifier
  read a submitted prompt as stranded and press Enter again.
- While a turn runs a line `⠦ Thinking... (esc to cancel, 6s)` animates about
  every 80 ms (largest gap mid-turn: 214 ms). The label can be any loading
  phrase, so the working line is the `(esc to cancel, <n>` suffix, or a
  spinner frame opening a line for when a long phrase wraps that suffix.
  Nothing at rest matches either.
- A tool confirmation (default mode) replaces the composer, stops the
  spinner and the pane goes silent (3.9 s gap), so it reads as idle.

Verified on an isolated instance from this branch: a YOLO turn emitted one
session:working (+170 ms) and one session:idle (2.5 s after the last output);
a default-mode turn went working -> idle while the confirmation waited ->
working once allowed -> idle at the end; after a server restart four restored
gemini panes went busy -> idle in about 4 s; a fresh launch settled in 3 s.

The launch-settle and uncharacterised-CLI tests that used gemini as their
example of a CLI without work detection now use grok and deepseek.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 03:37:41 +02:00
Codeman maintainer 112c533ac7 fix(omp): read omp's status bar so an omp turn can end
An omp session stayed "working" for good once a turn started, the same
latch pi had: omp's braille spinner trips the SPINNER_PATTERN fast path,
and only a composer glyph arms the idle confirmation. omp declared none,
so it fell back to Claude's `❯`, which omp never draws once its setup
wizard is done.

Measured on live omp 18.8.6 and 18.0.11 panes, holding a turn open
against a local endpoint that never answers: the input row is `╰─ <text>`
and is redrawn at submit, at the end of a turn, at launch and on
reattach. While a turn runs, the status bar's leading `π` becomes a
braille spinner plus the elapsed time (` ⠼ 14s > ⬢ model > ...`; 18.0.11
pads it with two spaces, past a minute it reads `1m`), with a
`⎋ Working…` row above it. The registry entry now names the input row as
the glyph and either working signal as the working line.

The glyph also switches the submit verifier on for omp, which reads the
input row the way it reads Claude's composer. A prompt sent mid-turn goes
to omp's Steering queue and clears the row, so the verifier stands down.
Text left in the row after an Enter is the one case it re-presses.

Verified end to end on a sandboxed instance (own HOME and PATH, omp
18.8.6): session:idle at launch, session:working during a turn,
session:idle about 3 s after it ended, and a restored pane settled idle
about 3 s after a server restart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:43:22 +02:00
Codeman maintainer e7d661158b fix(opencode): read opencode's composer bar and footer spinner so a turn can end
An opencode session that ran a tool stayed "working" for good. The running
tool row draws a braille spinner (`⠋ Sleep for 12 seconds...`), which trips
the generic SPINNER_PATTERN and marks the pane working, but only a composer
glyph arms the idle confirmation and opencode declared none, so it fell back
to Claude's `❯`, which opencode never draws. Measured on an isolated
instance: a 16 s turn latched busy/isWorking for the rest of the session.
A text-only turn had the opposite problem and never showed as working.

Measured on live opencode 1.3.0 panes (capture-pane every 250-300 ms through
real turns at 40, 60, 120 and 200 columns, plus the raw PTY stream):

- Every composer row starts with a `┃` bar, and the submitted prompt lands in
  the transcript with the same bar, so a turn's first repaint arms the idle
  check, and tmux's reattach repaint does the same for a restored pane.
- While a turn runs the footer row starts with an 8-cell knight-rider
  spinner, `⬝■■■■■■⬝  esc interrupt`, redrawn about every 40 ms (largest
  gap mid-turn: 121 ms). At rest the TUI is silent and nothing on screen
  draws a `⬝`/`■` run, the 200-column sidebar included.
- The working line is the spinner run, `[⬝■]{8}`, not the label: tmux ships
  `esc` and `interrupt` as separate words joined by cursor moves, so the
  label never reaches the stream detector, and at 40 columns the footer
  wraps it to `esc` / `interr` / `upt`. All 344 spinner chunks of a turn
  match the run after Codeman's ANSI strip.
- A pending permission prompt replaces the composer and stops the spinner,
  so it reads as idle (waiting on the user).
- The last `┃` row on screen is the composer's agent/model row, or the
  permission box's closing bar, never the prompt text, so the submit
  verifier stands down and can never press Enter into a dialog.

Verified on an isolated instance from this branch: a 15 s tool turn emitted
exactly one session:working (+271 ms) and one session:idle (3 s after the
spinner stopped); a permission prompt read idle and the allowed turn went
working -> idle; after a server restart both restored opencode panes (one
at rest, one on a permission prompt) went busy -> idle in about 4 s; a
fresh launch reached an open page as idle in 3 s.

The launch-settle tests that used opencode as their example of a CLI
without work detection now use gemini and antigravity.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:40:04 +02:00
Codeman maintainer db168cdbe5 fix(session): never settle a just-prompted pane idle at launch
40560ace made the 3 s launch settle announce its idle. For a CLI with
work detection that edge could now land in the middle of a turn: a
prompt sent ~2.9 s after launch has not been marked working yet (the
working line goes through the deferred parsers), so the settle called
the pane idle and a send-and-wait registered for that prompt resolved
before the turn even started. Before 40560ace the settle was silent, so
this edge is new.

The settle now also leaves a pane to `_confirmIdle()` when a prompt was
submitted since the timer was armed, the same way it already does for a
pane marked working; that confirmation reads the screen before it ends
the turn. A CLI without work detection still settles: nothing else ever
would.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 02:18:26 +02:00
Codeman maintainer b539780f33 fix(pi): read pi's composer rule so a pi turn can end
A pi session stayed "working" for good once a turn started. pi's braille
spinner trips the SPINNER_PATTERN fast path, which marks the pane working,
but only a composer glyph arms the idle confirmation and pi declared none,
so it fell back to Claude's `❯`, which pi never draws. Measured on beta136:
an errored turn stayed busy/isWorking for 3+ minutes after pi was back at
rest.

pi has no composer glyph. Measured on a live pi 1.1.0 pane (capture-pane
every 250 ms through a turn): its composer sits between two `─` rules, and
while a turn runs it rewrites the top rule as `── ⠏ Working ───` on every
frame. The registry entry now names the rule as the glyph that arms the
check and a spinner frame inside it as the working line.

The same glyph settles a reattached pi pane (a restored pane gets no launch
timer): tmux's reattach repaint carries `─`, which arms the confirmation.
The submit verifier reads the last rule, finds no prompt text and stands
down, so it can never press Enter on a pi pane.

Verified on an isolated instance from this branch: a real pi turn emitted
session:working then session:idle, and after a server restart the restored
pi pane went busy -> idle in about 3 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 01:49:19 +02:00
Codeman maintainer 40560aced1 fix(session): announce when a fresh or re-attached agent pane goes idle
A fresh codex, pi or opencode tile could spin "working" forever while the
pane sat at its composer. The external-CLI launch timer set the status to
idle WITHOUT an event, only `needsRefresh`. When the launch paint never
marked the pane working, the later idle confirmation found the status
already idle and announced nothing either, so every open browser kept the
`busy` from the spawn broadcast. A reload fixed it, which is why only open
pages were stuck. Measured on the 1.36.0 beta: w8 (codex) had
`lastPromptTime: 0`, i.e. no idle edge ever, and a page held a fresh pi
session at `busy` while GET /api/sessions said `idle`. pi and opencode hit
it on every launch (no work detection, so the timer is all they have);
codex only when its launch paint lost the race against the timer.

- `_concludeIdle()` is now the one place a pane is concluded idle: status,
  working flag and prompt stamp change together and `idle` is emitted
  (session:idle + state broadcast). `_confirmIdle()` uses it too.
- The launch settle (`_settlePaneStartup`) concludes a pane still in its
  spawn-time `busy`. A pane already working is left to `_confirmIdle()`
  only when its CLI declares `capabilities.workDetect`; for the rest the
  timer is the only thing that can settle it, so it also clears a working
  flag a launch spinner glyph latched.
- `_armPaneSettle()` also arms it for a RESTORED pane (Codeman restart,
  auto-reattach, tile Attach) of a CLI without work detection (at this
  commit shell, opencode, gemini, antigravity, pi, grok, deepseek and omp),
  which used to stay `busy` server-side with nothing to clear it. Restored claude and
  codex panes stay on their composer glyph, so a restart mid-turn is never
  called idle. No `needsRefresh` there: an attach refetches by itself.

Pre-existing since the OpenCode integration, not a regression of this
release. Restore-path gap reported by the opencode and pi sessions working
the same symptom.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 01:49:04 +02:00
Codeman maintainer f1b7283393 fix(cli-registry): guard workDetect.workingLine like every other config regex
#385 made the composer glyph and the working status line per-CLI registry
data, which is right, but `workingLine` arrived as a config-supplied regex
validated with a bare `new RegExp()`. That skips `compileVersionRegex()`,
the helper the registry uses for exactly this: a `~/.codeman/clis.json`
override can set the field, the compiled pattern is run against every
accumulated PTY chunk and every pane capture, and a nested quantifier there
backtracks on the event loop for the whole server rather than one session.

Route it through the helper in both places, which are not redundant: the
schema refine rejects the entry at LOAD time so a bad pattern never reaches
a session, and `_workingLinePattern()` compiles through the same helper so
the runtime cannot hold a pattern the schema would have refused. The helper
returns null instead of throwing, so the Claude-pattern fallback stops being
a try/catch and becomes structural. Both shipped patterns compile unchanged,
and Claude's is behaviourally identical to CLAUDE_WORKING_LINE_PATTERN.

Also match the Codex footer case-insensitively on the E. It was
characterised against codex-cli 0.152.1, which prints a lowercase `esc`;
a version capitalising it would make the whole fix silently inert, since
the pane would simply never look like it was working.

Docs: CLAUDE.md, architecture-invariants and cli-registry.md all still
stated the Claude-mode-only rule this PR retires, and none of them named
the new capability or the regex guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 22:42:20 +02:00
Michael Grundberg 51957e2ed4 fix(session): let each CLI declare how its own pane shows work
A Codex session reported `isWorking: false` for its entire life, including
mid-turn. Codeman has four paths that mark a session working, and all four were
inert for Codex:

- The spinner fast path tests eight braille frames, and Codex animates none.
- The activity-streak fallback was wrapped in `!isExternalCliMode(mode)`.
- The pane probe inside `_confirmIdle` would have matched, since Codex prints
  `esc to interrupt`, but arming it required the literal glyph `❯` and Codex
  draws `›` on its composer row.
- The text detector sat inside `_processExpensiveParsers`, whose first statement
  returns early for an external CLI.

Add an optional `workDetect: { promptGlyph, workingLine }` to CliCapabilities,
so the two strings that differ per CLI are registry data rather than constants
in the detector. Claude declares its existing pair and behaves as before. Codex
declares `›` and `esc to interrupt`. The text detector moves above the
external-CLI early return, guarded on the descriptor so a CLI without one still
skips the ANSI strip that the early return used to save it.

A CLI that declares no descriptor falls back to Claude's pair, and the
activity-streak gate now reads "has a descriptor, or is not external", so the
plain shell mode keeps the behaviour it had.

Rewrite the test that asserted the old premise in its own comment, so it makes
the same guarantee for a genuinely uncharacterised CLI, and add Codex coverage
built from verbatim pane captures on Codex CLI 0.152.1.
2026-09-06 17:41:01 +02:00
Codeman maintainer cb9149879d restore the activity stamp across restarts: the quiet ordering no longer flattens on deploy
Root cause of the reviewer's mass-bump measurement (17 of 17 sessions with an
identical lastActivityAt): every restart restamps all sessions in the
constructor loop, and the boot auto-attach's repaint re-bumps the rest within
the same second. A 12-minute steady-state sample shows NO ambient mass bump,
so restarts are the whole story, and Codeman restarts on every deploy.

The stamp now has a display twin: recovery threads the previous run's
lastActivityAt from state.json into the wire-visible stamp (getter + toState),
and a 15s settle window keeps the attach repaint from overwriting it. Real
actions (input, task assignment, respawn) always write through. The private
stamp keeps its boot-anchored semantics untouched, because the idle
confirmation reads it as how long the pane has been quiet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 20:32:41 +02:00
Codeman maintainer b03780dfd2 fix(session): decide working/idle from the pane, not the composer redraw
Every working Claude session reported `status: "idle"` about two seconds
into its turn. Measured on live workers: two sessions mid-tool-call at 13
and 17 minutes both read `idle` while their panes showed
`✻ Actualizing… (13m 23s · ↓ 47.5k tokens)`.

Two things had drifted apart:

1. The working indicator changed. Claude animates the glyph through
   `· ✢ ✳ ∗ ✻ ✽` and randomizes the gerund per turn, so neither
   SPINNER_PATTERN (braille, no longer drawn) nor the keyword list
   (Thinking/Writing/Reading/Running) matches a turn anymore.
2. A `❯` sighting is not the end of a turn. Claude redraws the composer
   roughly once a second all the way through one, and that redraw armed
   the "2s later, call it idle" timer.

Matching the new status line in the STREAM does not fix it either: tmux
ships partial repaints, so the complete line reached the PTY about once
every 20 seconds while the `❯` arrived every second.

So the decision moves off the stream:

- An unbroken run of repaints marks a turn as started. Sampled once a
  second for 12s over six live sessions, the two working ones produced
  output in 12/12 windows and the four idle ones in 0/12. Pure helpers in
  session-activity.ts carry the thresholds.
- Idle now needs the pane to go quiet AND the screen to agree.
  `_confirmIdle()` asks tmux what is rendered (new `capturePaneText()`,
  one plain `capture-pane`, floored at 1.5s per session and only ever at
  a transition) and re-checks every 5s while the screen still shows work.
  A turn can sit silent for tens of seconds inside one tool call, so
  silence alone proves nothing.
- The same screen check vetoes keystroke echo, which is a steady stream
  of repaints too but is not work.

CLAUDE_WORKING_LINE_PATTERN matches the `… (elapsed)` shape rather than
the glyph, because the FINISHED line (`✻ Cooked for 2m 49s`) carries the
same glyph and would otherwise pin a session at working forever.

Claude mode only. An external CLI has no `❯`, so nothing would arm the
confirmation and such a session would latch busy.

respawn-patterns.hasWorkingPattern() had the same blind spot (its gerund
list cannot see "Actualizing"), so it takes the pattern as an extra
signal. That can only make respawn less eager, never more.

Idle now lands about 3 to 5 seconds after a turn ends instead of 2
seconds into one. Verified end to end against a live worker, sampled
against the CLI's own "esc to interrupt" footer as independent ground
truth: busy for all 25s of a turn, idle 3s after it ended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:31:02 +02:00