@atercates/claude-deck 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/sessions/status/acknowledge/route.ts +8 -0
- package/app/api/sessions/status/route.ts +2 -233
- package/app/page.tsx +34 -0
- package/components/Pane/DesktopTabBar.tsx +62 -28
- package/components/Pane/index.tsx +5 -0
- package/components/QuickSwitcher.tsx +63 -11
- package/components/SessionList/ActiveSessionsSection.tsx +116 -0
- package/components/SessionList/index.tsx +9 -1
- package/components/SessionStatusBar.tsx +155 -0
- package/components/WaitingBanner.tsx +122 -0
- package/components/views/DesktopView.tsx +32 -8
- package/components/views/types.ts +2 -0
- package/data/statuses/queries.ts +68 -34
- package/lib/claude/watcher.ts +28 -5
- package/lib/hooks/reporter.ts +116 -0
- package/lib/hooks/setup.ts +164 -0
- package/lib/orchestration.ts +6 -8
- package/lib/providers/registry.ts +1 -1
- package/lib/status-monitor.ts +278 -0
- package/package.json +1 -1
- package/server.ts +4 -0
- package/lib/status-detector.ts +0 -375
package/lib/status-detector.ts
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Status Detection System
|
|
3
|
-
*
|
|
4
|
-
* States:
|
|
5
|
-
* - "running" (GREEN): Sustained activity within cooldown period
|
|
6
|
-
* - "waiting" (YELLOW): Cooldown expired, NOT acknowledged (needs attention)
|
|
7
|
-
* - "idle" (GRAY): Cooldown expired, acknowledged (user saw it)
|
|
8
|
-
* - "dead": Session doesn't exist
|
|
9
|
-
*
|
|
10
|
-
* Detection Strategy:
|
|
11
|
-
* 1. Busy indicators + recent activity (highest priority - actively working)
|
|
12
|
-
* 2. Waiting patterns - user input needed
|
|
13
|
-
* 3. Spike detection - activity timestamp changes (2+ in 1s = sustained)
|
|
14
|
-
* 4. Cooldown - 2s grace period after activity stops
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { exec } from "child_process";
|
|
18
|
-
import { promisify } from "util";
|
|
19
|
-
|
|
20
|
-
const execAsync = promisify(exec);
|
|
21
|
-
|
|
22
|
-
// Configuration constants
|
|
23
|
-
const CONFIG = {
|
|
24
|
-
ACTIVITY_COOLDOWN_MS: 2000, // Grace period after activity
|
|
25
|
-
SPIKE_WINDOW_MS: 1000, // Window to detect sustained activity
|
|
26
|
-
SUSTAINED_THRESHOLD: 2, // Changes needed to confirm activity
|
|
27
|
-
CACHE_VALIDITY_MS: 2000, // How long tmux cache is valid
|
|
28
|
-
RECENT_ACTIVITY_MS: 120000, // Window for "recent" activity (2 min, tmux updates slowly)
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
|
-
// Detection patterns
|
|
32
|
-
const BUSY_INDICATORS = [
|
|
33
|
-
"esc to interrupt",
|
|
34
|
-
"(esc to interrupt)",
|
|
35
|
-
"· esc to interrupt",
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
39
|
-
|
|
40
|
-
const WHIMSICAL_WORDS = [
|
|
41
|
-
"accomplishing",
|
|
42
|
-
"actioning",
|
|
43
|
-
"actualizing",
|
|
44
|
-
"baking",
|
|
45
|
-
"booping",
|
|
46
|
-
"brewing",
|
|
47
|
-
"calculating",
|
|
48
|
-
"cerebrating",
|
|
49
|
-
"channelling",
|
|
50
|
-
"churning",
|
|
51
|
-
"clauding",
|
|
52
|
-
"coalescing",
|
|
53
|
-
"cogitating",
|
|
54
|
-
"combobulating",
|
|
55
|
-
"computing",
|
|
56
|
-
"concocting",
|
|
57
|
-
"conjuring",
|
|
58
|
-
"considering",
|
|
59
|
-
"contemplating",
|
|
60
|
-
"cooking",
|
|
61
|
-
"crafting",
|
|
62
|
-
"creating",
|
|
63
|
-
"crunching",
|
|
64
|
-
"deciphering",
|
|
65
|
-
"deliberating",
|
|
66
|
-
"determining",
|
|
67
|
-
"discombobulating",
|
|
68
|
-
"divining",
|
|
69
|
-
"doing",
|
|
70
|
-
"effecting",
|
|
71
|
-
"elucidating",
|
|
72
|
-
"enchanting",
|
|
73
|
-
"envisioning",
|
|
74
|
-
"finagling",
|
|
75
|
-
"flibbertigibbeting",
|
|
76
|
-
"forging",
|
|
77
|
-
"forming",
|
|
78
|
-
"frolicking",
|
|
79
|
-
"generating",
|
|
80
|
-
"germinating",
|
|
81
|
-
"hatching",
|
|
82
|
-
"herding",
|
|
83
|
-
"honking",
|
|
84
|
-
"hustling",
|
|
85
|
-
"ideating",
|
|
86
|
-
"imagining",
|
|
87
|
-
"incubating",
|
|
88
|
-
"inferring",
|
|
89
|
-
"jiving",
|
|
90
|
-
"manifesting",
|
|
91
|
-
"marinating",
|
|
92
|
-
"meandering",
|
|
93
|
-
"moseying",
|
|
94
|
-
"mulling",
|
|
95
|
-
"mustering",
|
|
96
|
-
"musing",
|
|
97
|
-
"noodling",
|
|
98
|
-
"percolating",
|
|
99
|
-
"perusing",
|
|
100
|
-
"philosophising",
|
|
101
|
-
"pondering",
|
|
102
|
-
"pontificating",
|
|
103
|
-
"processing",
|
|
104
|
-
"puttering",
|
|
105
|
-
"puzzling",
|
|
106
|
-
"reticulating",
|
|
107
|
-
"ruminating",
|
|
108
|
-
"scheming",
|
|
109
|
-
"schlepping",
|
|
110
|
-
"shimmying",
|
|
111
|
-
"shucking",
|
|
112
|
-
"simmering",
|
|
113
|
-
"smooshing",
|
|
114
|
-
"spelunking",
|
|
115
|
-
"spinning",
|
|
116
|
-
"stewing",
|
|
117
|
-
"sussing",
|
|
118
|
-
"synthesizing",
|
|
119
|
-
"thinking",
|
|
120
|
-
"tinkering",
|
|
121
|
-
"transmuting",
|
|
122
|
-
"unfurling",
|
|
123
|
-
"unravelling",
|
|
124
|
-
"vibing",
|
|
125
|
-
"wandering",
|
|
126
|
-
"whirring",
|
|
127
|
-
"wibbling",
|
|
128
|
-
"wizarding",
|
|
129
|
-
"working",
|
|
130
|
-
"wrangling",
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
const WAITING_PATTERNS = [
|
|
134
|
-
/\[Y\/n\]/i,
|
|
135
|
-
/\[y\/N\]/i,
|
|
136
|
-
/Allow\?/i,
|
|
137
|
-
/Approve\?/i,
|
|
138
|
-
/Continue\?/i,
|
|
139
|
-
/Press Enter to/i,
|
|
140
|
-
/waiting for input/i,
|
|
141
|
-
/\(yes\/no\)/i,
|
|
142
|
-
/Do you want to/i,
|
|
143
|
-
/Enter to confirm.*Esc to cancel/i,
|
|
144
|
-
/>\s*1\.\s*Yes/,
|
|
145
|
-
/Yes, allow all/i,
|
|
146
|
-
/allow all edits/i,
|
|
147
|
-
/allow all commands/i,
|
|
148
|
-
];
|
|
149
|
-
|
|
150
|
-
export type SessionStatus = "running" | "waiting" | "idle" | "dead";
|
|
151
|
-
|
|
152
|
-
interface StateTracker {
|
|
153
|
-
lastChangeTime: number;
|
|
154
|
-
acknowledged: boolean;
|
|
155
|
-
lastActivityTimestamp: number;
|
|
156
|
-
spikeWindowStart: number | null;
|
|
157
|
-
spikeChangeCount: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
interface SessionCache {
|
|
161
|
-
data: Map<string, number>;
|
|
162
|
-
updatedAt: number;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Content analysis helpers
|
|
166
|
-
function checkBusyIndicators(content: string): boolean {
|
|
167
|
-
const lines = content.split("\n");
|
|
168
|
-
// Focus on last 10 lines to avoid old scrollback false positives
|
|
169
|
-
const recentContent = lines.slice(-10).join("\n").toLowerCase();
|
|
170
|
-
|
|
171
|
-
// Check text indicators in recent lines
|
|
172
|
-
if (BUSY_INDICATORS.some((ind) => recentContent.includes(ind))) return true;
|
|
173
|
-
|
|
174
|
-
// Check whimsical words + "tokens" pattern in recent lines
|
|
175
|
-
if (
|
|
176
|
-
recentContent.includes("tokens") &&
|
|
177
|
-
WHIMSICAL_WORDS.some((w) => recentContent.includes(w))
|
|
178
|
-
)
|
|
179
|
-
return true;
|
|
180
|
-
|
|
181
|
-
// Check spinners in last 5 lines
|
|
182
|
-
const last5 = lines.slice(-5).join("");
|
|
183
|
-
if (SPINNER_CHARS.some((s) => last5.includes(s))) return true;
|
|
184
|
-
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function checkWaitingPatterns(content: string): boolean {
|
|
189
|
-
const recentLines = content.split("\n").slice(-5).join("\n");
|
|
190
|
-
return WAITING_PATTERNS.some((p) => p.test(recentLines));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
class SessionStatusDetector {
|
|
194
|
-
private trackers = new Map<string, StateTracker>();
|
|
195
|
-
private cache: SessionCache = { data: new Map(), updatedAt: 0 };
|
|
196
|
-
|
|
197
|
-
// Cache management
|
|
198
|
-
async refreshCache(): Promise<void> {
|
|
199
|
-
if (Date.now() - this.cache.updatedAt < CONFIG.CACHE_VALIDITY_MS) return;
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
const { stdout } = await execAsync(
|
|
203
|
-
`tmux list-sessions -F '#{session_name}\t#{session_activity}' 2>/dev/null || echo ""`
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const newData = new Map<string, number>();
|
|
207
|
-
for (const line of stdout.trim().split("\n")) {
|
|
208
|
-
if (!line) continue;
|
|
209
|
-
const [name, activity] = line.split("\t");
|
|
210
|
-
if (name && activity) newData.set(name, parseInt(activity, 10) || 0);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
this.cache = { data: newData, updatedAt: Date.now() };
|
|
214
|
-
} catch {
|
|
215
|
-
// Keep existing cache on error
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
sessionExists(name: string): boolean {
|
|
220
|
-
return this.cache.data.has(name);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
getTimestamp(name: string): number {
|
|
224
|
-
return this.cache.data.get(name) || 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async capturePane(name: string): Promise<string> {
|
|
228
|
-
try {
|
|
229
|
-
const { stdout } = await execAsync(
|
|
230
|
-
`tmux capture-pane -t "${name}" -p 2>/dev/null || echo ""`
|
|
231
|
-
);
|
|
232
|
-
return stdout.trim();
|
|
233
|
-
} catch {
|
|
234
|
-
return "";
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private getTracker(name: string, timestamp: number): StateTracker {
|
|
239
|
-
let tracker = this.trackers.get(name);
|
|
240
|
-
if (!tracker) {
|
|
241
|
-
tracker = {
|
|
242
|
-
lastChangeTime: Date.now() - CONFIG.ACTIVITY_COOLDOWN_MS,
|
|
243
|
-
acknowledged: true,
|
|
244
|
-
lastActivityTimestamp: timestamp,
|
|
245
|
-
spikeWindowStart: null,
|
|
246
|
-
spikeChangeCount: 0,
|
|
247
|
-
};
|
|
248
|
-
this.trackers.set(name, tracker);
|
|
249
|
-
}
|
|
250
|
-
return tracker;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Spike detection: filters single activity spikes from sustained activity
|
|
254
|
-
private processSpikeDetection(
|
|
255
|
-
tracker: StateTracker,
|
|
256
|
-
currentTimestamp: number
|
|
257
|
-
): "running" | null {
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
const timestampChanged = tracker.lastActivityTimestamp !== currentTimestamp;
|
|
260
|
-
|
|
261
|
-
if (timestampChanged) {
|
|
262
|
-
tracker.lastActivityTimestamp = currentTimestamp;
|
|
263
|
-
|
|
264
|
-
const windowExpired =
|
|
265
|
-
tracker.spikeWindowStart === null ||
|
|
266
|
-
now - tracker.spikeWindowStart > CONFIG.SPIKE_WINDOW_MS;
|
|
267
|
-
|
|
268
|
-
if (windowExpired) {
|
|
269
|
-
// Start new detection window
|
|
270
|
-
tracker.spikeWindowStart = now;
|
|
271
|
-
tracker.spikeChangeCount = 1;
|
|
272
|
-
} else {
|
|
273
|
-
// Within window - count change
|
|
274
|
-
tracker.spikeChangeCount++;
|
|
275
|
-
if (tracker.spikeChangeCount >= CONFIG.SUSTAINED_THRESHOLD) {
|
|
276
|
-
// Sustained activity confirmed
|
|
277
|
-
tracker.lastChangeTime = now;
|
|
278
|
-
tracker.acknowledged = false;
|
|
279
|
-
tracker.spikeWindowStart = null;
|
|
280
|
-
tracker.spikeChangeCount = 0;
|
|
281
|
-
return "running";
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} else if (
|
|
285
|
-
tracker.spikeChangeCount === 1 &&
|
|
286
|
-
tracker.spikeWindowStart !== null
|
|
287
|
-
) {
|
|
288
|
-
// Check if single spike should be filtered
|
|
289
|
-
if (now - tracker.spikeWindowStart > CONFIG.SPIKE_WINDOW_MS) {
|
|
290
|
-
tracker.spikeWindowStart = null;
|
|
291
|
-
tracker.spikeChangeCount = 0;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private isInSpikeWindow(tracker: StateTracker): boolean {
|
|
299
|
-
return (
|
|
300
|
-
tracker.spikeWindowStart !== null &&
|
|
301
|
-
Date.now() - tracker.spikeWindowStart < CONFIG.SPIKE_WINDOW_MS
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private isInCooldown(tracker: StateTracker): boolean {
|
|
306
|
-
return Date.now() - tracker.lastChangeTime < CONFIG.ACTIVITY_COOLDOWN_MS;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private getIdleOrWaiting(tracker: StateTracker): SessionStatus {
|
|
310
|
-
return tracker.acknowledged ? "idle" : "waiting";
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async getStatus(sessionName: string): Promise<SessionStatus> {
|
|
314
|
-
await this.refreshCache();
|
|
315
|
-
|
|
316
|
-
// Dead check
|
|
317
|
-
if (!this.sessionExists(sessionName)) {
|
|
318
|
-
this.trackers.delete(sessionName);
|
|
319
|
-
return "dead";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const timestamp = this.getTimestamp(sessionName);
|
|
323
|
-
const tracker = this.getTracker(sessionName, timestamp);
|
|
324
|
-
const content = await this.capturePane(sessionName);
|
|
325
|
-
|
|
326
|
-
// 1. Busy indicators in last 10 lines (highest priority - Claude is actively working)
|
|
327
|
-
// No activity timestamp check needed since we only look at recent terminal lines
|
|
328
|
-
if (checkBusyIndicators(content)) {
|
|
329
|
-
tracker.lastChangeTime = Date.now();
|
|
330
|
-
tracker.acknowledged = false;
|
|
331
|
-
return "running";
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// 2. Waiting patterns (only if not actively running)
|
|
335
|
-
if (checkWaitingPatterns(content)) return "waiting";
|
|
336
|
-
|
|
337
|
-
// 3. Spike detection
|
|
338
|
-
const spikeResult = this.processSpikeDetection(tracker, timestamp);
|
|
339
|
-
if (spikeResult) return spikeResult;
|
|
340
|
-
|
|
341
|
-
// 4. During spike window, maintain stable status
|
|
342
|
-
if (this.isInSpikeWindow(tracker)) {
|
|
343
|
-
return this.isInCooldown(tracker)
|
|
344
|
-
? "running"
|
|
345
|
-
: this.getIdleOrWaiting(tracker);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// 5. Cooldown check
|
|
349
|
-
if (this.isInCooldown(tracker)) return "running";
|
|
350
|
-
|
|
351
|
-
// 6. Cooldown expired
|
|
352
|
-
return this.getIdleOrWaiting(tracker);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
acknowledge(sessionName: string): void {
|
|
356
|
-
const tracker = this.trackers.get(sessionName);
|
|
357
|
-
if (tracker) tracker.acknowledged = true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async getAllStatuses(names: string[]): Promise<Map<string, SessionStatus>> {
|
|
361
|
-
await this.refreshCache();
|
|
362
|
-
const results = await Promise.all(
|
|
363
|
-
names.map(async (name) => ({ name, status: await this.getStatus(name) }))
|
|
364
|
-
);
|
|
365
|
-
return new Map(results.map((r) => [r.name, r.status]));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
cleanup(): void {
|
|
369
|
-
for (const [name] of this.trackers) {
|
|
370
|
-
if (!this.sessionExists(name)) this.trackers.delete(name);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export const statusDetector = new SessionStatusDetector();
|