agenthud 0.7.4 → 0.8.1

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.
@@ -0,0 +1,1703 @@
1
+ // src/main.ts
2
+ import { createInterface } from "readline";
3
+ import { existsSync as existsSync5, rmSync } from "fs";
4
+ import { join as join5 } from "path";
5
+ import { render } from "ink";
6
+ import React from "react";
7
+
8
+ // src/cli.ts
9
+ import { readFileSync } from "fs";
10
+ import { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
12
+ function getHelp() {
13
+ return `Usage: agenthud [options]
14
+
15
+ Monitors all running Claude Code sessions in real-time.
16
+
17
+ Options:
18
+ -w, --watch Watch mode (default) \u2014 live updates
19
+ --once Print once and exit
20
+ -V, --version Show version number
21
+ -h, --help Show this help message
22
+
23
+ Config: ~/.agenthud/config.yaml
24
+ Logs: ~/.agenthud/logs/
25
+ `;
26
+ }
27
+ function getVersion() {
28
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
29
+ const packageJson = JSON.parse(
30
+ readFileSync(join(__dirname2, "..", "package.json"), "utf-8")
31
+ );
32
+ return packageJson.version;
33
+ }
34
+ function clearScreen() {
35
+ console.clear();
36
+ }
37
+ function parseArgs(args) {
38
+ if (args.includes("--help") || args.includes("-h")) {
39
+ return { mode: "watch", command: "help" };
40
+ }
41
+ if (args.includes("--version") || args.includes("-V")) {
42
+ return { mode: "watch", command: "version" };
43
+ }
44
+ if (args.includes("--once")) {
45
+ return { mode: "once" };
46
+ }
47
+ return { mode: "watch" };
48
+ }
49
+
50
+ // src/ui/App.tsx
51
+ import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
52
+ import { join as join4 } from "path";
53
+ import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
54
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
55
+
56
+ // src/config/globalConfig.ts
57
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
58
+ import { homedir } from "os";
59
+ import { join as join2 } from "path";
60
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
61
+ var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
62
+ var DEFAULT_GLOBAL_CONFIG = {
63
+ refreshIntervalMs: 2e3,
64
+ logDir: join2(homedir(), ".agenthud", "logs"),
65
+ hiddenSessions: [],
66
+ hiddenSubAgents: []
67
+ };
68
+ function parseInterval(value) {
69
+ const match = value.match(/^(\d+)(s|m)$/);
70
+ if (!match) return null;
71
+ const n = parseInt(match[1], 10);
72
+ return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
73
+ }
74
+ function loadGlobalConfig() {
75
+ const config = { ...DEFAULT_GLOBAL_CONFIG };
76
+ if (!existsSync(CONFIG_PATH)) {
77
+ return config;
78
+ }
79
+ let raw;
80
+ try {
81
+ raw = readFileSync2(CONFIG_PATH, "utf-8");
82
+ } catch {
83
+ return config;
84
+ }
85
+ let parsed;
86
+ try {
87
+ parsed = parseYaml(raw) ?? {};
88
+ } catch {
89
+ return config;
90
+ }
91
+ if (typeof parsed.refreshInterval === "string") {
92
+ const ms = parseInterval(parsed.refreshInterval);
93
+ if (ms !== null) config.refreshIntervalMs = ms;
94
+ }
95
+ if (typeof parsed.logDir === "string") {
96
+ config.logDir = parsed.logDir.replace(/^~/, homedir());
97
+ }
98
+ if (Array.isArray(parsed.hiddenSessions)) {
99
+ config.hiddenSessions = parsed.hiddenSessions.filter(
100
+ (s) => typeof s === "string"
101
+ );
102
+ }
103
+ if (Array.isArray(parsed.hiddenSubAgents)) {
104
+ config.hiddenSubAgents = parsed.hiddenSubAgents.filter(
105
+ (s) => typeof s === "string"
106
+ );
107
+ }
108
+ return config;
109
+ }
110
+ function writeConfig(updates) {
111
+ const configDir = join2(homedir(), ".agenthud");
112
+ if (!existsSync(configDir)) {
113
+ mkdirSync(configDir, { recursive: true });
114
+ }
115
+ let raw = {};
116
+ if (existsSync(CONFIG_PATH)) {
117
+ try {
118
+ raw = parseYaml(readFileSync2(CONFIG_PATH, "utf-8")) ?? {};
119
+ } catch {
120
+ raw = {};
121
+ }
122
+ }
123
+ if (updates.hiddenSessions !== void 0)
124
+ raw.hiddenSessions = updates.hiddenSessions;
125
+ if (updates.hiddenSubAgents !== void 0)
126
+ raw.hiddenSubAgents = updates.hiddenSubAgents;
127
+ writeFileSync(CONFIG_PATH, stringifyYaml(raw), "utf-8");
128
+ }
129
+ function hideSession(id) {
130
+ const config = loadGlobalConfig();
131
+ if (config.hiddenSessions.includes(id)) return;
132
+ writeConfig({ hiddenSessions: [...config.hiddenSessions, id] });
133
+ }
134
+ function hideSubAgent(id) {
135
+ const config = loadGlobalConfig();
136
+ if (config.hiddenSubAgents.includes(id)) return;
137
+ writeConfig({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
138
+ }
139
+ function ensureLogDir(logDir) {
140
+ if (!existsSync(logDir)) {
141
+ mkdirSync(logDir, { recursive: true });
142
+ }
143
+ }
144
+ function hasProjectLevelConfig() {
145
+ return existsSync(join2(process.cwd(), ".agenthud", "config.yaml"));
146
+ }
147
+
148
+ // src/data/sessionHistory.ts
149
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
150
+
151
+ // src/data/activityParser.ts
152
+ import { basename } from "path";
153
+
154
+ // src/types/index.ts
155
+ var ICONS = {
156
+ User: ">",
157
+ Response: "<",
158
+ Thinking: "\u2026",
159
+ Edit: "~",
160
+ Write: "~",
161
+ Read: "\u25CB",
162
+ Bash: "$",
163
+ Glob: "*",
164
+ Grep: "*",
165
+ WebFetch: "@",
166
+ WebSearch: "@",
167
+ Task: "\xBB",
168
+ TodoWrite: "~",
169
+ AskUserQuestion: "?",
170
+ Default: "$"
171
+ };
172
+
173
+ // src/data/activityParser.ts
174
+ function stripAnsi(text) {
175
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
176
+ }
177
+ function parseModelName(modelId) {
178
+ const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
179
+ if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
180
+ const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
181
+ if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
182
+ const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
183
+ if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
184
+ return modelId.replace(/-\d{8}$/, "");
185
+ }
186
+ function getToolDetail(_toolName, input) {
187
+ if (!input) return "";
188
+ if (input.command) return stripAnsi(input.command.replace(/\n/g, " "));
189
+ if (input.file_path) return basename(input.file_path);
190
+ if (input.pattern) return stripAnsi(input.pattern);
191
+ if (input.query) return stripAnsi(input.query);
192
+ if (input.description) return stripAnsi(input.description);
193
+ return "";
194
+ }
195
+ function parseActivitiesFromLines(lines) {
196
+ const activities = [];
197
+ let tokenCount = 0;
198
+ let modelName = null;
199
+ let sessionStartTime = null;
200
+ for (const line of lines) {
201
+ let entry;
202
+ try {
203
+ entry = JSON.parse(line);
204
+ } catch {
205
+ continue;
206
+ }
207
+ const timestamp = entry.timestamp ? new Date(entry.timestamp) : /* @__PURE__ */ new Date();
208
+ if (!sessionStartTime && entry.timestamp) {
209
+ sessionStartTime = timestamp;
210
+ }
211
+ if (entry.type === "user") {
212
+ const userEntry = entry;
213
+ const msgContent = userEntry.message?.content;
214
+ let userText = "";
215
+ if (typeof msgContent === "string") {
216
+ userText = msgContent;
217
+ } else if (Array.isArray(msgContent)) {
218
+ const textBlock = msgContent.find((c) => c.type === "text" && c.text);
219
+ if (textBlock?.text) userText = textBlock.text;
220
+ }
221
+ if (userText) {
222
+ activities.push({
223
+ timestamp,
224
+ type: "user",
225
+ icon: ICONS.User,
226
+ label: "User",
227
+ detail: userText.replace(/\n/g, " ")
228
+ });
229
+ }
230
+ }
231
+ if (entry.type === "assistant") {
232
+ const assistantEntry = entry;
233
+ if (assistantEntry.message?.model && !modelName) {
234
+ modelName = parseModelName(assistantEntry.message.model);
235
+ }
236
+ const usage = assistantEntry.message?.usage;
237
+ if (usage) {
238
+ tokenCount += (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.output_tokens ?? 0);
239
+ }
240
+ const content = assistantEntry.message?.content;
241
+ if (Array.isArray(content)) {
242
+ for (const block of content) {
243
+ if (block.type === "thinking" && block.thinking) {
244
+ activities.push({
245
+ timestamp,
246
+ type: "thinking",
247
+ icon: ICONS.Thinking,
248
+ label: "Thinking",
249
+ detail: block.thinking.replace(/\n/g, " ")
250
+ });
251
+ } else if (block.type === "tool_use" && block.name) {
252
+ if (block.name === "TodoWrite") continue;
253
+ const icon = ICONS[block.name] ?? ICONS.Default;
254
+ const detail = getToolDetail(block.name, block.input);
255
+ const last = activities[activities.length - 1];
256
+ if (last && last.type === "tool" && last.label === block.name && last.detail === detail) {
257
+ last.count = (last.count ?? 1) + 1;
258
+ last.timestamp = timestamp;
259
+ } else {
260
+ activities.push({
261
+ timestamp,
262
+ type: "tool",
263
+ icon,
264
+ label: block.name,
265
+ detail
266
+ });
267
+ }
268
+ } else if (block.type === "text" && block.text && block.text.length > 10) {
269
+ activities.push({
270
+ timestamp,
271
+ type: "response",
272
+ icon: ICONS.Response,
273
+ label: "Response",
274
+ detail: block.text.replace(/\n/g, " ")
275
+ });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+ return { activities, tokenCount, modelName, sessionStartTime };
282
+ }
283
+
284
+ // src/data/sessionHistory.ts
285
+ function parseSessionHistory(filePath) {
286
+ if (!existsSync2(filePath)) return [];
287
+ let content;
288
+ try {
289
+ content = readFileSync3(filePath, "utf-8");
290
+ } catch {
291
+ return [];
292
+ }
293
+ const lines = content.trim().split("\n").filter(Boolean);
294
+ const { activities } = parseActivitiesFromLines(lines);
295
+ return activities;
296
+ }
297
+
298
+ // src/data/sessions.ts
299
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
300
+ import { homedir as homedir2 } from "os";
301
+ import { basename as basename2, join as join3 } from "path";
302
+
303
+ // src/ui/constants.ts
304
+ import stringWidth from "string-width";
305
+ var THIRTY_SECONDS_MS = 30 * 1e3;
306
+ var THIRTY_MINUTES_MS = 30 * 60 * 1e3;
307
+ var ONE_HOUR_MS = 60 * 60 * 1e3;
308
+ var FIVE_MINUTES_MS = 5 * 60 * 1e3;
309
+ var DEFAULT_PANEL_WIDTH = 70;
310
+ var CONTENT_WIDTH = DEFAULT_PANEL_WIDTH - 4;
311
+ var INNER_WIDTH = DEFAULT_PANEL_WIDTH - 2;
312
+ function getInnerWidth(panelWidth) {
313
+ return panelWidth - 2;
314
+ }
315
+ var BOX = {
316
+ tl: "\u250C",
317
+ tr: "\u2510",
318
+ bl: "\u2514",
319
+ br: "\u2518",
320
+ h: "\u2500",
321
+ v: "\u2502",
322
+ ml: "\u251C",
323
+ mr: "\u2524"
324
+ };
325
+ function createTitleLine(label, suffix = "", panelWidth = DEFAULT_PANEL_WIDTH) {
326
+ const leftPart = `${BOX.h} ${label} `;
327
+ const rightPart = suffix ? ` ${suffix} ${BOX.h}` : "";
328
+ const leftWidth = getDisplayWidth(leftPart);
329
+ const rightWidth = suffix ? getDisplayWidth(rightPart) : 0;
330
+ const dashCount = panelWidth - 1 - leftWidth - rightWidth - 1;
331
+ const dashes = BOX.h.repeat(Math.max(0, dashCount));
332
+ return BOX.tl + leftPart + dashes + rightPart + BOX.tr;
333
+ }
334
+ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
335
+ return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
336
+ }
337
+ var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
338
+ var getDisplayWidth = stringWidth;
339
+
340
+ // src/data/sessions.ts
341
+ function getProjectsDir() {
342
+ return join3(homedir2(), ".claude", "projects");
343
+ }
344
+ function decodeProjectPath(encoded) {
345
+ const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
346
+ if (windowsDriveMatch) {
347
+ const drive = windowsDriveMatch[1];
348
+ const rest = windowsDriveMatch[2].replace(/-/g, "\\");
349
+ return `${drive}:\\${rest}`;
350
+ }
351
+ return encoded.replace(/-/g, "/");
352
+ }
353
+ function getSessionStatus(mtimeMs) {
354
+ const now = Date.now();
355
+ const age = now - mtimeMs;
356
+ if (age < THIRTY_MINUTES_MS) return "hot";
357
+ if (age < ONE_HOUR_MS) return "warm";
358
+ const mtime = new Date(mtimeMs);
359
+ const nowDate = new Date(now);
360
+ if (mtime.getUTCFullYear() === nowDate.getUTCFullYear() && mtime.getUTCMonth() === nowDate.getUTCMonth() && mtime.getUTCDate() === nowDate.getUTCDate()) {
361
+ return "cool";
362
+ }
363
+ return "cold";
364
+ }
365
+ function extractTaskDescription(content) {
366
+ const headerMatch = content.match(/##\s*(Task\s+\d+[:\s].+)/m);
367
+ if (headerMatch) return headerMatch[1].trim().slice(0, 60);
368
+ const thisTaskMatch = content.match(/\*\*This Task[^:]+:\*\*\s*(.+)/);
369
+ if (thisTaskMatch) return thisTaskMatch[1].trim().slice(0, 60);
370
+ const firstLine = content.split("\n").find((l) => l.trim());
371
+ return (firstLine ?? "").trim().slice(0, 60);
372
+ }
373
+ function readSubAgentInfo(filePath) {
374
+ if (!existsSync3(filePath)) return { agentId: null, taskDescription: null };
375
+ try {
376
+ const firstLine = readFileSync4(filePath, "utf-8").split("\n")[0];
377
+ if (!firstLine) return { agentId: null, taskDescription: null };
378
+ const entry = JSON.parse(firstLine);
379
+ const agentId = typeof entry.agentId === "string" ? entry.agentId : null;
380
+ const content = typeof entry.message?.content === "string" ? entry.message.content : null;
381
+ const taskDescription = content ? extractTaskDescription(content) : null;
382
+ return { agentId, taskDescription };
383
+ } catch {
384
+ return { agentId: null, taskDescription: null };
385
+ }
386
+ }
387
+ function readModelName(filePath) {
388
+ if (!existsSync3(filePath)) return null;
389
+ try {
390
+ const content = readFileSync4(filePath, "utf-8");
391
+ const lines = content.trim().split("\n").filter(Boolean);
392
+ for (const line of lines.slice(-50).reverse()) {
393
+ try {
394
+ const entry = JSON.parse(line);
395
+ if (entry.type === "assistant" && entry.message?.model) {
396
+ return parseModelName(entry.message.model);
397
+ }
398
+ } catch {
399
+ }
400
+ }
401
+ } catch {
402
+ }
403
+ return null;
404
+ }
405
+ function buildSubAgents(parentId, projectDir, config, projectName) {
406
+ const subagentsDir = join3(projectDir, parentId, "subagents");
407
+ if (!existsSync3(subagentsDir)) return [];
408
+ let files;
409
+ try {
410
+ files = readdirSync(subagentsDir).filter(
411
+ (f) => f.endsWith(".jsonl")
412
+ );
413
+ } catch {
414
+ return [];
415
+ }
416
+ return files.map((file) => {
417
+ const id = file.replace(/\.jsonl$/, "");
418
+ const hideKey = `${projectName}/${id}`;
419
+ const filePath = join3(subagentsDir, file);
420
+ try {
421
+ const stat = statSync(filePath);
422
+ const { agentId, taskDescription } = readSubAgentInfo(filePath);
423
+ return {
424
+ id,
425
+ hideKey,
426
+ filePath,
427
+ projectPath: "",
428
+ projectName: "",
429
+ lastModifiedMs: stat.mtimeMs,
430
+ status: getSessionStatus(stat.mtimeMs),
431
+ modelName: readModelName(filePath),
432
+ subAgents: [],
433
+ agentId: agentId ?? void 0,
434
+ taskDescription: taskDescription ?? void 0
435
+ };
436
+ } catch {
437
+ return null;
438
+ }
439
+ }).filter(
440
+ (n) => n !== null && !config.hiddenSubAgents.includes(n.hideKey)
441
+ ).sort((a, b) => b.lastModifiedMs - a.lastModifiedMs);
442
+ }
443
+ function discoverSessions(config) {
444
+ const projectsDir = getProjectsDir();
445
+ if (!existsSync3(projectsDir)) {
446
+ return { sessions: [], totalCount: 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
447
+ }
448
+ let projectDirs;
449
+ try {
450
+ projectDirs = readdirSync(projectsDir).filter((entry) => {
451
+ try {
452
+ return statSync(join3(projectsDir, entry)).isDirectory();
453
+ } catch {
454
+ return false;
455
+ }
456
+ });
457
+ } catch {
458
+ return { sessions: [], totalCount: 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
459
+ }
460
+ const allSessions = [];
461
+ for (const encodedDir of projectDirs) {
462
+ const projectDir = join3(projectsDir, encodedDir);
463
+ const decodedPath = decodeProjectPath(encodedDir);
464
+ const projectName = basename2(decodedPath);
465
+ let files;
466
+ try {
467
+ files = readdirSync(projectDir).filter(
468
+ (f) => f.endsWith(".jsonl")
469
+ );
470
+ } catch {
471
+ continue;
472
+ }
473
+ for (const file of files) {
474
+ const id = file.replace(/\.jsonl$/, "");
475
+ const hideKey = `${projectName}/${id}`;
476
+ const filePath = join3(projectDir, file);
477
+ try {
478
+ const stat = statSync(filePath);
479
+ const subAgents = buildSubAgents(id, projectDir, config, projectName);
480
+ allSessions.push({
481
+ id,
482
+ hideKey,
483
+ filePath,
484
+ projectPath: decodedPath,
485
+ projectName,
486
+ lastModifiedMs: stat.mtimeMs,
487
+ status: getSessionStatus(stat.mtimeMs),
488
+ modelName: readModelName(filePath),
489
+ subAgents
490
+ });
491
+ } catch {
492
+ }
493
+ }
494
+ }
495
+ allSessions.sort((a, b) => {
496
+ const statusOrder = {
497
+ hot: 0,
498
+ warm: 1,
499
+ cool: 2,
500
+ cold: 3
501
+ };
502
+ const statusDiff = statusOrder[a.status] - statusOrder[b.status];
503
+ if (statusDiff !== 0) return statusDiff;
504
+ return b.lastModifiedMs - a.lastModifiedMs;
505
+ });
506
+ const visible = allSessions.filter(
507
+ (s) => !config.hiddenSessions.includes(s.hideKey)
508
+ );
509
+ const totalCount = visible.length + visible.reduce((sum, s) => sum + s.subAgents.length, 0);
510
+ return {
511
+ sessions: visible,
512
+ totalCount,
513
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
514
+ };
515
+ }
516
+
517
+ // src/ui/ActivityViewerPanel.tsx
518
+ import { Box, Text } from "ink";
519
+ import { jsx, jsxs } from "react/jsx-runtime";
520
+ function getActivityStyle(activity) {
521
+ if (activity.type === "user") {
522
+ return { color: "white", dimColor: false };
523
+ }
524
+ if (activity.type === "response") {
525
+ return { color: "green", dimColor: false };
526
+ }
527
+ if (activity.type === "thinking") {
528
+ return { color: "magenta", dimColor: true };
529
+ }
530
+ if (activity.type === "tool") {
531
+ if (activity.label === "Bash") {
532
+ return { color: "gray", dimColor: false };
533
+ }
534
+ return { dimColor: true };
535
+ }
536
+ return { dimColor: true };
537
+ }
538
+ function formatActivityTime(date, now) {
539
+ const hours = String(date.getHours()).padStart(2, "0");
540
+ const minutes = String(date.getMinutes()).padStart(2, "0");
541
+ const seconds = String(date.getSeconds()).padStart(2, "0");
542
+ const time = `${hours}:${minutes}:${seconds}`;
543
+ const sameDay = date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth() && date.getDate() === now.getDate();
544
+ if (sameDay) return time;
545
+ const month = String(date.getMonth() + 1).padStart(2, "0");
546
+ const day = String(date.getDate()).padStart(2, "0");
547
+ return `${month}/${day} ${time}`;
548
+ }
549
+ function truncateDetail(detail, maxWidth) {
550
+ if (getDisplayWidth(detail) <= maxWidth) return detail;
551
+ let truncated = "";
552
+ let currentWidth = 0;
553
+ for (const char of detail) {
554
+ const charWidth = getDisplayWidth(char);
555
+ if (currentWidth + charWidth > maxWidth - 3) {
556
+ truncated += "...";
557
+ break;
558
+ }
559
+ truncated += char;
560
+ currentWidth += charWidth;
561
+ }
562
+ return truncated;
563
+ }
564
+ function ActivityViewerPanel({
565
+ activities,
566
+ sessionName,
567
+ scrollOffset,
568
+ isLive,
569
+ newCount,
570
+ visibleRows,
571
+ width,
572
+ cursorLine,
573
+ hasFocus,
574
+ spinner = ""
575
+ }) {
576
+ const innerWidth = getInnerWidth(width);
577
+ const contentWidth = innerWidth - 1;
578
+ let titleSuffix;
579
+ if (isLive) {
580
+ titleSuffix = `[LIVE ${spinner || "\u25BC"}]`;
581
+ } else {
582
+ const badge = newCount > 0 ? ` +${newCount}\u2191` : "";
583
+ titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}]`;
584
+ }
585
+ let visibleActivities;
586
+ if (activities.length === 0) {
587
+ visibleActivities = [];
588
+ } else if (isLive) {
589
+ visibleActivities = activities.slice(-visibleRows).reverse();
590
+ } else {
591
+ const end = Math.max(0, activities.length - scrollOffset);
592
+ const start = Math.max(0, end - visibleRows);
593
+ visibleActivities = activities.slice(start, end).reverse();
594
+ }
595
+ const now = /* @__PURE__ */ new Date();
596
+ const lines = [];
597
+ if (visibleActivities.length === 0) {
598
+ const emptyText = "No activity yet";
599
+ const emptyPadding = Math.max(0, contentWidth - emptyText.length - 1);
600
+ lines.push(
601
+ /* @__PURE__ */ jsxs(Text, { children: [
602
+ BOX.v,
603
+ " ",
604
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: emptyText }),
605
+ " ".repeat(emptyPadding),
606
+ BOX.v
607
+ ] }, "empty")
608
+ );
609
+ } else {
610
+ const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
611
+ for (let i = 0; i < visibleActivities.length; i++) {
612
+ const activity = visibleActivities[i];
613
+ const style = getActivityStyle(activity);
614
+ const isCursor = hasFocus && i === effectiveCursor;
615
+ const time = formatActivityTime(activity.timestamp, now);
616
+ const timestamp = `[${time}] `;
617
+ const timestampWidth = timestamp.length;
618
+ const icon = activity.icon;
619
+ const iconWidth = getDisplayWidth(icon);
620
+ const label = activity.label;
621
+ const detail = activity.detail;
622
+ const count = activity.count;
623
+ const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
624
+ const countSuffixWidth = countSuffix.length;
625
+ const prefixWidth = 2 + timestampWidth + iconWidth + 1;
626
+ const labelPart = detail ? `${label}: ` : label;
627
+ const labelWidth = labelPart.length;
628
+ const _availableForDetail = contentWidth - prefixWidth - labelWidth - countSuffixWidth + 1;
629
+ const detailMaxWidth = width - 2 - timestampWidth - iconWidth - 1 - labelWidth - countSuffixWidth - 1;
630
+ let labelContent;
631
+ let _displayWidth;
632
+ if (detail) {
633
+ const truncated = truncateDetail(detail, Math.max(0, detailMaxWidth));
634
+ labelContent = `${labelPart}${truncated}${countSuffix}`;
635
+ _displayWidth = prefixWidth - 1 + labelWidth + getDisplayWidth(truncated) + countSuffixWidth;
636
+ } else {
637
+ labelContent = label + countSuffix;
638
+ _displayWidth = prefixWidth - 1 + label.length + countSuffixWidth;
639
+ }
640
+ const usedWidth = 1 + 1 + timestampWidth + iconWidth + 1 + getDisplayWidth(labelContent) + 1;
641
+ const padding = Math.max(0, width - usedWidth);
642
+ lines.push(
643
+ /* @__PURE__ */ jsxs(Text, { children: [
644
+ BOX.v,
645
+ " ",
646
+ /* @__PURE__ */ jsxs(Text, { backgroundColor: isCursor ? "blue" : void 0, children: [
647
+ /* @__PURE__ */ jsx(Text, { dimColor: !isCursor, children: timestamp }),
648
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: icon }),
649
+ " ",
650
+ /* @__PURE__ */ jsx(
651
+ Text,
652
+ {
653
+ color: isCursor ? void 0 : style.color,
654
+ dimColor: !isCursor && style.dimColor,
655
+ children: labelContent
656
+ }
657
+ ),
658
+ " ".repeat(padding)
659
+ ] }),
660
+ BOX.v
661
+ ] }, `activity-${i}`)
662
+ );
663
+ }
664
+ }
665
+ const emptyRow = `${BOX.v}${" ".repeat(contentWidth + 1)}${BOX.v}`;
666
+ while (lines.length < visibleRows) {
667
+ lines.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${lines.length}`));
668
+ }
669
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
670
+ /* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
671
+ lines,
672
+ /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
673
+ ] });
674
+ }
675
+
676
+ // src/ui/DetailViewPanel.tsx
677
+ import { Box as Box2, Text as Text2 } from "ink";
678
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
679
+ function wrapText(text, maxWidth) {
680
+ if (!text) return ["(empty)"];
681
+ const words = text.split(" ");
682
+ const lines = [];
683
+ let current = "";
684
+ for (const word of words) {
685
+ if (!current) {
686
+ current = word;
687
+ } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
688
+ current += ` ${word}`;
689
+ } else {
690
+ lines.push(current);
691
+ current = word;
692
+ }
693
+ }
694
+ if (current) lines.push(current);
695
+ return lines.length > 0 ? lines : ["(empty)"];
696
+ }
697
+ function DetailViewPanel({
698
+ activity,
699
+ width,
700
+ visibleRows,
701
+ scrollOffset
702
+ }) {
703
+ const innerWidth = getInnerWidth(width);
704
+ const contentWidth = innerWidth - 1;
705
+ const allLines = wrapText(activity.detail, contentWidth);
706
+ const totalLines = allLines.length;
707
+ const clampedOffset = Math.min(
708
+ scrollOffset,
709
+ Math.max(0, totalLines - visibleRows)
710
+ );
711
+ const visibleSlice = allLines.slice(
712
+ clampedOffset,
713
+ clampedOffset + visibleRows
714
+ );
715
+ const style = getActivityStyle(activity);
716
+ const scrollSuffix = totalLines > visibleRows ? `[${clampedOffset + 1}-${Math.min(clampedOffset + visibleRows, totalLines)}/${totalLines}]` : "";
717
+ const iconWidth = getDisplayWidth(activity.icon);
718
+ const labelWidth = activity.label.length;
719
+ const scrollPart = scrollSuffix ? ` ${scrollSuffix} ${BOX.h}` : "";
720
+ const scrollPartWidth = scrollSuffix ? getDisplayWidth(scrollPart) : 0;
721
+ const dashCount = Math.max(
722
+ 0,
723
+ width - 3 - iconWidth - 1 - labelWidth - 1 - scrollPartWidth - 1
724
+ );
725
+ const dashes = BOX.h.repeat(dashCount);
726
+ const titleRight = `${dashes}${scrollPart}${BOX.tr}`;
727
+ const contentRows = [];
728
+ for (let i = 0; i < visibleRows; i++) {
729
+ const line = visibleSlice[i] ?? "";
730
+ const padding = Math.max(0, contentWidth - getDisplayWidth(line));
731
+ contentRows.push(
732
+ /* @__PURE__ */ jsxs2(Text2, { children: [
733
+ BOX.v,
734
+ " ",
735
+ line,
736
+ " ".repeat(padding),
737
+ BOX.v
738
+ ] }, i)
739
+ );
740
+ }
741
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
742
+ /* @__PURE__ */ jsxs2(Text2, { children: [
743
+ BOX.tl,
744
+ BOX.h,
745
+ " ",
746
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: activity.icon }),
747
+ " ",
748
+ /* @__PURE__ */ jsx2(Text2, { color: style.color, dimColor: style.dimColor, children: activity.label }),
749
+ " ",
750
+ titleRight
751
+ ] }),
752
+ contentRows,
753
+ /* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
754
+ ] });
755
+ }
756
+
757
+ // src/ui/hooks/useHotkeys.ts
758
+ function useHotkeys({
759
+ focus,
760
+ detailMode,
761
+ onSwitchFocus,
762
+ onScrollUp,
763
+ onScrollDown,
764
+ onScrollPageUp,
765
+ onScrollPageDown,
766
+ onScrollHalfPageUp,
767
+ onScrollHalfPageDown,
768
+ onScrollTop,
769
+ onScrollBottom,
770
+ onSaveLog,
771
+ onRefresh,
772
+ onQuit,
773
+ onEnter,
774
+ onHide,
775
+ onDetailClose,
776
+ onDetailScrollUp,
777
+ onDetailScrollDown
778
+ }) {
779
+ const handleInput = (input, key) => {
780
+ if (detailMode) {
781
+ if (key.upArrow || input === "k") {
782
+ onDetailScrollUp();
783
+ return;
784
+ }
785
+ if (key.downArrow || input === "j") {
786
+ onDetailScrollDown();
787
+ return;
788
+ }
789
+ if (key.return || key.escape || input === "q") {
790
+ onDetailClose();
791
+ return;
792
+ }
793
+ return;
794
+ }
795
+ if (input === "q") {
796
+ onQuit();
797
+ return;
798
+ }
799
+ if (key.tab) {
800
+ onSwitchFocus();
801
+ return;
802
+ }
803
+ if (key.return) {
804
+ onEnter();
805
+ return;
806
+ }
807
+ if (input === "r") {
808
+ onRefresh();
809
+ return;
810
+ }
811
+ if (key.pageUp) {
812
+ onScrollPageUp();
813
+ return;
814
+ }
815
+ if (key.pageDown) {
816
+ onScrollPageDown();
817
+ return;
818
+ }
819
+ if (key.ctrl) {
820
+ if (input === "b") {
821
+ onScrollPageUp();
822
+ return;
823
+ }
824
+ if (input === "f") {
825
+ onScrollPageDown();
826
+ return;
827
+ }
828
+ if (input === "u") {
829
+ onScrollHalfPageUp();
830
+ return;
831
+ }
832
+ if (input === "d") {
833
+ onScrollHalfPageDown();
834
+ return;
835
+ }
836
+ }
837
+ if (focus === "tree") {
838
+ if (input === "h") {
839
+ onHide();
840
+ return;
841
+ }
842
+ if (key.upArrow || input === "k") {
843
+ onScrollUp();
844
+ return;
845
+ }
846
+ if (key.downArrow || input === "j") {
847
+ onScrollDown();
848
+ return;
849
+ }
850
+ }
851
+ if (focus === "viewer") {
852
+ if (key.upArrow || input === "k") {
853
+ onScrollUp();
854
+ return;
855
+ }
856
+ if (key.downArrow || input === "j") {
857
+ onScrollDown();
858
+ return;
859
+ }
860
+ if (input === "g") {
861
+ onScrollTop();
862
+ return;
863
+ }
864
+ if (input === "G") {
865
+ onScrollBottom();
866
+ return;
867
+ }
868
+ if (input === "s") {
869
+ onSaveLog();
870
+ return;
871
+ }
872
+ }
873
+ };
874
+ const statusBarItems = detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close"] : focus === "tree" ? [
875
+ "Tab: viewer",
876
+ "\u2191\u2193/jk: select",
877
+ "PgUp/Dn: page",
878
+ "\u21B5: expand",
879
+ "h: hide",
880
+ "r: refresh",
881
+ "q: quit"
882
+ ] : [
883
+ "Tab: sessions",
884
+ "\u2191\u2193/jk: scroll",
885
+ "PgUp/Dn: page",
886
+ "g: top",
887
+ "G: live",
888
+ "\u21B5: detail",
889
+ "q: quit"
890
+ ];
891
+ return { handleInput, statusBarItems };
892
+ }
893
+
894
+ // src/ui/hooks/useSpinner.ts
895
+ import { useEffect, useState } from "react";
896
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
897
+ function useSpinner(active, intervalMs = 100) {
898
+ const [index, setIndex] = useState(0);
899
+ useEffect(() => {
900
+ if (!active) return;
901
+ const timer = setInterval(() => {
902
+ setIndex((i) => (i + 1) % FRAMES.length);
903
+ }, intervalMs);
904
+ return () => clearInterval(timer);
905
+ }, [active, intervalMs]);
906
+ return active ? FRAMES[index] : "";
907
+ }
908
+
909
+ // src/ui/SessionTreePanel.tsx
910
+ import { homedir as homedir3 } from "os";
911
+ import { Box as Box3, Text as Text3 } from "ink";
912
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
913
+ function formatElapsed(lastModifiedMs) {
914
+ const elapsed = Date.now() - lastModifiedMs;
915
+ const seconds = Math.floor(elapsed / 1e3);
916
+ const minutes = Math.floor(seconds / 60);
917
+ const hours = Math.floor(minutes / 60);
918
+ if (hours > 0) return `${hours}h${minutes % 60}m`;
919
+ if (minutes > 0) return `${minutes}m`;
920
+ if (seconds > 0) return `${seconds}s`;
921
+ return "<1s";
922
+ }
923
+ function getStatusColor(status) {
924
+ switch (status) {
925
+ case "hot":
926
+ return "green";
927
+ case "warm":
928
+ return "yellow";
929
+ case "cool":
930
+ return "cyan";
931
+ case "cold":
932
+ return "gray";
933
+ }
934
+ }
935
+ function formatProjectPath(projectPath) {
936
+ const home = homedir3();
937
+ const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
938
+ return raw;
939
+ }
940
+ function truncatePath(path, maxWidth) {
941
+ if (getDisplayWidth(path) <= maxWidth) return path;
942
+ if (maxWidth < 4) return "";
943
+ return `...${path.slice(-(maxWidth - 3))}`;
944
+ }
945
+ function SessionRow({
946
+ session,
947
+ isSelected,
948
+ hasFocus,
949
+ prefix,
950
+ contentWidth
951
+ }) {
952
+ const isParent = prefix === "";
953
+ const statusColor = getStatusColor(session.status);
954
+ const badge = `[${session.status}]`;
955
+ const elapsed = formatElapsed(session.lastModifiedMs);
956
+ const model = session.modelName ?? "";
957
+ const name = isParent ? session.projectName || session.id.slice(0, 8) : session.agentId ?? session.id.slice(0, 8);
958
+ const shortId = isParent && session.projectName ? ` #${session.id.slice(0, 4)}` : "";
959
+ const rightParts = [elapsed];
960
+ if (model) rightParts.push(model);
961
+ const rightSide = rightParts.join(" ");
962
+ const leftCore = `${prefix}${name}${shortId} ${badge}`;
963
+ const leftCoreWidth = getDisplayWidth(leftCore);
964
+ const rightWidth = getDisplayWidth(rightSide);
965
+ const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - 1;
966
+ let middleText = "";
967
+ if (middleAvailable > 3) {
968
+ const raw = isParent ? session.projectPath ? formatProjectPath(session.projectPath) : "" : session.taskDescription ?? "";
969
+ if (raw) {
970
+ const truncated = truncatePath(raw, middleAvailable);
971
+ if (truncated) middleText = truncated;
972
+ }
973
+ }
974
+ const middleSection = middleText ? ` ${middleText}` : "";
975
+ const gapWidth = Math.max(
976
+ 1,
977
+ contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
978
+ );
979
+ const gap = " ".repeat(gapWidth);
980
+ const fullLine = leftCore + middleSection + gap + rightSide;
981
+ const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
982
+ const highlight = isSelected && hasFocus;
983
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
984
+ BOX.v,
985
+ " ",
986
+ /* @__PURE__ */ jsxs3(Text3, { backgroundColor: highlight ? "blue" : void 0, bold: highlight, children: [
987
+ /* @__PURE__ */ jsx3(Text3, { children: prefix }),
988
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: name }),
989
+ shortId ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: shortId }) : null,
990
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
991
+ /* @__PURE__ */ jsx3(Text3, { color: statusColor, children: badge }),
992
+ middleText ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: middleSection }) : null,
993
+ /* @__PURE__ */ jsx3(Text3, { children: gap }),
994
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: elapsed }),
995
+ model ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${model}` }) : null
996
+ ] }),
997
+ " ".repeat(linePadding),
998
+ BOX.v
999
+ ] });
1000
+ }
1001
+ function appendSessionRows(result, session, expandedIds) {
1002
+ const isExpanded = expandedIds.has(session.id);
1003
+ const hotWarm = session.subAgents.filter(
1004
+ (s) => s.status === "hot" || s.status === "warm"
1005
+ );
1006
+ const cool = session.subAgents.filter((s) => s.status === "cool");
1007
+ const cold = session.subAgents.filter((s) => s.status === "cold");
1008
+ if (isExpanded) {
1009
+ const all = [...hotWarm, ...cool, ...cold];
1010
+ for (let i = 0; i < all.length; i++) {
1011
+ const isLast = i === all.length - 1;
1012
+ result.push({
1013
+ kind: "session",
1014
+ session: all[i],
1015
+ prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1016
+ });
1017
+ }
1018
+ } else {
1019
+ const hasSummary = cool.length > 0 || cold.length > 0;
1020
+ for (let i = 0; i < hotWarm.length; i++) {
1021
+ const isLast = i === hotWarm.length - 1 && !hasSummary;
1022
+ result.push({
1023
+ kind: "session",
1024
+ session: hotWarm[i],
1025
+ prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1026
+ });
1027
+ }
1028
+ if (hasSummary) {
1029
+ result.push({
1030
+ kind: "subagent-summary",
1031
+ parentId: session.id,
1032
+ coolCount: cool.length,
1033
+ coldCount: cold.length
1034
+ });
1035
+ }
1036
+ }
1037
+ }
1038
+ function flattenSessions(sessions, expandedIds) {
1039
+ const result = [];
1040
+ const visibleSessions = sessions.filter((s) => s.status !== "cold");
1041
+ const coldSessions = sessions.filter((s) => s.status === "cold");
1042
+ for (const session of visibleSessions) {
1043
+ result.push({ kind: "session", session, prefix: "" });
1044
+ appendSessionRows(result, session, expandedIds);
1045
+ }
1046
+ if (coldSessions.length > 0) {
1047
+ result.push({ kind: "cold-sessions-summary", count: coldSessions.length });
1048
+ if (expandedIds.has("__cold__")) {
1049
+ for (const session of coldSessions) {
1050
+ result.push({ kind: "session", session, prefix: "" });
1051
+ appendSessionRows(result, session, expandedIds);
1052
+ }
1053
+ }
1054
+ }
1055
+ return result;
1056
+ }
1057
+ function SubagentSummaryRow({
1058
+ coolCount,
1059
+ coldCount,
1060
+ contentWidth,
1061
+ isSelected,
1062
+ hasFocus
1063
+ }) {
1064
+ const parts = [];
1065
+ if (coolCount > 0) parts.push(`${coolCount} cool`);
1066
+ if (coldCount > 0) parts.push(`${coldCount} cold`);
1067
+ const hint = " +";
1068
+ const text = `\u2514\u2500 ... ${parts.join(" ")}`;
1069
+ const padding = Math.max(
1070
+ 0,
1071
+ contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
1072
+ );
1073
+ const active = isSelected && hasFocus;
1074
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
1075
+ BOX.v,
1076
+ " ",
1077
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: !active, inverse: active, children: [
1078
+ text,
1079
+ " ".repeat(padding),
1080
+ hint
1081
+ ] }),
1082
+ BOX.v
1083
+ ] });
1084
+ }
1085
+ function ColdSessionsSummaryRow({
1086
+ count,
1087
+ isSelected,
1088
+ hasFocus,
1089
+ width
1090
+ }) {
1091
+ const innerWidth = getInnerWidth(width);
1092
+ const label = ` ${count} cold `;
1093
+ const hint = isSelected && hasFocus ? " + " : "";
1094
+ const hintWidth = getDisplayWidth(hint);
1095
+ const labelWidth = getDisplayWidth(label);
1096
+ const dashCount = Math.max(0, innerWidth - 1 - labelWidth - hintWidth);
1097
+ const dashes = BOX.h.repeat(dashCount);
1098
+ const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
1099
+ const highlight = isSelected && hasFocus;
1100
+ return /* @__PURE__ */ jsx3(
1101
+ Text3,
1102
+ {
1103
+ backgroundColor: highlight ? "blue" : void 0,
1104
+ bold: highlight,
1105
+ dimColor: !highlight,
1106
+ children: line
1107
+ }
1108
+ );
1109
+ }
1110
+ function SessionTreePanel({
1111
+ sessions,
1112
+ selectedId,
1113
+ hasFocus,
1114
+ width = DEFAULT_PANEL_WIDTH,
1115
+ maxRows,
1116
+ expandedIds = /* @__PURE__ */ new Set()
1117
+ }) {
1118
+ const innerWidth = getInnerWidth(width);
1119
+ const contentWidth = innerWidth - 1;
1120
+ const titleLine = createTitleLine("Sessions", "", width);
1121
+ const bottomLine = createBottomLine(width);
1122
+ if (sessions.length === 0) {
1123
+ const emptyText = "No Claude sessions";
1124
+ const emptyPadding = Math.max(0, contentWidth - emptyText.length);
1125
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1126
+ /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
1127
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1128
+ BOX.v,
1129
+ " ",
1130
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: emptyText }),
1131
+ " ".repeat(emptyPadding),
1132
+ BOX.v
1133
+ ] }),
1134
+ /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
1135
+ ] });
1136
+ }
1137
+ const flatRows = flattenSessions(sessions, expandedIds);
1138
+ const totalRows = flatRows.length;
1139
+ const selectedFlatIndex = flatRows.findIndex((row) => {
1140
+ if (row.kind === "session") return row.session.id === selectedId;
1141
+ if (row.kind === "subagent-summary")
1142
+ return selectedId === `__sub-${row.parentId}__`;
1143
+ if (row.kind === "cold-sessions-summary") return selectedId === "__cold__";
1144
+ return false;
1145
+ });
1146
+ const needsOverflow = maxRows !== void 0 && totalRows > maxRows;
1147
+ const visibleCount = needsOverflow ? maxRows - 1 : totalRows;
1148
+ let scrollTop = 0;
1149
+ if (needsOverflow && selectedFlatIndex >= 0) {
1150
+ scrollTop = Math.max(0, selectedFlatIndex - visibleCount + 1);
1151
+ scrollTop = Math.min(scrollTop, Math.max(0, totalRows - visibleCount));
1152
+ }
1153
+ const displayRows = flatRows.slice(scrollTop, scrollTop + visibleCount);
1154
+ const hiddenBelow = totalRows - (scrollTop + displayRows.length);
1155
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1156
+ /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
1157
+ displayRows.map(
1158
+ (row, idx) => row.kind === "session" ? /* @__PURE__ */ jsx3(
1159
+ SessionRow,
1160
+ {
1161
+ session: row.session,
1162
+ isSelected: row.session.id === selectedId,
1163
+ hasFocus,
1164
+ prefix: row.prefix,
1165
+ contentWidth
1166
+ },
1167
+ `${row.session.id}-${idx}`
1168
+ ) : row.kind === "subagent-summary" ? /* @__PURE__ */ jsx3(
1169
+ SubagentSummaryRow,
1170
+ {
1171
+ coolCount: row.coolCount,
1172
+ coldCount: row.coldCount,
1173
+ contentWidth,
1174
+ isSelected: selectedId === `__sub-${row.parentId}__`,
1175
+ hasFocus
1176
+ },
1177
+ `subagent-summary-${idx}`
1178
+ ) : /* @__PURE__ */ jsx3(
1179
+ ColdSessionsSummaryRow,
1180
+ {
1181
+ count: row.count,
1182
+ isSelected: selectedId === "__cold__",
1183
+ hasFocus,
1184
+ width
1185
+ },
1186
+ "cold-summary"
1187
+ )
1188
+ ),
1189
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text3, { children: [
1190
+ BOX.v,
1191
+ " ",
1192
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `... ${hiddenBelow} more` }),
1193
+ " ".repeat(
1194
+ Math.max(0, contentWidth - `... ${hiddenBelow} more`.length - 1)
1195
+ ),
1196
+ BOX.v
1197
+ ] }),
1198
+ /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
1199
+ ] });
1200
+ }
1201
+
1202
+ // src/ui/App.tsx
1203
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1204
+ var VIEWER_HEIGHT_FRACTION = 0.55;
1205
+ function subSummarySentinel(parentId) {
1206
+ return {
1207
+ id: `__sub-${parentId}__`,
1208
+ filePath: "",
1209
+ projectPath: "",
1210
+ projectName: "",
1211
+ lastModifiedMs: 0,
1212
+ status: "cold",
1213
+ modelName: null,
1214
+ subAgents: []
1215
+ };
1216
+ }
1217
+ function appendSubAgentRows(result, session, expandedIds) {
1218
+ if (expandedIds.has(session.id)) {
1219
+ result.push(...session.subAgents);
1220
+ } else {
1221
+ result.push(
1222
+ ...session.subAgents.filter(
1223
+ (sub) => sub.status === "hot" || sub.status === "warm"
1224
+ )
1225
+ );
1226
+ if (session.subAgents.some(
1227
+ (sub) => sub.status === "cool" || sub.status === "cold"
1228
+ )) {
1229
+ result.push(subSummarySentinel(session.id));
1230
+ }
1231
+ }
1232
+ }
1233
+ function flattenSessions2(tree, expandedIds) {
1234
+ const result = [];
1235
+ const visible = tree.sessions.filter((s) => s.status !== "cold");
1236
+ const cold = tree.sessions.filter((s) => s.status === "cold");
1237
+ for (const s of visible) {
1238
+ result.push(s);
1239
+ appendSubAgentRows(result, s, expandedIds);
1240
+ }
1241
+ if (cold.length > 0) {
1242
+ result.push({
1243
+ id: "__cold__",
1244
+ filePath: "",
1245
+ projectPath: "",
1246
+ projectName: `${cold.length} cold`,
1247
+ lastModifiedMs: 0,
1248
+ status: "cold",
1249
+ modelName: null,
1250
+ subAgents: []
1251
+ });
1252
+ if (expandedIds.has("__cold__")) {
1253
+ for (const s of cold) {
1254
+ result.push(s);
1255
+ appendSubAgentRows(result, s, expandedIds);
1256
+ }
1257
+ }
1258
+ }
1259
+ return result;
1260
+ }
1261
+ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
1262
+ if (acts.length === 0) return null;
1263
+ let visible;
1264
+ if (live) {
1265
+ visible = acts.slice(-rows).reverse();
1266
+ } else {
1267
+ const end = Math.max(0, acts.length - scrollOff);
1268
+ const start = Math.max(0, end - rows);
1269
+ visible = acts.slice(start, end).reverse();
1270
+ }
1271
+ const effectiveCursor = Math.min(cursorLine, visible.length - 1);
1272
+ return visible[effectiveCursor] ?? null;
1273
+ }
1274
+ function App({ mode }) {
1275
+ const { exit } = useApp();
1276
+ const { stdout } = useStdout();
1277
+ const isWatchMode = mode === "watch";
1278
+ const config = useMemo(() => loadGlobalConfig(), []);
1279
+ const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
1280
+ const [sessionTree, setSessionTree] = useState2(
1281
+ () => discoverSessions(config)
1282
+ );
1283
+ const [selectedId, setSelectedId] = useState2(() => {
1284
+ const first = sessionTree.sessions[0];
1285
+ return first?.id ?? null;
1286
+ });
1287
+ const [focus, setFocus] = useState2("tree");
1288
+ const [scrollOffset, setScrollOffset] = useState2(0);
1289
+ const [isLive, setIsLive] = useState2(true);
1290
+ const [activities, setActivities] = useState2([]);
1291
+ const [newCount, setNewCount] = useState2(0);
1292
+ const [expandedIds, setExpandedIds] = useState2(/* @__PURE__ */ new Set());
1293
+ const [viewerCursorLine, setViewerCursorLine] = useState2(0);
1294
+ const [detailMode, setDetailMode] = useState2(false);
1295
+ const [detailActivity, setDetailActivity] = useState2(
1296
+ null
1297
+ );
1298
+ const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
1299
+ const allFlat = useMemo(
1300
+ () => flattenSessions2(sessionTree, expandedIds),
1301
+ [sessionTree, expandedIds]
1302
+ );
1303
+ const allFlatRef = useRef(allFlat);
1304
+ useEffect2(() => {
1305
+ allFlatRef.current = allFlat;
1306
+ }, [allFlat]);
1307
+ const activitiesLengthRef = useRef(0);
1308
+ useEffect2(() => {
1309
+ activitiesLengthRef.current = activities.length;
1310
+ }, [activities.length]);
1311
+ useEffect2(() => {
1312
+ const node = allFlatRef.current.find((s) => s.id === selectedId);
1313
+ if (node?.filePath) {
1314
+ setActivities(parseSessionHistory(node.filePath));
1315
+ setScrollOffset(0);
1316
+ setIsLive(true);
1317
+ setNewCount(0);
1318
+ setViewerCursorLine(0);
1319
+ } else {
1320
+ setActivities([]);
1321
+ }
1322
+ }, [selectedId]);
1323
+ const refresh = useCallback(() => {
1324
+ const freshConfig = loadGlobalConfig();
1325
+ const tree = discoverSessions(freshConfig);
1326
+ setSessionTree(tree);
1327
+ const updatedFlat = flattenSessions2(tree, expandedIds);
1328
+ const node = updatedFlat.find((s) => s.id === selectedId);
1329
+ if (!node || !node.filePath) return;
1330
+ const newActivities = parseSessionHistory(node.filePath);
1331
+ const delta = newActivities.length - activitiesLengthRef.current;
1332
+ setActivities(newActivities);
1333
+ if (!isLive && delta > 0) {
1334
+ setScrollOffset((o) => o + delta);
1335
+ setNewCount((n) => n + delta);
1336
+ }
1337
+ }, [selectedId, isLive, expandedIds]);
1338
+ const refreshRef = useRef(refresh);
1339
+ useEffect2(() => {
1340
+ refreshRef.current = refresh;
1341
+ }, [refresh]);
1342
+ useEffect2(() => {
1343
+ if (!isWatchMode) return;
1344
+ const projectsDir = getProjectsDir();
1345
+ const usePolling = process.platform === "linux" || !existsSync4(projectsDir);
1346
+ if (usePolling) {
1347
+ const timer = setInterval(
1348
+ () => refreshRef.current(),
1349
+ config.refreshIntervalMs
1350
+ );
1351
+ return () => clearInterval(timer);
1352
+ }
1353
+ let debounce = null;
1354
+ let watcher = null;
1355
+ try {
1356
+ watcher = watch(projectsDir, { recursive: true }, () => {
1357
+ if (debounce) clearTimeout(debounce);
1358
+ debounce = setTimeout(() => refreshRef.current(), 150);
1359
+ });
1360
+ } catch {
1361
+ const timer = setInterval(
1362
+ () => refreshRef.current(),
1363
+ config.refreshIntervalMs
1364
+ );
1365
+ return () => clearInterval(timer);
1366
+ }
1367
+ return () => {
1368
+ watcher?.close();
1369
+ if (debounce) clearTimeout(debounce);
1370
+ };
1371
+ }, [isWatchMode, config.refreshIntervalMs]);
1372
+ const selectedIndex = allFlat.findIndex((s) => s.id === selectedId);
1373
+ const height = (stdout?.rows ?? 41) - 1;
1374
+ const width = stdout?.columns ?? 80;
1375
+ const viewerRows = Math.max(
1376
+ 5,
1377
+ Math.floor(height * VIEWER_HEIGHT_FRACTION) - 4
1378
+ );
1379
+ const treeRows = Math.max(3, height - viewerRows - 7);
1380
+ const saveLog = useCallback(() => {
1381
+ if (!activities.length || !selectedId) return;
1382
+ ensureLogDir(config.logDir);
1383
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1384
+ const filePath = join4(
1385
+ config.logDir,
1386
+ `${date}-${selectedId.slice(0, 8)}.txt`
1387
+ );
1388
+ const lines = activities.map(
1389
+ (a) => `[${a.timestamp.toISOString()}] ${a.icon} ${a.label} ${a.detail}`
1390
+ );
1391
+ try {
1392
+ writeFileSync2(filePath, `${lines.join("\n")}
1393
+ `, "utf-8");
1394
+ } catch {
1395
+ }
1396
+ }, [activities, selectedId, config.logDir]);
1397
+ const spinner = useSpinner(isWatchMode);
1398
+ const { handleInput, statusBarItems } = useHotkeys({
1399
+ focus,
1400
+ detailMode,
1401
+ onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
1402
+ onScrollUp: () => {
1403
+ if (focus === "tree") {
1404
+ const prev = Math.max(0, selectedIndex - 1);
1405
+ setSelectedId(allFlat[prev]?.id ?? selectedId);
1406
+ } else {
1407
+ if (viewerCursorLine > 0) {
1408
+ setViewerCursorLine((c) => c - 1);
1409
+ } else {
1410
+ setScrollOffset((o) => {
1411
+ const newOffset = Math.max(0, o - 1);
1412
+ if (newOffset === 0) {
1413
+ setIsLive(true);
1414
+ setNewCount(0);
1415
+ }
1416
+ return newOffset;
1417
+ });
1418
+ }
1419
+ }
1420
+ },
1421
+ onScrollDown: () => {
1422
+ if (focus === "tree") {
1423
+ const next = Math.min(allFlat.length - 1, selectedIndex + 1);
1424
+ setSelectedId(allFlat[next]?.id ?? selectedId);
1425
+ } else {
1426
+ if (viewerCursorLine < viewerRows - 1) {
1427
+ setViewerCursorLine((c) => c + 1);
1428
+ } else {
1429
+ setIsLive(false);
1430
+ setScrollOffset(
1431
+ (o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
1432
+ );
1433
+ }
1434
+ }
1435
+ },
1436
+ onScrollPageUp: () => {
1437
+ if (focus === "tree") {
1438
+ const prev = Math.max(0, selectedIndex - 5);
1439
+ setSelectedId(allFlat[prev]?.id ?? selectedId);
1440
+ } else {
1441
+ setViewerCursorLine(0);
1442
+ setScrollOffset((o) => {
1443
+ const newOffset = Math.max(0, o - viewerRows);
1444
+ if (newOffset === 0) {
1445
+ setIsLive(true);
1446
+ setNewCount(0);
1447
+ }
1448
+ return newOffset;
1449
+ });
1450
+ }
1451
+ },
1452
+ onScrollPageDown: () => {
1453
+ if (focus === "tree") {
1454
+ const next = Math.min(allFlat.length - 1, selectedIndex + 5);
1455
+ setSelectedId(allFlat[next]?.id ?? selectedId);
1456
+ } else {
1457
+ setViewerCursorLine(0);
1458
+ setIsLive(false);
1459
+ setScrollOffset(
1460
+ (o) => Math.min(o + viewerRows, Math.max(0, activities.length - viewerRows))
1461
+ );
1462
+ }
1463
+ },
1464
+ onScrollHalfPageUp: () => {
1465
+ if (focus === "tree") {
1466
+ const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
1467
+ setSelectedId(allFlat[prev]?.id ?? selectedId);
1468
+ } else {
1469
+ setViewerCursorLine(0);
1470
+ setScrollOffset((o) => {
1471
+ const newOffset = Math.max(0, o - Math.floor(viewerRows / 2));
1472
+ if (newOffset === 0) {
1473
+ setIsLive(true);
1474
+ setNewCount(0);
1475
+ }
1476
+ return newOffset;
1477
+ });
1478
+ }
1479
+ },
1480
+ onScrollHalfPageDown: () => {
1481
+ if (focus === "tree") {
1482
+ const next = Math.min(
1483
+ allFlat.length - 1,
1484
+ selectedIndex + Math.ceil(5 / 2)
1485
+ );
1486
+ setSelectedId(allFlat[next]?.id ?? selectedId);
1487
+ } else {
1488
+ setViewerCursorLine(0);
1489
+ setIsLive(false);
1490
+ setScrollOffset(
1491
+ (o) => Math.min(
1492
+ o + Math.floor(viewerRows / 2),
1493
+ Math.max(0, activities.length - viewerRows)
1494
+ )
1495
+ );
1496
+ }
1497
+ },
1498
+ onScrollTop: () => {
1499
+ setViewerCursorLine(0);
1500
+ setIsLive(false);
1501
+ setScrollOffset(Math.max(0, activities.length - viewerRows));
1502
+ },
1503
+ onScrollBottom: () => {
1504
+ setViewerCursorLine(0);
1505
+ setIsLive(true);
1506
+ setScrollOffset(0);
1507
+ setNewCount(0);
1508
+ },
1509
+ onDetailClose: () => {
1510
+ setDetailMode(false);
1511
+ },
1512
+ onDetailScrollUp: () => {
1513
+ setDetailScrollOffset((o) => Math.max(0, o - 1));
1514
+ },
1515
+ onDetailScrollDown: () => {
1516
+ setDetailScrollOffset((o) => o + 1);
1517
+ },
1518
+ onEnter: () => {
1519
+ if (focus === "viewer") {
1520
+ const act = getSelectedActivity(
1521
+ activities,
1522
+ isLive,
1523
+ scrollOffset,
1524
+ viewerRows,
1525
+ viewerCursorLine
1526
+ );
1527
+ if (act) {
1528
+ setDetailActivity(act);
1529
+ setDetailMode(true);
1530
+ setDetailScrollOffset(0);
1531
+ }
1532
+ return;
1533
+ }
1534
+ if (focus !== "tree" || !selectedId) return;
1535
+ if (selectedId === "__cold__") {
1536
+ setExpandedIds((prev) => {
1537
+ const next = new Set(prev);
1538
+ if (next.has("__cold__")) {
1539
+ next.delete("__cold__");
1540
+ } else {
1541
+ next.add("__cold__");
1542
+ }
1543
+ return next;
1544
+ });
1545
+ return;
1546
+ }
1547
+ if (selectedId.startsWith("__sub-") && selectedId.endsWith("__")) {
1548
+ const parentId = selectedId.slice(6, -2);
1549
+ setExpandedIds((prev) => {
1550
+ const next = new Set(prev);
1551
+ if (next.has(parentId)) {
1552
+ next.delete(parentId);
1553
+ } else {
1554
+ next.add(parentId);
1555
+ }
1556
+ return next;
1557
+ });
1558
+ return;
1559
+ }
1560
+ const parentSession = sessionTree.sessions.find(
1561
+ (s) => s.id === selectedId
1562
+ );
1563
+ if (!parentSession || !parentSession.subAgents.some(
1564
+ (s) => s.status === "cool" || s.status === "cold"
1565
+ ))
1566
+ return;
1567
+ setExpandedIds((prev) => {
1568
+ const next = new Set(prev);
1569
+ if (next.has(selectedId)) {
1570
+ next.delete(selectedId);
1571
+ } else {
1572
+ next.add(selectedId);
1573
+ }
1574
+ return next;
1575
+ });
1576
+ },
1577
+ onHide: () => {
1578
+ if (focus !== "tree" || !selectedId) return;
1579
+ if (selectedId === "__cold__") {
1580
+ const coldSessions = sessionTree.sessions.filter(
1581
+ (s) => s.status === "cold"
1582
+ );
1583
+ for (const s of coldSessions) hideSession(s.hideKey);
1584
+ const nextId = allFlat[selectedIndex - 1]?.id ?? null;
1585
+ refresh();
1586
+ setSelectedId(nextId);
1587
+ return;
1588
+ }
1589
+ const selectedSession2 = sessionTree.sessions.find(
1590
+ (s) => s.id === selectedId
1591
+ );
1592
+ if (selectedSession2) {
1593
+ hideSession(selectedSession2.hideKey);
1594
+ const nextId = allFlat[selectedIndex + 1]?.id ?? allFlat[selectedIndex - 1]?.id ?? null;
1595
+ refresh();
1596
+ setSelectedId(nextId);
1597
+ return;
1598
+ }
1599
+ for (const s of sessionTree.sessions) {
1600
+ const selectedSubAgent = s.subAgents.find((sa) => sa.id === selectedId);
1601
+ if (selectedSubAgent) {
1602
+ hideSubAgent(selectedSubAgent.hideKey);
1603
+ const nextId = allFlat[selectedIndex + 1]?.id ?? allFlat[selectedIndex - 1]?.id ?? null;
1604
+ refresh();
1605
+ setSelectedId(nextId);
1606
+ return;
1607
+ }
1608
+ }
1609
+ },
1610
+ onSaveLog: saveLog,
1611
+ onRefresh: refresh,
1612
+ onQuit: exit
1613
+ });
1614
+ useInput((input, key) => handleInput(input, key), { isActive: isWatchMode });
1615
+ const selectedSession = allFlat.find((s) => s.id === selectedId);
1616
+ const isPlaceholderSelected = !selectedSession || selectedId === "__cold__" || !!selectedId && selectedId.startsWith("__sub-") && selectedId.endsWith("__");
1617
+ const sessionDisplayName = isPlaceholderSelected ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
1618
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
1619
+ migrationWarning && /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
1620
+ isWatchMode && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, justifyContent: "space-between", width, children: [
1621
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
1622
+ spinner,
1623
+ " AgentHUD v",
1624
+ getVersion()
1625
+ ] }),
1626
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
1627
+ ] }),
1628
+ /* @__PURE__ */ jsx4(
1629
+ SessionTreePanel,
1630
+ {
1631
+ sessions: sessionTree.sessions,
1632
+ selectedId,
1633
+ hasFocus: focus === "tree",
1634
+ width,
1635
+ maxRows: treeRows,
1636
+ expandedIds
1637
+ }
1638
+ ),
1639
+ /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx4(
1640
+ DetailViewPanel,
1641
+ {
1642
+ activity: detailActivity,
1643
+ sessionName: sessionDisplayName,
1644
+ scrollOffset: detailScrollOffset,
1645
+ visibleRows: viewerRows,
1646
+ width
1647
+ }
1648
+ ) : /* @__PURE__ */ jsx4(
1649
+ ActivityViewerPanel,
1650
+ {
1651
+ activities,
1652
+ sessionName: sessionDisplayName,
1653
+ scrollOffset,
1654
+ isLive,
1655
+ newCount,
1656
+ visibleRows: viewerRows,
1657
+ width,
1658
+ cursorLine: viewerCursorLine,
1659
+ hasFocus: focus === "viewer",
1660
+ spinner
1661
+ }
1662
+ ) })
1663
+ ] });
1664
+ }
1665
+
1666
+ // src/main.ts
1667
+ var options = parseArgs(process.argv.slice(2));
1668
+ if (options.command === "help") {
1669
+ console.log(getHelp());
1670
+ process.exit(0);
1671
+ }
1672
+ if (options.command === "version") {
1673
+ console.log(getVersion());
1674
+ process.exit(0);
1675
+ }
1676
+ var legacyConfig = join5(process.cwd(), ".agenthud", "config.yaml");
1677
+ if (existsSync5(legacyConfig)) {
1678
+ console.log(
1679
+ "The project-level config file (.agenthud/config.yaml) is no longer supported."
1680
+ );
1681
+ console.log("Settings have moved to ~/.agenthud/config.yaml.");
1682
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1683
+ await new Promise((resolve) => {
1684
+ rl.question(
1685
+ "Delete the old config file and continue? [y/N] ",
1686
+ (answer) => {
1687
+ rl.close();
1688
+ if (answer.trim().toLowerCase() === "y") {
1689
+ rmSync(legacyConfig);
1690
+ console.log("Deleted .agenthud/config.yaml.");
1691
+ } else {
1692
+ console.log("Aborted.");
1693
+ process.exit(0);
1694
+ }
1695
+ resolve();
1696
+ }
1697
+ );
1698
+ });
1699
+ }
1700
+ if (options.mode === "watch") {
1701
+ clearScreen();
1702
+ }
1703
+ render(React.createElement(App, { mode: options.mode }));