fix: detect idle state with inline suggestions and esc-to-interrupt signal

Claude Code shows inline suggestions after the prompt when idle (e.g.
"❯ check CI status on the PR"). Also strip separator lines (─) from
terminal output before detection. Use status bar "esc to interrupt"
presence as the authoritative active-vs-idle signal.

- Strip separator lines and empty lines alongside status bar lines
- Check raw output for "esc to interrupt" before stripping
- Treat ❯ + text as idle when status bar lacks esc to interrupt
- 4 new tests covering suggestion detection edge cases
This commit is contained in:
sjd9021 2026-02-16 01:16:34 +05:30
parent f389c99a9c
commit fad2938804
2 changed files with 45 additions and 0 deletions

View File

@ -423,6 +423,39 @@ describe("detectActivity", () => {
agent.detectActivity("⏵⏵ bypass permissions on (shift+tab to cycle)\n"),
).toBe("idle");
});
it("returns idle for prompt with separator and status bar (no esc to interrupt)", () => {
// Full TUI frame: prompt + separator + status bar, idle state
expect(
agent.detectActivity(" \n────────────────────────────────────────────────────────────────────────────────\n ⏵⏵ bypass permissions on (shift+tab to cycle)"),
).toBe("idle");
});
it("returns idle for prompt with inline suggestion (no esc to interrupt)", () => {
// Claude Code shows suggestions like " check CI status" when idle
expect(
agent.detectActivity(" check CI status on the PR\n────────────────────────────────────────────────────────────────────────────────\n ⏵⏵ bypass permissions on (shift+tab to cycle)"),
).toBe("idle");
});
it("returns active for prompt with input being processed (esc to interrupt)", () => {
// When Claude Code is actively processing, status bar has "esc to interrupt"
expect(
agent.detectActivity(" fix the bug\n────────────────────────────────────────────────────────────────────────────────\n ⏵⏵ bypass permissions on (shift+tab to cycle) · esc to interrupt"),
).toBe("active");
});
it("returns idle for real finished session output with suggestion", () => {
const realOutput = [
" - 18 new tests (12 for the command, 6 for plugin resolution) — all passing",
"✻ Churned for 10m 56s",
"────────────────────────────────────────────────────────────────────────────────",
" check CI status on the PR",
"────────────────────────────────────────────────────────────────────────────────",
" ⏵⏵ bypass permissions on (shift+tab to cycle)",
].join("\n");
expect(agent.detectActivity(realOutput)).toBe("idle");
});
});
// =========================================================================

View File

@ -472,6 +472,13 @@ function classifyTerminalOutput(terminalOutput: string): ActivityState {
const lines = terminalOutput.trim().split("\n");
// Claude Code's status bar is the most reliable activity signal:
// - Active: "⏵⏵ bypass permissions on ... · esc to interrupt"
// - Idle: "⏵⏵ bypass permissions on ..." (no esc to interrupt)
// Check BEFORE stripping decorative lines.
const rawTail = lines.slice(-3).join("\n");
const hasEscToInterrupt = /esc to interrupt/i.test(rawTail);
// Strip trailing decorative lines from Claude Code's TUI. From bottom up,
// remove: status bar lines (contain ⏵), separator lines (all ─), and
// empty lines. This exposes the actual prompt or content line for detection.
@ -493,6 +500,11 @@ function classifyTerminalOutput(terminalOutput: string): ActivityState {
// The is Claude Code's prompt character.
if (/^[>$#]\s*$/.test(lastLine)) return "idle";
// Claude Code shows inline suggestions after the prompt when idle, e.g.
// " check CI status on the PR". Distinguish from active processing by
// checking the status bar: no "esc to interrupt" = idle with suggestion.
if (/^[>]\s+/.test(lastLine) && !hasEscToInterrupt) return "idle";
// Check the bottom of the buffer for permission prompts BEFORE checking
// full-buffer active indicators. Historical "Thinking"/"Reading" text in
// the buffer must not override a current permission prompt at the bottom.