agenthud 0.3.1 → 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 +864 -159
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,15 +7,21 @@ import { existsSync } from "fs";
|
|
|
7
7
|
|
|
8
8
|
// src/ui/App.tsx
|
|
9
9
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
10
|
-
import { Box as
|
|
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 =
|
|
18
|
-
var INNER_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
|
+
}
|
|
19
25
|
var BOX = {
|
|
20
26
|
tl: "\u250C",
|
|
21
27
|
tr: "\u2510",
|
|
@@ -26,18 +32,19 @@ var BOX = {
|
|
|
26
32
|
ml: "\u251C",
|
|
27
33
|
mr: "\u2524"
|
|
28
34
|
};
|
|
29
|
-
function createTitleLine(label, suffix = "") {
|
|
35
|
+
function createTitleLine(label, suffix = "", panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
30
36
|
const leftPart = BOX.h + " " + label + " ";
|
|
31
37
|
const rightPart = suffix ? " " + suffix + " " + BOX.h : "";
|
|
32
|
-
const dashCount =
|
|
38
|
+
const dashCount = panelWidth - 1 - leftPart.length - rightPart.length - 1;
|
|
33
39
|
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
34
40
|
return BOX.tl + leftPart + dashes + rightPart + BOX.tr;
|
|
35
41
|
}
|
|
36
|
-
function createBottomLine() {
|
|
37
|
-
return BOX.bl + BOX.h.repeat(
|
|
42
|
+
function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
43
|
+
return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
|
|
38
44
|
}
|
|
39
|
-
function padLine(content) {
|
|
40
|
-
const
|
|
45
|
+
function padLine(content, panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
46
|
+
const innerWidth = getInnerWidth(panelWidth);
|
|
47
|
+
const padding = innerWidth - content.length;
|
|
41
48
|
return content + " ".repeat(Math.max(0, padding));
|
|
42
49
|
}
|
|
43
50
|
var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
|
|
@@ -49,22 +56,25 @@ function truncate(text, maxLength) {
|
|
|
49
56
|
// src/ui/GitPanel.tsx
|
|
50
57
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
51
58
|
var MAX_COMMITS = 5;
|
|
52
|
-
var MAX_MESSAGE_LENGTH = CONTENT_WIDTH - 10;
|
|
53
59
|
function formatCountdown(seconds) {
|
|
54
60
|
if (seconds == null) return "";
|
|
55
|
-
|
|
61
|
+
const padded = String(seconds).padStart(2, " ");
|
|
62
|
+
return `\u21BB ${padded}s`;
|
|
56
63
|
}
|
|
57
|
-
function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
58
|
-
const countdownSuffix = formatCountdown(countdown);
|
|
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;
|
|
59
69
|
if (branch === null) {
|
|
60
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width
|
|
61
|
-
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix) }),
|
|
70
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
71
|
+
/* @__PURE__ */ jsx(Text, { children: createTitleLine("Git", countdownSuffix, width) }),
|
|
62
72
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
63
73
|
BOX.v,
|
|
64
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" Not a git repository") }),
|
|
74
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" Not a git repository", width) }),
|
|
65
75
|
BOX.v
|
|
66
76
|
] }),
|
|
67
|
-
/* @__PURE__ */ jsx(Text, { children: createBottomLine() })
|
|
77
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
68
78
|
] });
|
|
69
79
|
}
|
|
70
80
|
const displayCommits = commits.slice(0, MAX_COMMITS);
|
|
@@ -72,20 +82,23 @@ function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
|
72
82
|
const commitWord = commits.length === 1 ? "commit" : "commits";
|
|
73
83
|
const fileWord = stats.files === 1 ? "file" : "files";
|
|
74
84
|
const hasUncommitted = uncommitted > 0;
|
|
75
|
-
let
|
|
85
|
+
let statsSuffix = "";
|
|
76
86
|
if (hasCommits) {
|
|
77
|
-
|
|
87
|
+
statsSuffix = ` \xB7 +${stats.added} -${stats.deleted} \xB7 ${commits.length} ${commitWord} \xB7 ${stats.files} ${fileWord}`;
|
|
78
88
|
}
|
|
79
89
|
if (hasUncommitted) {
|
|
80
|
-
|
|
90
|
+
statsSuffix += ` \xB7 ${uncommitted} dirty`;
|
|
81
91
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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) }),
|
|
85
98
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
86
99
|
BOX.v,
|
|
87
100
|
" ",
|
|
88
|
-
/* @__PURE__ */ jsx(Text, { color: "green", children:
|
|
101
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: displayBranch }),
|
|
89
102
|
hasCommits && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
90
103
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
91
104
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
@@ -119,9 +132,9 @@ function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
|
119
132
|
BOX.v
|
|
120
133
|
] }),
|
|
121
134
|
hasCommits ? /* @__PURE__ */ jsx(Fragment, { children: displayCommits.map((commit) => {
|
|
122
|
-
const msg = truncate(commit.message,
|
|
135
|
+
const msg = truncate(commit.message, maxMessageLength);
|
|
123
136
|
const lineLength = 3 + 7 + 1 + msg.length;
|
|
124
|
-
const commitPadding = Math.max(0,
|
|
137
|
+
const commitPadding = Math.max(0, innerWidth - lineLength);
|
|
125
138
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
126
139
|
BOX.v,
|
|
127
140
|
" \u2022 ",
|
|
@@ -133,10 +146,10 @@ function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
|
133
146
|
] }, commit.hash);
|
|
134
147
|
}) }) : /* @__PURE__ */ jsxs(Text, { children: [
|
|
135
148
|
BOX.v,
|
|
136
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" No commits today") }),
|
|
149
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: padLine(" No commits today", width) }),
|
|
137
150
|
BOX.v
|
|
138
151
|
] }),
|
|
139
|
-
/* @__PURE__ */ jsx(Text, { children: createBottomLine() })
|
|
152
|
+
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
140
153
|
] });
|
|
141
154
|
}
|
|
142
155
|
|
|
@@ -144,8 +157,6 @@ function GitPanel({ branch, commits, stats, uncommitted = 0, countdown }) {
|
|
|
144
157
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
145
158
|
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
146
159
|
var PROGRESS_BAR_WIDTH = 10;
|
|
147
|
-
var MAX_STEP_LENGTH = CONTENT_WIDTH - 2;
|
|
148
|
-
var MAX_DECISION_LENGTH = CONTENT_WIDTH - 2;
|
|
149
160
|
function createProgressBar(done, total) {
|
|
150
161
|
if (total === 0) return "\u2591".repeat(PROGRESS_BAR_WIDTH);
|
|
151
162
|
const filled = Math.round(done / total * PROGRESS_BAR_WIDTH);
|
|
@@ -154,75 +165,80 @@ function createProgressBar(done, total) {
|
|
|
154
165
|
}
|
|
155
166
|
function formatCountdown2(seconds) {
|
|
156
167
|
if (seconds == null) return "";
|
|
157
|
-
|
|
168
|
+
const padded = String(seconds).padStart(2, " ");
|
|
169
|
+
return `\u21BB ${padded}s`;
|
|
158
170
|
}
|
|
159
|
-
function createPlanTitleLine(done, total, countdown) {
|
|
171
|
+
function createPlanTitleLine(done, total, countdown, panelWidth, suffixOverride) {
|
|
160
172
|
const label = " Plan ";
|
|
161
173
|
const count = ` ${done}/${total} `;
|
|
162
174
|
const bar = createProgressBar(done, total);
|
|
163
|
-
const
|
|
164
|
-
const suffix =
|
|
165
|
-
const dashCount =
|
|
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;
|
|
166
178
|
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
167
179
|
return BOX.tl + BOX.h + label + dashes + count + bar + suffix + BOX.tr;
|
|
168
180
|
}
|
|
169
|
-
function createSimpleTitleLine(countdown) {
|
|
181
|
+
function createSimpleTitleLine(countdown, panelWidth) {
|
|
170
182
|
const label = " Plan ";
|
|
171
183
|
const countdownStr = formatCountdown2(countdown);
|
|
172
184
|
const suffix = countdownStr ? ` ${countdownStr} ` + BOX.h : "";
|
|
173
|
-
const dashCount =
|
|
185
|
+
const dashCount = panelWidth - 3 - label.length - suffix.length;
|
|
174
186
|
const dashes = BOX.h.repeat(Math.max(0, dashCount));
|
|
175
187
|
return BOX.tl + BOX.h + label + dashes + suffix + BOX.tr;
|
|
176
188
|
}
|
|
177
|
-
function createDecisionsHeader() {
|
|
189
|
+
function createDecisionsHeader(panelWidth) {
|
|
178
190
|
const label = "\u2500 Decisions ";
|
|
179
|
-
const dashCount =
|
|
191
|
+
const dashCount = panelWidth - 1 - label.length - 1;
|
|
180
192
|
return label + "\u2500".repeat(dashCount) + "\u2524";
|
|
181
193
|
}
|
|
182
|
-
function PlanPanel({ plan, decisions, error, countdown }) {
|
|
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;
|
|
183
198
|
if (error || !plan || !plan.goal || !plan.steps) {
|
|
184
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width
|
|
185
|
-
/* @__PURE__ */ jsx2(Text2, { children: createSimpleTitleLine(countdown) }),
|
|
199
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width, children: [
|
|
200
|
+
/* @__PURE__ */ jsx2(Text2, { children: createSimpleTitleLine(countdown, width) }),
|
|
186
201
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
187
202
|
BOX.v,
|
|
188
|
-
padLine(" " + (error || "No plan found")),
|
|
203
|
+
padLine(" " + (error || "No plan found"), width),
|
|
189
204
|
BOX.v
|
|
190
205
|
] }),
|
|
191
|
-
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine() })
|
|
206
|
+
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
|
|
192
207
|
] });
|
|
193
208
|
}
|
|
194
209
|
const doneCount = plan.steps.filter((s) => s.status === "done").length;
|
|
195
210
|
const totalCount = plan.steps.length;
|
|
196
|
-
|
|
197
|
-
|
|
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) }),
|
|
198
214
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
199
215
|
BOX.v,
|
|
200
|
-
padLine(" " + truncate(plan.goal,
|
|
216
|
+
padLine(" " + truncate(plan.goal, contentWidth), width),
|
|
201
217
|
BOX.v
|
|
202
218
|
] }),
|
|
203
219
|
plan.steps.map((step, index) => {
|
|
204
|
-
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);
|
|
205
221
|
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
206
222
|
BOX.v,
|
|
207
|
-
padLine(stepText),
|
|
223
|
+
padLine(stepText, width),
|
|
208
224
|
BOX.v
|
|
209
|
-
] }, index);
|
|
225
|
+
] }, `step-${index}`);
|
|
210
226
|
}),
|
|
211
227
|
decisions.length > 0 && /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
212
228
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
213
229
|
"\u251C",
|
|
214
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: createDecisionsHeader() })
|
|
230
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: createDecisionsHeader(width) })
|
|
215
231
|
] }),
|
|
216
232
|
decisions.map((decision, index) => {
|
|
217
|
-
const decText = " \u2022 " + truncate(decision.decision,
|
|
233
|
+
const decText = " \u2022 " + truncate(decision.decision, maxDecisionLength);
|
|
218
234
|
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
219
235
|
BOX.v,
|
|
220
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(decText) }),
|
|
236
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: padLine(decText, width) }),
|
|
221
237
|
BOX.v
|
|
222
|
-
] }, index);
|
|
238
|
+
] }, `decision-${index}`);
|
|
223
239
|
})
|
|
224
240
|
] }),
|
|
225
|
-
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine() })
|
|
241
|
+
/* @__PURE__ */ jsx2(Text2, { children: createBottomLine(width) })
|
|
226
242
|
] });
|
|
227
243
|
}
|
|
228
244
|
|
|
@@ -241,28 +257,40 @@ function formatRelativeTime(timestamp) {
|
|
|
241
257
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
242
258
|
return `${diffDays}d ago`;
|
|
243
259
|
}
|
|
244
|
-
function createSeparator() {
|
|
245
|
-
return BOX.ml + BOX.h.repeat(
|
|
260
|
+
function createSeparator(panelWidth) {
|
|
261
|
+
return BOX.ml + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.mr;
|
|
246
262
|
}
|
|
247
263
|
function TestPanel({
|
|
248
264
|
results,
|
|
249
265
|
isOutdated,
|
|
250
266
|
commitsBehind,
|
|
251
|
-
error
|
|
267
|
+
error,
|
|
268
|
+
width = DEFAULT_PANEL_WIDTH,
|
|
269
|
+
isRunning = false,
|
|
270
|
+
justCompleted = false
|
|
252
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();
|
|
253
281
|
if (error || !results) {
|
|
254
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width
|
|
255
|
-
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests",
|
|
282
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
|
|
283
|
+
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", titleSuffix, width) }),
|
|
256
284
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
257
285
|
BOX.v,
|
|
258
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" " + (error || "No test results")) }),
|
|
286
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: padLine(" " + (error || "No test results"), width) }),
|
|
259
287
|
BOX.v
|
|
260
288
|
] }),
|
|
261
|
-
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine() })
|
|
289
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
|
|
262
290
|
] });
|
|
263
291
|
}
|
|
264
292
|
const hasFailures = results.failures.length > 0;
|
|
265
|
-
const relativeTime =
|
|
293
|
+
const relativeTime = titleSuffix;
|
|
266
294
|
let summaryLength = 1 + 2 + String(results.passed).length + " passed".length;
|
|
267
295
|
if (results.failed > 0) {
|
|
268
296
|
summaryLength += 2 + 2 + String(results.failed).length + " failed".length;
|
|
@@ -271,12 +299,12 @@ function TestPanel({
|
|
|
271
299
|
summaryLength += 2 + 2 + String(results.skipped).length + " skipped".length;
|
|
272
300
|
}
|
|
273
301
|
summaryLength += " \xB7 ".length + results.hash.length;
|
|
274
|
-
const summaryPadding = Math.max(0,
|
|
275
|
-
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width
|
|
276
|
-
/* @__PURE__ */ jsx3(Text3, { children: createTitleLine("Tests", relativeTime) }),
|
|
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) }),
|
|
277
305
|
isOutdated && /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
278
306
|
BOX.v,
|
|
279
|
-
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: padLine(` \u26A0 Outdated (${commitsBehind} ${commitsBehind === 1 ? "commit" : "commits"} behind)
|
|
307
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: padLine(` \u26A0 Outdated (${commitsBehind} ${commitsBehind === 1 ? "commit" : "commits"} behind)`, width) }),
|
|
280
308
|
BOX.v
|
|
281
309
|
] }),
|
|
282
310
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
@@ -311,12 +339,12 @@ function TestPanel({
|
|
|
311
339
|
BOX.v
|
|
312
340
|
] }),
|
|
313
341
|
hasFailures && /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
314
|
-
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: createSeparator() }),
|
|
342
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: createSeparator(width) }),
|
|
315
343
|
results.failures.map((failure, index) => {
|
|
316
|
-
const fileName = truncate(failure.file,
|
|
317
|
-
const filePadding = Math.max(0,
|
|
318
|
-
const testName = truncate(failure.name,
|
|
319
|
-
const testPadding = Math.max(0,
|
|
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);
|
|
320
348
|
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
321
349
|
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
322
350
|
BOX.v,
|
|
@@ -336,33 +364,203 @@ function TestPanel({
|
|
|
336
364
|
" ".repeat(testPadding),
|
|
337
365
|
BOX.v
|
|
338
366
|
] })
|
|
339
|
-
] }, index);
|
|
367
|
+
] }, `failure-${index}`);
|
|
340
368
|
})
|
|
341
369
|
] }),
|
|
342
|
-
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine() })
|
|
370
|
+
/* @__PURE__ */ jsx3(Text3, { children: createBottomLine(width) })
|
|
343
371
|
] });
|
|
344
372
|
}
|
|
345
373
|
|
|
346
|
-
// src/ui/
|
|
374
|
+
// src/ui/GenericPanel.tsx
|
|
347
375
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
348
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
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: [
|
|
481
|
+
"\u2717 ",
|
|
482
|
+
stats.failed,
|
|
483
|
+
" failed"
|
|
484
|
+
] })
|
|
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) })
|
|
539
|
+
] });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/ui/WelcomePanel.tsx
|
|
543
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
544
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
349
545
|
function WelcomePanel() {
|
|
350
|
-
return /* @__PURE__ */
|
|
351
|
-
/* @__PURE__ */
|
|
352
|
-
/* @__PURE__ */
|
|
353
|
-
/* @__PURE__ */
|
|
354
|
-
/* @__PURE__ */
|
|
355
|
-
/* @__PURE__ */
|
|
356
|
-
/* @__PURE__ */
|
|
357
|
-
/* @__PURE__ */
|
|
358
|
-
/* @__PURE__ */
|
|
359
|
-
/* @__PURE__ */
|
|
360
|
-
/* @__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" })
|
|
361
557
|
] });
|
|
362
558
|
}
|
|
363
559
|
|
|
364
560
|
// src/data/git.ts
|
|
365
|
-
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);
|
|
366
564
|
var execFn = (command, options2) => nodeExecSync(command, options2);
|
|
367
565
|
function getUncommittedCount() {
|
|
368
566
|
try {
|
|
@@ -432,6 +630,58 @@ function getGitData(config) {
|
|
|
432
630
|
const uncommitted = getUncommittedCount();
|
|
433
631
|
return { branch, commits, stats, uncommitted };
|
|
434
632
|
}
|
|
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;
|
|
640
|
+
try {
|
|
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
|
+
};
|
|
657
|
+
});
|
|
658
|
+
} catch {
|
|
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 };
|
|
681
|
+
}
|
|
682
|
+
const uncommitted = getUncommittedCount();
|
|
683
|
+
return { branch, commits, stats, uncommitted };
|
|
684
|
+
}
|
|
435
685
|
|
|
436
686
|
// src/data/plan.ts
|
|
437
687
|
import { readFileSync as nodeReadFileSync } from "fs";
|
|
@@ -512,9 +762,171 @@ function getTestData(dir = process.cwd()) {
|
|
|
512
762
|
return { results, isOutdated, commitsBehind, error };
|
|
513
763
|
}
|
|
514
764
|
|
|
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
|
+
|
|
515
927
|
// src/runner/command.ts
|
|
516
|
-
import { execSync as
|
|
517
|
-
var
|
|
928
|
+
import { execSync as nodeExecSync4 } from "child_process";
|
|
929
|
+
var execFn3 = (command, options2) => nodeExecSync4(command, options2);
|
|
518
930
|
function parseVitestOutput(output) {
|
|
519
931
|
try {
|
|
520
932
|
const data = JSON.parse(output);
|
|
@@ -544,7 +956,7 @@ function parseVitestOutput(output) {
|
|
|
544
956
|
}
|
|
545
957
|
function getHeadHash() {
|
|
546
958
|
try {
|
|
547
|
-
return
|
|
959
|
+
return execFn3("git rev-parse --short HEAD", {
|
|
548
960
|
encoding: "utf-8",
|
|
549
961
|
stdio: ["pipe", "pipe", "pipe"]
|
|
550
962
|
}).trim();
|
|
@@ -555,7 +967,7 @@ function getHeadHash() {
|
|
|
555
967
|
function runTestCommand(command) {
|
|
556
968
|
let output;
|
|
557
969
|
try {
|
|
558
|
-
output =
|
|
970
|
+
output = execFn3(command, {
|
|
559
971
|
encoding: "utf-8",
|
|
560
972
|
stdio: ["pipe", "pipe", "pipe"]
|
|
561
973
|
});
|
|
@@ -597,13 +1009,16 @@ function runTestCommand(command) {
|
|
|
597
1009
|
// src/config/parser.ts
|
|
598
1010
|
import {
|
|
599
1011
|
existsSync as nodeExistsSync,
|
|
600
|
-
readFileSync as
|
|
1012
|
+
readFileSync as nodeReadFileSync4
|
|
601
1013
|
} from "fs";
|
|
602
1014
|
import { parse as parseYaml } from "yaml";
|
|
603
1015
|
var fs = {
|
|
604
1016
|
existsSync: nodeExistsSync,
|
|
605
|
-
readFileSync: (path) =>
|
|
1017
|
+
readFileSync: (path) => nodeReadFileSync4(path, "utf-8")
|
|
606
1018
|
};
|
|
1019
|
+
var DEFAULT_WIDTH = 70;
|
|
1020
|
+
var MIN_WIDTH = 50;
|
|
1021
|
+
var MAX_WIDTH = 120;
|
|
607
1022
|
var CONFIG_PATH = ".agenthud/config.yaml";
|
|
608
1023
|
function parseInterval(interval) {
|
|
609
1024
|
if (!interval || interval === "manual") {
|
|
@@ -641,10 +1056,13 @@ function getDefaultConfig() {
|
|
|
641
1056
|
interval: null
|
|
642
1057
|
// manual
|
|
643
1058
|
}
|
|
644
|
-
}
|
|
1059
|
+
},
|
|
1060
|
+
panelOrder: ["git", "plan", "tests"],
|
|
1061
|
+
width: DEFAULT_WIDTH
|
|
645
1062
|
};
|
|
646
1063
|
}
|
|
647
|
-
var
|
|
1064
|
+
var BUILTIN_PANELS = ["git", "plan", "tests"];
|
|
1065
|
+
var VALID_RENDERERS = ["list", "progress", "status"];
|
|
648
1066
|
function parseConfig() {
|
|
649
1067
|
const warnings = [];
|
|
650
1068
|
const defaultConfig = getDefaultConfig();
|
|
@@ -664,16 +1082,26 @@ function parseConfig() {
|
|
|
664
1082
|
return { config: defaultConfig, warnings };
|
|
665
1083
|
}
|
|
666
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
|
+
}
|
|
667
1097
|
const panels = parsed.panels;
|
|
668
1098
|
if (!panels || typeof panels !== "object") {
|
|
669
|
-
return { config
|
|
1099
|
+
return { config, warnings };
|
|
670
1100
|
}
|
|
671
|
-
const
|
|
1101
|
+
const customPanels = {};
|
|
1102
|
+
const panelOrder = [];
|
|
672
1103
|
for (const panelName of Object.keys(panels)) {
|
|
673
|
-
|
|
674
|
-
warnings.push(`Unknown panel '${panelName}' in config`);
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
1104
|
+
panelOrder.push(panelName);
|
|
677
1105
|
const panelConfig = panels[panelName];
|
|
678
1106
|
if (!panelConfig || typeof panelConfig !== "object") {
|
|
679
1107
|
continue;
|
|
@@ -690,6 +1118,7 @@ function parseConfig() {
|
|
|
690
1118
|
config.panels.git.interval = interval;
|
|
691
1119
|
}
|
|
692
1120
|
}
|
|
1121
|
+
continue;
|
|
693
1122
|
}
|
|
694
1123
|
if (panelName === "plan") {
|
|
695
1124
|
if (typeof panelConfig.enabled === "boolean") {
|
|
@@ -706,6 +1135,7 @@ function parseConfig() {
|
|
|
706
1135
|
if (typeof panelConfig.source === "string") {
|
|
707
1136
|
config.panels.plan.source = panelConfig.source;
|
|
708
1137
|
}
|
|
1138
|
+
continue;
|
|
709
1139
|
}
|
|
710
1140
|
if (panelName === "tests") {
|
|
711
1141
|
if (typeof panelConfig.enabled === "boolean") {
|
|
@@ -722,16 +1152,57 @@ function parseConfig() {
|
|
|
722
1152
|
if (typeof panelConfig.command === "string") {
|
|
723
1153
|
config.panels.tests.command = panelConfig.command;
|
|
724
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);
|
|
725
1189
|
}
|
|
726
1190
|
}
|
|
1191
|
+
config.panelOrder = panelOrder;
|
|
727
1192
|
return { config, warnings };
|
|
728
1193
|
}
|
|
729
1194
|
|
|
730
1195
|
// src/ui/App.tsx
|
|
731
|
-
import { jsx as
|
|
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;
|
|
732
1203
|
function generateHotkeys(config, actions) {
|
|
733
1204
|
const hotkeys = [];
|
|
734
|
-
const usedKeys = /* @__PURE__ */ new Set();
|
|
1205
|
+
const usedKeys = /* @__PURE__ */ new Set(["r", "q"]);
|
|
735
1206
|
if (config.panels.tests.enabled && config.panels.tests.interval === null && actions.tests) {
|
|
736
1207
|
const name = "tests";
|
|
737
1208
|
for (const char of name.toLowerCase()) {
|
|
@@ -746,10 +1217,39 @@ function generateHotkeys(config, actions) {
|
|
|
746
1217
|
}
|
|
747
1218
|
}
|
|
748
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
|
+
}
|
|
749
1237
|
return hotkeys;
|
|
750
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`;
|
|
1250
|
+
}
|
|
751
1251
|
function WelcomeApp() {
|
|
752
|
-
return /* @__PURE__ */
|
|
1252
|
+
return /* @__PURE__ */ jsx6(WelcomePanel, {});
|
|
753
1253
|
}
|
|
754
1254
|
function DashboardApp({ mode }) {
|
|
755
1255
|
const { exit } = useApp();
|
|
@@ -774,26 +1274,160 @@ function DashboardApp({ mode }) {
|
|
|
774
1274
|
const refreshTest = useCallback(() => {
|
|
775
1275
|
setTestData(getTestDataFromConfig());
|
|
776
1276
|
}, [getTestDataFromConfig]);
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
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;
|
|
780
1291
|
});
|
|
781
|
-
const
|
|
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 () => {
|
|
782
1394
|
if (config.panels.git.enabled) {
|
|
783
|
-
|
|
1395
|
+
void refreshGitAsync();
|
|
784
1396
|
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
785
1397
|
}
|
|
786
1398
|
if (config.panels.plan.enabled) {
|
|
787
|
-
|
|
1399
|
+
refreshPlanWithFeedback();
|
|
788
1400
|
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
789
1401
|
}
|
|
790
1402
|
if (config.panels.tests.enabled) {
|
|
791
|
-
|
|
1403
|
+
void refreshTestAsync();
|
|
792
1404
|
}
|
|
793
|
-
|
|
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
|
+
]);
|
|
794
1425
|
const hotkeys = useMemo(
|
|
795
|
-
() => generateHotkeys(config, {
|
|
796
|
-
|
|
1426
|
+
() => generateHotkeys(config, {
|
|
1427
|
+
tests: () => void refreshTestAsync(),
|
|
1428
|
+
customPanels: customPanelActionsAsync
|
|
1429
|
+
}),
|
|
1430
|
+
[config, refreshTestAsync, customPanelActionsAsync]
|
|
797
1431
|
);
|
|
798
1432
|
useEffect(() => {
|
|
799
1433
|
if (mode !== "watch") return;
|
|
@@ -801,7 +1435,7 @@ function DashboardApp({ mode }) {
|
|
|
801
1435
|
if (config.panels.git.enabled && config.panels.git.interval !== null) {
|
|
802
1436
|
timers.push(
|
|
803
1437
|
setInterval(() => {
|
|
804
|
-
|
|
1438
|
+
void refreshGitAsync();
|
|
805
1439
|
setCountdowns((prev) => ({ ...prev, git: gitIntervalSeconds }));
|
|
806
1440
|
}, config.panels.git.interval)
|
|
807
1441
|
);
|
|
@@ -809,26 +1443,54 @@ function DashboardApp({ mode }) {
|
|
|
809
1443
|
if (config.panels.plan.enabled && config.panels.plan.interval !== null) {
|
|
810
1444
|
timers.push(
|
|
811
1445
|
setInterval(() => {
|
|
812
|
-
|
|
1446
|
+
refreshPlanWithFeedback();
|
|
813
1447
|
setCountdowns((prev) => ({ ...prev, plan: planIntervalSeconds }));
|
|
814
1448
|
}, config.panels.plan.interval)
|
|
815
1449
|
);
|
|
816
1450
|
}
|
|
817
1451
|
if (config.panels.tests.enabled && config.panels.tests.interval !== null) {
|
|
818
|
-
timers.push(setInterval(
|
|
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
|
+
}
|
|
819
1466
|
}
|
|
820
1467
|
return () => timers.forEach((t) => clearInterval(t));
|
|
821
|
-
}, [
|
|
1468
|
+
}, [
|
|
1469
|
+
mode,
|
|
1470
|
+
config,
|
|
1471
|
+
refreshGitAsync,
|
|
1472
|
+
refreshPlanWithFeedback,
|
|
1473
|
+
refreshTestAsync,
|
|
1474
|
+
refreshCustomPanelAsync,
|
|
1475
|
+
gitIntervalSeconds,
|
|
1476
|
+
planIntervalSeconds
|
|
1477
|
+
]);
|
|
822
1478
|
useEffect(() => {
|
|
823
1479
|
if (mode !== "watch") return;
|
|
824
1480
|
const tick = setInterval(() => {
|
|
825
|
-
setCountdowns((prev) =>
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
+
});
|
|
829
1491
|
}, 1e3);
|
|
830
1492
|
return () => clearInterval(tick);
|
|
831
|
-
}, [mode]);
|
|
1493
|
+
}, [mode, customPanelNames]);
|
|
832
1494
|
useInput(
|
|
833
1495
|
(input) => {
|
|
834
1496
|
if (input === "q") {
|
|
@@ -850,44 +1512,87 @@ function DashboardApp({ mode }) {
|
|
|
850
1512
|
for (const hotkey of hotkeys) {
|
|
851
1513
|
statusBarItems.push(`${hotkey.key}: ${hotkey.label}`);
|
|
852
1514
|
}
|
|
853
|
-
statusBarItems.push("r: refresh");
|
|
1515
|
+
statusBarItems.push("r: refresh all");
|
|
854
1516
|
statusBarItems.push("q: quit");
|
|
855
|
-
return /* @__PURE__ */
|
|
856
|
-
warnings.length > 0 && /* @__PURE__ */
|
|
1517
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
1518
|
+
warnings.length > 0 && /* @__PURE__ */ jsx6(Box6, { marginBottom: 1, children: /* @__PURE__ */ jsxs6(Text6, { color: "yellow", children: [
|
|
857
1519
|
"\u26A0 ",
|
|
858
1520
|
warnings.join(", ")
|
|
859
1521
|
] }) }),
|
|
860
|
-
config.
|
|
861
|
-
|
|
862
|
-
{
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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}`);
|
|
868
1539
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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}`);
|
|
877
1553
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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}`);
|
|
886
1568
|
}
|
|
887
|
-
|
|
888
|
-
|
|
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: [
|
|
889
1594
|
index > 0 && " \xB7 ",
|
|
890
|
-
/* @__PURE__ */
|
|
1595
|
+
/* @__PURE__ */ jsxs6(Text6, { color: "cyan", children: [
|
|
891
1596
|
item.split(":")[0],
|
|
892
1597
|
":"
|
|
893
1598
|
] }),
|
|
@@ -897,9 +1602,9 @@ function DashboardApp({ mode }) {
|
|
|
897
1602
|
}
|
|
898
1603
|
function App({ mode, agentDirExists: agentDirExists2 = true }) {
|
|
899
1604
|
if (!agentDirExists2) {
|
|
900
|
-
return /* @__PURE__ */
|
|
1605
|
+
return /* @__PURE__ */ jsx6(WelcomeApp, {});
|
|
901
1606
|
}
|
|
902
|
-
return /* @__PURE__ */
|
|
1607
|
+
return /* @__PURE__ */ jsx6(DashboardApp, { mode });
|
|
903
1608
|
}
|
|
904
1609
|
|
|
905
1610
|
// src/cli.ts
|
|
@@ -922,14 +1627,14 @@ import {
|
|
|
922
1627
|
existsSync as nodeExistsSync2,
|
|
923
1628
|
mkdirSync as nodeMkdirSync,
|
|
924
1629
|
writeFileSync as nodeWriteFileSync,
|
|
925
|
-
readFileSync as
|
|
1630
|
+
readFileSync as nodeReadFileSync5,
|
|
926
1631
|
appendFileSync as nodeAppendFileSync
|
|
927
1632
|
} from "fs";
|
|
928
1633
|
var fs2 = {
|
|
929
1634
|
existsSync: nodeExistsSync2,
|
|
930
1635
|
mkdirSync: nodeMkdirSync,
|
|
931
1636
|
writeFileSync: nodeWriteFileSync,
|
|
932
|
-
readFileSync: (path) =>
|
|
1637
|
+
readFileSync: (path) => nodeReadFileSync5(path, "utf-8"),
|
|
933
1638
|
appendFileSync: nodeAppendFileSync
|
|
934
1639
|
};
|
|
935
1640
|
var AGENT_STATE_SECTION = `## Agent State
|