mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
Merge pull request #648: fix(antigravity): show the working indicator while an agy turn runs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
d55267a07c
11 files changed
+219
-29
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'aicodeman': patch
|
||||
---
|
||||
|
||||
An Antigravity (`agy`) tab now shows that it is working. Its spinner animates a braille set Codeman did not recognise and the entry declared no work detection, so a running turn never marked the tab working and the activity indicator stayed off. The entry now declares what a turn draws (`⣯ Generating...`, `Working...`, `Loading...`) and its footer hint (`? for shortcuts`) as the idle mark, so a finished turn reads as idle again. A shell call waiting for approval keeps showing as working until you answer it.
|
||||
@@ -217,7 +217,7 @@ runs no agent, and the external CLIs render their own TUIs and post no hooks
|
||||
is `blocked` on `codex`: Codeman reads codex's approval, question and trust dialogs off
|
||||
its screen and raises `blocked` for them (see [Signals](#signals)). For every other mode
|
||||
only `idle`, `working` and `exit` are accepted. `exit` is dependable everywhere; `idle` and
|
||||
`working` are screen-checked for `opencode`, `gemini`, `pi` and `omp` (as for `claude` and
|
||||
`working` are screen-checked for `opencode`, `gemini`, `pi`, `omp` and `antigravity` (as for `claude` and
|
||||
`codex`) and not guaranteed elsewhere: see the caveats under [Signals](#signals) before
|
||||
building on `idle`.
|
||||
Requesting a signal the session can never emit **explicitly** is a `400` (`stop` on
|
||||
@@ -246,8 +246,8 @@ contract is a **marker unique to each call** (`MARK="DONE_$RANDOM"`, send
|
||||
|
||||
| Signal | Source | Actually fires for |
|
||||
|--------|--------|--------------------|
|
||||
| `idle` | the session's own `idle` event | `claude`: yes, on ❯-prompt detection after activity. `codex`, `opencode`, `gemini`, `pi`, `omp`: yes, the same screen check through the prompt marker and working line each declares (`capabilities.workDetect`). `shell`: **once only**, ~500 ms after start, and never again. Other external CLIs (`antigravity`, `grok`, `deepseek`): not guaranteed (they render their own TUIs and readiness is output stabilization) |
|
||||
| `working` | the session's own `working` event | `claude`, plus `codex`, `opencode`, `gemini`, `pi` and `omp` through the working line each declares. Elsewhere not guaranteed: it rests on generic output heuristics such as a braille spinner glyph |
|
||||
| `idle` | the session's own `idle` event | `claude`: yes, on ❯-prompt detection after activity. `codex`, `opencode`, `gemini`, `pi`, `omp`, `antigravity`: yes, the same screen check through the prompt marker and working line each declares (`capabilities.workDetect`); an `antigravity` session waiting on a shell-command approval stays `working` until it is answered. `shell`: **once only**, ~500 ms after start, and never again. Other external CLIs (`grok`, `deepseek`): not guaranteed (they render their own TUIs and readiness is output stabilization) |
|
||||
| `working` | the session's own `working` event | `claude`, plus `codex`, `opencode`, `gemini`, `pi`, `omp` and `antigravity` through the working line each declares. Elsewhere not guaranteed: it rests on generic output heuristics such as a braille spinner glyph |
|
||||
| `stop` | the Claude Code `stop` hook, the definitive end-of-turn signal | `claude` (and `deepseek`) only |
|
||||
| `blocked` | a `permission_prompt` or `elicitation_dialog` hook; on `codex`, a dialog read off its screen | `claude`, `deepseek` and `codex`; on `claude` rarer than it looks: see below |
|
||||
| `exit` | no process is behind the session | every mode |
|
||||
|
||||
@@ -272,7 +272,7 @@ So: `_confirmIdle()` (session.ts) requires the pane to go quiet, and then asks t
|
||||
|
||||
⚠️ **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.
|
||||
⚠️ **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, antigravity declares the head of its footer hint `? for shortcuts` (`? for sh`, drawn only at rest, so nothing arms the idle check during a turn and a pending shell approval reads as working until it is answered; the composer's `>` is also the echoed prompt's) plus its 8-dot braille spinner row `⣯ Generating...`, 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.
|
||||
|
||||
|
||||
@@ -122,11 +122,11 @@ the remote host, `grok login --device-auth` exists for exactly this.
|
||||
## Known gaps
|
||||
|
||||
- **No idle/completion hook yet.** The registry entry declares no `workDetect` prompt
|
||||
marker or working line, which is what gives Codex, OpenCode, Gemini, Pi and OMP a
|
||||
screen check. So Codeman calls a new grok pane idle 3 seconds after launch, and after
|
||||
marker or working line, which is what gives Codex, OpenCode, Gemini, Pi, OMP and
|
||||
Antigravity a screen check. So Codeman calls a new grok pane idle 3 seconds after launch, and after
|
||||
that it has only generic cues: Claude's `❯` marker, which grok does not print (see the
|
||||
cron item below), and a braille spinner glyph. Working and idle are unreliable for
|
||||
grok, as for Antigravity. Grok has a hooks system, so a Codeman hook POSTing to
|
||||
grok. Grok has a hooks system, so a Codeman hook POSTing to
|
||||
`/api/hook-event` is the highest-value follow-up.
|
||||
- **No response viewer.** Grok writes ACP JSONL sessions under
|
||||
`~/.grok/sessions/<encoded-cwd>/<session-id>/updates.jsonl`; nothing reads them yet.
|
||||
|
||||
+12
-1
@@ -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, OMP and Gemini: same screen check, via their own prompt and working line. DeepSeek: reports its end of turn itself. Antigravity and Grok: generic cues only, unreliable |
|
||||
| Precise idle detection | Yes | Codex, Pi, OpenCode, OMP, Gemini and Antigravity: same screen check, via their own prompt and working line. DeepSeek: reports its end of turn itself. Grok: generic cues only, unreliable |
|
||||
| Auto-resume when a usage limit resets | Yes | No |
|
||||
| Plan usage chip | Yes | No |
|
||||
| Approvals Inbox | Yes | DeepSeek and Codex as alerts (answer in the terminal); others no |
|
||||
@@ -184,6 +184,17 @@ Google's successor to the consumer Gemini CLI, invoked as `agy`. It keeps all of
|
||||
in `~/.gemini/antigravity-cli/`, so the credential handling that applies to Gemini applies
|
||||
to it as well.
|
||||
|
||||
- **Work detection reads its spinner row.** While a turn runs, `agy` animates an 8-dot braille
|
||||
frame and a label one row above the composer (`⣯ Generating...`; `Working...` and
|
||||
`Loading...` too), and its footer reads `esc to cancel`. Codeman matches that row, which is
|
||||
what puts the activity indicator on the tab. The frames are not the braille set Claude and
|
||||
Gemini use, which is why the tab used to look idle through a whole turn.
|
||||
- **At rest the footer hint is the idle mark.** The composer's `>` is no good for that, since
|
||||
the submitted prompt is echoed as `> <text>` as well. `? for shortcuts` is drawn only at
|
||||
rest, and the turn's last repaint carries it. A shell call that waits for approval replaces
|
||||
the composer and the footer hint is not drawn there, so the tab keeps reading as working
|
||||
until you answer.
|
||||
|
||||
### Pi
|
||||
|
||||
Pi needs the opposite instincts from every other CLI here.
|
||||
|
||||
@@ -28,14 +28,13 @@ There are several layers stacked on that: a completion message from the CLI, an
|
||||
output silence, and token stability.
|
||||
|
||||
**For the other CLIs** it depends on what the CLI tells Codeman. Codex, OpenCode, Gemini
|
||||
CLI, Pi and OMP each declare their own prompt marker and working line, so they get the same
|
||||
screen check Claude does (before 1.26.1 every Codex session reported idle for its whole
|
||||
life). DeepSeek Harness reports its end of turn and its dialogs to Codeman itself, which is
|
||||
as precise as hooks; its working and idle display still comes from its output. Antigravity
|
||||
and Grok Build declare neither. Codeman calls a new pane of theirs idle 3 seconds after
|
||||
launch, and after that it has only generic cues to go on (Claude's `❯` prompt marker, a
|
||||
braille spinner glyph), which these CLIs may not draw, so their working and idle status is
|
||||
unreliable.
|
||||
CLI, Pi, OMP and Antigravity each declare their own prompt marker and working line, so they
|
||||
get the same screen check Claude does (before 1.26.1 every Codex session reported idle for
|
||||
its whole life). DeepSeek Harness reports its end of turn and its dialogs to Codeman itself,
|
||||
which is as precise as hooks; its working and idle display still comes from its output.
|
||||
Grok Build declares neither. Codeman calls a new grok pane idle 3 seconds after launch, and
|
||||
after that it has only generic cues to go on (Claude's `❯` prompt marker, a braille spinner
|
||||
glyph), which grok may not draw, so its working and idle status is unreliable.
|
||||
|
||||
## The Respawn Controller
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ hooks, but its dialogs are distinctive enough to read off the screen, so a Codex
|
||||
waiting on an approval, a question or the "Trust this folder?" prompt turns red too, and
|
||||
clears a few seconds after you answer. For the other CLIs there are no hooks and no dialog
|
||||
reading, so you get only the coarser working and idle state: read off the screen for
|
||||
OpenCode, Gemini CLI, Pi and OMP, and unreliable for Antigravity and Grok Build, which
|
||||
declare nothing Codeman can read it from.
|
||||
OpenCode, Gemini CLI, Pi, OMP and Antigravity, and unreliable for Grok Build, which
|
||||
declares nothing Codeman can read it from.
|
||||
|
||||
## Window title and OS notifications
|
||||
|
||||
|
||||
@@ -116,12 +116,14 @@ seconds in. Current versions confirm against the actual screen before believing
|
||||
|
||||
### A session is stuck showing busy
|
||||
|
||||
For Antigravity and Grok Build the busy and idle status is unreliable: those CLIs expose no
|
||||
hooks and declare no working line, so after launch Codeman has only generic cues to go on
|
||||
(Claude's `❯` prompt marker, a braille spinner glyph), which they may not draw. Codex,
|
||||
OpenCode, Gemini CLI, Pi and OMP get the same screen check Claude does, so a session of
|
||||
theirs that has genuinely gone quiet will settle. If it never does, interrupt it (`Ctrl+C`
|
||||
with nothing selected).
|
||||
For Grok Build the busy and idle status is unreliable: it exposes no hooks and declares no
|
||||
working line, so after launch Codeman has only generic cues to go on (Claude's `❯` prompt
|
||||
marker, a braille spinner glyph), which it may not draw. Codex, OpenCode, Gemini CLI, Pi,
|
||||
OMP and Antigravity get the same screen check Claude does, so a session of theirs that has
|
||||
genuinely gone quiet will settle. An Antigravity session waiting on a shell-command approval
|
||||
("Run this command?") shows busy until you answer it: `agy` draws nothing Codeman reads as
|
||||
"at rest" while that list is up. If a session never settles, interrupt it (`Ctrl+C` with
|
||||
nothing selected).
|
||||
|
||||
### The agent asks about bypass permissions every time
|
||||
|
||||
|
||||
@@ -996,6 +996,30 @@ const ANTIGRAVITY: CliEntry = {
|
||||
...AGENTS_DIR_SKILL,
|
||||
altScreen: 'strip-mux-only',
|
||||
echo: { policy: 'buffer', anchor: { kind: 'cursor' } },
|
||||
// Measured on a live `agy` 1.3.3 pane (capture-pane every second through a prose turn
|
||||
// and a shell-call turn, plus the raw PTY stream, 2026-10-11). A running turn animates
|
||||
// `⣯ Generating...` (the label also reads `Working...` and `Loading...`) one row above
|
||||
// the composer, and the footer's left end reads `esc to cancel`; at rest the footer
|
||||
// reads `? for shortcuts` and nothing else on screen matches. A shell call replaces the
|
||||
// composer with a `Run this command?` list, the spinner stops, and the pane goes
|
||||
// silent. Nothing arms the idle check while the list is up (the footer hint is drawn only
|
||||
// at rest), so a pending approval reads as working until it is answered. That differs from
|
||||
// gemini, whose `▀` bar comes back on every spinner repaint.
|
||||
// ⚠️ The frames are the 8-dot braille set (`⣯ ⡿ ⣻ ⣾ ⣷ ⣟ ⢿ ⣽`), none of which is in
|
||||
// SPINNER_PATTERN's Claude set, and this entry used to declare nothing, so no frame
|
||||
// ever marked an antigravity tab working and it never showed the activity spinner.
|
||||
// The composer's `>` is no arming mark: the submitted prompt is echoed as `> <text>`
|
||||
// too. The footer hint is drawn only at rest, and the turn's last repaint carries it.
|
||||
// The schema caps a glyph at 8 characters, so this is the head of `? for shortcuts`. It also
|
||||
// keeps the submit verifier inert (no row starts with it and holds prompt text).
|
||||
// The working line is anchored to the start of a row, like gemini's braille branch: the
|
||||
// idle probe tests it against the whole pane, and an answer that QUOTES the spinner row
|
||||
// (an agent explaining this entry) would otherwise hold the tab busy while it is on screen.
|
||||
// Every `agy` capture draws the spinner at column 0.
|
||||
workDetect: {
|
||||
promptGlyph: '? for sh',
|
||||
workingLine: String.raw`(?:^|\n)[⠀-⣿] [A-Z][A-Za-z ]{2,30}(?:\.\.\.|…)`,
|
||||
},
|
||||
// Like codex: an ABSENT config already defaults safe (no bypass flag), so only a
|
||||
// SENT config needs the flag forced off — nothing is materialized.
|
||||
privilegedParams: [{ param: 'dangerouslySkipPermissions', clampTo: false }],
|
||||
|
||||
+1
-1
@@ -4800,7 +4800,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, omp and gemini declare a promptGlyph; the other modes would fall back
|
||||
// Only claude, codex, pi, opencode, omp, gemini and antigravity 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Working/idle detection for an interactive agent pane, Claude's, Codex's, pi's, opencode's, omp's
|
||||
* and Gemini CLI's.
|
||||
* Gemini CLI's and Antigravity'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
|
||||
@@ -15,8 +15,9 @@
|
||||
*
|
||||
* 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, 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).
|
||||
* opencode 1.3.0, omp 18.8.6 / 18.0.11, 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) and
|
||||
* Antigravity `agy` 1.3.3.
|
||||
*/
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest';
|
||||
import { Session } from '../src/session.js';
|
||||
@@ -51,7 +52,7 @@ function feed(session: Session, data: string): void {
|
||||
*/
|
||||
function withFakePane(
|
||||
screen: string | (() => string),
|
||||
mode: 'claude' | 'codex' | 'pi' | 'opencode' | 'omp' | 'gemini' = 'claude'
|
||||
mode: 'claude' | 'codex' | 'pi' | 'opencode' | 'omp' | 'gemini' | 'antigravity' = 'claude'
|
||||
): Session {
|
||||
const read = typeof screen === 'function' ? screen : () => screen;
|
||||
const mux = {
|
||||
@@ -978,6 +979,154 @@ describe('gemini interactive idle detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Antigravity's (`agy` 1.3.3) pane, verbatim from live captures (rules shortened). A running
|
||||
* turn animates an 8-dot braille frame and a label one row above the composer, and the
|
||||
* footer reads `esc to cancel`; at rest the footer reads `? for shortcuts`. The composer
|
||||
* row is `>`, which the echoed prompt starts with as well.
|
||||
*/
|
||||
const AGY_RULE = '─'.repeat(48);
|
||||
const AGY_FOOTER_REST = '? for shortcuts Gemini 3.8 Flash · medium\n';
|
||||
const AGY_FOOTER_WORKING = 'esc to cancel Gemini 3.8 Flash · medium\n';
|
||||
const AGY_WORKING = `> say ok\n⣷ Generating...\n${AGY_RULE}\n>\n${AGY_RULE}\n${AGY_FOOTER_WORKING}`;
|
||||
const AGY_AT_REST = ` ok\n${AGY_RULE}\n>\n${AGY_RULE}\n${AGY_FOOTER_REST}`;
|
||||
/** A shell call replaces the composer with a confirmation list and the spinner stops. */
|
||||
const AGY_CONFIRM =
|
||||
'> Run the shell command: sleep 6 && echo done\n sleep 6 && echo done\nRun this command?\n' +
|
||||
"> 1. Yes, run command\n 2. Yes, and always allow in this conversation for commands that start with 'sleep'\n" +
|
||||
' 4. No, cancel\n ↑/↓ Navigate · tab Amend · ctrl+g edit/expand command\n';
|
||||
/** One spinner frame on the wire, verbatim: the glyph is not one of SPINNER_PATTERN's. */
|
||||
const AGY_SPINNER_FRAME = '\r\n\x1b[34;1m> say ok\x1b[m\x1b[K\r\n⣷ \x1b[90mGenerating...\x1b[m\x1b[K\r\n';
|
||||
/** The turn's last repaint, verbatim (shortened): the footer hint comes back. */
|
||||
const AGY_TURN_END = `${AGY_RULE}\x1b[m\r\n\x1b[90m? for shortcuts\x1b[m\x1b[80X\x1b[80C\x1b[2mGemini 3.8 Flash · medium\r`;
|
||||
|
||||
describe("antigravity's work-detection descriptor", () => {
|
||||
const agy = getCli('antigravity')?.capabilities.workDetect;
|
||||
const working = () => new RegExp(agy!.workingLine);
|
||||
|
||||
it('matches every frame of the spinner and every label, on screen and on the wire', () => {
|
||||
expect(working().test(AGY_WORKING)).toBe(true);
|
||||
expect(working().test(stripAnsi(AGY_SPINNER_FRAME))).toBe(true);
|
||||
for (const frame of ['⣯', '⡿', '⣻', '⣾', '⣷', '⣟', '⢿', '⣽']) {
|
||||
for (const label of ['Generating...', 'Working...', 'Loading...']) {
|
||||
expect(working().test(`${frame} ${label}`)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('does not match a pane at rest, a pending confirmation, or prose', () => {
|
||||
expect(working().test(AGY_AT_REST)).toBe(false);
|
||||
expect(working().test(AGY_CONFIRM)).toBe(false);
|
||||
expect(working().test('Loading... is what the spinner label reads\n')).toBe(false);
|
||||
// An answer that quotes the spinner row mid-line must not hold the tab busy.
|
||||
expect(working().test(` ok\n it matches \`⣯ Generating...\` in the pane\n${AGY_RULE}\n>\n`)).toBe(false);
|
||||
});
|
||||
|
||||
it('names the footer hint the turn end carries, and no working repaint does', () => {
|
||||
expect(AGY_TURN_END).toContain(agy!.promptGlyph);
|
||||
expect(AGY_AT_REST).toContain(agy!.promptGlyph);
|
||||
expect(AGY_WORKING).not.toContain(agy!.promptGlyph);
|
||||
expect(AGY_SPINNER_FRAME).not.toContain(agy!.promptGlyph);
|
||||
});
|
||||
|
||||
it('leaves the submit verifier unable to press Enter on an antigravity pane', () => {
|
||||
// The composer row starts with `>`, but the glyph is the footer hint, so a prompt
|
||||
// typed into the composer (or echoed above it) is never read as "still unsubmitted".
|
||||
const typed = AGY_AT_REST.replace('>\n', '> say ok\n');
|
||||
expect(promptStillInComposer(typed, 'say ok', agy!.promptGlyph)).not.toBe(true);
|
||||
expect(promptStillInComposer(AGY_WORKING, 'say ok', agy!.promptGlyph)).not.toBe(true);
|
||||
expect(promptStillInComposer(AGY_CONFIRM, 'Run the shell command', agy!.promptGlyph)).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('antigravity interactive activity', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
function spin(session: Session, ms: number): void {
|
||||
for (let t = 0; t < ms; t += 100) {
|
||||
feed(session, AGY_SPINNER_FRAME);
|
||||
vi.advanceTimersByTime(100);
|
||||
}
|
||||
}
|
||||
|
||||
it('shows a turn as working, and lets it end instead of latching busy', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = AGY_WORKING;
|
||||
const session = withFakePane(() => screen, 'antigravity');
|
||||
(session as unknown as { _status: string })._status = 'idle';
|
||||
const events: string[] = [];
|
||||
session.on('working', () => events.push('working'));
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
// Before antigravity declared its spinner line no frame ever marked the tab working.
|
||||
spin(session, 4000);
|
||||
expect(events).toEqual(['working']);
|
||||
expect(session.status).toBe('busy');
|
||||
|
||||
screen = AGY_AT_REST;
|
||||
feed(session, AGY_TURN_END);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
|
||||
expect(events).toEqual(['working', 'idle']);
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
|
||||
it('keeps a pending shell confirmation working until it is answered, and ends the resumed turn too', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = AGY_WORKING;
|
||||
const session = withFakePane(() => screen, 'antigravity');
|
||||
(session as unknown as { _status: string })._status = 'idle';
|
||||
const events: string[] = [];
|
||||
session.on('working', () => events.push('working'));
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
spin(session, 3000);
|
||||
expect(events).toEqual(['working']);
|
||||
|
||||
screen = AGY_CONFIRM;
|
||||
feed(session, 'Run this command?\r\n');
|
||||
vi.advanceTimersByTime(60_000);
|
||||
// The footer hint is not drawn under the confirmation, so nothing arms the idle check:
|
||||
// the pane stays working (unlike gemini, whose bar returns on every repaint) until the
|
||||
// user answers and the turn ends below.
|
||||
expect(events).toEqual(['working']);
|
||||
expect(session.status).toBe('busy');
|
||||
screen = AGY_WORKING;
|
||||
spin(session, 3000);
|
||||
screen = AGY_AT_REST;
|
||||
feed(session, AGY_TURN_END);
|
||||
vi.advanceTimersByTime(20_000);
|
||||
|
||||
expect(events[0]).toBe('working');
|
||||
expect(events[events.length - 1]).toBe('idle');
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('antigravity reattach', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// Declaring workDetect turns off the 3 s settle for a restored pane, so a reattached agy pane
|
||||
// depends on the footer hint arriving in tmux's attach repaint to leave `busy`.
|
||||
it('settles a reattached antigravity pane that is at rest', () => {
|
||||
vi.useFakeTimers();
|
||||
const session = withFakePane(AGY_AT_REST, 'antigravity');
|
||||
(session as unknown as { _status: string })._status = 'busy';
|
||||
const events: string[] = [];
|
||||
session.on('idle', () => events.push('idle'));
|
||||
|
||||
feed(session, AGY_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
|
||||
|
||||
Reference in new issue
Block a user