Repository navigation
Expand file tree
/
Copy pathclassifier.ts
More file actions
84 lines (76 loc) · 3.98 KB
/
Copy pathclassifier.ts
File metadata and controls
84 lines (76 loc) · 3.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { createHash } from "node:crypto";
export type ClaudePaneStatus = "active" | "idle" | "permission" | "missing";
export interface PaneClassification {
status: ClaudePaneStatus;
dangerous: boolean;
question: boolean;
unresolvedFailure: boolean;
claudeLike: boolean;
fingerprint: string;
excerpt: string;
}
const ANSI = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
const ACTIVE = /(?:esc to interrupt|(?:Creating|Thinking|Running|Working|Committing|Cogitating|Researching|Implementing|Testing|Building|Analyzing|Reading|Writing|Updating|Installing|Fixing|Reviewing|Planning|Searching|Compacting|Baking|Brewing|Crunching|Finagling|Hatching|Pondering|Ruminating|Whirring|Wandering|Channelling|Churning|Clauding)[…\.]{1,3})/i;
const PERMISSION = /(?:do you want to proceed|allow this command|allow (?:once|for this session|this tool)|yes,? and don['’]t ask|❯\s*1\.\s*yes|esc to cancel)/i;
const DANGEROUS = /(?:\brm\s+-[^\n]*r|\bsudo\b|\bgit\s+(?:push|reset\s+--hard|clean\s+-f)|\b(?:ssh|scp|rsync)\b|\b(?:deploy|production|staging)\b|\b(?:drop|truncate|delete)\s+(?:database|table|from)\b|\b(?:migrate|migration|artisan)\b|\.env\b|secret|credential|token|password)/i;
const QUESTION = /(?:\?\s*$|please (?:choose|confirm)|which (?:option|approach)|would you like|should i|need your (?:input|decision))/im;
const UNRESOLVED_FAILURE = /(?:\b(?:tests?|build|typecheck|lint|migration|deployment)\s+(?:has\s+)?failed\b|\bfailed\s+(?:tests?|build|typecheck|lint|migration|deployment)\b|\bcommand\s+(?:failed|exited)\s+(?:with\s+)?(?:code|status)\s*[1-9]\d*\b|\b(?:task|work|implementation)\s+(?:is\s+)?(?:blocked|incomplete|not\s+complete)\b|\b(?:unable|could not|cannot)\s+to\s+complete\b|\bunresolved\s+(?:failure|error|issue)\b)/im;
const CLAUDE_LIKE = /(?:claude code|accept edits on|esc to interrupt|thinking with (?:low|medium|high|xhigh|max) effort)/im;
export function cleanPane(raw: string): string {
return raw
.replace(ANSI, "")
.replace(/\r/g, "")
.split("\n")
.map((line) => line.replace(/[ \t]+$/g, ""))
.join("\n")
.trim();
}
function meaningfulExcerpt(cleaned: string): string {
const lines = cleaned.split("\n").filter((line) => {
const text = line.trim();
if (!text) return false;
if (/^[─━═]+$/.test(text)) return false;
if (/^(?:⏵⏵\s*)?accept edits on/i.test(text)) return false;
if (/^(?:◈|✳|✻|·|\*)?\s*\d+\s+tokens?$/i.test(text)) return false;
return true;
});
return lines.slice(-80).join("\n").slice(-12_000);
}
export function classifyPane(raw: string, exists = true): PaneClassification {
if (!exists) {
return {
status: "missing",
dangerous: false,
question: false,
unresolvedFailure: false,
claudeLike: false,
fingerprint: "missing",
excerpt: "tmux session does not exist",
};
}
const cleaned = cleanPane(raw);
const excerpt = meaningfulExcerpt(cleaned);
// capture-pane includes scrollback. Only the live tail should decide whether
// Claude is currently active or waiting at a permission prompt.
const liveLines = cleaned.split("\n");
const stateWindow = liveLines.slice(-16).join("\n");
const outcomeWindow = liveLines.slice(-24).join("\n");
const permission = PERMISSION.test(stateWindow);
const active = !permission && ACTIVE.test(stateWindow);
const status: ClaudePaneStatus = permission ? "permission" : active ? "active" : "idle";
const unresolvedFailure = status === "idle" && UNRESOLVED_FAILURE.test(outcomeWindow);
const stableText = excerpt
.replace(/\(\d+[smh](?:\s+\d+[smh])?[^)]*\)/g, "(elapsed)")
.replace(/\b\d{1,3}(?:,\d{3})*\s+tokens?\b/gi, "tokens")
.replace(/[✳✻✽·*]\s+/g, "");
const fingerprint = createHash("sha256").update(`${status}\n${stableText}`).digest("hex").slice(0, 20);
return {
status,
dangerous: permission && DANGEROUS.test(outcomeWindow),
question: permission || QUESTION.test(excerpt),
unresolvedFailure,
claudeLike: CLAUDE_LIKE.test(cleaned),
fingerprint,
excerpt,
};
}