mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
fix(session): never settle a just-prompted pane idle at launch
40560acemade 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. Before40560acethe 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>
This commit is contained in:
+14
-8
@@ -3465,7 +3465,8 @@ export class Session extends EventEmitter {
|
||||
private _armPaneSettle(isRestored: boolean): void {
|
||||
const applies = isRestored ? !getCli(this.mode)?.capabilities.workDetect : isExternalCliMode(this.mode);
|
||||
if (!applies) return;
|
||||
this._promptCheckTimeout = setTimeout(() => this._settlePaneStartup(!isRestored), 3000);
|
||||
const armedAt = Date.now();
|
||||
this._promptCheckTimeout = setTimeout(() => this._settlePaneStartup(!isRestored, armedAt), 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3478,18 +3479,23 @@ export class Session extends EventEmitter {
|
||||
* never tripped `_markWorking()`, the later `_confirmIdle()` found the status
|
||||
* already idle and emitted nothing, so no browser ever learned the pane was ready.
|
||||
*
|
||||
* A pane marked working by then is left alone only when its CLI declares
|
||||
* `capabilities.workDetect`: that CLI's composer glyph arms `_confirmIdle()`, which
|
||||
* reads the screen and ends the turn properly. For every other CLI this timer is the
|
||||
* only thing that ever settles a fresh pane, so it settles it even if a stray spinner
|
||||
* glyph in the launch paint latched `_isWorking`.
|
||||
* A pane is left to `_confirmIdle()` only when its CLI declares
|
||||
* `capabilities.workDetect` (its composer glyph arms that confirmation, which reads
|
||||
* the screen first) AND it is already working, or a prompt was submitted since the
|
||||
* timer was armed: a turn started 2.9 s in is not marked working before the deferred
|
||||
* parsers run, and an idle edge here would end a send-and-wait registered for it.
|
||||
* For every other CLI this timer is the only thing that ever settles a fresh pane,
|
||||
* so it settles it even if a stray spinner glyph in the launch paint latched
|
||||
* `_isWorking`.
|
||||
*
|
||||
* @param refreshScreen emit `needsRefresh` (a new pane; an attach refetches by itself)
|
||||
* @param armedAt when the timer was armed (a submit at or after it means a prompt)
|
||||
*/
|
||||
private _settlePaneStartup(refreshScreen: boolean): void {
|
||||
private _settlePaneStartup(refreshScreen: boolean, armedAt: number): void {
|
||||
this._promptCheckTimeout = null;
|
||||
if (this._isStopped) return;
|
||||
const leaveToConfirm = this._isWorking && !!getCli(this.mode)?.capabilities.workDetect;
|
||||
const busyWithTurn = this._isWorking || this.lastSubmitAt >= armedAt;
|
||||
const leaveToConfirm = busyWithTurn && !!getCli(this.mode)?.capabilities.workDetect;
|
||||
if (this._status === 'busy' && !leaveToConfirm) this._concludeIdle(false);
|
||||
if (refreshScreen) this.emit('needsRefresh');
|
||||
}
|
||||
|
||||
@@ -430,6 +430,46 @@ describe('external CLI launch settle', () => {
|
||||
expect(session.isWorking).toBe(false);
|
||||
});
|
||||
|
||||
it('never announces idle for a codex pane prompted before the timer, so a send-and-wait is not ended early', () => {
|
||||
vi.useFakeTimers();
|
||||
let screen = CODEX_READY;
|
||||
const session = withFakePane(() => screen, 'codex');
|
||||
const events = launch(session);
|
||||
|
||||
// A prompt 2 s in: its turn has not been marked working when the timer fires.
|
||||
vi.advanceTimersByTime(2000);
|
||||
session.markPromptSubmitted();
|
||||
screen = CODEX_WORKING;
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(events).toEqual(['needsRefresh']);
|
||||
expect(session.status).toBe('busy');
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
feed(session, CODEX_COMPOSER_REPAINT);
|
||||
vi.advanceTimersByTime(1000);
|
||||
}
|
||||
screen = CODEX_FINISHED;
|
||||
vi.advanceTimersByTime(30_000);
|
||||
|
||||
// The turn's own end is the one idle edge.
|
||||
expect(events.filter((e) => e === 'idle')).toEqual(['idle']);
|
||||
expect(events[events.length - 1]).toBe('idle');
|
||||
expect(session.status).toBe('idle');
|
||||
});
|
||||
|
||||
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: 'opencode' });
|
||||
const events = launch(session);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
session.markPromptSubmitted();
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(events).toEqual(['idle', 'needsRefresh']);
|
||||
});
|
||||
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user