agenthud 0.5.16 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,16 +67,99 @@ panels:
67
67
  interval: 10s
68
68
  ```
69
69
 
70
+ ## Panels
71
+
72
+ ### Claude Panel
73
+
74
+ Shows real-time Claude Code activity:
75
+
76
+ ```
77
+ ┌─ Claude ─────────────────────────────────────────────┐
78
+ │ [10:23:45] ○ Read: src/components/Button.tsx │
79
+ │ [10:23:46] ~ Edit: src/components/Button.tsx │
80
+ │ [10:23:47] $ Bash: npm test │
81
+ │ [10:23:50] < Response: Tests passed successfully... │
82
+ └──────────────────────────────────────────────────────┘
83
+ ```
84
+
85
+ - **○ Read**: File being read
86
+ - **~ Edit/Write**: File being modified
87
+ - **$ Bash**: Command being executed
88
+ - **< Response**: Claude's text response
89
+
90
+ ### Git Panel
91
+
92
+ Shows today's git activity and current state:
93
+
94
+ ```
95
+ ┌─ Git ────────────────────────────────────────────────┐
96
+ │ feat/add-dashboard · +142 -23 · 3 commits · 5 files │
97
+ │ • abc1234 Add dashboard component │
98
+ │ • def5678 Fix styling issues │
99
+ └──────────────────────────────────────────────────────┘
100
+ ```
101
+
102
+ - **Branch name**: Current working branch (green)
103
+ - **Stats**: Lines added/deleted, commits, files changed
104
+ - **dirty**: Shows uncommitted change count (yellow)
105
+
106
+ ### Tests Panel
107
+
108
+ Shows test results with staleness detection:
109
+
110
+ ```
111
+ ┌─ Tests ──────────────────────────────────────────────┐
112
+ │ ✓ 42 passed ✗ 1 failed ○ 2 skipped · abc1234 │
113
+ │ ⚠ Outdated (3 commits behind) │
114
+ │──────────────────────────────────────────────────────│
115
+ │ ✗ Button.test.tsx │
116
+ │ • should render correctly │
117
+ └──────────────────────────────────────────────────────┘
118
+ ```
119
+
120
+ - **✓ passed** (green), **✗ failed** (red), **○ skipped**
121
+ - **⚠ Outdated**: Warning if tests are behind commits
122
+ - **Failures**: Shows failing test file and name
123
+
124
+ **Auto-detection**: During `agenthud init`, the test framework is automatically detected:
125
+
126
+ | Framework | Detection |
127
+ |-----------|-----------|
128
+ | vitest | package.json devDependencies |
129
+ | jest | package.json devDependencies |
130
+ | mocha | package.json devDependencies |
131
+ | pytest | pytest.ini, conftest.py, pyproject.toml, requirements.txt |
132
+
133
+ If the test command fails, the panel is automatically disabled.
134
+
135
+ ### Project Panel
136
+
137
+ Shows project overview and structure:
138
+
139
+ ```
140
+ ┌─ Project ────────────────────────────────────────────┐
141
+ │ agenthud · TypeScript · MIT │
142
+ │ Stack: react, ink, vitest │
143
+ │ Files: 45 .ts · Lines: 3.2k │
144
+ │ Deps: 12 prod · 8 dev │
145
+ └──────────────────────────────────────────────────────┘
146
+ ```
147
+
148
+ - **Name/Language/License**: Project basics
149
+ - **Stack**: Detected frameworks and tools
150
+ - **Files/Lines**: Source code stats
151
+ - **Deps**: Dependency counts
152
+
70
153
  ### Other Sessions Panel
71
154
 
72
155
  Shows activity from your other Claude Code projects:
73
156
 
74
157
  ```
75
158
  ┌─ Other Sessions ─────────────────────────────────────┐
76
- │ 📁 dotfiles, pain-radar, myapp +4 | ⚡ 1 active
159
+ │ 📁 dotfiles, pain-radar, myapp +4 | ⚡ 1 active
77
160
  │ │
78
- │ 🔵 dotfiles (2m ago)
79
- │ "Updated the config file as requested..."
161
+ │ 🔵 dotfiles (2m ago)
162
+ │ "Updated the config file as requested..."
80
163
  └──────────────────────────────────────────────────────┘
