acp-visualizer-tui 0.1.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/cli.js ADDED
@@ -0,0 +1,1505 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+ import meow from "meow";
6
+ import fs4 from "fs";
7
+ import path2 from "path";
8
+
9
+ // src/lib/yaml-loader.ts
10
+ import yaml from "js-yaml";
11
+ var TASK_ALIASES = {
12
+ est_hours: "estimated_hours",
13
+ hours: "estimated_hours",
14
+ estimate: "estimated_hours",
15
+ completed: "completed_date",
16
+ done_date: "completed_date",
17
+ filename: "file",
18
+ path: "file"
19
+ };
20
+ function extractKnown(obj, knownKeys) {
21
+ if (!obj || typeof obj !== "object") return { known: {}, extra: {} };
22
+ const known = {};
23
+ const extra = {};
24
+ for (const [key, value] of Object.entries(obj)) {
25
+ const resolved = TASK_ALIASES[key] ?? key;
26
+ if (knownKeys.includes(resolved)) {
27
+ known[resolved] = value;
28
+ } else {
29
+ extra[key] = value;
30
+ }
31
+ }
32
+ return { known, extra };
33
+ }
34
+ function normalizeStatus(value) {
35
+ const s = String(value || "not_started").toLowerCase().replace(/[\s-]/g, "_");
36
+ if (s === "completed" || s === "done" || s === "complete") return "completed";
37
+ if (s === "in_progress" || s === "active" || s === "wip") return "in_progress";
38
+ return "not_started";
39
+ }
40
+ function normalizeStringArray(value) {
41
+ if (!value) return [];
42
+ if (typeof value === "string") return [value];
43
+ if (!Array.isArray(value)) return [];
44
+ return value.map(String);
45
+ }
46
+ function safeString(value, fallback = "") {
47
+ if (value == null) return fallback;
48
+ return String(value);
49
+ }
50
+ function safeNumber(value, fallback = 0) {
51
+ const n = Number(value);
52
+ return Number.isFinite(n) ? n : fallback;
53
+ }
54
+ function normalizeProject(raw) {
55
+ const obj = raw && typeof raw === "object" ? raw : {};
56
+ const { known, extra } = extractKnown(obj, [
57
+ "name",
58
+ "version",
59
+ "started",
60
+ "status",
61
+ "current_milestone",
62
+ "description"
63
+ ]);
64
+ return {
65
+ name: safeString(known.name, "Unknown Project"),
66
+ version: safeString(known.version, "0.0.0"),
67
+ started: safeString(known.started),
68
+ status: normalizeStatus(known.status),
69
+ current_milestone: known.current_milestone ? safeString(known.current_milestone) : void 0,
70
+ description: safeString(known.description),
71
+ extra
72
+ };
73
+ }
74
+ function normalizeMilestone(raw) {
75
+ const obj = raw && typeof raw === "object" ? raw : {};
76
+ const { known, extra } = extractKnown(obj, [
77
+ "id",
78
+ "name",
79
+ "status",
80
+ "progress",
81
+ "started",
82
+ "completed",
83
+ "estimated_weeks",
84
+ "tasks_completed",
85
+ "tasks_total",
86
+ "notes"
87
+ ]);
88
+ return {
89
+ id: safeString(known.id),
90
+ name: safeString(known.name),
91
+ status: normalizeStatus(known.status),
92
+ progress: safeNumber(known.progress),
93
+ started: known.started ? safeString(known.started) : null,
94
+ completed: known.completed ? safeString(known.completed) : null,
95
+ estimated_weeks: safeString(known.estimated_weeks),
96
+ tasks_completed: safeNumber(known.tasks_completed),
97
+ tasks_total: safeNumber(known.tasks_total),
98
+ notes: safeString(known.notes),
99
+ extra
100
+ };
101
+ }
102
+ function normalizeMilestones(raw) {
103
+ if (!Array.isArray(raw)) return [];
104
+ return raw.map(normalizeMilestone);
105
+ }
106
+ function normalizeTask(raw, milestoneId) {
107
+ const obj = raw && typeof raw === "object" ? raw : {};
108
+ const { known, extra } = extractKnown(obj, [
109
+ "id",
110
+ "name",
111
+ "status",
112
+ "file",
113
+ "estimated_hours",
114
+ "completed_date",
115
+ "notes"
116
+ ]);
117
+ return {
118
+ id: safeString(known.id),
119
+ name: safeString(known.name),
120
+ status: normalizeStatus(known.status),
121
+ milestone_id: milestoneId,
122
+ file: safeString(known.file),
123
+ estimated_hours: safeString(known.estimated_hours),
124
+ completed_date: known.completed_date ? safeString(known.completed_date) : null,
125
+ notes: safeString(known.notes),
126
+ extra
127
+ };
128
+ }
129
+ function normalizeTasks(raw) {
130
+ if (!raw || typeof raw !== "object") return {};
131
+ const result = {};
132
+ for (const [milestoneId, tasks] of Object.entries(raw)) {
133
+ if (Array.isArray(tasks)) {
134
+ result[milestoneId] = tasks.map((t) => normalizeTask(t, milestoneId));
135
+ }
136
+ }
137
+ return result;
138
+ }
139
+ function normalizeWorkEntry(raw) {
140
+ const obj = raw && typeof raw === "object" ? raw : {};
141
+ const { known, extra } = extractKnown(obj, ["date", "description", "items"]);
142
+ return {
143
+ date: safeString(known.date),
144
+ description: safeString(known.description),
145
+ items: normalizeStringArray(known.items),
146
+ extra
147
+ };
148
+ }
149
+ function normalizeWorkEntries(raw) {
150
+ if (!Array.isArray(raw)) return [];
151
+ return raw.map(normalizeWorkEntry);
152
+ }
153
+ function normalizeDocStats(raw) {
154
+ const obj = raw && typeof raw === "object" ? raw : {};
155
+ return {
156
+ design_documents: safeNumber(obj?.design_documents),
157
+ milestone_documents: safeNumber(obj?.milestone_documents),
158
+ pattern_documents: safeNumber(obj?.pattern_documents),
159
+ task_documents: safeNumber(obj?.task_documents)
160
+ };
161
+ }
162
+ function normalizeProgress(raw) {
163
+ const obj = raw && typeof raw === "object" ? raw : {};
164
+ return {
165
+ planning: safeNumber(obj?.planning),
166
+ implementation: safeNumber(obj?.implementation),
167
+ overall: safeNumber(obj?.overall)
168
+ };
169
+ }
170
+ function parseProgressYaml(raw) {
171
+ try {
172
+ const doc = yaml.load(raw);
173
+ if (!doc || typeof doc !== "object") {
174
+ return emptyProgressData();
175
+ }
176
+ const milestones = normalizeMilestones(doc.milestones);
177
+ const tasks = normalizeTasks(doc.tasks);
178
+ for (const milestone of milestones) {
179
+ const milestoneTasks = tasks[milestone.id];
180
+ if (milestoneTasks && milestoneTasks.length > 0) {
181
+ const completed = milestoneTasks.filter((t) => t.status === "completed").length;
182
+ const total = milestoneTasks.length;
183
+ if (milestone.tasks_total === 0) milestone.tasks_total = total;
184
+ if (milestone.tasks_completed === 0) milestone.tasks_completed = completed;
185
+ if (milestone.progress === 0 && total > 0) {
186
+ milestone.progress = Math.round(completed / total * 100);
187
+ }
188
+ }
189
+ }
190
+ return {
191
+ project: normalizeProject(doc.project),
192
+ milestones,
193
+ tasks,
194
+ recent_work: normalizeWorkEntries(doc.recent_work),
195
+ next_steps: normalizeStringArray(doc.next_steps),
196
+ notes: normalizeStringArray(doc.notes),
197
+ current_blockers: normalizeStringArray(doc.current_blockers),
198
+ documentation: normalizeDocStats(doc.documentation),
199
+ progress: normalizeProgress(doc.progress)
200
+ };
201
+ } catch {
202
+ return emptyProgressData();
203
+ }
204
+ }
205
+ function emptyProgressData() {
206
+ return {
207
+ project: {
208
+ name: "Unknown",
209
+ version: "0.0.0",
210
+ started: "",
211
+ status: "not_started",
212
+ description: "",
213
+ extra: {}
214
+ },
215
+ milestones: [],
216
+ tasks: {},
217
+ recent_work: [],
218
+ next_steps: [],
219
+ notes: [],
220
+ current_blockers: [],
221
+ documentation: { design_documents: 0, milestone_documents: 0, pattern_documents: 0, task_documents: 0 },
222
+ progress: { planning: 0, implementation: 0, overall: 0 }
223
+ };
224
+ }
225
+
226
+ // src/app.tsx
227
+ import { useState as useState8, useCallback as useCallback5 } from "react";
228
+ import { Box as Box12, Text as Text15, useApp, useInput as useInput6 } from "ink";
229
+
230
+ // src/hooks/useProgressData.ts
231
+ import { useState, useCallback } from "react";
232
+ import fs from "fs";
233
+ function useProgressData(filePath2) {
234
+ const [state, setState] = useState(() => {
235
+ try {
236
+ const raw = fs.readFileSync(filePath2, "utf-8");
237
+ return { data: parseProgressYaml(raw), error: null, loading: false };
238
+ } catch (err) {
239
+ const msg = err instanceof Error ? err.message : String(err);
240
+ return { data: null, error: msg, loading: false };
241
+ }
242
+ });
243
+ const reload = useCallback(() => {
244
+ try {
245
+ const raw = fs.readFileSync(filePath2, "utf-8");
246
+ const data = parseProgressYaml(raw);
247
+ setState({ data, error: null, loading: false });
248
+ } catch (err) {
249
+ const msg = err instanceof Error ? err.message : String(err);
250
+ setState((prev) => ({ data: prev.data, error: msg, loading: false }));
251
+ }
252
+ }, [filePath2]);
253
+ return { ...state, reload };
254
+ }
255
+
256
+ // src/hooks/useWatchMode.ts
257
+ import { useEffect, useRef } from "react";
258
+ import fs2 from "fs";
259
+ function useWatchMode(filePath2, enabled, onReload) {
260
+ const timerRef = useRef(null);
261
+ useEffect(() => {
262
+ if (!enabled) return;
263
+ let watcher = null;
264
+ try {
265
+ watcher = fs2.watch(filePath2, () => {
266
+ if (timerRef.current) clearTimeout(timerRef.current);
267
+ timerRef.current = setTimeout(() => {
268
+ onReload();
269
+ }, 300);
270
+ });
271
+ watcher.on("error", () => {
272
+ });
273
+ } catch {
274
+ }
275
+ return () => {
276
+ if (timerRef.current) clearTimeout(timerRef.current);
277
+ if (watcher) watcher.close();
278
+ };
279
+ }, [filePath2, enabled, onReload]);
280
+ }
281
+
282
+ // src/hooks/useNavigation.ts
283
+ import { useState as useState2, useCallback as useCallback2 } from "react";
284
+ var VIEW_ORDER = ["dashboard", "milestones", "tasks", "activity", "blockers"];
285
+ var VIEW_LABELS = {
286
+ dashboard: "Dashboard",
287
+ milestones: "Milestones",
288
+ tasks: "Tasks",
289
+ activity: "Activity",
290
+ blockers: "Blockers"
291
+ };
292
+ function useNavigation(initialView) {
293
+ const initial = VIEW_ORDER.includes(initialView) ? initialView : "dashboard";
294
+ const [currentView, setCurrentView] = useState2(initial);
295
+ const [viewStack, setViewStack] = useState2([]);
296
+ const nextView = useCallback2(() => {
297
+ setCurrentView((prev) => {
298
+ const idx = VIEW_ORDER.indexOf(prev);
299
+ return VIEW_ORDER[(idx + 1) % VIEW_ORDER.length];
300
+ });
301
+ }, []);
302
+ const prevView = useCallback2(() => {
303
+ setCurrentView((prev) => {
304
+ const idx = VIEW_ORDER.indexOf(prev);
305
+ return VIEW_ORDER[(idx - 1 + VIEW_ORDER.length) % VIEW_ORDER.length];
306
+ });
307
+ }, []);
308
+ const goToView = useCallback2((view) => {
309
+ setCurrentView(view);
310
+ }, []);
311
+ const pushView = useCallback2((view) => {
312
+ setCurrentView((prev) => {
313
+ setViewStack((stack) => [...stack, prev]);
314
+ return view;
315
+ });
316
+ }, []);
317
+ const popView = useCallback2(() => {
318
+ setViewStack((stack) => {
319
+ if (stack.length === 0) return stack;
320
+ const newStack = [...stack];
321
+ const prev = newStack.pop();
322
+ setCurrentView(prev);
323
+ return newStack;
324
+ });
325
+ }, []);
326
+ return {
327
+ currentView,
328
+ viewStack,
329
+ views: VIEW_ORDER,
330
+ labels: VIEW_LABELS,
331
+ nextView,
332
+ prevView,
333
+ goToView,
334
+ pushView,
335
+ popView
336
+ };
337
+ }
338
+
339
+ // src/hooks/useFilter.ts
340
+ import { useState as useState3, useCallback as useCallback3 } from "react";
341
+ var FILTER_ORDER = ["all", "in_progress", "completed", "not_started"];
342
+ var FILTER_LABELS = {
343
+ all: "All",
344
+ in_progress: "In Progress",
345
+ completed: "Completed",
346
+ not_started: "Not Started"
347
+ };
348
+ function useFilter() {
349
+ const [filter, setFilter] = useState3("all");
350
+ const cycleFilter = useCallback3(() => {
351
+ setFilter((prev) => {
352
+ const idx = FILTER_ORDER.indexOf(prev);
353
+ return FILTER_ORDER[(idx + 1) % FILTER_ORDER.length];
354
+ });
355
+ }, []);
356
+ const matches = useCallback3(
357
+ (status) => filter === "all" || filter === status,
358
+ [filter]
359
+ );
360
+ return {
361
+ filter,
362
+ label: FILTER_LABELS[filter],
363
+ cycleFilter,
364
+ matches
365
+ };
366
+ }
367
+
368
+ // src/components/Header.tsx
369
+ import { Box, Text } from "ink";
370
+ import { jsx, jsxs } from "react/jsx-runtime";
371
+ function Header({
372
+ projectName,
373
+ projectVersion,
374
+ currentView,
375
+ views,
376
+ labels,
377
+ filterLabel
378
+ }) {
379
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [
380
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
381
+ projectName,
382
+ " ",
383
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
384
+ "v",
385
+ projectVersion
386
+ ] })
387
+ ] }),
388
+ /* @__PURE__ */ jsx(Box, { gap: 1, children: views.map((view) => /* @__PURE__ */ jsx(
389
+ Text,
390
+ {
391
+ bold: view === currentView,
392
+ color: view === currentView ? "cyan" : void 0,
393
+ dimColor: view !== currentView,
394
+ children: labels[view]
395
+ },
396
+ view
397
+ )) }),
398
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
399
+ "Filter: ",
400
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: filterLabel })
401
+ ] })
402
+ ] });
403
+ }
404
+
405
+ // src/components/HelpBar.tsx
406
+ import { Box as Box2, Text as Text2 } from "ink";
407
+ import { jsx as jsx2 } from "react/jsx-runtime";
408
+ var COMMON_KEYS = "Tab:View f:Filter r:Refresh q:Quit ?:Help";
409
+ var VIEW_KEYS = {
410
+ dashboard: `j/k:Scroll ${COMMON_KEYS}`,
411
+ milestones: `j/k:Navigate s:Sort Enter:Detail ${COMMON_KEYS}`,
412
+ tasks: `j/k:Navigate Enter:Expand ${COMMON_KEYS}`,
413
+ activity: `j/k:Scroll ${COMMON_KEYS}`,
414
+ blockers: `j/k:Scroll ${COMMON_KEYS}`
415
+ };
416
+ function HelpBar({ currentView }) {
417
+ return /* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: VIEW_KEYS[currentView] }) });
418
+ }
419
+
420
+ // src/components/Dashboard.tsx
421
+ import { Box as Box3, Text as Text5 } from "ink";
422
+
423
+ // src/components/StatusBadge.tsx
424
+ import { Text as Text3 } from "ink";
425
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
426
+ var STATUS_CONFIG = {
427
+ completed: { dot: "\u2713", label: "Completed", color: "green" },
428
+ in_progress: { dot: "\u25CF", label: "In Progress", color: "cyan" },
429
+ not_started: { dot: "\u25CB", label: "Not Started", color: "gray" }
430
+ };
431
+ function StatusBadge({ status, compact }) {
432
+ const { dot, label, color } = STATUS_CONFIG[status];
433
+ if (compact) {
434
+ return /* @__PURE__ */ jsx3(Text3, { color, children: dot });
435
+ }
436
+ return /* @__PURE__ */ jsxs2(Text3, { color, children: [
437
+ dot,
438
+ " ",
439
+ label
440
+ ] });
441
+ }
442
+
443
+ // src/components/ProgressBar.tsx
444
+ import { Text as Text4 } from "ink";
445
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
446
+ function ProgressBar({ percent, width = 20, label }) {
447
+ const clamped = Math.max(0, Math.min(100, percent));
448
+ const filled = Math.round(clamped / 100 * width);
449
+ const empty = width - filled;
450
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
451
+ const color = clamped >= 80 ? "green" : clamped >= 40 ? "cyan" : "gray";
452
+ return /* @__PURE__ */ jsxs3(Text4, { children: [
453
+ label && /* @__PURE__ */ jsxs3(Text4, { children: [
454
+ label,
455
+ " "
456
+ ] }),
457
+ /* @__PURE__ */ jsx4(Text4, { color, children: bar }),
458
+ /* @__PURE__ */ jsxs3(Text4, { children: [
459
+ " ",
460
+ clamped,
461
+ "%"
462
+ ] })
463
+ ] });
464
+ }
465
+
466
+ // src/components/Dashboard.tsx
467
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
468
+ function Dashboard({ data }) {
469
+ const { project, milestones, next_steps, current_blockers } = data;
470
+ const completed = milestones.filter((m) => m.status === "completed").length;
471
+ const inProgress = milestones.filter((m) => m.status === "in_progress").length;
472
+ const notStarted = milestones.filter((m) => m.status === "not_started").length;
473
+ const currentMilestone = milestones.find((m) => m.id === project.current_milestone);
474
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", gap: 1, children: [
475
+ /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
476
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: " Project" }),
477
+ /* @__PURE__ */ jsxs4(Text5, { children: [
478
+ project.name,
479
+ " ",
480
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
481
+ "v",
482
+ project.version
483
+ ] })
484
+ ] }),
485
+ /* @__PURE__ */ jsxs4(Box3, { gap: 2, children: [
486
+ /* @__PURE__ */ jsx5(StatusBadge, { status: project.status }),
487
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
488
+ "Started: ",
489
+ project.started || "N/A"
490
+ ] })
491
+ ] }),
492
+ /* @__PURE__ */ jsx5(ProgressBar, { percent: data.progress.overall, label: "Progress:" }),
493
+ currentMilestone && /* @__PURE__ */ jsxs4(Text5, { children: [
494
+ "Current: ",
495
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: currentMilestone.name })
496
+ ] })
497
+ ] }),
498
+ /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
499
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: " Milestones" }),
500
+ /* @__PURE__ */ jsxs4(Box3, { gap: 3, children: [
501
+ /* @__PURE__ */ jsxs4(Text5, { color: "green", children: [
502
+ "\u2713 Completed: ",
503
+ completed
504
+ ] }),
505
+ /* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
506
+ "\u25CF In Progress: ",
507
+ inProgress
508
+ ] }),
509
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
510
+ "\u25CB Not Started: ",
511
+ notStarted
512
+ ] })
513
+ ] })
514
+ ] }),
515
+ current_blockers.length > 0 && /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
516
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "red", children: " Blockers" }),
517
+ current_blockers.map((b, i) => /* @__PURE__ */ jsxs4(Text5, { color: "red", children: [
518
+ " \u2022 ",
519
+ b
520
+ ] }, i))
521
+ ] }),
522
+ next_steps.length > 0 && /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
523
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: " Next Steps" }),
524
+ next_steps.map((step, i) => /* @__PURE__ */ jsxs4(Text5, { children: [
525
+ " ",
526
+ i + 1,
527
+ ". ",
528
+ step
529
+ ] }, i))
530
+ ] })
531
+ ] });
532
+ }
533
+
534
+ // src/components/MilestoneTable.tsx
535
+ import { useState as useState4 } from "react";
536
+ import { Box as Box4, Text as Text6, useInput } from "ink";
537
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
538
+ var SORT_KEYS = ["name", "status", "progress", "tasks", "started", "estimated_weeks"];
539
+ var STATUS_ORDER = {
540
+ in_progress: 0,
541
+ not_started: 1,
542
+ completed: 2
543
+ };
544
+ function sortMilestones(milestones, key, asc) {
545
+ return [...milestones].sort((a, b) => {
546
+ let cmp = 0;
547
+ switch (key) {
548
+ case "name":
549
+ cmp = a.name.localeCompare(b.name);
550
+ break;
551
+ case "status":
552
+ cmp = STATUS_ORDER[a.status] - STATUS_ORDER[b.status];
553
+ break;
554
+ case "progress":
555
+ cmp = a.progress - b.progress;
556
+ break;
557
+ case "tasks":
558
+ cmp = a.tasks_completed - b.tasks_completed;
559
+ break;
560
+ case "started":
561
+ cmp = (a.started || "").localeCompare(b.started || "");
562
+ break;
563
+ case "estimated_weeks":
564
+ cmp = parseFloat(a.estimated_weeks || "0") - parseFloat(b.estimated_weeks || "0");
565
+ break;
566
+ }
567
+ return asc ? cmp : -cmp;
568
+ });
569
+ }
570
+ function MilestoneTable({ milestones, filterMatch, active, onSelect }) {
571
+ const [selectedIdx, setSelectedIdx] = useState4(0);
572
+ const [sortKey, setSortKey] = useState4("status");
573
+ const [sortAsc, setSortAsc] = useState4(true);
574
+ const filtered = milestones.filter((m) => filterMatch(m.status));
575
+ const sorted = sortMilestones(filtered, sortKey, sortAsc);
576
+ useInput((input, key) => {
577
+ if (!active) return;
578
+ if (input === "j" || key.downArrow) {
579
+ setSelectedIdx((i) => Math.min(i + 1, sorted.length - 1));
580
+ } else if (input === "k" || key.upArrow) {
581
+ setSelectedIdx((i) => Math.max(i - 1, 0));
582
+ } else if (input === "s") {
583
+ setSortKey((prev) => {
584
+ const idx = SORT_KEYS.indexOf(prev);
585
+ const next = SORT_KEYS[(idx + 1) % SORT_KEYS.length];
586
+ if (next === prev) setSortAsc((a) => !a);
587
+ return next;
588
+ });
589
+ } else if (key.return && sorted[selectedIdx] && onSelect) {
590
+ onSelect(sorted[selectedIdx]);
591
+ }
592
+ });
593
+ if (sorted.length === 0) {
594
+ return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No milestones match current filter." });
595
+ }
596
+ return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
597
+ /* @__PURE__ */ jsxs5(Box4, { gap: 1, children: [
598
+ /* @__PURE__ */ jsx6(Box4, { width: 30, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
599
+ colLabel("name", sortKey),
600
+ " Name"
601
+ ] }) }),
602
+ /* @__PURE__ */ jsx6(Box4, { width: 14, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
603
+ colLabel("status", sortKey),
604
+ " Status"
605
+ ] }) }),
606
+ /* @__PURE__ */ jsx6(Box4, { width: 10, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
607
+ colLabel("progress", sortKey),
608
+ " Prog"
609
+ ] }) }),
610
+ /* @__PURE__ */ jsx6(Box4, { width: 8, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
611
+ colLabel("tasks", sortKey),
612
+ " Tasks"
613
+ ] }) }),
614
+ /* @__PURE__ */ jsx6(Box4, { width: 12, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
615
+ colLabel("started", sortKey),
616
+ " Started"
617
+ ] }) }),
618
+ /* @__PURE__ */ jsx6(Box4, { width: 6, children: /* @__PURE__ */ jsxs5(Text6, { bold: true, dimColor: true, children: [
619
+ colLabel("estimated_weeks", sortKey),
620
+ " Est"
621
+ ] }) })
622
+ ] }),
623
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(80) }),
624
+ sorted.map((m, i) => {
625
+ const isSelected = i === selectedIdx && active;
626
+ return /* @__PURE__ */ jsxs5(Box4, { gap: 1, children: [
627
+ /* @__PURE__ */ jsx6(Box4, { width: 30, children: /* @__PURE__ */ jsx6(Text6, { bold: isSelected, inverse: isSelected, children: truncate(m.name, 28) }) }),
628
+ /* @__PURE__ */ jsx6(Box4, { width: 14, children: /* @__PURE__ */ jsx6(StatusBadge, { status: m.status }) }),
629
+ /* @__PURE__ */ jsx6(Box4, { width: 10, children: /* @__PURE__ */ jsxs5(Text6, { children: [
630
+ m.progress,
631
+ "%"
632
+ ] }) }),
633
+ /* @__PURE__ */ jsx6(Box4, { width: 8, children: /* @__PURE__ */ jsxs5(Text6, { children: [
634
+ m.tasks_completed,
635
+ "/",
636
+ m.tasks_total
637
+ ] }) }),
638
+ /* @__PURE__ */ jsx6(Box4, { width: 12, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: m.started ? shortDate(m.started) : "\u2014" }) }),
639
+ /* @__PURE__ */ jsx6(Box4, { width: 6, children: /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
640
+ m.estimated_weeks,
641
+ "w"
642
+ ] }) })
643
+ ] }, m.id);
644
+ })
645
+ ] });
646
+ }
647
+ function colLabel(col, active) {
648
+ return col === active ? "\u25BC" : " ";
649
+ }
650
+ function truncate(s, max) {
651
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
652
+ }
653
+ function shortDate(d) {
654
+ try {
655
+ const date = new Date(d);
656
+ return date.toISOString().slice(0, 10);
657
+ } catch {
658
+ return d.slice(0, 10);
659
+ }
660
+ }
661
+
662
+ // src/components/TaskTree.tsx
663
+ import { useState as useState5, useMemo } from "react";
664
+ import { Box as Box5, Text as Text7, useInput as useInput2 } from "ink";
665
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
666
+ function TaskTree({ milestones, tasks, filterMatch, active, onSelectTask }) {
667
+ const [expanded, setExpanded] = useState5(() => {
668
+ const set = /* @__PURE__ */ new Set();
669
+ for (const m of milestones) {
670
+ if (m.status === "in_progress") set.add(m.id);
671
+ }
672
+ return set;
673
+ });
674
+ const [cursorIdx, setCursorIdx] = useState5(0);
675
+ const flatItems = useMemo(() => {
676
+ const items = [];
677
+ for (const m of milestones) {
678
+ const milestoneTasks = (tasks[m.id] || []).filter((t) => filterMatch(t.status));
679
+ if (milestoneTasks.length === 0 && !filterMatch(m.status)) continue;
680
+ items.push({ type: "milestone", milestone: m });
681
+ if (expanded.has(m.id)) {
682
+ for (const t of milestoneTasks) {
683
+ items.push({ type: "task", milestone: m, task: t });
684
+ }
685
+ }
686
+ }
687
+ return items;
688
+ }, [milestones, tasks, filterMatch, expanded]);
689
+ useInput2((input, key) => {
690
+ if (!active) return;
691
+ if (input === "j" || key.downArrow) {
692
+ setCursorIdx((i) => Math.min(i + 1, flatItems.length - 1));
693
+ } else if (input === "k" || key.upArrow) {
694
+ setCursorIdx((i) => Math.max(i - 1, 0));
695
+ } else if (key.return || input === " ") {
696
+ const item = flatItems[cursorIdx];
697
+ if (!item) return;
698
+ if (item.type === "milestone") {
699
+ setExpanded((prev) => {
700
+ const next = new Set(prev);
701
+ if (next.has(item.milestone.id)) {
702
+ next.delete(item.milestone.id);
703
+ } else {
704
+ next.add(item.milestone.id);
705
+ }
706
+ return next;
707
+ });
708
+ } else if (item.task && onSelectTask) {
709
+ onSelectTask(item.task);
710
+ }
711
+ }
712
+ });
713
+ if (flatItems.length === 0) {
714
+ return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "No items match current filter." });
715
+ }
716
+ return /* @__PURE__ */ jsx7(Box5, { flexDirection: "column", children: flatItems.map((item, i) => {
717
+ const isSelected = i === cursorIdx && active;
718
+ if (item.type === "milestone") {
719
+ const m = item.milestone;
720
+ const icon = expanded.has(m.id) ? "\u25BC" : "\u25BA";
721
+ const taskCount = (tasks[m.id] || []).filter((t2) => filterMatch(t2.status)).length;
722
+ return /* @__PURE__ */ jsxs6(Box5, { gap: 1, children: [
723
+ /* @__PURE__ */ jsxs6(Text7, { bold: isSelected, inverse: isSelected, children: [
724
+ icon,
725
+ " ",
726
+ m.name
727
+ ] }),
728
+ /* @__PURE__ */ jsx7(StatusBadge, { status: m.status, compact: true }),
729
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
730
+ "[",
731
+ m.progress,
732
+ "%]"
733
+ ] }),
734
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
735
+ "(",
736
+ taskCount,
737
+ " tasks)"
738
+ ] })
739
+ ] }, m.id);
740
+ }
741
+ const t = item.task;
742
+ return /* @__PURE__ */ jsxs6(Box5, { gap: 1, marginLeft: 4, children: [
743
+ /* @__PURE__ */ jsxs6(Text7, { bold: isSelected, inverse: isSelected, children: [
744
+ /* @__PURE__ */ jsx7(StatusBadge, { status: t.status, compact: true }),
745
+ " ",
746
+ t.name
747
+ ] }),
748
+ t.estimated_hours && /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
749
+ t.estimated_hours,
750
+ "h"
751
+ ] })
752
+ ] }, t.id);
753
+ }) });
754
+ }
755
+
756
+ // src/components/ActivityLog.tsx
757
+ import { Box as Box6, Text as Text8 } from "ink";
758
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
759
+ function ActivityLog({ entries }) {
760
+ if (entries.length === 0) {
761
+ return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "No recent work entries." });
762
+ }
763
+ return /* @__PURE__ */ jsx8(Box6, { flexDirection: "column", gap: 1, children: entries.map((entry, i) => /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", children: [
764
+ /* @__PURE__ */ jsxs7(Text8, { children: [
765
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: entry.date }),
766
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
767
+ " \u2014 ",
768
+ entry.description
769
+ ] })
770
+ ] }),
771
+ entry.items.map((item, j) => /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
772
+ " \u2022 ",
773
+ item
774
+ ] }, j))
775
+ ] }, i)) });
776
+ }
777
+
778
+ // src/components/BlockersNextSteps.tsx
779
+ import { Box as Box7, Text as Text9 } from "ink";
780
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
781
+ function BlockersNextSteps({ blockers, nextSteps }) {
782
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", gap: 1, children: [
783
+ /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
784
+ /* @__PURE__ */ jsx9(Text9, { bold: true, color: blockers.length > 0 ? "red" : void 0, children: " Current Blockers" }),
785
+ blockers.length === 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (none)" }) : blockers.map((b, i) => /* @__PURE__ */ jsxs8(Text9, { color: "red", children: [
786
+ " \u2022 ",
787
+ b
788
+ ] }, i))
789
+ ] }),
790
+ /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
791
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: " Next Steps" }),
792
+ nextSteps.length === 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " (none)" }) : nextSteps.map((s, i) => /* @__PURE__ */ jsxs8(Text9, { children: [
793
+ " ",
794
+ i + 1,
795
+ ". ",
796
+ s
797
+ ] }, i))
798
+ ] })
799
+ ] });
800
+ }
801
+
802
+ // src/components/MilestoneDetail.tsx
803
+ import React4, { useMemo as useMemo3 } from "react";
804
+ import { Box as Box9, Text as Text12, useInput as useInput3 } from "ink";
805
+
806
+ // src/lib/markdown-loader.ts
807
+ import fs3 from "fs";
808
+ import path from "path";
809
+ function getBasePath(progressYamlPath) {
810
+ const dir = path.dirname(progressYamlPath);
811
+ if (path.basename(dir) === "agent") {
812
+ return path.dirname(dir);
813
+ }
814
+ return dir;
815
+ }
816
+ function loadMarkdownFile(basePath, relativePath) {
817
+ const fullPath = path.resolve(basePath, relativePath);
818
+ try {
819
+ const content = fs3.readFileSync(fullPath, "utf-8");
820
+ return { content, filePath: relativePath };
821
+ } catch (err) {
822
+ const msg = err instanceof Error ? err.message : String(err);
823
+ if (msg.includes("ENOENT")) {
824
+ return { error: `No document found at ${relativePath}` };
825
+ }
826
+ if (msg.includes("EACCES")) {
827
+ return { error: `Cannot read ${relativePath}: permission denied` };
828
+ }
829
+ return { error: `Error reading ${relativePath}: ${msg}` };
830
+ }
831
+ }
832
+ function resolveMilestoneFile(basePath, milestoneId) {
833
+ const match = milestoneId.match(/(\d+)/);
834
+ if (!match) return null;
835
+ const num = match[1];
836
+ const milestonesDir = path.join(basePath, "agent", "milestones");
837
+ try {
838
+ const files = fs3.readdirSync(milestonesDir);
839
+ const pattern = `milestone-${num}-`;
840
+ const found = files.find(
841
+ (f) => f.startsWith(pattern) && f.endsWith(".md") && !f.includes("template")
842
+ );
843
+ return found ? path.join("agent", "milestones", found) : null;
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
848
+
849
+ // src/components/Breadcrumb.tsx
850
+ import { Text as Text10 } from "ink";
851
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
852
+ function Breadcrumb({ items }) {
853
+ return /* @__PURE__ */ jsx10(Text10, { children: items.map((item, i) => /* @__PURE__ */ jsx10(Text10, { children: i < items.length - 1 ? /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
854
+ item.label,
855
+ " > "
856
+ ] }) : /* @__PURE__ */ jsx10(Text10, { bold: true, children: item.label }) }, i)) });
857
+ }
858
+
859
+ // src/components/MarkdownRenderer.tsx
860
+ import { useMemo as useMemo2 } from "react";
861
+ import { Text as Text11, Box as Box8 } from "ink";
862
+ import { Marked } from "marked";
863
+ import { markedTerminal } from "marked-terminal";
864
+ import { jsx as jsx11 } from "react/jsx-runtime";
865
+ function MarkdownRenderer({ content }) {
866
+ const rendered = useMemo2(() => {
867
+ try {
868
+ const marked = new Marked(markedTerminal());
869
+ return marked.parse(content);
870
+ } catch {
871
+ return content;
872
+ }
873
+ }, [content]);
874
+ return /* @__PURE__ */ jsx11(Box8, { flexDirection: "column", children: /* @__PURE__ */ jsx11(Text11, { children: rendered }) });
875
+ }
876
+
877
+ // src/components/MilestoneDetail.tsx
878
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
879
+ function MilestoneDetail({
880
+ milestone,
881
+ tasks,
882
+ filePath: filePath2,
883
+ active,
884
+ onBack,
885
+ onSelectTask
886
+ }) {
887
+ const markdownResult = useMemo3(() => {
888
+ const basePath = getBasePath(filePath2);
889
+ const resolved = resolveMilestoneFile(basePath, milestone.id);
890
+ if (!resolved) return { error: `No milestone document found for ${milestone.id}` };
891
+ return loadMarkdownFile(basePath, resolved);
892
+ }, [filePath2, milestone.id]);
893
+ const [taskIdx, setTaskIdx] = React4.useState(0);
894
+ useInput3((input, key) => {
895
+ if (!active) return;
896
+ if (key.escape || key.backspace || key.delete) {
897
+ onBack();
898
+ } else if (input === "j" || key.downArrow) {
899
+ setTaskIdx((i) => Math.min(i + 1, tasks.length - 1));
900
+ } else if (input === "k" || key.upArrow) {
901
+ setTaskIdx((i) => Math.max(i - 1, 0));
902
+ } else if (key.return && tasks[taskIdx] && onSelectTask) {
903
+ onSelectTask(tasks[taskIdx]);
904
+ }
905
+ });
906
+ return /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", gap: 1, children: [
907
+ /* @__PURE__ */ jsx12(Breadcrumb, { items: [{ label: "Milestones" }, { label: milestone.name }] }),
908
+ /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
909
+ /* @__PURE__ */ jsxs10(Box9, { gap: 2, children: [
910
+ /* @__PURE__ */ jsx12(StatusBadge, { status: milestone.status }),
911
+ /* @__PURE__ */ jsx12(ProgressBar, { percent: milestone.progress, width: 15 })
912
+ ] }),
913
+ /* @__PURE__ */ jsxs10(Box9, { gap: 2, children: [
914
+ /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
915
+ "Started: ",
916
+ milestone.started || "\u2014"
917
+ ] }),
918
+ /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
919
+ "Est: ",
920
+ milestone.estimated_weeks,
921
+ "w"
922
+ ] }),
923
+ /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
924
+ "Tasks: ",
925
+ milestone.tasks_completed,
926
+ "/",
927
+ milestone.tasks_total
928
+ ] })
929
+ ] }),
930
+ milestone.notes && /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: milestone.notes })
931
+ ] }),
932
+ "content" in markdownResult ? /* @__PURE__ */ jsx12(MarkdownRenderer, { content: markdownResult.content }) : /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "error" in markdownResult ? markdownResult.error : "No content" }),
933
+ tasks.length > 0 && /* @__PURE__ */ jsxs10(Box9, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
934
+ /* @__PURE__ */ jsx12(Text12, { bold: true, children: " Tasks" }),
935
+ tasks.map((t, i) => /* @__PURE__ */ jsxs10(Text12, { bold: i === taskIdx && active, inverse: i === taskIdx && active, children: [
936
+ /* @__PURE__ */ jsx12(StatusBadge, { status: t.status, compact: true }),
937
+ " ",
938
+ t.name
939
+ ] }, t.id))
940
+ ] }),
941
+ /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Backspace:Back j/k:Navigate tasks Enter:Open task" })
942
+ ] });
943
+ }
944
+
945
+ // src/components/TaskDetail.tsx
946
+ import { useMemo as useMemo4 } from "react";
947
+ import { Box as Box10, Text as Text13, useInput as useInput4 } from "ink";
948
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
949
+ function TaskDetail({
950
+ task,
951
+ milestone,
952
+ siblings,
953
+ filePath: filePath2,
954
+ active,
955
+ onBack,
956
+ onNavigateSibling
957
+ }) {
958
+ const markdownResult = useMemo4(() => {
959
+ if (!task.file) return { error: `No file path for ${task.id}` };
960
+ const basePath = getBasePath(filePath2);
961
+ return loadMarkdownFile(basePath, task.file);
962
+ }, [filePath2, task.file, task.id]);
963
+ const siblingIdx = siblings.findIndex((t) => t.id === task.id);
964
+ const prevTask = siblingIdx > 0 ? siblings[siblingIdx - 1] : null;
965
+ const nextTask = siblingIdx < siblings.length - 1 ? siblings[siblingIdx + 1] : null;
966
+ useInput4((input, key) => {
967
+ if (!active) return;
968
+ if (key.escape || key.backspace || key.delete) {
969
+ onBack();
970
+ } else if (input === "[" && prevTask && onNavigateSibling) {
971
+ onNavigateSibling(prevTask);
972
+ } else if (input === "]" && nextTask && onNavigateSibling) {
973
+ onNavigateSibling(nextTask);
974
+ }
975
+ });
976
+ return /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", gap: 1, children: [
977
+ /* @__PURE__ */ jsx13(
978
+ Breadcrumb,
979
+ {
980
+ items: [
981
+ { label: "Milestones" },
982
+ { label: milestone.name },
983
+ { label: task.name }
984
+ ]
985
+ }
986
+ ),
987
+ /* @__PURE__ */ jsxs11(Box10, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: [
988
+ /* @__PURE__ */ jsx13(StatusBadge, { status: task.status }),
989
+ /* @__PURE__ */ jsxs11(Box10, { gap: 2, children: [
990
+ task.estimated_hours && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
991
+ "Est: ",
992
+ task.estimated_hours,
993
+ "h"
994
+ ] }),
995
+ task.completed_date && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
996
+ "Completed: ",
997
+ task.completed_date
998
+ ] })
999
+ ] }),
1000
+ /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
1001
+ "Milestone: ",
1002
+ milestone.name
1003
+ ] }),
1004
+ task.notes && /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: task.notes })
1005
+ ] }),
1006
+ "content" in markdownResult ? /* @__PURE__ */ jsx13(MarkdownRenderer, { content: markdownResult.content }) : /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "error" in markdownResult ? markdownResult.error : "No content" }),
1007
+ /* @__PURE__ */ jsxs11(Box10, { gap: 2, children: [
1008
+ prevTask ? /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
1009
+ "[: \u2190 ",
1010
+ prevTask.name
1011
+ ] }) : /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "[: (first task)" }),
1012
+ nextTask ? /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
1013
+ "]: \u2192 ",
1014
+ nextTask.name
1015
+ ] }) : /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "]: (last task)" })
1016
+ ] }),
1017
+ /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: "Backspace:Back [/]:Prev/Next task" })
1018
+ ] });
1019
+ }
1020
+
1021
+ // src/components/SearchResults.tsx
1022
+ import { useState as useState6 } from "react";
1023
+ import { Box as Box11, Text as Text14, useInput as useInput5 } from "ink";
1024
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1025
+ function SearchResults({
1026
+ query,
1027
+ results,
1028
+ active,
1029
+ onSelectMilestone,
1030
+ onSelectTask,
1031
+ onCancel
1032
+ }) {
1033
+ const [selectedIdx, setSelectedIdx] = useState6(0);
1034
+ useInput5((input, key) => {
1035
+ if (!active) return;
1036
+ if (key.escape) {
1037
+ onCancel();
1038
+ return;
1039
+ }
1040
+ if (input === "j" || key.downArrow) {
1041
+ setSelectedIdx((i) => Math.min(i + 1, results.length - 1));
1042
+ } else if (input === "k" || key.upArrow) {
1043
+ setSelectedIdx((i) => Math.max(i - 1, 0));
1044
+ } else if (key.return && results[selectedIdx]) {
1045
+ const r = results[selectedIdx];
1046
+ if (r.type === "milestone" && r.milestone && onSelectMilestone) {
1047
+ onSelectMilestone(r.milestone);
1048
+ } else if (r.type === "task" && r.task && onSelectTask) {
1049
+ onSelectTask(r.task);
1050
+ }
1051
+ }
1052
+ });
1053
+ const milestoneResults = results.filter((r) => r.type === "milestone");
1054
+ const taskResults = results.filter((r) => r.type === "task");
1055
+ let flatIdx = 0;
1056
+ return /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", gap: 1, children: [
1057
+ /* @__PURE__ */ jsxs12(Text14, { children: [
1058
+ "Search: ",
1059
+ /* @__PURE__ */ jsx14(Text14, { bold: true, children: query }),
1060
+ query && /* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
1061
+ " (",
1062
+ results.length,
1063
+ " results)"
1064
+ ] })
1065
+ ] }),
1066
+ results.length === 0 && query.trim() && /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "No results found." }),
1067
+ milestoneResults.length > 0 && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
1068
+ /* @__PURE__ */ jsxs12(Text14, { bold: true, dimColor: true, children: [
1069
+ "Milestones (",
1070
+ milestoneResults.length,
1071
+ ")"
1072
+ ] }),
1073
+ milestoneResults.map((r) => {
1074
+ const idx = flatIdx++;
1075
+ const m = r.milestone;
1076
+ return /* @__PURE__ */ jsxs12(Text14, { bold: idx === selectedIdx, inverse: idx === selectedIdx, children: [
1077
+ " ",
1078
+ /* @__PURE__ */ jsx14(StatusBadge, { status: m.status, compact: true }),
1079
+ " ",
1080
+ m.name
1081
+ ] }, r.id);
1082
+ })
1083
+ ] }),
1084
+ taskResults.length > 0 && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", children: [
1085
+ /* @__PURE__ */ jsxs12(Text14, { bold: true, dimColor: true, children: [
1086
+ "Tasks (",
1087
+ taskResults.length,
1088
+ ")"
1089
+ ] }),
1090
+ taskResults.map((r) => {
1091
+ const idx = flatIdx++;
1092
+ const t = r.task;
1093
+ return /* @__PURE__ */ jsxs12(Text14, { bold: idx === selectedIdx, inverse: idx === selectedIdx, children: [
1094
+ " ",
1095
+ /* @__PURE__ */ jsx14(StatusBadge, { status: t.status, compact: true }),
1096
+ " ",
1097
+ t.name
1098
+ ] }, r.id);
1099
+ })
1100
+ ] }),
1101
+ /* @__PURE__ */ jsx14(Text14, { dimColor: true, children: "j/k:Navigate Enter:Open Esc:Cancel" })
1102
+ ] });
1103
+ }
1104
+
1105
+ // src/hooks/useSearch.ts
1106
+ import { useState as useState7, useMemo as useMemo5, useCallback as useCallback4 } from "react";
1107
+ import Fuse from "fuse.js";
1108
+ function useSearch(data) {
1109
+ const [query, setQuery] = useState7("");
1110
+ const [isSearching, setIsSearching] = useState7(false);
1111
+ const fuse = useMemo5(() => {
1112
+ if (!data) return null;
1113
+ const items = [];
1114
+ for (const m of data.milestones) {
1115
+ items.push({ type: "milestone", milestone: m, name: m.name, id: m.id });
1116
+ }
1117
+ for (const [, tasks] of Object.entries(data.tasks)) {
1118
+ for (const t of tasks) {
1119
+ items.push({ type: "task", task: t, name: t.name, id: t.id });
1120
+ }
1121
+ }
1122
+ return new Fuse(items, {
1123
+ keys: [
1124
+ { name: "name", weight: 0.7 },
1125
+ { name: "task.notes", weight: 0.2 },
1126
+ { name: "milestone.notes", weight: 0.1 }
1127
+ ],
1128
+ threshold: 0.4,
1129
+ includeScore: true
1130
+ });
1131
+ }, [data]);
1132
+ const results = useMemo5(() => {
1133
+ if (!fuse || !query.trim()) return [];
1134
+ return fuse.search(query).map((r) => r.item);
1135
+ }, [fuse, query]);
1136
+ const startSearch = useCallback4(() => {
1137
+ setIsSearching(true);
1138
+ setQuery("");
1139
+ }, []);
1140
+ const cancelSearch = useCallback4(() => {
1141
+ setIsSearching(false);
1142
+ setQuery("");
1143
+ }, []);
1144
+ return { query, setQuery, results, isSearching, startSearch, cancelSearch };
1145
+ }
1146
+
1147
+ // src/app.tsx
1148
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1149
+ function App({ filePath: filePath2, watch, initialView }) {
1150
+ const { data, error, reload } = useProgressData(filePath2);
1151
+ useWatchMode(filePath2, watch, reload);
1152
+ const nav = useNavigation(initialView);
1153
+ const filter = useFilter();
1154
+ const { exit } = useApp();
1155
+ const [showHelp, setShowHelp] = useState8(false);
1156
+ const [detail, setDetail] = useState8({ type: "none" });
1157
+ const search = useSearch(data);
1158
+ const openMilestoneDetail = useCallback5((milestone) => {
1159
+ setDetail({ type: "milestone", milestone });
1160
+ }, []);
1161
+ const openTaskDetail = useCallback5((task) => {
1162
+ if (!data) return;
1163
+ const milestone = data.milestones.find((m) => m.id === task.milestone_id);
1164
+ if (milestone) {
1165
+ setDetail({ type: "task", task, milestone });
1166
+ }
1167
+ }, [data]);
1168
+ const closeDetail = useCallback5(() => {
1169
+ setDetail((prev) => {
1170
+ if (prev.type === "task") {
1171
+ return { type: "milestone", milestone: prev.milestone };
1172
+ }
1173
+ return { type: "none" };
1174
+ });
1175
+ }, []);
1176
+ const navigateToSiblingTask = useCallback5((task) => {
1177
+ if (!data) return;
1178
+ const milestone = data.milestones.find((m) => m.id === task.milestone_id);
1179
+ if (milestone) {
1180
+ setDetail({ type: "task", task, milestone });
1181
+ }
1182
+ }, [data]);
1183
+ useInput6((input, key) => {
1184
+ if (showHelp) {
1185
+ setShowHelp(false);
1186
+ return;
1187
+ }
1188
+ if (search.isSearching) {
1189
+ if (key.escape) {
1190
+ search.cancelSearch();
1191
+ return;
1192
+ }
1193
+ if (key.backspace || key.delete) {
1194
+ search.setQuery(search.query.slice(0, -1));
1195
+ return;
1196
+ }
1197
+ if (search.query && (input === "j" || input === "k" || key.return || key.downArrow || key.upArrow)) {
1198
+ return;
1199
+ }
1200
+ if (input && !key.ctrl && !key.meta && input.length === 1 && input !== "q") {
1201
+ search.setQuery(search.query + input);
1202
+ return;
1203
+ }
1204
+ return;
1205
+ }
1206
+ if (detail.type !== "none") {
1207
+ if (input === "q") {
1208
+ exit();
1209
+ return;
1210
+ }
1211
+ if (input === "?") {
1212
+ setShowHelp(true);
1213
+ return;
1214
+ }
1215
+ if (input === "r") {
1216
+ reload();
1217
+ return;
1218
+ }
1219
+ return;
1220
+ }
1221
+ if (input === "q") {
1222
+ exit();
1223
+ return;
1224
+ }
1225
+ if (input === "?") {
1226
+ setShowHelp(true);
1227
+ return;
1228
+ }
1229
+ if (input === "f") {
1230
+ filter.cycleFilter();
1231
+ return;
1232
+ }
1233
+ if (input === "r") {
1234
+ reload();
1235
+ return;
1236
+ }
1237
+ if (input === "/") {
1238
+ search.startSearch();
1239
+ return;
1240
+ }
1241
+ if (key.tab) {
1242
+ if (key.shift) {
1243
+ nav.prevView();
1244
+ } else {
1245
+ nav.nextView();
1246
+ }
1247
+ }
1248
+ });
1249
+ if (error && !data) {
1250
+ return /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs13(Text15, { color: "red", children: [
1251
+ "Error: ",
1252
+ error
1253
+ ] }) });
1254
+ }
1255
+ if (!data) {
1256
+ return /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Loading..." }) });
1257
+ }
1258
+ if (showHelp) {
1259
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", padding: 1, children: [
1260
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Keyboard Shortcuts" }),
1261
+ /* @__PURE__ */ jsx15(Text15, {}),
1262
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1263
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Tab" }),
1264
+ " / ",
1265
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Shift+Tab" }),
1266
+ " Switch views"
1267
+ ] }),
1268
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1269
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "j/k" }),
1270
+ " or ",
1271
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "\u2191/\u2193" }),
1272
+ " Navigate"
1273
+ ] }),
1274
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1275
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Enter" }),
1276
+ " Expand / open detail"
1277
+ ] }),
1278
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1279
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "Backspace/Esc" }),
1280
+ " Back from detail"
1281
+ ] }),
1282
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1283
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "[/]" }),
1284
+ " Prev/next task (in detail)"
1285
+ ] }),
1286
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1287
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "s" }),
1288
+ " Cycle sort (table)"
1289
+ ] }),
1290
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1291
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "f" }),
1292
+ " Cycle status filter"
1293
+ ] }),
1294
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1295
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "r" }),
1296
+ " Refresh data"
1297
+ ] }),
1298
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1299
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "q" }),
1300
+ " Quit"
1301
+ ] }),
1302
+ /* @__PURE__ */ jsxs13(Text15, { children: [
1303
+ /* @__PURE__ */ jsx15(Text15, { bold: true, children: "?" }),
1304
+ " Toggle this help"
1305
+ ] }),
1306
+ /* @__PURE__ */ jsx15(Text15, {}),
1307
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Press any key to dismiss" })
1308
+ ] });
1309
+ }
1310
+ if (search.isSearching) {
1311
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1312
+ /* @__PURE__ */ jsx15(
1313
+ Header,
1314
+ {
1315
+ projectName: data.project.name,
1316
+ projectVersion: data.project.version,
1317
+ currentView: nav.currentView,
1318
+ views: nav.views,
1319
+ labels: nav.labels,
1320
+ filterLabel: filter.label
1321
+ }
1322
+ ),
1323
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u2500".repeat(80) }),
1324
+ /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx15(
1325
+ SearchResults,
1326
+ {
1327
+ query: search.query,
1328
+ results: search.results,
1329
+ active: true,
1330
+ onSelectMilestone: (m) => {
1331
+ search.cancelSearch();
1332
+ openMilestoneDetail(m);
1333
+ },
1334
+ onSelectTask: (t) => {
1335
+ search.cancelSearch();
1336
+ openTaskDetail(t);
1337
+ },
1338
+ onCancel: search.cancelSearch
1339
+ }
1340
+ ) })
1341
+ ] });
1342
+ }
1343
+ if (detail.type === "milestone") {
1344
+ const milestoneTasks = data.tasks[detail.milestone.id] || [];
1345
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1346
+ /* @__PURE__ */ jsx15(
1347
+ Header,
1348
+ {
1349
+ projectName: data.project.name,
1350
+ projectVersion: data.project.version,
1351
+ currentView: nav.currentView,
1352
+ views: nav.views,
1353
+ labels: nav.labels,
1354
+ filterLabel: filter.label
1355
+ }
1356
+ ),
1357
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u2500".repeat(80) }),
1358
+ /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx15(
1359
+ MilestoneDetail,
1360
+ {
1361
+ milestone: detail.milestone,
1362
+ tasks: milestoneTasks,
1363
+ data,
1364
+ filePath: filePath2,
1365
+ active: true,
1366
+ onBack: closeDetail,
1367
+ onSelectTask: openTaskDetail
1368
+ }
1369
+ ) })
1370
+ ] });
1371
+ }
1372
+ if (detail.type === "task") {
1373
+ const siblings = data.tasks[detail.milestone.id] || [];
1374
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1375
+ /* @__PURE__ */ jsx15(
1376
+ Header,
1377
+ {
1378
+ projectName: data.project.name,
1379
+ projectVersion: data.project.version,
1380
+ currentView: nav.currentView,
1381
+ views: nav.views,
1382
+ labels: nav.labels,
1383
+ filterLabel: filter.label
1384
+ }
1385
+ ),
1386
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u2500".repeat(80) }),
1387
+ /* @__PURE__ */ jsx15(Box12, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: /* @__PURE__ */ jsx15(
1388
+ TaskDetail,
1389
+ {
1390
+ task: detail.task,
1391
+ milestone: detail.milestone,
1392
+ siblings,
1393
+ data,
1394
+ filePath: filePath2,
1395
+ active: true,
1396
+ onBack: closeDetail,
1397
+ onNavigateSibling: navigateToSiblingTask
1398
+ }
1399
+ ) })
1400
+ ] });
1401
+ }
1402
+ return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", children: [
1403
+ /* @__PURE__ */ jsx15(
1404
+ Header,
1405
+ {
1406
+ projectName: data.project.name,
1407
+ projectVersion: data.project.version,
1408
+ currentView: nav.currentView,
1409
+ views: nav.views,
1410
+ labels: nav.labels,
1411
+ filterLabel: filter.label
1412
+ }
1413
+ ),
1414
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u2500".repeat(80) }),
1415
+ /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
1416
+ nav.currentView === "dashboard" && /* @__PURE__ */ jsx15(Dashboard, { data }),
1417
+ nav.currentView === "milestones" && /* @__PURE__ */ jsx15(
1418
+ MilestoneTable,
1419
+ {
1420
+ milestones: data.milestones,
1421
+ filterMatch: filter.matches,
1422
+ active: true,
1423
+ onSelect: openMilestoneDetail
1424
+ }
1425
+ ),
1426
+ nav.currentView === "tasks" && /* @__PURE__ */ jsx15(
1427
+ TaskTree,
1428
+ {
1429
+ milestones: data.milestones,
1430
+ tasks: data.tasks,
1431
+ filterMatch: filter.matches,
1432
+ active: true,
1433
+ onSelectTask: openTaskDetail
1434
+ }
1435
+ ),
1436
+ nav.currentView === "activity" && /* @__PURE__ */ jsx15(ActivityLog, { entries: data.recent_work }),
1437
+ nav.currentView === "blockers" && /* @__PURE__ */ jsx15(
1438
+ BlockersNextSteps,
1439
+ {
1440
+ blockers: data.current_blockers,
1441
+ nextSteps: data.next_steps
1442
+ }
1443
+ )
1444
+ ] }),
1445
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "\u2500".repeat(80) }),
1446
+ /* @__PURE__ */ jsx15(HelpBar, { currentView: nav.currentView })
1447
+ ] });
1448
+ }
1449
+
1450
+ // src/cli.tsx
1451
+ import { jsx as jsx16 } from "react/jsx-runtime";
1452
+ var cli = meow(`
1453
+ Usage
1454
+ $ acp-visualizer-tui [path]
1455
+
1456
+ Arguments
1457
+ path Path to progress.yaml (default: ./agent/progress.yaml)
1458
+
1459
+ Options
1460
+ -w, --watch Watch mode: auto-refresh on file changes
1461
+ -v, --view Initial view: dashboard|milestones|tasks|activity|blockers
1462
+ --json Output parsed data as JSON (non-interactive)
1463
+ --no-color Disable colors
1464
+ -h, --help Show help
1465
+ -V, --version Show version
1466
+ `, {
1467
+ importMeta: import.meta,
1468
+ flags: {
1469
+ watch: { type: "boolean", shortFlag: "w", default: false },
1470
+ view: { type: "string", shortFlag: "v", default: "dashboard" },
1471
+ json: { type: "boolean", default: false }
1472
+ }
1473
+ });
1474
+ var filePath = cli.input[0] || "./agent/progress.yaml";
1475
+ var resolvedPath = path2.resolve(filePath);
1476
+ if (!fs4.existsSync(resolvedPath)) {
1477
+ console.error(`Error: File not found: ${resolvedPath}`);
1478
+ console.error(`
1479
+ Make sure progress.yaml exists at the specified path.`);
1480
+ console.error(`Default path: ./agent/progress.yaml`);
1481
+ process.exit(1);
1482
+ }
1483
+ if (cli.flags.json) {
1484
+ try {
1485
+ const raw = fs4.readFileSync(resolvedPath, "utf-8");
1486
+ const data = parseProgressYaml(raw);
1487
+ console.log(JSON.stringify(data, null, 2));
1488
+ } catch (err) {
1489
+ const msg = err instanceof Error ? err.message : String(err);
1490
+ console.error(`Error reading file: ${msg}`);
1491
+ process.exit(1);
1492
+ }
1493
+ process.exit(0);
1494
+ }
1495
+ render(
1496
+ /* @__PURE__ */ jsx16(
1497
+ App,
1498
+ {
1499
+ filePath: resolvedPath,
1500
+ watch: cli.flags.watch,
1501
+ initialView: cli.flags.view
1502
+ }
1503
+ )
1504
+ );
1505
+ //# sourceMappingURL=cli.js.map