@pruddiman/hem 0.0.1-beta-5671db0

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agents/arbiter-agent.d.ts +72 -0
  3. package/dist/agents/arbiter-agent.js +149 -0
  4. package/dist/agents/architecture-agent.d.ts +148 -0
  5. package/dist/agents/architecture-agent.js +459 -0
  6. package/dist/agents/base-agent.d.ts +44 -0
  7. package/dist/agents/base-agent.js +57 -0
  8. package/dist/agents/crossref-agent.d.ts +140 -0
  9. package/dist/agents/crossref-agent.js +560 -0
  10. package/dist/agents/crossref-arbiter-agent.d.ts +72 -0
  11. package/dist/agents/crossref-arbiter-agent.js +147 -0
  12. package/dist/agents/documentation-agent.d.ts +55 -0
  13. package/dist/agents/documentation-agent.js +159 -0
  14. package/dist/agents/exploration-agent.d.ts +58 -0
  15. package/dist/agents/exploration-agent.js +102 -0
  16. package/dist/agents/grouping-agent.d.ts +167 -0
  17. package/dist/agents/grouping-agent.js +557 -0
  18. package/dist/agents/index-agent.d.ts +86 -0
  19. package/dist/agents/index-agent.js +360 -0
  20. package/dist/agents/organization-agent.d.ts +144 -0
  21. package/dist/agents/organization-agent.js +607 -0
  22. package/dist/auth.d.ts +372 -0
  23. package/dist/auth.js +1072 -0
  24. package/dist/broadcast-mcp.d.ts +21 -0
  25. package/dist/broadcast-mcp.js +59 -0
  26. package/dist/changelog.d.ts +85 -0
  27. package/dist/changelog.js +223 -0
  28. package/dist/decision-queue.d.ts +173 -0
  29. package/dist/decision-queue.js +265 -0
  30. package/dist/diff-scope.d.ts +24 -0
  31. package/dist/diff-scope.js +28 -0
  32. package/dist/discovery.d.ts +54 -0
  33. package/dist/discovery.js +405 -0
  34. package/dist/grouping.d.ts +37 -0
  35. package/dist/grouping.js +343 -0
  36. package/dist/helpers/format.d.ts +5 -0
  37. package/dist/helpers/format.js +13 -0
  38. package/dist/helpers/index.d.ts +11 -0
  39. package/dist/helpers/index.js +11 -0
  40. package/dist/helpers/parsing.d.ts +52 -0
  41. package/dist/helpers/parsing.js +128 -0
  42. package/dist/helpers/paths.d.ts +41 -0
  43. package/dist/helpers/paths.js +67 -0
  44. package/dist/helpers/strings.d.ts +45 -0
  45. package/dist/helpers/strings.js +97 -0
  46. package/dist/index.d.ts +135 -0
  47. package/dist/index.js +1087 -0
  48. package/dist/merge-utils.d.ts +22 -0
  49. package/dist/merge-utils.js +34 -0
  50. package/dist/orchestrator.d.ts +194 -0
  51. package/dist/orchestrator.js +1169 -0
  52. package/dist/output.d.ts +106 -0
  53. package/dist/output.js +243 -0
  54. package/dist/progress.d.ts +228 -0
  55. package/dist/progress.js +644 -0
  56. package/dist/providers/copilot.d.ts +247 -0
  57. package/dist/providers/copilot.js +598 -0
  58. package/dist/providers/index.d.ts +15 -0
  59. package/dist/providers/index.js +12 -0
  60. package/dist/providers/opencode.d.ts +156 -0
  61. package/dist/providers/opencode.js +416 -0
  62. package/dist/providers/types.d.ts +156 -0
  63. package/dist/providers/types.js +16 -0
  64. package/dist/resources.d.ts +76 -0
  65. package/dist/resources.js +151 -0
  66. package/dist/search-index.d.ts +71 -0
  67. package/dist/search-index.js +187 -0
  68. package/dist/search-mcp.d.ts +25 -0
  69. package/dist/search-mcp.js +100 -0
  70. package/dist/server-utils.d.ts +56 -0
  71. package/dist/server-utils.js +135 -0
  72. package/dist/session.d.ts +227 -0
  73. package/dist/session.js +370 -0
  74. package/dist/types.d.ts +272 -0
  75. package/dist/types.js +5 -0
  76. package/dist/worktree.d.ts +82 -0
  77. package/dist/worktree.js +187 -0
  78. package/package.json +45 -0