81
164
  ```
82
165
 
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import React2 from "react";
4
+ import React3 from "react";
5
5
  import { render } from "ink";
6
6
  import { existsSync } from "fs";
7
7
 
8
8
  // src/ui/App.tsx
9
- import React, { useState, useEffect, useCallback, useMemo } from "react";
9
+ import React2, { useState as useState2, useEffect as useEffect2, useCallback, useMemo } from "react";
10
10
  import { Box as Box8, Text as Text8, useApp, useInput, useStdout } from "ink";
11
11
 
12
12
  // src/ui/GitPanel.tsx
@@ -48,6 +48,13 @@ function createTitleLine(label, suffix = "", panelWidth = DEFAULT_PANEL_WIDTH) {
48
48
  function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
49
49
  return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
50
50
  }
51
+ function createSeparatorLine(title, panelWidth = DEFAULT_PANEL_WIDTH) {
52
+ const leftPart = BOX.h + " " + title + " ";
53
+ const leftWidth = leftPart.length;
54
+ const dashCount = panelWidth - 1 - leftWidth - 1;
55
+ const dashes = BOX.h.repeat(Math.max(0, dashCount));
56
+ return BOX.ml + leftPart + dashes + BOX.mr;
57
+ }
51
58
  function padLine(content, panelWidth = DEFAULT_PANEL_WIDTH) {
52
59
  const innerWidth = getInnerWidth(panelWidth);
53
60
  const padding = innerWidth - content.length;
@@ -421,8 +428,9 @@ function ProjectPanel({
421
428
  }
422
429
 
423
430
  // src/ui/ClaudePanel.tsx
431
+ import { useState, useEffect } from "react";
424
432
  import { Box as Box4, Text as Text4 } from "ink";
425
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
433
+ import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
426
434
  function getActivityStyle(activity) {
427
435
  if (activity.type === "user") {
428
436
  return { color: "white", dimColor: false };
@@ -481,15 +489,46 @@ function formatActivityParts(activity, maxWidth) {
481
489
  const icon = activity.icon;
482
490
  const label = activity.label;
483
491
  const detail = activity.detail;
492
+ const count = activity.count;
493
+ const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
494
+ const countSuffixWidth = countSuffix.length;
495
+ const skipLabel = label === "User" || label === "Response";
484
496
  const timestamp = `[${time}] `;
485
497
  const timestampWidth = timestamp.length;
486
498
  const iconWidth = getDisplayWidth(icon);
499
+ if (skipLabel && detail) {
500
+ const prefixWidth = timestampWidth + iconWidth + 1;
501
+ const availableWidth = maxWidth - prefixWidth - countSuffixWidth;
502
+ let truncatedDetail = detail;
503
+ let detailDisplayWidth = getDisplayWidth(detail);
504
+ if (detailDisplayWidth > availableWidth) {
505
+ truncatedDetail = "";
506
+ let currentWidth = 0;
507
+ for (const char of detail) {
508
+ const charWidth = getDisplayWidth(char);
509
+ if (currentWidth + charWidth > availableWidth - 3) {
510
+ truncatedDetail += "...";
511
+ currentWidth += 3;
512
+ break;
513
+ }
514
+ truncatedDetail += char;
515
+ currentWidth += charWidth;
516
+ }
517
+ detailDisplayWidth = currentWidth;
518
+ }
519
+ return {
520
+ timestamp,
521
+ icon,
522
+ labelContent: truncatedDetail + countSuffix,
523
+ displayWidth: prefixWidth + detailDisplayWidth + countSuffixWidth
524
+ };
525
+ }
487
526
  const labelWidth = label.length;
488
527
  const separatorWidth = detail ? 2 : 0;
489
528
  const contentPrefixWidth = iconWidth + 1 + labelWidth + separatorWidth;
490
529
  const totalPrefixWidth = timestampWidth + contentPrefixWidth;
491
530
  if (detail) {
492
- const availableWidth = maxWidth - totalPrefixWidth;
531
+ const availableWidth = maxWidth - totalPrefixWidth - countSuffixWidth;
493
532
  let truncatedDetail = detail;
494
533
  let detailDisplayWidth = getDisplayWidth(detail);
495
534
  if (detailDisplayWidth > availableWidth) {
@@ -507,14 +546,80 @@ function formatActivityParts(activity, maxWidth) {
507
546
  }
508
547
  detailDisplayWidth = currentWidth;
509
548
  }
510
- const labelContent2 = `${label}: ${truncatedDetail}`;
511
- const displayWidth2 = totalPrefixWidth + detailDisplayWidth;
549
+ const labelContent2 = `${label}: ${truncatedDetail}${countSuffix}`;
550
+ const displayWidth2 = totalPrefixWidth + detailDisplayWidth + countSuffixWidth;
512
551
  return { timestamp, icon, labelContent: labelContent2, displayWidth: displayWidth2 };
513
552
  }
514
- const labelContent = label;
515
- const displayWidth = totalPrefixWidth;
553
+ const labelContent = label + countSuffix;
554
+ const displayWidth = totalPrefixWidth + countSuffixWidth;
516
555
  return { timestamp, icon, labelContent, displayWidth };
517
556
  }
557
+ var TODO_ICONS = {
558
+ completed: "\u2713",
559
+ in_progress_left: "\u25D0",
560
+ in_progress_right: "\u25D1",
561
+ pending: "\u25CB"
562
+ };
563
+ function TodoSection({ todos, width }) {
564
+ const [tick, setTick] = useState(false);
565
+ const innerWidth = getInnerWidth(width);
566
+ const contentWidth = innerWidth - 1;
567
+ useEffect(() => {
568
+ const timer = setInterval(() => setTick((t) => !t), 500);
569
+ return () => clearInterval(timer);
570
+ }, []);
571
+ const completedCount = todos.filter((t) => t.status === "completed").length;
572
+ const totalCount = todos.length;
573
+ const headerTitle = `Todo (${completedCount}/${totalCount})`;
574
+ const inProgressIcon = tick ? TODO_ICONS.in_progress_left : TODO_ICONS.in_progress_right;
575
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
576
+ /* @__PURE__ */ jsx4(Text4, { children: createSeparatorLine(headerTitle, width) }),
577
+ todos.map((todo, i) => {
578
+ let icon;
579
+ let iconColor;
580
+ switch (todo.status) {
581
+ case "completed":
582
+ icon = TODO_ICONS.completed;
583
+ iconColor = "green";
584
+ break;
585
+ case "in_progress":
586
+ icon = inProgressIcon;
587
+ iconColor = "yellow";
588
+ break;
589
+ default:
590
+ icon = TODO_ICONS.pending;
591
+ iconColor = void 0;
592
+ }
593
+ const text = todo.status === "in_progress" ? todo.activeForm : todo.content;
594
+ const maxTextWidth = contentWidth - 3;
595
+ let displayText = text;
596
+ if (getDisplayWidth(text) > maxTextWidth) {
597
+ displayText = "";
598
+ let currentWidth = 0;
599
+ for (const char of text) {
600
+ const charWidth = getDisplayWidth(char);
601
+ if (currentWidth + charWidth > maxTextWidth - 3) {
602
+ displayText += "...";
603
+ currentWidth += 3;
604
+ break;
605
+ }
606
+ displayText += char;
607
+ currentWidth += charWidth;
608
+ }
609
+ }
610
+ const padding = Math.max(0, contentWidth - getDisplayWidth(icon) - 1 - getDisplayWidth(displayText));
611
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
612
+ BOX.v,
613
+ " ",
614
+ /* @__PURE__ */ jsx4(Text4, { color: iconColor, children: icon }),
615
+ " ",
616
+ /* @__PURE__ */ jsx4(Text4, { dimColor: todo.status === "completed", children: displayText }),
617
+ " ".repeat(padding),
618
+ BOX.v
619
+ ] }, `todo-${i}`);
620
+ })
621
+ ] });
622
+ }
518
623
  function ClaudePanel({
519
624
  data,
520
625
  countdown,
@@ -607,9 +712,51 @@ function ClaudePanel({
607
712
  ] }, "tokens")
608
713
  );
609
714
  }
715
+ const hasTodos = state.todos && state.todos.length > 0;
716
+ const allCompleted = hasTodos && state.todos.every((t) => t.status === "completed");
717
+ if (hasTodos && allCompleted) {
718
+ const todos = state.todos;
719
+ const summaryText = `Todo (${todos.length}/${todos.length} done)`;
720
+ const summaryIcon = "\u2713";
721
+ const timestamp = formatActivityTime(/* @__PURE__ */ new Date());
722
+ const timestampStr = `[${timestamp}] `;
723
+ const timestampWidth = timestampStr.length;
724
+ const iconWidth = getDisplayWidth(summaryIcon);
725
+ const prefixWidth = timestampWidth + iconWidth + 1;
726
+ const maxTextWidth = contentWidth - prefixWidth;
727
+ let displaySummary = summaryText;
728
+ if (getDisplayWidth(summaryText) > maxTextWidth) {
729
+ displaySummary = "";
730
+ let currentWidth = 0;
731
+ for (const char of summaryText) {
732
+ const charWidth = getDisplayWidth(char);
733
+ if (currentWidth + charWidth > maxTextWidth - 3) {
734
+ displaySummary += "...";
735
+ break;
736
+ }
737
+ displaySummary += char;
738
+ currentWidth += charWidth;
739
+ }
740
+ }
741
+ const summaryPadding = Math.max(0, contentWidth - prefixWidth - getDisplayWidth(displaySummary));
742
+ lines.push(
743
+ /* @__PURE__ */ jsxs4(Text4, { children: [
744
+ BOX.v,
745
+ " ",
746
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: timestampStr }),
747
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: summaryIcon }),
748
+ " ",
749
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: displaySummary }),
750
+ " ".repeat(summaryPadding),
751
+ BOX.v
752
+ ] }, "todo-summary")
753
+ );
754
+ }
755
+ const showTodoSection = hasTodos && !allCompleted;
610
756
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
611
757
  /* @__PURE__ */ jsx4(Text4, { children: createTitleLine("Claude", titleSuffix, width) }),
612
758
  lines,
759
+ showTodoSection && /* @__PURE__ */ jsx4(TodoSection, { todos: state.todos, width }),
613
760
  /* @__PURE__ */ jsx4(Text4, { children: createBottomLine(width) })
614
761
  ] });
615
762
  }
@@ -768,7 +915,7 @@ function OtherSessionsPanel({
768
915
 
769
916
  // src/ui/GenericPanel.tsx
770
917
  import { Box as Box6, Text as Text6 } from "ink";
771
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
918
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
772
919
  var PROGRESS_BAR_WIDTH = 10;
773
920
  function createProgressBar(done, total) {
774
921
  if (total === 0) return "\u2591".repeat(PROGRESS_BAR_WIDTH);
@@ -804,7 +951,7 @@ function ListRenderer({ data, width }) {
804
951
  BOX.v
805
952
  ] });
806
953
  }
807
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
954
+ return /* @__PURE__ */ jsxs6(Fragment5, { children: [
808
955
  data.summary && /* @__PURE__ */ jsxs6(Text6, { children: [
809
956
  BOX.v,
810
957
  padLine(" " + truncate(data.summary, contentWidth), width),
@@ -821,7 +968,7 @@ function ListRenderer({ data, width }) {
821
968
  function ProgressRenderer({ data, width }) {
822
969
  const items = data.items || [];
823
970
  const contentWidth = getContentWidth(width);
824
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
971
+ return /* @__PURE__ */ jsxs6(Fragment5, { children: [
825
972
  data.summary && /* @__PURE__ */ jsxs6(Text6, { children: [
826
973
  BOX.v,
827
974
  padLine(" " + truncate(data.summary, contentWidth), width),
@@ -856,7 +1003,7 @@ function StatusRenderer({ data, width }) {
856
1003
  summaryLength += 2 + 2 + String(stats.skipped).length + " skipped".length;
857
1004
  }
858
1005
  const summaryPadding = Math.max(0, innerWidth - summaryLength);
859
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
1006
+ return /* @__PURE__ */ jsxs6(Fragment5, { children: [
860
1007
  data.summary && /* @__PURE__ */ jsxs6(Text6, { children: [
861
1008
  BOX.v,
862
1009
  padLine(" " + truncate(data.summary, contentWidth), width),
@@ -870,7 +1017,7 @@ function StatusRenderer({ data, width }) {
870
1017
  stats.passed,
871
1018
  " passed"
872
1019
  ] }),
873
- stats.failed > 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
1020
+ stats.failed > 0 && /* @__PURE__ */ jsxs6(Fragment5, { children: [
874
1021
  " ",
875
1022
  /* @__PURE__ */ jsxs6(Text6, { color: "red", children: [
876
1023
  "\u2717 ",
@@ -878,7 +1025,7 @@ function StatusRenderer({ data, width }) {
878
1025
  " failed"
879
1026
  ] })
880
1027
  ] }),
881
- stats.skipped && stats.skipped > 0 && /* @__PURE__ */ jsxs6(Fragment4, { children: [
1028
+ stats.skipped && stats.skipped > 0 && /* @__PURE__ */ jsxs6(Fragment5, { children: [
882
1029
  " ",
883
1030
  /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
884
1031
  "\u25CB ",
@@ -1557,6 +1704,9 @@ function getToolDetail(toolName, input) {
1557
1704
  if (input.query) {
1558
1705
  return input.query;
1559
1706
  }
1707
+ if (input.description) {
1708
+ return input.description;
1709
+ }
1560
1710
  return "";
1561
1711
  }
1562
1712
  function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES) {
@@ -1564,7 +1714,8 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1564
1714
  status: "none",
1565
1715
  activities: [],
1566
1716
  tokenCount: 0,
1567
- sessionStartTime: null
1717
+ sessionStartTime: null,
1718
+ todos: null
1568
1719
  };
1569
1720
  if (!fs.existsSync(sessionFile)) {
1570
1721
  return defaultState;
@@ -1594,6 +1745,7 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1594
1745
  let tokenCount = 0;
1595
1746
  let lastTimestamp = null;
1596
1747
  let lastType = null;
1748
+ let todos = null;
1597
1749
  const recentLines = lines.slice(-MAX_LINES_TO_SCAN);
1598
1750
  for (const line of recentLines) {
1599
1751
  try {
@@ -1624,6 +1776,13 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1624
1776
  detail: userText.replace(/\n/g, " ")
1625
1777
  });
1626
1778
  }
1779
+ if (userEntry.toolUseResult?.newTodos) {
1780
+ todos = userEntry.toolUseResult.newTodos.map((t) => ({
1781
+ content: t.content,
1782
+ status: t.status,
1783
+ activeForm: t.activeForm
1784
+ }));
1785
+ }
1627
1786
  lastType = "user";
1628
1787
  }
1629
1788
  if (entry.type === "assistant") {
@@ -1636,15 +1795,25 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1636
1795
  for (const block of messageContent) {
1637
1796
  if (block.type === "tool_use") {
1638
1797
  const toolName = block.name || "Tool";
1798
+ if (toolName === "TodoWrite") {
1799
+ lastType = "tool";
1800
+ continue;
1801
+ }
1639
1802
  const icon = ICONS[toolName] || ICONS.Default;
1640
1803
  const detail = getToolDetail(toolName, block.input);
1641
- activities.push({
1642
- timestamp: lastTimestamp || /* @__PURE__ */ new Date(),
1643
- type: "tool",
1644
- icon,
1645
- label: toolName,
1646
- detail
1647
- });
1804
+ const lastActivity = activities[activities.length - 1];
1805
+ if (lastActivity && lastActivity.type === "tool" && lastActivity.label === toolName && lastActivity.detail === detail) {
1806
+ lastActivity.count = (lastActivity.count || 1) + 1;
1807
+ lastActivity.timestamp = lastTimestamp || /* @__PURE__ */ new Date();
1808
+ } else {
1809
+ activities.push({
1810
+ timestamp: lastTimestamp || /* @__PURE__ */ new Date(),
1811
+ type: "tool",
1812
+ icon,
1813
+ label: toolName,
1814
+ detail
1815
+ });
1816
+ }
1648
1817
  lastType = "tool";
1649
1818
  } else if (block.type === "text" && block.text) {
1650
1819
  if (block.text.length > 10) {
@@ -1695,7 +1864,8 @@ function parseSessionState(sessionFile, maxActivities = DEFAULT_MAX_ACTIVITIES)
1695
1864
  status,
1696
1865
  activities: activities.slice(-maxActivities).reverse(),
1697
1866
  tokenCount,
1698
- sessionStartTime
1867
+ sessionStartTime,
1868
+ todos
1699
1869
  };
1700
1870
  }
1701
1871
  function getClaudeData(projectPath, maxActivities) {
@@ -1703,7 +1873,8 @@ function getClaudeData(projectPath, maxActivities) {
1703
1873
  status: "none",
1704
1874
  activities: [],
1705
1875
  tokenCount: 0,
1706
- sessionStartTime: null
1876
+ sessionStartTime: null,
1877
+ todos: null
1707
1878
  };
1708
1879
  try {
1709
1880
  const sessionDir = getClaudeSessionPath(projectPath);
@@ -2522,8 +2693,8 @@ function DashboardApp({ mode }) {
2522
2693
  }
2523
2694
  return getClampedWidth(terminalColumns);
2524
2695
  };
2525
- const [width, setWidth] = useState(() => getEffectiveWidth(stdout?.columns));
2526
- useEffect(() => {
2696
+ const [width, setWidth] = useState2(() => getEffectiveWidth(stdout?.columns));
2697
+ useEffect2(() => {
2527
2698
  if (!config.width) {
2528
2699
  const newWidth = getEffectiveWidth(stdout?.columns);
2529
2700
  if (newWidth !== width) {
@@ -2531,7 +2702,7 @@ function DashboardApp({ mode }) {
2531
2702
  }
2532
2703
  }
2533
2704
  }, [stdout?.columns, width, config.width]);
2534
- useEffect(() => {
2705
+ useEffect2(() => {
2535
2706
  if (config.width) return;
2536
2707
  const handleResize = () => {
2537
2708
  setWidth(getEffectiveWidth(stdout?.columns));
@@ -2546,11 +2717,11 @@ function DashboardApp({ mode }) {
2546
2717
  const claudeIntervalSeconds = config.panels.claude.interval ? config.panels.claude.interval / 1e3 : null;
2547
2718
  const otherSessionsIntervalSeconds = config.panels.other_sessions.interval ? config.panels.other_sessions.interval / 1e3 : null;
2548
2719
  const cwd = process.cwd();
2549
- const [projectData, setProjectData] = useState(() => getProjectData());
2720
+ const [projectData, setProjectData] = useState2(() => getProjectData());
2550
2721
  const refreshProject = useCallback(() => {
2551
2722
  setProjectData(getProjectData());
2552
2723
  }, []);
2553
- const [gitData, setGitData] = useState(() => getGitData(config.panels.git));
2724
+ const [gitData, setGitData] = useState2(() => getGitData(config.panels.git));
2554
2725
  const refreshGit = useCallback(() => {
2555
2726
  setGitData(getGitData(config.panels.git));
2556
2727
  }, [config.panels.git]);
@@ -2560,15 +2731,22 @@ function DashboardApp({ mode }) {
2560
2731
  }
2561
2732
  return getTestData();
2562
2733
  }, [config.panels.tests.command]);
2563
- const [testData, setTestData] = useState(() => getTestDataFromConfig());
2734
+ const initialTestData = useMemo(() => getTestDataFromConfig(), [getTestDataFromConfig]);
2735
+ const initialTestsDisabled = !!(initialTestData.error && config.panels.tests.command);
2736
+ const [testsDisabled, setTestsDisabled] = useState2(initialTestsDisabled);
2737
+ const [testData, setTestData] = useState2(initialTestData);
2564
2738
  const refreshTest = useCallback(() => {
2565
- setTestData(getTestDataFromConfig());
2566
- }, [getTestDataFromConfig]);
2567
- const [claudeData, setClaudeData] = useState(() => getClaudeData(cwd, config.panels.claude.maxActivities));
2739
+ const data = getTestDataFromConfig();
2740
+ if (config.panels.tests.command) {
2741
+ setTestsDisabled(!!data.error);
2742
+ }
2743
+ setTestData(data);
2744
+ }, [getTestDataFromConfig, config.panels.tests.command]);
2745
+ const [claudeData, setClaudeData] = useState2(() => getClaudeData(cwd, config.panels.claude.maxActivities));
2568
2746
  const refreshClaude = useCallback(() => {
2569
2747
  setClaudeData(getClaudeData(cwd, config.panels.claude.maxActivities));
2570
2748
  }, [cwd, config.panels.claude.maxActivities]);
2571
- const [otherSessionsData, setOtherSessionsData] = useState(
2749
+ const [otherSessionsData, setOtherSessionsData] = useState2(
2572
2750
  () => getOtherSessionsData(cwd, { activeThresholdMs: config.panels.other_sessions.activeThreshold })
2573
2751
  );
2574
2752
  const refreshOtherSessions = useCallback(() => {
@@ -2578,7 +2756,7 @@ function DashboardApp({ mode }) {
2578
2756
  () => Object.keys(config.customPanels || {}),
2579
2757
  [config.customPanels]
2580
2758
  );
2581
- const [customPanelData, setCustomPanelData] = useState(() => {
2759
+ const [customPanelData, setCustomPanelData] = useState2(() => {
2582
2760
  const data = {};
2583
2761
  if (config.customPanels) {
2584
2762
  for (const [name, panelConfig] of Object.entries(config.customPanels)) {
@@ -2614,7 +2792,7 @@ function DashboardApp({ mode }) {
2614
2792
  }
2615
2793
  return countdowns2;
2616
2794
  }, [projectIntervalSeconds, gitIntervalSeconds, claudeIntervalSeconds, otherSessionsIntervalSeconds, config.customPanels]);
2617
- const [countdowns, setCountdowns] = useState(initialCountdowns);
2795
+ const [countdowns, setCountdowns] = useState2(initialCountdowns);
2618
2796
  const initialVisualStates = useMemo(() => {
2619
2797
  const states = {
2620
2798
  project: { ...DEFAULT_VISUAL_STATE },
@@ -2628,7 +2806,7 @@ function DashboardApp({ mode }) {
2628
2806
  }
2629
2807
  return states;
2630
2808
  }, [customPanelNames]);
2631
- const [visualStates, setVisualStates] = useState(initialVisualStates);
2809
+ const [visualStates, setVisualStates] = useState2(initialVisualStates);
2632
2810
  const setVisualState = useCallback((panel, update) => {
2633
2811
  setVisualStates((prev) => ({
2634
2812
  ...prev,
@@ -2678,7 +2856,11 @@ function DashboardApp({ mode }) {
2678
2856
  try {
2679
2857
  await new Promise((resolve) => {
2680
2858
  setTimeout(() => {
2681
- setTestData(getTestDataFromConfig());
2859
+ const data = getTestDataFromConfig();
2860
+ if (config.panels.tests.command) {
2861
+ setTestsDisabled(!!data.error);
2862
+ }
2863
+ setTestData(data);
2682
2864
  resolve();
2683
2865
  }, 0);
2684
2866
  });
@@ -2686,7 +2868,7 @@ function DashboardApp({ mode }) {
2686
2868
  setVisualState("tests", { isRunning: false, justCompleted: true });
2687
2869
  clearFeedback("tests", "justCompleted");
2688
2870
  }
2689
- }, [getTestDataFromConfig, setVisualState, clearFeedback]);
2871
+ }, [getTestDataFromConfig, setVisualState, clearFeedback, config.panels.tests.command]);
2690
2872
  const refreshClaudeWithFeedback = useCallback(() => {
2691
2873
  setClaudeData(getClaudeData(cwd, config.panels.claude.maxActivities));
2692
2874
  setVisualState("claude", { justRefreshed: true });
@@ -2755,7 +2937,7 @@ function DashboardApp({ mode }) {
2755
2937
  }),
2756
2938
  [config, refreshTestAsync, customPanelActionsAsync]
2757
2939
  );
2758
- useEffect(() => {
2940
+ useEffect2(() => {
2759
2941
  if (mode !== "watch") return;
2760
2942
  const timers = [];
2761
2943
  if (config.panels.project.enabled && config.panels.project.interval !== null) {
@@ -2821,7 +3003,7 @@ function DashboardApp({ mode }) {
2821
3003
  claudeIntervalSeconds,
2822
3004
  otherSessionsIntervalSeconds
2823
3005
  ]);
2824
- useEffect(() => {
3006
+ useEffect2(() => {
2825
3007
  if (mode !== "watch") return;
2826
3008
  const tick = setInterval(() => {
2827
3009
  setCountdowns((prev) => {
@@ -2897,7 +3079,7 @@ function DashboardApp({ mode }) {
2897
3079
  }
2898
3080
  ) }, `panel-git-${index}`);
2899
3081
  }
2900
- if (panelName === "tests" && config.panels.tests.enabled) {
3082
+ if (panelName === "tests" && config.panels.tests.enabled && !testsDisabled) {
2901
3083
  const testsVisual = visualStates.tests || DEFAULT_VISUAL_STATE;
2902
3084
  return /* @__PURE__ */ jsx8(Box8, { marginTop: isFirst ? 0 : 1, children: /* @__PURE__ */ jsx8(
2903
3085
  TestPanel,
@@ -2961,7 +3143,7 @@ function DashboardApp({ mode }) {
2961
3143
  }
2962
3144
  return null;
2963
3145
  }),
2964
- mode === "watch" && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, width, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs8(React.Fragment, { children: [
3146
+ mode === "watch" && /* @__PURE__ */ jsx8(Box8, { marginTop: 1, width, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs8(React2.Fragment, { children: [
2965
3147
  index > 0 && " \xB7 ",
2966
3148
  /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
2967
3149
  item.split(":")[0],
@@ -3025,30 +3207,124 @@ function parseArgs(args) {
3025
3207
 
3026
3208
  // src/commands/init.ts
3027
3209
  import {
3028
- existsSync as nodeExistsSync5,
3210
+ existsSync as nodeExistsSync6,
3029
3211
  mkdirSync as nodeMkdirSync,
3030
3212
  writeFileSync as nodeWriteFileSync,
3031
- readFileSync as nodeReadFileSync7,
3213
+ readFileSync as nodeReadFileSync8,
3032
3214
  appendFileSync as nodeAppendFileSync
3033
3215
  } from "fs";
3034
3216
  import { fileURLToPath as fileURLToPath2 } from "url";
3035
3217
  import { dirname as dirname2, join as join5 } from "path";
3036
3218
  import { homedir as homedir3 } from "os";
3219
+
3220
+ // src/data/detectTestFramework.ts
3221
+ import {
3222
+ existsSync as nodeExistsSync5,
3223
+ readFileSync as nodeReadFileSync7
3224
+ } from "fs";
3037
3225
  var fs4 = {
3038
3226
  existsSync: nodeExistsSync5,
3227
+ readFileSync: (path) => nodeReadFileSync7(path, "utf-8")
3228
+ };
3229
+ var FRAMEWORK_COMMANDS = {
3230
+ vitest: "npx vitest run --reporter=json",
3231
+ jest: "npx jest --json",
3232
+ mocha: "npx mocha --reporter=json",
3233
+ pytest: "pytest --json-report --json-report-file=.agenthud/test-results.json"
3234
+ };
3235
+ var JS_FRAMEWORKS = ["vitest", "jest", "mocha"];
3236
+ function detectTestFramework() {
3237
+ const jsFramework = detectJsFramework();
3238
+ if (jsFramework) {
3239
+ return jsFramework;
3240
+ }
3241
+ const pythonFramework = detectPythonFramework();
3242
+ if (pythonFramework) {
3243
+ return pythonFramework;
3244
+ }
3245
+ return null;
3246
+ }
3247
+ function detectJsFramework() {
3248
+ if (!fs4.existsSync("package.json")) {
3249
+ return null;
3250
+ }
3251
+ let packageJson;
3252
+ try {
3253
+ const content = fs4.readFileSync("package.json");
3254
+ packageJson = JSON.parse(content);
3255
+ } catch {
3256
+ return null;
3257
+ }
3258
+ const allDeps = {
3259
+ ...packageJson.dependencies,
3260
+ ...packageJson.devDependencies
3261
+ };
3262
+ for (const framework of JS_FRAMEWORKS) {
3263
+ if (allDeps[framework]) {
3264
+ return {
3265
+ framework,
3266
+ command: FRAMEWORK_COMMANDS[framework]
3267
+ };
3268
+ }
3269
+ }
3270
+ return null;
3271
+ }
3272
+ function detectPythonFramework() {
3273
+ const pytestIndicators = ["pytest.ini", "conftest.py"];
3274
+ for (const file of pytestIndicators) {
3275
+ if (fs4.existsSync(file)) {
3276
+ return {
3277
+ framework: "pytest",
3278
+ command: FRAMEWORK_COMMANDS.pytest
3279
+ };
3280
+ }
3281
+ }
3282
+ if (fs4.existsSync("pyproject.toml")) {
3283
+ try {
3284
+ const content = fs4.readFileSync("pyproject.toml");
3285
+ if (content.includes("[tool.pytest") || content.includes("[tool.pytest.ini_options]")) {
3286
+ return {
3287
+ framework: "pytest",
3288
+ command: FRAMEWORK_COMMANDS.pytest
3289
+ };
3290
+ }
3291
+ } catch {
3292
+ }
3293
+ }
3294
+ const requirementsFiles = ["requirements.txt", "requirements-dev.txt"];
3295
+ for (const file of requirementsFiles) {
3296
+ if (fs4.existsSync(file)) {
3297
+ try {
3298
+ const content = fs4.readFileSync(file);
3299
+ if (content.includes("pytest")) {
3300
+ return {
3301
+ framework: "pytest",
3302
+ command: FRAMEWORK_COMMANDS.pytest
3303
+ };
3304
+ }
3305
+ } catch {
3306
+ }
3307
+ }
3308
+ }
3309
+ return null;
3310
+ }
3311
+
3312
+ // src/commands/init.ts
3313
+ var fs5 = {
3314
+ existsSync: nodeExistsSync6,
3039
3315
  mkdirSync: nodeMkdirSync,
3040
3316
  writeFileSync: nodeWriteFileSync,
3041
- readFileSync: (path) => nodeReadFileSync7(path, "utf-8"),
3317
+ readFileSync: (path) => nodeReadFileSync8(path, "utf-8"),
3042
3318
  appendFileSync: nodeAppendFileSync
3043
3319
  };
3044
3320
  var __filename2 = fileURLToPath2(import.meta.url);
3045
3321
  var __dirname2 = dirname2(__filename2);
3046
3322
  function getDefaultConfig2() {
3047
3323
  let templatePath = join5(__dirname2, "templates", "config.yaml");
3048
- if (!nodeExistsSync5(templatePath)) {
3324
+ if (!nodeExistsSync6(templatePath)) {
3049
3325
  templatePath = join5(__dirname2, "..", "templates", "config.yaml");
3050
3326
  }
3051
- return nodeReadFileSync7(templatePath, "utf-8");
3327
+ return nodeReadFileSync8(templatePath, "utf-8");
3052
3328
  }
3053
3329
  function getClaudeSessionPath2(projectPath) {
3054
3330
  const encoded = projectPath.replace(/[/\\]/g, "-");
@@ -3060,41 +3336,57 @@ function runInit(cwd = process.cwd()) {
3060
3336
  skipped: [],
3061
3337
  warnings: []
3062
3338
  };
3063
- if (!fs4.existsSync(".agenthud")) {
3064
- fs4.mkdirSync(".agenthud", { recursive: true });
3339
+ if (!fs5.existsSync(".agenthud")) {
3340
+ fs5.mkdirSync(".agenthud", { recursive: true });
3065
3341
  result.created.push(".agenthud/");
3066
3342
  } else {
3067
3343
  result.skipped.push(".agenthud/");
3068
3344
  }
3069
- if (!fs4.existsSync(".agenthud/tests")) {
3070
- fs4.mkdirSync(".agenthud/tests", { recursive: true });
3345
+ if (!fs5.existsSync(".agenthud/tests")) {
3346
+ fs5.mkdirSync(".agenthud/tests", { recursive: true });
3071
3347
  result.created.push(".agenthud/tests/");
3072
3348
  } else {
3073
3349
  result.skipped.push(".agenthud/tests/");
3074
3350
  }
3075
- if (!fs4.existsSync(".agenthud/config.yaml")) {
3076
- fs4.writeFileSync(".agenthud/config.yaml", getDefaultConfig2());
3351
+ const testFramework = detectTestFramework();
3352
+ if (testFramework) {
3353
+ result.detectedTestFramework = testFramework.framework;
3354
+ }
3355
+ if (!fs5.existsSync(".agenthud/config.yaml")) {
3356
+ let configContent = getDefaultConfig2();
3357
+ if (testFramework) {
3358
+ configContent = configContent.replace(
3359
+ /command: npx vitest run --reporter=json/,
3360
+ `command: ${testFramework.command}`
3361
+ );
3362
+ } else {
3363
+ configContent = configContent.replace(
3364
+ /command: npx vitest run --reporter=json/,
3365
+ "# command: (auto-detect failed - configure manually)"
3366
+ );
3367
+ }
3368
+ fs5.writeFileSync(".agenthud/config.yaml", configContent);
3077
3369
  result.created.push(".agenthud/config.yaml");
3078
3370
  } else {
3079
3371
  result.skipped.push(".agenthud/config.yaml");
3080
3372
  }
3081
- if (!fs4.existsSync(".gitignore")) {
3082
- fs4.writeFileSync(".gitignore", ".agenthud/\n");
3373
+ if (!fs5.existsSync(".gitignore")) {
3374
+ fs5.writeFileSync(".gitignore", ".agenthud/\n");
3083
3375
  result.created.push(".gitignore");
3084
3376
  } else {
3085
- const content = fs4.readFileSync(".gitignore");
3377
+ const content = fs5.readFileSync(".gitignore");
3086
3378
  if (!content.includes(".agenthud/")) {
3087
- fs4.appendFileSync(".gitignore", "\n.agenthud/\n");
3379
+ fs5.appendFileSync(".gitignore", "\n.agenthud/\n");
3088
3380
  result.created.push(".gitignore");
3089
3381
  } else {
3090
3382
  result.skipped.push(".gitignore");
3091
3383
  }
3092
3384
  }
3093
- if (!fs4.existsSync(".git")) {
3385
+ if (!fs5.existsSync(".git")) {
3094
3386
  result.warnings.push("Not a git repository - Git panel will show limited info");
3095
3387
  }
3096
3388
  const claudeSessionPath = getClaudeSessionPath2(cwd);
3097
- if (!fs4.existsSync(claudeSessionPath)) {
3389
+ if (!fs5.existsSync(claudeSessionPath)) {
3098
3390
  result.warnings.push("No Claude session found - start Claude to see activity");
3099
3391
  }
3100
3392
  return result;
@@ -3161,7 +3453,7 @@ if (options.mode === "watch") {
3161
3453
  clearScreen();
3162
3454
  }
3163
3455
  var { waitUntilExit } = render(
3164
- React2.createElement(App, { mode: options.mode, agentDirExists })
3456
+ React3.createElement(App, { mode: options.mode, agentDirExists })
3165
3457
  );
3166
3458
  if (options.mode === "once") {
3167
3459
  setTimeout(() => process.exit(0), 100);
@@ -1,6 +1,6 @@
1
1
  # agenthud configuration
2
2
 
3
- width: 80 # 50~120 사이
3
+ width: 100 # between 50~120
4
4
 
5
5
  panels:
6
6
  claude:
@@ -19,6 +19,7 @@ panels:
19
19
  tests:
20
20
  enabled: true
21
21
  interval: manual
22
+ # command is auto-detected from: vitest, jest, mocha, pytest
22
23
  command: npx vitest run --reporter=json
23
24
 
24
25
  project:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.5.16",
3
+ "version": "0.6.0",
4
4
  "description": "CLI tool to monitor agent status in real-time. Works with Claude Code, multi-agent workflows, and any AI agent system.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",