agenthud 0.3.0 → 0.4.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/dist/index.js +1332 -235
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -6,15 +6,47 @@ 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";
|
|
10
|
-
import { Box as
|
|
9
|
+
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
10
|
+
import { Box as Box6, Text as Text6, useApp, useInput } from "ink";
|
|
11
11
|
|
|
12
12
|
// src/ui/GitPanel.tsx
|
|
13
13
|
import { Box, Text } from "ink";
|
|
14
14
|
|
|
15
15
|
// src/ui/constants.ts
|
|
16
|
-
var
|
|
17
|
-
var CONTENT_WIDTH =
|
|
16
|
+
var DEFAULT_PANEL_WIDTH = 70;
|
|
17
|
+
var CONTENT_WIDTH = DEFAULT_PANEL_WIDTH - 4;
|
|
18
|
+
var INNER_WIDTH = DEFAULT_PANEL_WIDTH - 2;
|
|
19
|
+
function getContentWidth(panelWidth) {
|
|
20
|
+
return panelWidth - 4;
|
|
21
|
+
}
|
|
22
|
+
function getInnerWidth(panelWidth) {
|
|
23
|
+
return panelWidth - 2;
|
|
24
|
+
}
|
|
25
|
+
var BOX = {
|
|
26
|
+
tl: "\u250C",
|
|
27
|
+
tr: "\u2510",
|
|
28
|
+
bl: "\u2514",
|
|
29
|
+
br: "\u2518",
|
|
30
|
+
h: "\u2500",
|
|
31
|
+
v: "\u2502",
|
|
32
|
+
ml: "\u251C",
|
|
33
|
+
mr: "\u2524"
|
|
34
|
+
};
|
|
35
|
+
function createTitleLine(label, suffix = "", panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
36
|
+
const leftPart = BOX.h + " " + label + " ";
|
|
37
|
+
const rightPart = suffix ? " " + suffix + " " + BOX.h : "";
|
|
38
|
+
const dashCount = panelWidth - 1 - leftPart.length - rightPart.length - 1;
|
|
39
|
+
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
40
|
+
return BOX.tl + leftPart + dashes + rightPart + BOX.tr;
|
|
41
|
+
}
|
|
42
|
+
function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
43
|
+
return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
|
|
44
|
+
}
|
|
45
|
+
function padLine(content, panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
46
|
+
const innerWidth = getInnerWidth(panelWidth);
|
|
47
|
+
const padding = innerWidth - content.length;
|
|
48
|
+
return content + " ".repeat(Math.max(0, padding));
|
|
49
|
+
}
|
|
18
50
|
var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
|
|
19
51
|
function truncate(text, maxLength) {
|
|
20
52
|
if (text.length <= maxLength) return text;
|
|
@@ -24,12 +56,25 @@ function truncate(text, maxLength) {
|
|
|
24
56
|
// src/ui/GitPanel.tsx
|
|
25
57
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
26
58
|
var MAX_COMMITS = 5;
|
|
27
|
-
|
|
28
|
-
|
|
59
|
+
function formatCountdown(seconds) {
|
|
60
|
+
if (seconds == null) return "";
|
|
61
|
+
const padded = String(seconds).padStart(2, " ");
|
|
62
|
+
return `\u21BB ${padded}s`;
|
|
63
|
+
}
|
|
64
|
+
function GitPanel({ branch, commits, stats, uncommitted = 0, countdown, width = DEFAULT_PANEL_WIDTH, isRunning = false, justRefreshed = false }) {
|
|
65
|
+
const countdownSuffix = isRunning ? "running..." : formatCountdown(countdown);
|
|
66
|
+
const innerWidth = getInnerWidth(width);
|
|
67
|
+
const contentWidth = getContentWidth(width);
|
|
68
|
+
const maxMessageLength = contentWidth - 10;
|
|
29
69
|
if (branch === null) {
|
|
30
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column",
|
|
31
|
-
/* @__PURE__ */ jsx(
|
|
32
|
-
/* @__PURE__ */
|
|
70
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix, width) }),
|
|
72
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
73
|
+
BOX.v,
|
|
74
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" Not a git repository", width) }),
|
|
75
|
+
BOX.v
|
|
76
|
+
] }),
|
|
77
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
33
78
|
] });
|
|
34
79
|
}
|
|
35
80
|
const displayCommits = commits.slice(0, MAX_COMMITS);
|
|
@@ -37,10 +82,23 @@ function GitPanel({ branch, commits, stats, uncommitted = 0 }) {
|
|
|
37
82
|
const commitWord = commits.length === 1 ? "commit" : "commits";
|
|
38
83
|
const fileWord = stats.files === 1 ? "file" : "files";
|
|
39
84
|
const hasUncommitted = uncommitted > 0;
|
|
40
|
-
|
|
41
|
-
|
|
85
|
+
let statsSuffix = "";
|
|
86
|
+
if (hasCommits) {
|
|
87
|
+
statsSuffix = ` \xB7 +${stats.added} -${stats.deleted} \xB7 ${commits.length} ${commitWord} \xB7 ${stats.files} ${fileWord}`;
|
|
88
|
+
}
|
|
89
|
+
if (hasUncommitted) {
|
|
90
|
+
statsSuffix += ` \xB7 ${uncommitted} dirty`;
|
|
91
|
+
}
|
|
92
|
+
const availableForBranch = innerWidth - 1 - statsSuffix.length;
|
|
93
|
+
const displayBranch = availableForBranch > 3 ? truncate(branch, availableForBranch) : truncate(branch, 10);
|
|
94
|
+
const branchLineLength = 1 + displayBranch.length + statsSuffix.length;
|
|
95
|
+
const branchPadding = Math.max(0, innerWidth - branchLineLength);
|
|
96
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
97
|
+
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix, width) }),
|
|
42
98
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
43
|
-
|
|
99
|
+
BOX.v,
|
|
100
|
+
" ",
|
|
101
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: displayBranch }),
|
|
44
102
|
hasCommits && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
103
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
46
104
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
@@ -69,101 +127,118 @@ function GitPanel({ branch, commits, stats, uncommitted = 0 }) {
|
|
|
69
127
|
uncommitted,
|
|
70
128
|
" dirty"
|
|
71
129
|
] })
|
|
72
|
-
] })
|
|
130
|
+
] }),
|
|
131
|
+
" ".repeat(branchPadding),
|
|
132
|
+
BOX.v
|
|
73
133
|
] }),
|
|
74
|
-
hasCommits ? /* @__PURE__ */ jsx(Fragment, { children: displayCommits.map((commit) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
134
|
+
hasCommits ? /* @__PURE__ */ jsx(Fragment, { children: displayCommits.map((commit) => {
|
|
135
|
+
const msg = truncate(commit.message, maxMessageLength);
|
|
136
|
+
const lineLength = 3 + 7 + 1 + msg.length;
|
|
137
|
+
const commitPadding = Math.max(0, innerWidth - lineLength);
|
|
138
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
139
|
+
BOX.v,
|
|
140
|
+
" \u2022 ",
|
|
141
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: commit.hash.slice(0, 7) }),
|
|
142
|
+
" ",
|
|
143
|
+
msg,
|
|
144
|
+
" ".repeat(commitPadding),
|
|
145
|
+
BOX.v
|
|
146
|
+
] }, commit.hash);
|
|
147
|
+
}) }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
148
|
+
BOX.v,
|
|
149
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" No commits today", width) }),
|
|
150
|
+
BOX.v
|
|
151
|
+
] }),
|
|
152
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
80
153
|
] });
|
|
81
154
|
}
|
|
82
155
|
|
|
83
156
|
// src/ui/PlanPanel.tsx
|
|
84
157
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
85
158
|
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
159
|
var PROGRESS_BAR_WIDTH = 10;
|
|
88
|
-
var MAX_STEP_LENGTH = CONTENT_WIDTH - 2;
|
|
89
|
-
var MAX_DECISION_LENGTH = CONTENT_WIDTH - 2;
|
|
90
160
|
function createProgressBar(done, total) {
|
|
91
161
|
if (total === 0) return "\u2591".repeat(PROGRESS_BAR_WIDTH);
|
|
92
162
|
const filled = Math.round(done / total * PROGRESS_BAR_WIDTH);
|
|
93
163
|
const empty = PROGRESS_BAR_WIDTH - filled;
|
|
94
164
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
95
165
|
}
|
|
96
|
-
|
|
97
|
-
|
|
166
|
+
function formatCountdown2(seconds) {
|
|
167
|
+
if (seconds == null) return "";
|
|
168
|
+
const padded = String(seconds).padStart(2, " ");
|
|
169
|
+
return `\u21BB ${padded}s`;
|
|
170
|
+
}
|
|
171
|
+
function createPlanTitleLine(done, total, countdown, panelWidth, suffixOverride) {
|
|
98
172
|
const label = " Plan ";
|
|
99
173
|
const count = ` ${done}/${total} `;
|
|
100
174
|
const bar = createProgressBar(done, total);
|
|
101
|
-
const
|
|
175
|
+
const suffixStr = suffixOverride || formatCountdown2(countdown);
|
|
176
|
+
const suffix = suffixStr ? ` \xB7 ${suffixStr} ` + BOX.h : "";
|
|
177
|
+
const dashCount = panelWidth - 3 - label.length - count.length - bar.length - suffix.length;
|
|
102
178
|
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;
|
|
179
|
+
return BOX.tl + BOX.h + label + dashes + count + bar + suffix + BOX.tr;
|
|
107
180
|
}
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
|
|
181
|
+
function createSimpleTitleLine(countdown, panelWidth) {
|
|
182
|
+
const label = " Plan ";
|
|
183
|
+
const countdownStr = formatCountdown2(countdown);
|
|
184
|
+
const suffix = countdownStr ? ` ${countdownStr} ` + BOX.h : "";
|
|
185
|
+
const dashCount = panelWidth - 3 - label.length - suffix.length;
|
|
186
|
+
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
187
|
+
return BOX.tl + BOX.h + label + dashes + suffix + BOX.tr;
|
|
111
188
|
}
|
|
112
|
-
function createDecisionsHeader() {
|
|
189
|
+
function createDecisionsHeader(panelWidth) {
|
|
113
190
|
const label = "\u2500 Decisions ";
|
|
114
|
-
const dashCount =
|
|
191
|
+
const dashCount = panelWidth - 1 - label.length - 1;
|
|
115
192
|
return label + "\u2500".repeat(dashCount) + "\u2524";
|
|
116
193
|
}
|
|
117
|
-
function PlanPanel({ plan, decisions, error }) {
|
|
194
|
+
function PlanPanel({ plan, decisions, error, countdown, width = DEFAULT_PANEL_WIDTH, justRefreshed = false, relativeTime }) {
|
|
195
|
+
const contentWidth = getContentWidth(width);
|
|
196
|
+
const maxStepLength = contentWidth - 2;
|
|
197
|
+
const maxDecisionLength = contentWidth - 2;
|
|
118
198
|
if (error || !plan || !plan.goal || !plan.steps) {
|
|
119
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width
|
|
120
|
-
/* @__PURE__ */
|
|
121
|
-
BOX.tl,
|
|
122
|
-
BOX.h,
|
|
123
|
-
" Plan ",
|
|
124
|
-
BOX.h.repeat(INNER_WIDTH - 7),
|
|
125
|
-
BOX.tr
|
|
126
|
-
] }),
|
|
199
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
|
|
200
|
+
/* @__PURE__ */ jsx2(Text2, { children: createSimpleTitleLine(countdown, width) }),
|
|
127
201
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
128
202
|
BOX.v,
|
|
129
|
-
padLine(" " + (error || "No plan found")),
|
|
203
|
+
padLine(" " + (error || "No plan found"), width),
|
|
130
204
|
BOX.v
|
|
131
205
|
] }),
|
|
132
|
-
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine() })
|
|
206
|
+
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
|
|
133
207
|
] });
|
|
134
208
|
}
|
|
135
209
|
const doneCount = plan.steps.filter((s) => s.status === "done").length;
|
|
136
210
|
const totalCount = plan.steps.length;
|
|
137
|
-
|
|
138
|
-
|
|
211
|
+
const titleSuffix = justRefreshed ? "just now" : relativeTime || void 0;
|
|
212
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
|
|
213
|
+
/* @__PURE__ */ jsx2(Text2, { children: createPlanTitleLine(doneCount, totalCount, countdown, width, titleSuffix) }),
|
|
139
214
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
140
215
|
BOX.v,
|
|
141
|
-
padLine(" " + truncate(plan.goal,
|
|
216
|
+
padLine(" " + truncate(plan.goal, contentWidth), width),
|
|
142
217
|
BOX.v
|
|
143
218
|
] }),
|
|
144
219
|
plan.steps.map((step, index) => {
|
|
145
|
-
const stepText = " " + (step.status === "done" ? "\u2713" : step.status === "in-progress" ? "\u2192" : "\u25CB") + " " + truncate(step.step,
|
|
220
|
+
const stepText = " " + (step.status === "done" ? "\u2713" : step.status === "in-progress" ? "\u2192" : "\u25CB") + " " + truncate(step.step, maxStepLength);
|
|
146
221
|
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
147
222
|
BOX.v,
|
|
148
|
-
padLine(stepText),
|
|
223
|
+
padLine(stepText, width),
|
|
149
224
|
BOX.v
|
|
150
|
-
] }, index);
|
|
225
|
+
] }, `step-${index}`);
|
|
151
226
|
}),
|
|
152
227
|
decisions.length > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
153
228
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
154
229
|
"\u251C",
|
|
155
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: createDecisionsHeader() })
|
|
230
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: createDecisionsHeader(width) })
|
|
156
231
|
] }),
|
|
157
232
|
decisions.map((decision, index) => {
|
|
158
|
-
const decText = " \u2022 " + truncate(decision.decision,
|
|
233
|
+
const decText = " \u2022 " + truncate(decision.decision, maxDecisionLength);
|
|
159
234
|
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
160
235
|
BOX.v,
|
|
161
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(decText) }),
|
|
236
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(decText, width) }),
|
|
162
237
|
BOX.v
|
|
163
|
-
] }, index);
|
|
238
|
+
] }, `decision-${index}`);
|
|
164
239
|
})
|
|
165
240
|
] }),
|
|
166
|
-
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine() })
|
|
241
|
+
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
|
|
167
242
|
] });
|
|
168
243
|
}
|
|
169
244
|
|
|
@@ -182,30 +257,59 @@ function formatRelativeTime(timestamp) {
|
|
|
182
257
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
183
258
|
return `${diffDays}d ago`;
|
|
184
259
|
}
|
|
260
|
+
function createSeparator(panelWidth) {
|
|
261
|
+
return BOX.ml + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.mr;
|
|
262
|
+
}
|
|
185
263
|
function TestPanel({
|
|
186
264
|
results,
|
|
187
265
|
isOutdated,
|
|
188
266
|
commitsBehind,
|
|
189
|
-
error
|
|
267
|
+
error,
|
|
268
|
+
width = DEFAULT_PANEL_WIDTH,
|
|
269
|
+
isRunning = false,
|
|
270
|
+
justCompleted = false
|
|
190
271
|
}) {
|
|
272
|
+
const innerWidth = getInnerWidth(width);
|
|
273
|
+
const contentWidth = getContentWidth(width);
|
|
274
|
+
const getTitleSuffix = () => {
|
|
275
|
+
if (isRunning) return "running...";
|
|
276
|
+
if (justCompleted) return "just now";
|
|
277
|
+
if (results) return formatRelativeTime(results.timestamp);
|
|
278
|
+
return "";
|
|
279
|
+
};
|
|
280
|
+
const titleSuffix = getTitleSuffix();
|
|
191
281
|
if (error || !results) {
|
|
192
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column",
|
|
193
|
-
/* @__PURE__ */ jsx3(
|
|
194
|
-
/* @__PURE__ */
|
|
282
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
|
|
283
|
+
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", titleSuffix, width) }),
|
|
284
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
285
|
+
BOX.v,
|
|
286
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" " + (error || "No test results"), width) }),
|
|
287
|
+
BOX.v
|
|
288
|
+
] }),
|
|
289
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
|
|
195
290
|
] });
|
|
196
291
|
}
|
|
197
292
|
const hasFailures = results.failures.length > 0;
|
|
198
|
-
const relativeTime =
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
293
|
+
const relativeTime = titleSuffix;
|
|
294
|
+
let summaryLength = 1 + 2 + String(results.passed).length + " passed".length;
|
|
295
|
+
if (results.failed > 0) {
|
|
296
|
+
summaryLength += 2 + 2 + String(results.failed).length + " failed".length;
|
|
297
|
+
}
|
|
298
|
+
if (results.skipped > 0) {
|
|
299
|
+
summaryLength += 2 + 2 + String(results.skipped).length + " skipped".length;
|
|
300
|
+
}
|
|
301
|
+
summaryLength += " \xB7 ".length + results.hash.length;
|
|
302
|
+
const summaryPadding = Math.max(0, innerWidth - summaryLength);
|
|
303
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
|
|
304
|
+
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", relativeTime, width) }),
|
|
305
|
+
isOutdated && /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
306
|
+
BOX.v,
|
|
307
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: padLine(` \u26A0 Outdated (${commitsBehind} ${commitsBehind === 1 ? "commit" : "commits"} behind)`, width) }),
|
|
308
|
+
BOX.v
|
|
207
309
|
] }),
|
|
208
310
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
311
|
+
BOX.v,
|
|
312
|
+
" ",
|
|
209
313
|
/* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
210
314
|
"\u2713 ",
|
|
211
315
|
results.passed,
|
|
@@ -228,68 +332,270 @@ function TestPanel({
|
|
|
228
332
|
] })
|
|
229
333
|
] }),
|
|
230
334
|
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
231
|
-
" ",
|
|
232
|
-
"\xB7 ",
|
|
233
|
-
results.hash,
|
|
234
335
|
" \xB7 ",
|
|
235
|
-
|
|
236
|
-
] })
|
|
336
|
+
results.hash
|
|
337
|
+
] }),
|
|
338
|
+
" ".repeat(summaryPadding),
|
|
339
|
+
BOX.v
|
|
237
340
|
] }),
|
|
238
341
|
hasFailures && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
239
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children:
|
|
240
|
-
results.failures.map((failure, index) =>
|
|
241
|
-
|
|
342
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: createSeparator(width) }),
|
|
343
|
+
results.failures.map((failure, index) => {
|
|
344
|
+
const fileName = truncate(failure.file, contentWidth - 3);
|
|
345
|
+
const filePadding = Math.max(0, innerWidth - 3 - fileName.length);
|
|
346
|
+
const testName = truncate(failure.name, contentWidth - 5);
|
|
347
|
+
const testPadding = Math.max(0, innerWidth - 5 - testName.length);
|
|
348
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
349
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
350
|
+
BOX.v,
|
|
351
|
+
" ",
|
|
352
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
|
|
353
|
+
"\u2717 ",
|
|
354
|
+
fileName
|
|
355
|
+
] }),
|
|
356
|
+
" ".repeat(filePadding),
|
|
357
|
+
BOX.v
|
|
358
|
+
] }),
|
|
359
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
360
|
+
BOX.v,
|
|
361
|
+
" ",
|
|
362
|
+
"\u2022 ",
|
|
363
|
+
testName,
|
|
364
|
+
" ".repeat(testPadding),
|
|
365
|
+
BOX.v
|
|
366
|
+
] })
|
|
367
|
+
] }, `failure-${index}`);
|
|
368
|
+
})
|
|
369
|
+
] }),
|
|
370
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
|
|
371
|
+
] });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/ui/GenericPanel.tsx
|
|
375
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
376
|
+
import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
377
|
+
var PROGRESS_BAR_WIDTH2 = 10;
|
|
378
|
+
function createProgressBar2(done, total) {
|
|
379
|
+
if (total === 0) return "\u2591".repeat(PROGRESS_BAR_WIDTH2);
|
|
380
|
+
const filled = Math.round(done / total * PROGRESS_BAR_WIDTH2);
|
|
381
|
+
const empty = PROGRESS_BAR_WIDTH2 - filled;
|
|
382
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
383
|
+
}
|
|
384
|
+
function formatTitleSuffix(countdown, relativeTime) {
|
|
385
|
+
if (countdown != null) {
|
|
386
|
+
const padded = String(countdown).padStart(2, " ");
|
|
387
|
+
return `\u21BB ${padded}s`;
|
|
388
|
+
}
|
|
389
|
+
if (relativeTime) return relativeTime;
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
function createProgressTitleLine(title, done, total, panelWidth, countdown, relativeTime) {
|
|
393
|
+
const label = ` ${title} `;
|
|
394
|
+
const count = ` ${done}/${total} `;
|
|
395
|
+
const bar = createProgressBar2(done, total);
|
|
396
|
+
const suffix = formatTitleSuffix(countdown, relativeTime);
|
|
397
|
+
const suffixPart = suffix ? ` \xB7 ${suffix} ` + BOX.h : "";
|
|
398
|
+
const dashCount = panelWidth - 3 - label.length - count.length - bar.length - suffixPart.length;
|
|
399
|
+
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
400
|
+
return BOX.tl + BOX.h + label + dashes + count + bar + suffixPart + BOX.tr;
|
|
401
|
+
}
|
|
402
|
+
function ListRenderer({ data, width }) {
|
|
403
|
+
const items = data.items || [];
|
|
404
|
+
const contentWidth = getContentWidth(width);
|
|
405
|
+
if (items.length === 0 && !data.summary) {
|
|
406
|
+
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
407
|
+
BOX.v,
|
|
408
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padLine(" No data", width) }),
|
|
409
|
+
BOX.v
|
|
410
|
+
] });
|
|
411
|
+
}
|
|
412
|
+
return /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
413
|
+
data.summary && /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
414
|
+
BOX.v,
|
|
415
|
+
padLine(" " + truncate(data.summary, contentWidth), width),
|
|
416
|
+
BOX.v
|
|
417
|
+
] }),
|
|
418
|
+
items.map((item, index) => /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
419
|
+
BOX.v,
|
|
420
|
+
padLine(" \u2022 " + truncate(item.text, contentWidth - 3), width),
|
|
421
|
+
BOX.v
|
|
422
|
+
] }, `list-item-${index}`)),
|
|
423
|
+
items.length === 0 && data.summary && null
|
|
424
|
+
] });
|
|
425
|
+
}
|
|
426
|
+
function ProgressRenderer({ data, width }) {
|
|
427
|
+
const items = data.items || [];
|
|
428
|
+
const contentWidth = getContentWidth(width);
|
|
429
|
+
return /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
430
|
+
data.summary && /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
431
|
+
BOX.v,
|
|
432
|
+
padLine(" " + truncate(data.summary, contentWidth), width),
|
|
433
|
+
BOX.v
|
|
434
|
+
] }),
|
|
435
|
+
items.map((item, index) => {
|
|
436
|
+
const icon = item.status === "done" ? "\u2713" : item.status === "failed" ? "\u2717" : "\u25CB";
|
|
437
|
+
const line = ` ${icon} ${truncate(item.text, contentWidth - 3)}`;
|
|
438
|
+
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
439
|
+
BOX.v,
|
|
440
|
+
padLine(line, width),
|
|
441
|
+
BOX.v
|
|
442
|
+
] }, `progress-item-${index}`);
|
|
443
|
+
}),
|
|
444
|
+
items.length === 0 && !data.summary && /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
445
|
+
BOX.v,
|
|
446
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padLine(" No data", width) }),
|
|
447
|
+
BOX.v
|
|
448
|
+
] })
|
|
449
|
+
] });
|
|
450
|
+
}
|
|
451
|
+
function StatusRenderer({ data, width }) {
|
|
452
|
+
const stats = data.stats || { passed: 0, failed: 0 };
|
|
453
|
+
const items = data.items?.filter((i) => i.status === "failed") || [];
|
|
454
|
+
const innerWidth = getInnerWidth(width);
|
|
455
|
+
const contentWidth = getContentWidth(width);
|
|
456
|
+
let summaryLength = 1 + 2 + String(stats.passed).length + " passed".length;
|
|
457
|
+
if (stats.failed > 0) {
|
|
458
|
+
summaryLength += 2 + 2 + String(stats.failed).length + " failed".length;
|
|
459
|
+
}
|
|
460
|
+
if (stats.skipped && stats.skipped > 0) {
|
|
461
|
+
summaryLength += 2 + 2 + String(stats.skipped).length + " skipped".length;
|
|
462
|
+
}
|
|
463
|
+
const summaryPadding = Math.max(0, innerWidth - summaryLength);
|
|
464
|
+
return /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
465
|
+
data.summary && /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
466
|
+
BOX.v,
|
|
467
|
+
padLine(" " + truncate(data.summary, contentWidth), width),
|
|
468
|
+
BOX.v
|
|
469
|
+
] }),
|
|
470
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
471
|
+
BOX.v,
|
|
472
|
+
" ",
|
|
473
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "green", children: [
|
|
474
|
+
"\u2713 ",
|
|
475
|
+
stats.passed,
|
|
476
|
+
" passed"
|
|
477
|
+
] }),
|
|
478
|
+
stats.failed > 0 && /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
479
|
+
" ",
|
|
480
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "red", children: [
|
|
242
481
|
"\u2717 ",
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
246
|
-
" ",
|
|
247
|
-
"\u2022 ",
|
|
248
|
-
truncate(failure.name, CONTENT_WIDTH - 4)
|
|
482
|
+
stats.failed,
|
|
483
|
+
" failed"
|
|
249
484
|
] })
|
|
250
|
-
] },
|
|
251
|
-
|
|
485
|
+
] }),
|
|
486
|
+
stats.skipped && stats.skipped > 0 && /* @__PURE__ */ jsxs4(Fragment4, { children: [
|
|
487
|
+
" ",
|
|
488
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
489
|
+
"\u25CB ",
|
|
490
|
+
stats.skipped,
|
|
491
|
+
" skipped"
|
|
492
|
+
] })
|
|
493
|
+
] }),
|
|
494
|
+
" ".repeat(summaryPadding),
|
|
495
|
+
BOX.v
|
|
496
|
+
] }),
|
|
497
|
+
items.length > 0 && items.map((item, index) => /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
498
|
+
BOX.v,
|
|
499
|
+
padLine(" \u2022 " + truncate(item.text, contentWidth - 3), width),
|
|
500
|
+
BOX.v
|
|
501
|
+
] }, `status-item-${index}`))
|
|
502
|
+
] });
|
|
503
|
+
}
|
|
504
|
+
function GenericPanel({
|
|
505
|
+
data,
|
|
506
|
+
renderer = "list",
|
|
507
|
+
countdown,
|
|
508
|
+
relativeTime,
|
|
509
|
+
error,
|
|
510
|
+
width = DEFAULT_PANEL_WIDTH,
|
|
511
|
+
isRunning = false,
|
|
512
|
+
justRefreshed = false
|
|
513
|
+
}) {
|
|
514
|
+
const suffix = isRunning ? "running..." : formatTitleSuffix(countdown, relativeTime);
|
|
515
|
+
const suffixColor = isRunning ? "yellow" : justRefreshed ? "green" : void 0;
|
|
516
|
+
const progress = data.progress || { done: 0, total: 0 };
|
|
517
|
+
if (error) {
|
|
518
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
|
|
519
|
+
/* @__PURE__ */ jsx4(Text4, { children: createTitleLine(data.title, suffix, width) }),
|
|
520
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
521
|
+
BOX.v,
|
|
522
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padLine(" " + error, width) }),
|
|
523
|
+
BOX.v
|
|
524
|
+
] }),
|
|
525
|
+
/* @__PURE__ */ jsx4(Text4, { children: createBottomLine(width) })
|
|
526
|
+
] });
|
|
527
|
+
}
|
|
528
|
+
if (renderer === "progress") {
|
|
529
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
|
|
530
|
+
/* @__PURE__ */ jsx4(Text4, { children: createProgressTitleLine(data.title, progress.done, progress.total, width, countdown, relativeTime) }),
|
|
531
|
+
/* @__PURE__ */ jsx4(ProgressRenderer, { data, width }),
|
|
532
|
+
/* @__PURE__ */ jsx4(Text4, { children: createBottomLine(width) })
|
|
533
|
+
] });
|
|
534
|
+
}
|
|
535
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
|
|
536
|
+
/* @__PURE__ */ jsx4(Text4, { children: createTitleLine(data.title, suffix, width) }),
|
|
537
|
+
renderer === "status" ? /* @__PURE__ */ jsx4(StatusRenderer, { data, width }) : /* @__PURE__ */ jsx4(ListRenderer, { data, width }),
|
|
538
|
+
/* @__PURE__ */ jsx4(Text4, { children: createBottomLine(width) })
|
|
252
539
|
] });
|
|
253
540
|
}
|
|
254
541
|
|
|
255
542
|
// src/ui/WelcomePanel.tsx
|
|
256
|
-
import { Box as
|
|
257
|
-
import { jsx as
|
|
543
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
544
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
258
545
|
function WelcomePanel() {
|
|
259
|
-
return /* @__PURE__ */
|
|
260
|
-
/* @__PURE__ */
|
|
261
|
-
/* @__PURE__ */
|
|
262
|
-
/* @__PURE__ */
|
|
263
|
-
/* @__PURE__ */
|
|
264
|
-
/* @__PURE__ */
|
|
265
|
-
/* @__PURE__ */
|
|
266
|
-
/* @__PURE__ */
|
|
267
|
-
/* @__PURE__ */
|
|
268
|
-
/* @__PURE__ */
|
|
269
|
-
/* @__PURE__ */
|
|
546
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "single", paddingX: 1, width: DEFAULT_PANEL_WIDTH, children: [
|
|
547
|
+
/* @__PURE__ */ jsx5(Box5, { marginTop: -1, children: /* @__PURE__ */ jsx5(Text5, { children: " Welcome to agenthud " }) }),
|
|
548
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
549
|
+
/* @__PURE__ */ jsx5(Text5, { children: " No .agenthud/ directory found." }),
|
|
550
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
551
|
+
/* @__PURE__ */ jsx5(Text5, { children: " Quick setup:" }),
|
|
552
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: " npx agenthud init" }),
|
|
553
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
554
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " Or visit: github.com/neochoon/agenthud" }),
|
|
555
|
+
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
556
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " Press q to quit" })
|
|
270
557
|
] });
|
|
271
558
|
}
|
|
272
559
|
|
|
273
560
|
// src/data/git.ts
|
|
274
|
-
import { execSync as nodeExecSync } from "child_process";
|
|
561
|
+
import { execSync as nodeExecSync, exec as nodeExec } from "child_process";
|
|
562
|
+
import { promisify } from "util";
|
|
563
|
+
var execAsync = promisify(nodeExec);
|
|
275
564
|
var execFn = (command, options2) => nodeExecSync(command, options2);
|
|
276
|
-
function
|
|
565
|
+
function getUncommittedCount() {
|
|
277
566
|
try {
|
|
278
|
-
const result = execFn("git
|
|
567
|
+
const result = execFn("git status --porcelain", {
|
|
279
568
|
encoding: "utf-8"
|
|
280
569
|
});
|
|
281
|
-
|
|
570
|
+
const lines = result.trim().split("\n").filter(Boolean);
|
|
571
|
+
return lines.length;
|
|
282
572
|
} catch {
|
|
283
|
-
return
|
|
573
|
+
return 0;
|
|
284
574
|
}
|
|
285
575
|
}
|
|
286
|
-
|
|
576
|
+
var DEFAULT_COMMANDS = {
|
|
577
|
+
branch: "git branch --show-current",
|
|
578
|
+
commits: 'git log --since=midnight --format="%h|%aI|%s"',
|
|
579
|
+
stats: 'git log --since=midnight --numstat --format=""'
|
|
580
|
+
};
|
|
581
|
+
function getGitData(config) {
|
|
582
|
+
const commands = {
|
|
583
|
+
branch: config.command?.branch || DEFAULT_COMMANDS.branch,
|
|
584
|
+
commits: config.command?.commits || DEFAULT_COMMANDS.commits,
|
|
585
|
+
stats: config.command?.stats || DEFAULT_COMMANDS.stats
|
|
586
|
+
};
|
|
587
|
+
let branch = null;
|
|
287
588
|
try {
|
|
288
|
-
const result = execFn(
|
|
289
|
-
|
|
290
|
-
|
|
589
|
+
const result = execFn(commands.branch, { encoding: "utf-8" });
|
|
590
|
+
branch = result.trim();
|
|
591
|
+
} catch {
|
|
592
|
+
branch = null;
|
|
593
|
+
}
|
|
594
|
+
let commits = [];
|
|
595
|
+
try {
|
|
596
|
+
const result = execFn(commands.commits, { encoding: "utf-8" });
|
|
291
597
|
const lines = result.trim().split("\n").filter(Boolean);
|
|
292
|
-
|
|
598
|
+
commits = lines.map((line) => {
|
|
293
599
|
const [hash, timestamp, ...messageParts] = line.split("|");
|
|
294
600
|
return {
|
|
295
601
|
hash,
|
|
@@ -298,14 +604,11 @@ function getTodayCommits() {
|
|
|
298
604
|
};
|
|
299
605
|
});
|
|
300
606
|
} catch {
|
|
301
|
-
|
|
607
|
+
commits = [];
|
|
302
608
|
}
|
|
303
|
-
}
|
|
304
|
-
function getTodayStats() {
|
|
609
|
+
let stats = { added: 0, deleted: 0, files: 0 };
|
|
305
610
|
try {
|
|
306
|
-
const result = execFn(
|
|
307
|
-
encoding: "utf-8"
|
|
308
|
-
});
|
|
611
|
+
const result = execFn(commands.stats, { encoding: "utf-8" });
|
|
309
612
|
const lines = result.trim().split("\n").filter(Boolean);
|
|
310
613
|
let added = 0;
|
|
311
614
|
let deleted = 0;
|
|
@@ -320,34 +623,75 @@ function getTodayStats() {
|
|
|
320
623
|
deleted += parseInt(deletedStr, 10) || 0;
|
|
321
624
|
if (filename) filesSet.add(filename);
|
|
322
625
|
}
|
|
323
|
-
|
|
626
|
+
stats = { added, deleted, files: filesSet.size };
|
|
324
627
|
} catch {
|
|
325
|
-
|
|
628
|
+
stats = { added: 0, deleted: 0, files: 0 };
|
|
326
629
|
}
|
|
630
|
+
const uncommitted = getUncommittedCount();
|
|
631
|
+
return { branch, commits, stats, uncommitted };
|
|
327
632
|
}
|
|
328
|
-
function
|
|
633
|
+
async function getGitDataAsync(config) {
|
|
634
|
+
const commands = {
|
|
635
|
+
branch: config.command?.branch || DEFAULT_COMMANDS.branch,
|
|
636
|
+
commits: config.command?.commits || DEFAULT_COMMANDS.commits,
|
|
637
|
+
stats: config.command?.stats || DEFAULT_COMMANDS.stats
|
|
638
|
+
};
|
|
639
|
+
let branch = null;
|
|
329
640
|
try {
|
|
330
|
-
const
|
|
331
|
-
|
|
641
|
+
const { stdout } = await execAsync(commands.branch);
|
|
642
|
+
branch = stdout.trim();
|
|
643
|
+
} catch {
|
|
644
|
+
branch = null;
|
|
645
|
+
}
|
|
646
|
+
let commits = [];
|
|
647
|
+
try {
|
|
648
|
+
const { stdout } = await execAsync(commands.commits);
|
|
649
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
650
|
+
commits = lines.map((line) => {
|
|
651
|
+
const [hash, timestamp, ...messageParts] = line.split("|");
|
|
652
|
+
return {
|
|
653
|
+
hash,
|
|
654
|
+
message: messageParts.join("|"),
|
|
655
|
+
timestamp: new Date(timestamp)
|
|
656
|
+
};
|
|
332
657
|
});
|
|
333
|
-
const lines = result.trim().split("\n").filter(Boolean);
|
|
334
|
-
return lines.length;
|
|
335
658
|
} catch {
|
|
336
|
-
|
|
659
|
+
commits = [];
|
|
660
|
+
}
|
|
661
|
+
let stats = { added: 0, deleted: 0, files: 0 };
|
|
662
|
+
try {
|
|
663
|
+
const { stdout } = await execAsync(commands.stats);
|
|
664
|
+
const lines = stdout.trim().split("\n").filter(Boolean);
|
|
665
|
+
let added = 0;
|
|
666
|
+
let deleted = 0;
|
|
667
|
+
const filesSet = /* @__PURE__ */ new Set();
|
|
668
|
+
for (const line of lines) {
|
|
669
|
+
const [addedStr, deletedStr, filename] = line.split(" ");
|
|
670
|
+
if (addedStr === "-" || deletedStr === "-") {
|
|
671
|
+
if (filename) filesSet.add(filename);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
added += parseInt(addedStr, 10) || 0;
|
|
675
|
+
deleted += parseInt(deletedStr, 10) || 0;
|
|
676
|
+
if (filename) filesSet.add(filename);
|
|
677
|
+
}
|
|
678
|
+
stats = { added, deleted, files: filesSet.size };
|
|
679
|
+
} catch {
|
|
680
|
+
stats = { added: 0, deleted: 0, files: 0 };
|
|
337
681
|
}
|
|
682
|
+
const uncommitted = getUncommittedCount();
|
|
683
|
+
return { branch, commits, stats, uncommitted };
|
|
338
684
|
}
|
|
339
685
|
|
|
340
686
|
// src/data/plan.ts
|
|
341
687
|
import { readFileSync as nodeReadFileSync } from "fs";
|
|
342
|
-
import { join } from "path";
|
|
343
|
-
var AGENT_DIR = ".agenthud";
|
|
344
|
-
var PLAN_FILE = "plan.json";
|
|
345
|
-
var DECISIONS_FILE = "decisions.json";
|
|
688
|
+
import { join, dirname } from "path";
|
|
346
689
|
var MAX_DECISIONS = 3;
|
|
347
690
|
var readFileFn = (path) => nodeReadFileSync(path, "utf-8");
|
|
348
|
-
function
|
|
349
|
-
const planPath =
|
|
350
|
-
const
|
|
691
|
+
function getPlanDataWithConfig(config) {
|
|
692
|
+
const planPath = config.source;
|
|
693
|
+
const planDir = dirname(planPath);
|
|
694
|
+
const decisionsPath = join(planDir, "decisions.json");
|
|
351
695
|
let plan = null;
|
|
352
696
|
let decisions = [];
|
|
353
697
|
let error;
|
|
@@ -376,7 +720,7 @@ function getPlanData(dir = process.cwd()) {
|
|
|
376
720
|
import { readFileSync as nodeReadFileSync2 } from "fs";
|
|
377
721
|
import { execSync as nodeExecSync2 } from "child_process";
|
|
378
722
|
import { join as join2 } from "path";
|
|
379
|
-
var
|
|
723
|
+
var AGENT_DIR = ".agenthud";
|
|
380
724
|
var TEST_RESULTS_FILE = "test-results.json";
|
|
381
725
|
var readFileFn2 = (path) => nodeReadFileSync2(path, "utf-8");
|
|
382
726
|
var getHeadHashFn = () => {
|
|
@@ -389,7 +733,7 @@ var getCommitCountFn = (fromHash) => {
|
|
|
389
733
|
return parseInt(result, 10) || 0;
|
|
390
734
|
};
|
|
391
735
|
function getTestData(dir = process.cwd()) {
|
|
392
|
-
const testResultsPath = join2(dir,
|
|
736
|
+
const testResultsPath = join2(dir, AGENT_DIR, TEST_RESULTS_FILE);
|
|
393
737
|
let results = null;
|
|
394
738
|
let isOutdated = false;
|
|
395
739
|
let commitsBehind = 0;
|
|
@@ -418,68 +762,735 @@ function getTestData(dir = process.cwd()) {
|
|
|
418
762
|
return { results, isOutdated, commitsBehind, error };
|
|
419
763
|
}
|
|
420
764
|
|
|
421
|
-
// src/
|
|
422
|
-
import {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
765
|
+
// src/data/custom.ts
|
|
766
|
+
import { execSync as nodeExecSync3, exec as nodeExec2 } from "child_process";
|
|
767
|
+
import { readFileSync as nodeReadFileSync3, promises as fsPromises } from "fs";
|
|
768
|
+
import { promisify as promisify2 } from "util";
|
|
769
|
+
var execAsync2 = promisify2(nodeExec2);
|
|
770
|
+
var execFn2 = (cmd, options2) => nodeExecSync3(cmd, options2);
|
|
771
|
+
var readFileFn3 = (path) => nodeReadFileSync3(path, "utf-8");
|
|
772
|
+
function capitalizeFirst(str) {
|
|
773
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
774
|
+
}
|
|
775
|
+
function getCustomPanelData(name, panelConfig) {
|
|
776
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
777
|
+
const defaultData = {
|
|
778
|
+
title: capitalizeFirst(name)
|
|
779
|
+
};
|
|
780
|
+
if (panelConfig.command) {
|
|
781
|
+
try {
|
|
782
|
+
const output = execFn2(panelConfig.command, { encoding: "utf-8" }).trim();
|
|
783
|
+
try {
|
|
784
|
+
const parsed = JSON.parse(output);
|
|
785
|
+
return {
|
|
786
|
+
data: {
|
|
787
|
+
title: parsed.title || capitalizeFirst(name),
|
|
788
|
+
summary: parsed.summary,
|
|
789
|
+
items: parsed.items,
|
|
790
|
+
progress: parsed.progress,
|
|
791
|
+
stats: parsed.stats
|
|
792
|
+
},
|
|
793
|
+
timestamp
|
|
794
|
+
};
|
|
795
|
+
} catch {
|
|
796
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
797
|
+
return {
|
|
798
|
+
data: {
|
|
799
|
+
title: capitalizeFirst(name),
|
|
800
|
+
items: lines.map((text) => ({ text }))
|
|
801
|
+
},
|
|
802
|
+
timestamp
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
807
|
+
return {
|
|
808
|
+
data: defaultData,
|
|
809
|
+
error: `Command failed: ${message.split("\n")[0]}`,
|
|
810
|
+
timestamp
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (panelConfig.source) {
|
|
815
|
+
try {
|
|
816
|
+
const content = readFileFn3(panelConfig.source);
|
|
817
|
+
const parsed = JSON.parse(content);
|
|
818
|
+
return {
|
|
819
|
+
data: {
|
|
820
|
+
title: parsed.title || capitalizeFirst(name),
|
|
821
|
+
summary: parsed.summary,
|
|
822
|
+
items: parsed.items,
|
|
823
|
+
progress: parsed.progress,
|
|
824
|
+
stats: parsed.stats
|
|
825
|
+
},
|
|
826
|
+
timestamp
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
830
|
+
if (message.includes("ENOENT")) {
|
|
831
|
+
return {
|
|
832
|
+
data: defaultData,
|
|
833
|
+
error: "File not found",
|
|
834
|
+
timestamp
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
data: defaultData,
|
|
839
|
+
error: "Invalid JSON",
|
|
840
|
+
timestamp
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
data: defaultData,
|
|
846
|
+
error: "No command or source configured",
|
|
847
|
+
timestamp
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
async function getCustomPanelDataAsync(name, panelConfig) {
|
|
851
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
852
|
+
const defaultData = {
|
|
853
|
+
title: capitalizeFirst(name)
|
|
854
|
+
};
|
|
855
|
+
if (panelConfig.command) {
|
|
856
|
+
try {
|
|
857
|
+
const { stdout } = await execAsync2(panelConfig.command);
|
|
858
|
+
const output = stdout.trim();
|
|
859
|
+
try {
|
|
860
|
+
const parsed = JSON.parse(output);
|
|
861
|
+
return {
|
|
862
|
+
data: {
|
|
863
|
+
title: parsed.title || capitalizeFirst(name),
|
|
864
|
+
summary: parsed.summary,
|
|
865
|
+
items: parsed.items,
|
|
866
|
+
progress: parsed.progress,
|
|
867
|
+
stats: parsed.stats
|
|
868
|
+
},
|
|
869
|
+
timestamp
|
|
870
|
+
};
|
|
871
|
+
} catch {
|
|
872
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
873
|
+
return {
|
|
874
|
+
data: {
|
|
875
|
+
title: capitalizeFirst(name),
|
|
876
|
+
items: lines.map((text) => ({ text }))
|
|
877
|
+
},
|
|
878
|
+
timestamp
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
} catch (error) {
|
|
882
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
883
|
+
return {
|
|
884
|
+
data: defaultData,
|
|
885
|
+
error: `Command failed: ${message.split("\n")[0]}`,
|
|
886
|
+
timestamp
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (panelConfig.source) {
|
|
891
|
+
try {
|
|
892
|
+
const content = await fsPromises.readFile(panelConfig.source, "utf-8");
|
|
893
|
+
const parsed = JSON.parse(content);
|
|
894
|
+
return {
|
|
895
|
+
data: {
|
|
896
|
+
title: parsed.title || capitalizeFirst(name),
|
|
897
|
+
summary: parsed.summary,
|
|
898
|
+
items: parsed.items,
|
|
899
|
+
progress: parsed.progress,
|
|
900
|
+
stats: parsed.stats
|
|
901
|
+
},
|
|
902
|
+
timestamp
|
|
903
|
+
};
|
|
904
|
+
} catch (error) {
|
|
905
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
906
|
+
if (message.includes("ENOENT")) {
|
|
907
|
+
return {
|
|
908
|
+
data: defaultData,
|
|
909
|
+
error: "File not found",
|
|
910
|
+
timestamp
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
data: defaultData,
|
|
915
|
+
error: "Invalid JSON",
|
|
916
|
+
timestamp
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
data: defaultData,
|
|
922
|
+
error: "No command or source configured",
|
|
923
|
+
timestamp
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/runner/command.ts
|
|
928
|
+
import { execSync as nodeExecSync4 } from "child_process";
|
|
929
|
+
var execFn3 = (command, options2) => nodeExecSync4(command, options2);
|
|
930
|
+
function parseVitestOutput(output) {
|
|
931
|
+
try {
|
|
932
|
+
const data = JSON.parse(output);
|
|
933
|
+
if (typeof data.numPassedTests !== "number" || typeof data.numFailedTests !== "number") {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
const failures = [];
|
|
937
|
+
for (const testResult of data.testResults || []) {
|
|
938
|
+
for (const assertion of testResult.assertionResults || []) {
|
|
939
|
+
if (assertion.status === "failed") {
|
|
940
|
+
failures.push({
|
|
941
|
+
file: testResult.name,
|
|
942
|
+
name: assertion.title
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
passed: data.numPassedTests,
|
|
949
|
+
failed: data.numFailedTests,
|
|
950
|
+
skipped: data.numPendingTests || 0,
|
|
951
|
+
failures
|
|
952
|
+
};
|
|
953
|
+
} catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function getHeadHash() {
|
|
958
|
+
try {
|
|
959
|
+
return execFn3("git rev-parse --short HEAD", {
|
|
960
|
+
encoding: "utf-8",
|
|
961
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
962
|
+
}).trim();
|
|
963
|
+
} catch {
|
|
964
|
+
return "unknown";
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function runTestCommand(command) {
|
|
968
|
+
let output;
|
|
969
|
+
try {
|
|
970
|
+
output = execFn3(command, {
|
|
971
|
+
encoding: "utf-8",
|
|
972
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
438
973
|
});
|
|
439
|
-
}
|
|
440
|
-
|
|
974
|
+
} catch (error) {
|
|
975
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
976
|
+
return {
|
|
977
|
+
results: null,
|
|
978
|
+
isOutdated: false,
|
|
979
|
+
commitsBehind: 0,
|
|
980
|
+
error: message
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const parsed = parseVitestOutput(output);
|
|
984
|
+
if (!parsed) {
|
|
985
|
+
return {
|
|
986
|
+
results: null,
|
|
987
|
+
isOutdated: false,
|
|
988
|
+
commitsBehind: 0,
|
|
989
|
+
error: "Failed to parse test output"
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
const hash = getHeadHash();
|
|
993
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
994
|
+
const results = {
|
|
995
|
+
hash,
|
|
996
|
+
timestamp,
|
|
997
|
+
passed: parsed.passed,
|
|
998
|
+
failed: parsed.failed,
|
|
999
|
+
skipped: parsed.skipped,
|
|
1000
|
+
failures: parsed.failures
|
|
1001
|
+
};
|
|
1002
|
+
return {
|
|
1003
|
+
results,
|
|
1004
|
+
isOutdated: false,
|
|
1005
|
+
commitsBehind: 0
|
|
1006
|
+
};
|
|
441
1007
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1008
|
+
|
|
1009
|
+
// src/config/parser.ts
|
|
1010
|
+
import {
|
|
1011
|
+
existsSync as nodeExistsSync,
|
|
1012
|
+
readFileSync as nodeReadFileSync4
|
|
1013
|
+
} from "fs";
|
|
1014
|
+
import { parse as parseYaml } from "yaml";
|
|
1015
|
+
var fs = {
|
|
1016
|
+
existsSync: nodeExistsSync,
|
|
1017
|
+
readFileSync: (path) => nodeReadFileSync4(path, "utf-8")
|
|
1018
|
+
};
|
|
1019
|
+
var DEFAULT_WIDTH = 70;
|
|
1020
|
+
var MIN_WIDTH = 50;
|
|
1021
|
+
var MAX_WIDTH = 120;
|
|
1022
|
+
var CONFIG_PATH = ".agenthud/config.yaml";
|
|
1023
|
+
function parseInterval(interval) {
|
|
1024
|
+
if (!interval || interval === "manual") {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
const match = interval.match(/^(\d+)(s|m)$/);
|
|
1028
|
+
if (!match) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
const value = parseInt(match[1], 10);
|
|
1032
|
+
const unit = match[2];
|
|
1033
|
+
if (unit === "s") {
|
|
1034
|
+
return value * 1e3;
|
|
1035
|
+
} else if (unit === "m") {
|
|
1036
|
+
return value * 60 * 1e3;
|
|
1037
|
+
}
|
|
1038
|
+
return null;
|
|
448
1039
|
}
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1040
|
+
function getDefaultConfig() {
|
|
1041
|
+
return {
|
|
1042
|
+
panels: {
|
|
1043
|
+
git: {
|
|
1044
|
+
enabled: true,
|
|
1045
|
+
interval: 3e4
|
|
1046
|
+
// 30s
|
|
1047
|
+
},
|
|
1048
|
+
plan: {
|
|
1049
|
+
enabled: true,
|
|
1050
|
+
interval: 1e4,
|
|
1051
|
+
// 10s
|
|
1052
|
+
source: ".agenthud/plan.json"
|
|
1053
|
+
},
|
|
1054
|
+
tests: {
|
|
1055
|
+
enabled: true,
|
|
1056
|
+
interval: null
|
|
1057
|
+
// manual
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
panelOrder: ["git", "plan", "tests"],
|
|
1061
|
+
width: DEFAULT_WIDTH
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
var BUILTIN_PANELS = ["git", "plan", "tests"];
|
|
1065
|
+
var VALID_RENDERERS = ["list", "progress", "status"];
|
|
1066
|
+
function parseConfig() {
|
|
1067
|
+
const warnings = [];
|
|
1068
|
+
const defaultConfig = getDefaultConfig();
|
|
1069
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
1070
|
+
return { config: defaultConfig, warnings };
|
|
1071
|
+
}
|
|
1072
|
+
let rawConfig;
|
|
1073
|
+
try {
|
|
1074
|
+
const content = fs.readFileSync(CONFIG_PATH);
|
|
1075
|
+
rawConfig = parseYaml(content);
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1078
|
+
warnings.push(`Failed to parse config: ${message}`);
|
|
1079
|
+
return { config: defaultConfig, warnings };
|
|
1080
|
+
}
|
|
1081
|
+
if (!rawConfig || typeof rawConfig !== "object") {
|
|
1082
|
+
return { config: defaultConfig, warnings };
|
|
1083
|
+
}
|
|
1084
|
+
const parsed = rawConfig;
|
|
1085
|
+
const config = getDefaultConfig();
|
|
1086
|
+
if (typeof parsed.width === "number") {
|
|
1087
|
+
if (parsed.width < MIN_WIDTH) {
|
|
1088
|
+
warnings.push(`Width ${parsed.width} is too small, using minimum of ${MIN_WIDTH}`);
|
|
1089
|
+
config.width = MIN_WIDTH;
|
|
1090
|
+
} else if (parsed.width > MAX_WIDTH) {
|
|
1091
|
+
warnings.push(`Width ${parsed.width} is too large, using maximum of ${MAX_WIDTH}`);
|
|
1092
|
+
config.width = MAX_WIDTH;
|
|
1093
|
+
} else {
|
|
1094
|
+
config.width = parsed.width;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const panels = parsed.panels;
|
|
1098
|
+
if (!panels || typeof panels !== "object") {
|
|
1099
|
+
return { config, warnings };
|
|
1100
|
+
}
|
|
1101
|
+
const customPanels = {};
|
|
1102
|
+
const panelOrder = [];
|
|
1103
|
+
for (const panelName of Object.keys(panels)) {
|
|
1104
|
+
panelOrder.push(panelName);
|
|
1105
|
+
const panelConfig = panels[panelName];
|
|
1106
|
+
if (!panelConfig || typeof panelConfig !== "object") {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (panelName === "git") {
|
|
1110
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
1111
|
+
config.panels.git.enabled = panelConfig.enabled;
|
|
1112
|
+
}
|
|
1113
|
+
if (typeof panelConfig.interval === "string") {
|
|
1114
|
+
const interval = parseInterval(panelConfig.interval);
|
|
1115
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
1116
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for git panel, using default`);
|
|
1117
|
+
} else {
|
|
1118
|
+
config.panels.git.interval = interval;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (panelName === "plan") {
|
|
1124
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
1125
|
+
config.panels.plan.enabled = panelConfig.enabled;
|
|
1126
|
+
}
|
|
1127
|
+
if (typeof panelConfig.interval === "string") {
|
|
1128
|
+
const interval = parseInterval(panelConfig.interval);
|
|
1129
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
1130
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for plan panel, using default`);
|
|
1131
|
+
} else {
|
|
1132
|
+
config.panels.plan.interval = interval;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (typeof panelConfig.source === "string") {
|
|
1136
|
+
config.panels.plan.source = panelConfig.source;
|
|
1137
|
+
}
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (panelName === "tests") {
|
|
1141
|
+
if (typeof panelConfig.enabled === "boolean") {
|
|
1142
|
+
config.panels.tests.enabled = panelConfig.enabled;
|
|
1143
|
+
}
|
|
1144
|
+
if (typeof panelConfig.interval === "string") {
|
|
1145
|
+
const interval = parseInterval(panelConfig.interval);
|
|
1146
|
+
if (interval === null && panelConfig.interval !== "manual") {
|
|
1147
|
+
warnings.push(`Invalid interval '${panelConfig.interval}' for tests panel, using default`);
|
|
1148
|
+
} else {
|
|
1149
|
+
config.panels.tests.interval = interval;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (typeof panelConfig.command === "string") {
|
|
1153
|
+
config.panels.tests.command = panelConfig.command;
|
|
1154
|
+
}
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const customPanel = {
|
|
1158
|
+
enabled: typeof panelConfig.enabled === "boolean" ? panelConfig.enabled : true,
|
|
1159
|
+
interval: 3e4,
|
|
1160
|
+
// default 30s
|
|
1161
|
+
renderer: "list"
|
|
1162
|
+
// default
|
|
1163
|
+
};
|
|
1164
|
+
if (typeof panelConfig.interval === "string") {
|
|
1165
|
+
const interval = parseInterval(panelConfig.interval);
|
|
1166
|
+
customPanel.interval = interval;
|
|
1167
|
+
}
|
|
1168
|
+
if (typeof panelConfig.command === "string") {
|
|
1169
|
+
customPanel.command = panelConfig.command;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof panelConfig.source === "string") {
|
|
1172
|
+
customPanel.source = panelConfig.source;
|
|
1173
|
+
}
|
|
1174
|
+
if (typeof panelConfig.renderer === "string") {
|
|
1175
|
+
if (VALID_RENDERERS.includes(panelConfig.renderer)) {
|
|
1176
|
+
customPanel.renderer = panelConfig.renderer;
|
|
1177
|
+
} else {
|
|
1178
|
+
warnings.push(`Invalid renderer '${panelConfig.renderer}' for custom panel, using 'list'`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
customPanels[panelName] = customPanel;
|
|
1182
|
+
}
|
|
1183
|
+
if (Object.keys(customPanels).length > 0) {
|
|
1184
|
+
config.customPanels = customPanels;
|
|
1185
|
+
}
|
|
1186
|
+
for (const builtIn of BUILTIN_PANELS) {
|
|
1187
|
+
if (!panelOrder.includes(builtIn)) {
|
|
1188
|
+
panelOrder.push(builtIn);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
config.panelOrder = panelOrder;
|
|
1192
|
+
return { config, warnings };
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/ui/App.tsx
|
|
1196
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1197
|
+
var DEFAULT_VISUAL_STATE = {
|
|
1198
|
+
isRunning: false,
|
|
1199
|
+
justRefreshed: false,
|
|
1200
|
+
justCompleted: false
|
|
1201
|
+
};
|
|
1202
|
+
var FEEDBACK_DURATION = 1500;
|
|
1203
|
+
function generateHotkeys(config, actions) {
|
|
1204
|
+
const hotkeys = [];
|
|
1205
|
+
const usedKeys = /* @__PURE__ */ new Set(["r", "q"]);
|
|
1206
|
+
if (config.panels.tests.enabled && config.panels.tests.interval === null && actions.tests) {
|
|
1207
|
+
const name = "tests";
|
|
1208
|
+
for (const char of name.toLowerCase()) {
|
|
1209
|
+
if (!usedKeys.has(char)) {
|
|
1210
|
+
usedKeys.add(char);
|
|
1211
|
+
hotkeys.push({
|
|
1212
|
+
key: char,
|
|
1213
|
+
label: "run tests",
|
|
1214
|
+
action: actions.tests
|
|
1215
|
+
});
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (config.customPanels && actions.customPanels) {
|
|
1221
|
+
for (const [name, panelConfig] of Object.entries(config.customPanels)) {
|
|
1222
|
+
if (panelConfig.enabled && panelConfig.interval === null && actions.customPanels[name]) {
|
|
1223
|
+
for (const char of name.toLowerCase()) {
|
|
1224
|
+
if (!usedKeys.has(char)) {
|
|
1225
|
+
usedKeys.add(char);
|
|
1226
|
+
hotkeys.push({
|
|
1227
|
+
key: char,
|
|
1228
|
+
label: `run ${name}`,
|
|
1229
|
+
action: actions.customPanels[name]
|
|
1230
|
+
});
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return hotkeys;
|
|
1238
|
+
}
|
|
1239
|
+
function formatRelativeTime2(timestamp) {
|
|
1240
|
+
const now = Date.now();
|
|
1241
|
+
const then = new Date(timestamp).getTime();
|
|
1242
|
+
const diffMs = now - then;
|
|
1243
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
1244
|
+
const diffHours = Math.floor(diffMs / 36e5);
|
|
1245
|
+
const diffDays = Math.floor(diffMs / 864e5);
|
|
1246
|
+
if (diffMins < 1) return "just now";
|
|
1247
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
1248
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
1249
|
+
return `${diffDays}d ago`;
|
|
455
1250
|
}
|
|
456
1251
|
function WelcomeApp() {
|
|
457
|
-
return /* @__PURE__ */
|
|
1252
|
+
return /* @__PURE__ */ jsx6(WelcomePanel, {});
|
|
458
1253
|
}
|
|
459
1254
|
function DashboardApp({ mode }) {
|
|
460
1255
|
const { exit } = useApp();
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
const [
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1256
|
+
const { config, warnings } = useMemo(() => parseConfig(), []);
|
|
1257
|
+
const gitIntervalSeconds = config.panels.git.interval ? config.panels.git.interval / 1e3 : null;
|
|
1258
|
+
const planIntervalSeconds = config.panels.plan.interval ? config.panels.plan.interval / 1e3 : null;
|
|
1259
|
+
const [gitData, setGitData] = useState(() => getGitData(config.panels.git));
|
|
1260
|
+
const refreshGit = useCallback(() => {
|
|
1261
|
+
setGitData(getGitData(config.panels.git));
|
|
1262
|
+
}, [config.panels.git]);
|
|
1263
|
+
const [planData, setPlanData] = useState(() => getPlanDataWithConfig(config.panels.plan));
|
|
1264
|
+
const refreshPlan = useCallback(() => {
|
|
1265
|
+
setPlanData(getPlanDataWithConfig(config.panels.plan));
|
|
1266
|
+
}, [config.panels.plan]);
|
|
1267
|
+
const getTestDataFromConfig = useCallback(() => {
|
|
1268
|
+
if (config.panels.tests.command) {
|
|
1269
|
+
return runTestCommand(config.panels.tests.command);
|
|
1270
|
+
}
|
|
1271
|
+
return getTestData();
|
|
1272
|
+
}, [config.panels.tests.command]);
|
|
1273
|
+
const [testData, setTestData] = useState(() => getTestDataFromConfig());
|
|
1274
|
+
const refreshTest = useCallback(() => {
|
|
1275
|
+
setTestData(getTestDataFromConfig());
|
|
1276
|
+
}, [getTestDataFromConfig]);
|
|
1277
|
+
const customPanelNames = useMemo(
|
|
1278
|
+
() => Object.keys(config.customPanels || {}),
|
|
1279
|
+
[config.customPanels]
|
|
1280
|
+
);
|
|
1281
|
+
const [customPanelData, setCustomPanelData] = useState(() => {
|
|
1282
|
+
const data = {};
|
|
1283
|
+
if (config.customPanels) {
|
|
1284
|
+
for (const [name, panelConfig] of Object.entries(config.customPanels)) {
|
|
1285
|
+
if (panelConfig.enabled) {
|
|
1286
|
+
data[name] = getCustomPanelData(name, panelConfig);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return data;
|
|
1291
|
+
});
|
|
1292
|
+
const refreshCustomPanel = useCallback(
|
|
1293
|
+
(name) => {
|
|
1294
|
+
if (config.customPanels && config.customPanels[name]) {
|
|
1295
|
+
setCustomPanelData((prev) => ({
|
|
1296
|
+
...prev,
|
|
1297
|
+
[name]: getCustomPanelData(name, config.customPanels[name])
|
|
1298
|
+
}));
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
[config.customPanels]
|
|
1302
|
+
);
|
|
1303
|
+
const initialCountdowns = useMemo(() => {
|
|
1304
|
+
const countdowns2 = {
|
|
1305
|
+
git: gitIntervalSeconds,
|
|
1306
|
+
plan: planIntervalSeconds
|
|
1307
|
+
};
|
|
1308
|
+
if (config.customPanels) {
|
|
1309
|
+
for (const [name, panelConfig] of Object.entries(config.customPanels)) {
|
|
1310
|
+
countdowns2[name] = panelConfig.interval ? panelConfig.interval / 1e3 : null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
return countdowns2;
|
|
1314
|
+
}, [gitIntervalSeconds, planIntervalSeconds, config.customPanels]);
|
|
1315
|
+
const [countdowns, setCountdowns] = useState(initialCountdowns);
|
|
1316
|
+
const initialVisualStates = useMemo(() => {
|
|
1317
|
+
const states = {
|
|
1318
|
+
git: { ...DEFAULT_VISUAL_STATE },
|
|
1319
|
+
plan: { ...DEFAULT_VISUAL_STATE },
|
|
1320
|
+
tests: { ...DEFAULT_VISUAL_STATE }
|
|
1321
|
+
};
|
|
1322
|
+
for (const name of customPanelNames) {
|
|
1323
|
+
states[name] = { ...DEFAULT_VISUAL_STATE };
|
|
1324
|
+
}
|
|
1325
|
+
return states;
|
|
1326
|
+
}, [customPanelNames]);
|
|
1327
|
+
const [visualStates, setVisualStates] = useState(initialVisualStates);
|
|
1328
|
+
const setVisualState = useCallback((panel, update) => {
|
|
1329
|
+
setVisualStates((prev) => ({
|
|
1330
|
+
...prev,
|
|
1331
|
+
[panel]: { ...prev[panel], ...update }
|
|
1332
|
+
}));
|
|
1333
|
+
}, []);
|
|
1334
|
+
const clearFeedback = useCallback((panel, key) => {
|
|
1335
|
+
setTimeout(() => {
|
|
1336
|
+
setVisualState(panel, { [key]: false });
|
|
1337
|
+
}, FEEDBACK_DURATION);
|
|
1338
|
+
}, [setVisualState]);
|
|
1339
|
+
const refreshGitAsync = useCallback(async () => {
|
|
1340
|
+
setVisualState("git", { isRunning: true });
|
|
1341
|
+
try {
|
|
1342
|
+
const data = await getGitDataAsync(config.panels.git);
|
|
1343
|
+
setGitData(data);
|
|
1344
|
+
} finally {
|
|
1345
|
+
setVisualState("git", { isRunning: false, justRefreshed: true });
|
|
1346
|
+
clearFeedback("git", "justRefreshed");
|
|
1347
|
+
}
|
|
1348
|
+
}, [config.panels.git, setVisualState, clearFeedback]);
|
|
1349
|
+
const refreshCustomPanelAsync = useCallback(
|
|
1350
|
+
async (name) => {
|
|
1351
|
+
if (config.customPanels && config.customPanels[name]) {
|
|
1352
|
+
setVisualState(name, { isRunning: true });
|
|
1353
|
+
try {
|
|
1354
|
+
const result = await getCustomPanelDataAsync(name, config.customPanels[name]);
|
|
1355
|
+
setCustomPanelData((prev) => ({
|
|
1356
|
+
...prev,
|
|
1357
|
+
[name]: result
|
|
1358
|
+
}));
|
|
1359
|
+
} finally {
|
|
1360
|
+
setVisualState(name, { isRunning: false, justRefreshed: true });
|
|
1361
|
+
clearFeedback(name, "justRefreshed");
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
[config.customPanels, setVisualState, clearFeedback]
|
|
1366
|
+
);
|
|
1367
|
+
const refreshTestAsync = useCallback(async () => {
|
|
1368
|
+
setVisualState("tests", { isRunning: true });
|
|
1369
|
+
try {
|
|
1370
|
+
await new Promise((resolve) => {
|
|
1371
|
+
setTimeout(() => {
|
|
1372
|
+
setTestData(getTestDataFromConfig());
|
|
1373
|
+
resolve();
|
|
1374
|
+
}, 0);
|
|
1375
|
+
});
|
|
1376
|
+
} finally {
|
|
1377
|
+
setVisualState("tests", { isRunning: false, justCompleted: true });
|
|
1378
|
+
clearFeedback("tests", "justCompleted");
|
|
1379
|
+
}
|
|
1380
|
+
}, [getTestDataFromConfig, setVisualState, clearFeedback]);
|
|
1381
|
+
const refreshPlanWithFeedback = useCallback(() => {
|
|
1382
|
+
setPlanData(getPlanDataWithConfig(config.panels.plan));
|
|
1383
|
+
setVisualState("plan", { justRefreshed: true });
|
|
1384
|
+
clearFeedback("plan", "justRefreshed");
|
|
1385
|
+
}, [config.panels.plan, setVisualState, clearFeedback]);
|
|
1386
|
+
const customPanelActionsAsync = useMemo(() => {
|
|
1387
|
+
const actions = {};
|
|
1388
|
+
for (const name of customPanelNames) {
|
|
1389
|
+
actions[name] = () => void refreshCustomPanelAsync(name);
|
|
1390
|
+
}
|
|
1391
|
+
return actions;
|
|
1392
|
+
}, [customPanelNames, refreshCustomPanelAsync]);
|
|
1393
|
+
const refreshAll = useCallback(async () => {
|
|
1394
|
+
if (config.panels.git.enabled) {
|
|
1395
|
+
void refreshGitAsync();
|
|
1396
|
+
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
1397
|
+
}
|
|
1398
|
+
if (config.panels.plan.enabled) {
|
|
1399
|
+
refreshPlanWithFeedback();
|
|
1400
|
+
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
1401
|
+
}
|
|
1402
|
+
if (config.panels.tests.enabled) {
|
|
1403
|
+
void refreshTestAsync();
|
|
1404
|
+
}
|
|
1405
|
+
for (const name of customPanelNames) {
|
|
1406
|
+
if (config.customPanels[name].enabled) {
|
|
1407
|
+
void refreshCustomPanelAsync(name);
|
|
1408
|
+
const interval = config.customPanels[name].interval;
|
|
1409
|
+
setCountdowns((prev) => ({
|
|
1410
|
+
...prev,
|
|
1411
|
+
[name]: interval ? interval / 1e3 : null
|
|
1412
|
+
}));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}, [
|
|
1416
|
+
refreshGitAsync,
|
|
1417
|
+
refreshPlanWithFeedback,
|
|
1418
|
+
refreshTestAsync,
|
|
1419
|
+
refreshCustomPanelAsync,
|
|
1420
|
+
config,
|
|
1421
|
+
gitIntervalSeconds,
|
|
1422
|
+
planIntervalSeconds,
|
|
1423
|
+
customPanelNames
|
|
1424
|
+
]);
|
|
1425
|
+
const hotkeys = useMemo(
|
|
1426
|
+
() => generateHotkeys(config, {
|
|
1427
|
+
tests: () => void refreshTestAsync(),
|
|
1428
|
+
customPanels: customPanelActionsAsync
|
|
1429
|
+
}),
|
|
1430
|
+
[config, refreshTestAsync, customPanelActionsAsync]
|
|
1431
|
+
);
|
|
471
1432
|
useEffect(() => {
|
|
472
1433
|
if (mode !== "watch") return;
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
1434
|
+
const timers = [];
|
|
1435
|
+
if (config.panels.git.enabled && config.panels.git.interval !== null) {
|
|
1436
|
+
timers.push(
|
|
1437
|
+
setInterval(() => {
|
|
1438
|
+
void refreshGitAsync();
|
|
1439
|
+
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
1440
|
+
}, config.panels.git.interval)
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
if (config.panels.plan.enabled && config.panels.plan.interval !== null) {
|
|
1444
|
+
timers.push(
|
|
1445
|
+
setInterval(() => {
|
|
1446
|
+
refreshPlanWithFeedback();
|
|
1447
|
+
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
1448
|
+
}, config.panels.plan.interval)
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (config.panels.tests.enabled && config.panels.tests.interval !== null) {
|
|
1452
|
+
timers.push(setInterval(() => void refreshTestAsync(), config.panels.tests.interval));
|
|
1453
|
+
}
|
|
1454
|
+
if (config.customPanels) {
|
|
1455
|
+
for (const [name, panelConfig] of Object.entries(config.customPanels)) {
|
|
1456
|
+
if (panelConfig.enabled && panelConfig.interval !== null) {
|
|
1457
|
+
const intervalSeconds = panelConfig.interval / 1e3;
|
|
1458
|
+
timers.push(
|
|
1459
|
+
setInterval(() => {
|
|
1460
|
+
void refreshCustomPanelAsync(name);
|
|
1461
|
+
setCountdowns((prev) => ({ ...prev, [name]: intervalSeconds }));
|
|
1462
|
+
}, panelConfig.interval)
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return () => timers.forEach((t) => clearInterval(t));
|
|
1468
|
+
}, [
|
|
1469
|
+
mode,
|
|
1470
|
+
config,
|
|
1471
|
+
refreshGitAsync,
|
|
1472
|
+
refreshPlanWithFeedback,
|
|
1473
|
+
refreshTestAsync,
|
|
1474
|
+
refreshCustomPanelAsync,
|
|
1475
|
+
gitIntervalSeconds,
|
|
1476
|
+
planIntervalSeconds
|
|
1477
|
+
]);
|
|
476
1478
|
useEffect(() => {
|
|
477
1479
|
if (mode !== "watch") return;
|
|
478
1480
|
const tick = setInterval(() => {
|
|
479
|
-
|
|
1481
|
+
setCountdowns((prev) => {
|
|
1482
|
+
const next = {
|
|
1483
|
+
git: prev.git !== null && prev.git > 1 ? prev.git - 1 : prev.git,
|
|
1484
|
+
plan: prev.plan !== null && prev.plan > 1 ? prev.plan - 1 : prev.plan
|
|
1485
|
+
};
|
|
1486
|
+
for (const name of customPanelNames) {
|
|
1487
|
+
next[name] = prev[name] !== null && prev[name] > 1 ? prev[name] - 1 : prev[name];
|
|
1488
|
+
}
|
|
1489
|
+
return next;
|
|
1490
|
+
});
|
|
480
1491
|
}, 1e3);
|
|
481
1492
|
return () => clearInterval(tick);
|
|
482
|
-
}, [mode]);
|
|
1493
|
+
}, [mode, customPanelNames]);
|
|
483
1494
|
useInput(
|
|
484
1495
|
(input) => {
|
|
485
1496
|
if (input === "q") {
|
|
@@ -488,52 +1499,112 @@ function DashboardApp({ mode }) {
|
|
|
488
1499
|
if (input === "r") {
|
|
489
1500
|
refreshAll();
|
|
490
1501
|
}
|
|
1502
|
+
for (const hotkey of hotkeys) {
|
|
1503
|
+
if (input === hotkey.key) {
|
|
1504
|
+
hotkey.action();
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
491
1508
|
},
|
|
492
1509
|
{ isActive: mode === "watch" }
|
|
493
1510
|
);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
1511
|
+
const statusBarItems = [];
|
|
1512
|
+
for (const hotkey of hotkeys) {
|
|
1513
|
+
statusBarItems.push(`${hotkey.key}: ${hotkey.label}`);
|
|
1514
|
+
}
|
|
1515
|
+
statusBarItems.push("r: refresh all");
|
|
1516
|
+
statusBarItems.push("q: quit");
|
|
1517
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1518
|
+
warnings.length > 0 && /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
|
|
1519
|
+
"\u26A0 ",
|
|
1520
|
+
warnings.join(", ")
|
|
1521
|
+
] }) }),
|
|
1522
|
+
config.panelOrder.map((panelName, index) => {
|
|
1523
|
+
const isFirst = index === 0;
|
|
1524
|
+
if (panelName === "git" && config.panels.git.enabled) {
|
|
1525
|
+
const gitVisual = visualStates.git || DEFAULT_VISUAL_STATE;
|
|
1526
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: isFirst ? 0 : 1, children: /* @__PURE__ */ jsx6(
|
|
1527
|
+
GitPanel,
|
|
1528
|
+
{
|
|
1529
|
+
branch: gitData.branch,
|
|
1530
|
+
commits: gitData.commits,
|
|
1531
|
+
stats: gitData.stats,
|
|
1532
|
+
uncommitted: gitData.uncommitted,
|
|
1533
|
+
countdown: mode === "watch" ? countdowns.git : null,
|
|
1534
|
+
width: config.width,
|
|
1535
|
+
isRunning: gitVisual.isRunning,
|
|
1536
|
+
justRefreshed: gitVisual.justRefreshed
|
|
1537
|
+
}
|
|
1538
|
+
) }, `panel-git-${index}`);
|
|
1539
|
+
}
|
|
1540
|
+
if (panelName === "plan" && config.panels.plan.enabled) {
|
|
1541
|
+
const planVisual = visualStates.plan || DEFAULT_VISUAL_STATE;
|
|
1542
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: isFirst ? 0 : 1, children: /* @__PURE__ */ jsx6(
|
|
1543
|
+
PlanPanel,
|
|
1544
|
+
{
|
|
1545
|
+
plan: planData.plan,
|
|
1546
|
+
decisions: planData.decisions,
|
|
1547
|
+
error: planData.error,
|
|
1548
|
+
countdown: mode === "watch" ? countdowns.plan : null,
|
|
1549
|
+
width: config.width,
|
|
1550
|
+
justRefreshed: planVisual.justRefreshed
|
|
1551
|
+
}
|
|
1552
|
+
) }, `panel-plan-${index}`);
|
|
1553
|
+
}
|
|
1554
|
+
if (panelName === "tests" && config.panels.tests.enabled) {
|
|
1555
|
+
const testsVisual = visualStates.tests || DEFAULT_VISUAL_STATE;
|
|
1556
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: isFirst ? 0 : 1, children: /* @__PURE__ */ jsx6(
|
|
1557
|
+
TestPanel,
|
|
1558
|
+
{
|
|
1559
|
+
results: testData.results,
|
|
1560
|
+
isOutdated: testData.isOutdated,
|
|
1561
|
+
commitsBehind: testData.commitsBehind,
|
|
1562
|
+
error: testData.error,
|
|
1563
|
+
width: config.width,
|
|
1564
|
+
isRunning: testsVisual.isRunning,
|
|
1565
|
+
justCompleted: testsVisual.justCompleted
|
|
1566
|
+
}
|
|
1567
|
+
) }, `panel-tests-${index}`);
|
|
1568
|
+
}
|
|
1569
|
+
const customConfig = config.customPanels?.[panelName];
|
|
1570
|
+
if (customConfig && customConfig.enabled) {
|
|
1571
|
+
const result = customPanelData[panelName];
|
|
1572
|
+
if (!result) return null;
|
|
1573
|
+
const customVisual = visualStates[panelName] || DEFAULT_VISUAL_STATE;
|
|
1574
|
+
const isManual = customConfig.interval === null;
|
|
1575
|
+
const relativeTime = isManual ? formatRelativeTime2(result.timestamp) : void 0;
|
|
1576
|
+
const countdown = !isManual && mode === "watch" ? countdowns[panelName] : null;
|
|
1577
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: isFirst ? 0 : 1, children: /* @__PURE__ */ jsx6(
|
|
1578
|
+
GenericPanel,
|
|
1579
|
+
{
|
|
1580
|
+
data: result.data,
|
|
1581
|
+
renderer: customConfig.renderer,
|
|
1582
|
+
countdown,
|
|
1583
|
+
relativeTime,
|
|
1584
|
+
error: result.error,
|
|
1585
|
+
width: config.width,
|
|
1586
|
+
isRunning: customVisual.isRunning,
|
|
1587
|
+
justRefreshed: customVisual.justRefreshed
|
|
1588
|
+
}
|
|
1589
|
+
) }, `panel-${panelName}-${index}`);
|
|
1590
|
+
}
|
|
1591
|
+
return null;
|
|
1592
|
+
}),
|
|
1593
|
+
mode === "watch" && /* @__PURE__ */ jsx6(Box6, { marginTop: 1, width: config.width, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: statusBarItems.map((item, index) => /* @__PURE__ */ jsxs6(React.Fragment, { children: [
|
|
1594
|
+
index > 0 && " \xB7 ",
|
|
1595
|
+
/* @__PURE__ */ jsxs6(Text6, { color: "cyan", children: [
|
|
1596
|
+
item.split(":")[0],
|
|
1597
|
+
":"
|
|
1598
|
+
] }),
|
|
1599
|
+
item.split(":").slice(1).join(":")
|
|
1600
|
+
] }, index)) }) })
|
|
530
1601
|
] });
|
|
531
1602
|
}
|
|
532
1603
|
function App({ mode, agentDirExists: agentDirExists2 = true }) {
|
|
533
1604
|
if (!agentDirExists2) {
|
|
534
|
-
return /* @__PURE__ */
|
|
1605
|
+
return /* @__PURE__ */ jsx6(WelcomeApp, {});
|
|
535
1606
|
}
|
|
536
|
-
return /* @__PURE__ */
|
|
1607
|
+
return /* @__PURE__ */ jsx6(DashboardApp, { mode });
|
|
537
1608
|
}
|
|
538
1609
|
|
|
539
1610
|
// src/cli.ts
|
|
@@ -553,17 +1624,17 @@ function parseArgs(args) {
|
|
|
553
1624
|
|
|
554
1625
|
// src/commands/init.ts
|
|
555
1626
|
import {
|
|
556
|
-
existsSync as
|
|
1627
|
+
existsSync as nodeExistsSync2,
|
|
557
1628
|
mkdirSync as nodeMkdirSync,
|
|
558
1629
|
writeFileSync as nodeWriteFileSync,
|
|
559
|
-
readFileSync as
|
|
1630
|
+
readFileSync as nodeReadFileSync5,
|
|
560
1631
|
appendFileSync as nodeAppendFileSync
|
|
561
1632
|
} from "fs";
|
|
562
|
-
var
|
|
563
|
-
existsSync:
|
|
1633
|
+
var fs2 = {
|
|
1634
|
+
existsSync: nodeExistsSync2,
|
|
564
1635
|
mkdirSync: nodeMkdirSync,
|
|
565
1636
|
writeFileSync: nodeWriteFileSync,
|
|
566
|
-
readFileSync: (path) =>
|
|
1637
|
+
readFileSync: (path) => nodeReadFileSync5(path, "utf-8"),
|
|
567
1638
|
appendFileSync: nodeAppendFileSync
|
|
568
1639
|
};
|
|
569
1640
|
var AGENT_STATE_SECTION = `## Agent State
|
|
@@ -572,48 +1643,74 @@ Maintain \`.agenthud/\` directory:
|
|
|
572
1643
|
- Update \`plan.json\` when plan changes
|
|
573
1644
|
- Append to \`decisions.json\` for key decisions
|
|
574
1645
|
`;
|
|
1646
|
+
var DEFAULT_CONFIG = `# agenthud configuration
|
|
1647
|
+
panels:
|
|
1648
|
+
git:
|
|
1649
|
+
enabled: true
|
|
1650
|
+
interval: 30s
|
|
1651
|
+
command:
|
|
1652
|
+
branch: git branch --show-current
|
|
1653
|
+
commits: git log --since=midnight --pretty=format:"%h|%aI|%s"
|
|
1654
|
+
stats: git log --since=midnight --numstat --pretty=format:""
|
|
1655
|
+
|
|
1656
|
+
plan:
|
|
1657
|
+
enabled: true
|
|
1658
|
+
interval: 10s
|
|
1659
|
+
source: .agenthud/plan.json
|
|
1660
|
+
|
|
1661
|
+
tests:
|
|
1662
|
+
enabled: true
|
|
1663
|
+
interval: manual
|
|
1664
|
+
command: npx vitest run --reporter=json
|
|
1665
|
+
`;
|
|
575
1666
|
function runInit() {
|
|
576
1667
|
const result = {
|
|
577
1668
|
created: [],
|
|
578
1669
|
skipped: []
|
|
579
1670
|
};
|
|
580
|
-
if (!
|
|
581
|
-
|
|
1671
|
+
if (!fs2.existsSync(".agenthud")) {
|
|
1672
|
+
fs2.mkdirSync(".agenthud", { recursive: true });
|
|
582
1673
|
result.created.push(".agenthud/");
|
|
583
1674
|
} else {
|
|
584
1675
|
result.skipped.push(".agenthud/");
|
|
585
1676
|
}
|
|
586
|
-
if (!
|
|
587
|
-
|
|
1677
|
+
if (!fs2.existsSync(".agenthud/plan.json")) {
|
|
1678
|
+
fs2.writeFileSync(".agenthud/plan.json", "{}\n");
|
|
588
1679
|
result.created.push(".agenthud/plan.json");
|
|
589
1680
|
} else {
|
|
590
1681
|
result.skipped.push(".agenthud/plan.json");
|
|
591
1682
|
}
|
|
592
|
-
if (!
|
|
593
|
-
|
|
1683
|
+
if (!fs2.existsSync(".agenthud/decisions.json")) {
|
|
1684
|
+
fs2.writeFileSync(".agenthud/decisions.json", "[]\n");
|
|
594
1685
|
result.created.push(".agenthud/decisions.json");
|
|
595
1686
|
} else {
|
|
596
1687
|
result.skipped.push(".agenthud/decisions.json");
|
|
597
1688
|
}
|
|
598
|
-
if (!
|
|
599
|
-
|
|
1689
|
+
if (!fs2.existsSync(".agenthud/config.yaml")) {
|
|
1690
|
+
fs2.writeFileSync(".agenthud/config.yaml", DEFAULT_CONFIG);
|
|
1691
|
+
result.created.push(".agenthud/config.yaml");
|
|
1692
|
+
} else {
|
|
1693
|
+
result.skipped.push(".agenthud/config.yaml");
|
|
1694
|
+
}
|
|
1695
|
+
if (!fs2.existsSync(".gitignore")) {
|
|
1696
|
+
fs2.writeFileSync(".gitignore", ".agenthud/\n");
|
|
600
1697
|
result.created.push(".gitignore");
|
|
601
1698
|
} else {
|
|
602
|
-
const content =
|
|
1699
|
+
const content = fs2.readFileSync(".gitignore");
|
|
603
1700
|
if (!content.includes(".agenthud/")) {
|
|
604
|
-
|
|
1701
|
+
fs2.appendFileSync(".gitignore", "\n.agenthud/\n");
|
|
605
1702
|
result.created.push(".gitignore");
|
|
606
1703
|
} else {
|
|
607
1704
|
result.skipped.push(".gitignore");
|
|
608
1705
|
}
|
|
609
1706
|
}
|
|
610
|
-
if (!
|
|
611
|
-
|
|
1707
|
+
if (!fs2.existsSync("CLAUDE.md")) {
|
|
1708
|
+
fs2.writeFileSync("CLAUDE.md", AGENT_STATE_SECTION);
|
|
612
1709
|
result.created.push("CLAUDE.md");
|
|
613
1710
|
} else {
|
|
614
|
-
const content =
|
|
1711
|
+
const content = fs2.readFileSync("CLAUDE.md");
|
|
615
1712
|
if (!content.includes("## Agent State")) {
|
|
616
|
-
|
|
1713
|
+
fs2.appendFileSync("CLAUDE.md", "\n" + AGENT_STATE_SECTION);
|
|
617
1714
|
result.created.push("CLAUDE.md");
|
|
618
1715
|
} else {
|
|
619
1716
|
result.skipped.push("CLAUDE.md");
|