mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
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:
@@ -266,7 +266,7 @@ 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 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, 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.
|
||||
⚠️ **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.
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ output. The other CLIs expose no equivalent.
|
||||
| Respawn cycling and unattended runs | Yes | Yes |
|
||||
| Cron jobs | Yes | Yes |
|
||||
| Docker cases, remote SSH cases | Yes | Yes |
|
||||
| Precise idle detection | Yes | Codex, Pi, OpenCode and OMP: same screen check, via their own prompt and working line. DeepSeek: reports its state itself. Others: output stabilization, coarser |
|
||||
| Precise idle detection | Yes | Codex, Pi, OpenCode, OMP and Gemini: same screen check, via their own prompt and working line. DeepSeek: reports its state itself. Others: output stabilization, coarser |
|
||||
| Auto-resume when a usage limit resets | Yes | No |
|
||||
| Plan usage chip | Yes | No |
|
||||
| Approvals Inbox | Yes | DeepSeek yes; others no |
|
||||
@@ -153,6 +153,11 @@ needs `GOOGLE_CLOUD_PROJECT`, `GOOGLE_APPLICATION_CREDENTIALS`, and
|
||||
`GOOGLE_GENAI_USE_VERTEXAI`. That is the loosest allowlist entry in Codeman and it affects
|
||||
only the CLI you spawned yourself.
|
||||
|
||||
Working and idle come from the screen: while a turn runs, Gemini CLI draws a spinner line
|
||||
(`⠦ Thinking... (esc to cancel, 6s)`) above its composer, and Codeman reads that. A tool
|
||||
confirmation that waits for you shows as idle. Before 1.36.0 a Gemini session showed as
|
||||
working for good after its first turn.
|
||||
|
||||
### Antigravity
|
||||
|
||||
Google's successor to the consumer Gemini CLI, invoked as `agy`. It keeps all of its state
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -4485,7 +4485,7 @@ export class Session extends EventEmitter {
|
||||
: this._mux.capturePaneText?.(this._muxSession.muxName),
|
||||
sendEnter: () => this._mux?.sendInput(this.id, '\r'),
|
||||
// ⚠ NO fallback glyph here, unlike the screen-reading probe elsewhere in this file.
|
||||
// Only claude, codex, pi, opencode and omp declare a promptGlyph; the other modes would fall back
|
||||
// Only claude, codex, pi, opencode, omp and gemini declare a promptGlyph; the other modes would fall back
|
||||
// to claude's `❯`, which is ALSO starship's default shell prompt (and pure's, and
|
||||
// spaceship's, and p10k lean's). On a shell session the line `❯ npm run build` sits
|
||||
// on screen for as long as the command runs, promptStillInComposer() reads that as
|
||||
|
||||
+160
-13
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Working/idle detection for an interactive agent pane, Claude's, Codex's, pi's, opencode's and omp's.
|
||||
* Working/idle detection for an interactive agent pane, Claude's, Codex's, pi's, opencode's, omp's
|
||||
* and Gemini CLI's.
|
||||
*
|
||||
* The bug this pins: Claude redraws the composer (`❯`) about once a second all
|
||||
* the way through a turn, so the old "saw a ❯, wait 2s, call it idle" rule
|
||||
@@ -14,7 +15,8 @@
|
||||
*
|
||||
* The status-line fixtures below are verbatim captures from live panes
|
||||
* (`tmux -L codeman capture-pane -p`) on Claude Code 2.1.220, Codex CLI 0.152.1, pi 1.1.0,
|
||||
* opencode 1.3.0 and omp 18.8.6 / 18.0.11.
|
||||
* opencode 1.3.0, omp 18.8.6 / 18.0.11 and Gemini CLI 0.63.0 (its turns driven by a local
|
||||
* stand-in for the Gemini API, since the CLI's look does not depend on the backend).
|
||||
*/
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest';
|
||||
import { Session } from '../src/session.js';
|
||||
@@ -49,7 +51,7 @@ function feed(session: Session, data: string): void {
|
||||
*/
|
||||
function withFakePane(
|
||||
screen: string | (() => string),
|
||||
mode: 'claude' | 'codex' | 'pi' | 'opencode' | 'omp' = 'claude'
|
||||
mode: 'claude' | 'codex' | 'pi' | 'opencode' | 'omp' | 'gemini' = 'claude'
|
||||
): Session {
|
||||
const read = typeof screen === 'function' ? screen : () => screen;
|
||||
const mux = {
|
||||
@@ -157,6 +159,39 @@ const OMP_SPINNER_FRAME =
|
||||
/** The input row omp redraws when a turn is submitted and when it ends. */
|
||||
const OMP_INPUT_REPAINT = '\x1b[20;1H\x1b[0m\x1b[K\x1b[38;5;239m╰─ \x1b[39;38;5;254;39m \x1b[0m';
|
||||
|
||||
/**
|
||||
* Gemini CLI's pane, verbatim from live 0.63.0 captures (bars shortened). The composer
|
||||
* sits between a `▄` bar and a `▀` bar, and a running turn draws a spinner line above it.
|
||||
*/
|
||||
const GEM_COMPOSER =
|
||||
`${'─'.repeat(40)}\n YOLO Ctrl+Y\n${'▄'.repeat(40)}\n * Type your message or @path/to/file\n` +
|
||||
`${'▀'.repeat(40)}\n workspace sandbox\n /tmp/.../gem-turn no sandbox …\n`;
|
||||
const GEM_WORKING =
|
||||
'╭──────────────────────────────────╮\n│ ⊶ Shell sleep 8; echo done │\n' +
|
||||
`╰──────────────────────────────────╯\n ⠦ Thinking... (esc to cancel, 6s)\n${GEM_COMPOSER}`;
|
||||
const GEM_AT_REST =
|
||||
'✦ The sleep command pauses the shell for\n the number of seconds it is given,\n' +
|
||||
` before printing done.\n${GEM_COMPOSER}`;
|
||||
/**
|
||||
* Default approval mode: the confirmation replaces the composer and the spinner stops.
|
||||
* The submitted prompt above it is echoed between the composer's own bars.
|
||||
*/
|
||||
const GEM_CONFIRM =
|
||||
`${'▄'.repeat(40)}\n > Run sleep 8 in the shell, then explain what sleep does.\n${'▀'.repeat(40)}\n` +
|
||||
'╭──────────────────────────────────╮\n│ ? Shell sleep 8; echo done │\n' +
|
||||
'│ Allow execution of [Shell]? │\n│ │\n' +
|
||||
'│ ● 1. Allow once │\n│ 2. Allow for this session │\n' +
|
||||
'│ 3. No, suggest changes (esc) │\n╰──────────────────────────────────╯\n';
|
||||
/** One spinner frame on the wire (every ~80 ms), verbatim: the line starts with a cursor move. */
|
||||
const GEM_SPINNER_FRAME =
|
||||
'\x1b[9;1H\x1b(B\x1b[m \x1b[38;5;111m⠧\x1b[39m \x1b[38;5;231m\x1b[3mThinking...\x1b(B\x1b[m ' +
|
||||
`\x1b[38;5;145m(esc to cancel, 8s)\r\n\x1b[15;1H\x1b[49m\x1b[38;5;59m${'▀'.repeat(40)}`;
|
||||
/** The turn's last repaint, verbatim (shortened): no spinner line, the composer bars redrawn. */
|
||||
const GEM_TURN_END =
|
||||
`\x1b[?2026h\x1b[11;1H\x1b(B\x1b[m \x1b[38;5;211mYOLO\x1b[38;5;145m Ctrl+Y\x1b[13;1H\x1b[38;5;59m${'▄'.repeat(40)}` +
|
||||
'\x1b[14;1H\x1b[39m\x1b[48;5;59m \x1b[38;5;211m* \x1b[39m\x1b[7m \x1b(B\x1b[m\x1b[38;5;145m\x1b[48;5;59m Type your message' +
|
||||
` or @path/to/file\x1b[39m \x1b[15;1H\x1b[49m\x1b[38;5;59m${'▀'.repeat(40)}\x1b[?2026l`;
|
||||
|
||||
/** A composer repaint: the frame Claude ships roughly once a second while working. */
|
||||
const COMPOSER_REPAINT =
|
||||
'\x1b[31;1H\x1b[38;5;246m❯\xa0\x1b[39m\x1b[0m\x1b[33;1H \x1b[38;5;246mOpus 5 in:143,699 out:669 ctx:14%\x1b[39m';
|
||||
@@ -337,11 +372,11 @@ describe('Session interactive idle detection', () => {
|
||||
|
||||
it('does not mark an uncharacterised CLI working off raw activity', () => {
|
||||
vi.useFakeTimers();
|
||||
// Gemini and OpenCode render their own TUIs, and Codeman knows neither one's glyph,
|
||||
// so nothing would arm the idle confirmation and a session marked working here would
|
||||
// never recover. A CLI that names no glyph therefore reports no work at all.
|
||||
expect(getCli('gemini')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'gemini' });
|
||||
// Grok renders its own TUI, and Codeman knows no glyph for it, so nothing would arm
|
||||
// the idle confirmation and a session marked working here would never recover. A CLI
|
||||
// that names no glyph therefore reports no work at all.
|
||||
expect(getCli('grok')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'grok' });
|
||||
const events: string[] = [];
|
||||
session.on('working', () => events.push('working'));
|
||||
|
||||
@@ -486,8 +521,8 @@ describe('external CLI launch settle', () => {
|
||||
vi.useFakeTimers();
|
||||
// Nothing arms an idle confirmation for a CLI that names no composer glyph, so
|
||||
// the launch timer is the only thing that can ever settle this pane.
|
||||
expect(getCli('gemini')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'gemini' });
|
||||
expect(getCli('grok')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'grok' });
|
||||
const events = launch(session);
|
||||
|
||||
feed(session, '\x1b[5;3H⠋ Loading');
|
||||
@@ -529,7 +564,7 @@ describe('external CLI launch settle', () => {
|
||||
|
||||
it('still settles a prompted CLI without work detection, which has nothing else to settle it', () => {
|
||||
vi.useFakeTimers();
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'gemini' });
|
||||
const session = new Session({ workingDir: '/tmp', mode: 'grok' });
|
||||
const events = launch(session);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
@@ -542,8 +577,8 @@ describe('external CLI launch settle', () => {
|
||||
it('settles a RESTORED pane of a CLI without work detection, without a refetch', () => {
|
||||
vi.useFakeTimers();
|
||||
// A Codeman restart re-attaches every surviving pane through startInteractive(),
|
||||
// which leaves it busy; gemini and antigravity have no glyph that would ever clear that.
|
||||
for (const mode of ['gemini', 'antigravity', 'shell'] as const) {
|
||||
// which leaves it busy; grok and deepseek have no glyph that would ever clear that.
|
||||
for (const mode of ['grok', 'deepseek', 'shell'] as const) {
|
||||
expect(getCli(mode)?.capabilities.workDetect).toBeUndefined();
|
||||
const session = new Session({ workingDir: '/tmp', mode });
|
||||
const events = launch(session, true);
|
||||
@@ -831,6 +866,118 @@ describe('opencode interactive idle detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("gemini's work-detection descriptor", () => {
|
||||
const gem = getCli('gemini')?.capabilities.workDetect;
|
||||
const working = () => new RegExp(gem!.workingLine);
|
||||
|
||||
it('matches the spinner line while a turn runs, on screen and on the wire', () => {
|
||||
expect(working().test(GEM_WORKING)).toBe(true);
|
||||
// The stream detector reads the ANSI-stripped chunk, where a cursor move, not a
|
||||
// newline, opens the spinner line: the `(esc to cancel` half is what matches there.
|
||||
expect(working().test(stripAnsi(GEM_SPINNER_FRAME))).toBe(true);
|
||||
// A long loading phrase can push the suffix onto the next line on a narrow pane
|
||||
// (constructed, not captured): the spinner frame opening its line still matches.
|
||||
expect(working().test(' ⠦ Reticulating the splines for your\n request... (esc to cancel,\n')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not match a pane at rest, nor a pending tool confirmation', () => {
|
||||
expect(working().test(GEM_AT_REST)).toBe(false);
|
||||
expect(working().test(GEM_CONFIRM)).toBe(false);
|
||||
});
|
||||
|
||||
it('names the composer bar every repaint carries, so the idle check can arm', () => {
|
||||
expect(GEM_SPINNER_FRAME).toContain(gem!.promptGlyph);
|
||||
expect(GEM_TURN_END).toContain(gem!.promptGlyph);
|
||||
expect(GEM_AT_REST).toContain(gem!.promptGlyph);
|
||||
});
|
||||
|
||||
it('leaves the submit verifier unable to press Enter on a gemini pane', () => {
|
||||
// The last row starting with `▀` is a bar, never prompt text, even when the echoed
|
||||
// prompt sits just above it while a confirmation waits.
|
||||
const typed = GEM_COMPOSER.replace('Type your message or @path/to/file', 'say ok');
|
||||
expect(promptStillInComposer(typed, 'say ok', gem!.promptGlyph)).toBe(false);
|
||||
expect(promptStillInComposer(GEM_AT_REST, 'say ok', gem!.promptGlyph)).toBe(false);
|
||||
expect(promptStillInComposer(GEM_CONFIRM, 'Run sleep 8 in the shell', gem!.promptGlyph)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gemini interactive idle detection', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function spin(session: Session, ms: number): void {
|
||||
for (let t = 0; t < ms; t += 80) {
|
||||
feed(session, GEM_SPINNER_FRAME);
|
||||
vi.advanceTimersByTime(80);
|
||||
}
|
||||
}
|
||||
|
||||
it('lets a turn end, instead of latching busy', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = GEM_WORKING;
|
||||
const session = withFakePane(() => screen, 'gemini');
|
||||
const events: string[] = [];
|
||||
session.on('working', () => events.push('working'));
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
spin(session, 8000);
|
||||
expect(events).toEqual(['working']);
|
||||
expect(session.status).toBe('busy');
|
||||
|
||||
// Before gemini declared its bar and spinner line, nothing ever armed the idle check
|
||||
// (gemini never draws `❯`), so this session stayed busy for good.
|
||||
screen = GEM_AT_REST;
|
||||
feed(session, GEM_TURN_END);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
|
||||
expect(events).toEqual(['working', 'idle']);
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('reads a pending tool confirmation as idle, and ends the resumed turn too', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = GEM_WORKING;
|
||||
const session = withFakePane(() => screen, 'gemini');
|
||||
const events: string[] = [];
|
||||
session.on('working', () => events.push('working'));
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
spin(session, 3000);
|
||||
|
||||
// The confirmation replaces the composer and the pane goes silent: waiting on the user.
|
||||
screen = GEM_CONFIRM;
|
||||
feed(session, `\x1b[5;1H${'▀'.repeat(40)}\x1b[7;1H│ Allow execution of [Shell]?`);
|
||||
vi.advanceTimersByTime(10_000);
|
||||
expect(events).toEqual(['working', 'idle']);
|
||||
|
||||
// Allowed: the spinner line returns with every repaint carrying the bar.
|
||||
screen = GEM_WORKING;
|
||||
spin(session, 4000);
|
||||
expect(events).toEqual(['working', 'idle', 'working']);
|
||||
|
||||
screen = GEM_AT_REST;
|
||||
feed(session, GEM_TURN_END);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
expect(events).toEqual(['working', 'idle', 'working', 'idle']);
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('settles a reattached gemini pane that is at rest', () => {
|
||||
vi.useFakeTimers();
|
||||
const session = withFakePane(GEM_AT_REST, 'gemini');
|
||||
(session as unknown as { _status: string })._status = 'busy';
|
||||
const events: string[] = [];
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
feed(session, GEM_TURN_END);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
|
||||
expect(events).toEqual(['idle']);
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wire activity stamp across recovery', () => {
|
||||
// The stamp both home screens sort the quiet group on. Recovery restores the
|
||||
// previous run's value, and the settle window keeps the boot attach repaint
|
||||
|
||||
@@ -373,8 +373,8 @@ describe('Session.watching', () => {
|
||||
|
||||
it('reports nothing for a CLI whose screen nobody has characterised', () => {
|
||||
vi.useFakeTimers();
|
||||
expect(getCli('gemini')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = withFakePane(WITH_MONITOR, 'gemini');
|
||||
expect(getCli('grok')?.capabilities.workDetect).toBeUndefined();
|
||||
const session = withFakePane(WITH_MONITOR, 'grok');
|
||||
runAndSettle(session);
|
||||
expect(session.watching).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user