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>
This commit is contained in:
Codeman maintainer
2026-10-09 03:37:41 +02:00
parent f3b2080e69
commit 7d8c188f83
6 changed files with 192 additions and 18 deletions
+22
View File
@@ -790,6 +790,28 @@ const GEMINI: CliEntry = {
...agentDefaults(),
altScreen: 'strip-full',
echo: { policy: 'buffer', anchor: { kind: 'cursor' } },
// Measured on a live Gemini CLI 0.63.0 pane (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, 2026-10-09). The TUI repaints its whole bottom region on
// every frame, composer included, and the composer sits between a `▄` bar and a `▀`
// bar; the submitted prompt is echoed between the same bars. So the `▀` bar arms the
// idle confirmation (every repaint and tmux's reattach repaint carry it), and it is the
// glyph rather than the composer's prompt character, which follows the approval mode
// (`*` in YOLO) and whose `>` also starts the echoed prompt. 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 where a long phrase
// pushed that suffix onto the next one. At rest nothing on screen matches either. A tool
// confirmation (default mode) replaces the composer and stops the spinner, and the pane
// goes silent, so it reads as idle (waiting on the user).
// ⚠️ Without this entry a gemini session latched `busy` after its first turn: the braille
// spinner trips SPINNER_PATTERN, and gemini never draws Claude's `❯`, the fallback that
// would have armed the idle check. A line starting with `▀` is a bar, never prompt text,
// so the submit verifier stands down.
workDetect: {
promptGlyph: '▀',
workingLine: String.raw`\(esc to cancel, \d|(?:^|\n) ?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] `,
},
// gemini's builder defaults an ABSENT approvalMode to 'yolo', so the clamp must
// MATERIALIZE a config (not just touch an already-sent one) or a non-granted owner who
// sends no geminiConfig at all would still get yolo for free.