From 8dd7b4846d45ac2e729c2d5f0b862b864b56439c Mon Sep 17 00:00:00 2001 From: Corwin Marsh Date: Thu, 16 Jul 2026 19:16:57 -0700 Subject: [PATCH 1/2] fix: detect Claude Code panes using version-string command Recent Claude Code releases report the version string (e.g. "2.1.206") as pane_current_command instead of "claude", and set the pane title to the current task summary prefixed with a status glyph rather than "Claude Code". Neither the command nor the title hint matched, so those panes were filtered out as non-agents even though the hooks were still writing state. Add a `command:claude-version` detection signal that fires when the command is a semver-like string and the title has a leading status glyph, corroborating the two matching so arbitrary semver-named processes are not misclassified. --- src/core/tmux.ts | 14 ++++++++++++++ test/tmux.test.ts | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/core/tmux.ts b/src/core/tmux.ts index 85196ad..f4e4b61 100644 --- a/src/core/tmux.ts +++ b/src/core/tmux.ts @@ -105,6 +105,12 @@ function processArgsContainCodex(stdoutText: string): boolean { .some((line) => line.split(/\s+/).some((token) => isCodexProcessToken(token))); } +// Recent Claude Code releases rename their process so tmux reports the version +// string (e.g. "2.1.206") as pane_current_command instead of "claude". +function isClaudeVersionCommand(command: string): boolean { + return /^\d+\.\d+\.\d+/.test(command); +} + function pickDetectedAgent( candidates: Array<{ agent: AgentKind; @@ -165,6 +171,12 @@ export function detectAgentPane(pane: TmuxPane): PaneDetection { lowerTitle === "pi" || lowerTitle.startsWith("pi - ") || title.startsWith("π - "); const hasClaudeTitleHint = normalizedLowerTitle === "claude" || normalizedLowerTitle.startsWith("claude code"); + // Recent Claude Code releases set the pane title to the current task summary + // prefixed with a status glyph (e.g. "✳ Set up deployment", braille spinner + // frames). Detect the presence of that leading glyph so we can corroborate a + // version-string command without matching arbitrary semver-named processes. + const hasLeadingGlyphTitle = + title.length > 0 && normalizedLowerTitle.length > 0 && normalizedLowerTitle !== lowerTitle; const hasKiroTitleHint = normalizedLowerTitle === "kiro" || normalizedLowerTitle.startsWith("kiro cli"); @@ -184,6 +196,8 @@ export function detectAgentPane(pane: TmuxPane): PaneDetection { if (matchesCommand(command, "claude")) { claudeReasons.push("command:claude"); + } else if (isClaudeVersionCommand(command) && hasLeadingGlyphTitle) { + claudeReasons.push("command:claude-version"); } if (hasKiroTitleHint) { diff --git a/test/tmux.test.ts b/test/tmux.test.ts index eff30f5..a9876ba 100644 --- a/test/tmux.test.ts +++ b/test/tmux.test.ts @@ -145,7 +145,43 @@ test("detectAgentPane recognizes OpenCode, Codex, Pi, Claude, Kiro, and no-signa { agent: "claude", confidence: "high", - reasons: ["title:Claude"], + reasons: ["title:Claude", "command:claude-version"], + }, + ); + + // Recent Claude Code releases report the version string as the pane command + // and set the title to the current task summary (no "Claude" text), prefixed + // with a status glyph. Detect via the version command + leading glyph. + assert.deepEqual( + detectAgentPane( + createPane({ paneTitle: "✳ Set up AWS deployment", currentCommand: "2.1.206" }), + ), + { + agent: "claude", + confidence: "medium", + reasons: ["command:claude-version"], + }, + ); + + assert.deepEqual( + detectAgentPane( + createPane({ paneTitle: "⠂ Resolve deprecated npm warnings", currentCommand: "2.1.211" }), + ), + { + agent: "claude", + confidence: "medium", + reasons: ["command:claude-version"], + }, + ); + + // A bare version-string command with a plain (non-glyph) title must not be + // misclassified as Claude. + assert.deepEqual( + detectAgentPane(createPane({ paneTitle: "node repl", currentCommand: "2.1.206" })), + { + agent: null, + confidence: "low", + reasons: [], }, ); From 849cede17e2712631f8898b848d9b39feb36a729 Mon Sep 17 00:00:00 2001 From: Corwin Marsh Date: Fri, 17 Jul 2026 08:54:56 -0700 Subject: [PATCH 2/2] fix: restrict Claude glyph detection to known status glyphs Address PR review: the leading-glyph check treated any non-alphanumeric title prefix as a Claude status glyph, so decorated titles like "[prod] api" or "# build" with a semver-named command could be misclassified as Claude. Match only Claude's actual status glyphs (sparkle/asterisk dingbats U+2720-U+274F, braille spinner frames U+2800-U+28FF, and the idle middle dot) via CLAUDE_STATUS_GLYPH_PATTERN, and add negative tests for decorated titles. --- src/core/tmux.ts | 19 ++++++++++++++----- test/tmux.test.ts | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/core/tmux.ts b/src/core/tmux.ts index f4e4b61..ee6c91c 100644 --- a/src/core/tmux.ts +++ b/src/core/tmux.ts @@ -111,6 +111,16 @@ function isClaudeVersionCommand(command: string): boolean { return /^\d+\.\d+\.\d+/.test(command); } +// Claude Code prefixes its pane title with a rotating status glyph: a +// sparkle/asterisk dingbat (e.g. "✳", "✶", "✻", "✽"), a braille spinner +// frame (U+2800–U+28FF), or a middle dot when idle. Match only these glyphs so +// arbitrary decorative title prefixes (e.g. "[prod]", "# build") don't count. +const CLAUDE_STATUS_GLYPH_PATTERN = /^[\u2720-\u274F\u2800-\u28FF\u00B7]/; + +function hasClaudeStatusGlyphPrefix(title: string): boolean { + return CLAUDE_STATUS_GLYPH_PATTERN.test(title.trimStart()); +} + function pickDetectedAgent( candidates: Array<{ agent: AgentKind; @@ -172,11 +182,10 @@ export function detectAgentPane(pane: TmuxPane): PaneDetection { const hasClaudeTitleHint = normalizedLowerTitle === "claude" || normalizedLowerTitle.startsWith("claude code"); // Recent Claude Code releases set the pane title to the current task summary - // prefixed with a status glyph (e.g. "✳ Set up deployment", braille spinner - // frames). Detect the presence of that leading glyph so we can corroborate a + // prefixed with a Claude status glyph (e.g. "✳ Set up deployment", braille + // spinner frames). Detect that specific glyph so we can corroborate a // version-string command without matching arbitrary semver-named processes. - const hasLeadingGlyphTitle = - title.length > 0 && normalizedLowerTitle.length > 0 && normalizedLowerTitle !== lowerTitle; + const hasClaudeGlyphTitle = hasClaudeStatusGlyphPrefix(title); const hasKiroTitleHint = normalizedLowerTitle === "kiro" || normalizedLowerTitle.startsWith("kiro cli"); @@ -196,7 +205,7 @@ export function detectAgentPane(pane: TmuxPane): PaneDetection { if (matchesCommand(command, "claude")) { claudeReasons.push("command:claude"); - } else if (isClaudeVersionCommand(command) && hasLeadingGlyphTitle) { + } else if (isClaudeVersionCommand(command) && hasClaudeGlyphTitle) { claudeReasons.push("command:claude-version"); } diff --git a/test/tmux.test.ts b/test/tmux.test.ts index a9876ba..ea31fc1 100644 --- a/test/tmux.test.ts +++ b/test/tmux.test.ts @@ -185,6 +185,26 @@ test("detectAgentPane recognizes OpenCode, Codex, Pi, Claude, Kiro, and no-signa }, ); + // A decorative (non-Claude) title prefix such as "[prod]" or "# build" must + // not be treated as a Claude status glyph even with a version-string command. + assert.deepEqual( + detectAgentPane(createPane({ paneTitle: "[prod] api", currentCommand: "2.1.206" })), + { + agent: null, + confidence: "low", + reasons: [], + }, + ); + + assert.deepEqual( + detectAgentPane(createPane({ paneTitle: "# build", currentCommand: "2.1.206" })), + { + agent: null, + confidence: "low", + reasons: [], + }, + ); + assert.deepEqual( detectAgentPane(createPane({ paneTitle: "Kiro CLI", currentCommand: "kiro-cli-chat" })), {