agenthud 0.7.0 → 0.7.2

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