From fad29388040901dd94ea680dcfb9db4d2bab2105 Mon Sep 17 00:00:00 2001 From: sjd9021 Date: Mon, 16 Feb 2026 01:16:34 +0530 Subject: [PATCH] fix: detect idle state with inline suggestions and esc-to-interrupt signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../agent-claude-code/src/index.test.ts | 33 +++++++++++++++++++ .../plugins/agent-claude-code/src/index.ts | 12 +++++++ 2 files changed, 45 insertions(+) diff --git a/packages/plugins/agent-claude-code/src/index.test.ts b/packages/plugins/agent-claude-code/src/index.test.ts index d38793e0a..16391e599 100644 --- a/packages/plugins/agent-claude-code/src/index.test.ts +++ b/packages/plugins/agent-claude-code/src/index.test.ts @@ -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"); + }); }); // ========================================================================= diff --git a/packages/plugins/agent-claude-code/src/index.ts b/packages/plugins/agent-claude-code/src/index.ts index f54727c57..b09bd6895 100644 --- a/packages/plugins/agent-claude-code/src/index.ts +++ b/packages/plugins/agent-claude-code/src/index.ts @@ -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.