agenthud 0.7.4 → 0.8.0

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