agenthud 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +588 -184
- package/package.json +3 -2
- package/scripts/save-test-results.ts +3 -3
package/README.md
CHANGED
|
@@ -44,14 +44,14 @@ npx agenthud
|
|
|
44
44
|
## Features
|
|
45
45
|
|
|
46
46
|
- **Git**: Branch, commits, line changes
|
|
47
|
-
- **Plan**: Progress, decisions from `.
|
|
47
|
+
- **Plan**: Progress, decisions from `.agenthud/plan.json`
|
|
48
48
|
- **Tests**: Results with outdated detection
|
|
49
49
|
|
|
50
50
|
## Setup
|
|
51
51
|
|
|
52
52
|
Add to your `CLAUDE.md`:
|
|
53
53
|
```markdown
|
|
54
|
-
Maintain `.
|
|
54
|
+
Maintain `.agenthud/` directory:
|
|
55
55
|
- Update `plan.json` when plan changes
|
|
56
56
|
- Update `decisions.json` for key decisions
|
|
57
57
|
```
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { render } from "ink";
|
|
|
6
6
|
import { existsSync } from "fs";
|
|
7
7
|
|
|
8
8
|
// src/ui/App.tsx
|
|
9
|
-
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
10
10
|
import { Box as Box5, Text as Text5, useApp, useInput } from "ink";
|
|
11
11
|
|
|
12
12
|
// src/ui/GitPanel.tsx
|
|
@@ -15,6 +15,31 @@ import { Box, Text } from "ink";
|
|
|
15
15
|
// src/ui/constants.ts
|
|
16
16
|
var PANEL_WIDTH = 60;
|
|
17
17
|
var CONTENT_WIDTH = PANEL_WIDTH - 4;
|
|
18
|
+
var INNER_WIDTH = PANEL_WIDTH - 2;
|
|
19
|
+
var BOX = {
|
|
20
|
+
tl: "\u250C",
|
|
21
|
+
tr: "\u2510",
|
|
22
|
+
bl: "\u2514",
|
|
23
|
+
br: "\u2518",
|
|
24
|
+
h: "\u2500",
|
|
25
|
+
v: "\u2502",
|
|
26
|
+
ml: "\u251C",
|
|
27
|
+
mr: "\u2524"
|
|
28
|
+
};
|
|
29
|
+
function createTitleLine(label, suffix = "") {
|
|
30
|
+
const leftPart = BOX.h + " " + label + " ";
|
|
31
|
+
const rightPart = suffix ? " " + suffix + " " + BOX.h : "";
|
|
32
|
+
const dashCount = PANEL_WIDTH - 1 - leftPart.length - rightPart.length - 1;
|
|
33
|
+
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
34
|
+
return BOX.tl + leftPart + dashes + rightPart + BOX.tr;
|
|
35
|
+
}
|
|
36
|
+
function createBottomLine() {
|
|
37
|
+
return BOX.bl + BOX.h.repeat(INNER_WIDTH) + BOX.br;
|
|
38
|
+
}
|
|
39
|
+
function padLine(content) {
|
|
40
|
+
const padding = INNER_WIDTH - content.length;
|
|
41
|
+
return content + " ".repeat(Math.max(0, padding));
|
|
42
|
+
}
|
|
18
43
|
var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
|
|
19
44
|
function truncate(text, maxLength) {
|
|
20
45
|
if (text.length <= maxLength) return text;
|
|
@@ -25,11 +50,21 @@ function truncate(text, maxLength) {
|
|
|
25
50
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
26
51
|
var MAX_COMMITS = 5;
|
|
27
52
|
var MAX_MESSAGE_LENGTH = CONTENT_WIDTH - 10;
|
|
28
|
-
function
|
|
53
|
+
function formatCountdown(seconds) {
|
|
54
|
+
if (seconds == null) return "";
|
|
55
|
+
return `\u21BB ${seconds}s`;
|
|
56
|
+
}
|
|
57
|
+
function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
58
|
+
const countdownSuffix = formatCountdown(countdown);
|
|
29
59
|
if (branch === null) {
|
|
30
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column",
|
|
31
|
-
/* @__PURE__ */ jsx(
|
|
32
|
-
/* @__PURE__ */
|
|
60
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
61
|
+
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix) }),
|
|
62
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
63
|
+
BOX.v,
|
|
64
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" Not a git repository") }),
|
|
65
|
+
BOX.v
|
|
66
|
+
] }),
|
|
67
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine() })
|
|
33
68
|
] });
|
|
34
69
|
}
|
|
35
70
|
const displayCommits = commits.slice(0, MAX_COMMITS);
|
|
@@ -37,9 +72,19 @@ function GitPanel({ branch, commits, stats, uncommitted = 0 }) {
|
|
|
37
72
|
const commitWord = commits.length === 1 ? "commit" : "commits";
|
|
38
73
|
const fileWord = stats.files === 1 ? "file" : "files";
|
|
39
74
|
const hasUncommitted = uncommitted > 0;
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
let branchLineLength = 1 + branch.length;
|
|
76
|
+
if (hasCommits) {
|
|
77
|
+
branchLineLength += ` \xB7 +${stats.added} -${stats.deleted} \xB7 ${commits.length} ${commitWord} \xB7 ${stats.files} ${fileWord}`.length;
|
|
78
|
+
}
|
|
79
|
+
if (hasUncommitted) {
|
|
80
|
+
branchLineLength += ` \xB7 ${uncommitted} dirty`.length;
|
|
81
|
+
}
|
|
82
|
+
const branchPadding = Math.max(0, INNER_WIDTH - branchLineLength);
|
|
83
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
84
|
+
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix) }),
|
|
42
85
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
86
|
+
BOX.v,
|
|
87
|
+
" ",
|
|
43
88
|
/* @__PURE__ */ jsx(Text, { color: "green", children: branch }),
|
|
44
89
|
hasCommits && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
90
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
@@ -69,21 +114,35 @@ function GitPanel({ branch, commits, stats, uncommitted = 0 }) {
|
|
|
69
114
|
uncommitted,
|
|
70
115
|
" dirty"
|
|
71
116
|
] })
|
|
72
|
-
] })
|
|
117
|
+
] }),
|
|
118
|
+
" ".repeat(branchPadding),
|
|
119
|
+
BOX.v
|
|
73
120
|
] }),
|
|
74
|
-
hasCommits ? /* @__PURE__ */ jsx(Fragment, { children: displayCommits.map((commit) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
121
|
+
hasCommits ? /* @__PURE__ */ jsx(Fragment, { children: displayCommits.map((commit) => {
|
|
122
|
+
const msg = truncate(commit.message, MAX_MESSAGE_LENGTH);
|
|
123
|
+
const lineLength = 3 + 7 + 1 + msg.length;
|
|
124
|
+
const commitPadding = Math.max(0, INNER_WIDTH - lineLength);
|
|
125
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
126
|
+
BOX.v,
|
|
127
|
+
" \u2022 ",
|
|
128
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: commit.hash.slice(0, 7) }),
|
|
129
|
+
" ",
|
|
130
|
+
msg,
|
|
131
|
+
" ".repeat(commitPadding),
|
|
132
|
+
BOX.v
|
|
133
|
+
] }, commit.hash);
|
|
134
|
+
}) }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
135
|
+
BOX.v,
|
|
136
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" No commits today") }),
|
|
137
|
+
BOX.v
|
|
138
|
+
] }),
|
|
139
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine() })
|
|
80
140
|
] });
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
// src/ui/PlanPanel.tsx
|
|
84
144
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
85
145
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
86
|
-
var BOX = { tl: "\u250C", tr: "\u2510", bl: "\u2514", br: "\u2518", h: "\u2500", v: "\u2502" };
|
|
87
146
|
var PROGRESS_BAR_WIDTH = 10;
|
|
88
147
|
var MAX_STEP_LENGTH = CONTENT_WIDTH - 2;
|
|
89
148
|
var MAX_DECISION_LENGTH = CONTENT_WIDTH - 2;
|
|
@@ -93,37 +152,37 @@ function createProgressBar(done, total) {
|
|
|
93
152
|
const empty = PROGRESS_BAR_WIDTH - filled;
|
|
94
153
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
95
154
|
}
|
|
96
|
-
|
|
97
|
-
|
|
155
|
+
function formatCountdown2(seconds) {
|
|
156
|
+
if (seconds == null) return "";
|
|
157
|
+
return `\u21BB ${seconds}s`;
|
|
158
|
+
}
|
|
159
|
+
function createPlanTitleLine(done, total, countdown) {
|
|
98
160
|
const label = " Plan ";
|
|
99
161
|
const count = ` ${done}/${total} `;
|
|
100
162
|
const bar = createProgressBar(done, total);
|
|
101
|
-
const
|
|
163
|
+
const countdownStr = formatCountdown2(countdown);
|
|
164
|
+
const suffix = countdownStr ? ` \xB7 ${countdownStr} ` + BOX.h : "";
|
|
165
|
+
const dashCount = PANEL_WIDTH - 3 - label.length - count.length - bar.length - suffix.length;
|
|
102
166
|
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
103
|
-
return BOX.tl + BOX.h + label + dashes + count + bar + BOX.tr;
|
|
104
|
-
}
|
|
105
|
-
function createBottomLine() {
|
|
106
|
-
return BOX.bl + BOX.h.repeat(INNER_WIDTH) + BOX.br;
|
|
167
|
+
return BOX.tl + BOX.h + label + dashes + count + bar + suffix + BOX.tr;
|
|
107
168
|
}
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
|
|
169
|
+
function createSimpleTitleLine(countdown) {
|
|
170
|
+
const label = " Plan ";
|
|
171
|
+
const countdownStr = formatCountdown2(countdown);
|
|
172
|
+
const suffix = countdownStr ? ` ${countdownStr} ` + BOX.h : "";
|
|
173
|
+
const dashCount = PANEL_WIDTH - 3 - label.length - suffix.length;
|
|
174
|
+
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
175
|
+
return BOX.tl + BOX.h + label + dashes + suffix + BOX.tr;
|
|
111
176
|
}
|
|
112
177
|
function createDecisionsHeader() {
|
|
113
178
|
const label = "\u2500 Decisions ";
|
|
114
179
|
const dashCount = PANEL_WIDTH - 1 - label.length - 1;
|
|
115
180
|
return label + "\u2500".repeat(dashCount) + "\u2524";
|
|
116
181
|
}
|
|
117
|
-
function PlanPanel({ plan, decisions, error }) {
|
|
182
|
+
function PlanPanel({ plan, decisions, error, countdown }) {
|
|
118
183
|
if (error || !plan || !plan.goal || !plan.steps) {
|
|
119
184
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
120
|
-
/* @__PURE__ */
|
|
121
|
-
BOX.tl,
|
|
122
|
-
BOX.h,
|
|
123
|
-
" Plan ",
|
|
124
|
-
BOX.h.repeat(INNER_WIDTH - 7),
|
|
125
|
-
BOX.tr
|
|
126
|
-
] }),
|
|
185
|
+
/* @__PURE__ */ jsx2(Text2, { children: createSimpleTitleLine(countdown) }),
|
|
127
186
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
128
187
|
BOX.v,
|
|
129
188
|
padLine(" " + (error || "No plan found")),
|
|
@@ -135,7 +194,7 @@ function PlanPanel({ plan, decisions, error }) {
|
|
|
135
194
|
const doneCount = plan.steps.filter((s) => s.status === "done").length;
|
|
136
195
|
const totalCount = plan.steps.length;
|
|
137
196
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
138
|
-
/* @__PURE__ */ jsx2(Text2, { children:
|
|
197
|
+
/* @__PURE__ */ jsx2(Text2, { children: createPlanTitleLine(doneCount, totalCount, countdown) }),
|
|
139
198
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
140
199
|
BOX.v,
|
|
141
200
|
padLine(" " + truncate(plan.goal, CONTENT_WIDTH)),
|
|
@@ -182,6 +241,9 @@ function formatRelativeTime(timestamp) {
|
|
|
182
241
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
183
242
|
return `${diffDays}d ago`;
|
|
184
243
|
}
|
|
244
|
+
function createSeparator() {
|
|
245
|
+
return BOX.ml + BOX.h.repeat(INNER_WIDTH) + BOX.mr;
|
|
246
|
+
}
|
|
185
247
|
function TestPanel({
|
|
186
248
|
results,
|
|
187
249
|
isOutdated,
|
|
@@ -189,23 +251,37 @@ function TestPanel({
|
|
|
189
251
|
error
|
|
190
252
|
}) {
|
|
191
253
|
if (error || !results) {
|
|
192
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column",
|
|
193
|
-
/* @__PURE__ */ jsx3(
|
|
194
|
-
/* @__PURE__ */
|
|
254
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
255
|
+
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", "") }),
|
|
256
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
257
|
+
BOX.v,
|
|
258
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" " + (error || "No test results")) }),
|
|
259
|
+
BOX.v
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine() })
|
|
195
262
|
] });
|
|
196
263
|
}
|
|
197
264
|
const hasFailures = results.failures.length > 0;
|
|
198
265
|
const relativeTime = formatRelativeTime(results.timestamp);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
266
|
+
let summaryLength = 1 + 2 + String(results.passed).length + " passed".length;
|
|
267
|
+
if (results.failed > 0) {
|
|
268
|
+
summaryLength += 2 + 2 + String(results.failed).length + " failed".length;
|
|
269
|
+
}
|
|
270
|
+
if (results.skipped > 0) {
|
|
271
|
+
summaryLength += 2 + 2 + String(results.skipped).length + " skipped".length;
|
|
272
|
+
}
|
|
273
|
+
summaryLength += " \xB7 ".length + results.hash.length;
|
|
274
|
+
const summaryPadding = Math.max(0, INNER_WIDTH - summaryLength);
|
|
275
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: PANEL_WIDTH, children: [
|
|
276
|
+
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", relativeTime) }),
|
|
277
|
+
isOutdated && /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
278
|
+
BOX.v,
|
|
279
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: padLine(` \u26A0 Outdated (${commitsBehind} ${commitsBehind === 1 ? "commit" : "commits"} behind)`) }),
|
|
280
|
+
BOX.v
|
|
207
281
|
] }),
|
|
208
282
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
283
|
+
BOX.v,
|
|
284
|
+
" ",
|
|
209
285
|
/* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
210
286
|
"\u2713 ",
|
|
211
287
|
results.passed,
|
|
@@ -228,27 +304,42 @@ function TestPanel({
|
|
|
228
304
|
] })
|
|
229
305
|
] }),
|
|
230
306
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
231
|
-
" ",
|
|
232
|
-
"\xB7 ",
|
|
233
|
-
results.hash,
|
|
234
307
|
" \xB7 ",
|
|
235
|
-
|
|
236
|
-
] })
|
|
308
|
+
results.hash
|
|
309
|
+
] }),
|
|
310
|
+
" ".repeat(summaryPadding),
|
|
311
|
+
BOX.v
|
|
237
312
|
] }),
|
|
238
313
|
hasFailures && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
239
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
240
|
-
results.failures.map((failure, index) =>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
/* @__PURE__ */ jsxs3(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
314
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: createSeparator() }),
|
|
315
|
+
results.failures.map((failure, index) => {
|
|
316
|
+
const fileName = truncate(failure.file, CONTENT_WIDTH - 3);
|
|
317
|
+
const filePadding = Math.max(0, INNER_WIDTH - 3 - fileName.length);
|
|
318
|
+
const testName = truncate(failure.name, CONTENT_WIDTH - 5);
|
|
319
|
+
const testPadding = Math.max(0, INNER_WIDTH - 5 - testName.length);
|
|
320
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
321
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
322
|
+
BOX.v,
|
|
323
|
+
" ",
|
|
324
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
325
|
+
"\u2717 ",
|
|
326
|
+
fileName
|
|
327
|
+
] }),
|
|
328
|
+
" ".repeat(filePadding),
|
|
329
|
+
BOX.v
|
|
330
|
+
] }),
|
|
331
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
332
|
+
BOX.v,
|
|
333
|
+
" ",
|
|
334
|
+
"\u2022 ",
|
|
335
|
+
testName,
|
|
336
|
+
" ".repeat(testPadding),
|
|
337
|
+
BOX.v
|
|
338
|
+
] })
|
|
339
|
+
] }, index);
|
|
340
|
+
})
|
|
341
|
+
] }),
|
|
342
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine() })
|
|
252
343
|
] });
|
|
253
344
|
}
|
|
254
345
|
|
|
@@ -259,7 +350,7 @@ function WelcomePanel() {
|
|
|
259
350
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "single", paddingX: 1, width: PANEL_WIDTH, children: [
|
|
260
351
|
/* @__PURE__ */ jsx4(Box4, { marginTop: -1, children: /* @__PURE__ */ jsx4(Text4, { children: " Welcome to agenthud " }) }),
|
|
261
352
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
262
|
-
/* @__PURE__ */ jsx4(Text4, { children: " No .
|
|
353
|
+
/* @__PURE__ */ jsx4(Text4, { children: " No .agenthud/ directory found." }),
|
|
263
354
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
264
355
|
/* @__PURE__ */ jsx4(Text4, { children: " Quick setup:" }),
|
|
265
356
|
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: " npx agenthud init" }),
|
|
@@ -273,23 +364,40 @@ function WelcomePanel() {
|
|
|
273
364
|
// src/data/git.ts
|
|
274
365
|
import { execSync as nodeExecSync } from "child_process";
|
|
275
366
|
var execFn = (command, options2) => nodeExecSync(command, options2);
|
|
276
|
-
function
|
|
367
|
+
function getUncommittedCount() {
|
|
277
368
|
try {
|
|
278
|
-
const result = execFn("git
|
|
369
|
+
const result = execFn("git status --porcelain", {
|
|
279
370
|
encoding: "utf-8"
|
|
280
371
|
});
|
|
281
|
-
|
|
372
|
+
const lines = result.trim().split("\n").filter(Boolean);
|
|
373
|
+
return lines.length;
|
|
282
374
|
} catch {
|
|
283
|
-
return
|
|
375
|
+
return 0;
|
|
284
376
|
}
|
|
285
377
|
}
|
|
286
|
-
|
|
378
|
+
var DEFAULT_COMMANDS = {
|
|
379
|
+
branch: "git branch --show-current",
|
|
380
|
+
commits: 'git log --since=midnight --format="%h|%aI|%s"',
|
|
381
|
+
stats: 'git log --since=midnight --numstat --format=""'
|
|
382
|
+
};
|
|
383
|
+
function getGitData(config) {
|
|
384
|
+
const commands = {
|
|
385
|
+
branch: config.command?.branch || DEFAULT_COMMANDS.branch,
|
|
386
|
+
commits: config.command?.commits || DEFAULT_COMMANDS.commits,
|
|
387
|
+
stats: config.command?.stats || DEFAULT_COMMANDS.stats
|
|
388
|
+
};
|
|
389
|
+
let branch = null;
|
|
287
390
|
try {
|
|
288
|
-
const result = execFn(
|
|
289
|
-
|
|
290
|
-
|
|
391
|
+
const result = execFn(commands.branch, { encoding: "utf-8" });
|
|
392
|
+
branch = result.trim();
|
|
393
|
+
} catch {
|
|
394
|
+
branch = null;
|
|
395
|
+
}
|
|
396
|
+
let commits = [];
|
|
397
|
+
try {
|
|
398
|
+
const result = execFn(commands.commits, { encoding: "utf-8" });
|
|
291
399
|
const lines = result.trim().split("\n").filter(Boolean);
|
|
292
|
-
|
|
400
|
+
commits = lines.map((line) => {
|
|
293
401
|
const [hash, timestamp, ...messageParts] = line.split("|");
|
|
294
402
|
return {
|
|
295
403
|
hash,
|
|
@@ -298,14 +406,11 @@ function getTodayCommits() {
|
|
|
298
406
|
};
|
|
299
407
|
});
|
|
300
408
|
} catch {
|
|
301
|
-
|
|
409
|
+
commits = [];
|
|
302
410
|
}
|
|
303
|
-
}
|
|
304
|
-
function getTodayStats() {
|
|
411
|
+
let stats = { added: 0, deleted: 0, files: 0 };
|
|
305
412
|
try {
|
|
306
|
-
const result = execFn(
|
|
307
|
-
encoding: "utf-8"
|
|
308
|
-
});
|
|
413
|
+
const result = execFn(commands.stats, { encoding: "utf-8" });
|
|
309
414
|
const lines = result.trim().split("\n").filter(Boolean);
|
|
310
415
|
let added = 0;
|
|
311
416
|
let deleted = 0;
|
|
@@ -320,34 +425,23 @@ function getTodayStats() {
|
|
|
320
425
|
deleted += parseInt(deletedStr, 10) || 0;
|
|
321
426
|
if (filename) filesSet.add(filename);
|
|
322
427
|
}
|
|
323
|
-
|
|
324
|
-
} catch {
|
|
325
|
-
return { added: 0, deleted: 0, files: 0 };
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
function getUncommittedCount() {
|
|
329
|
-
try {
|
|
330
|
-
const result = execFn("git status --porcelain", {
|
|
331
|
-
encoding: "utf-8"
|
|
332
|
-
});
|
|
333
|
-
const lines = result.trim().split("\n").filter(Boolean);
|
|
334
|
-
return lines.length;
|
|
428
|
+
stats = { added, deleted, files: filesSet.size };
|
|
335
429
|
} catch {
|
|
336
|
-
|
|
430
|
+
stats = { added: 0, deleted: 0, files: 0 };
|
|
337
431
|
}
|
|
432
|
+
const uncommitted = getUncommittedCount();
|
|
433
|
+
return { branch, commits, stats, uncommitted };
|
|
338
434
|
}
|
|
339
435
|
|
|
340
436
|
// src/data/plan.ts
|
|
341
437
|
import { readFileSync as nodeReadFileSync } from "fs";
|
|
342
|
-
import { join } from "path";
|
|
343
|
-
var AGENT_DIR = ".agent";
|
|
344
|
-
var PLAN_FILE = "plan.json";
|
|
345
|
-
var DECISIONS_FILE = "decisions.json";
|
|
438
|
+
import { join, dirname } from "path";
|
|
346
439
|
var MAX_DECISIONS = 3;
|
|
347
440
|
var readFileFn = (path) => nodeReadFileSync(path, "utf-8");
|
|
348
|
-
function
|
|
349
|
-
const planPath =
|
|
350
|
-
const
|
|
441
|
+
function getPlanDataWithConfig(config) {
|
|
442
|
+
const planPath = config.source;
|
|
443
|
+
const planDir = dirname(planPath);
|
|
444
|
+
const decisionsPath = join(planDir, "decisions.json");
|
|
351
445
|
let plan = null;
|
|
352
446
|
let decisions = [];
|
|
353
447
|
let error;
|
|
@@ -376,7 +470,7 @@ function getPlanData(dir = process.cwd()) {
|
|
|
376
470
|
import { readFileSync as nodeReadFileSync2 } from "fs";
|
|
377
471
|
import { execSync as nodeExecSync2 } from "child_process";
|
|
378
472
|
import { join as join2 } from "path";
|
|
379
|
-
var
|
|
473
|
+
var AGENT_DIR = ".agenthud";
|
|
380
474
|
var TEST_RESULTS_FILE = "test-results.json";
|
|
381
475
|
var readFileFn2 = (path) => nodeReadFileSync2(path, "utf-8");
|
|
382
476
|
var getHeadHashFn = () => {
|
|
@@ -389,7 +483,7 @@ var getCommitCountFn = (fromHash) => {
|
|
|
389
483
|
return parseInt(result, 10) || 0;
|
|
390
484
|
};
|
|
391
485
|
function getTestData(dir = process.cwd()) {
|
|
392
|
-
const testResultsPath = join2(dir,
|
|
486
|
+
const testResultsPath = join2(dir, AGENT_DIR, TEST_RESULTS_FILE);
|
|
393
487
|
let results = null;
|
|
394
488
|
let isOutdated = false;
|
|
395
489
|
let commitsBehind = 0;
|
|
@@ -418,65 +512,320 @@ function getTestData(dir = process.cwd()) {
|
|
|
418
512
|
return { results, isOutdated, commitsBehind, error };
|
|
419
513
|
}
|
|
420
514
|
|
|
515
|
+
// src/runner/command.ts
|
|
516
|
+
import { execSync as nodeExecSync3 } from "child_process";
|
|
517
|
+
var execFn2 = (command, options2) => nodeExecSync3(command, options2);
|
|
518
|
+
function parseVitestOutput(output) {
|
|
519
|
+
try {
|
|
520
|
+
const data = JSON.parse(output);
|
|
521
|
+
if (typeof data.numPassedTests !== "number" || typeof data.numFailedTests !== "number") {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
const failures = [];
|
|
525
|
+
for (const testResult of data.testResults || []) {
|
|
526
|
+
for (const assertion of testResult.assertionResults || []) {
|
|
527
|
+
if (assertion.status === "failed") {
|
|
528
|
+
failures.push({
|
|
529
|
+
file: testResult.name,
|
|
530
|
+
name: assertion.title
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
passed: data.numPassedTests,
|
|
537
|
+
failed: data.numFailedTests,
|
|
538
|
+
skipped: data.numPendingTests || 0,
|
|
539
|
+
failures
|
|
540
|
+
};
|
|
541
|
+
} catch {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function getHeadHash() {
|
|
546
|
+
try {
|
|
547
|
+
return execFn2("git rev-parse --short HEAD", {
|
|
548
|
+
encoding: "utf-8",
|
|
549
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
550
|
+
}).trim();
|
|
551
|
+
} catch {
|
|
552
|
+
return "unknown";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function runTestCommand(command) {
|
|
556
|
+
let output;
|
|
557
|
+
try {
|
|
558
|
+
output = execFn2(command, {
|
|
559
|
+
encoding: "utf-8",
|
|
560
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
561
|
+
});
|
|
562
|
+
} catch (error) {
|
|
563
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
564
|
+
return {
|
|
565
|
+
results: null,
|
|
566
|
+
isOutdated: false,
|
|
567
|
+
commitsBehind: 0,
|
|
568
|
+
error: message
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const parsed = parseVitestOutput(output);
|
|
572
|
+
if (!parsed) {
|
|
573
|
+
return {
|
|
574
|
+
results: null,
|
|
575
|
+
isOutdated: false,
|
|
576
|
+
commitsBehind: 0,
|
|
577
|
+
error: "Failed to parse test output"
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
const hash = getHeadHash();
|
|
581
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
582
|
+
const results = {
|
|
583
|
+
hash,
|
|
584
|
+
timestamp,
|
|
585
|
+
passed: parsed.passed,
|
|
586
|
+
failed: parsed.failed,
|
|
587
|
+
skipped: parsed.skipped,
|
|
588
|
+
failures: parsed.failures
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
results,
|
|
592
|
+
isOutdated: false,
|
|
593
|
+
commitsBehind: 0
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/config/parser.ts
|
|
598
|
+
import {
|
|
599
|
+
existsSync as nodeExistsSync,
|
|
600
|
+
readFileSync as nodeReadFileSync3
|
|
601
|
+
} from "fs";
|
|
602
|
+
import { parse as parseYaml } from "yaml";
|
|
603
|
+
var fs = {
|
|
604
|
+
existsSync: nodeExistsSync,
|
|
605
|
+
readFileSync: (path) => nodeReadFileSync3(path, "utf-8")
|
|
606
|
+
};
|
|
607
|
+
var CONFIG_PATH = ".agenthud/config.yaml";
|
|
608
|
+
function parseInterval(interval) {
|
|
609
|
+
if (!interval || interval === "manual") {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
const match = interval.match(/^(\d+)(s|m)$/);
|
|
613
|
+
if (!match) {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
const value = parseInt(match[1], 10);
|
|
617
|
+
const unit = match[2];
|
|
618
|
+
if (unit === "s") {
|
|
619
|
+
return value * 1e3;
|
|
620
|
+
} else if (unit === "m") {
|
|
621
|
+
return value * 60 * 1e3;
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
function getDefaultConfig() {
|
|
626
|
+
return {
|
|
627
|
+
panels: {
|
|
628
|
+
git: {
|
|
629
|
+
enabled: true,
|
|
630
|
+
interval: 3e4
|
|
631
|
+
// 30s
|
|
632
|
+
},
|
|
633
|
+
plan: {
|
|
634
|
+
enabled: true,
|
|
635
|
+
interval: 1e4,
|
|
636
|
+
// 10s
|
|
637
|
+
source: ".agenthud/plan.json"
|
|
638
|
+
},
|
|
639
|
+
tests: {
|
|
640
|
+
enabled: true,
|
|
641
|
+
interval: null
|
|
642
|
+
// manual
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
var VALID_PANELS = ["git", "plan", "tests"];
|
|
648
|
+
function parseConfig() {
|
|
649
|
+
const warnings = [];
|
|
650
|
+
const defaultConfig = getDefaultConfig();
|
|
651
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
652
|
+
return { config: defaultConfig, warnings };
|
|
653
|
+
}
|
|
654
|
+
let rawConfig;
|
|
655
|
+
try {
|
|
656
|
+
const content = fs.readFileSync(CONFIG_PATH);
|
|
657
|
+
rawConfig = parseYaml(content);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
660
|
+
warnings.push(`Failed to parse config: ${message}`);
|
|
661
|
+
return { config: defaultConfig, warnings };
|
|
662
|
+
}
|
|
663
|
+
if (!rawConfig || typeof rawConfig !== "object") {
|
|
664
|
+
return { config: defaultConfig, warnings };
|
|
665
|
+
}
|
|
666
|
+
const parsed = rawConfig;
|
|
667
|
+
const panels = parsed.panels;
|
|
668
|
+
if (!panels || typeof panels !== "object") {
|
|
669
|
+
return { config: defaultConfig, warnings };
|
|
670
|
+
}
|
|
671
|
+
const config = getDefaultConfig();
|
|
672
|
+
for (const panelName of Object.keys(panels)) {
|
|
673
|
+
if (!VALID_PANELS.includes(panelName)) {
|
|
674
|
+
warnings.push(`Unknown panel '${panelName}' in config`);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const panelConfig = panels[panelName];
|
|
678
|
+
if (!panelConfig || typeof panelConfig !== "object") {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (panelName === "git") {
|
|
682
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
683
|
+
config.panels.git.enabled = panelConfig.enabled;
|
|
684
|
+
}
|
|
685
|
+
if (typeof panelConfig.interval === "string") {
|
|
686
|
+
const interval = parseInterval(panelConfig.interval);
|
|
687
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
688
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for git panel, using default`);
|
|
689
|
+
} else {
|
|
690
|
+
config.panels.git.interval = interval;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (panelName === "plan") {
|
|
695
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
696
|
+
config.panels.plan.enabled = panelConfig.enabled;
|
|
697
|
+
}
|
|
698
|
+
if (typeof panelConfig.interval === "string") {
|
|
699
|
+
const interval = parseInterval(panelConfig.interval);
|
|
700
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
701
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for plan panel, using default`);
|
|
702
|
+
} else {
|
|
703
|
+
config.panels.plan.interval = interval;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (typeof panelConfig.source === "string") {
|
|
707
|
+
config.panels.plan.source = panelConfig.source;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (panelName === "tests") {
|
|
711
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
712
|
+
config.panels.tests.enabled = panelConfig.enabled;
|
|
713
|
+
}
|
|
714
|
+
if (typeof panelConfig.interval === "string") {
|
|
715
|
+
const interval = parseInterval(panelConfig.interval);
|
|
716
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
717
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for tests panel, using default`);
|
|
718
|
+
} else {
|
|
719
|
+
config.panels.tests.interval = interval;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (typeof panelConfig.command === "string") {
|
|
723
|
+
config.panels.tests.command = panelConfig.command;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return { config, warnings };
|
|
728
|
+
}
|
|
729
|
+
|
|
421
730
|
// src/ui/App.tsx
|
|
422
731
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
function usePlanData() {
|
|
443
|
-
const [data, setData] = useState(() => getPlanData());
|
|
444
|
-
const refresh = useCallback(() => {
|
|
445
|
-
setData(getPlanData());
|
|
446
|
-
}, []);
|
|
447
|
-
return [data, refresh];
|
|
448
|
-
}
|
|
449
|
-
function useTestData() {
|
|
450
|
-
const [data, setData] = useState(() => getTestData());
|
|
451
|
-
const refresh = useCallback(() => {
|
|
452
|
-
setData(getTestData());
|
|
453
|
-
}, []);
|
|
454
|
-
return [data, refresh];
|
|
732
|
+
function generateHotkeys(config, actions) {
|
|
733
|
+
const hotkeys = [];
|
|
734
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
735
|
+
if (config.panels.tests.enabled && config.panels.tests.interval === null && actions.tests) {
|
|
736
|
+
const name = "tests";
|
|
737
|
+
for (const char of name.toLowerCase()) {
|
|
738
|
+
if (!usedKeys.has(char)) {
|
|
739
|
+
usedKeys.add(char);
|
|
740
|
+
hotkeys.push({
|
|
741
|
+
key: char,
|
|
742
|
+
label: "run tests",
|
|
743
|
+
action: actions.tests
|
|
744
|
+
});
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return hotkeys;
|
|
455
750
|
}
|
|
456
751
|
function WelcomeApp() {
|
|
457
752
|
return /* @__PURE__ */ jsx5(WelcomePanel, {});
|
|
458
753
|
}
|
|
459
754
|
function DashboardApp({ mode }) {
|
|
460
755
|
const { exit } = useApp();
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
const [
|
|
756
|
+
const { config, warnings } = useMemo(() => parseConfig(), []);
|
|
757
|
+
const gitIntervalSeconds = config.panels.git.interval ? config.panels.git.interval / 1e3 : null;
|
|
758
|
+
const planIntervalSeconds = config.panels.plan.interval ? config.panels.plan.interval / 1e3 : null;
|
|
759
|
+
const [gitData, setGitData] = useState(() => getGitData(config.panels.git));
|
|
760
|
+
const refreshGit = useCallback(() => {
|
|
761
|
+
setGitData(getGitData(config.panels.git));
|
|
762
|
+
}, [config.panels.git]);
|
|
763
|
+
const [planData, setPlanData] = useState(() => getPlanDataWithConfig(config.panels.plan));
|
|
764
|
+
const refreshPlan = useCallback(() => {
|
|
765
|
+
setPlanData(getPlanDataWithConfig(config.panels.plan));
|
|
766
|
+
}, [config.panels.plan]);
|
|
767
|
+
const getTestDataFromConfig = useCallback(() => {
|
|
768
|
+
if (config.panels.tests.command) {
|
|
769
|
+
return runTestCommand(config.panels.tests.command);
|
|
770
|
+
}
|
|
771
|
+
return getTestData();
|
|
772
|
+
}, [config.panels.tests.command]);
|
|
773
|
+
const [testData, setTestData] = useState(() => getTestDataFromConfig());
|
|
774
|
+
const refreshTest = useCallback(() => {
|
|
775
|
+
setTestData(getTestDataFromConfig());
|
|
776
|
+
}, [getTestDataFromConfig]);
|
|
777
|
+
const [countdowns, setCountdowns] = useState({
|
|
778
|
+
git: gitIntervalSeconds,
|
|
779
|
+
plan: planIntervalSeconds
|
|
780
|
+
});
|
|
465
781
|
const refreshAll = useCallback(() => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
782
|
+
if (config.panels.git.enabled) {
|
|
783
|
+
refreshGit();
|
|
784
|
+
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
785
|
+
}
|
|
786
|
+
if (config.panels.plan.enabled) {
|
|
787
|
+
refreshPlan();
|
|
788
|
+
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
789
|
+
}
|
|
790
|
+
if (config.panels.tests.enabled) {
|
|
791
|
+
refreshTest();
|
|
792
|
+
}
|
|
793
|
+
}, [refreshGit, refreshPlan, refreshTest, config, gitIntervalSeconds, planIntervalSeconds]);
|
|
794
|
+
const hotkeys = useMemo(
|
|
795
|
+
() => generateHotkeys(config, { tests: refreshTest }),
|
|
796
|
+
[config, refreshTest]
|
|
797
|
+
);
|
|
471
798
|
useEffect(() => {
|
|
472
799
|
if (mode !== "watch") return;
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
800
|
+
const timers = [];
|
|
801
|
+
if (config.panels.git.enabled && config.panels.git.interval !== null) {
|
|
802
|
+
timers.push(
|
|
803
|
+
setInterval(() => {
|
|
804
|
+
refreshGit();
|
|
805
|
+
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
806
|
+
}, config.panels.git.interval)
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (config.panels.plan.enabled && config.panels.plan.interval !== null) {
|
|
810
|
+
timers.push(
|
|
811
|
+
setInterval(() => {
|
|
812
|
+
refreshPlan();
|
|
813
|
+
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
814
|
+
}, config.panels.plan.interval)
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (config.panels.tests.enabled && config.panels.tests.interval !== null) {
|
|
818
|
+
timers.push(setInterval(refreshTest, config.panels.tests.interval));
|
|
819
|
+
}
|
|
820
|
+
return () => timers.forEach((t) => clearInterval(t));
|
|
821
|
+
}, [mode, config, refreshGit, refreshPlan, refreshTest, gitIntervalSeconds, planIntervalSeconds]);
|
|
476
822
|
useEffect(() => {
|
|
477
823
|
if (mode !== "watch") return;
|
|
478
824
|
const tick = setInterval(() => {
|
|
479
|
-
|
|
825
|
+
setCountdowns((prev) => ({
|
|
826
|
+
git: prev.git !== null && prev.git > 1 ? prev.git - 1 : prev.git,
|
|
827
|
+
plan: prev.plan !== null && prev.plan > 1 ? prev.plan - 1 : prev.plan
|
|
828
|
+
}));
|
|
480
829
|
}, 1e3);
|
|
481
830
|
return () => clearInterval(tick);
|
|
482
831
|
}, [mode]);
|
|
@@ -488,28 +837,46 @@ function DashboardApp({ mode }) {
|
|
|
488
837
|
if (input === "r") {
|
|
489
838
|
refreshAll();
|
|
490
839
|
}
|
|
840
|
+
for (const hotkey of hotkeys) {
|
|
841
|
+
if (input === hotkey.key) {
|
|
842
|
+
hotkey.action();
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
491
846
|
},
|
|
492
847
|
{ isActive: mode === "watch" }
|
|
493
848
|
);
|
|
849
|
+
const statusBarItems = [];
|
|
850
|
+
for (const hotkey of hotkeys) {
|
|
851
|
+
statusBarItems.push(`${hotkey.key}: ${hotkey.label}`);
|
|
852
|
+
}
|
|
853
|
+
statusBarItems.push("r: refresh");
|
|
854
|
+
statusBarItems.push("q: quit");
|
|
494
855
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
495
|
-
/* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */
|
|
856
|
+
warnings.length > 0 && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: "yellow", children: [
|
|
857
|
+
"\u26A0 ",
|
|
858
|
+
warnings.join(", ")
|
|
859
|
+
] }) }),
|
|
860
|
+
config.panels.git.enabled && /* @__PURE__ */ jsx5(Box5, { children: /* @__PURE__ */ jsx5(
|
|
496
861
|
GitPanel,
|
|
497
862
|
{
|
|
498
863
|
branch: gitData.branch,
|
|
499
864
|
commits: gitData.commits,
|
|
500
865
|
stats: gitData.stats,
|
|
501
|
-
uncommitted: gitData.uncommitted
|
|
866
|
+
uncommitted: gitData.uncommitted,
|
|
867
|
+
countdown: mode === "watch" ? countdowns.git : null
|
|
502
868
|
}
|
|
503
869
|
) }),
|
|
504
|
-
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(
|
|
870
|
+
config.panels.plan.enabled && /* @__PURE__ */ jsx5(Box5, { marginTop: config.panels.git.enabled ? 1 : 0, children: /* @__PURE__ */ jsx5(
|
|
505
871
|
PlanPanel,
|
|
506
872
|
{
|
|
507
873
|
plan: planData.plan,
|
|
508
874
|
decisions: planData.decisions,
|
|
509
|
-
error: planData.error
|
|
875
|
+
error: planData.error,
|
|
876
|
+
countdown: mode === "watch" ? countdowns.plan : null
|
|
510
877
|
}
|
|
511
878
|
) }),
|
|
512
|
-
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(
|
|
879
|
+
config.panels.tests.enabled && /* @__PURE__ */ jsx5(Box5, { marginTop: config.panels.git.enabled || config.panels.plan.enabled ? 1 : 0, children: /* @__PURE__ */ jsx5(
|
|
513
880
|
TestPanel,
|
|
514
881
|
{
|
|
515
882
|
results: testData.results,
|
|
@@ -518,15 +885,14 @@ function DashboardApp({ mode }) {
|
|
|
518
885
|
error: testData.error
|
|
519
886
|
}
|
|
520
887
|
) }),
|
|
521
|
-
mode === "watch" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, width: PANEL_WIDTH, children: /* @__PURE__ */
|
|
522
|
-
"\
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
] }) })
|
|
888
|
+
mode === "watch" && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, width: PANEL_WIDTH, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs5(React.Fragment, { children: [
|
|
889
|
+
index > 0 && " \xB7 ",
|
|
890
|
+
/* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
891
|
+
item.split(":")[0],
|
|
892
|
+
":"
|
|
893
|
+
] }),
|
|
894
|
+
item.split(":").slice(1).join(":")
|
|
895
|
+
] }, index)) }) })
|
|
530
896
|
] });
|
|
531
897
|
}
|
|
532
898
|
function App({ mode, agentDirExists: agentDirExists2 = true }) {
|
|
@@ -553,55 +919,93 @@ function parseArgs(args) {
|
|
|
553
919
|
|
|
554
920
|
// src/commands/init.ts
|
|
555
921
|
import {
|
|
556
|
-
existsSync as
|
|
922
|
+
existsSync as nodeExistsSync2,
|
|
557
923
|
mkdirSync as nodeMkdirSync,
|
|
558
924
|
writeFileSync as nodeWriteFileSync,
|
|
559
|
-
readFileSync as
|
|
925
|
+
readFileSync as nodeReadFileSync4,
|
|
560
926
|
appendFileSync as nodeAppendFileSync
|
|
561
927
|
} from "fs";
|
|
562
|
-
var
|
|
563
|
-
existsSync:
|
|
928
|
+
var fs2 = {
|
|
929
|
+
existsSync: nodeExistsSync2,
|
|
564
930
|
mkdirSync: nodeMkdirSync,
|
|
565
931
|
writeFileSync: nodeWriteFileSync,
|
|
566
|
-
readFileSync: (path) =>
|
|
932
|
+
readFileSync: (path) => nodeReadFileSync4(path, "utf-8"),
|
|
567
933
|
appendFileSync: nodeAppendFileSync
|
|
568
934
|
};
|
|
569
935
|
var AGENT_STATE_SECTION = `## Agent State
|
|
570
936
|
|
|
571
|
-
Maintain \`.
|
|
937
|
+
Maintain \`.agenthud/\` directory:
|
|
572
938
|
- Update \`plan.json\` when plan changes
|
|
573
939
|
- Append to \`decisions.json\` for key decisions
|
|
574
940
|
`;
|
|
941
|
+
var DEFAULT_CONFIG = `# agenthud configuration
|
|
942
|
+
panels:
|
|
943
|
+
git:
|
|
944
|
+
enabled: true
|
|
945
|
+
interval: 30s
|
|
946
|
+
command:
|
|
947
|
+
branch: git branch --show-current
|
|
948
|
+
commits: git log --since=midnight --pretty=format:"%h|%aI|%s"
|
|
949
|
+
stats: git log --since=midnight --numstat --pretty=format:""
|
|
950
|
+
|
|
951
|
+
plan:
|
|
952
|
+
enabled: true
|
|
953
|
+
interval: 10s
|
|
954
|
+
source: .agenthud/plan.json
|
|
955
|
+
|
|
956
|
+
tests:
|
|
957
|
+
enabled: true
|
|
958
|
+
interval: manual
|
|
959
|
+
command: npx vitest run --reporter=json
|
|
960
|
+
`;
|
|
575
961
|
function runInit() {
|
|
576
962
|
const result = {
|
|
577
963
|
created: [],
|
|
578
964
|
skipped: []
|
|
579
965
|
};
|
|
580
|
-
if (!
|
|
581
|
-
|
|
582
|
-
result.created.push(".
|
|
966
|
+
if (!fs2.existsSync(".agenthud")) {
|
|
967
|
+
fs2.mkdirSync(".agenthud", { recursive: true });
|
|
968
|
+
result.created.push(".agenthud/");
|
|
969
|
+
} else {
|
|
970
|
+
result.skipped.push(".agenthud/");
|
|
971
|
+
}
|
|
972
|
+
if (!fs2.existsSync(".agenthud/plan.json")) {
|
|
973
|
+
fs2.writeFileSync(".agenthud/plan.json", "{}\n");
|
|
974
|
+
result.created.push(".agenthud/plan.json");
|
|
975
|
+
} else {
|
|
976
|
+
result.skipped.push(".agenthud/plan.json");
|
|
977
|
+
}
|
|
978
|
+
if (!fs2.existsSync(".agenthud/decisions.json")) {
|
|
979
|
+
fs2.writeFileSync(".agenthud/decisions.json", "[]\n");
|
|
980
|
+
result.created.push(".agenthud/decisions.json");
|
|
583
981
|
} else {
|
|
584
|
-
result.skipped.push(".
|
|
982
|
+
result.skipped.push(".agenthud/decisions.json");
|
|
585
983
|
}
|
|
586
|
-
if (!
|
|
587
|
-
|
|
588
|
-
result.created.push(".
|
|
984
|
+
if (!fs2.existsSync(".agenthud/config.yaml")) {
|
|
985
|
+
fs2.writeFileSync(".agenthud/config.yaml", DEFAULT_CONFIG);
|
|
986
|
+
result.created.push(".agenthud/config.yaml");
|
|
589
987
|
} else {
|
|
590
|
-
result.skipped.push(".
|
|
988
|
+
result.skipped.push(".agenthud/config.yaml");
|
|
591
989
|
}
|
|
592
|
-
if (!
|
|
593
|
-
|
|
594
|
-
result.created.push(".
|
|
990
|
+
if (!fs2.existsSync(".gitignore")) {
|
|
991
|
+
fs2.writeFileSync(".gitignore", ".agenthud/\n");
|
|
992
|
+
result.created.push(".gitignore");
|
|
595
993
|
} else {
|
|
596
|
-
|
|
994
|
+
const content = fs2.readFileSync(".gitignore");
|
|
995
|
+
if (!content.includes(".agenthud/")) {
|
|
996
|
+
fs2.appendFileSync(".gitignore", "\n.agenthud/\n");
|
|
997
|
+
result.created.push(".gitignore");
|
|
998
|
+
} else {
|
|
999
|
+
result.skipped.push(".gitignore");
|
|
1000
|
+
}
|
|
597
1001
|
}
|
|
598
|
-
if (!
|
|
599
|
-
|
|
1002
|
+
if (!fs2.existsSync("CLAUDE.md")) {
|
|
1003
|
+
fs2.writeFileSync("CLAUDE.md", AGENT_STATE_SECTION);
|
|
600
1004
|
result.created.push("CLAUDE.md");
|
|
601
1005
|
} else {
|
|
602
|
-
const content =
|
|
1006
|
+
const content = fs2.readFileSync("CLAUDE.md");
|
|
603
1007
|
if (!content.includes("## Agent State")) {
|
|
604
|
-
|
|
1008
|
+
fs2.appendFileSync("CLAUDE.md", "\n" + AGENT_STATE_SECTION);
|
|
605
1009
|
result.created.push("CLAUDE.md");
|
|
606
1010
|
} else {
|
|
607
1011
|
result.skipped.push("CLAUDE.md");
|
|
@@ -624,11 +1028,11 @@ if (options.command === "init") {
|
|
|
624
1028
|
result.skipped.forEach((file) => console.log(` ${file}`));
|
|
625
1029
|
}
|
|
626
1030
|
console.log("\nNext steps:");
|
|
627
|
-
console.log(" 1. Edit .
|
|
1031
|
+
console.log(" 1. Edit .agenthud/plan.json to add your project plan");
|
|
628
1032
|
console.log(" 2. Run: npx agenthud\n");
|
|
629
1033
|
process.exit(0);
|
|
630
1034
|
}
|
|
631
|
-
var agentDirExists = existsSync(".
|
|
1035
|
+
var agentDirExists = existsSync(".agenthud");
|
|
632
1036
|
if (options.mode === "watch") {
|
|
633
1037
|
clearScreen();
|
|
634
1038
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenthud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"ink": "^6.6.0",
|
|
54
|
-
"react": "^19.2.3"
|
|
54
|
+
"react": "^19.2.3",
|
|
55
|
+
"yaml": "^2.8.2"
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* npm run test:save
|
|
8
8
|
*
|
|
9
9
|
* Output:
|
|
10
|
-
* .
|
|
10
|
+
* .agenthud/test-results.json
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { execSync } from "child_process";
|
|
@@ -106,8 +106,8 @@ function main(): void {
|
|
|
106
106
|
failures: extractFailures(vitestResult),
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
// Ensure .
|
|
110
|
-
const agentDir = join(process.cwd(), ".
|
|
109
|
+
// Ensure .agenthud directory exists
|
|
110
|
+
const agentDir = join(process.cwd(), ".agenthud");
|
|
111
111
|
if (!existsSync(agentDir)) {
|
|
112
112
|
mkdirSync(agentDir, { recursive: true });
|
|
113
113
|
}
|