@@ -0,0 +1,644 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Ink React components for terminal UI — auth prompts, dashboard, phase display.
4
+ *
5
+ * Components:
6
+ * - AuthPrompt — First-run 4-option select menu
7
+ * - ApiKeyInput — Provider select + API key text entry
8
+ * - FreeModelPicker — Free model list with data security annotations
9
+ * - ConfigPrompt — Project config provider/model selection
10
+ * - App — Root dashboard component holding ProgressState
11
+ * - Header — Version + model display
12
+ * - PhaseRow — Single pipeline phase with status indicator
13
+ * - GenerationDashboard — Multi-line group status during generation phase
14
+ * - GroupRow — Single file group with spinner/status/label
15
+ * - Summary — Final success/failure report with timing
16
+ *
17
+ * Reference: plan.md lines 310-351 (Terminal UI Architecture),
18
+ * contracts/cli-interface.md lines 93-127, 148-189.
19
+ */
20
+ import React, { useState, useCallback, useRef } from "react";
21
+ import { render as inkRender, Box, Text, useInput, useStdout } from "ink";
22
+ import Spinner from "ink-spinner";
23
+ import { formatElapsed } from "./helpers/format.js";
24
+ /** The four auth options shown on first run. */
25
+ const AUTH_OPTIONS = [
26
+ {
27
+ value: "oauth",
28
+ label: "Log in with a provider (recommended)",
29
+ description: "Opens your browser to authenticate with Claude, GitHub Copilot, OpenAI, etc.",
30
+ },
31
+ {
32
+ value: "api-key",
33
+ label: "Enter an API key",
34
+ description: "Manually paste an API key for any supported provider.",
35
+ },
36
+ {
37
+ value: "free",
38
+ label: "Use free models",
39
+ description: "Try free models from Opencode. No account required to start.",
40
+ },
41
+ {
42
+ value: "exit",
43
+ label: "Exit",
44
+ description: "Set up later and come back.",
45
+ },
46
+ ];
47
+ /**
48
+ * First-run authentication prompt.
49
+ *
50
+ * Renders a 4-option interactive select menu using arrow keys + Enter.
51
+ */
52
+ export function AuthPrompt({ onSelect }) {
53
+ const [selectedIndex, setSelectedIndex] = useState(0);
54
+ // Read through a ref inside useInput so a fast ENTER right after an
55
+ // arrow-key press sees the new index, not the closure's stale snapshot.
56
+ const selectedIndexRef = useRef(selectedIndex);
57
+ selectedIndexRef.current = selectedIndex;
58
+ useInput((input, key) => {
59
+ if (key.upArrow) {
60
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
61
+ }
62
+ else if (key.downArrow) {
63
+ setSelectedIndex((prev) => prev < AUTH_OPTIONS.length - 1 ? prev + 1 : prev);
64
+ }
65
+ else if (key.return) {
66
+ onSelect(AUTH_OPTIONS[selectedIndexRef.current].value);
67
+ }
68
+ });
69
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "No LLM provider configured. Choose how to get started:"] }) }), AUTH_OPTIONS.map((option, index) => {
70
+ const isSelected = index === selectedIndex;
71
+ const pointer = isSelected ? "\u276F" : " ";
72
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: _jsxs(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: [pointer, " ", option.label] }) }), _jsx(Text, { children: _jsxs(Text, { dimColor: true, children: [" ", option.description] }) }), index < AUTH_OPTIONS.length - 1 && _jsx(Text, { children: "" })] }, option.value));
73
+ })] }));
74
+ }
75
+ /**
76
+ * API key entry component.
77
+ *
78
+ * Two-step flow:
79
+ * 1. Select a provider from a list using arrow keys + Enter.
80
+ * 2. Type/paste the API key and press Enter to submit.
81
+ */
82
+ export function ApiKeyInput({ providers, onSubmit, }) {
83
+ const [phase, setPhase] = useState("select-provider");
84
+ const [selectedIndex, setSelectedIndex] = useState(0);
85
+ const [selectedProvider, setSelectedProvider] = useState("");
86
+ const [keyBuffer, setKeyBuffer] = useState("");
87
+ const selectedIndexRef = useRef(selectedIndex);
88
+ selectedIndexRef.current = selectedIndex;
89
+ useInput((input, key) => {
90
+ if (phase === "select-provider") {
91
+ if (key.upArrow) {
92
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
93
+ }
94
+ else if (key.downArrow) {
95
+ setSelectedIndex((prev) => prev < providers.length - 1 ? prev + 1 : prev);
96
+ }
97
+ else if (key.return && providers.length > 0) {
98
+ setSelectedProvider(providers[selectedIndexRef.current]);
99
+ setPhase("enter-key");
100
+ }
101
+ }
102
+ else if (phase === "enter-key") {
103
+ if (key.return) {
104
+ if (keyBuffer.length > 0) {
105
+ onSubmit(selectedProvider, keyBuffer);
106
+ }
107
+ }
108
+ else if (key.backspace || key.delete) {
109
+ setKeyBuffer((prev) => prev.slice(0, -1));
110
+ }
111
+ else if (!key.ctrl && !key.meta && input && !key.tab && !key.escape) {
112
+ setKeyBuffer((prev) => prev + input);
113
+ }
114
+ }
115
+ });
116
+ if (phase === "select-provider") {
117
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "Select a provider:"] }) }), providers.map((provider, index) => {
118
+ const isSelected = index === selectedIndex;
119
+ const pointer = isSelected ? "\u276F" : " ";
120
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: [pointer, " ", provider] }) }, provider));
121
+ })] }));
122
+ }
123
+ // Phase: enter-key
124
+ const maskedKey = keyBuffer.length > 0
125
+ ? keyBuffer.slice(0, 4) + "\u2022".repeat(Math.max(0, keyBuffer.length - 4))
126
+ : "";
127
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "Enter API key for", " ", _jsx(Text, { bold: true, color: "cyan", children: selectedProvider }), ":"] }) }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [" \u276F ", maskedKey, _jsx(Text, { dimColor: true, children: "_" })] }) }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "Press Enter to submit"] }) })] }));
128
+ }
129
+ /**
130
+ * OAuth provider selection component.
131
+ */
132
+ export function OAuthProviderSelect({ providers, onSelect, }) {
133
+ const [selectedIndex, setSelectedIndex] = useState(0);
134
+ const selectedIndexRef = useRef(selectedIndex);
135
+ selectedIndexRef.current = selectedIndex;
136
+ useInput((input, key) => {
137
+ if (key.upArrow) {
138
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
139
+ }
140
+ else if (key.downArrow) {
141
+ setSelectedIndex((prev) => prev < providers.length - 1 ? prev + 1 : prev);
142
+ }
143
+ else if (key.return && providers.length > 0) {
144
+ onSelect(providers[selectedIndexRef.current]);
145
+ }
146
+ });
147
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "Select a provider to log in with:"] }) }), providers.map((provider, index) => {
148
+ const isSelected = index === selectedIndex;
149
+ const pointer = isSelected ? "\u276F" : " ";
150
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: [pointer, " ", provider.name] }) }, provider.id));
151
+ }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "This will open your browser to complete authentication."] }) })] }));
152
+ }
153
+ /**
154
+ * OAuth waiting indicator component.
155
+ */
156
+ export function OAuthWaiting({ providerName, url, }) {
157
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "Opening browser to authenticate with", " ", _jsx(Text, { bold: true, color: "cyan", children: providerName }), "..."] }) }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "If your browser doesn't open, visit:"] }) }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "blue", children: [" ", url] }) }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "Waiting for authentication to complete..."] }) })] }));
158
+ }
159
+ /**
160
+ * Format the data retention annotation for a model.
161
+ * @internal
162
+ */
163
+ function retentionAnnotation(model) {
164
+ switch (model.dataRetention) {
165
+ case "zero-retention":
166
+ return { text: "Free \u00B7 Zero data retention \u2713", color: "green" };
167
+ case "may-train":
168
+ return {
169
+ text: "Free \u00B7 Data may be used for training \u26A0",
170
+ color: "yellow",
171
+ };
172
+ case "unknown":
173
+ default:
174
+ return { text: "Free \u00B7 Data retention unknown", color: undefined };
175
+ }
176
+ }
177
+ /**
178
+ * Free model selection component.
179
+ */
180
+ export function FreeModelPicker({ models, onSelect, }) {
181
+ const [selectedIndex, setSelectedIndex] = useState(0);
182
+ const selectedIndexRef = useRef(selectedIndex);
183
+ selectedIndexRef.current = selectedIndex;
184
+ useInput((input, key) => {
185
+ if (key.upArrow) {
186
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev));
187
+ }
188
+ else if (key.downArrow) {
189
+ setSelectedIndex((prev) => prev < models.length - 1 ? prev + 1 : prev);
190
+ }
191
+ else if (key.return && models.length > 0) {
192
+ onSelect(models[selectedIndexRef.current]);
193
+ }
194
+ });
195
+ // Find the longest model name for alignment.
196
+ const maxNameLen = models.reduce((max, m) => Math.max(max, m.name.length), 0);
197
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", "Available free models:"] }) }), models.map((model, index) => {
198
+ const isSelected = index === selectedIndex;
199
+ const pointer = isSelected ? "\u276F" : " ";
200
+ const annotation = retentionAnnotation(model);
201
+ const padding = " ".repeat(Math.max(1, maxNameLen - model.name.length + 4));
202
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: [pointer, " ", model.name] }), _jsx(Text, { children: padding }), _jsx(Text, { color: annotation.color, children: annotation.text })] }) }, model.id));
203
+ }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [" ", "\u26A0", " Models marked with ", "\u26A0", " may use your code for model training"] }) }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [" ", "during their free period. For proprietary code, choose a"] }) }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: [" ", "zero-retention model or log in with a paid provider."] }) }), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "Note: Free model availability is fetched live and may change."] }) })] }));
204
+ }
205
+ /**
206
+ * Interactive provider/model selection prompt for project configuration.
207
+ *
208
+ * Renders a list of provider/model options using arrow keys + Enter,
209
+ * following the same navigation pattern as AuthPrompt.
210
+ */
211
+ export function ConfigPrompt({ options, onSelect, title, maxVisible, }) {
212
+ const [navState, setNavState] = useState({ selectedIndex: 0, viewportStart: 0 });
213
+ const { selectedIndex, viewportStart } = navState;
214
+ const { stdout } = useStdout();
215
+ // Mirror selectedIndex into a ref so the ENTER branch below reads the latest
216
+ // value even when fired between an arrow-key press and the next render.
217
+ const selectedIndexRef = useRef(selectedIndex);
218
+ selectedIndexRef.current = selectedIndex;
219
+ // Fixed overhead: header(2) + footer(2) + up-indicator(1) + down-indicator(1) = 6
220
+ const terminalRows = stdout?.rows ?? 24;
221
+ const maxVis = maxVisible ?? Math.max(3, Math.floor((terminalRows - 6) / 2));
222
+ // Use functional updater form so rapid key presses don't read stale state.
223
+ useInput((input, key) => {
224
+ if (key.upArrow) {
225
+ setNavState((prev) => {
226
+ if (prev.selectedIndex <= 0)
227
+ return prev;
228
+ const next = prev.selectedIndex - 1;
229
+ const newViewport = next < prev.viewportStart ? next : prev.viewportStart;
230
+ return { selectedIndex: next, viewportStart: newViewport };
231
+ });
232
+ }
233
+ else if (key.downArrow) {
234
+ setNavState((prev) => {
235
+ if (prev.selectedIndex >= options.length - 1)
236
+ return prev;
237
+ const next = prev.selectedIndex + 1;
238
+ const newViewport = next >= prev.viewportStart + maxVis ? next - maxVis + 1 : prev.viewportStart;
239
+ return { selectedIndex: next, viewportStart: newViewport };
240
+ });
241
+ }
242
+ else if (key.return && options.length > 0) {
243
+ onSelect(options[selectedIndexRef.current].value);
244
+ }
245
+ });
246
+ const visibleOptions = options.slice(viewportStart, viewportStart + maxVis);
247
+ const hasAbove = viewportStart > 0;
248
+ const hasBelow = viewportStart + maxVis < options.length;
249
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: [" ", title ?? "Select a provider and model for this project:"] }) }), hasAbove && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "\u2191 ", viewportStart, " more above"] }) })), visibleOptions.map((option, i) => {
250
+ const index = viewportStart + i;
251
+ const isSelected = index === selectedIndex;
252
+ const pointer = isSelected ? "\u276F" : " ";
253
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsx(Text, { children: _jsxs(Text, { color: isSelected ? "cyan" : undefined, bold: isSelected, children: [pointer, " ", option.label] }) }), option.description && (_jsx(Text, { children: _jsxs(Text, { dimColor: true, children: [" ", option.description] }) })), i < visibleOptions.length - 1 && _jsx(Text, { children: "" })] }, `${option.value.providerID}/${option.value.modelID}`));
254
+ }), hasBelow && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "\u2193 ", options.length - viewportStart - maxVis, " more below"] }) })), _jsx(Box, { marginTop: 1, marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: [" ", "Use arrow keys to navigate, Enter to confirm."] }) })] }));
255
+ }
256
+ /** Ordered pipeline phases for display. */
257
+ const PIPELINE_PHASES = [
258
+ {
259
+ key: "discovery",
260
+ label: "discovery",
261
+ description: (s) => `Found ${s.totalFiles} files${s.binaryFilesSkipped > 0 ? ` (${s.binaryFilesSkipped} binary skipped)` : ""}`,
262
+ },
263
+ {
264
+ key: "grouping",
265
+ label: "grouping",
266
+ description: (s) => `${s.totalGroups} groups (${s.featureGroups} feature, ${s.layerGroups} layer)`,
267
+ },
268
+ {
269
+ key: "exploration",
270
+ label: "exploration",
271
+ description: (s) => {
272
+ const total = s.explorationStatuses.length;
273
+ if (total === 0)
274
+ return "Pending";
275
+ const completed = s.explorationStatuses.filter((g) => g.status === "completed").length;
276
+ const failed = s.explorationStatuses.filter((g) => g.status === "failed").length;
277
+ const active = s.explorationStatuses.filter((g) => g.status === "generating").length;
278
+ const hasSubAgents = s.explorationStatuses.some((g) => g.subAgentProgress && g.subAgentProgress.total > 1);
279
+ let base = `Exploring: ${completed}/${total} groups complete`;
280
+ if (hasSubAgents) {
281
+ let totalAgents = 0;
282
+ let completedAgents = 0;
283
+ for (const g of s.explorationStatuses) {
284
+ if (g.subAgentProgress) {
285
+ totalAgents += g.subAgentProgress.total;
286
+ completedAgents += g.subAgentProgress.completed;
287
+ }
288
+ }
289
+ base += `, ${completedAgents}/${totalAgents} agents`;
290
+ }
291
+ if (failed > 0)
292
+ return `${base} (${failed} failed)`;
293
+ if (active > 0)
294
+ return `${base} (${active} active)`;
295
+ return base;
296
+ },
297
+ },
298
+ {
299
+ key: "generation",
300
+ label: "generation",
301
+ description: (s) => {
302
+ const activeCount = s.groupStatuses.filter((g) => g.status === "generating").length;
303
+ const hasSubAgents = s.groupStatuses.some((g) => g.subAgentProgress && g.subAgentProgress.total > 1);
304
+ let base = `${s.completedSessions}/${s.totalPages} complete`;
305
+ if (hasSubAgents) {
306
+ let totalAgents = 0;
307
+ let completedAgents = 0;
308
+ for (const g of s.groupStatuses) {
309
+ if (g.subAgentProgress) {
310
+ totalAgents += g.subAgentProgress.total;
311
+ completedAgents += g.subAgentProgress.completed;
312
+ }
313
+ }
314
+ base += `, ${completedAgents}/${totalAgents} agents`;
315
+ }
316
+ return activeCount > 0 ? `${base} (${activeCount} active)` : base;
317
+ },
318
+ },
319
+ {
320
+ key: "organization",
321
+ label: "organization",
322
+ description: () => "Reviewing and reorganizing documentation",
323
+ },
324
+ {
325
+ key: "crossref",
326
+ label: "cross-references",
327
+ description: () => "Adding inter-document links",
328
+ },
329
+ {
330
+ key: "architecture",
331
+ label: "architecture",
332
+ description: () => "Generating architecture overview",
333
+ },
334
+ {
335
+ key: "indexing",
336
+ label: "indexing",
337
+ description: (s) => s.indexFiles.length > 0 ? s.indexFiles.join(", ") : "Pending",
338
+ },
339
+ ];
340
+ /** The ordered phase keys for comparison. */
341
+ const PHASE_ORDER = PIPELINE_PHASES.map((p) => p.key);
342
+ /**
343
+ * Get the numeric index of a phase in the pipeline order.
344
+ * @internal
345
+ */
346
+ function phaseIndex(phase) {
347
+ const idx = PHASE_ORDER.indexOf(phase);
348
+ return idx >= 0 ? idx : PHASE_ORDER.length;
349
+ }
350
+ /**
351
+ * Header component.
352
+ */
353
+ export function Header({ modelLabel, providerLabel }) {
354
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Hem v1.0.0" }), _jsxs(Text, { children: [" ", "\u00B7", " "] }), _jsx(Text, { bold: true, children: providerLabel }), _jsxs(Text, { dimColor: true, children: [" / ", modelLabel] })] }));
355
+ }
356
+ /**
357
+ * Renders a single pipeline phase row with status indicator.
358
+ */
359
+ export function PhaseRow({ label, description, status, }) {
360
+ return (_jsxs(Box, { children: [_jsxs(Text, { children: [_jsxs(Text, { color: status === "active" ? "cyan" : status === "done" ? "green" : undefined, children: ["[", label, "]"] }), " ", description, " "] }), status === "active" && (_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) })), status === "done" && _jsx(Text, { color: "green", children: "\u2713" }), status === "pending" && _jsx(Text, { dimColor: true, children: "Pending" })] }));
361
+ }
362
+ /**
363
+ * Renders a single file group row within the generation dashboard.
364
+ *
365
+ * Uses `groupId` as the display identifier and `label` as the description.
366
+ */
367
+ export function GroupRow({ group }) {
368
+ switch (group.status) {
369
+ case "generating":
370
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", _jsx(Text, { children: group.groupId }), " ", _jsx(Text, { dimColor: true, children: group.subAgentProgress && group.subAgentProgress.total > 1
371
+ ? `Generating... (${group.subAgentProgress.completed}/${group.subAgentProgress.total} agents)`
372
+ : "Generating..." })] }) }));
373
+ case "completed":
374
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: "green", children: "\u2713" }), " ", _jsx(Text, { children: group.groupId }), " ", _jsxs(Text, { dimColor: true, children: [group.label, group.subAgentProgress && group.subAgentProgress.total > 1
375
+ ? ` (${group.subAgentProgress.total} agents)`
376
+ : ""] })] }) }));
377
+ case "failed":
378
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "\u2717" }), " ", _jsx(Text, { children: group.groupId }), " ", _jsx(Text, { color: "red", children: "Failed" })] }) }), group.error && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { dimColor: true, children: group.error }) }))] }));
379
+ case "queued":
380
+ default:
381
+ return (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u00B7" }), " ", _jsx(Text, { dimColor: true, children: group.groupId }), " ", _jsx(Text, { dimColor: true, children: group.subAgentProgress && group.subAgentProgress.total > 1
382
+ ? `Queued (${group.subAgentProgress.total} agents)`
383
+ : "Queued" })] }) }));
384
+ }
385
+ }
386
+ /**
387
+ * Renders the exploration phase dashboard showing per-group exploration progress.
388
+ */
389
+ export function ExplorationDashboard({ explorationStatuses, }) {
390
+ const hasSubAgents = explorationStatuses.some((g) => g.subAgentProgress && g.subAgentProgress.total > 1);
391
+ let subAgentSummary;
392
+ if (hasSubAgents) {
393
+ let totalAgents = 0;
394
+ let completedAgents = 0;
395
+ for (const g of explorationStatuses) {
396
+ if (g.subAgentProgress) {
397
+ totalAgents += g.subAgentProgress.total;
398
+ completedAgents += g.subAgentProgress.completed;
399
+ }
400
+ }
401
+ subAgentSummary = `${completedAgents}/${totalAgents} sub-agents complete`;
402
+ }
403
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [subAgentSummary && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: subAgentSummary }) })), explorationStatuses.map((group) => (_jsx(GroupRow, { group: group }, group.groupId)))] }));
404
+ }
405
+ /**
406
+ * Renders the generation phase dashboard showing progress for each group.
407
+ */
408
+ export function GenerationDashboard({ groupStatuses, completedSessions, totalPages, }) {
409
+ const hasSubAgents = groupStatuses.some((g) => g.subAgentProgress && g.subAgentProgress.total > 1);
410
+ let subAgentSummary;
411
+ if (hasSubAgents) {
412
+ let totalAgents = 0;
413
+ let completedAgents = 0;
414
+ for (const g of groupStatuses) {
415
+ if (g.subAgentProgress) {
416
+ totalAgents += g.subAgentProgress.total;
417
+ completedAgents += g.subAgentProgress.completed;
418
+ }
419
+ }
420
+ subAgentSummary = `${completedAgents}/${totalAgents} sub-agents complete`;
421
+ }
422
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [subAgentSummary && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: subAgentSummary }) })), groupStatuses.map((group) => (_jsx(GroupRow, { group: group }, group.groupId)))] }));
423
+ }
424
+ /**
425
+ * Renders the final summary after pipeline completion.
426
+ */
427
+ export function Summary({ totalPages, failedSessions, startedAt, completedAt, warnings, }) {
428
+ const elapsed = completedAt
429
+ ? formatElapsed(startedAt, completedAt)
430
+ : "...";
431
+ const successCount = totalPages - failedSessions;
432
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [failedSessions === 0 ? (_jsxs(Text, { color: "green", children: ["\u2713", " Documentation generated (", successCount, " pages, ", elapsed, ")"] })) : (_jsxs(Text, { color: "yellow", children: ["\u26A0", " Documentation generated with errors (", successCount, "/", totalPages, " pages, ", elapsed, ")"] })), warnings.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: warnings.map((warning, index) => (_jsxs(Text, { dimColor: true, children: ["Warnings: ", warning] }, index))) }))] }));
433
+ }
434
+ /**
435
+ * Root dashboard component.
436
+ */
437
+ export function App({ initialState, onStateRef }) {
438
+ const [state, setState] = useState(initialState);
439
+ const setterRef = useCallback((partial) => {
440
+ setState((prev) => ({ ...prev, ...partial }));
441
+ }, []);
442
+ React.useEffect(() => {
443
+ onStateRef(setterRef);
444
+ }, [onStateRef, setterRef]);
445
+ const currentPhaseIdx = phaseIndex(state.phase);
446
+ const isComplete = state.phase === "complete";
447
+ const isError = state.phase === "error";
448
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { modelLabel: state.modelLabel, providerLabel: state.providerLabel }), PIPELINE_PHASES.map((phase) => {
449
+ const idx = phaseIndex(phase.key);
450
+ let status;
451
+ if (isComplete || isError) {
452
+ status = idx <= currentPhaseIdx ? "done" : "pending";
453
+ if (phase.key === "generation" && state.totalPages === 0 && !isComplete) {
454
+ status = "pending";
455
+ }
456
+ }
457
+ else if (idx < currentPhaseIdx) {
458
+ status = "done";
459
+ }
460
+ else if (idx === currentPhaseIdx) {
461
+ status = "active";
462
+ }
463
+ else {
464
+ status = "pending";
465
+ }
466
+ // Override exploration phase status based on the explicit completion flag
467
+ // rather than phase-index comparison, because the streaming pipeline may
468
+ // advance to "generation" while exploration is still running.
469
+ if (phase.key === "exploration") {
470
+ if (state.explorationComplete) {
471
+ status = "done";
472
+ }
473
+ else if (state.explorationStatuses.length > 0) {
474
+ status = "active";
475
+ }
476
+ }
477
+ return (_jsx(PhaseRow, { label: phase.label, description: phase.description(state), status: status }, phase.key));
478
+ }), !state.explorationComplete && state.explorationStatuses.length > 0 && (_jsx(ExplorationDashboard, { explorationStatuses: state.explorationStatuses })), state.phase === "generation" && state.groupStatuses.length > 0 && (_jsx(GenerationDashboard, { groupStatuses: state.groupStatuses, completedSessions: state.completedSessions, totalPages: state.totalPages })), (isComplete || isError) && (_jsx(Summary, { totalPages: state.totalPages, failedSessions: state.failedSessions, startedAt: state.startedAt, completedAt: state.completedAt, warnings: state.warnings }))] }));
479
+ }
480
+ // ── LogDashboard ─────────────────────────────────────────────────────
481
+ /**
482
+ * Non-interactive log-style dashboard for piped output and CI environments.
483
+ */
484
+ export class LogDashboard {
485
+ state;
486
+ log;
487
+ verbose;
488
+ loggedGroupStarts = new Set();
489
+ loggedGroupCompletions = new Set();
490
+ loggedExplorationStarts = new Set();
491
+ loggedExplorationCompletions = new Set();
492
+ constructor(initialState, log = console.log, verbose = false) {
493
+ this.state = { ...initialState };
494
+ this.log = log;
495
+ this.verbose = verbose;
496
+ this.log(`[info] Provider: ${this.state.providerLabel}, Model: ${this.state.modelLabel}`);
497
+ }
498
+ updateState(partial) {
499
+ const prevPhase = this.state.phase;
500
+ const prevGroupStatuses = this.state.groupStatuses;
501
+ const prevExplorationStatuses = this.state.explorationStatuses;
502
+ this.state = { ...this.state, ...partial };
503
+ if (partial.phase !== undefined && partial.phase !== prevPhase) {
504
+ this.logPhaseTransition(prevPhase);
505
+ }
506
+ if (partial.groupStatuses !== undefined) {
507
+ this.logGroupChanges(prevGroupStatuses);
508
+ }
509
+ if (partial.explorationStatuses !== undefined) {
510
+ this.logExplorationChanges(prevExplorationStatuses);
511
+ }
512
+ }
513
+ logPhaseTransition(prevPhase) {
514
+ const s = this.state;
515
+ if (!this.verbose) {
516
+ switch (prevPhase) {
517
+ case "discovery":
518
+ this.log(`[discovery] Found ${s.totalFiles} files` +
519
+ (s.binaryFilesSkipped > 0
520
+ ? ` (${s.binaryFilesSkipped} binary skipped)`
521
+ : ""));
522
+ break;
523
+ case "grouping":
524
+ this.log(`[grouping] ${s.totalGroups} groups (${s.featureGroups} feature, ${s.layerGroups} layer)`);
525
+ break;
526
+ case "exploration": {
527
+ const total = s.explorationStatuses.length;
528
+ const completed = s.explorationStatuses.filter((g) => g.status === "completed").length;
529
+ const failed = s.explorationStatuses.filter((g) => g.status === "failed").length;
530
+ if (total > 0) {
531
+ const base = `[exploration] ${completed}/${total} groups explored`;
532
+ this.log(failed > 0 ? `${base} (${failed} failed)` : base);
533
+ }
534
+ break;
535
+ }
536
+ case "indexing":
537
+ if (s.indexFiles.length > 0) {
538
+ this.log(`[indexing] Generated ${s.indexFiles.join(", ")}`);
539
+ }
540
+ break;
541
+ }
542
+ }
543
+ if (s.phase === "complete" || s.phase === "error") {
544
+ const elapsed = s.completedAt
545
+ ? formatElapsed(s.startedAt, s.completedAt)
546
+ : "...";
547
+ const successCount = s.totalPages - s.failedSessions;
548
+ this.log(`Done: ${successCount} pages generated in ${elapsed}`);
549
+ }
550
+ }
551
+ logGroupChanges(_prevStatuses) {
552
+ for (const group of this.state.groupStatuses) {
553
+ if (group.status === "generating" &&
554
+ !this.loggedGroupStarts.has(group.groupId)) {
555
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
556
+ ? ` (${group.subAgentProgress.total} agents)`
557
+ : "";
558
+ this.log(`[generation] Starting ${group.label}...${agentInfo}`);
559
+ this.loggedGroupStarts.add(group.groupId);
560
+ }
561
+ if (group.status === "completed" &&
562
+ !this.loggedGroupCompletions.has(group.groupId)) {
563
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
564
+ ? ` (${group.subAgentProgress.completed}/${group.subAgentProgress.total} agents)`
565
+ : "";
566
+ this.log(`[generation] Completed ${group.label}${agentInfo}`);
567
+ this.loggedGroupCompletions.add(group.groupId);
568
+ }
569
+ if (group.status === "failed" &&
570
+ !this.loggedGroupCompletions.has(group.groupId)) {
571
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
572
+ ? ` (${group.subAgentProgress.completed}/${group.subAgentProgress.total} agents)`
573
+ : "";
574
+ this.log(`[generation] Failed ${group.label}${agentInfo}`);
575
+ this.loggedGroupCompletions.add(group.groupId);
576
+ }
577
+ }
578
+ }
579
+ logExplorationChanges(_prevStatuses) {
580
+ for (const group of this.state.explorationStatuses) {
581
+ if (group.status === "generating" &&
582
+ !this.loggedExplorationStarts.has(group.groupId)) {
583
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
584
+ ? ` (${group.subAgentProgress.total} agents)`
585
+ : "";
586
+ this.log(`[exploration] Starting ${group.label}...${agentInfo}`);
587
+ this.loggedExplorationStarts.add(group.groupId);
588
+ }
589
+ if (group.status === "completed" &&
590
+ !this.loggedExplorationCompletions.has(group.groupId)) {
591
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
592
+ ? ` (${group.subAgentProgress.completed}/${group.subAgentProgress.total} agents)`
593
+ : "";
594
+ this.log(`[exploration] Completed ${group.label}${agentInfo}`);
595
+ this.loggedExplorationCompletions.add(group.groupId);
596
+ }
597
+ if (group.status === "failed" &&
598
+ !this.loggedExplorationCompletions.has(group.groupId)) {
599
+ const agentInfo = group.subAgentProgress && group.subAgentProgress.total > 1
600
+ ? ` (${group.subAgentProgress.completed}/${group.subAgentProgress.total} agents)`
601
+ : "";
602
+ this.log(group.error
603
+ ? `[exploration] Failed ${group.label}${agentInfo}: ${group.error}`
604
+ : `[exploration] Failed ${group.label}${agentInfo}`);
605
+ this.loggedExplorationCompletions.add(group.groupId);
606
+ }
607
+ }
608
+ }
609
+ waitUntilExit() {
610
+ return Promise.resolve();
611
+ }
612
+ }
613
+ /**
614
+ * Creates the progress dashboard and returns a state updater function
615
+ * and a `waitUntilExit` promise.
616
+ */
617
+ export function renderDashboard(initialState, verbose) {
618
+ if (verbose || !process.stdout.isTTY) {
619
+ const logDashboard = new LogDashboard(initialState, console.log, verbose ?? false);
620
+ return {
621
+ updateState: (partial) => logDashboard.updateState(partial),
622
+ waitUntilExit: () => logDashboard.waitUntilExit(),
623
+ };
624
+ }
625
+ let stateUpdater;
626
+ const instance = inkRender(_jsx(App, { initialState: initialState, onStateRef: (setter) => {
627
+ stateUpdater = setter;
628
+ } }));
629
+ return {
630
+ updateState: (partial) => {
631
+ if (stateUpdater) {
632
+ stateUpdater(partial);
633
+ }
634
+ },
635
+ waitUntilExit: async () => {
636
+ // Allow React to commit the final state update and Ink to render
637
+ // the completed frame (spinners → checkmarks) before tearing down.
638
+ // Ink throttles renders to ~16ms, so 32ms covers two frames.
639
+ await new Promise((resolve) => setTimeout(resolve, 32));
640
+ instance.unmount();
641
+ return instance.waitUntilExit();
642
+ },
643
+ };
644
+ }