agenthud 0.7.0 → 0.7.2

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,4104 @@
1
+ // src/main.ts
2
+ import { existsSync as existsSync9 } from "fs";
3
+ import { render } from "ink";
4
+ import React2 from "react";
5
+
6
+ // src/cli.ts
7
+ import { readFileSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ function getHelp() {
11
+ return `Usage: agenthud [command] [options]
12
+
13
+ Commands:
14
+ init Initialize agenthud in current directory
15
+
16
+ Options:
17
+ -w, --watch Watch mode (default)
18
+ --once Run once and exit
19
+ -V, --version Show version number
20
+ -h, --help Show this help message
21
+ `;
22
+ }
23
+ function getVersion() {
24
+ const __dirname3 = dirname(fileURLToPath(import.meta.url));
25
+ const packageJson = JSON.parse(
26
+ readFileSync(join(__dirname3, "..", "package.json"), "utf-8")
27
+ );
28
+ return packageJson.version;
29
+ }
30
+ function clearScreen() {
31
+ console.clear();
32
+ }
33
+ function parseArgs(args) {
34
+ const hasOnce = args.includes("--once");
35
+ const hasVersion = args.includes("--version") || args.includes("-V");
36
+ const hasHelp = args.includes("--help") || args.includes("-h");
37
+ if (hasHelp) {
38
+ return { mode: "watch", command: "help" };
39
+ }
40
+ if (hasVersion) {
41
+ return { mode: "watch", command: "version" };
42
+ }
43
+ const command = args[0] === "init" ? "init" : void 0;
44
+ if (hasOnce) {
45
+ return { mode: "once", command };
46
+ }
47
+ return { mode: "watch", command };
48
+ }
49
+
50
+ // src/commands/init.ts
51
+ import {
52
+ appendFileSync,
53
+ existsSync as existsSync2,
54
+ mkdirSync,
55
+ readFileSync as readFileSync3,
56
+ writeFileSync
57
+ } from "fs";
58
+ import { homedir } from "os";
59
+ import { dirname as dirname2, join as join2 } from "path";
60
+ import { fileURLToPath as fileURLToPath2 } from "url";
61
+
62
+ // src/data/detectTestFramework.ts
63
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
64
+ var TEST_RESULTS_FILE = ".agenthud/test-results.xml";
65
+ var FRAMEWORK_COMMANDS = {
66
+ vitest: `npx vitest run --reporter=junit --outputFile=${TEST_RESULTS_FILE}`,
67
+ jest: `JEST_JUNIT_OUTPUT_FILE=${TEST_RESULTS_FILE} npx jest --reporters=jest-junit`,
68
+ mocha: `npx mocha --reporter mocha-junit-reporter --reporter-options mochaFile=${TEST_RESULTS_FILE}`,
69
+ pytest: `uv run pytest --junitxml=${TEST_RESULTS_FILE}`
70
+ };
71
+ var JS_FRAMEWORKS = ["vitest", "jest", "mocha"];
72
+ function detectTestFramework() {
73
+ const jsFramework = detectJsFramework();
74
+ if (jsFramework) {
75
+ return jsFramework;
76
+ }
77
+ const pythonFramework = detectPythonFramework();
78
+ if (pythonFramework) {
79
+ return pythonFramework;
80
+ }
81
+ return null;
82
+ }
83
+ function detectJsFramework() {
84
+ if (!existsSync("package.json")) {
85
+ return null;
86
+ }
87
+ let packageJson;
88
+ try {
89
+ const content = readFileSync2("package.json", "utf-8");
90
+ packageJson = JSON.parse(content);
91
+ } catch {
92
+ return null;
93
+ }
94
+ const allDeps = {
95
+ ...packageJson.dependencies,
96
+ ...packageJson.devDependencies
97
+ };
98
+ for (const framework of JS_FRAMEWORKS) {
99
+ if (allDeps[framework]) {
100
+ return {
101
+ framework,
102
+ command: FRAMEWORK_COMMANDS[framework]
103
+ };
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ function detectPythonFramework() {
109
+ const pytestIndicators = ["pytest.ini", "conftest.py"];
110
+ for (const file of pytestIndicators) {
111
+ if (existsSync(file)) {
112
+ return {
113
+ framework: "pytest",
114
+ command: FRAMEWORK_COMMANDS.pytest
115
+ };
116
+ }
117
+ }
118
+ if (existsSync("pyproject.toml")) {
119
+ try {
120
+ const content = readFileSync2("pyproject.toml", "utf-8");
121
+ if (content.includes("[tool.pytest") || content.includes("[tool.pytest.ini_options]")) {
122
+ return {
123
+ framework: "pytest",
124
+ command: FRAMEWORK_COMMANDS.pytest
125
+ };
126
+ }
127
+ } catch {
128
+ }
129
+ }
130
+ const requirementsFiles = ["requirements.txt", "requirements-dev.txt"];
131
+ for (const file of requirementsFiles) {
132
+ if (existsSync(file)) {
133
+ try {
134
+ const content = readFileSync2(file, "utf-8");
135
+ if (content.includes("pytest")) {
136
+ return {
137
+ framework: "pytest",
138
+ command: FRAMEWORK_COMMANDS.pytest
139
+ };
140
+ }
141
+ } catch {
142
+ }
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ // src/commands/init.ts
149
+ var __filename2 = fileURLToPath2(import.meta.url);
150
+ var __dirname2 = dirname2(__filename2);
151
+ function getDefaultConfig() {
152
+ let templatePath = join2(__dirname2, "templates", "config.yaml");
153
+ if (!existsSync2(templatePath)) {
154
+ templatePath = join2(__dirname2, "..", "templates", "config.yaml");
155
+ }
156
+ return readFileSync3(templatePath, "utf-8");
157
+ }
158
+ function getClaudeSessionPath(projectPath) {
159
+ const encoded = projectPath.replace(/[/\\]/g, "-");
160
+ return join2(homedir(), ".claude", "projects", encoded);
161
+ }
162
+ function runInit(cwd = process.cwd()) {
163
+ const result = {
164
+ created: [],
165
+ skipped: [],
166
+ warnings: []
167
+ };
168
+ if (!existsSync2(".agenthud")) {
169
+ mkdirSync(".agenthud", { recursive: true });
170
+ result.created.push(".agenthud/");
171
+ } else {
172
+ result.skipped.push(".agenthud/");
173
+ }
174
+ if (!existsSync2(".agenthud/tests")) {
175
+ mkdirSync(".agenthud/tests", { recursive: true });
176
+ result.created.push(".agenthud/tests/");
177
+ } else {
178
+ result.skipped.push(".agenthud/tests/");
179
+ }
180
+ const testFramework = detectTestFramework();
181
+ if (testFramework) {
182
+ result.detectedTestFramework = testFramework.framework;
183
+ }
184
+ if (!existsSync2(".agenthud/config.yaml")) {
185
+ let configContent = getDefaultConfig();
186
+ if (testFramework) {
187
+ configContent = configContent.replace(
188
+ /command: npx vitest run --reporter=json/,
189
+ `command: ${testFramework.command}`
190
+ );
191
+ } else {
192
+ configContent = configContent.replace(
193
+ /command: npx vitest run --reporter=json/,
194
+ "# command: (auto-detect failed - configure manually)"
195
+ );
196
+ }
197
+ writeFileSync(".agenthud/config.yaml", configContent);
198
+ result.created.push(".agenthud/config.yaml");
199
+ } else {
200
+ result.skipped.push(".agenthud/config.yaml");
201
+ }
202
+ if (existsSync2(".git")) {
203
+ if (!existsSync2(".gitignore")) {
204
+ writeFileSync(".gitignore", ".agenthud/\n");
205
+ result.created.push(".gitignore");
206
+ } else {
207
+ const content = readFileSync3(".gitignore", "utf-8");
208
+ if (!content.includes(".agenthud/")) {
209
+ appendFileSync(".gitignore", "\n.agenthud/\n");
210
+ result.created.push(".gitignore");
211
+ } else {
212
+ result.skipped.push(".gitignore");
213
+ }
214
+ }
215
+ }
216
+ if (!existsSync2(".git")) {
217
+ result.warnings.push(
218
+ "Not a git repository - Git panel will show limited info"
219
+ );
220
+ }
221
+ const claudeSessionPath = getClaudeSessionPath(cwd);
222
+ if (!existsSync2(claudeSessionPath)) {
223
+ result.warnings.push(
224
+ "No Claude session found - start Claude to see activity"
225
+ );
226
+ }
227
+ return result;
228
+ }
229
+
230
+ // src/data/sessionAvailability.ts
231
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
232
+ import { homedir as homedir3 } from "os";
233
+ import { basename as basename2, join as join4 } from "path";
234
+
235
+ // src/data/otherSessions.ts
236
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
237
+ import { homedir as homedir2 } from "os";
238
+ import { basename, join as join3, sep } from "path";
239
+
240
+ // src/ui/constants.ts
241
+ import stringWidth from "string-width";
242
+ var THIRTY_SECONDS_MS = 30 * 1e3;
243
+ var FIVE_MINUTES_MS = 5 * 60 * 1e3;
244
+ var DEFAULT_PANEL_WIDTH = 70;
245
+ var MIN_TERMINAL_WIDTH = 50;
246
+ var MAX_TERMINAL_WIDTH = 120;
247
+ var DEFAULT_FALLBACK_WIDTH = 80;
248
+ var CONTENT_WIDTH = DEFAULT_PANEL_WIDTH - 4;
249
+ var INNER_WIDTH = DEFAULT_PANEL_WIDTH - 2;
250
+ function getContentWidth(panelWidth) {
251
+ return panelWidth - 4;
252
+ }
253
+ function getInnerWidth(panelWidth) {
254
+ return panelWidth - 2;
255
+ }
256
+ var BOX = {
257
+ tl: "\u250C",
258
+ tr: "\u2510",
259
+ bl: "\u2514",
260
+ br: "\u2518",
261
+ h: "\u2500",
262
+ v: "\u2502",
263
+ ml: "\u251C",
264
+ mr: "\u2524"
265
+ };
266
+ function createTitleLine(label, suffix = "", panelWidth = DEFAULT_PANEL_WIDTH) {
267
+ const leftPart = `${BOX.h} ${label} `;
268
+ const rightPart = suffix ? ` ${suffix} ${BOX.h}` : "";
269
+ const leftWidth = getDisplayWidth(leftPart);
270
+ const rightWidth = suffix ? getDisplayWidth(rightPart) : 0;
271
+ const dashCount = panelWidth - 1 - leftWidth - rightWidth - 1;
272
+ const dashes = BOX.h.repeat(Math.max(0, dashCount));
273
+ return BOX.tl + leftPart + dashes + rightPart + BOX.tr;
274
+ }
275
+ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
276
+ return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
277
+ }
278
+ function createSeparatorLine(title, panelWidth = DEFAULT_PANEL_WIDTH) {
279
+ const leftPart = `${BOX.h} ${title} `;
280
+ const leftWidth = leftPart.length;
281
+ const dashCount = panelWidth - 1 - leftWidth - 1;
282
+ const dashes = BOX.h.repeat(Math.max(0, dashCount));
283
+ return BOX.ml + leftPart + dashes + BOX.mr;
284
+ }
285
+ function padLine(content, panelWidth = DEFAULT_PANEL_WIDTH) {
286
+ const innerWidth = getInnerWidth(panelWidth);
287
+ const padding = innerWidth - content.length;
288
+ return content + " ".repeat(Math.max(0, padding));
289
+ }
290
+ var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
291
+ function truncate(text, maxLength) {
292
+ if (text.length <= maxLength) return text;
293
+ return `${text.slice(0, maxLength - 3)}...`;
294
+ }
295
+ var getDisplayWidth = stringWidth;
296
+
297
+ // src/data/otherSessions.ts
298
+ var MAX_LINES_TO_SCAN = 100;
299
+ function getProjectsDir() {
300
+ return join3(homedir2(), ".claude", "projects");
301
+ }
302
+ function decodeProjectPath(encoded) {
303
+ const naiveDecoded = encoded.replace(/-/g, sep);
304
+ if (existsSync3(naiveDecoded)) {
305
+ return naiveDecoded;
306
+ }
307
+ const segments = encoded.split("-").filter(Boolean);
308
+ if (segments.length === 0) {
309
+ return naiveDecoded;
310
+ }
311
+ let currentPath = "";
312
+ let i = 0;
313
+ while (i < segments.length) {
314
+ let found = false;
315
+ for (let j = segments.length; j > i; j--) {
316
+ const segment = segments.slice(i, j).join("-");
317
+ const testPath = currentPath ? join3(currentPath, segment) : sep + segment;
318
+ try {
319
+ if (existsSync3(testPath)) {
320
+ const stat = statSync(testPath);
321
+ if (stat.isDirectory()) {
322
+ currentPath = testPath;
323
+ i = j;
324
+ found = true;
325
+ break;
326
+ }
327
+ }
328
+ } catch {
329
+ }
330
+ }
331
+ if (!found) {
332
+ currentPath = currentPath ? join3(currentPath, segments[i]) : sep + segments[i];
333
+ i++;
334
+ }
335
+ }
336
+ return currentPath || naiveDecoded;
337
+ }
338
+ function getAllProjects() {
339
+ const projectsDir = getProjectsDir();
340
+ if (!existsSync3(projectsDir)) {
341
+ return [];
342
+ }
343
+ const entries = readdirSync(projectsDir);
344
+ const projects = [];
345
+ for (const entry of entries) {
346
+ const fullPath = join3(projectsDir, entry);
347
+ try {
348
+ const stat = statSync(fullPath);
349
+ if (stat.isDirectory()) {
350
+ projects.push({
351
+ encodedPath: entry,
352
+ decodedPath: decodeProjectPath(entry)
353
+ });
354
+ }
355
+ } catch {
356
+ }
357
+ }
358
+ return projects;
359
+ }
360
+ function parseLastAssistantMessage(sessionFile) {
361
+ if (!existsSync3(sessionFile)) {
362
+ return null;
363
+ }
364
+ let content;
365
+ try {
366
+ content = readFileSync4(sessionFile, "utf-8");
367
+ } catch {
368
+ return null;
369
+ }
370
+ const lines = content.trim().split("\n").filter(Boolean);
371
+ if (lines.length === 0) {
372
+ return null;
373
+ }
374
+ const recentLines = lines.slice(-MAX_LINES_TO_SCAN).reverse();
375
+ for (const line of recentLines) {
376
+ try {
377
+ const entry = JSON.parse(line);
378
+ if (entry.type === "assistant") {
379
+ const assistantEntry = entry;
380
+ const content2 = assistantEntry.message?.content;
381
+ if (Array.isArray(content2)) {
382
+ const textBlock = content2.find((c) => c.type === "text" && c.text);
383
+ if (textBlock?.text) {
384
+ return textBlock.text.replace(/\n/g, " ");
385
+ }
386
+ }
387
+ }
388
+ } catch {
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+ function formatRelativeTime(date) {
394
+ const elapsed = Date.now() - date.getTime();
395
+ const seconds = Math.floor(elapsed / 1e3);
396
+ const minutes = Math.floor(seconds / 60);
397
+ const hours = Math.floor(minutes / 60);
398
+ const days = Math.floor(hours / 24);
399
+ if (seconds < 1) {
400
+ return "just now";
401
+ }
402
+ if (seconds < 60) {
403
+ return `${seconds}s ago`;
404
+ }
405
+ if (minutes < 60) {
406
+ return `${minutes}m ago`;
407
+ }
408
+ if (hours < 24) {
409
+ return `${hours}h ago`;
410
+ }
411
+ return `${days}d ago`;
412
+ }
413
+ function findMostRecentSession(projectDir) {
414
+ if (!existsSync3(projectDir)) {
415
+ return null;
416
+ }
417
+ let files;
418
+ try {
419
+ files = readdirSync(projectDir);
420
+ } catch {
421
+ return null;
422
+ }
423
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
424
+ if (jsonlFiles.length === 0) {
425
+ return null;
426
+ }
427
+ let latestFile = null;
428
+ let latestMtime = 0;
429
+ for (const file of jsonlFiles) {
430
+ const filePath = join3(projectDir, file);
431
+ try {
432
+ const stat = statSync(filePath);
433
+ if (stat.mtimeMs && stat.mtimeMs > latestMtime) {
434
+ latestMtime = stat.mtimeMs;
435
+ latestFile = filePath;
436
+ }
437
+ } catch {
438
+ }
439
+ }
440
+ if (!latestFile) {
441
+ return null;
442
+ }
443
+ return { file: latestFile, mtimeMs: latestMtime };
444
+ }
445
+ function getOtherSessionsData(currentProjectPath, options = {}) {
446
+ const activeThresholdMs = options.activeThresholdMs ?? FIVE_MINUTES_MS;
447
+ const projectsDir = getProjectsDir();
448
+ const defaultResult = {
449
+ totalProjects: 0,
450
+ activeCount: 0,
451
+ projectNames: [],
452
+ recentSession: null,
453
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
454
+ };
455
+ if (!existsSync3(projectsDir)) {
456
+ return defaultResult;
457
+ }
458
+ const allProjects = getAllProjects();
459
+ defaultResult.totalProjects = allProjects.length;
460
+ const normalizedCurrentPath = currentProjectPath.replace(/[/\\]$/, "").replace(/\\/g, "/");
461
+ const otherSessions = [];
462
+ for (const project of allProjects) {
463
+ const normalizedDecodedPath = project.decodedPath.replace(/\\/g, "/");
464
+ if (normalizedDecodedPath === normalizedCurrentPath) {
465
+ continue;
466
+ }
467
+ const projectDir = join3(projectsDir, project.encodedPath);
468
+ const sessionInfo = findMostRecentSession(projectDir);
469
+ if (sessionInfo) {
470
+ otherSessions.push({
471
+ projectPath: project.decodedPath,
472
+ projectName: basename(project.decodedPath),
473
+ lastModified: new Date(sessionInfo.mtimeMs),
474
+ mtimeMs: sessionInfo.mtimeMs,
475
+ sessionFile: sessionInfo.file
476
+ });
477
+ }
478
+ }
479
+ const now = Date.now();
480
+ let activeCount = 0;
481
+ for (const session of otherSessions) {
482
+ if (now - session.mtimeMs < activeThresholdMs) {
483
+ activeCount++;
484
+ }
485
+ }
486
+ defaultResult.activeCount = activeCount;
487
+ if (otherSessions.length === 0) {
488
+ return defaultResult;
489
+ }
490
+ otherSessions.sort((a, b) => b.mtimeMs - a.mtimeMs);
491
+ const seen = /* @__PURE__ */ new Set();
492
+ defaultResult.projectNames = otherSessions.map((s) => s.projectName).filter((name) => {
493
+ if (seen.has(name)) return false;
494
+ seen.add(name);
495
+ return true;
496
+ });
497
+ const mostRecent = otherSessions[0];
498
+ const lastMessage = parseLastAssistantMessage(mostRecent.sessionFile);
499
+ const isActive = now - mostRecent.mtimeMs < activeThresholdMs;
500
+ defaultResult.recentSession = {
501
+ projectPath: mostRecent.projectPath,
502
+ projectName: mostRecent.projectName,
503
+ lastModified: mostRecent.lastModified,
504
+ lastMessage,
505
+ isActive,
506
+ relativeTime: formatRelativeTime(mostRecent.lastModified)
507
+ };
508
+ return defaultResult;
509
+ }
510
+
511
+ // src/data/sessionAvailability.ts
512
+ function shortenPath(path) {
513
+ const home = homedir3();
514
+ if (path === home) {
515
+ return "~";
516
+ }
517
+ if (path.startsWith(home + "/") || path.startsWith(home + "\\")) {
518
+ return "~" + path.slice(home.length);
519
+ }
520
+ return path;
521
+ }
522
+ var PROJECT_INDICATORS = [
523
+ ".git",
524
+ // Git repository
525
+ "package.json",
526
+ // Node.js
527
+ "Cargo.toml",
528
+ // Rust
529
+ "pyproject.toml",
530
+ // Python (modern)
531
+ "setup.py",
532
+ // Python (legacy)
533
+ "go.mod",
534
+ // Go
535
+ "Makefile",
536
+ // Make-based projects
537
+ "CMakeLists.txt",
538
+ // CMake projects
539
+ "pom.xml",
540
+ // Java Maven
541
+ "build.gradle",
542
+ // Java Gradle
543
+ "Gemfile",
544
+ // Ruby
545
+ "composer.json"
546
+ // PHP
547
+ ];
548
+ function isDevProject(projectPath) {
549
+ for (const indicator of PROJECT_INDICATORS) {
550
+ if (existsSync4(join4(projectPath, indicator))) {
551
+ return true;
552
+ }
553
+ }
554
+ return false;
555
+ }
556
+ function getSessionPath(projectPath) {
557
+ const encoded = projectPath.replace(/[/\\]/g, "-");
558
+ return join4(homedir3(), ".claude", "projects", encoded);
559
+ }
560
+ function getProjectMostRecentMtime(encodedPath) {
561
+ const projectDir = join4(homedir3(), ".claude", "projects", encodedPath);
562
+ if (!existsSync4(projectDir)) {
563
+ return 0;
564
+ }
565
+ let files;
566
+ try {
567
+ files = readdirSync2(projectDir);
568
+ } catch {
569
+ return 0;
570
+ }
571
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
572
+ if (jsonlFiles.length === 0) {
573
+ return 0;
574
+ }
575
+ let latestMtime = 0;
576
+ for (const file of jsonlFiles) {
577
+ const filePath = join4(projectDir, file);
578
+ try {
579
+ const stat = statSync2(filePath);
580
+ if (stat.mtimeMs && stat.mtimeMs > latestMtime) {
581
+ latestMtime = stat.mtimeMs;
582
+ }
583
+ } catch {
584
+ }
585
+ }
586
+ return latestMtime;
587
+ }
588
+ function hasCurrentProjectSession(cwd) {
589
+ const sessionPath = getSessionPath(cwd);
590
+ return existsSync4(sessionPath);
591
+ }
592
+ function getProjectsWithSessions(currentPath) {
593
+ const allProjects = getAllProjects();
594
+ const currentEncoded = currentPath.replace(/[/\\]/g, "-");
595
+ const projectsWithMtime = allProjects.filter((p) => p.encodedPath !== currentEncoded).filter((p) => existsSync4(p.decodedPath)).filter((p) => isDevProject(p.decodedPath)).map((p) => ({
596
+ name: basename2(p.decodedPath),
597
+ path: p.decodedPath,
598
+ mtime: getProjectMostRecentMtime(p.encodedPath)
599
+ }));
600
+ projectsWithMtime.sort((a, b) => b.mtime - a.mtime);
601
+ return projectsWithMtime.map(({ name, path }) => ({ name, path }));
602
+ }
603
+ function checkSessionAvailability(cwd) {
604
+ const hasCurrentSession = hasCurrentProjectSession(cwd);
605
+ if (hasCurrentSession) {
606
+ return {
607
+ hasCurrentSession: true,
608
+ otherProjects: []
609
+ };
610
+ }
611
+ const otherProjects = getProjectsWithSessions(cwd);
612
+ return {
613
+ hasCurrentSession: false,
614
+ otherProjects
615
+ };
616
+ }
617
+
618
+ // src/ui/App.tsx
619
+ import { Box as Box8, Text as Text8, useApp, useInput, useStdout } from "ink";
620
+ import React, {
621
+ useCallback as useCallback4,
622
+ useEffect as useEffect4,
623
+ useMemo as useMemo3,
624
+ useRef as useRef3,
625
+ useState as useState4
626
+ } from "react";
627
+
628
+ // src/config/parser.ts
629
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
630
+ import { parse as parseYaml } from "yaml";
631
+ var DEFAULT_WIDTH = DEFAULT_PANEL_WIDTH;
632
+ var MIN_WIDTH = MIN_TERMINAL_WIDTH;
633
+ var MAX_WIDTH = MAX_TERMINAL_WIDTH;
634
+ var CONFIG_PATH = ".agenthud/config.yaml";
635
+ function parseInterval(interval) {
636
+ if (!interval || interval === "manual") {
637
+ return null;
638
+ }
639
+ const match = interval.match(/^(\d+)(s|m)$/);
640
+ if (!match) {
641
+ return null;
642
+ }
643
+ const value = parseInt(match[1], 10);
644
+ const unit = match[2];
645
+ if (unit === "s") {
646
+ return value * 1e3;
647
+ } else if (unit === "m") {
648
+ return value * 60 * 1e3;
649
+ }
650
+ return null;
651
+ }
652
+ function getDefaultConfig2() {
653
+ return {
654
+ panels: {
655
+ project: {
656
+ enabled: true,
657
+ interval: 3e5
658
+ // 5 minutes (doesn't change often)
659
+ },
660
+ git: {
661
+ enabled: true,
662
+ interval: 3e4
663
+ // 30s
664
+ },
665
+ tests: {
666
+ enabled: true,
667
+ interval: null
668
+ // manual
669
+ },
670
+ claude: {
671
+ enabled: true,
672
+ interval: 1e4,
673
+ // 10 seconds default
674
+ sessionTimeout: 60 * 60 * 1e3
675
+ // 60 minutes default
676
+ },
677
+ other_sessions: {
678
+ enabled: true,
679
+ interval: 1e4,
680
+ // 10 seconds default
681
+ activeThreshold: 5 * 60 * 1e3,
682
+ // 5 minutes
683
+ messageMaxLength: 50
684
+ }
685
+ },
686
+ panelOrder: ["project", "git", "tests", "claude", "other_sessions"],
687
+ width: DEFAULT_WIDTH,
688
+ wideLayoutThreshold: null
689
+ // disabled by default
690
+ };
691
+ }
692
+ var BUILTIN_PANELS = ["project", "git", "tests", "claude", "other_sessions"];
693
+ var VALID_RENDERERS = ["list", "progress", "status"];
694
+ function parseBasePanelConfig(panelConfig, targetConfig, panelName, warnings) {
695
+ if (typeof panelConfig.enabled === "boolean") {
696
+ targetConfig.enabled = panelConfig.enabled;
697
+ }
698
+ if (typeof panelConfig.interval === "string") {
699
+ const interval = parseInterval(panelConfig.interval);
700
+ if (interval === null && panelConfig.interval !== "manual") {
701
+ warnings.push(
702
+ `Invalid interval '${panelConfig.interval}' for ${panelName} panel, using default`
703
+ );
704
+ } else {
705
+ targetConfig.interval = interval;
706
+ }
707
+ }
708
+ }
709
+ function parseConfig() {
710
+ const warnings = [];
711
+ const defaultConfig = getDefaultConfig2();
712
+ if (!existsSync5(CONFIG_PATH)) {
713
+ return { config: defaultConfig, warnings };
714
+ }
715
+ let rawConfig;
716
+ try {
717
+ const content = readFileSync5(CONFIG_PATH, "utf-8");
718
+ rawConfig = parseYaml(content);
719
+ } catch (error) {
720
+ const message = error instanceof Error ? error.message : String(error);
721
+ warnings.push(`Failed to parse config: ${message}`);
722
+ return { config: defaultConfig, warnings };
723
+ }
724
+ if (!rawConfig || typeof rawConfig !== "object") {
725
+ return { config: defaultConfig, warnings };
726
+ }
727
+ const parsed = rawConfig;
728
+ const config = getDefaultConfig2();
729
+ if (typeof parsed.width === "number") {
730
+ if (parsed.width < MIN_WIDTH) {
731
+ warnings.push(
732
+ `Width ${parsed.width} is too small, using minimum of ${MIN_WIDTH}`
733
+ );
734
+ config.width = MIN_WIDTH;
735
+ } else if (parsed.width > MAX_WIDTH) {
736
+ warnings.push(
737
+ `Width ${parsed.width} is too large, using maximum of ${MAX_WIDTH}`
738
+ );
739
+ config.width = MAX_WIDTH;
740
+ } else {
741
+ config.width = parsed.width;
742
+ }
743
+ }
744
+ const MIN_WIDE_THRESHOLD = 140;
745
+ const wideThresholdValue = parsed.wideLayoutThreshold ?? parsed.wide_layout_threshold;
746
+ if (typeof wideThresholdValue === "number") {
747
+ if (wideThresholdValue < MIN_WIDE_THRESHOLD) {
748
+ warnings.push(
749
+ `wideLayoutThreshold ${wideThresholdValue} is too small, using minimum of ${MIN_WIDE_THRESHOLD}`
750
+ );
751
+ config.wideLayoutThreshold = MIN_WIDE_THRESHOLD;
752
+ } else {
753
+ config.wideLayoutThreshold = wideThresholdValue;
754
+ }
755
+ }
756
+ const panels = parsed.panels;
757
+ if (!panels || typeof panels !== "object") {
758
+ return { config, warnings };
759
+ }
760
+ const customPanels = {};
761
+ const panelOrder = [];
762
+ for (const panelName of Object.keys(panels)) {
763
+ panelOrder.push(panelName);
764
+ const panelConfig = panels[panelName];
765
+ if (!panelConfig || typeof panelConfig !== "object") {
766
+ continue;
767
+ }
768
+ if (panelName === "project") {
769
+ parseBasePanelConfig(
770
+ panelConfig,
771
+ config.panels.project,
772
+ panelName,
773
+ warnings
774
+ );
775
+ continue;
776
+ }
777
+ if (panelName === "git") {
778
+ parseBasePanelConfig(panelConfig, config.panels.git, panelName, warnings);
779
+ continue;
780
+ }
781
+ if (panelName === "tests") {
782
+ parseBasePanelConfig(
783
+ panelConfig,
784
+ config.panels.tests,
785
+ panelName,
786
+ warnings
787
+ );
788
+ if (typeof panelConfig.command === "string") {
789
+ config.panels.tests.command = panelConfig.command;
790
+ }
791
+ continue;
792
+ }
793
+ if (panelName === "claude") {
794
+ parseBasePanelConfig(
795
+ panelConfig,
796
+ config.panels.claude,
797
+ panelName,
798
+ warnings
799
+ );
800
+ if (typeof panelConfig.max_activities === "number") {
801
+ config.panels.claude.maxActivities = panelConfig.max_activities;
802
+ }
803
+ if (typeof panelConfig.session_timeout === "string") {
804
+ const timeout = parseInterval(panelConfig.session_timeout);
805
+ if (timeout !== null) {
806
+ config.panels.claude.sessionTimeout = timeout;
807
+ }
808
+ }
809
+ continue;
810
+ }
811
+ if (panelName === "other_sessions") {
812
+ parseBasePanelConfig(
813
+ panelConfig,
814
+ config.panels.other_sessions,
815
+ panelName,
816
+ warnings
817
+ );
818
+ if (typeof panelConfig.active_threshold === "string") {
819
+ const threshold = parseInterval(panelConfig.active_threshold);
820
+ if (threshold !== null) {
821
+ config.panels.other_sessions.activeThreshold = threshold;
822
+ }
823
+ }
824
+ if (typeof panelConfig.message_max_length === "number") {
825
+ config.panels.other_sessions.messageMaxLength = panelConfig.message_max_length;
826
+ }
827
+ continue;
828
+ }
829
+ const customPanel = {
830
+ enabled: typeof panelConfig.enabled === "boolean" ? panelConfig.enabled : true,
831
+ interval: 3e4,
832
+ // default 30s
833
+ renderer: "list"
834
+ // default
835
+ };
836
+ if (typeof panelConfig.interval === "string") {
837
+ const interval = parseInterval(panelConfig.interval);
838
+ customPanel.interval = interval;
839
+ }
840
+ if (typeof panelConfig.command === "string") {
841
+ customPanel.command = panelConfig.command;
842
+ }
843
+ if (typeof panelConfig.source === "string") {
844
+ customPanel.source = panelConfig.source;
845
+ }
846
+ if (typeof panelConfig.renderer === "string") {
847
+ if (VALID_RENDERERS.includes(panelConfig.renderer)) {
848
+ customPanel.renderer = panelConfig.renderer;
849
+ } else {
850
+ warnings.push(
851
+ `Invalid renderer '${panelConfig.renderer}' for custom panel, using 'list'`
852
+ );
853
+ }
854
+ }
855
+ customPanels[panelName] = customPanel;
856
+ }
857
+ if (Object.keys(customPanels).length > 0) {
858
+ config.customPanels = customPanels;
859
+ }
860
+ for (const builtIn of BUILTIN_PANELS) {
861
+ if (!panelOrder.includes(builtIn)) {
862
+ panelOrder.push(builtIn);
863
+ }
864
+ }
865
+ config.panelOrder = panelOrder;
866
+ return { config, warnings };
867
+ }
868
+
869
+ // src/data/claude.ts
870
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
871
+ import { homedir as homedir4 } from "os";
872
+ import { basename as basename3, join as join5 } from "path";
873
+
874
+ // src/types/index.ts
875
+ var ICONS = {
876
+ // Activity types
877
+ User: ">",
878
+ Response: "<",
879
+ // Tools
880
+ Edit: "~",
881
+ Write: "~",
882
+ Read: "\u25CB",
883
+ Bash: "$",
884
+ Glob: "*",
885
+ Grep: "*",
886
+ WebFetch: "@",
887
+ WebSearch: "@",
888
+ Task: "\xBB",
889
+ TodoWrite: "~",
890
+ AskUserQuestion: "?",
891
+ // Fallback
892
+ Default: "$"
893
+ };
894
+
895
+ // src/data/claude.ts
896
+ function stripAnsi(text) {
897
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
898
+ }
899
+ var MAX_LINES_TO_SCAN2 = 200;
900
+ var DEFAULT_MAX_ACTIVITIES = 10;
901
+ function getClaudeSessionPath2(projectPath) {
902
+ const encoded = projectPath.replace(/[/\\]/g, "-");
903
+ return join5(homedir4(), ".claude", "projects", encoded);
904
+ }
905
+ function findActiveSession(sessionDir, sessionTimeout) {
906
+ if (!existsSync6(sessionDir)) {
907
+ return null;
908
+ }
909
+ const files = readdirSync3(sessionDir);
910
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
911
+ if (jsonlFiles.length === 0) {
912
+ return null;
913
+ }
914
+ let latestFile = null;
915
+ let latestMtime = 0;
916
+ let latestSize = 0;
917
+ for (const file of jsonlFiles) {
918
+ const filePath = join5(sessionDir, file);
919
+ const stat = statSync3(filePath);
920
+ if (stat.mtimeMs > latestMtime || stat.mtimeMs === latestMtime && stat.size > latestSize) {
921
+ latestMtime = stat.mtimeMs;
922
+ latestSize = stat.size;
923
+ latestFile = file;
924
+ }
925
+ }
926
+ const cutoff = Date.now() - sessionTimeout;
927
+ if (latestMtime > cutoff && latestFile) {
928
+ return join5(sessionDir, latestFile);
929
+ }
930
+ return null;
931
+ }
932
+ function getToolDetail(_toolName, input) {
933
+ if (!input) return "";
934
+ if (input.command) {
935
+ return stripAnsi(input.command.replace(/\n/g, " "));
936
+ }
937
+ if (input.file_path) {
938
+ return basename3(input.file_path);
939
+ }
940
+ if (input.pattern) {
941
+ return stripAnsi(input.pattern);
942
+ }
943
+ if (input.query) {
944
+ return stripAnsi(input.query);
945
+ }
946
+ if (input.description) {
947
+ return stripAnsi(input.description);
948
+ }
949
+ return "";
950
+ }
951
+ var MAX_SUB_ACTIVITIES = 3;
952
+ function getSubagentFiles(sessionFile) {
953
+ const subagentsDir = join5(sessionFile.replace(/\.jsonl$/, ""), "subagents");
954
+ if (!existsSync6(subagentsDir)) {
955
+ return [];
956
+ }
957
+ try {
958
+ const files = readdirSync3(subagentsDir).filter(
959
+ (f) => f.endsWith(".jsonl")
960
+ );
961
+ const fileInfos = files.map((file) => {
962
+ const filePath = join5(subagentsDir, file);
963
+ const stat = statSync3(filePath);
964
+ return { filePath, mtimeMs: stat.mtimeMs };
965
+ });
966
+ fileInfos.sort((a, b) => b.mtimeMs - a.mtimeMs);
967
+ return fileInfos;
968
+ } catch {
969
+ return [];
970
+ }
971
+ }
972
+ function parseSubagentFile(filePath) {
973
+ try {
974
+ const content = readFileSync6(filePath, "utf-8");
975
+ const lines = content.trim().split("\n").filter(Boolean);
976
+ const allActivities = [];
977
+ for (const line of lines) {
978
+ try {
979
+ const entry = JSON.parse(line);
980
+ if (entry.type === "assistant" && entry.message?.content) {
981
+ const messageContent = entry.message.content;
982
+ if (Array.isArray(messageContent)) {
983
+ for (const block of messageContent) {
984
+ if (block.type === "tool_use" && block.name) {
985
+ const toolName = block.name;
986
+ if (toolName === "TodoWrite") continue;
987
+ const icon = ICONS[toolName] || ICONS.Default;
988
+ const detail = getToolDetail(toolName, block.input);
989
+ const timestamp = entry.timestamp ? new Date(entry.timestamp) : /* @__PURE__ */ new Date();
990
+ allActivities.push({
991
+ timestamp,
992
+ type: "tool",
993
+ icon,
994
+ label: toolName,
995
+ detail
996
+ });
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ allActivities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
1005
+ return {
1006
+ activities: allActivities.slice(0, MAX_SUB_ACTIVITIES),
1007
+ totalCount: allActivities.length
1008
+ };
1009
+ } catch {
1010
+ return { activities: [], totalCount: 0 };
1011
+ }
1012
+ }
1013
+ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES) {
1014
+ const defaultState = {
1015
+ status: "none",
1016
+ activities: [],
1017
+ tokenCount: 0,
1018
+ sessionStartTime: null,
1019
+ todos: null
1020
+ };
1021
+ if (!existsSync6(sessionFile)) {
1022
+ return defaultState;
1023
+ }
1024
+ let content;
1025
+ try {
1026
+ content = readFileSync6(sessionFile, "utf-8");
1027
+ } catch {
1028
+ return defaultState;
1029
+ }
1030
+ const lines = content.trim().split("\n").filter(Boolean);
1031
+ if (lines.length === 0) {
1032
+ return defaultState;
1033
+ }
1034
+ let sessionStartTime = null;
1035
+ for (let i = 0; i < Math.min(50, lines.length); i++) {
1036
+ try {
1037
+ const entry = JSON.parse(lines[i]);
1038
+ if (entry.timestamp && typeof entry.timestamp === "string") {
1039
+ sessionStartTime = new Date(entry.timestamp);
1040
+ break;
1041
+ }
1042
+ } catch {
1043
+ }
1044
+ }
1045
+ const activities = [];
1046
+ let tokenCount = 0;
1047
+ let lastTimestamp = null;
1048
+ let lastType = null;
1049
+ let todos = null;
1050
+ const recentLines = lines.slice(-MAX_LINES_TO_SCAN2);
1051
+ for (const line of recentLines) {
1052
+ try {
1053
+ const entry = JSON.parse(line);
1054
+ if (entry.type === "user") {
1055
+ const userEntry = entry;
1056
+ if (userEntry.timestamp) {
1057
+ lastTimestamp = new Date(userEntry.timestamp);
1058
+ }
1059
+ const msgContent = userEntry.message?.content;
1060
+ let userText = "";
1061
+ if (typeof msgContent === "string") {
1062
+ userText = msgContent;
1063
+ } else if (Array.isArray(msgContent)) {
1064
+ const textBlock = msgContent.find(
1065
+ (c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string"
1066
+ );
1067
+ if (textBlock) {
1068
+ userText = textBlock.text;
1069
+ }
1070
+ }
1071
+ if (userText) {
1072
+ activities.push({
1073
+ timestamp: lastTimestamp || /* @__PURE__ */ new Date(),
1074
+ type: "user",
1075
+ icon: ICONS.User,
1076
+ label: "User",
1077
+ detail: userText.replace(/\n/g, " ")
1078
+ });
1079
+ }
1080
+ if (userEntry.toolUseResult?.newTodos) {
1081
+ todos = userEntry.toolUseResult.newTodos.map((t) => ({
1082
+ content: t.content,
1083
+ status: t.status,
1084
+ activeForm: t.activeForm
1085
+ }));
1086
+ }
1087
+ lastType = "user";
1088
+ }
1089
+ if (entry.type === "assistant") {
1090
+ const assistantEntry = entry;
1091
+ if (assistantEntry.timestamp) {
1092
+ lastTimestamp = new Date(assistantEntry.timestamp);
1093
+ }
1094
+ const messageContent = assistantEntry.message?.content;
1095
+ if (Array.isArray(messageContent)) {
1096
+ for (const block of messageContent) {
1097
+ if (block.type === "tool_use") {
1098
+ const toolName = block.name || "Tool";
1099
+ if (toolName === "TodoWrite") {
1100
+ lastType = "tool";
1101
+ continue;
1102
+ }
1103
+ const icon = ICONS[toolName] || ICONS.Default;
1104
+ const detail = getToolDetail(toolName, block.input);
1105
+ const lastActivity = activities[activities.length - 1];
1106
+ if (lastActivity && lastActivity.type === "tool" && lastActivity.label === toolName && lastActivity.detail === detail) {
1107
+ lastActivity.count = (lastActivity.count || 1) + 1;
1108
+ lastActivity.timestamp = lastTimestamp || /* @__PURE__ */ new Date();
1109
+ } else {
1110
+ const activity = {
1111
+ timestamp: lastTimestamp || /* @__PURE__ */ new Date(),
1112
+ type: "tool",
1113
+ icon,
1114
+ label: toolName,
1115
+ detail
1116
+ };
1117
+ activities.push(activity);
1118
+ }
1119
+ lastType = "tool";
1120
+ } else if (block.type === "text" && block.text) {
1121
+ if (block.text.length > 10) {
1122
+ activities.push({
1123
+ timestamp: lastTimestamp || /* @__PURE__ */ new Date(),
1124
+ type: "response",
1125
+ icon: ICONS.Response,
1126
+ label: "Response",
1127
+ detail: block.text.replace(/\n/g, " ")
1128
+ });
1129
+ lastType = "response";
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ const usage = assistantEntry.message?.usage;
1135
+ if (usage) {
1136
+ tokenCount += (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.output_tokens || 0);
1137
+ }
1138
+ }
1139
+ if (entry.type === "system") {
1140
+ const systemEntry = entry;
1141
+ if (systemEntry.subtype === "stop_hook_summary") {
1142
+ lastType = "stop";
1143
+ if (systemEntry.timestamp) {
1144
+ lastTimestamp = new Date(systemEntry.timestamp);
1145
+ }
1146
+ }
1147
+ }
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ let status = "none";
1152
+ if (lastTimestamp) {
1153
+ const elapsed = Date.now() - lastTimestamp.getTime();
1154
+ if (elapsed < THIRTY_SECONDS_MS) {
1155
+ if (lastType === "stop" || lastType === "response") {
1156
+ status = "completed";
1157
+ } else {
1158
+ status = "running";
1159
+ }
1160
+ } else {
1161
+ status = "completed";
1162
+ }
1163
+ }
1164
+ const subagentsDir = join5(sessionFile.replace(/\.jsonl$/, ""), "subagents");
1165
+ if (existsSync6(subagentsDir)) {
1166
+ try {
1167
+ const subagentFiles2 = readdirSync3(subagentsDir).filter(
1168
+ (f) => f.endsWith(".jsonl")
1169
+ );
1170
+ for (const file of subagentFiles2) {
1171
+ const filePath = join5(subagentsDir, file);
1172
+ try {
1173
+ const subContent = readFileSync6(filePath, "utf-8");
1174
+ const subLines = subContent.trim().split("\n").filter(Boolean);
1175
+ for (const line of subLines) {
1176
+ try {
1177
+ const entry = JSON.parse(line);
1178
+ if (entry.type === "assistant" && entry.message?.usage) {
1179
+ const usage = entry.message.usage;
1180
+ tokenCount += (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.output_tokens || 0);
1181
+ }
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ } catch {
1189
+ }
1190
+ }
1191
+ const finalActivities = activities.slice(-maxActivities).reverse();
1192
+ const subagentFiles = getSubagentFiles(sessionFile);
1193
+ let taskIndex = 0;
1194
+ for (const activity of finalActivities) {
1195
+ if (activity.label === "Task" && taskIndex < subagentFiles.length) {
1196
+ const subagentData = parseSubagentFile(subagentFiles[taskIndex].filePath);
1197
+ if (subagentData.totalCount > 0) {
1198
+ activity.subActivities = subagentData.activities;
1199
+ activity.subActivityCount = subagentData.totalCount;
1200
+ }
1201
+ taskIndex++;
1202
+ }
1203
+ }
1204
+ return {
1205
+ status,
1206
+ activities: finalActivities,
1207
+ tokenCount,
1208
+ sessionStartTime,
1209
+ todos
1210
+ };
1211
+ }
1212
+ var DEFAULT_SESSION_TIMEOUT = 60 * 60 * 1e3;
1213
+ function getClaudeData(projectPath, maxActivities, sessionTimeout = DEFAULT_SESSION_TIMEOUT) {
1214
+ const defaultState = {
1215
+ status: "none",
1216
+ activities: [],
1217
+ tokenCount: 0,
1218
+ sessionStartTime: null,
1219
+ todos: null
1220
+ };
1221
+ try {
1222
+ const sessionDir = getClaudeSessionPath2(projectPath);
1223
+ const hasSession = existsSync6(sessionDir);
1224
+ const sessionFile = findActiveSession(sessionDir, sessionTimeout);
1225
+ if (!sessionFile) {
1226
+ return {
1227
+ state: defaultState,
1228
+ hasSession,
1229
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1230
+ };
1231
+ }
1232
+ const state = parseSessionState(sessionFile, maxActivities);
1233
+ return {
1234
+ state,
1235
+ hasSession: true,
1236
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1237
+ };
1238
+ } catch (error) {
1239
+ const message = error instanceof Error ? error.message : String(error);
1240
+ return {
1241
+ state: defaultState,
1242
+ hasSession: false,
1243
+ error: message,
1244
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1245
+ };
1246
+ }
1247
+ }
1248
+
1249
+ // src/data/custom.ts
1250
+ import { exec, execSync } from "child_process";
1251
+ import { promises as fsPromises, readFileSync as readFileSync7 } from "fs";
1252
+ import { promisify } from "util";
1253
+ var execAsync = promisify(exec);
1254
+ function capitalizeFirst(str) {
1255
+ return str.charAt(0).toUpperCase() + str.slice(1);
1256
+ }
1257
+ function getCustomPanelData(name, panelConfig) {
1258
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1259
+ const defaultData = {
1260
+ title: capitalizeFirst(name)
1261
+ };
1262
+ if (panelConfig.command) {
1263
+ try {
1264
+ const output = execSync(panelConfig.command, { encoding: "utf-8" }).trim();
1265
+ try {
1266
+ const parsed = JSON.parse(output);
1267
+ return {
1268
+ data: {
1269
+ title: parsed.title || capitalizeFirst(name),
1270
+ summary: parsed.summary,
1271
+ items: parsed.items,
1272
+ progress: parsed.progress,
1273
+ stats: parsed.stats
1274
+ },
1275
+ timestamp
1276
+ };
1277
+ } catch {
1278
+ const lines = output.split("\n").filter((l) => l.trim());
1279
+ return {
1280
+ data: {
1281
+ title: capitalizeFirst(name),
1282
+ items: lines.map((text) => ({ text }))
1283
+ },
1284
+ timestamp
1285
+ };
1286
+ }
1287
+ } catch (error) {
1288
+ const message = error instanceof Error ? error.message : String(error);
1289
+ return {
1290
+ data: defaultData,
1291
+ error: `Command failed: ${message.split("\n")[0]}`,
1292
+ timestamp
1293
+ };
1294
+ }
1295
+ }
1296
+ if (panelConfig.source) {
1297
+ try {
1298
+ const content = readFileSync7(panelConfig.source, "utf-8");
1299
+ const parsed = JSON.parse(content);
1300
+ return {
1301
+ data: {
1302
+ title: parsed.title || capitalizeFirst(name),
1303
+ summary: parsed.summary,
1304
+ items: parsed.items,
1305
+ progress: parsed.progress,
1306
+ stats: parsed.stats
1307
+ },
1308
+ timestamp
1309
+ };
1310
+ } catch (error) {
1311
+ const message = error instanceof Error ? error.message : String(error);
1312
+ if (message.includes("ENOENT")) {
1313
+ return {
1314
+ data: defaultData,
1315
+ error: "File not found",
1316
+ timestamp
1317
+ };
1318
+ }
1319
+ return {
1320
+ data: defaultData,
1321
+ error: "Invalid JSON",
1322
+ timestamp
1323
+ };
1324
+ }
1325
+ }
1326
+ return {
1327
+ data: defaultData,
1328
+ error: "No command or source configured",
1329
+ timestamp
1330
+ };
1331
+ }
1332
+ async function getCustomPanelDataAsync(name, panelConfig) {
1333
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1334
+ const defaultData = {
1335
+ title: capitalizeFirst(name)
1336
+ };
1337
+ if (panelConfig.command) {
1338
+ try {
1339
+ const { stdout } = await execAsync(panelConfig.command);
1340
+ const output = stdout.trim();
1341
+ try {
1342
+ const parsed = JSON.parse(output);
1343
+ return {
1344
+ data: {
1345
+ title: parsed.title || capitalizeFirst(name),
1346
+ summary: parsed.summary,
1347
+ items: parsed.items,
1348
+ progress: parsed.progress,
1349
+ stats: parsed.stats
1350
+ },
1351
+ timestamp
1352
+ };
1353
+ } catch {
1354
+ const lines = output.split("\n").filter((l) => l.trim());
1355
+ return {
1356
+ data: {
1357
+ title: capitalizeFirst(name),
1358
+ items: lines.map((text) => ({ text }))
1359
+ },
1360
+ timestamp
1361
+ };
1362
+ }
1363
+ } catch (error) {
1364
+ const message = error instanceof Error ? error.message : String(error);
1365
+ return {
1366
+ data: defaultData,
1367
+ error: `Command failed: ${message.split("\n")[0]}`,
1368
+ timestamp
1369
+ };
1370
+ }
1371
+ }
1372
+ if (panelConfig.source) {
1373
+ try {
1374
+ const content = await fsPromises.readFile(panelConfig.source, "utf-8");
1375
+ const parsed = JSON.parse(content);
1376
+ return {
1377
+ data: {
1378
+ title: parsed.title || capitalizeFirst(name),
1379
+ summary: parsed.summary,
1380
+ items: parsed.items,
1381
+ progress: parsed.progress,
1382
+ stats: parsed.stats
1383
+ },
1384
+ timestamp
1385
+ };
1386
+ } catch (error) {
1387
+ const message = error instanceof Error ? error.message : String(error);
1388
+ if (message.includes("ENOENT")) {
1389
+ return {
1390
+ data: defaultData,
1391
+ error: "File not found",
1392
+ timestamp
1393
+ };
1394
+ }
1395
+ return {
1396
+ data: defaultData,
1397
+ error: "Invalid JSON",
1398
+ timestamp
1399
+ };
1400
+ }
1401
+ }
1402
+ return {
1403
+ data: defaultData,
1404
+ error: "No command or source configured",
1405
+ timestamp
1406
+ };
1407
+ }
1408
+
1409
+ // src/data/git.ts
1410
+ import { exec as exec2, execSync as execSync2 } from "child_process";
1411
+ import { promisify as promisify2 } from "util";
1412
+ var execAsync2 = promisify2(exec2);
1413
+ function cleanOutput(str) {
1414
+ let result = str.replace(/^\uFEFF/, "");
1415
+ result = result.replace(/^"|"$/g, "");
1416
+ return result.trim();
1417
+ }
1418
+ function getUncommittedCount() {
1419
+ try {
1420
+ const result = execSync2("git status --porcelain", {
1421
+ encoding: "utf-8",
1422
+ stdio: ["pipe", "pipe", "pipe"]
1423
+ });
1424
+ const lines = result.trim().split("\n").filter(Boolean);
1425
+ return lines.length;
1426
+ } catch {
1427
+ return 0;
1428
+ }
1429
+ }
1430
+ var DEFAULT_COMMANDS = {
1431
+ branch: "git branch --show-current",
1432
+ commits: 'git log --since=midnight --format="%h|%aI|%s"',
1433
+ stats: 'git log --since=midnight --numstat --format=""'
1434
+ };
1435
+ function parseCommitsOutput(output) {
1436
+ const lines = cleanOutput(output).split("\n").filter(Boolean);
1437
+ return lines.map((line) => {
1438
+ const cleanLine = cleanOutput(line);
1439
+ const [hash, timestamp, ...messageParts] = cleanLine.split("|");
1440
+ return {
1441
+ hash: cleanOutput(hash),
1442
+ message: messageParts.join("|"),
1443
+ timestamp: new Date(timestamp)
1444
+ };
1445
+ });
1446
+ }
1447
+ function parseStatsOutput(output) {
1448
+ const lines = output.trim().split("\n").filter(Boolean);
1449
+ let added = 0;
1450
+ let deleted = 0;
1451
+ const filesSet = /* @__PURE__ */ new Set();
1452
+ for (const line of lines) {
1453
+ const [addedStr, deletedStr, filename] = line.split(" ");
1454
+ if (addedStr === "-" || deletedStr === "-") {
1455
+ if (filename) filesSet.add(filename);
1456
+ continue;
1457
+ }
1458
+ added += parseInt(addedStr, 10) || 0;
1459
+ deleted += parseInt(deletedStr, 10) || 0;
1460
+ if (filename) filesSet.add(filename);
1461
+ }
1462
+ return { added, deleted, files: filesSet.size };
1463
+ }
1464
+ function getGitData(config) {
1465
+ const commands = {
1466
+ branch: config.command?.branch || DEFAULT_COMMANDS.branch,
1467
+ commits: config.command?.commits || DEFAULT_COMMANDS.commits,
1468
+ stats: config.command?.stats || DEFAULT_COMMANDS.stats
1469
+ };
1470
+ let branch = null;
1471
+ try {
1472
+ const result = execSync2(commands.branch, {
1473
+ encoding: "utf-8",
1474
+ stdio: ["pipe", "pipe", "pipe"]
1475
+ });
1476
+ branch = result.trim();
1477
+ } catch {
1478
+ branch = null;
1479
+ }
1480
+ let commits = [];
1481
+ try {
1482
+ const result = execSync2(commands.commits, {
1483
+ encoding: "utf-8",
1484
+ stdio: ["pipe", "pipe", "pipe"]
1485
+ });
1486
+ commits = parseCommitsOutput(result);
1487
+ } catch {
1488
+ commits = [];
1489
+ }
1490
+ let stats = { added: 0, deleted: 0, files: 0 };
1491
+ try {
1492
+ const result = execSync2(commands.stats, {
1493
+ encoding: "utf-8",
1494
+ stdio: ["pipe", "pipe", "pipe"]
1495
+ });
1496
+ stats = parseStatsOutput(result);
1497
+ } catch {
1498
+ stats = { added: 0, deleted: 0, files: 0 };
1499
+ }
1500
+ const uncommitted = getUncommittedCount();
1501
+ return { branch, commits, stats, uncommitted };
1502
+ }
1503
+ async function getGitDataAsync(config) {
1504
+ const commands = {
1505
+ branch: config.command?.branch || DEFAULT_COMMANDS.branch,
1506
+ commits: config.command?.commits || DEFAULT_COMMANDS.commits,
1507
+ stats: config.command?.stats || DEFAULT_COMMANDS.stats
1508
+ };
1509
+ let branch = null;
1510
+ try {
1511
+ const { stdout } = await execAsync2(commands.branch);
1512
+ branch = stdout.trim();
1513
+ } catch {
1514
+ branch = null;
1515
+ }
1516
+ let commits = [];
1517
+ try {
1518
+ const { stdout } = await execAsync2(commands.commits);
1519
+ commits = parseCommitsOutput(stdout);
1520
+ } catch {
1521
+ commits = [];
1522
+ }
1523
+ let stats = { added: 0, deleted: 0, files: 0 };
1524
+ try {
1525
+ const { stdout } = await execAsync2(commands.stats);
1526
+ stats = parseStatsOutput(stdout);
1527
+ } catch {
1528
+ stats = { added: 0, deleted: 0, files: 0 };
1529
+ }
1530
+ const uncommitted = getUncommittedCount();
1531
+ return { branch, commits, stats, uncommitted };
1532
+ }
1533
+
1534
+ // src/data/project.ts
1535
+ import { execSync as execSync3 } from "child_process";
1536
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1537
+ import { basename as basename4 } from "path";
1538
+ var LANGUAGE_INDICATORS = [
1539
+ { file: "tsconfig.json", language: "TypeScript" },
1540
+ { file: "package.json", language: "JavaScript" },
1541
+ { file: "pyproject.toml", language: "Python" },
1542
+ { file: "requirements.txt", language: "Python" },
1543
+ { file: "setup.py", language: "Python" },
1544
+ { file: "go.mod", language: "Go" },
1545
+ { file: "Cargo.toml", language: "Rust" },
1546
+ { file: "Gemfile", language: "Ruby" },
1547
+ { file: "pom.xml", language: "Java" },
1548
+ { file: "build.gradle", language: "Java" }
1549
+ ];
1550
+ var KNOWN_STACK = {
1551
+ frameworks: [
1552
+ // JS/TS frameworks
1553
+ "react",
1554
+ "vue",
1555
+ "angular",
1556
+ "svelte",
1557
+ "next",
1558
+ "nuxt",
1559
+ "express",
1560
+ "fastify",
1561
+ "koa",
1562
+ "hono",
1563
+ "ink",
1564
+ // Python frameworks
1565
+ "django",
1566
+ "flask",
1567
+ "fastapi",
1568
+ "tornado",
1569
+ "pyramid"
1570
+ ],
1571
+ tools: [
1572
+ // JS/TS tools
1573
+ "vitest",
1574
+ "jest",
1575
+ "mocha",
1576
+ "webpack",
1577
+ "vite",
1578
+ "rollup",
1579
+ "esbuild",
1580
+ "tsup",
1581
+ "eslint",
1582
+ "prettier",
1583
+ // Python tools
1584
+ "pytest",
1585
+ "pandas",
1586
+ "numpy",
1587
+ "tensorflow",
1588
+ "pytorch",
1589
+ "scikit-learn",
1590
+ "sqlalchemy",
1591
+ "celery"
1592
+ ]
1593
+ };
1594
+ var FILE_EXTENSIONS = {
1595
+ TypeScript: { ext: "ts", patterns: ["*.ts", "*.tsx"] },
1596
+ JavaScript: { ext: "js", patterns: ["*.js", "*.jsx"] },
1597
+ Python: { ext: "py", patterns: ["*.py"] },
1598
+ Go: { ext: "go", patterns: ["*.go"] },
1599
+ Rust: { ext: "rs", patterns: ["*.rs"] },
1600
+ Ruby: { ext: "rb", patterns: ["*.rb"] },
1601
+ Java: { ext: "java", patterns: ["*.java"] }
1602
+ };
1603
+ var SOURCE_DIRS = ["src", "lib", "app"];
1604
+ var EXCLUDE_DIRS = [
1605
+ "node_modules",
1606
+ "dist",
1607
+ "build",
1608
+ ".git",
1609
+ "__pycache__",
1610
+ "venv",
1611
+ ".venv",
1612
+ "target"
1613
+ ];
1614
+ function detectLanguage() {
1615
+ for (const { file, language } of LANGUAGE_INDICATORS) {
1616
+ if (existsSync7(file)) {
1617
+ return language;
1618
+ }
1619
+ }
1620
+ return null;
1621
+ }
1622
+ function parsePackageJson(content) {
1623
+ const pkg = JSON.parse(content);
1624
+ const deps = Object.keys(pkg.dependencies || {});
1625
+ const devDeps = Object.keys(pkg.devDependencies || {});
1626
+ return {
1627
+ name: pkg.name || "unknown",
1628
+ license: pkg.license || null,
1629
+ prodDeps: deps.length,
1630
+ devDeps: devDeps.length,
1631
+ allDeps: [...deps, ...devDeps]
1632
+ };
1633
+ }
1634
+ function parsePyprojectToml(content) {
1635
+ const lines = content.split("\n");
1636
+ let name = "unknown";
1637
+ let license = null;
1638
+ const deps = [];
1639
+ const devDeps = [];
1640
+ let inProject = false;
1641
+ let inDeps = false;
1642
+ let inDevDeps = false;
1643
+ for (const line of lines) {
1644
+ const trimmed = line.trim();
1645
+ if (trimmed === "[project]") {
1646
+ inProject = true;
1647
+ inDeps = false;
1648
+ inDevDeps = false;
1649
+ continue;
1650
+ }
1651
+ if (trimmed.startsWith("[") && trimmed !== "[project]") {
1652
+ if (trimmed === "[project.optional-dependencies]") {
1653
+ inDevDeps = true;
1654
+ inDeps = false;
1655
+ } else {
1656
+ inProject = false;
1657
+ inDeps = false;
1658
+ inDevDeps = false;
1659
+ }
1660
+ continue;
1661
+ }
1662
+ if (inProject) {
1663
+ const nameMatch = trimmed.match(/^name\s*=\s*"([^"]+)"/);
1664
+ if (nameMatch) {
1665
+ name = nameMatch[1];
1666
+ }
1667
+ const licenseMatch = trimmed.match(
1668
+ /^license\s*=\s*\{text\s*=\s*"([^"]+)"/
1669
+ );
1670
+ if (licenseMatch) {
1671
+ license = licenseMatch[1];
1672
+ }
1673
+ const simpleLicense = trimmed.match(/^license\s*=\s*"([^"]+)"/);
1674
+ if (simpleLicense) {
1675
+ license = simpleLicense[1];
1676
+ }
1677
+ if (trimmed.startsWith("dependencies")) {
1678
+ inDeps = true;
1679
+ const inlineMatch = trimmed.match(/dependencies\s*=\s*\[([^\]]*)\]/);
1680
+ if (inlineMatch) {
1681
+ const items = inlineMatch[1].match(/"([^"]+)"/g);
1682
+ if (items) {
1683
+ deps.push(
1684
+ ...items.map((s) => s.replace(/"/g, "").split(/[<>=[]/)[0])
1685
+ );
1686
+ }
1687
+ inDeps = false;
1688
+ }
1689
+ continue;
1690
+ }
1691
+ }
1692
+ if (inDeps && trimmed.startsWith('"')) {
1693
+ const depMatch = trimmed.match(/"([^"]+)"/);
1694
+ if (depMatch) {
1695
+ deps.push(depMatch[1].split(/[<>=[]/)[0]);
1696
+ }
1697
+ if (trimmed.endsWith("]")) {
1698
+ inDeps = false;
1699
+ }
1700
+ }
1701
+ if (inDevDeps && trimmed.startsWith('"')) {
1702
+ const depMatch = trimmed.match(/"([^"]+)"/);
1703
+ if (depMatch) {
1704
+ devDeps.push(depMatch[1].split(/[<>=[]/)[0]);
1705
+ }
1706
+ }
1707
+ if (inDevDeps && trimmed.match(/^dev\s*=\s*\[/)) {
1708
+ const inlineMatch = trimmed.match(/dev\s*=\s*\[([^\]]*)\]/);
1709
+ if (inlineMatch) {
1710
+ const items = inlineMatch[1].match(/"([^"]+)"/g);
1711
+ if (items) {
1712
+ devDeps.push(
1713
+ ...items.map((s) => s.replace(/"/g, "").split(/[<>=[]/)[0])
1714
+ );
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+ return {
1720
+ name,
1721
+ license,
1722
+ prodDeps: deps.length,
1723
+ devDeps: devDeps.length,
1724
+ allDeps: [...deps, ...devDeps]
1725
+ };
1726
+ }
1727
+ function parseSetupPy(content) {
1728
+ let name = "unknown";
1729
+ const deps = [];
1730
+ const nameMatch = content.match(/name\s*=\s*["']([^"']+)["']/);
1731
+ if (nameMatch) {
1732
+ name = nameMatch[1];
1733
+ }
1734
+ const reqMatch = content.match(/install_requires\s*=\s*\[([^\]]+)\]/s);
1735
+ if (reqMatch) {
1736
+ const items = reqMatch[1].match(/["']([^"']+)["']/g);
1737
+ if (items) {
1738
+ deps.push(...items.map((s) => s.replace(/["']/g, "").split(/[<>=[]/)[0]));
1739
+ }
1740
+ }
1741
+ return {
1742
+ name,
1743
+ license: null,
1744
+ prodDeps: deps.length,
1745
+ devDeps: 0,
1746
+ allDeps: deps
1747
+ };
1748
+ }
1749
+ function getFolderName() {
1750
+ try {
1751
+ return execSync3('basename "$PWD"', { encoding: "utf-8" }).trim();
1752
+ } catch {
1753
+ return basename4(process.cwd());
1754
+ }
1755
+ }
1756
+ function getProjectInfo() {
1757
+ if (existsSync7("package.json")) {
1758
+ try {
1759
+ const content = readFileSync8("package.json", "utf-8");
1760
+ return parsePackageJson(content);
1761
+ } catch {
1762
+ }
1763
+ }
1764
+ if (existsSync7("pyproject.toml")) {
1765
+ try {
1766
+ const content = readFileSync8("pyproject.toml", "utf-8");
1767
+ return parsePyprojectToml(content);
1768
+ } catch {
1769
+ }
1770
+ }
1771
+ if (existsSync7("setup.py")) {
1772
+ try {
1773
+ const content = readFileSync8("setup.py", "utf-8");
1774
+ return parseSetupPy(content);
1775
+ } catch {
1776
+ }
1777
+ }
1778
+ return {
1779
+ name: getFolderName(),
1780
+ license: null,
1781
+ prodDeps: 0,
1782
+ devDeps: 0,
1783
+ allDeps: []
1784
+ };
1785
+ }
1786
+ function detectStack(deps) {
1787
+ const normalizedDeps = deps.map((d) => d.toLowerCase());
1788
+ const frameworks = [];
1789
+ const tools = [];
1790
+ for (const framework of KNOWN_STACK.frameworks) {
1791
+ if (normalizedDeps.includes(framework)) {
1792
+ frameworks.push(framework);
1793
+ }
1794
+ }
1795
+ for (const tool of KNOWN_STACK.tools) {
1796
+ if (normalizedDeps.includes(tool)) {
1797
+ tools.push(tool);
1798
+ }
1799
+ }
1800
+ return [...frameworks, ...tools].slice(0, 5);
1801
+ }
1802
+ function findSourceDir() {
1803
+ for (const dir of SOURCE_DIRS) {
1804
+ if (existsSync7(dir)) {
1805
+ return dir;
1806
+ }
1807
+ }
1808
+ return null;
1809
+ }
1810
+ function countFiles(language) {
1811
+ const sourceDir = findSourceDir();
1812
+ if (!sourceDir || !language) {
1813
+ return { count: 0, extension: "" };
1814
+ }
1815
+ const config = FILE_EXTENSIONS[language];
1816
+ if (!config) {
1817
+ return { count: 0, extension: "" };
1818
+ }
1819
+ try {
1820
+ const excludes = EXCLUDE_DIRS.map((d) => `-path "*/${d}/*"`).join(" -o ");
1821
+ const namePatterns = config.patterns.map((p) => `-name "${p}"`).join(" -o ");
1822
+ const cmd = `find ${sourceDir} \\( ${excludes} \\) -prune -o -type f \\( ${namePatterns} \\) -print | wc -l`;
1823
+ const result = execSync3(cmd, { encoding: "utf-8" });
1824
+ const count = parseInt(result.trim(), 10) || 0;
1825
+ return { count, extension: config.ext };
1826
+ } catch {
1827
+ return { count: 0, extension: config.ext };
1828
+ }
1829
+ }
1830
+ function countLines(language) {
1831
+ const sourceDir = findSourceDir();
1832
+ if (!sourceDir || !language) {
1833
+ return 0;
1834
+ }
1835
+ const config = FILE_EXTENSIONS[language];
1836
+ if (!config) {
1837
+ return 0;
1838
+ }
1839
+ try {
1840
+ const excludes = EXCLUDE_DIRS.map((d) => `-path "*/${d}/*"`).join(" -o ");
1841
+ const namePatterns = config.patterns.map((p) => `-name "${p}"`).join(" -o ");
1842
+ const cmd = `find ${sourceDir} \\( ${excludes} \\) -prune -o -type f \\( ${namePatterns} \\) -print0 | xargs -0 wc -l 2>/dev/null | tail -1 | awk '{print $1}'`;
1843
+ const result = execSync3(cmd, { encoding: "utf-8" });
1844
+ return parseInt(result.trim(), 10) || 0;
1845
+ } catch {
1846
+ return 0;
1847
+ }
1848
+ }
1849
+ function getProjectData() {
1850
+ try {
1851
+ const language = detectLanguage();
1852
+ const projectInfo = getProjectInfo();
1853
+ const stack = detectStack(projectInfo.allDeps);
1854
+ const fileCount = countFiles(language);
1855
+ const lineCount = countLines(language);
1856
+ return {
1857
+ name: projectInfo.name,
1858
+ language,
1859
+ license: projectInfo.license,
1860
+ stack,
1861
+ fileCount: fileCount.count,
1862
+ fileExtension: fileCount.extension,
1863
+ lineCount,
1864
+ prodDeps: projectInfo.prodDeps,
1865
+ devDeps: projectInfo.devDeps
1866
+ };
1867
+ } catch (error) {
1868
+ const message = error instanceof Error ? error.message : String(error);
1869
+ return {
1870
+ name: getFolderName(),
1871
+ language: null,
1872
+ license: null,
1873
+ stack: [],
1874
+ fileCount: 0,
1875
+ fileExtension: "",
1876
+ lineCount: 0,
1877
+ prodDeps: 0,
1878
+ devDeps: 0,
1879
+ error: message
1880
+ };
1881
+ }
1882
+ }
1883
+
1884
+ // src/data/tests.ts
1885
+ import { execSync as execSync5 } from "child_process";
1886
+ import { readFileSync as readFileSync10 } from "fs";
1887
+ import { join as join6 } from "path";
1888
+
1889
+ // src/runner/command.ts
1890
+ import { execSync as execSync4 } from "child_process";
1891
+ import { existsSync as existsSync8, readFileSync as readFileSync9, unlinkSync } from "fs";
1892
+ function parseJUnitXml(xml) {
1893
+ try {
1894
+ if (!xml.includes("<testsuite") && !xml.includes("<testsuites")) {
1895
+ return null;
1896
+ }
1897
+ let totalTests = 0;
1898
+ let totalErrors = 0;
1899
+ let totalFailures = 0;
1900
+ let totalSkipped = 0;
1901
+ const failures = [];
1902
+ const testsuiteMatches = xml.match(/<testsuite\b[^>]*(?:\/>|>[\s\S]*?<\/testsuite>)/g) || [];
1903
+ const testsuites = testsuiteMatches.length > 0 ? testsuiteMatches : [xml];
1904
+ for (const suite of testsuites) {
1905
+ const suiteTag = suite.match(/<testsuite[^>]*>/)?.[0] || "";
1906
+ const testsMatch = suiteTag.match(/tests="(\d+)"/);
1907
+ const errorsMatch = suiteTag.match(/errors="(\d+)"/);
1908
+ const failuresMatch = suiteTag.match(/failures="(\d+)"/);
1909
+ const skippedMatch = suiteTag.match(/skipped="(\d+)"/);
1910
+ totalTests += testsMatch ? parseInt(testsMatch[1], 10) : 0;
1911
+ totalErrors += errorsMatch ? parseInt(errorsMatch[1], 10) : 0;
1912
+ totalFailures += failuresMatch ? parseInt(failuresMatch[1], 10) : 0;
1913
+ totalSkipped += skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
1914
+ const testcaseRegex = /<testcase[^>]*classname="([^"]*)"[^>]*name="([^"]*)"[^/>]*(?:\/>|>[\s\S]*?<\/testcase>)/g;
1915
+ const testcaseMatches = suite.matchAll(testcaseRegex);
1916
+ for (const testcaseMatch of testcaseMatches) {
1917
+ const testcaseContent = testcaseMatch[0];
1918
+ const classname = testcaseMatch[1];
1919
+ const name = testcaseMatch[2];
1920
+ if (testcaseContent.includes("<failure") || testcaseContent.includes("<error")) {
1921
+ failures.push({
1922
+ file: classname,
1923
+ name
1924
+ });
1925
+ }
1926
+ }
1927
+ }
1928
+ if (totalTests === 0 && testsuiteMatches.length === 0) {
1929
+ return null;
1930
+ }
1931
+ const failed = totalFailures + totalErrors;
1932
+ const passed = totalTests - failed - totalSkipped;
1933
+ return {
1934
+ passed,
1935
+ failed,
1936
+ skipped: totalSkipped,
1937
+ failures
1938
+ };
1939
+ } catch {
1940
+ return null;
1941
+ }
1942
+ }
1943
+ function getHeadHash() {
1944
+ try {
1945
+ return execSync4("git rev-parse --short HEAD", {
1946
+ encoding: "utf-8",
1947
+ stdio: ["pipe", "pipe", "pipe"]
1948
+ }).trim();
1949
+ } catch {
1950
+ return "unknown";
1951
+ }
1952
+ }
1953
+ function runTestCommand(command, source = TEST_RESULTS_FILE) {
1954
+ try {
1955
+ if (existsSync8(source)) {
1956
+ unlinkSync(source);
1957
+ }
1958
+ } catch {
1959
+ }
1960
+ try {
1961
+ execSync4(command, {
1962
+ encoding: "utf-8",
1963
+ stdio: ["pipe", "pipe", "pipe"]
1964
+ });
1965
+ } catch {
1966
+ }
1967
+ if (!existsSync8(source)) {
1968
+ return {
1969
+ results: null,
1970
+ isOutdated: false,
1971
+ commitsBehind: 0,
1972
+ error: "Test command failed to produce output file"
1973
+ };
1974
+ }
1975
+ let content;
1976
+ try {
1977
+ content = readFileSync9(source, "utf-8");
1978
+ } catch (error) {
1979
+ const message = error instanceof Error ? error.message : String(error);
1980
+ return {
1981
+ results: null,
1982
+ isOutdated: false,
1983
+ commitsBehind: 0,
1984
+ error: `Failed to read result file: ${message}`
1985
+ };
1986
+ }
1987
+ const parsed = parseJUnitXml(content);
1988
+ if (!parsed) {
1989
+ return {
1990
+ results: null,
1991
+ isOutdated: false,
1992
+ commitsBehind: 0,
1993
+ error: "Failed to parse test results XML"
1994
+ };
1995
+ }
1996
+ const hash = getHeadHash();
1997
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1998
+ const results = {
1999
+ hash,
2000
+ timestamp,
2001
+ passed: parsed.passed,
2002
+ failed: parsed.failed,
2003
+ skipped: parsed.skipped,
2004
+ failures: parsed.failures
2005
+ };
2006
+ return {
2007
+ results,
2008
+ isOutdated: false,
2009
+ commitsBehind: 0
2010
+ };
2011
+ }
2012
+
2013
+ // src/data/tests.ts
2014
+ var AGENT_DIR = ".agenthud";
2015
+ var TEST_RESULTS_FILE2 = "test-results.json";
2016
+ function getHeadHash2() {
2017
+ return execSync5("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
2018
+ }
2019
+ function getCommitCount(fromHash) {
2020
+ const result = execSync5(`git rev-list ${fromHash}..HEAD --count`, {
2021
+ encoding: "utf-8"
2022
+ }).trim();
2023
+ return parseInt(result, 10) || 0;
2024
+ }
2025
+ function getTestData(dir = process.cwd()) {
2026
+ const testResultsPath = join6(dir, AGENT_DIR, TEST_RESULTS_FILE2);
2027
+ let results = null;
2028
+ let isOutdated = false;
2029
+ let commitsBehind = 0;
2030
+ let error;
2031
+ try {
2032
+ const content = readFileSync10(testResultsPath, "utf-8");
2033
+ results = JSON.parse(content);
2034
+ } catch (e) {
2035
+ if (e instanceof SyntaxError) {
2036
+ error = "Invalid test-results.json";
2037
+ } else {
2038
+ error = "No test results";
2039
+ }
2040
+ return { results: null, isOutdated: false, commitsBehind: 0, error };
2041
+ }
2042
+ try {
2043
+ const currentHash = getHeadHash2();
2044
+ if (results.hash !== currentHash) {
2045
+ isOutdated = true;
2046
+ commitsBehind = getCommitCount(results.hash);
2047
+ }
2048
+ } catch {
2049
+ isOutdated = false;
2050
+ commitsBehind = 0;
2051
+ }
2052
+ return { results, isOutdated, commitsBehind, error };
2053
+ }
2054
+
2055
+ // src/ui/ClaudePanel.tsx
2056
+ import { Box, Text } from "ink";
2057
+ import { useEffect, useState } from "react";
2058
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2059
+ function getActivityStyle(activity) {
2060
+ if (activity.type === "user") {
2061
+ return { color: "white", dimColor: false };
2062
+ }
2063
+ if (activity.type === "response") {
2064
+ return { color: "green", dimColor: false };
2065
+ }
2066
+ if (activity.type === "tool") {
2067
+ if (activity.label === "Bash") {
2068
+ return { color: "gray", dimColor: false };
2069
+ }
2070
+ return { dimColor: true };
2071
+ }
2072
+ return { dimColor: true };
2073
+ }
2074
+ function formatCountdown(seconds) {
2075
+ if (seconds == null) return "";
2076
+ const padded = String(seconds).padStart(2, " ");
2077
+ return `\u21BB ${padded}s`;
2078
+ }
2079
+ function formatTokenCount(tokens) {
2080
+ if (tokens <= 0) return "";
2081
+ if (tokens < 1e3) return `${tokens} tokens`;
2082
+ if (tokens < 1e6) return `${Math.round(tokens / 1e3)}K tokens`;
2083
+ return `${(tokens / 1e6).toFixed(1)}M tokens`;
2084
+ }
2085
+ function formatSessionTime(startTime) {
2086
+ if (!startTime) return "";
2087
+ const startHours = String(startTime.getHours()).padStart(2, "0");
2088
+ const startMinutes = String(startTime.getMinutes()).padStart(2, "0");
2089
+ const startStr = `${startHours}:${startMinutes}`;
2090
+ const elapsed = Date.now() - startTime.getTime();
2091
+ const elapsedMinutes = Math.floor(elapsed / 6e4);
2092
+ const hours = Math.floor(elapsedMinutes / 60);
2093
+ const remainingMinutes = elapsedMinutes % 60;
2094
+ let elapsedStr;
2095
+ if (hours >= 10) {
2096
+ elapsedStr = `${hours}h`;
2097
+ } else if (hours > 0) {
2098
+ elapsedStr = `${hours}h ${remainingMinutes}m`;
2099
+ } else if (elapsedMinutes > 0) {
2100
+ elapsedStr = `${elapsedMinutes}m`;
2101
+ } else {
2102
+ elapsedStr = "<1m";
2103
+ }
2104
+ return `${startStr} (${elapsedStr})`;
2105
+ }
2106
+ function getStatusIcon(status) {
2107
+ switch (status) {
2108
+ case "running":
2109
+ return "\u{1F504}";
2110
+ case "completed":
2111
+ return "\u2705";
2112
+ case "idle":
2113
+ return "\u23F3";
2114
+ default:
2115
+ return "";
2116
+ }
2117
+ }
2118
+ function formatActivityTime(date) {
2119
+ const hours = String(date.getHours()).padStart(2, "0");
2120
+ const minutes = String(date.getMinutes()).padStart(2, "0");
2121
+ const seconds = String(date.getSeconds()).padStart(2, "0");
2122
+ return `${hours}:${minutes}:${seconds}`;
2123
+ }
2124
+ function formatActivityParts(activity, maxWidth) {
2125
+ const time = formatActivityTime(activity.timestamp);
2126
+ const icon = activity.icon;
2127
+ const label = activity.label;
2128
+ const detail = activity.detail;
2129
+ const count = activity.count;
2130
+ const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
2131
+ const countSuffixWidth = countSuffix.length;
2132
+ const skipLabel = label === "User" || label === "Response";
2133
+ const timestamp = `[${time}] `;
2134
+ const timestampWidth = timestamp.length;
2135
+ const iconWidth = getDisplayWidth(icon);
2136
+ if (skipLabel && detail) {
2137
+ const prefixWidth = timestampWidth + iconWidth + 1;
2138
+ const availableWidth = maxWidth - prefixWidth - countSuffixWidth;
2139
+ let truncatedDetail = detail;
2140
+ let detailDisplayWidth = getDisplayWidth(detail);
2141
+ if (detailDisplayWidth > availableWidth) {
2142
+ truncatedDetail = "";
2143
+ let currentWidth = 0;
2144
+ for (const char of detail) {
2145
+ const charWidth = getDisplayWidth(char);
2146
+ if (currentWidth + charWidth > availableWidth - 3) {
2147
+ truncatedDetail += "...";
2148
+ currentWidth += 3;
2149
+ break;
2150
+ }
2151
+ truncatedDetail += char;
2152
+ currentWidth += charWidth;
2153
+ }
2154
+ detailDisplayWidth = currentWidth;
2155
+ }
2156
+ return {
2157
+ timestamp,
2158
+ icon,
2159
+ labelContent: truncatedDetail + countSuffix,
2160
+ displayWidth: prefixWidth + detailDisplayWidth + countSuffixWidth
2161
+ };
2162
+ }
2163
+ const labelWidth = label.length;
2164
+ const separatorWidth = detail ? 2 : 0;
2165
+ const contentPrefixWidth = iconWidth + 1 + labelWidth + separatorWidth;
2166
+ const totalPrefixWidth = timestampWidth + contentPrefixWidth;
2167
+ if (detail) {
2168
+ const availableWidth = maxWidth - totalPrefixWidth - countSuffixWidth;
2169
+ let truncatedDetail = detail;
2170
+ let detailDisplayWidth = getDisplayWidth(detail);
2171
+ if (detailDisplayWidth > availableWidth) {
2172
+ truncatedDetail = "";
2173
+ let currentWidth = 0;
2174
+ for (const char of detail) {
2175
+ const charWidth = getDisplayWidth(char);
2176
+ if (currentWidth + charWidth > availableWidth - 3) {
2177
+ truncatedDetail += "...";
2178
+ currentWidth += 3;
2179
+ break;
2180
+ }
2181
+ truncatedDetail += char;
2182
+ currentWidth += charWidth;
2183
+ }
2184
+ detailDisplayWidth = currentWidth;
2185
+ }
2186
+ const labelContent2 = `${label}: ${truncatedDetail}${countSuffix}`;
2187
+ const displayWidth2 = totalPrefixWidth + detailDisplayWidth + countSuffixWidth;
2188
+ return { timestamp, icon, labelContent: labelContent2, displayWidth: displayWidth2 };
2189
+ }
2190
+ const labelContent = label + countSuffix;
2191
+ const displayWidth = totalPrefixWidth + countSuffixWidth;
2192
+ return { timestamp, icon, labelContent, displayWidth };
2193
+ }
2194
+ var TODO_ICONS = {
2195
+ completed: "\u2713",
2196
+ in_progress_left: "\u25D0",
2197
+ in_progress_right: "\u25D1",
2198
+ pending: "\u25CB"
2199
+ };
2200
+ function TodoSection({ todos, width }) {
2201
+ const [tick, setTick] = useState(false);
2202
+ const innerWidth = getInnerWidth(width);
2203
+ const contentWidth = innerWidth - 1;
2204
+ useEffect(() => {
2205
+ const timer = setInterval(() => setTick((t) => !t), 500);
2206
+ return () => clearInterval(timer);
2207
+ }, []);
2208
+ const completedCount = todos.filter((t) => t.status === "completed").length;
2209
+ const totalCount = todos.length;
2210
+ const headerTitle = `Todo (${completedCount}/${totalCount})`;
2211
+ const inProgressIcon = tick ? TODO_ICONS.in_progress_left : TODO_ICONS.in_progress_right;
2212
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2213
+ /* @__PURE__ */ jsx(Text, { children: createSeparatorLine(headerTitle, width) }),
2214
+ todos.map((todo, i) => {
2215
+ let icon;
2216
+ let iconColor;
2217
+ switch (todo.status) {
2218
+ case "completed":
2219
+ icon = TODO_ICONS.completed;
2220
+ iconColor = "green";
2221
+ break;
2222
+ case "in_progress":
2223
+ icon = inProgressIcon;
2224
+ iconColor = "yellow";
2225
+ break;
2226
+ default:
2227
+ icon = TODO_ICONS.pending;
2228
+ iconColor = void 0;
2229
+ }
2230
+ const text = todo.status === "in_progress" ? todo.activeForm : todo.content;
2231
+ const maxTextWidth = contentWidth - 3;
2232
+ let displayText = text;
2233
+ if (getDisplayWidth(text) > maxTextWidth) {
2234
+ displayText = "";
2235
+ let currentWidth = 0;
2236
+ for (const char of text) {
2237
+ const charWidth = getDisplayWidth(char);
2238
+ if (currentWidth + charWidth > maxTextWidth - 3) {
2239
+ displayText += "...";
2240
+ currentWidth += 3;
2241
+ break;
2242
+ }
2243
+ displayText += char;
2244
+ currentWidth += charWidth;
2245
+ }
2246
+ }
2247
+ const padding = Math.max(
2248
+ 0,
2249
+ contentWidth - getDisplayWidth(icon) - 1 - getDisplayWidth(displayText)
2250
+ );
2251
+ return /* @__PURE__ */ jsxs(Text, { children: [
2252
+ BOX.v,
2253
+ " ",
2254
+ /* @__PURE__ */ jsx(Text, { color: iconColor, children: icon }),
2255
+ " ",
2256
+ /* @__PURE__ */ jsx(Text, { dimColor: todo.status === "completed", children: displayText }),
2257
+ " ".repeat(padding),
2258
+ BOX.v
2259
+ ] }, `todo-${i}`);
2260
+ })
2261
+ ] });
2262
+ }
2263
+ function ClaudePanel({
2264
+ data,
2265
+ countdown,
2266
+ width = DEFAULT_PANEL_WIDTH,
2267
+ isRunning = false,
2268
+ justRefreshed = false,
2269
+ maxActivities
2270
+ }) {
2271
+ const countdownSuffix = isRunning ? "running..." : formatCountdown(countdown);
2272
+ const innerWidth = getInnerWidth(width);
2273
+ const contentWidth = innerWidth - 1;
2274
+ const { state } = data;
2275
+ const _statusIcon = getStatusIcon(state.status);
2276
+ const sessionTime = formatSessionTime(state.sessionStartTime);
2277
+ const tokenDisplay = formatTokenCount(state.tokenCount);
2278
+ const titleParts = [];
2279
+ if (tokenDisplay) titleParts.push(tokenDisplay);
2280
+ if (sessionTime) titleParts.push(sessionTime);
2281
+ if (countdownSuffix) titleParts.push(countdownSuffix);
2282
+ const titleSuffix = titleParts.join(" \xB7 ");
2283
+ if (data.error) {
2284
+ const errorPadding = Math.max(0, contentWidth - data.error.length);
2285
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
2286
+ /* @__PURE__ */ jsx(Text, { children: createTitleLine("Claude", titleSuffix, width) }),
2287
+ /* @__PURE__ */ jsxs(Text, { children: [
2288
+ BOX.v,
2289
+ " ",
2290
+ /* @__PURE__ */ jsx(Text, { color: "red", children: data.error }),
2291
+ " ".repeat(errorPadding),
2292
+ BOX.v
2293
+ ] }),
2294
+ /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
2295
+ ] });
2296
+ }
2297
+ if (!data.hasSession) {
2298
+ const noSessionText = "No Claude session";
2299
+ const noSessionPadding = Math.max(0, contentWidth - noSessionText.length);
2300
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
2301
+ /* @__PURE__ */ jsx(Text, { children: createTitleLine("Claude", countdownSuffix, width) }),
2302
+ /* @__PURE__ */ jsxs(Text, { children: [
2303
+ BOX.v,
2304
+ " ",
2305
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: noSessionText }),
2306
+ " ".repeat(noSessionPadding),
2307
+ BOX.v
2308
+ ] }),
2309
+ /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
2310
+ ] });
2311
+ }
2312
+ if (state.status === "none" || state.activities.length === 0) {
2313
+ const noActiveText = "No active session";
2314
+ const noActivePadding = Math.max(0, contentWidth - noActiveText.length);
2315
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
2316
+ /* @__PURE__ */ jsx(Text, { children: createTitleLine("Claude", titleSuffix, width) }),
2317
+ /* @__PURE__ */ jsxs(Text, { children: [
2318
+ BOX.v,
2319
+ " ",
2320
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: noActiveText }),
2321
+ " ".repeat(noActivePadding),
2322
+ BOX.v
2323
+ ] }),
2324
+ /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
2325
+ ] });
2326
+ }
2327
+ const lines = [];
2328
+ const displayActivities = maxActivities !== void 0 ? state.activities.slice(0, maxActivities) : state.activities;
2329
+ for (let i = 0; i < displayActivities.length; i++) {
2330
+ const activity = displayActivities[i];
2331
+ let modifiedActivity = activity;
2332
+ if (activity.label === "Task" && activity.subActivityCount && activity.subActivityCount > 0) {
2333
+ modifiedActivity = {
2334
+ ...activity,
2335
+ detail: activity.detail ? `${activity.detail} (${activity.subActivityCount})` : `(${activity.subActivityCount})`
2336
+ };
2337
+ }
2338
+ const { timestamp, icon, labelContent, displayWidth } = formatActivityParts(
2339
+ modifiedActivity,
2340
+ contentWidth
2341
+ );
2342
+ const padding = Math.max(0, contentWidth - displayWidth);
2343
+ const style = getActivityStyle(activity);
2344
+ lines.push(
2345
+ /* @__PURE__ */ jsxs(Text, { children: [
2346
+ BOX.v,
2347
+ " ",
2348
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: timestamp }),
2349
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: icon }),
2350
+ " ",
2351
+ /* @__PURE__ */ jsx(Text, { color: style.color, dimColor: style.dimColor, children: labelContent }),
2352
+ " ".repeat(padding),
2353
+ BOX.v
2354
+ ] }, `activity-${i}`)
2355
+ );
2356
+ if (activity.subActivities && activity.subActivities.length > 0) {
2357
+ const subPrefix = " \u2514 ";
2358
+ const subPrefixWidth = getDisplayWidth(subPrefix);
2359
+ for (let j = 0; j < activity.subActivities.length; j++) {
2360
+ const sub = activity.subActivities[j];
2361
+ const subStyle = getActivityStyle(sub);
2362
+ const subIcon = sub.icon;
2363
+ const subIconWidth = getDisplayWidth(subIcon);
2364
+ const subLabel = sub.label;
2365
+ const subDetail = sub.detail;
2366
+ const subContentPrefixWidth = subPrefixWidth + subIconWidth + 1;
2367
+ const availableWidth = contentWidth - subContentPrefixWidth;
2368
+ let subLabelContent;
2369
+ let subDisplayWidth;
2370
+ if (subDetail) {
2371
+ const labelPart = `${subLabel}: `;
2372
+ const detailAvailable = availableWidth - labelPart.length;
2373
+ let truncatedDetail = subDetail;
2374
+ if (getDisplayWidth(subDetail) > detailAvailable) {
2375
+ truncatedDetail = "";
2376
+ let currentWidth = 0;
2377
+ for (const char of subDetail) {
2378
+ const charWidth = getDisplayWidth(char);
2379
+ if (currentWidth + charWidth > detailAvailable - 3) {
2380
+ truncatedDetail += "...";
2381
+ currentWidth += 3;
2382
+ break;
2383
+ }
2384
+ truncatedDetail += char;
2385
+ currentWidth += charWidth;
2386
+ }
2387
+ }
2388
+ subLabelContent = labelPart + truncatedDetail;
2389
+ subDisplayWidth = subContentPrefixWidth + labelPart.length + getDisplayWidth(truncatedDetail);
2390
+ } else {
2391
+ subLabelContent = subLabel;
2392
+ subDisplayWidth = subContentPrefixWidth + subLabel.length;
2393
+ }
2394
+ const subPadding = Math.max(0, contentWidth - subDisplayWidth);
2395
+ lines.push(
2396
+ /* @__PURE__ */ jsxs(Text, { children: [
2397
+ BOX.v,
2398
+ " ",
2399
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: subPrefix }),
2400
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: subIcon }),
2401
+ " ",
2402
+ /* @__PURE__ */ jsx(Text, { color: subStyle.color, dimColor: subStyle.dimColor, children: subLabelContent }),
2403
+ " ".repeat(subPadding),
2404
+ BOX.v
2405
+ ] }, `activity-${i}-sub-${j}`)
2406
+ );
2407
+ }
2408
+ }
2409
+ }
2410
+ const hasTodos = state.todos && state.todos.length > 0;
2411
+ const allCompleted = hasTodos && state.todos?.every((t) => t.status === "completed");
2412
+ if (hasTodos && allCompleted) {
2413
+ const todos = state.todos;
2414
+ const summaryText = `Todo (${todos.length}/${todos.length} done)`;
2415
+ const summaryIcon = "\u2713";
2416
+ const timestamp = formatActivityTime(/* @__PURE__ */ new Date());
2417
+ const timestampStr = `[${timestamp}] `;
2418
+ const timestampWidth = timestampStr.length;
2419
+ const iconWidth = getDisplayWidth(summaryIcon);
2420
+ const prefixWidth = timestampWidth + iconWidth + 1;
2421
+ const maxTextWidth = contentWidth - prefixWidth;
2422
+ let displaySummary = summaryText;
2423
+ if (getDisplayWidth(summaryText) > maxTextWidth) {
2424
+ displaySummary = "";
2425
+ let currentWidth = 0;
2426
+ for (const char of summaryText) {
2427
+ const charWidth = getDisplayWidth(char);
2428
+ if (currentWidth + charWidth > maxTextWidth - 3) {
2429
+ displaySummary += "...";
2430
+ break;
2431
+ }
2432
+ displaySummary += char;
2433
+ currentWidth += charWidth;
2434
+ }
2435
+ }
2436
+ const summaryPadding = Math.max(
2437
+ 0,
2438
+ contentWidth - prefixWidth - getDisplayWidth(displaySummary)
2439
+ );
2440
+ lines.push(
2441
+ /* @__PURE__ */ jsxs(Text, { children: [
2442
+ BOX.v,
2443
+ " ",
2444
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: timestampStr }),
2445
+ /* @__PURE__ */ jsx(Text, { color: "green", children: summaryIcon }),
2446
+ " ",
2447
+ /* @__PURE__ */ jsx(Text, { color: "green", children: displaySummary }),
2448
+ " ".repeat(summaryPadding),
2449
+ BOX.v
2450
+ ] }, "todo-summary")
2451
+ );
2452
+ }
2453
+ const showTodoSection = hasTodos && !allCompleted;
2454
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
2455
+ /* @__PURE__ */ jsx(Text, { children: createTitleLine("Claude", titleSuffix, width) }),
2456
+ lines,
2457
+ showTodoSection && /* @__PURE__ */ jsx(TodoSection, { todos: state.todos, width }),
2458
+ /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
2459
+ ] });
2460
+ }
2461
+
2462
+ // src/ui/GenericPanel.tsx
2463
+ import { Box as Box2, Text as Text2 } from "ink";
2464
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2465
+ var PROGRESS_BAR_WIDTH = 10;
2466
+ function createProgressBar(done, total) {
2467
+ if (total === 0) return "\u2591".repeat(PROGRESS_BAR_WIDTH);
2468
+ const filled = Math.round(done / total * PROGRESS_BAR_WIDTH);
2469
+ const empty = PROGRESS_BAR_WIDTH - filled;
2470
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
2471
+ }
2472
+ function formatTitleSuffix(countdown, relativeTime) {
2473
+ if (countdown != null) {
2474
+ const padded = String(countdown).padStart(2, " ");
2475
+ return `\u21BB ${padded}s`;
2476
+ }
2477
+ if (relativeTime) return relativeTime;
2478
+ return "";
2479
+ }
2480
+ function createProgressTitleLine(title, done, total, panelWidth, countdown, relativeTime) {
2481
+ const label = ` ${title} `;
2482
+ const count = ` ${done}/${total} `;
2483
+ const bar = createProgressBar(done, total);
2484
+ const suffix = formatTitleSuffix(countdown, relativeTime);
2485
+ const suffixPart = suffix ? ` \xB7 ${suffix} ${BOX.h}` : "";
2486
+ const dashCount = panelWidth - 3 - label.length - count.length - bar.length - suffixPart.length;
2487
+ const dashes = BOX.h.repeat(Math.max(0, dashCount));
2488
+ return BOX.tl + BOX.h + label + dashes + count + bar + suffixPart + BOX.tr;
2489
+ }
2490
+ function ListRenderer({
2491
+ data,
2492
+ width
2493
+ }) {
2494
+ const items = data.items || [];
2495
+ const contentWidth = getContentWidth(width);
2496
+ if (items.length === 0 && !data.summary) {
2497
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
2498
+ BOX.v,
2499
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(" No data", width) }),
2500
+ BOX.v
2501
+ ] });
2502
+ }
2503
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2504
+ data.summary && /* @__PURE__ */ jsxs2(Text2, { children: [
2505
+ BOX.v,
2506
+ padLine(` ${truncate(data.summary, contentWidth)}`, width),
2507
+ BOX.v
2508
+ ] }),
2509
+ items.map((item, index) => /* @__PURE__ */ jsxs2(Text2, { children: [
2510
+ BOX.v,
2511
+ padLine(` \u2022 ${truncate(item.text, contentWidth - 3)}`, width),
2512
+ BOX.v
2513
+ ] }, `list-item-${index}`)),
2514
+ items.length === 0 && data.summary && null
2515
+ ] });
2516
+ }
2517
+ function ProgressRenderer({
2518
+ data,
2519
+ width
2520
+ }) {
2521
+ const items = data.items || [];
2522
+ const contentWidth = getContentWidth(width);
2523
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2524
+ data.summary && /* @__PURE__ */ jsxs2(Text2, { children: [
2525
+ BOX.v,
2526
+ padLine(` ${truncate(data.summary, contentWidth)}`, width),
2527
+ BOX.v
2528
+ ] }),
2529
+ items.map((item, index) => {
2530
+ const icon = item.status === "done" ? "\u2713" : item.status === "failed" ? "\u2717" : "\u25CB";
2531
+ const line = ` ${icon} ${truncate(item.text, contentWidth - 3)}`;
2532
+ return /* @__PURE__ */ jsxs2(Text2, { children: [
2533
+ BOX.v,
2534
+ padLine(line, width),
2535
+ BOX.v
2536
+ ] }, `progress-item-${index}`);
2537
+ }),
2538
+ items.length === 0 && !data.summary && /* @__PURE__ */ jsxs2(Text2, { children: [
2539
+ BOX.v,
2540
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(" No data", width) }),
2541
+ BOX.v
2542
+ ] })
2543
+ ] });
2544
+ }
2545
+ function StatusRenderer({
2546
+ data,
2547
+ width
2548
+ }) {
2549
+ const stats = data.stats || { passed: 0, failed: 0 };
2550
+ const items = data.items?.filter((i) => i.status === "failed") || [];
2551
+ const innerWidth = getInnerWidth(width);
2552
+ const contentWidth = getContentWidth(width);
2553
+ let summaryLength = 1 + 2 + String(stats.passed).length + " passed".length;
2554
+ if (stats.failed > 0) {
2555
+ summaryLength += 2 + 2 + String(stats.failed).length + " failed".length;
2556
+ }
2557
+ if (stats.skipped && stats.skipped > 0) {
2558
+ summaryLength += 2 + 2 + String(stats.skipped).length + " skipped".length;
2559
+ }
2560
+ const summaryPadding = Math.max(0, innerWidth - summaryLength);
2561
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
2562
+ data.summary && /* @__PURE__ */ jsxs2(Text2, { children: [
2563
+ BOX.v,
2564
+ padLine(` ${truncate(data.summary, contentWidth)}`, width),
2565
+ BOX.v
2566
+ ] }),
2567
+ /* @__PURE__ */ jsxs2(Text2, { children: [
2568
+ BOX.v,
2569
+ " ",
2570
+ /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
2571
+ "\u2713 ",
2572
+ stats.passed,
2573
+ " passed"
2574
+ ] }),
2575
+ stats.failed > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
2576
+ " ",
2577
+ /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
2578
+ "\u2717 ",
2579
+ stats.failed,
2580
+ " failed"
2581
+ ] })
2582
+ ] }),
2583
+ stats.skipped && stats.skipped > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
2584
+ " ",
2585
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2586
+ "\u25CB ",
2587
+ stats.skipped,
2588
+ " skipped"
2589
+ ] })
2590
+ ] }),
2591
+ " ".repeat(summaryPadding),
2592
+ BOX.v
2593
+ ] }),
2594
+ items.length > 0 && items.map((item, index) => /* @__PURE__ */ jsxs2(Text2, { children: [
2595
+ BOX.v,
2596
+ padLine(` \u2022 ${truncate(item.text, contentWidth - 3)}`, width),
2597
+ BOX.v
2598
+ ] }, `status-item-${index}`))
2599
+ ] });
2600
+ }
2601
+ function GenericPanel({
2602
+ data,
2603
+ renderer = "list",
2604
+ countdown,
2605
+ relativeTime,
2606
+ error,
2607
+ width = DEFAULT_PANEL_WIDTH,
2608
+ isRunning = false,
2609
+ justRefreshed = false
2610
+ }) {
2611
+ const suffix = isRunning ? "running..." : formatTitleSuffix(countdown, relativeTime);
2612
+ const _suffixColor = isRunning ? "yellow" : justRefreshed ? "green" : void 0;
2613
+ const progress = data.progress || { done: 0, total: 0 };
2614
+ if (error) {
2615
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
2616
+ /* @__PURE__ */ jsx2(Text2, { children: createTitleLine(data.title, suffix, width) }),
2617
+ /* @__PURE__ */ jsxs2(Text2, { children: [
2618
+ BOX.v,
2619
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(` ${error}`, width) }),
2620
+ BOX.v
2621
+ ] }),
2622
+ /* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
2623
+ ] });
2624
+ }
2625
+ if (renderer === "progress") {
2626
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
2627
+ /* @__PURE__ */ jsx2(Text2, { children: createProgressTitleLine(
2628
+ data.title,
2629
+ progress.done,
2630
+ progress.total,
2631
+ width,
2632
+ countdown,
2633
+ relativeTime
2634
+ ) }),
2635
+ /* @__PURE__ */ jsx2(ProgressRenderer, { data, width }),
2636
+ /* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
2637
+ ] });
2638
+ }
2639
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
2640
+ /* @__PURE__ */ jsx2(Text2, { children: createTitleLine(data.title, suffix, width) }),
2641
+ renderer === "status" ? /* @__PURE__ */ jsx2(StatusRenderer, { data, width }) : /* @__PURE__ */ jsx2(ListRenderer, { data, width }),
2642
+ /* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
2643
+ ] });
2644
+ }
2645
+
2646
+ // src/ui/GitPanel.tsx
2647
+ import { Box as Box3, Text as Text3 } from "ink";
2648
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2649
+ var MAX_COMMITS = 5;
2650
+ function formatCountdown2(seconds) {
2651
+ if (seconds == null) return "";
2652
+ const padded = String(seconds).padStart(2, " ");
2653
+ return `\u21BB ${padded}s`;
2654
+ }
2655
+ function GitPanel({
2656
+ branch,
2657
+ commits,
2658
+ stats,
2659
+ uncommitted = 0,
2660
+ countdown,
2661
+ width = DEFAULT_PANEL_WIDTH,
2662
+ isRunning = false,
2663
+ justRefreshed = false
2664
+ }) {
2665
+ const countdownSuffix = isRunning ? "running..." : formatCountdown2(countdown);
2666
+ const innerWidth = getInnerWidth(width);
2667
+ const contentWidth = getContentWidth(width);
2668
+ const maxMessageLength = contentWidth - 10;
2669
+ if (branch === null) {
2670
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
2671
+ /* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Git", countdownSuffix, width) }),
2672
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2673
+ BOX.v,
2674
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" Not a git repository", width) }),
2675
+ BOX.v
2676
+ ] }),
2677
+ /* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
2678
+ ] });
2679
+ }
2680
+ const displayCommits = commits.slice(0, MAX_COMMITS);
2681
+ const hasCommits = commits.length > 0;
2682
+ const commitWord = commits.length === 1 ? "commit" : "commits";
2683
+ const fileWord = stats.files === 1 ? "file" : "files";
2684
+ const hasUncommitted = uncommitted > 0;
2685
+ let statsSuffix = "";
2686
+ if (hasCommits) {
2687
+ statsSuffix = ` \xB7 +${stats.added} -${stats.deleted} \xB7 ${commits.length} ${commitWord} \xB7 ${stats.files} ${fileWord}`;
2688
+ }
2689
+ if (hasUncommitted) {
2690
+ statsSuffix += ` \xB7 ${uncommitted} dirty`;
2691
+ }
2692
+ const availableForBranch = innerWidth - 1 - statsSuffix.length;
2693
+ const displayBranch = availableForBranch > 3 ? truncate(branch, availableForBranch) : truncate(branch, 10);
2694
+ const branchLineLength = 1 + displayBranch.length + statsSuffix.length;
2695
+ const branchPadding = Math.max(0, innerWidth - branchLineLength);
2696
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
2697
+ /* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Git", countdownSuffix, width) }),
2698
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2699
+ BOX.v,
2700
+ " ",
2701
+ /* @__PURE__ */ jsx3(Text3, { color: "green", children: displayBranch }),
2702
+ hasCommits && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2703
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \xB7 " }),
2704
+ /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
2705
+ "+",
2706
+ stats.added
2707
+ ] }),
2708
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
2709
+ /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
2710
+ "-",
2711
+ stats.deleted
2712
+ ] }),
2713
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2714
+ " ",
2715
+ "\xB7 ",
2716
+ commits.length,
2717
+ " ",
2718
+ commitWord,
2719
+ " \xB7 ",
2720
+ stats.files,
2721
+ " ",
2722
+ fileWord
2723
+ ] })
2724
+ ] }),
2725
+ hasUncommitted && /* @__PURE__ */ jsxs3(Fragment3, { children: [
2726
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " \xB7 " }),
2727
+ /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
2728
+ uncommitted,
2729
+ " dirty"
2730
+ ] })
2731
+ ] }),
2732
+ " ".repeat(branchPadding),
2733
+ BOX.v
2734
+ ] }),
2735
+ hasCommits ? /* @__PURE__ */ jsx3(Fragment3, { children: displayCommits.map((commit) => {
2736
+ const msg = truncate(commit.message, maxMessageLength);
2737
+ const lineLength = 3 + 7 + 1 + msg.length;
2738
+ const commitPadding = Math.max(0, innerWidth - lineLength);
2739
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
2740
+ BOX.v,
2741
+ " \u2022 ",
2742
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: commit.hash.slice(0, 7) }),
2743
+ " ",
2744
+ msg,
2745
+ " ".repeat(commitPadding),
2746
+ BOX.v
2747
+ ] }, commit.hash);
2748
+ }) }) : /* @__PURE__ */ jsxs3(Text3, { children: [
2749
+ BOX.v,
2750
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" No commits today", width) }),
2751
+ BOX.v
2752
+ ] }),
2753
+ /* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
2754
+ ] });
2755
+ }
2756
+
2757
+ // src/ui/hooks/useCountdown.ts
2758
+ import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
2759
+ function toSeconds(intervalMs) {
2760
+ if (intervalMs === null) return null;
2761
+ return Math.floor(intervalMs / 1e3);
2762
+ }
2763
+ function useCountdown({
2764
+ panels,
2765
+ customPanels,
2766
+ enabled
2767
+ }) {
2768
+ const initialCountdowns = useMemo(() => {
2769
+ const result = {};
2770
+ for (const [name, config] of Object.entries(panels)) {
2771
+ result[name] = toSeconds(config.interval);
2772
+ }
2773
+ if (customPanels) {
2774
+ for (const [name, config] of Object.entries(customPanels)) {
2775
+ result[name] = toSeconds(config.interval);
2776
+ }
2777
+ }
2778
+ return result;
2779
+ }, [panels, customPanels]);
2780
+ const intervalSeconds = useMemo(() => {
2781
+ const result = {};
2782
+ for (const [name, config] of Object.entries(panels)) {
2783
+ result[name] = toSeconds(config.interval);
2784
+ }
2785
+ if (customPanels) {
2786
+ for (const [name, config] of Object.entries(customPanels)) {
2787
+ result[name] = toSeconds(config.interval);
2788
+ }
2789
+ }
2790
+ return result;
2791
+ }, [panels, customPanels]);
2792
+ const [countdowns, setCountdowns] = useState2(initialCountdowns);
2793
+ const intervalSecondsRef = useRef(intervalSeconds);
2794
+ intervalSecondsRef.current = intervalSeconds;
2795
+ useEffect2(() => {
2796
+ if (!enabled) return;
2797
+ const timer = setInterval(() => {
2798
+ setCountdowns((prev) => {
2799
+ const next = {};
2800
+ for (const [name, value] of Object.entries(prev)) {
2801
+ if (value === null) {
2802
+ next[name] = null;
2803
+ } else if (value > 1) {
2804
+ next[name] = value - 1;
2805
+ } else {
2806
+ next[name] = 1;
2807
+ }
2808
+ }
2809
+ return next;
2810
+ });
2811
+ }, 1e3);
2812
+ return () => clearInterval(timer);
2813
+ }, [enabled]);
2814
+ const reset = useCallback((panelName) => {
2815
+ const interval = intervalSecondsRef.current[panelName];
2816
+ if (interval === null || interval === void 0) return;
2817
+ setCountdowns((prev) => ({
2818
+ ...prev,
2819
+ [panelName]: interval
2820
+ }));
2821
+ }, []);
2822
+ const resetAll = useCallback(() => {
2823
+ setCountdowns((prev) => {
2824
+ const next = {};
2825
+ for (const name of Object.keys(prev)) {
2826
+ const interval = intervalSecondsRef.current[name];
2827
+ next[name] = interval ?? prev[name];
2828
+ }
2829
+ return next;
2830
+ });
2831
+ }, []);
2832
+ return {
2833
+ countdowns,
2834
+ reset,
2835
+ resetAll
2836
+ };
2837
+ }
2838
+
2839
+ // src/ui/hooks/useHotkeys.ts
2840
+ import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
2841
+ var RESERVED_KEYS = /* @__PURE__ */ new Set(["r", "q"]);
2842
+ function useHotkeys({
2843
+ manualPanels,
2844
+ onRefreshAll,
2845
+ onQuit
2846
+ }) {
2847
+ const hotkeys = useMemo2(() => {
2848
+ const result = [];
2849
+ const usedKeys = new Set(RESERVED_KEYS);
2850
+ for (const panel of manualPanels) {
2851
+ let assignedKey = null;
2852
+ for (const char of panel.name.toLowerCase()) {
2853
+ if (!usedKeys.has(char)) {
2854
+ assignedKey = char;
2855
+ usedKeys.add(char);
2856
+ break;
2857
+ }
2858
+ }
2859
+ if (assignedKey) {
2860
+ result.push({
2861
+ key: assignedKey,
2862
+ label: panel.label,
2863
+ action: panel.action
2864
+ });
2865
+ }
2866
+ }
2867
+ result.push({
2868
+ key: "r",
2869
+ label: "refresh all",
2870
+ action: onRefreshAll
2871
+ });
2872
+ result.push({
2873
+ key: "q",
2874
+ label: "quit",
2875
+ action: onQuit
2876
+ });
2877
+ return result;
2878
+ }, [manualPanels, onRefreshAll, onQuit]);
2879
+ const handleInput = useCallback2(
2880
+ (key) => {
2881
+ const hotkey = hotkeys.find((h) => h.key === key);
2882
+ if (hotkey) {
2883
+ hotkey.action();
2884
+ }
2885
+ },
2886
+ [hotkeys]
2887
+ );
2888
+ const statusBarItems = useMemo2(() => {
2889
+ return hotkeys.map((h) => `${h.key}: ${h.label}`);
2890
+ }, [hotkeys]);
2891
+ return {
2892
+ hotkeys,
2893
+ handleInput,
2894
+ statusBarItems
2895
+ };
2896
+ }
2897
+
2898
+ // src/ui/hooks/useVisualFeedback.ts
2899
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
2900
+ var DEFAULT_VISUAL_STATE = {
2901
+ isRunning: false,
2902
+ justRefreshed: false,
2903
+ justCompleted: false
2904
+ };
2905
+ var FEEDBACK_DURATION = 1500;
2906
+ function useVisualFeedback({
2907
+ panels
2908
+ }) {
2909
+ const [states, setStates] = useState3(() => {
2910
+ const initial = {};
2911
+ for (const panel of panels) {
2912
+ initial[panel] = { ...DEFAULT_VISUAL_STATE };
2913
+ }
2914
+ return initial;
2915
+ });
2916
+ const timeoutsRef = useRef2(/* @__PURE__ */ new Map());
2917
+ useEffect3(() => {
2918
+ return () => {
2919
+ for (const timeout of timeoutsRef.current.values()) {
2920
+ clearTimeout(timeout);
2921
+ }
2922
+ timeoutsRef.current.clear();
2923
+ };
2924
+ }, []);
2925
+ const updateState = useCallback3(
2926
+ (panel, update) => {
2927
+ setStates((prev) => ({
2928
+ ...prev,
2929
+ [panel]: { ...prev[panel] || DEFAULT_VISUAL_STATE, ...update }
2930
+ }));
2931
+ },
2932
+ []
2933
+ );
2934
+ const scheduleAutoClear = useCallback3(
2935
+ (panel, key) => {
2936
+ const timeoutKey = `${panel}:${key}`;
2937
+ const existingTimeout = timeoutsRef.current.get(timeoutKey);
2938
+ if (existingTimeout) {
2939
+ clearTimeout(existingTimeout);
2940
+ }
2941
+ const timeout = setTimeout(() => {
2942
+ updateState(panel, { [key]: false });
2943
+ timeoutsRef.current.delete(timeoutKey);
2944
+ }, FEEDBACK_DURATION);
2945
+ timeoutsRef.current.set(timeoutKey, timeout);
2946
+ },
2947
+ [updateState]
2948
+ );
2949
+ const setRunning = useCallback3(
2950
+ (panel, running) => {
2951
+ updateState(panel, { isRunning: running });
2952
+ },
2953
+ [updateState]
2954
+ );
2955
+ const setRefreshed = useCallback3(
2956
+ (panel) => {
2957
+ updateState(panel, { justRefreshed: true });
2958
+ scheduleAutoClear(panel, "justRefreshed");
2959
+ },
2960
+ [updateState, scheduleAutoClear]
2961
+ );
2962
+ const setCompleted = useCallback3(
2963
+ (panel) => {
2964
+ updateState(panel, { justCompleted: true });
2965
+ scheduleAutoClear(panel, "justCompleted");
2966
+ },
2967
+ [updateState, scheduleAutoClear]
2968
+ );
2969
+ const startAsync = useCallback3(
2970
+ (panel) => {
2971
+ setRunning(panel, true);
2972
+ },
2973
+ [setRunning]
2974
+ );
2975
+ const endAsync = useCallback3(
2976
+ (panel, opts) => {
2977
+ setRunning(panel, false);
2978
+ if (opts?.completed) {
2979
+ setCompleted(panel);
2980
+ } else {
2981
+ setRefreshed(panel);
2982
+ }
2983
+ },
2984
+ [setRunning, setRefreshed, setCompleted]
2985
+ );
2986
+ const getState = useCallback3(
2987
+ (panel) => {
2988
+ return states[panel] || { ...DEFAULT_VISUAL_STATE };
2989
+ },
2990
+ [states]
2991
+ );
2992
+ return {
2993
+ states,
2994
+ setRunning,
2995
+ setRefreshed,
2996
+ setCompleted,
2997
+ startAsync,
2998
+ endAsync,
2999
+ getState
3000
+ };
3001
+ }
3002
+
3003
+ // src/ui/OtherSessionsPanel.tsx
3004
+ import { Box as Box4, Text as Text4 } from "ink";
3005
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3006
+ function formatCountdown3(seconds) {
3007
+ if (seconds == null) return "";
3008
+ const padded = String(seconds).padStart(2, " ");
3009
+ return `\u21BB ${padded}s`;
3010
+ }
3011
+ function truncateMessage(message, maxLength) {
3012
+ if (getDisplayWidth(message) <= maxLength) {
3013
+ return message;
3014
+ }
3015
+ let truncated = "";
3016
+ let currentWidth = 0;
3017
+ for (const char of message) {
3018
+ const charWidth = getDisplayWidth(char);
3019
+ if (currentWidth + charWidth > maxLength - 3) {
3020
+ truncated += "...";
3021
+ break;
3022
+ }
3023
+ truncated += char;
3024
+ currentWidth += charWidth;
3025
+ }
3026
+ return truncated;
3027
+ }
3028
+ function formatProjectNames(projectNames, maxWidth) {
3029
+ if (projectNames.length === 0) {
3030
+ return "No projects";
3031
+ }
3032
+ const MAX_NAMES_TO_SHOW = 3;
3033
+ const remaining = projectNames.length - MAX_NAMES_TO_SHOW;
3034
+ let namesToShow = projectNames.slice(0, MAX_NAMES_TO_SHOW);
3035
+ let suffix = remaining > 0 ? ` +${remaining}` : "";
3036
+ const availableWidth = maxWidth;
3037
+ let text = namesToShow.join(", ") + suffix;
3038
+ if (text.length <= availableWidth) {
3039
+ return text;
3040
+ }
3041
+ for (let count = MAX_NAMES_TO_SHOW - 1; count >= 1; count--) {
3042
+ namesToShow = projectNames.slice(0, count);
3043
+ const newRemaining = projectNames.length - count;
3044
+ suffix = newRemaining > 0 ? ` +${newRemaining}` : "";
3045
+ text = namesToShow.join(", ") + suffix;
3046
+ if (text.length <= availableWidth) {
3047
+ return text;
3048
+ }
3049
+ }
3050
+ const firstProject = projectNames[0];
3051
+ const remainingCount = projectNames.length - 1;
3052
+ suffix = remainingCount > 0 ? ` +${remainingCount}` : "";
3053
+ const suffixLen = suffix.length;
3054
+ const maxNameLen = availableWidth - suffixLen - 3;
3055
+ if (maxNameLen > 0) {
3056
+ return `${firstProject.slice(0, maxNameLen)}...${suffix}`;
3057
+ }
3058
+ return "...";
3059
+ }
3060
+ function OtherSessionsPanel({
3061
+ data,
3062
+ countdown,
3063
+ width = DEFAULT_PANEL_WIDTH,
3064
+ isRunning = false,
3065
+ messageMaxLength = 50
3066
+ }) {
3067
+ const countdownSuffix = isRunning ? "running..." : formatCountdown3(countdown);
3068
+ const innerWidth = getInnerWidth(width);
3069
+ const contentWidth = innerWidth - 1;
3070
+ const { activeCount, projectNames, recentSession } = data;
3071
+ const activeSuffix = ` | * ${activeCount} active`;
3072
+ const projectsAvailableWidth = contentWidth - getDisplayWidth(activeSuffix);
3073
+ const projectsText = formatProjectNames(projectNames, projectsAvailableWidth);
3074
+ const headerText = `${projectsText}${activeSuffix}`;
3075
+ const headerPadding = Math.max(0, contentWidth - getDisplayWidth(headerText));
3076
+ const hasProjects = projectNames.length > 0;
3077
+ const hasActive = activeCount > 0;
3078
+ const lines = [];
3079
+ lines.push(
3080
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3081
+ BOX.v,
3082
+ " ",
3083
+ /* @__PURE__ */ jsx4(Text4, { dimColor: !hasProjects, color: hasProjects ? "cyan" : void 0, children: projectsText }),
3084
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: !hasActive, color: hasActive ? "yellow" : void 0, children: [
3085
+ " ",
3086
+ "| * ",
3087
+ activeCount,
3088
+ " active"
3089
+ ] }),
3090
+ " ".repeat(headerPadding),
3091
+ BOX.v
3092
+ ] }, "header")
3093
+ );
3094
+ lines.push(
3095
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3096
+ BOX.v,
3097
+ " ",
3098
+ " ".repeat(contentWidth),
3099
+ BOX.v
3100
+ ] }, "empty")
3101
+ );
3102
+ if (recentSession) {
3103
+ const statusIcon = recentSession.isActive ? "*" : "o";
3104
+ const sessionLine = `${statusIcon} ${recentSession.projectName} (${recentSession.relativeTime})`;
3105
+ const sessionLinePadding = Math.max(
3106
+ 0,
3107
+ contentWidth - getDisplayWidth(sessionLine)
3108
+ );
3109
+ lines.push(
3110
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3111
+ BOX.v,
3112
+ " ",
3113
+ /* @__PURE__ */ jsx4(Text4, { children: sessionLine }),
3114
+ " ".repeat(sessionLinePadding),
3115
+ BOX.v
3116
+ ] }, "session")
3117
+ );
3118
+ if (recentSession.lastMessage) {
3119
+ const indent = " ";
3120
+ const quotePrefix = '"';
3121
+ const quoteSuffix = '"';
3122
+ const availableWidth = contentWidth - indent.length - 2;
3123
+ const truncatedMessage = truncateMessage(
3124
+ recentSession.lastMessage,
3125
+ Math.min(availableWidth, messageMaxLength)
3126
+ );
3127
+ const messageText = `${indent}${quotePrefix}${truncatedMessage}${quoteSuffix}`;
3128
+ const messagePadding = Math.max(
3129
+ 0,
3130
+ contentWidth - getDisplayWidth(messageText)
3131
+ );
3132
+ lines.push(
3133
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3134
+ BOX.v,
3135
+ " ",
3136
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: messageText }),
3137
+ " ".repeat(messagePadding),
3138
+ BOX.v
3139
+ ] }, "message")
3140
+ );
3141
+ }
3142
+ } else {
3143
+ const noSessionText = "No other active sessions";
3144
+ const noSessionPadding = Math.max(0, contentWidth - noSessionText.length);
3145
+ lines.push(
3146
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3147
+ BOX.v,
3148
+ " ",
3149
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: noSessionText }),
3150
+ " ".repeat(noSessionPadding),
3151
+ BOX.v
3152
+ ] }, "no-session")
3153
+ );
3154
+ }
3155
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
3156
+ /* @__PURE__ */ jsx4(Text4, { children: createTitleLine("Other Sessions", countdownSuffix, width) }),
3157
+ lines,
3158
+ /* @__PURE__ */ jsx4(Text4, { children: createBottomLine(width) })
3159
+ ] });
3160
+ }
3161
+
3162
+ // src/ui/ProjectPanel.tsx
3163
+ import { Box as Box5, Text as Text5 } from "ink";
3164
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3165
+ function formatCountdown4(seconds) {
3166
+ if (seconds == null) return "";
3167
+ const padded = String(seconds).padStart(2, " ");
3168
+ return `\u21BB ${padded}s`;
3169
+ }
3170
+ function formatLineCount(count) {
3171
+ if (count >= 1e3) {
3172
+ return `${(count / 1e3).toFixed(1)}k`;
3173
+ }
3174
+ return String(count);
3175
+ }
3176
+ function ProjectPanel({
3177
+ data,
3178
+ countdown,
3179
+ width = DEFAULT_PANEL_WIDTH,
3180
+ isRunning = false,
3181
+ justRefreshed = false
3182
+ }) {
3183
+ const countdownSuffix = isRunning ? "running..." : formatCountdown4(countdown);
3184
+ const innerWidth = getInnerWidth(width);
3185
+ if (data.error) {
3186
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
3187
+ /* @__PURE__ */ jsx5(Text5, { children: createTitleLine("Project", countdownSuffix, width) }),
3188
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3189
+ BOX.v,
3190
+ /* @__PURE__ */ jsx5(Text5, { color: "red", children: padLine(` ${data.error}`, width) }),
3191
+ BOX.v
3192
+ ] }),
3193
+ /* @__PURE__ */ jsx5(Text5, { children: createBottomLine(width) })
3194
+ ] });
3195
+ }
3196
+ const headerParts = [data.name];
3197
+ if (data.language) {
3198
+ headerParts.push(data.language);
3199
+ }
3200
+ if (data.license) {
3201
+ headerParts.push(data.license);
3202
+ }
3203
+ const headerText = headerParts.join(" \xB7 ");
3204
+ const headerPadding = Math.max(0, innerWidth - 1 - headerText.length);
3205
+ const hasStack = data.stack.length > 0;
3206
+ const stackText = hasStack ? `Stack: ${data.stack.join(", ")}` : "";
3207
+ const stackPadding = Math.max(0, innerWidth - 1 - stackText.length);
3208
+ const filesText = `Files: ${data.fileCount} ${data.fileExtension}`;
3209
+ const linesText = `Lines: ${formatLineCount(data.lineCount)}`;
3210
+ const filesLinesText = `${filesText} \xB7 ${linesText}`;
3211
+ const filesLinesPadding = Math.max(0, innerWidth - 1 - filesLinesText.length);
3212
+ let depsText = "Deps: ";
3213
+ if (data.prodDeps > 0 && data.devDeps > 0) {
3214
+ depsText += `${data.prodDeps} prod \xB7 ${data.devDeps} dev`;
3215
+ } else if (data.prodDeps > 0) {
3216
+ depsText += `${data.prodDeps}`;
3217
+ } else if (data.devDeps > 0) {
3218
+ depsText += `${data.devDeps} dev`;
3219
+ } else {
3220
+ depsText += "0";
3221
+ }
3222
+ const depsPadding = Math.max(0, innerWidth - 1 - depsText.length);
3223
+ const _countdownColor = justRefreshed ? "green" : void 0;
3224
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
3225
+ /* @__PURE__ */ jsx5(Text5, { children: createTitleLine("Project", countdownSuffix, width) }),
3226
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3227
+ BOX.v,
3228
+ " ",
3229
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: data.name }),
3230
+ data.language && /* @__PURE__ */ jsxs5(Fragment4, { children: [
3231
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
3232
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: data.language })
3233
+ ] }),
3234
+ data.license && /* @__PURE__ */ jsxs5(Fragment4, { children: [
3235
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
3236
+ /* @__PURE__ */ jsx5(Text5, { children: data.license })
3237
+ ] }),
3238
+ " ".repeat(headerPadding),
3239
+ BOX.v
3240
+ ] }),
3241
+ hasStack && /* @__PURE__ */ jsxs5(Text5, { children: [
3242
+ BOX.v,
3243
+ " ",
3244
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Stack:" }),
3245
+ " ",
3246
+ /* @__PURE__ */ jsx5(Text5, { children: data.stack.join(", ") }),
3247
+ " ".repeat(stackPadding),
3248
+ BOX.v
3249
+ ] }),
3250
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3251
+ BOX.v,
3252
+ " ",
3253
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Files:" }),
3254
+ " ",
3255
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3256
+ data.fileCount,
3257
+ " ",
3258
+ data.fileExtension
3259
+ ] }),
3260
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
3261
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Lines:" }),
3262
+ " ",
3263
+ /* @__PURE__ */ jsx5(Text5, { children: formatLineCount(data.lineCount) }),
3264
+ " ".repeat(filesLinesPadding),
3265
+ BOX.v
3266
+ ] }),
3267
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3268
+ BOX.v,
3269
+ " ",
3270
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Deps:" }),
3271
+ " ",
3272
+ data.prodDeps > 0 && data.devDeps > 0 ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
3273
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3274
+ data.prodDeps,
3275
+ " prod"
3276
+ ] }),
3277
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
3278
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3279
+ data.devDeps,
3280
+ " dev"
3281
+ ] })
3282
+ ] }) : data.prodDeps > 0 ? /* @__PURE__ */ jsx5(Text5, { children: data.prodDeps }) : data.devDeps > 0 ? /* @__PURE__ */ jsxs5(Text5, { children: [
3283
+ data.devDeps,
3284
+ " dev"
3285
+ ] }) : /* @__PURE__ */ jsx5(Text5, { children: "0" }),
3286
+ " ".repeat(depsPadding),
3287
+ BOX.v
3288
+ ] }),
3289
+ /* @__PURE__ */ jsx5(Text5, { children: createBottomLine(width) })
3290
+ ] });
3291
+ }
3292
+
3293
+ // src/ui/TestPanel.tsx
3294
+ import { Box as Box6, Text as Text6 } from "ink";
3295
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3296
+ function formatRelativeTime2(timestamp) {
3297
+ const now = Date.now();
3298
+ const then = new Date(timestamp).getTime();
3299
+ const diffMs = now - then;
3300
+ const diffMins = Math.floor(diffMs / 6e4);
3301
+ const diffHours = Math.floor(diffMs / 36e5);
3302
+ const diffDays = Math.floor(diffMs / 864e5);
3303
+ if (diffMins < 1) return "just now";
3304
+ if (diffMins < 60) return `${diffMins}m ago`;
3305
+ if (diffHours < 24) return `${diffHours}h ago`;
3306
+ return `${diffDays}d ago`;
3307
+ }
3308
+ function createSeparator(panelWidth) {
3309
+ return BOX.ml + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.mr;
3310
+ }
3311
+ function TestPanel({
3312
+ results,
3313
+ isOutdated,
3314
+ commitsBehind,
3315
+ error,
3316
+ width = DEFAULT_PANEL_WIDTH,
3317
+ isRunning = false,
3318
+ justCompleted = false
3319
+ }) {
3320
+ const innerWidth = getInnerWidth(width);
3321
+ const contentWidth = getContentWidth(width);
3322
+ const getTitleSuffix = () => {
3323
+ if (isRunning) return "running...";
3324
+ if (justCompleted) return "just now";
3325
+ if (results) return formatRelativeTime2(results.timestamp);
3326
+ return "";
3327
+ };
3328
+ const titleSuffix = getTitleSuffix();
3329
+ if (error || !results) {
3330
+ const message = error || (isRunning ? "Running..." : "Loading...");
3331
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width, children: [
3332
+ /* @__PURE__ */ jsx6(Text6, { children: createTitleLine("Tests", titleSuffix, width) }),
3333
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3334
+ BOX.v,
3335
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: padLine(` ${message}`, width) }),
3336
+ BOX.v
3337
+ ] }),
3338
+ /* @__PURE__ */ jsx6(Text6, { children: createBottomLine(width) })
3339
+ ] });
3340
+ }
3341
+ const hasFailures = results.failures.length > 0;
3342
+ const relativeTime = titleSuffix;
3343
+ let summaryLength = 1 + 2 + String(results.passed).length + " passed".length;
3344
+ if (results.failed > 0) {
3345
+ summaryLength += 2 + 2 + String(results.failed).length + " failed".length;
3346
+ }
3347
+ if (results.skipped > 0) {
3348
+ summaryLength += 2 + 2 + String(results.skipped).length + " skipped".length;
3349
+ }
3350
+ summaryLength += " \xB7 ".length + results.hash.length;
3351
+ const summaryPadding = Math.max(0, innerWidth - summaryLength);
3352
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", width, children: [
3353
+ /* @__PURE__ */ jsx6(Text6, { children: createTitleLine("Tests", relativeTime, width) }),
3354
+ isOutdated && /* @__PURE__ */ jsxs6(Text6, { children: [
3355
+ BOX.v,
3356
+ /* @__PURE__ */ jsx6(Text6, { color: "yellow", children: padLine(
3357
+ ` \u26A0 Outdated (${commitsBehind} ${commitsBehind === 1 ? "commit" : "commits"} behind)`,
3358
+ width
3359
+ ) }),
3360
+ BOX.v
3361
+ ] }),
3362
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3363
+ BOX.v,
3364
+ " ",
3365
+ /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
3366
+ "\u2713 ",
3367
+ results.passed,
3368
+ " passed"
3369
+ ] }),
3370
+ results.failed > 0 && /* @__PURE__ */ jsxs6(Fragment5, { children: [
3371
+ " ",
3372
+ /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
3373
+ "\u2717 ",
3374
+ results.failed,
3375
+ " failed"
3376
+ ] })
3377
+ ] }),
3378
+ results.skipped > 0 && /* @__PURE__ */ jsxs6(Fragment5, { children: [
3379
+ " ",
3380
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3381
+ "\u25CB ",
3382
+ results.skipped,
3383
+ " skipped"
3384
+ ] })
3385
+ ] }),
3386
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3387
+ " \xB7 ",
3388
+ results.hash
3389
+ ] }),
3390
+ " ".repeat(summaryPadding),
3391
+ BOX.v
3392
+ ] }),
3393
+ hasFailures && /* @__PURE__ */ jsxs6(Fragment5, { children: [
3394
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: createSeparator(width) }),
3395
+ results.failures.map((failure, index) => {
3396
+ const fileName = truncate(failure.file, contentWidth - 3);
3397
+ const filePadding = Math.max(0, innerWidth - 3 - fileName.length);
3398
+ const testName = truncate(failure.name, contentWidth - 5);
3399
+ const testPadding = Math.max(0, innerWidth - 5 - testName.length);
3400
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
3401
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3402
+ BOX.v,
3403
+ " ",
3404
+ /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
3405
+ "\u2717 ",
3406
+ fileName
3407
+ ] }),
3408
+ " ".repeat(filePadding),
3409
+ BOX.v
3410
+ ] }),
3411
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3412
+ BOX.v,
3413
+ " ",
3414
+ "\u2022 ",
3415
+ testName,
3416
+ " ".repeat(testPadding),
3417
+ BOX.v
3418
+ ] })
3419
+ ] }, `failure-${index}`);
3420
+ })
3421
+ ] }),
3422
+ /* @__PURE__ */ jsx6(Text6, { children: createBottomLine(width) })
3423
+ ] });
3424
+ }
3425
+
3426
+ // src/ui/WelcomePanel.tsx
3427
+ import { Box as Box7, Text as Text7 } from "ink";
3428
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3429
+ function WelcomePanel() {
3430
+ return /* @__PURE__ */ jsxs7(
3431
+ Box7,
3432
+ {
3433
+ flexDirection: "column",
3434
+ borderStyle: "single",
3435
+ paddingX: 1,
3436
+ width: DEFAULT_PANEL_WIDTH,
3437
+ children: [
3438
+ /* @__PURE__ */ jsx7(Box7, { marginTop: -1, children: /* @__PURE__ */ jsx7(Text7, { children: " Welcome to agenthud " }) }),
3439
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
3440
+ /* @__PURE__ */ jsx7(Text7, { children: " No .agenthud/ directory found." }),
3441
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
3442
+ /* @__PURE__ */ jsx7(Text7, { children: " Quick setup:" }),
3443
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: " npx agenthud init" }),
3444
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
3445
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " Or visit: github.com/neochoon/agenthud" }),
3446
+ /* @__PURE__ */ jsx7(Text7, { children: " " }),
3447
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " Press q to quit" })
3448
+ ]
3449
+ }
3450
+ );
3451
+ }
3452
+
3453
+ // src/ui/App.tsx
3454
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
3455
+ function formatRelativeTime3(timestamp) {
3456
+ const now = Date.now();
3457
+ const then = new Date(timestamp).getTime();
3458
+ const diffMs = now - then;
3459
+ const diffMins = Math.floor(diffMs / 6e4);
3460
+ const diffHours = Math.floor(diffMs / 36e5);
3461
+ const diffDays = Math.floor(diffMs / 864e5);
3462
+ if (diffMins < 1) return "just now";
3463
+ if (diffMins < 60) return `${diffMins}m ago`;
3464
+ if (diffHours < 24) return `${diffHours}h ago`;
3465
+ return `${diffDays}d ago`;
3466
+ }
3467
+ function WelcomeApp() {
3468
+ const { exit } = useApp();
3469
+ useInput((input) => {
3470
+ if (input === "q") {
3471
+ exit();
3472
+ }
3473
+ });
3474
+ return /* @__PURE__ */ jsx8(WelcomePanel, {});
3475
+ }
3476
+ function getClampedWidth(columns) {
3477
+ if (!columns || columns <= 0) {
3478
+ return DEFAULT_FALLBACK_WIDTH;
3479
+ }
3480
+ return Math.min(Math.max(columns, MIN_TERMINAL_WIDTH), MAX_TERMINAL_WIDTH);
3481
+ }
3482
+ function DashboardApp({
3483
+ mode
3484
+ }) {
3485
+ const { exit } = useApp();
3486
+ const { stdout } = useStdout();
3487
+ const isWatchMode = mode === "watch";
3488
+ const { config, warnings } = useMemo3(() => parseConfig(), []);
3489
+ const getEffectiveWidth = useCallback4(
3490
+ (terminalColumns) => {
3491
+ if (config.width) return config.width;
3492
+ return getClampedWidth(terminalColumns);
3493
+ },
3494
+ [config.width]
3495
+ );
3496
+ const getEffectiveMaxActivities = useCallback4(
3497
+ (terminalRows, todoCount = 0, isWideLayout = false) => {
3498
+ const configMax = config.panels.claude.maxActivities ?? 10;
3499
+ if (!terminalRows || !isWideLayout) {
3500
+ return configMax;
3501
+ }
3502
+ const todoHeight = todoCount > 0 ? 1 + todoCount : 0;
3503
+ const heightBasedMax = Math.max(5, terminalRows - 13 - todoHeight);
3504
+ return Math.max(configMax, heightBasedMax);
3505
+ },
3506
+ [config.panels.claude.maxActivities]
3507
+ );
3508
+ const [width, setWidth] = useState4(() => getEffectiveWidth(stdout?.columns));
3509
+ useEffect4(() => {
3510
+ if (!config.width) {
3511
+ const newWidth = getEffectiveWidth(stdout?.columns);
3512
+ if (newWidth !== width) setWidth(newWidth);
3513
+ }
3514
+ }, [stdout?.columns, width, config.width, getEffectiveWidth]);
3515
+ useEffect4(() => {
3516
+ if (config.width) return;
3517
+ const handleResize = () => setWidth(getEffectiveWidth(stdout?.columns));
3518
+ stdout?.on("resize", handleResize);
3519
+ return () => {
3520
+ stdout?.off("resize", handleResize);
3521
+ };
3522
+ }, [stdout, config.width, getEffectiveWidth]);
3523
+ const customPanelNames = useMemo3(
3524
+ () => Object.keys(config.customPanels || {}),
3525
+ [config.customPanels]
3526
+ );
3527
+ const allPanelNames = useMemo3(
3528
+ () => [
3529
+ "project",
3530
+ "git",
3531
+ "tests",
3532
+ "claude",
3533
+ "other_sessions",
3534
+ ...customPanelNames
3535
+ ],
3536
+ [customPanelNames]
3537
+ );
3538
+ const panelIntervals = useMemo3(() => {
3539
+ const panels = {
3540
+ project: { interval: config.panels.project.interval },
3541
+ git: { interval: config.panels.git.interval },
3542
+ tests: { interval: config.panels.tests.interval },
3543
+ claude: { interval: config.panels.claude.interval },
3544
+ other_sessions: { interval: config.panels.other_sessions.interval }
3545
+ };
3546
+ const customPanels = {};
3547
+ if (config.customPanels) {
3548
+ for (const [name, cfg] of Object.entries(config.customPanels)) {
3549
+ customPanels[name] = { interval: cfg.interval };
3550
+ }
3551
+ }
3552
+ return { panels, customPanels };
3553
+ }, [config]);
3554
+ const { countdowns, reset: resetCountdown } = useCountdown({
3555
+ panels: panelIntervals.panels,
3556
+ customPanels: panelIntervals.customPanels,
3557
+ enabled: isWatchMode
3558
+ });
3559
+ const visualFeedback = useVisualFeedback({ panels: allPanelNames });
3560
+ const cwd = process.cwd();
3561
+ const [projectData, setProjectData] = useState4(
3562
+ () => getProjectData()
3563
+ );
3564
+ const [gitData, setGitData] = useState4(
3565
+ () => getGitData(config.panels.git)
3566
+ );
3567
+ const fetchMaxActivities = Math.max(
3568
+ config.panels.claude.maxActivities ?? 10,
3569
+ stdout?.rows ?? 50
3570
+ );
3571
+ const [claudeData, setClaudeData] = useState4(
3572
+ () => getClaudeData(cwd, fetchMaxActivities, config.panels.claude.sessionTimeout)
3573
+ );
3574
+ const [otherSessionsData, setOtherSessionsData] = useState4(
3575
+ () => getOtherSessionsData(cwd, {
3576
+ activeThresholdMs: config.panels.other_sessions.activeThreshold
3577
+ })
3578
+ );
3579
+ const getTestDataFromConfig = useCallback4(() => {
3580
+ if (config.panels.tests.command) {
3581
+ return runTestCommand(config.panels.tests.command);
3582
+ }
3583
+ return getTestData();
3584
+ }, [config.panels.tests.command]);
3585
+ const [testData, setTestData] = useState4({
3586
+ results: null,
3587
+ isOutdated: false,
3588
+ commitsBehind: 0
3589
+ });
3590
+ const testsInitializedRef = useRef3(false);
3591
+ useEffect4(() => {
3592
+ if (testsInitializedRef.current) return;
3593
+ testsInitializedRef.current = true;
3594
+ const timer = setTimeout(() => {
3595
+ setTestData(getTestDataFromConfig());
3596
+ }, 0);
3597
+ return () => clearTimeout(timer);
3598
+ }, [getTestDataFromConfig]);
3599
+ const [customPanelData, setCustomPanelData] = useState4(() => {
3600
+ const data = {};
3601
+ if (config.customPanels) {
3602
+ for (const [name, panelConfig] of Object.entries(config.customPanels)) {
3603
+ if (panelConfig.enabled) {
3604
+ data[name] = getCustomPanelData(name, panelConfig);
3605
+ }
3606
+ }
3607
+ }
3608
+ return data;
3609
+ });
3610
+ const refreshProject = useCallback4(() => {
3611
+ setProjectData(getProjectData());
3612
+ visualFeedback.setRefreshed("project");
3613
+ resetCountdown("project");
3614
+ }, [visualFeedback, resetCountdown]);
3615
+ const refreshGitAsync = useCallback4(async () => {
3616
+ visualFeedback.startAsync("git");
3617
+ try {
3618
+ const data = await getGitDataAsync(config.panels.git);
3619
+ setGitData(data);
3620
+ } finally {
3621
+ visualFeedback.endAsync("git");
3622
+ resetCountdown("git");
3623
+ }
3624
+ }, [config.panels.git, visualFeedback, resetCountdown]);
3625
+ const refreshTestAsync = useCallback4(async () => {
3626
+ visualFeedback.startAsync("tests");
3627
+ try {
3628
+ await new Promise((resolve) => {
3629
+ setTimeout(() => {
3630
+ setTestData(getTestDataFromConfig());
3631
+ resolve();
3632
+ }, 0);
3633
+ });
3634
+ } finally {
3635
+ visualFeedback.endAsync("tests", { completed: true });
3636
+ }
3637
+ }, [getTestDataFromConfig, visualFeedback]);
3638
+ const refreshClaude = useCallback4(() => {
3639
+ const maxFetch = Math.max(
3640
+ config.panels.claude.maxActivities ?? 10,
3641
+ stdout?.rows ?? 50
3642
+ );
3643
+ setClaudeData(
3644
+ getClaudeData(cwd, maxFetch, config.panels.claude.sessionTimeout)
3645
+ );
3646
+ visualFeedback.setRefreshed("claude");
3647
+ resetCountdown("claude");
3648
+ }, [
3649
+ cwd,
3650
+ stdout?.rows,
3651
+ config.panels.claude.maxActivities,
3652
+ config.panels.claude.sessionTimeout,
3653
+ visualFeedback,
3654
+ resetCountdown
3655
+ ]);
3656
+ const refreshOtherSessions = useCallback4(() => {
3657
+ setOtherSessionsData(
3658
+ getOtherSessionsData(cwd, {
3659
+ activeThresholdMs: config.panels.other_sessions.activeThreshold
3660
+ })
3661
+ );
3662
+ visualFeedback.setRefreshed("other_sessions");
3663
+ resetCountdown("other_sessions");
3664
+ }, [
3665
+ cwd,
3666
+ config.panels.other_sessions.activeThreshold,
3667
+ visualFeedback,
3668
+ resetCountdown
3669
+ ]);
3670
+ const refreshCustomPanelAsync = useCallback4(
3671
+ async (name) => {
3672
+ if (config.customPanels?.[name]) {
3673
+ visualFeedback.startAsync(name);
3674
+ try {
3675
+ const result = await getCustomPanelDataAsync(
3676
+ name,
3677
+ config.customPanels[name]
3678
+ );
3679
+ setCustomPanelData((prev) => ({ ...prev, [name]: result }));
3680
+ } finally {
3681
+ visualFeedback.endAsync(name);
3682
+ resetCountdown(name);
3683
+ }
3684
+ }
3685
+ },
3686
+ [config.customPanels, visualFeedback, resetCountdown]
3687
+ );
3688
+ const refreshAll = useCallback4(() => {
3689
+ if (config.panels.project.enabled) refreshProject();
3690
+ if (config.panels.git.enabled) void refreshGitAsync();
3691
+ if (config.panels.tests.enabled) void refreshTestAsync();
3692
+ if (config.panels.claude.enabled) refreshClaude();
3693
+ if (config.panels.other_sessions.enabled) refreshOtherSessions();
3694
+ for (const name of customPanelNames) {
3695
+ if (config.customPanels?.[name].enabled) {
3696
+ void refreshCustomPanelAsync(name);
3697
+ }
3698
+ }
3699
+ }, [
3700
+ config,
3701
+ customPanelNames,
3702
+ refreshProject,
3703
+ refreshGitAsync,
3704
+ refreshTestAsync,
3705
+ refreshClaude,
3706
+ refreshOtherSessions,
3707
+ refreshCustomPanelAsync
3708
+ ]);
3709
+ const manualPanels = useMemo3(() => {
3710
+ const panels = [];
3711
+ if (config.panels.tests.enabled && config.panels.tests.interval === null) {
3712
+ panels.push({
3713
+ name: "tests",
3714
+ label: "run tests",
3715
+ action: () => void refreshTestAsync()
3716
+ });
3717
+ }
3718
+ if (config.customPanels) {
3719
+ for (const [name, panelConfig] of Object.entries(config.customPanels)) {
3720
+ if (panelConfig.enabled && panelConfig.interval === null) {
3721
+ panels.push({
3722
+ name,
3723
+ label: `run ${name}`,
3724
+ action: () => void refreshCustomPanelAsync(name)
3725
+ });
3726
+ }
3727
+ }
3728
+ }
3729
+ return panels;
3730
+ }, [config, refreshTestAsync, refreshCustomPanelAsync]);
3731
+ const { handleInput, statusBarItems } = useHotkeys({
3732
+ manualPanels,
3733
+ onRefreshAll: refreshAll,
3734
+ onQuit: exit
3735
+ });
3736
+ useInput(
3737
+ (input) => {
3738
+ handleInput(input);
3739
+ },
3740
+ { isActive: isWatchMode }
3741
+ );
3742
+ const refreshProjectRef = useRef3(refreshProject);
3743
+ const refreshGitAsyncRef = useRef3(refreshGitAsync);
3744
+ const refreshTestAsyncRef = useRef3(refreshTestAsync);
3745
+ const refreshClaudeRef = useRef3(refreshClaude);
3746
+ const refreshOtherSessionsRef = useRef3(refreshOtherSessions);
3747
+ const refreshCustomPanelAsyncRef = useRef3(refreshCustomPanelAsync);
3748
+ refreshProjectRef.current = refreshProject;
3749
+ refreshGitAsyncRef.current = refreshGitAsync;
3750
+ refreshTestAsyncRef.current = refreshTestAsync;
3751
+ refreshClaudeRef.current = refreshClaude;
3752
+ refreshOtherSessionsRef.current = refreshOtherSessions;
3753
+ refreshCustomPanelAsyncRef.current = refreshCustomPanelAsync;
3754
+ useEffect4(() => {
3755
+ if (!isWatchMode) return;
3756
+ const timers = [];
3757
+ if (config.panels.project.enabled && config.panels.project.interval !== null) {
3758
+ timers.push(
3759
+ setInterval(
3760
+ () => refreshProjectRef.current(),
3761
+ config.panels.project.interval
3762
+ )
3763
+ );
3764
+ }
3765
+ if (config.panels.git.enabled && config.panels.git.interval !== null) {
3766
+ timers.push(
3767
+ setInterval(
3768
+ () => void refreshGitAsyncRef.current(),
3769
+ config.panels.git.interval
3770
+ )
3771
+ );
3772
+ }
3773
+ if (config.panels.tests.enabled && config.panels.tests.interval !== null) {
3774
+ timers.push(
3775
+ setInterval(
3776
+ () => void refreshTestAsyncRef.current(),
3777
+ config.panels.tests.interval
3778
+ )
3779
+ );
3780
+ }
3781
+ if (config.panels.claude.enabled && config.panels.claude.interval !== null) {
3782
+ timers.push(
3783
+ setInterval(
3784
+ () => refreshClaudeRef.current(),
3785
+ config.panels.claude.interval
3786
+ )
3787
+ );
3788
+ }
3789
+ if (config.panels.other_sessions.enabled && config.panels.other_sessions.interval !== null) {
3790
+ timers.push(
3791
+ setInterval(
3792
+ () => refreshOtherSessionsRef.current(),
3793
+ config.panels.other_sessions.interval
3794
+ )
3795
+ );
3796
+ }
3797
+ if (config.customPanels) {
3798
+ for (const [name, panelConfig] of Object.entries(config.customPanels)) {
3799
+ if (panelConfig.enabled && panelConfig.interval !== null) {
3800
+ timers.push(
3801
+ setInterval(
3802
+ () => void refreshCustomPanelAsyncRef.current(name),
3803
+ panelConfig.interval
3804
+ )
3805
+ );
3806
+ }
3807
+ }
3808
+ }
3809
+ return () => timers.forEach((t) => clearInterval(t));
3810
+ }, [isWatchMode, config]);
3811
+ const terminalWidth = stdout?.columns ?? 0;
3812
+ const terminalHeight = stdout?.rows ?? 0;
3813
+ const columnGap = 2;
3814
+ const effectiveThreshold = config.wideLayoutThreshold ?? MIN_TERMINAL_WIDTH * 2 + columnGap;
3815
+ const useWideLayout = terminalWidth >= effectiveThreshold;
3816
+ const singleColumnWidth = getClampedWidth(terminalWidth);
3817
+ const leftColumnWidth = useWideLayout ? Math.floor((terminalWidth - columnGap) / 2) : singleColumnWidth;
3818
+ const rightColumnWidth = useWideLayout ? terminalWidth - leftColumnWidth - columnGap : singleColumnWidth;
3819
+ const claudeMaxActivities = useMemo3(() => {
3820
+ if (!useWideLayout) return void 0;
3821
+ const todos = claudeData.state.todos;
3822
+ const hasTodos = todos && todos.length > 0;
3823
+ const allCompleted = hasTodos && todos.every((t) => t.status === "completed");
3824
+ const activeTodoCount = hasTodos && !allCompleted ? todos.length : 0;
3825
+ return getEffectiveMaxActivities(terminalHeight, activeTodoCount, true);
3826
+ }, [
3827
+ useWideLayout,
3828
+ claudeData.state.todos,
3829
+ terminalHeight,
3830
+ getEffectiveMaxActivities
3831
+ ]);
3832
+ const renderPanel = (panelName, panelWidth, marginTop) => {
3833
+ if (panelName === "project" && config.panels.project.enabled) {
3834
+ const vs = visualFeedback.getState("project");
3835
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3836
+ ProjectPanel,
3837
+ {
3838
+ data: projectData,
3839
+ countdown: isWatchMode ? countdowns.project : null,
3840
+ width: panelWidth,
3841
+ justRefreshed: vs.justRefreshed
3842
+ }
3843
+ ) }, `panel-${panelName}`);
3844
+ }
3845
+ if (panelName === "git" && config.panels.git.enabled) {
3846
+ const vs = visualFeedback.getState("git");
3847
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3848
+ GitPanel,
3849
+ {
3850
+ branch: gitData.branch,
3851
+ commits: gitData.commits,
3852
+ stats: gitData.stats,
3853
+ uncommitted: gitData.uncommitted,
3854
+ countdown: isWatchMode ? countdowns.git : null,
3855
+ width: panelWidth,
3856
+ isRunning: vs.isRunning,
3857
+ justRefreshed: vs.justRefreshed
3858
+ }
3859
+ ) }, `panel-${panelName}`);
3860
+ }
3861
+ if (panelName === "tests" && config.panels.tests.enabled) {
3862
+ const vs = visualFeedback.getState("tests");
3863
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3864
+ TestPanel,
3865
+ {
3866
+ results: testData.results,
3867
+ isOutdated: testData.isOutdated,
3868
+ commitsBehind: testData.commitsBehind,
3869
+ error: testData.error,
3870
+ width: panelWidth,
3871
+ isRunning: vs.isRunning,
3872
+ justCompleted: vs.justCompleted
3873
+ }
3874
+ ) }, `panel-${panelName}`);
3875
+ }
3876
+ if (panelName === "claude" && config.panels.claude.enabled) {
3877
+ const vs = visualFeedback.getState("claude");
3878
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3879
+ ClaudePanel,
3880
+ {
3881
+ data: claudeData,
3882
+ countdown: isWatchMode ? countdowns.claude : null,
3883
+ width: panelWidth,
3884
+ justRefreshed: vs.justRefreshed,
3885
+ maxActivities: claudeMaxActivities
3886
+ }
3887
+ ) }, `panel-${panelName}`);
3888
+ }
3889
+ if (panelName === "other_sessions" && config.panels.other_sessions.enabled) {
3890
+ const vs = visualFeedback.getState("other_sessions");
3891
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3892
+ OtherSessionsPanel,
3893
+ {
3894
+ data: otherSessionsData,
3895
+ countdown: isWatchMode ? countdowns.other_sessions : null,
3896
+ width: panelWidth,
3897
+ isRunning: vs.isRunning,
3898
+ messageMaxLength: config.panels.other_sessions.messageMaxLength
3899
+ }
3900
+ ) }, `panel-${panelName}`);
3901
+ }
3902
+ const customConfig = config.customPanels?.[panelName];
3903
+ if (customConfig?.enabled) {
3904
+ const result = customPanelData[panelName];
3905
+ if (!result) return null;
3906
+ const vs = visualFeedback.getState(panelName);
3907
+ const isManual = customConfig.interval === null;
3908
+ const relativeTime = isManual ? formatRelativeTime3(result.timestamp) : void 0;
3909
+ const countdown = !isManual && isWatchMode ? countdowns[panelName] : null;
3910
+ return /* @__PURE__ */ jsx8(Box8, { marginTop, children: /* @__PURE__ */ jsx8(
3911
+ GenericPanel,
3912
+ {
3913
+ data: result.data,
3914
+ renderer: customConfig.renderer,
3915
+ countdown,
3916
+ relativeTime,
3917
+ error: result.error,
3918
+ width: panelWidth,
3919
+ isRunning: vs.isRunning,
3920
+ justRefreshed: vs.justRefreshed
3921
+ }
3922
+ ) }, `panel-${panelName}`);
3923
+ }
3924
+ return null;
3925
+ };
3926
+ if (useWideLayout) {
3927
+ const leftPanels = ["claude", "other_sessions"];
3928
+ const rightPanels = config.panelOrder.filter(
3929
+ (name) => !leftPanels.includes(name)
3930
+ );
3931
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3932
+ warnings.length > 0 && /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
3933
+ "\u26A0 ",
3934
+ warnings.join(", ")
3935
+ ] }) }),
3936
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", children: [
3937
+ /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", width: leftColumnWidth, children: [
3938
+ renderPanel("claude", leftColumnWidth, 0),
3939
+ renderPanel("other_sessions", leftColumnWidth, 1)
3940
+ ] }),
3941
+ /* @__PURE__ */ jsx8(
3942
+ Box8,
3943
+ {
3944
+ flexDirection: "column",
3945
+ width: rightColumnWidth,
3946
+ marginLeft: columnGap,
3947
+ children: rightPanels.map(
3948
+ (panelName, index) => renderPanel(panelName, rightColumnWidth, index === 0 ? 0 : 1)
3949
+ )
3950
+ }
3951
+ )
3952
+ ] }),
3953
+ isWatchMode && /* @__PURE__ */ jsxs8(
3954
+ Box8,
3955
+ {
3956
+ marginTop: 1,
3957
+ width: terminalWidth,
3958
+ justifyContent: "space-between",
3959
+ children: [
3960
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs8(React.Fragment, { children: [
3961
+ index > 0 && " \xB7 ",
3962
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
3963
+ item.split(":")[0],
3964
+ ":"
3965
+ ] }),
3966
+ item.split(":").slice(1).join(":")
3967
+ ] }, index)) }),
3968
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3969
+ "AgentHUD v",
3970
+ getVersion()
3971
+ ] })
3972
+ ]
3973
+ }
3974
+ )
3975
+ ] });
3976
+ }
3977
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
3978
+ warnings.length > 0 && /* @__PURE__ */ jsx8(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
3979
+ "\u26A0 ",
3980
+ warnings.join(", ")
3981
+ ] }) }),
3982
+ config.panelOrder.map(
3983
+ (panelName, index) => renderPanel(panelName, width, index === 0 ? 0 : 1)
3984
+ ),
3985
+ isWatchMode && /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, width, justifyContent: "space-between", children: [
3986
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs8(React.Fragment, { children: [
3987
+ index > 0 && " \xB7 ",
3988
+ /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
3989
+ item.split(":")[0],
3990
+ ":"
3991
+ ] }),
3992
+ item.split(":").slice(1).join(":")
3993
+ ] }, index)) }),
3994
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3995
+ "AgentHUD v",
3996
+ getVersion()
3997
+ ] })
3998
+ ] })
3999
+ ] });
4000
+ }
4001
+ function App({
4002
+ mode,
4003
+ agentDirExists = true
4004
+ }) {
4005
+ if (!agentDirExists) {
4006
+ return /* @__PURE__ */ jsx8(WelcomeApp, {});
4007
+ }
4008
+ return /* @__PURE__ */ jsx8(DashboardApp, { mode });
4009
+ }
4010
+
4011
+ // src/utils/performance.ts
4012
+ import { performance } from "perf_hooks";
4013
+ var DEFAULT_CLEANUP_INTERVAL = 6e4;
4014
+ var cleanupInterval = null;
4015
+ function clearPerformanceEntries() {
4016
+ performance.clearMarks();
4017
+ performance.clearMeasures();
4018
+ }
4019
+ function startPerformanceCleanup(intervalMs = DEFAULT_CLEANUP_INTERVAL) {
4020
+ if (cleanupInterval !== null) {
4021
+ clearInterval(cleanupInterval);
4022
+ }
4023
+ cleanupInterval = setInterval(() => {
4024
+ clearPerformanceEntries();
4025
+ }, intervalMs);
4026
+ }
4027
+ function stopPerformanceCleanup() {
4028
+ if (cleanupInterval !== null) {
4029
+ clearInterval(cleanupInterval);
4030
+ cleanupInterval = null;
4031
+ }
4032
+ }
4033
+
4034
+ // src/main.ts
4035
+ function main() {
4036
+ const options = parseArgs(process.argv.slice(2));
4037
+ if (options.command === "help") {
4038
+ console.log(getHelp());
4039
+ process.exit(0);
4040
+ }
4041
+ if (options.command === "version") {
4042
+ console.log(getVersion());
4043
+ process.exit(0);
4044
+ }
4045
+ if (options.command === "init") {
4046
+ const result = runInit();
4047
+ console.log("\n\u2713 agenthud initialized\n");
4048
+ if (result.created.length > 0) {
4049
+ console.log("Created:");
4050
+ result.created.forEach((file) => console.log(` ${file}`));
4051
+ }
4052
+ if (result.skipped.length > 0) {
4053
+ console.log("\nSkipped (already exists):");
4054
+ result.skipped.forEach((file) => console.log(` ${file}`));
4055
+ }
4056
+ if (result.warnings.length > 0) {
4057
+ console.log("\nWarnings:");
4058
+ result.warnings.forEach((warning) => console.log(` \u26A0 ${warning}`));
4059
+ }
4060
+ console.log("\nNext steps:");
4061
+ console.log(" Run: npx agenthud\n");
4062
+ process.exit(0);
4063
+ }
4064
+ const cwd = process.cwd();
4065
+ const sessionAvailability = checkSessionAvailability(cwd);
4066
+ if (!sessionAvailability.hasCurrentSession) {
4067
+ if (sessionAvailability.otherProjects.length > 0) {
4068
+ console.log("\nProjects with Claude Code sessions:");
4069
+ sessionAvailability.otherProjects.forEach((project, index) => {
4070
+ const shortPath = shortenPath(project.path);
4071
+ console.log(` ${index + 1}. ${project.name} (${shortPath})`);
4072
+ });
4073
+ const firstProject = sessionAvailability.otherProjects[0];
4074
+ const firstShortPath = shortenPath(firstProject.path);
4075
+ console.log(`
4076
+ Run: cd ${firstShortPath} && agenthud
4077
+ `);
4078
+ } else {
4079
+ console.log("\nCould not find any projects with Claude Code sessions.\n");
4080
+ console.log("Start a Claude Code session in a project directory first:");
4081
+ console.log(" $ claude\n");
4082
+ }
4083
+ process.exit(0);
4084
+ }
4085
+ const agentDirExists = existsSync9(".agenthud");
4086
+ if (options.mode === "watch") {
4087
+ clearScreen();
4088
+ }
4089
+ const { waitUntilExit } = render(
4090
+ React2.createElement(App, { mode: options.mode, agentDirExists })
4091
+ );
4092
+ if (options.mode === "once") {
4093
+ setTimeout(() => process.exit(0), 100);
4094
+ } else {
4095
+ startPerformanceCleanup();
4096
+ waitUntilExit().then(() => {
4097
+ stopPerformanceCleanup();
4098
+ process.exit(0);
4099
+ });
4100
+ }
4101
+ }
4102
+ export {
4103
+ main
4104
+ };