Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/core/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");

Expand All @@ -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) {
Expand Down
58 changes: 57 additions & 1 deletion test/tmux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
);

Expand Down
Loading