diff --git a/src/core/tmux.ts b/src/core/tmux.ts index 85196ad..ee6c91c 100644 --- a/src/core/tmux.ts +++ b/src/core/tmux.ts @@ -105,6 +105,22 @@ 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); +} + +// 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; @@ -165,6 +181,11 @@ 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 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 hasClaudeGlyphTitle = hasClaudeStatusGlyphPrefix(title); const hasKiroTitleHint = normalizedLowerTitle === "kiro" || normalizedLowerTitle.startsWith("kiro cli"); @@ -184,6 +205,8 @@ export function detectAgentPane(pane: TmuxPane): PaneDetection { if (matchesCommand(command, "claude")) { claudeReasons.push("command:claude"); + } else if (isClaudeVersionCommand(command) && hasClaudeGlyphTitle) { + claudeReasons.push("command:claude-version"); } if (hasKiroTitleHint) { diff --git a/test/tmux.test.ts b/test/tmux.test.ts index eff30f5..ea31fc1 100644 --- a/test/tmux.test.ts +++ b/test/tmux.test.ts @@ -145,7 +145,63 @@ 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: [], + }, + ); + + // 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: [], }, );