@oevery/rsp 3.2.0 → 3.3.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.
Files changed (37) hide show
  1. package/README.md +4 -3
  2. package/README.zh-CN.md +3 -2
  3. package/dist/{chunk-SEGJLWRE.mjs → chunk-7HXQNBIG.mjs} +447 -734
  4. package/dist/{chunk-JYDEJEM4.mjs → chunk-QM2UVPQK.mjs} +47 -30
  5. package/dist/{chunk-ZORUJTVS.mjs → chunk-TSUDEX5Y.mjs} +1163 -104
  6. package/dist/cli.mjs +5332 -3047
  7. package/dist/entry-GIDNR7CJ.mjs +1846 -0
  8. package/dist/{entry-RG6HREUN.mjs → entry-YAW3PYFA.mjs} +2 -2
  9. package/package.json +12 -7
  10. package/rules/rsp-rules.md +6 -5
  11. package/skills/rsp/SKILL.md +26 -57
  12. package/skills/rsp/references/control-outcome.md +31 -0
  13. package/skills/rsp/references/durable-review.md +18 -1
  14. package/skills/rsp/references/focus-continuation.md +9 -0
  15. package/skills/rsp/references/implementation-evidence.md +12 -0
  16. package/skills/rsp/references/managed-routing.md +9 -8
  17. package/skills/rsp/references/response-language.md +11 -0
  18. package/skills/rsp/references/setup-repair.md +3 -3
  19. package/skills/rsp-commit/SKILL.md +24 -9
  20. package/skills/rsp-design/SKILL.md +1 -1
  21. package/skills/rsp-diagnose/SKILL.md +3 -1
  22. package/skills/rsp-implement/SKILL.md +15 -5
  23. package/skills/rsp-manage/SKILL.md +56 -51
  24. package/skills/rsp-manage/references/closeout.md +3 -1
  25. package/skills/rsp-manage/references/delegation.md +47 -0
  26. package/skills/rsp-manage/references/interruption-recovery.md +15 -3
  27. package/skills/rsp-manage/references/review-convergence.md +3 -1
  28. package/skills/rsp-release-docs/SKILL.md +13 -13
  29. package/skills/rsp-release-docs/references/publication-lifecycle.md +4 -0
  30. package/skills/rsp-resolve-findings/SKILL.md +2 -2
  31. package/skills/rsp-review/SKILL.md +9 -12
  32. package/skills/rsp-review/references/code-review.md +3 -2
  33. package/skills/rsp-shape/SKILL.md +1 -1
  34. package/skills/rsp-tdd/SKILL.md +10 -2
  35. package/skills/rsp-verify/SKILL.md +40 -0
  36. package/dist/entry-I22OUIW5.mjs +0 -888
  37. package/skills/rsp/references/release-operations.md +0 -9
@@ -0,0 +1,1846 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ArchiveHistoryError,
4
+ SPECS_DEFAULT_RESULT_LIMIT,
5
+ SPECS_SEARCH_EXCERPT_CODE_POINTS,
6
+ WorkRefError,
7
+ historyInspectionComplete,
8
+ inspectArchiveHistory,
9
+ inspectProjectStatus,
10
+ inspectSpecs,
11
+ normalizeLogicalPath,
12
+ projectDependencyForest,
13
+ projectSpecsDetail,
14
+ projectSpecsSearch,
15
+ projectSpecsTree,
16
+ queryArchiveHistory,
17
+ readArchiveHistoryDetail,
18
+ readArchiveHistoryDocument,
19
+ resolveWorkRefPath
20
+ } from "./chunk-TSUDEX5Y.mjs";
21
+ import {
22
+ displayWidth,
23
+ openTerminalSession,
24
+ truncateDisplay,
25
+ wrapDisplay
26
+ } from "./chunk-JOMFFQRG.mjs";
27
+ import {
28
+ CHANGES_DIR
29
+ } from "./chunk-7HXQNBIG.mjs";
30
+
31
+ // src/tui/entry.tsx
32
+ import process from "process";
33
+ import { render } from "ink";
34
+ import React from "react";
35
+
36
+ // src/tui/app.tsx
37
+ import { Box as Box3, Text as Text3, useApp, useInput, useStdout } from "ink";
38
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
39
+
40
+ // src/tui/history-display.ts
41
+ function formatHistoryRow(record, width) {
42
+ const boundedWidth = Math.max(1, width);
43
+ if (boundedWidth < 24)
44
+ return truncateDisplay(`${record.date} ${record.workRef} [${record.kind}] ${record.summary}`, boundedWidth);
45
+ const date = truncateDisplay(record.date, Math.min(10, boundedWidth));
46
+ const remaining = Math.max(3, boundedWidth - displayWidth(date) - 4);
47
+ const workRefWidth = Math.ceil(remaining / 3);
48
+ const kindWidth = Math.ceil((remaining - workRefWidth) / 2);
49
+ const summaryWidth = Math.max(1, remaining - workRefWidth - kindWidth);
50
+ const kind = truncateDisplay(record.kind, Math.max(1, kindWidth - 2));
51
+ const summary = `${record.summary}${record.summaryTruncated ? "\u2026" : ""}`;
52
+ return `${date} ${truncateDisplay(record.workRef, workRefWidth)} [${kind}] ${truncateDisplay(summary, summaryWidth)}`;
53
+ }
54
+ function formatHistoryField(label, value, width) {
55
+ return truncateDisplay(`${label}: ${value}`, Math.max(1, width));
56
+ }
57
+ function formatHistoryDiagnostic(label, diagnostic, width) {
58
+ return truncateDisplay(`${label} ${diagnostic}`, Math.max(1, width));
59
+ }
60
+ var CHECKBOX_MARKERS = {
61
+ "x": "\u2713",
62
+ " ": "\u25CB",
63
+ "/": "\u25D0",
64
+ "-": "\u2212"
65
+ };
66
+ function projectEvidenceItem(value) {
67
+ const trimmed = value.trimStart();
68
+ const checkboxPrefix = trimmed.match(/^(?:[-*][ \t]+)?\[([x /-])\][ \t]*/i);
69
+ if (checkboxPrefix) {
70
+ const state = checkboxPrefix[1].toLowerCase();
71
+ return { marker: CHECKBOX_MARKERS[state], text: trimmed.slice(checkboxPrefix[0].length) };
72
+ }
73
+ const bulletPrefix = trimmed.match(/^[-*][ \t]+/);
74
+ return bulletPrefix ? { marker: "\u2022", text: trimmed.slice(bulletPrefix[0].length) } : { marker: "\u2022", text: value };
75
+ }
76
+ function formatHistoryEvidenceLines(items, none, width) {
77
+ if (items.length === 0)
78
+ return [truncateDisplay(` ${none}`, Math.max(1, width))];
79
+ return items.flatMap((value) => {
80
+ const item = projectEvidenceItem(value);
81
+ const prefix = ` ${item.marker} `;
82
+ return wrapDisplay(item.text, width, prefix, " ".repeat(displayWidth(prefix)));
83
+ });
84
+ }
85
+ function appendHistoryTruncationMarker(line, marker, width) {
86
+ const suffix = ` ${marker}`;
87
+ const contentWidth = Math.max(1, width - displayWidth(suffix));
88
+ return `${truncateDisplay(line, contentWidth)}${suffix}`;
89
+ }
90
+
91
+ // src/tui/history-detail.ts
92
+ var EVIDENCE_KEYS = ["tasks", "verify", "blockers"];
93
+ var HISTORY_DETAIL_FIXED_ROWS = 9;
94
+ var HISTORY_EVIDENCE_MIN_ROWS = EVIDENCE_KEYS.length * 2;
95
+ var HISTORY_DETAIL_MIN_HEIGHT = 12;
96
+ function projectHistoryDetailLines(detail, messages, width) {
97
+ const lines = [
98
+ { spans: [{ text: `${messages.summary}: ${detail.summary}${detail.summaryTruncated ? "\u2026" : ""}` }] },
99
+ { spans: [{ text: `${messages.scenarios}: ${detail.scenarioCount}` }] }
100
+ ];
101
+ for (const key of EVIDENCE_KEYS) {
102
+ const heading = key === "tasks" ? `${messages.tasks}: ${detail.checkboxes.tasks.done}/${detail.checkboxes.tasks.total}` : key === "verify" ? `${messages.verify}: ${detail.checkboxes.verify.done}/${detail.checkboxes.verify.total}` : `${messages.blockers}:`;
103
+ lines.push({ spans: [{ text: heading, bold: true }] });
104
+ const evidence = detail.evidence[key];
105
+ const values = formatHistoryEvidenceLines(evidence.items, messages.none, width);
106
+ if (evidence.truncated && values.length)
107
+ values[values.length - 1] = appendHistoryTruncationMarker(values.at(-1), messages.truncated, width);
108
+ lines.push(...values.map((text) => ({ spans: [{ text }] })));
109
+ }
110
+ return lines;
111
+ }
112
+ function projectHistoryEvidence(evidence, options) {
113
+ const { dynamicRows = 0, headings, height, none, truncationMarker, width } = options;
114
+ const rowBudget = Math.max(0, height - HISTORY_DETAIL_FIXED_ROWS - dynamicRows);
115
+ if (rowBudget < HISTORY_EVIDENCE_MIN_ROWS)
116
+ return [];
117
+ const formatted = new Map(EVIDENCE_KEYS.map((key) => [key, formatHistoryEvidenceLines(evidence[key].items, none, width)]));
118
+ const allocations = new Map(EVIDENCE_KEYS.map((key) => [key, 1]));
119
+ let remaining = rowBudget - HISTORY_EVIDENCE_MIN_ROWS;
120
+ let cursor = 0;
121
+ while (remaining > 0) {
122
+ const key = Array.from({ length: EVIDENCE_KEYS.length }, (_, offset) => EVIDENCE_KEYS[(cursor + offset) % EVIDENCE_KEYS.length]).find((candidate) => (allocations.get(candidate) ?? 1) < (formatted.get(candidate)?.length ?? 1));
123
+ if (!key)
124
+ break;
125
+ allocations.set(key, (allocations.get(key) ?? 1) + 1);
126
+ cursor = (EVIDENCE_KEYS.indexOf(key) + 1) % EVIDENCE_KEYS.length;
127
+ remaining -= 1;
128
+ }
129
+ return EVIDENCE_KEYS.map((key) => {
130
+ const value = evidence[key];
131
+ const allLines = formatted.get(key) ?? [];
132
+ const lines = allLines.slice(0, allocations.get(key));
133
+ const truncated = value.truncated || lines.length < allLines.length;
134
+ if (truncated && lines.length > 0)
135
+ lines[lines.length - 1] = appendHistoryTruncationMarker(lines[lines.length - 1], truncationMarker, width);
136
+ return { key, heading: headings[key], lines, truncated };
137
+ });
138
+ }
139
+
140
+ // src/tui/markdown-presentation.ts
141
+ import { fromMarkdown } from "mdast-util-from-markdown";
142
+ import { gfmFromMarkdown } from "mdast-util-gfm";
143
+ import { gfm } from "micromark-extension-gfm";
144
+ var EMPTY_LINE = { spans: [{ text: "" }] };
145
+ var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
146
+ function markdownLineText(line) {
147
+ return line.spans.map((span) => span.text).join("");
148
+ }
149
+ function projectMarkdownLines(content, width) {
150
+ const root = fromMarkdown(sanitizeMarkdown(stripFrontmatter(content)), { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
151
+ const logical = [];
152
+ root.children.forEach((node, index) => {
153
+ if (index > 0 && logical.length > 0 && markdownLineText(logical.at(-1)) !== "")
154
+ logical.push(EMPTY_LINE);
155
+ logical.push(...projectBlock(node, 0, Math.max(1, width)));
156
+ });
157
+ if (logical.length === 0)
158
+ logical.push(EMPTY_LINE);
159
+ return logical.flatMap((line) => wrapStyledLine(line, Math.max(1, width)));
160
+ }
161
+ function projectMarkdownLineViewport(lines, options) {
162
+ const capacity = Math.max(0, options.height);
163
+ const maxStart = Math.max(0, lines.length - capacity);
164
+ const start = Math.min(Math.max(0, options.offset ?? 0), maxStart);
165
+ const visible = lines.slice(start, start + capacity);
166
+ const end = start + visible.length;
167
+ return {
168
+ lines: visible,
169
+ start,
170
+ end,
171
+ total: lines.length,
172
+ hasPrevious: start > 0,
173
+ hasNext: end < lines.length
174
+ };
175
+ }
176
+ function stripFrontmatter(content) {
177
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)\r?\n/u, "");
178
+ }
179
+ function sanitizeMarkdown(value) {
180
+ let result = "";
181
+ for (let index = 0; index < value.length; index++) {
182
+ const code = value.charCodeAt(index);
183
+ if (code === 27 && value[index + 1] === "[") {
184
+ index += 2;
185
+ while (index < value.length) {
186
+ const sequenceCode = value.charCodeAt(index);
187
+ if (sequenceCode >= 64 && sequenceCode <= 126)
188
+ break;
189
+ index++;
190
+ }
191
+ continue;
192
+ }
193
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127)
194
+ continue;
195
+ result += code === 9 ? " " : value[index];
196
+ }
197
+ return result;
198
+ }
199
+ function projectBlock(node, depth, width) {
200
+ switch (node.type) {
201
+ case "heading": {
202
+ const marker = node.depth === 1 ? "\u25C6 " : node.depth === 2 ? "\u25C7 " : "\u2022 ";
203
+ return inlineLines(node.children, { bold: true, color: "cyan" }, marker);
204
+ }
205
+ case "paragraph":
206
+ return inlineLines(node.children);
207
+ case "blockquote":
208
+ return node.children.flatMap((child, index) => {
209
+ const lines = projectBlock(child, depth, width);
210
+ if (index > 0)
211
+ lines.unshift(EMPTY_LINE);
212
+ return lines.map((line) => prependLine(line, "\u2502 ", { dim: true, italic: true }));
213
+ });
214
+ case "code":
215
+ return (node.value || "").split("\n").map((value) => ({
216
+ spans: [{ text: ` ${value}`, code: true, color: "yellow" }]
217
+ }));
218
+ case "thematicBreak":
219
+ return [{ spans: [{ text: "\u2500".repeat(12), dim: true, color: "gray" }] }];
220
+ case "list":
221
+ return projectList(node, depth, width);
222
+ case "table":
223
+ return projectTable(node, width);
224
+ case "html":
225
+ return [metavariableLine(node.value) ?? { spans: [{ text: node.value, dim: true }] }];
226
+ default:
227
+ return "children" in node ? inlineLines(node.children.filter((child) => isPhrasing(child))) : [];
228
+ }
229
+ }
230
+ function projectList(list, depth, width) {
231
+ return list.children.flatMap((item, index) => projectListItem(item, depth, list.ordered ? (list.start ?? 1) + index : null, width));
232
+ }
233
+ function projectListItem(item, depth, ordinal, width) {
234
+ const indent = " ".repeat(depth);
235
+ const childLines = item.children.flatMap((child, index) => {
236
+ const lines = projectBlock(child, depth + 1, width);
237
+ if (index > 0 && child.type !== "list")
238
+ lines.unshift(EMPTY_LINE);
239
+ return lines;
240
+ });
241
+ const first = childLines[0] ?? EMPTY_LINE;
242
+ const parsedTask = taskMarker(first);
243
+ const task = typeof item.checked === "boolean" ? { marker: item.checked ? "\u2611 " : "\u2610 ", line: first } : parsedTask;
244
+ const marker = task?.marker ?? (ordinal === null ? "\u2022 " : `${ordinal}. `);
245
+ const firstLine = prependLine(task?.line ?? first, `${indent}${marker}`, { color: task ? "cyan" : void 0 });
246
+ return [firstLine, ...childLines.slice(1).map((line) => prependLine(line, `${indent} `))];
247
+ }
248
+ function taskMarker(line) {
249
+ const text = markdownLineText(line);
250
+ const match = text.match(/^\[([ x])\]\s+/iu);
251
+ if (!match)
252
+ return null;
253
+ return {
254
+ marker: match[1]?.toLowerCase() === "x" ? "\u2611 " : "\u2610 ",
255
+ line: sliceLine(line, match[0].length)
256
+ };
257
+ }
258
+ function inlineLines(children, style = {}, prefix = "") {
259
+ const spans = children.flatMap((child) => projectInline(child, style));
260
+ const lines = splitSpansAtNewlines(spans);
261
+ if (prefix && lines.length > 0)
262
+ lines[0] = prependLine(lines[0], prefix, style);
263
+ return lines;
264
+ }
265
+ function projectInline(node, inherited) {
266
+ switch (node.type) {
267
+ case "text":
268
+ return [{ text: node.value, ...inherited }];
269
+ case "html":
270
+ return metavariableSpan(node.value, inherited) ?? [{ text: node.value, ...inherited, dim: true }];
271
+ case "inlineCode":
272
+ return [{ text: node.value, ...inherited, code: true, color: "cyan" }];
273
+ case "break":
274
+ return [{ text: "\n", ...inherited }];
275
+ case "strong":
276
+ return node.children.flatMap((child) => projectInline(child, { ...inherited, bold: true }));
277
+ case "emphasis":
278
+ return node.children.flatMap((child) => projectInline(child, { ...inherited, italic: true }));
279
+ case "delete":
280
+ return node.children.flatMap((child) => projectInline(child, { ...inherited, dim: true }));
281
+ case "link":
282
+ return [
283
+ ...node.children.flatMap((child) => projectInline(child, { ...inherited, underline: true, color: "cyan" })),
284
+ { text: ` (${node.url})`, ...inherited, dim: true }
285
+ ];
286
+ case "image":
287
+ return [{ text: node.alt ? `[image: ${node.alt}]` : "[image]", ...inherited, dim: true }];
288
+ default:
289
+ return "children" in node ? node.children.flatMap((child) => projectInline(child, inherited)) : [];
290
+ }
291
+ }
292
+ function projectTable(node, width) {
293
+ const rows = node.children.map((row) => row.children.map((cell) => markdownLineText(inlineLines(cell.children)[0] ?? EMPTY_LINE)));
294
+ const headers = rows[0] ?? [];
295
+ const values = rows.slice(1);
296
+ const columnWidths = headers.map((header, index) => Math.max(displayWidth(header), ...values.map((row) => displayWidth(row[index] ?? ""))));
297
+ const wideWidth = columnWidths.reduce((sum, value) => sum + value, 0) + Math.max(0, columnWidths.length - 1) * 3;
298
+ if (wideWidth <= width) {
299
+ return rows.map((row, rowIndex) => ({
300
+ spans: row.flatMap((value, index) => {
301
+ const padded = value + " ".repeat(Math.max(0, columnWidths[index] - displayWidth(value)));
302
+ return [
303
+ { text: padded, bold: rowIndex === 0 },
304
+ ...index < row.length - 1 ? [{ text: " \u2502 ", dim: true }] : []
305
+ ];
306
+ })
307
+ }));
308
+ }
309
+ return values.flatMap((row, rowIndex) => [
310
+ ...rowIndex > 0 ? [EMPTY_LINE] : [],
311
+ ...headers.map((header, index) => ({ spans: [{ text: `${header}: `, bold: true }, { text: row[index] ?? "" }] }))
312
+ ]);
313
+ }
314
+ function metavariableLine(value) {
315
+ const spans = metavariableSpan(value);
316
+ return spans ? { spans } : null;
317
+ }
318
+ function metavariableSpan(value, inherited = {}) {
319
+ const match = value.match(/^<([a-z][a-z0-9]*(?:-[a-z0-9]+)+|reason)>$/u);
320
+ return match ? [{ text: match[1], ...inherited, code: true, color: "cyan" }] : null;
321
+ }
322
+ function isPhrasing(node) {
323
+ return !["blockquote", "code", "heading", "list", "thematicBreak"].includes(node.type);
324
+ }
325
+ function splitSpansAtNewlines(spans) {
326
+ const lines = [{ spans: [] }];
327
+ for (const span of spans) {
328
+ const parts = span.text.split("\n");
329
+ parts.forEach((part, index) => {
330
+ if (index > 0)
331
+ lines.push({ spans: [] });
332
+ if (part)
333
+ lines.at(-1).spans.push({ ...span, text: part });
334
+ });
335
+ }
336
+ return lines.map((line) => line.spans.length > 0 ? line : EMPTY_LINE);
337
+ }
338
+ function prependLine(line, text, style = {}) {
339
+ return { spans: [{ text, ...style }, ...line.spans] };
340
+ }
341
+ function sliceLine(line, codeUnits) {
342
+ let remaining = codeUnits;
343
+ const spans = [];
344
+ for (const span of line.spans) {
345
+ if (remaining >= span.text.length) {
346
+ remaining -= span.text.length;
347
+ continue;
348
+ }
349
+ spans.push({ ...span, text: span.text.slice(remaining) });
350
+ remaining = 0;
351
+ }
352
+ return { spans: spans.length > 0 ? spans : [{ text: "" }] };
353
+ }
354
+ function wrapStyledLine(line, width) {
355
+ if (markdownLineText(line) === "")
356
+ return [EMPTY_LINE];
357
+ const lines = [{ spans: [] }];
358
+ let currentWidth = 0;
359
+ for (const span of line.spans) {
360
+ for (const { segment: character } of graphemeSegmenter.segment(span.text)) {
361
+ const characterWidth = displayWidth(character);
362
+ if (currentWidth > 0 && currentWidth + characterWidth > width) {
363
+ lines.push({ spans: [] });
364
+ currentWidth = 0;
365
+ }
366
+ appendSpan(lines.at(-1), { ...span, text: character });
367
+ currentWidth += characterWidth;
368
+ }
369
+ }
370
+ return lines.map((item) => item.spans.length > 0 ? item : EMPTY_LINE);
371
+ }
372
+ function appendSpan(line, span) {
373
+ const previous = line.spans.at(-1);
374
+ if (previous && sameSpanStyle(previous, span))
375
+ previous.text += span.text;
376
+ else
377
+ line.spans.push(span);
378
+ }
379
+ function sameSpanStyle(left, right) {
380
+ return left.bold === right.bold && left.italic === right.italic && left.underline === right.underline && left.dim === right.dim && left.code === right.code && left.color === right.color;
381
+ }
382
+
383
+ // src/tui/specs-display.ts
384
+ var DEFAULT_ROOT_LABELS = {
385
+ specs: "Specs",
386
+ decisionRecords: "Decision Records"
387
+ };
388
+ function flattenSpecsTree(projection, expandedDirectoryKeys, labels = DEFAULT_ROOT_LABELS) {
389
+ return [
390
+ ...flattenRoot("specs", labels.specs, projection.tree, expandedDirectoryKeys),
391
+ ...flattenRoot("decision-records", labels.decisionRecords, projection.decisionRecords, expandedDirectoryKeys)
392
+ ];
393
+ }
394
+ function formatSpecsTreeRow(row, width) {
395
+ const boundedWidth = Math.max(1, width);
396
+ const indent = " ".repeat(row.depth);
397
+ if (row.type === "root" || row.type === "directory") {
398
+ const marker = row.expanded ? "\u25BE" : "\u25B8";
399
+ const label = row.type === "root" ? row.label : row.name;
400
+ return truncateDisplay(`${indent}${marker} ${label}`, boundedWidth);
401
+ }
402
+ const kind = row.kind === "decision-record" ? "decision" : "spec";
403
+ const summary = row.summary ? ` \u2014 ${row.summary}` : "";
404
+ return truncateDisplay(`${indent}\u2022 ${row.title} [${kind}]${summary}`, boundedWidth);
405
+ }
406
+ function formatSpecsSearchRow(match, width) {
407
+ const heading = match.heading ? ` \xB7 ${match.heading}` : "";
408
+ return truncateDisplay(
409
+ `[${match.kind}] ${match.path}:${match.line}${heading} \u2014 ${match.excerpt}`,
410
+ Math.max(1, width)
411
+ );
412
+ }
413
+ function flattenRoot(rootKind, label, node, expandedDirectoryKeys) {
414
+ const expanded = expandedDirectoryKeys.has(node.path);
415
+ const root = {
416
+ type: "root",
417
+ rootKind,
418
+ label,
419
+ key: node.path,
420
+ path: node.path,
421
+ depth: 0,
422
+ expanded
423
+ };
424
+ return expanded ? [root, ...flattenDirectoryContents(node, 1, expandedDirectoryKeys)] : [root];
425
+ }
426
+ function flattenDirectoryContents(node, depth, expandedDirectoryKeys) {
427
+ const directories = node.directories.flatMap((directory) => {
428
+ const expanded = expandedDirectoryKeys.has(directory.path);
429
+ const row = {
430
+ type: "directory",
431
+ name: directory.name,
432
+ key: directory.path,
433
+ path: directory.path,
434
+ depth,
435
+ expanded
436
+ };
437
+ return expanded ? [row, ...flattenDirectoryContents(directory, depth + 1, expandedDirectoryKeys)] : [row];
438
+ });
439
+ const documents = node.documents.map((document) => ({
440
+ type: "document",
441
+ key: document.path,
442
+ path: document.path,
443
+ depth,
444
+ title: document.title,
445
+ summary: document.summary,
446
+ kind: document.kind
447
+ }));
448
+ return [...directories, ...documents];
449
+ }
450
+
451
+ // src/tui/specs-pane.tsx
452
+ import { Box, Text } from "ink";
453
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
454
+ function specsSelectionKey(row) {
455
+ return "line" in row ? `${row.path}:${row.line}` : row.key;
456
+ }
457
+ function SpecsList({ filterActive, listWidth, messages, rows, searchLoading, selectedKey, status, wide }) {
458
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: listWidth, marginRight: wide ? 2 : 0, children: [
459
+ rows.map((row) => {
460
+ const key = specsSelectionKey(row);
461
+ return /* @__PURE__ */ jsxs(Text, { inverse: key === selectedKey, children: [
462
+ key === selectedKey ? "\u203A" : " ",
463
+ " ",
464
+ "line" in row ? formatSpecsSearchRow(row, Math.max(1, listWidth - 2)) : formatSpecsTreeRow(row, Math.max(1, listWidth - 2))
465
+ ] }, key);
466
+ }),
467
+ rows.length === 0 && status !== "loading" && !searchLoading && /* @__PURE__ */ jsx(Text, { dimColor: true, children: filterActive ? messages.noMatches : messages.noSpecs })
468
+ ] });
469
+ }
470
+ function SpecsSummary({ row, match, messages, width }) {
471
+ if (match) {
472
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, width, children: [
473
+ /* @__PURE__ */ jsx(Text, { bold: true, children: truncateDisplay(match.title, width) }),
474
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.path}: ${match.path}`, width) }),
475
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.kind}: ${match.kind}`, width) }),
476
+ match.heading && /* @__PURE__ */ jsx(Text, { children: truncateDisplay(match.heading, width) }),
477
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(match.excerpt, width) })
478
+ ] });
479
+ }
480
+ if (!row)
481
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: messages.noSpecs });
482
+ const label = row.type === "root" ? row.label : row.type === "directory" ? row.name : row.title;
483
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, width, children: [
484
+ /* @__PURE__ */ jsx(Text, { bold: true, children: truncateDisplay(label, width) }),
485
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.path}: ${row.path}`, width) }),
486
+ row.type === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [
487
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.kind}: ${row.kind}`, width) }),
488
+ row.summary && /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.summary}: ${row.summary}`, width) })
489
+ ] })
490
+ ] });
491
+ }
492
+ function SpecsDetail({ messages, specs, viewport, width }) {
493
+ const detail = specs.detail.record;
494
+ if (specs.detail.loadingPath)
495
+ return /* @__PURE__ */ jsx(Text, { children: messages.specsDetailLoading });
496
+ if (specs.detail.error)
497
+ return /* @__PURE__ */ jsx(Text, { color: "red", children: formatHistoryDiagnostic(messages.specsDetailFailed, specs.detail.error, width) });
498
+ if (!detail)
499
+ return /* @__PURE__ */ jsx(Text, { dimColor: true, children: messages.noSpecs });
500
+ return /* @__PURE__ */ jsx(MarkdownDocumentDetail, { contentTruncated: detail.document.contentTruncated, kind: detail.document.kind, messages, path: detail.document.path, title: detail.document.title, viewport, width });
501
+ }
502
+ function MarkdownDocumentDetail({ contentTruncated = false, kind, messages, path, title, viewport, width }) {
503
+ const contentWidth = Math.max(8, width - 1);
504
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
505
+ /* @__PURE__ */ jsx(Text, { bold: true, children: truncateDisplay(title, contentWidth) }),
506
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.path}: ${path}`, contentWidth) }),
507
+ /* @__PURE__ */ jsx(Text, { children: truncateDisplay(`${messages.kind}: ${kind}`, contentWidth) }),
508
+ /* @__PURE__ */ jsx(RenderedDetailViewport, { contentTruncated, messages, viewport })
509
+ ] });
510
+ }
511
+ function RenderedDetailViewport({ contentTruncated = false, messages, viewport }) {
512
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ viewport.lines.map((line, index) => /* @__PURE__ */ jsx(Text, { children: line.spans.length === 0 ? " " : line.spans.map((span, spanIndex) => /* @__PURE__ */ jsx(
514
+ Text,
515
+ {
516
+ bold: span.bold,
517
+ italic: span.italic,
518
+ underline: span.underline,
519
+ dimColor: span.dim,
520
+ color: span.color,
521
+ children: span.text || " "
522
+ },
523
+ spanIndex
524
+ )) }, viewport.start + index)),
525
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
526
+ viewport.start + 1,
527
+ "\u2013",
528
+ Math.max(viewport.start + 1, viewport.end),
529
+ "/",
530
+ viewport.total,
531
+ contentTruncated ? ` \xB7 ${messages.truncated}` : ""
532
+ ] })
533
+ ] });
534
+ }
535
+
536
+ // src/tui/state.ts
537
+ function dashboardLayout(width, height) {
538
+ if (width < 40 || height < 8)
539
+ return "compact";
540
+ return width >= 96 ? "wide" : "narrow";
541
+ }
542
+ function dashboardListWidth(layout, width) {
543
+ if (layout !== "wide")
544
+ return width;
545
+ return Math.min(56, Math.max(36, Math.floor(width * 0.4)));
546
+ }
547
+ function unfilteredItems(state, scope = state.scope) {
548
+ if (scope === "work") {
549
+ const changes = state.snapshot.records.map((record) => ({
550
+ type: "change",
551
+ key: record.output.name,
552
+ workRef: record.output.name,
553
+ title: record.output.summary ?? record.title,
554
+ searchable: `${record.output.name} ${record.output.summary ?? ""} ${record.title}`,
555
+ record
556
+ }));
557
+ const groups = state.snapshot.groups.map((group) => ({
558
+ type: "group",
559
+ key: group.name,
560
+ workRef: group.name,
561
+ title: group.summary ?? group.name,
562
+ searchable: `${group.name} ${group.summary ?? ""} ${group.slices.map((slice) => slice.name).join(" ")}`,
563
+ group
564
+ }));
565
+ return [...changes, ...groups];
566
+ }
567
+ if (scope === "specs")
568
+ return [];
569
+ return state.history.records.map((record) => ({
570
+ type: "history",
571
+ key: record.path,
572
+ workRef: record.workRef,
573
+ title: record.summary,
574
+ searchable: `${record.date} ${record.workRef} ${record.kind} ${record.summary} ${record.path}`,
575
+ history: record
576
+ }));
577
+ }
578
+ function itemsFor(state, scope = state.scope) {
579
+ const query = state.navigation[scope].filter.trim().toLocaleLowerCase();
580
+ const items = unfilteredItems(state, scope);
581
+ return query ? items.filter((item) => item.searchable.toLocaleLowerCase().includes(query)) : items;
582
+ }
583
+ function viewportCapacity(state) {
584
+ return Math.max(1, state.height - 7);
585
+ }
586
+ function reconcileScope(state, scope, preferredKey, preferredIndex) {
587
+ const current = state.navigation[scope];
588
+ const items = itemsFor(state, scope);
589
+ if (items.length === 0) {
590
+ return {
591
+ ...state,
592
+ navigation: { ...state.navigation, [scope]: { ...current, selectedKey: null, selectedIndex: 0, viewportStart: 0 } }
593
+ };
594
+ }
595
+ const retainedKey = preferredKey === void 0 ? current.selectedKey : preferredKey;
596
+ const retained = retainedKey ? items.findIndex((item) => item.key === retainedKey) : -1;
597
+ const candidateIndex = preferredIndex ?? current.selectedIndex;
598
+ const selectedIndex = retained >= 0 ? retained : Math.max(0, Math.min(candidateIndex, items.length - 1));
599
+ const capacity = viewportCapacity(state);
600
+ const viewportStart = selectedIndex < current.viewportStart ? selectedIndex : selectedIndex >= current.viewportStart + capacity ? selectedIndex - capacity + 1 : Math.min(current.viewportStart, Math.max(0, items.length - capacity));
601
+ return {
602
+ ...state,
603
+ navigation: {
604
+ ...state.navigation,
605
+ [scope]: { ...current, selectedKey: items[selectedIndex].key, selectedIndex, viewportStart }
606
+ }
607
+ };
608
+ }
609
+ function emptyNavigation(selectedKey = null) {
610
+ return { filter: "", selectedKey, selectedIndex: 0, viewportStart: 0 };
611
+ }
612
+ function initialDashboardState(snapshot, dimensions) {
613
+ return reconcileScope({
614
+ snapshot,
615
+ scope: "work",
616
+ navigation: {
617
+ work: emptyNavigation(snapshot.focused[0] ?? null),
618
+ specs: emptyNavigation(),
619
+ history: emptyNavigation()
620
+ },
621
+ history: {
622
+ status: "idle",
623
+ records: [],
624
+ summary: null,
625
+ error: null,
626
+ queued: false,
627
+ listRequestId: 0,
628
+ detail: { record: null, loadingPath: null, error: null, errorPath: null, requestId: 0 }
629
+ },
630
+ specs: {
631
+ status: "idle",
632
+ tree: null,
633
+ error: null,
634
+ listRequestId: 0,
635
+ detail: { record: null, loadingPath: null, error: null, requestId: 0, scroll: 0 },
636
+ search: { result: null, loading: false, error: null, requestId: 0, query: "" }
637
+ },
638
+ width: dimensions.width,
639
+ height: dimensions.height,
640
+ layout: dashboardLayout(dimensions.width, dimensions.height),
641
+ mode: "list",
642
+ refresh: { running: false, queued: false, error: null }
643
+ }, "work");
644
+ }
645
+ function reduceDashboard(state, action) {
646
+ switch (action.type) {
647
+ case "move": {
648
+ const items = itemsFor(state);
649
+ const current = state.navigation[state.scope];
650
+ const index = Math.max(0, Math.min(current.selectedIndex + action.delta, Math.max(0, items.length - 1)));
651
+ return reconcileScope(state, state.scope, items[index]?.key ?? null, index);
652
+ }
653
+ case "filter": {
654
+ const current = state.navigation[state.scope];
655
+ const next = {
656
+ ...state,
657
+ navigation: { ...state.navigation, [state.scope]: { ...current, filter: action.value, selectedIndex: 0, viewportStart: 0 } }
658
+ };
659
+ return reconcileScope(next, state.scope, null, 0);
660
+ }
661
+ case "scope":
662
+ return reconcileScope({ ...state, scope: action.scope, mode: "list" }, action.scope);
663
+ case "scope-next": {
664
+ const next = state.scope === "work" ? "specs" : state.scope === "specs" ? "history" : "work";
665
+ return reconcileScope({ ...state, scope: next, mode: "list" }, next);
666
+ }
667
+ case "resize": {
668
+ const next = { ...state, width: action.width, height: action.height, layout: dashboardLayout(action.width, action.height) };
669
+ return reconcileScope(next, state.scope);
670
+ }
671
+ case "mode":
672
+ return { ...state, mode: action.mode };
673
+ case "snapshot": {
674
+ const next = { ...state, snapshot: action.snapshot, refresh: { running: false, queued: false, error: null } };
675
+ return reconcileScope(next, "work");
676
+ }
677
+ case "refresh-requested":
678
+ return { ...state, refresh: { running: true, queued: state.refresh.running, error: null } };
679
+ case "refresh-failed":
680
+ return { ...state, refresh: { running: false, queued: false, error: action.message } };
681
+ case "history-list-requested":
682
+ return { ...state, history: { ...state.history, status: "loading", error: null, queued: false, listRequestId: action.requestId } };
683
+ case "history-list-queued":
684
+ return { ...state, history: { ...state.history, queued: true } };
685
+ case "history-list-loaded": {
686
+ if (action.requestId !== state.history.listRequestId)
687
+ return state;
688
+ const next = {
689
+ ...state,
690
+ history: { ...state.history, status: "ready", records: action.result.records, summary: action.result.summary, error: null }
691
+ };
692
+ return reconcileScope(next, "history");
693
+ }
694
+ case "history-list-failed":
695
+ return action.requestId === state.history.listRequestId ? { ...state, history: { ...state.history, status: "error", error: action.message } } : state;
696
+ case "history-detail-requested":
697
+ return {
698
+ ...state,
699
+ history: { ...state.history, detail: { ...state.history.detail, loadingPath: action.path, error: null, errorPath: null, requestId: action.requestId } }
700
+ };
701
+ case "history-detail-loaded":
702
+ return action.requestId === state.history.detail.requestId ? { ...state, history: { ...state.history, detail: { ...state.history.detail, record: action.record, loadingPath: null, error: null, errorPath: null } } } : state;
703
+ case "history-detail-failed":
704
+ return action.requestId === state.history.detail.requestId ? { ...state, history: { ...state.history, detail: { ...state.history.detail, loadingPath: null, error: action.message, errorPath: state.history.detail.loadingPath } } } : state;
705
+ case "specs-tree-requested":
706
+ return { ...state, specs: { ...state.specs, status: "loading", error: null, listRequestId: action.requestId } };
707
+ case "specs-tree-loaded":
708
+ return action.requestId === state.specs.listRequestId ? { ...state, specs: { ...state.specs, status: "ready", tree: action.result, error: null } } : state;
709
+ case "specs-tree-failed":
710
+ return action.requestId === state.specs.listRequestId ? { ...state, specs: { ...state.specs, status: state.specs.tree ? "stale" : "error", error: action.message } } : state;
711
+ case "specs-detail-requested":
712
+ return { ...state, specs: { ...state.specs, detail: { ...state.specs.detail, loadingPath: action.path, error: null, requestId: action.requestId, scroll: 0 } } };
713
+ case "specs-detail-loaded":
714
+ return action.requestId === state.specs.detail.requestId ? { ...state, specs: { ...state.specs, detail: { ...state.specs.detail, record: action.record, loadingPath: null, error: null } } } : state;
715
+ case "specs-detail-failed":
716
+ return action.requestId === state.specs.detail.requestId ? { ...state, specs: { ...state.specs, detail: { ...state.specs.detail, loadingPath: null, error: action.message } } } : state;
717
+ case "specs-search-requested":
718
+ return { ...state, specs: { ...state.specs, search: { ...state.specs.search, loading: true, error: null, query: action.query, requestId: action.requestId } } };
719
+ case "specs-search-loaded":
720
+ return action.requestId === state.specs.search.requestId ? { ...state, specs: { ...state.specs, search: { ...state.specs.search, loading: false, result: action.result, error: null } } } : state;
721
+ case "specs-search-failed":
722
+ return action.requestId === state.specs.search.requestId ? { ...state, specs: { ...state.specs, search: { ...state.specs.search, loading: false, error: action.message } } } : state;
723
+ case "specs-scroll":
724
+ return { ...state, specs: { ...state.specs, detail: { ...state.specs.detail, scroll: Math.max(0, action.offset) } } };
725
+ }
726
+ }
727
+ function allItems(state) {
728
+ return itemsFor(state);
729
+ }
730
+ function visibleItems(state) {
731
+ const navigation = state.navigation[state.scope];
732
+ return itemsFor(state).slice(navigation.viewportStart, navigation.viewportStart + viewportCapacity(state));
733
+ }
734
+
735
+ // src/tui/work-pane.tsx
736
+ import { Box as Box2, Text as Text2 } from "ink";
737
+
738
+ // src/tui/projection.ts
739
+ function projectItemState(item, snapshot) {
740
+ const blocked = item.type === "change" ? item.record.output.isBlocked : item.type === "group" ? item.group.blockers : false;
741
+ const ready = snapshot.plan.ready.includes(item.workRef) || item.type === "group" && item.group.readyToClose;
742
+ return {
743
+ focused: item.type === "change" && item.record.output.isFocused,
744
+ execution: blocked ? "blocked" : ready ? "ready" : "waiting"
745
+ };
746
+ }
747
+ function projectItemDependencyForest(item, snapshot) {
748
+ if (item.type === "history")
749
+ return [];
750
+ const roots = item.type === "group" ? item.group.slices.map((slice) => slice.name) : [item.workRef];
751
+ const forest = projectDependencyForest(snapshot.plan, roots);
752
+ return item.type === "change" ? forest[0]?.children ?? [] : forest;
753
+ }
754
+ function projectExternalBlockers(item, snapshot) {
755
+ if (item.type !== "change")
756
+ return [];
757
+ const edges = snapshot.plan.edges.filter((edge) => edge.change === item.workRef);
758
+ return item.record.blockerEntries.filter((entry) => {
759
+ const dependency = entry.match(/^requires\s+`([^`]+)`:\s*(\S.*)$/i);
760
+ if (!dependency)
761
+ return true;
762
+ return !edges.some((edge) => edge.requires === dependency[1] && edge.reason === dependency[2]);
763
+ });
764
+ }
765
+ function projectNextAction(item, snapshot) {
766
+ if (item.type === "history")
767
+ return { kind: "brief", value: item.history.path };
768
+ if (item.type === "change" && snapshot.plan.blocked.some((blocked) => blocked.change === item.workRef && blocked.external))
769
+ return { kind: "blocked" };
770
+ if (item.type === "change")
771
+ return { kind: "command", value: `rsp show ${item.workRef}` };
772
+ if (item.group.readyToClose)
773
+ return { kind: "command", value: `rsp group close ${item.workRef}` };
774
+ const executableSlice = item.group.slices.find((slice) => slice.state === "open" && snapshot.plan.ready.includes(slice.name));
775
+ if (executableSlice)
776
+ return { kind: "command", value: `rsp show ${executableSlice.name}` };
777
+ return { kind: "brief", value: item.group.path };
778
+ }
779
+
780
+ // src/tui/work-pane.tsx
781
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
782
+ function projectWorkDetailLines(item, snapshot, messages, width) {
783
+ if (!item || item.type === "history")
784
+ return [{ spans: [{ text: messages.noWork, dim: true }] }];
785
+ const lines = [];
786
+ const add = (text, style = {}) => {
787
+ for (const value of wrapDisplay(text, Math.max(1, width)))
788
+ lines.push({ spans: [{ text: value, ...style }] });
789
+ };
790
+ if (item.title !== item.workRef)
791
+ add(`${messages.summary}: ${item.title}`);
792
+ add(`${messages.progress}: ${item.type === "change" ? `${item.record.output.progress.done}/${item.record.output.progress.total}` : `${item.group.completion.done}/${item.group.completion.total}`}`);
793
+ add(`${messages.status}: ${workStateLabel(item, snapshot, messages)}`);
794
+ add(messages.dependencies, { bold: true });
795
+ const forest = projectItemDependencyForest(item, snapshot);
796
+ const visit = (node, prefix, last) => {
797
+ const connector = last === null ? "" : last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
798
+ const state = node.state === "archived" ? messages.resolved : messages[node.state === "missing" ? "blocked" : node.state];
799
+ add(`${prefix}${connector}${dependencySymbol(node, snapshot)} ${node.name} ${state}${node.shared ? ` \xB7 \u21A9 ${messages.shared}` : ""}`);
800
+ if (node.reason)
801
+ add(`${prefix}${last === false ? "\u2502 " : " "}${messages.reason}: ${node.reason}`, { dim: true });
802
+ const childPrefix = prefix + (last === null ? "" : last ? " " : "\u2502 ");
803
+ node.children.forEach((child, index) => visit(child, childPrefix, index === node.children.length - 1));
804
+ };
805
+ if (forest.length)
806
+ forest.forEach((node, index) => visit(node, "", forest.length === 1 ? null : index === forest.length - 1));
807
+ else
808
+ add(messages.none);
809
+ if (item.type === "group")
810
+ add(`${messages.changes}: ${item.group.slices.length ? item.group.slices.map((slice) => `${slice.name} (${slice.state})`).join(", ") : messages.none}`);
811
+ const external = projectExternalBlockers(item, snapshot);
812
+ const blockers = external.length ? external.join("; ") : item.type === "group" && item.group.blockers || snapshot.plan.blocked.some((blocked) => blocked.change === item.workRef && blocked.external) ? messages.yes : messages.none;
813
+ add(`${messages.blockers}: ${blockers}`);
814
+ const next = projectNextAction(item, snapshot);
815
+ add(`${next.kind === "command" ? messages.nextCommand : next.kind === "blocked" ? messages.nextAction : messages.reviewGroupBrief}: ${next.kind === "blocked" ? messages.awaitingOwnerDecision : next.value}`);
816
+ return lines;
817
+ }
818
+ function workStateLabel(item, snapshot, messages) {
819
+ if (item.type === "history")
820
+ return item.history.kind;
821
+ const state = projectItemState(item, snapshot);
822
+ return (state.focused ? [messages.focused, messages[state.execution]] : [messages[state.execution]]).join(" \xB7 ");
823
+ }
824
+ function dependencySymbol(node, snapshot) {
825
+ if (snapshot.focused.includes(node.name))
826
+ return "\u25CE";
827
+ if (node.state === "ready")
828
+ return "\u25CF";
829
+ if (node.state === "archived")
830
+ return "\u2713";
831
+ if (node.state === "blocked" || node.state === "missing")
832
+ return "!";
833
+ return "\u25CB";
834
+ }
835
+ function DependencyNode({ contentWidth, expanded, last, messages, node, prefix, snapshot }) {
836
+ const connector = last === null ? "" : last ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
837
+ const childPrefix = prefix + (last === null ? "" : last ? " " : "\u2502 ");
838
+ const state = node.state === "archived" ? messages.resolved : messages[node.state === "missing" ? "blocked" : node.state];
839
+ const reason = node.reason && (expanded ? node.reason : truncateDisplay(node.reason, Math.max(8, contentWidth - displayWidth(childPrefix) - displayWidth(messages.reason) - 2)));
840
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
841
+ /* @__PURE__ */ jsxs2(Text2, { children: [
842
+ prefix,
843
+ connector,
844
+ dependencySymbol(node, snapshot),
845
+ " ",
846
+ node.name,
847
+ " ",
848
+ state,
849
+ node.shared ? ` \xB7 \u21A9 ${messages.shared}` : ""
850
+ ] }),
851
+ reason && /* @__PURE__ */ jsx2(Box2, { paddingLeft: displayWidth(childPrefix), children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
852
+ messages.reason,
853
+ ":",
854
+ " ",
855
+ reason
856
+ ] }) }),
857
+ node.children.map((child, index) => /* @__PURE__ */ jsx2(DependencyNode, { contentWidth, expanded, last: index === node.children.length - 1, messages, node: child, prefix: childPrefix, snapshot }, `${node.name}:${child.name}`))
858
+ ] });
859
+ }
860
+ function WorkDetail({ expanded, item, messages, snapshot, width }) {
861
+ if (!item || item.type === "history")
862
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: messages.noWork });
863
+ const forest = projectItemDependencyForest(item, snapshot);
864
+ const externalBlockers = projectExternalBlockers(item, snapshot);
865
+ const nextAction = projectNextAction(item, snapshot);
866
+ const blockers = externalBlockers.length ? externalBlockers.join("; ") : item.type === "group" && item.group.blockers || snapshot.plan.blocked.some((blocked) => blocked.change === item.workRef && blocked.external) ? messages.yes : messages.none;
867
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, width, children: [
868
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
869
+ messages.detail,
870
+ ":",
871
+ " ",
872
+ item.workRef
873
+ ] }),
874
+ item.title !== item.workRef && /* @__PURE__ */ jsxs2(Text2, { children: [
875
+ messages.summary,
876
+ ":",
877
+ " ",
878
+ truncateDisplay(item.title, Math.max(8, width - displayWidth(messages.summary) - 3))
879
+ ] }),
880
+ /* @__PURE__ */ jsxs2(Text2, { children: [
881
+ messages.progress,
882
+ ":",
883
+ " ",
884
+ item.type === "change" ? `${item.record.output.progress.done}/${item.record.output.progress.total}` : `${item.group.completion.done}/${item.group.completion.total}`
885
+ ] }),
886
+ /* @__PURE__ */ jsxs2(Text2, { children: [
887
+ messages.status,
888
+ ":",
889
+ " ",
890
+ workStateLabel(item, snapshot, messages)
891
+ ] }),
892
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: messages.dependencies }),
893
+ forest.length ? forest.map((node, index) => /* @__PURE__ */ jsx2(DependencyNode, { contentWidth: Math.max(8, width - 1), expanded, last: forest.length === 1 ? null : index === forest.length - 1, messages, node, prefix: "", snapshot }, node.name)) : /* @__PURE__ */ jsx2(Text2, { children: messages.none }),
894
+ item.type === "group" && /* @__PURE__ */ jsxs2(Text2, { children: [
895
+ messages.changes,
896
+ ":",
897
+ " ",
898
+ item.group.slices.length ? item.group.slices.map((slice) => `${slice.name} (${slice.state})`).join(", ") : messages.none
899
+ ] }),
900
+ /* @__PURE__ */ jsxs2(Text2, { children: [
901
+ messages.blockers,
902
+ ":",
903
+ " ",
904
+ blockers
905
+ ] }),
906
+ /* @__PURE__ */ jsxs2(Text2, { children: [
907
+ nextAction.kind === "command" ? messages.nextCommand : nextAction.kind === "blocked" ? messages.nextAction : messages.reviewGroupBrief,
908
+ ":",
909
+ " ",
910
+ nextAction.kind === "blocked" ? messages.awaitingOwnerDecision : nextAction.value
911
+ ] })
912
+ ] });
913
+ }
914
+
915
+ // src/tui/app.tsx
916
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
917
+ function HistorySummary({ item, messages, width }) {
918
+ if (!item)
919
+ return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: messages.noHistory });
920
+ const contentWidth = Math.max(8, width - 1);
921
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, width, children: [
922
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: formatHistoryField(messages.detail, item.workRef, contentWidth) }),
923
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.archiveDate, item.history.date, contentWidth) }),
924
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.kind, item.history.kind, contentWidth) }),
925
+ /* @__PURE__ */ jsx3(Text3, { children: truncateDisplay(`${messages.summary}: ${item.history.summary}${item.history.summaryTruncated ? "\u2026" : ""}`, Math.max(8, width - 1)) }),
926
+ /* @__PURE__ */ jsx3(Text3, { children: truncateDisplay(`${messages.path}: ${item.history.path}`, Math.max(8, width - 1)) }),
927
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: messages.enterHistoryDetail })
928
+ ] });
929
+ }
930
+ function Evidence({ value }) {
931
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
932
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: value.heading }),
933
+ value.lines.map((line, index) => /* @__PURE__ */ jsx3(Text3, { children: line }, index))
934
+ ] });
935
+ }
936
+ function HistoryDetail({ height, history, item, messages, width }) {
937
+ if (!item)
938
+ return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: messages.noHistory });
939
+ if (height < HISTORY_DETAIL_MIN_HEIGHT)
940
+ return /* @__PURE__ */ jsx3(Text3, { children: messages.historyDetailCompact });
941
+ const detail = history.detail.record?.path === item.history.path ? history.detail.record : null;
942
+ const contentWidth = Math.max(8, width - 1);
943
+ const listChromeRows = history.status === "loading" || history.status === "error" ? 1 : 0;
944
+ const detailChromeRows = history.detail.loadingPath === item.history.path || history.detail.error && history.detail.errorPath === item.history.path ? 1 : 0;
945
+ const evidence = detail ? projectHistoryEvidence(detail.evidence, {
946
+ height,
947
+ width: contentWidth,
948
+ dynamicRows: listChromeRows + detailChromeRows,
949
+ headings: {
950
+ tasks: `${messages.tasks}: ${detail.checkboxes.tasks.done}/${detail.checkboxes.tasks.total}`,
951
+ verify: `${messages.verify}: ${detail.checkboxes.verify.done}/${detail.checkboxes.verify.total}`,
952
+ blockers: `${messages.blockers}:`
953
+ },
954
+ none: messages.none,
955
+ truncationMarker: messages.truncated
956
+ }) : [];
957
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
958
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: formatHistoryField(messages.detail, item.workRef, contentWidth) }),
959
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.archiveDate, item.history.date, contentWidth) }),
960
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.kind, item.history.kind, contentWidth) }),
961
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.path, item.history.path, contentWidth) }),
962
+ history.detail.loadingPath === item.history.path && /* @__PURE__ */ jsx3(Text3, { children: messages.historyDetailLoading }),
963
+ history.detail.error && history.detail.errorPath === item.history.path && /* @__PURE__ */ jsx3(Text3, { color: "red", children: formatHistoryDiagnostic(messages.historyDetailFailed, history.detail.error, contentWidth) }),
964
+ detail && /* @__PURE__ */ jsxs3(Fragment3, { children: [
965
+ height >= 12 && /* @__PURE__ */ jsx3(Text3, { children: truncateDisplay(`${messages.summary}: ${detail.summary}${detail.summaryTruncated ? "\u2026" : ""}`, contentWidth) }),
966
+ /* @__PURE__ */ jsxs3(Text3, { children: [
967
+ messages.scenarios,
968
+ ":",
969
+ " ",
970
+ detail.scenarioCount
971
+ ] }),
972
+ evidence.length > 0 ? /* @__PURE__ */ jsx3(Fragment3, { children: evidence.map((section) => /* @__PURE__ */ jsx3(Evidence, { value: section }, section.key)) }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: messages.historyEvidenceHidden })
973
+ ] })
974
+ ] });
975
+ }
976
+ function scopeNavigation(scope, messages, width) {
977
+ const labels = [
978
+ ["work", messages.work],
979
+ ["specs", messages.specs],
980
+ ["history", messages.history]
981
+ ];
982
+ const navigation = labels.map(([candidate, label]) => candidate === scope ? `[${label}]` : label).join(" ");
983
+ return truncateDisplay(`${messages.title} ${navigation}`, width);
984
+ }
985
+ function DashboardApp({ initialDimensions, initialSnapshot, inspectHistory, inspectHistoryDetail, inspectHistoryDocument, inspectStatus, messages, specsSource, workSource }) {
986
+ const { exit } = useApp();
987
+ const { stdout } = useStdout();
988
+ const dimensions = initialDimensions ?? { width: stdout.columns ?? 80, height: stdout.rows ?? 24 };
989
+ const [state, dispatch] = useReducer(reduceDashboard, initialDashboardState(initialSnapshot, dimensions));
990
+ const filterRefs = useRef({ work: "", specs: "", history: "" });
991
+ const refreshing = useRef(false);
992
+ const queuedRefresh = useRef(false);
993
+ const historyRequest = useRef(0);
994
+ const historyRefreshing = useRef(false);
995
+ const queuedHistoryRefresh = useRef(false);
996
+ const detailRequest = useRef(0);
997
+ const specsTreeRequest = useRef(0);
998
+ const specsDetailRequest = useRef(0);
999
+ const specsSearchRequest = useRef(0);
1000
+ const specsRootsInitialized = useRef(false);
1001
+ const [specsExpanded, setSpecsExpanded] = useState(/* @__PURE__ */ new Set());
1002
+ const [specsSelectedPath, setSpecsSelectedPath] = useState(null);
1003
+ const [specsSearchInput, setSpecsSearchInput] = useState("");
1004
+ const [specsContentSearch, setSpecsContentSearch] = useState(false);
1005
+ const [showSpecsSearchResults, setShowSpecsSearchResults] = useState(false);
1006
+ const [detailDocument, setDetailDocument] = useState(null);
1007
+ const [detailDocumentState, setDetailDocumentState] = useState("semantic");
1008
+ const [detailDocumentError, setDetailDocumentError] = useState(null);
1009
+ const items = allItems(state);
1010
+ const selectedKey = state.navigation[state.scope].selectedKey;
1011
+ const selected = items.find((item) => item.key === selectedKey);
1012
+ const selectedHistory = selected?.type === "history" ? selected : void 0;
1013
+ const specsTreeRows = useMemo(() => {
1014
+ if (!state.specs.tree)
1015
+ return [];
1016
+ const query = state.navigation.specs.filter.trim().toLocaleLowerCase();
1017
+ const rows = flattenSpecsTree(state.specs.tree, specsExpanded, { specs: messages.specs, decisionRecords: messages.decisions });
1018
+ if (!query)
1019
+ return rows;
1020
+ return rows.filter((row) => {
1021
+ const text = row.type === "root" ? `${row.label} ${row.path}` : row.type === "directory" ? `${row.name} ${row.path}` : `${row.title} ${row.summary ?? ""} ${row.kind} ${row.path}`;
1022
+ return text.toLocaleLowerCase().includes(query);
1023
+ });
1024
+ }, [messages.decisions, messages.specs, specsExpanded, state.navigation.specs.filter, state.specs.tree]);
1025
+ const specsSearchMatches = state.specs.search.result?.matches ?? [];
1026
+ const activeSpecsRows = showSpecsSearchResults ? specsSearchMatches : specsTreeRows;
1027
+ const selectedSpecsIndex = Math.max(0, activeSpecsRows.findIndex((row) => specsSelectionKey(row) === specsSelectedPath));
1028
+ const selectedSpecsRow = showSpecsSearchResults ? void 0 : specsTreeRows[selectedSpecsIndex];
1029
+ const selectedSpecsMatch = showSpecsSearchResults ? specsSearchMatches[selectedSpecsIndex] : void 0;
1030
+ const activeDocument = state.scope === "specs" ? state.specs.detail.record?.document : detailDocumentState === "document" ? detailDocument : null;
1031
+ const semanticDetailLines = useMemo(() => {
1032
+ if (state.scope === "work")
1033
+ return projectWorkDetailLines(selected, state.snapshot, messages, Math.max(8, state.width - 1));
1034
+ if (state.scope === "history" && selectedHistory && state.history.detail.record?.path === selectedHistory.history.path)
1035
+ return projectHistoryDetailLines(state.history.detail.record, messages, Math.max(8, state.width - 1));
1036
+ return [];
1037
+ }, [messages, selected, selectedHistory, state.history.detail.record, state.scope, state.snapshot, state.width]);
1038
+ const activeDetailLines = useMemo(() => {
1039
+ const content = activeDocument?.content;
1040
+ return content === void 0 ? semanticDetailLines : projectMarkdownLines(content, Math.max(8, state.width - 1));
1041
+ }, [activeDocument?.content, semanticDetailLines, state.width]);
1042
+ const detailDynamicChromeRows = state.scope === "history" ? Number(state.history.status === "loading" || state.history.status === "error") + Number(Boolean(state.history.summary)) : state.scope === "specs" ? Number(state.specs.status === "loading" || state.specs.status === "stale" || state.specs.status === "error") + Number(state.specs.search.loading) + Number(Boolean(state.specs.search.error)) + Number(showSpecsSearchResults && Boolean(state.specs.search.result)) : Number(state.refresh.running) + Number(Boolean(state.refresh.error)) + Number(state.snapshot.diagnostics.length > 0);
1043
+ const detailFixedChromeRows = state.scope === "history" && detailDocumentState === "semantic" ? 7 : state.scope === "work" && detailDocumentState === "semantic" ? 5 : 7;
1044
+ const detailViewportHeight = Math.max(1, state.height - detailFixedChromeRows - detailDynamicChromeRows);
1045
+ const specsDetailViewport = useMemo(() => projectMarkdownLineViewport(activeDetailLines, {
1046
+ height: detailViewportHeight,
1047
+ offset: state.specs.detail.scroll
1048
+ }), [activeDetailLines, detailViewportHeight, state.specs.detail.scroll]);
1049
+ const loadHistory = useCallback(async () => {
1050
+ if (historyRefreshing.current) {
1051
+ queuedHistoryRefresh.current = true;
1052
+ dispatch({ type: "history-list-queued" });
1053
+ return;
1054
+ }
1055
+ historyRefreshing.current = true;
1056
+ do {
1057
+ queuedHistoryRefresh.current = false;
1058
+ const requestId = ++historyRequest.current;
1059
+ dispatch({ type: "history-list-requested", requestId });
1060
+ try {
1061
+ dispatch({ type: "history-list-loaded", requestId, result: await inspectHistory() });
1062
+ } catch (error) {
1063
+ dispatch({ type: "history-list-failed", requestId, message: error instanceof Error ? error.message : String(error) });
1064
+ }
1065
+ } while (queuedHistoryRefresh.current);
1066
+ historyRefreshing.current = false;
1067
+ }, [inspectHistory]);
1068
+ const refreshStatus = useCallback(async () => {
1069
+ if (refreshing.current) {
1070
+ queuedRefresh.current = true;
1071
+ dispatch({ type: "refresh-requested" });
1072
+ return;
1073
+ }
1074
+ refreshing.current = true;
1075
+ dispatch({ type: "refresh-requested" });
1076
+ do {
1077
+ queuedRefresh.current = false;
1078
+ try {
1079
+ dispatch({ type: "snapshot", snapshot: await inspectStatus() });
1080
+ } catch (error) {
1081
+ dispatch({ type: "refresh-failed", message: error instanceof Error ? error.message : String(error) });
1082
+ }
1083
+ } while (queuedRefresh.current);
1084
+ refreshing.current = false;
1085
+ }, [inspectStatus]);
1086
+ const loadDetail = useCallback(async (path) => {
1087
+ const requestId = ++detailRequest.current;
1088
+ dispatch({ type: "history-detail-requested", requestId, path });
1089
+ try {
1090
+ dispatch({ type: "history-detail-loaded", requestId, record: await inspectHistoryDetail(path) });
1091
+ } catch (error) {
1092
+ dispatch({ type: "history-detail-failed", requestId, message: error instanceof Error ? error.message : String(error) });
1093
+ }
1094
+ }, [inspectHistoryDetail]);
1095
+ const loadSpecsTree = useCallback(async () => {
1096
+ if (!specsSource)
1097
+ return;
1098
+ const requestId = ++specsTreeRequest.current;
1099
+ dispatch({ type: "specs-tree-requested", requestId });
1100
+ try {
1101
+ const result = await specsSource.tree();
1102
+ if (!specsRootsInitialized.current) {
1103
+ specsRootsInitialized.current = true;
1104
+ setSpecsExpanded(/* @__PURE__ */ new Set([result.tree.path, result.decisionRecords.path]));
1105
+ }
1106
+ dispatch({ type: "specs-tree-loaded", requestId, result });
1107
+ } catch (error) {
1108
+ dispatch({ type: "specs-tree-failed", requestId, message: error instanceof Error ? error.message : String(error) });
1109
+ }
1110
+ }, [specsSource]);
1111
+ const loadSpecsDetail = useCallback(async (path) => {
1112
+ if (!specsSource)
1113
+ return;
1114
+ const requestId = ++specsDetailRequest.current;
1115
+ dispatch({ type: "specs-detail-requested", requestId, path });
1116
+ try {
1117
+ dispatch({ type: "specs-detail-loaded", requestId, record: await specsSource.detail(path) });
1118
+ } catch (error) {
1119
+ dispatch({ type: "specs-detail-failed", requestId, message: error instanceof Error ? error.message : String(error) });
1120
+ }
1121
+ }, [specsSource]);
1122
+ const searchSpecsContent = useCallback(async (literal) => {
1123
+ if (!specsSource)
1124
+ return;
1125
+ const requestId = ++specsSearchRequest.current;
1126
+ setShowSpecsSearchResults(true);
1127
+ dispatch({ type: "specs-search-requested", requestId, query: literal });
1128
+ try {
1129
+ dispatch({ type: "specs-search-loaded", requestId, result: await specsSource.search(literal) });
1130
+ } catch (error) {
1131
+ dispatch({ type: "specs-search-failed", requestId, message: error instanceof Error ? error.message : String(error) });
1132
+ }
1133
+ }, [specsSource]);
1134
+ useEffect(() => {
1135
+ if (state.scope === "history" && state.history.status === "idle")
1136
+ void loadHistory();
1137
+ }, [loadHistory, state.history.status, state.scope]);
1138
+ useEffect(() => {
1139
+ if (specsSource && state.scope === "specs" && state.specs.status === "idle")
1140
+ void loadSpecsTree();
1141
+ }, [loadSpecsTree, specsSource, state.scope, state.specs.status]);
1142
+ useEffect(() => {
1143
+ if (state.scope !== "specs" || activeSpecsRows.length === 0)
1144
+ return;
1145
+ const retained = activeSpecsRows.some((row) => specsSelectionKey(row) === specsSelectedPath);
1146
+ if (!retained)
1147
+ setSpecsSelectedPath(specsSelectionKey(activeSpecsRows[0]));
1148
+ }, [activeSpecsRows, showSpecsSearchResults, specsSelectedPath, state.scope]);
1149
+ useEffect(() => {
1150
+ setDetailDocument(null);
1151
+ setDetailDocumentState("semantic");
1152
+ setDetailDocumentError(null);
1153
+ dispatch({ type: "specs-scroll", offset: 0 });
1154
+ }, [selectedKey, state.scope]);
1155
+ const toggleDocumentView = useCallback(async () => {
1156
+ if (detailDocumentState === "document" || detailDocumentState === "error") {
1157
+ setDetailDocumentState("semantic");
1158
+ setDetailDocumentError(null);
1159
+ return;
1160
+ }
1161
+ if (state.scope === "work" && selected && selected.type !== "history" && workSource) {
1162
+ setDetailDocumentState("loading");
1163
+ try {
1164
+ const path = selected.type === "change" ? selected.record.output.path : selected.group.path;
1165
+ if (!path)
1166
+ throw new Error(`Work document path is unavailable: ${selected.workRef}`);
1167
+ setDetailDocument(await workSource.document(path));
1168
+ setDetailDocumentState("document");
1169
+ } catch (error) {
1170
+ setDetailDocumentError(error instanceof Error ? error.message : String(error));
1171
+ setDetailDocumentState("error");
1172
+ }
1173
+ } else if (state.scope === "history" && selected?.type === "history" && inspectHistoryDocument) {
1174
+ setDetailDocumentState("loading");
1175
+ try {
1176
+ setDetailDocument(await inspectHistoryDocument(selected.history.path));
1177
+ setDetailDocumentState("document");
1178
+ } catch (error) {
1179
+ setDetailDocumentError(error instanceof Error ? error.message : String(error));
1180
+ setDetailDocumentState("error");
1181
+ }
1182
+ }
1183
+ }, [detailDocumentState, inspectHistoryDocument, selected, state.scope, workSource]);
1184
+ useEffect(() => {
1185
+ const resize = () => dispatch({ type: "resize", width: stdout.columns ?? 80, height: stdout.rows ?? 24 });
1186
+ stdout.on("resize", resize);
1187
+ return () => {
1188
+ stdout.off("resize", resize);
1189
+ };
1190
+ }, [stdout]);
1191
+ useInput((input, key) => {
1192
+ if (key.ctrl && input === "c") {
1193
+ exit();
1194
+ return;
1195
+ }
1196
+ if (state.mode === "search") {
1197
+ if (specsContentSearch) {
1198
+ if (key.escape) {
1199
+ setSpecsContentSearch(false);
1200
+ setSpecsSearchInput("");
1201
+ dispatch({ type: "mode", mode: "list" });
1202
+ return;
1203
+ }
1204
+ if (key.return) {
1205
+ const literal = specsSearchInput.trim();
1206
+ setSpecsContentSearch(false);
1207
+ setSpecsSearchInput("");
1208
+ dispatch({ type: "mode", mode: "list" });
1209
+ if (literal)
1210
+ void searchSpecsContent(literal);
1211
+ return;
1212
+ }
1213
+ if (key.backspace || key.delete)
1214
+ setSpecsSearchInput((value) => value.slice(0, -1));
1215
+ else if (input && !key.ctrl && !key.meta)
1216
+ setSpecsSearchInput((value) => value + input);
1217
+ return;
1218
+ }
1219
+ if (key.escape || key.return) {
1220
+ dispatch({ type: "mode", mode: "list" });
1221
+ return;
1222
+ }
1223
+ if (key.backspace || key.delete)
1224
+ filterRefs.current[state.scope] = filterRefs.current[state.scope].slice(0, -1);
1225
+ else if (input && !key.ctrl && !key.meta)
1226
+ filterRefs.current[state.scope] += input;
1227
+ dispatch({ type: "filter", value: filterRefs.current[state.scope] });
1228
+ return;
1229
+ }
1230
+ if (key.escape) {
1231
+ if (state.scope === "specs" && showSpecsSearchResults)
1232
+ setShowSpecsSearchResults(false);
1233
+ else if (state.mode !== "list")
1234
+ dispatch({ type: "mode", mode: "list" });
1235
+ else
1236
+ exit();
1237
+ } else if (input === "q") {
1238
+ exit();
1239
+ } else if (key.tab) {
1240
+ dispatch({ type: "scope-next" });
1241
+ } else if (state.mode === "detail" && (key.upArrow || input === "k")) {
1242
+ dispatch({ type: "specs-scroll", offset: Math.max(0, specsDetailViewport.start - 1) });
1243
+ } else if (state.mode === "detail" && (key.downArrow || input === "j")) {
1244
+ dispatch({ type: "specs-scroll", offset: specsDetailViewport.hasNext ? specsDetailViewport.start + 1 : specsDetailViewport.start });
1245
+ } else if (key.upArrow || input === "k") {
1246
+ if (state.scope === "specs" && activeSpecsRows.length > 0) {
1247
+ const index = Math.max(0, selectedSpecsIndex - 1);
1248
+ setSpecsSelectedPath(specsSelectionKey(activeSpecsRows[index]));
1249
+ } else {
1250
+ dispatch({ type: "move", delta: -1 });
1251
+ }
1252
+ } else if (key.downArrow || input === "j") {
1253
+ if (state.scope === "specs" && activeSpecsRows.length > 0) {
1254
+ const index = Math.min(activeSpecsRows.length - 1, selectedSpecsIndex + 1);
1255
+ setSpecsSelectedPath(specsSelectionKey(activeSpecsRows[index]));
1256
+ } else {
1257
+ dispatch({ type: "move", delta: 1 });
1258
+ }
1259
+ } else if (input === "/") {
1260
+ if (state.scope === "specs")
1261
+ setShowSpecsSearchResults(false);
1262
+ dispatch({ type: "mode", mode: "search" });
1263
+ } else if (input === "s" && state.scope === "specs") {
1264
+ setSpecsContentSearch(true);
1265
+ setSpecsSearchInput("");
1266
+ dispatch({ type: "mode", mode: "search" });
1267
+ } else if (input === "?") {
1268
+ dispatch({ type: "mode", mode: state.mode === "help" ? "list" : "help" });
1269
+ } else if (input === "v" && state.mode === "detail" && (state.scope === "work" || state.scope === "history")) {
1270
+ void toggleDocumentView();
1271
+ } else if (input === "r") {
1272
+ if (state.scope === "history") {
1273
+ void loadHistory();
1274
+ } else if (state.scope === "specs") {
1275
+ setShowSpecsSearchResults(false);
1276
+ void loadSpecsTree();
1277
+ } else {
1278
+ void refreshStatus();
1279
+ }
1280
+ } else if (key.return) {
1281
+ if (state.scope === "specs") {
1282
+ if (selectedSpecsMatch) {
1283
+ dispatch({ type: "mode", mode: "detail" });
1284
+ void loadSpecsDetail(selectedSpecsMatch.path);
1285
+ } else if (selectedSpecsRow?.type === "document") {
1286
+ dispatch({ type: "mode", mode: "detail" });
1287
+ void loadSpecsDetail(selectedSpecsRow.path);
1288
+ } else if (selectedSpecsRow) {
1289
+ setSpecsExpanded((current) => {
1290
+ const next = new Set(current);
1291
+ if (next.has(selectedSpecsRow.path))
1292
+ next.delete(selectedSpecsRow.path);
1293
+ else
1294
+ next.add(selectedSpecsRow.path);
1295
+ return next;
1296
+ });
1297
+ }
1298
+ } else {
1299
+ dispatch({ type: "mode", mode: "detail" });
1300
+ }
1301
+ if (state.scope === "history" && selected?.type === "history" && state.history.detail.record?.path !== selected.history.path)
1302
+ void loadDetail(selected.history.path);
1303
+ }
1304
+ });
1305
+ if (state.layout === "compact")
1306
+ return /* @__PURE__ */ jsx3(Text3, { children: messages.compact });
1307
+ if (state.mode === "help") {
1308
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1309
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: messages.helpTitle }),
1310
+ /* @__PURE__ */ jsx3(Text3, { children: messages.help }),
1311
+ /* @__PURE__ */ jsx3(Text3, { children: messages.dependencyDirection }),
1312
+ /* @__PURE__ */ jsx3(Text3, { children: messages.legend }),
1313
+ /* @__PURE__ */ jsx3(Text3, { children: messages.legendMore }),
1314
+ /* @__PURE__ */ jsx3(Text3, { children: messages.canonicalStates }),
1315
+ /* @__PURE__ */ jsx3(Text3, { children: messages.closeHelp })
1316
+ ] });
1317
+ }
1318
+ const listWidth = dashboardListWidth(state.layout, state.width);
1319
+ const detailWidth = Math.max(8, state.width - listWidth - 2);
1320
+ const list = /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: listWidth, marginRight: state.layout === "wide" ? 2 : 0, children: [
1321
+ visibleItems(state).map((item) => {
1322
+ const selectedItem = item.key === selectedKey;
1323
+ if (item.type === "history") {
1324
+ return /* @__PURE__ */ jsxs3(Text3, { inverse: selectedItem, children: [
1325
+ selectedItem ? "\u203A" : " ",
1326
+ " ",
1327
+ formatHistoryRow(item.history, Math.max(1, listWidth - 2))
1328
+ ] }, item.key);
1329
+ }
1330
+ const label = workStateLabel(item, state.snapshot, messages);
1331
+ const kind = item.type === "change" ? messages.changes : messages.groups;
1332
+ const identity = item.title !== item.workRef ? `${item.workRef} \u2014 ${item.title}` : item.type === "group" && item.group.slices.length ? `${item.workRef}: ${item.group.slices.map((slice) => slice.name).join(", ")}` : item.workRef;
1333
+ return /* @__PURE__ */ jsxs3(Text3, { inverse: selectedItem, children: [
1334
+ selectedItem ? "\u203A" : " ",
1335
+ " ",
1336
+ truncateDisplay(identity, Math.max(8, listWidth - displayWidth(label) - displayWidth(kind) - 8)),
1337
+ " ",
1338
+ "[",
1339
+ label,
1340
+ "] [",
1341
+ kind,
1342
+ "]"
1343
+ ] }, item.key);
1344
+ }),
1345
+ items.length === 0 && !(state.scope === "history" && state.history.status === "loading") && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: state.navigation[state.scope].filter ? messages.noMatches : state.scope === "history" ? messages.noHistory : messages.noWork })
1346
+ ] });
1347
+ const specsCapacity = Math.max(1, state.height - 7);
1348
+ const specsViewportStart = Math.max(0, Math.min(
1349
+ Math.max(0, activeSpecsRows.length - specsCapacity),
1350
+ selectedSpecsIndex >= specsCapacity ? selectedSpecsIndex - specsCapacity + 1 : 0
1351
+ ));
1352
+ const visibleSpecsRows = activeSpecsRows.slice(specsViewportStart, specsViewportStart + specsCapacity);
1353
+ const specsList = /* @__PURE__ */ jsx3(
1354
+ SpecsList,
1355
+ {
1356
+ filterActive: Boolean(state.navigation.specs.filter),
1357
+ listWidth,
1358
+ messages,
1359
+ rows: visibleSpecsRows,
1360
+ searchLoading: state.specs.search.loading,
1361
+ selectedKey: specsSelectedPath,
1362
+ status: state.specs.status,
1363
+ wide: state.layout === "wide"
1364
+ }
1365
+ );
1366
+ const activeList = state.scope === "specs" ? specsList : list;
1367
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: state.width, height: state.height, children: [
1368
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: scopeNavigation(state.scope, messages, state.width) }),
1369
+ state.mode === "search" && (specsContentSearch ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
1370
+ /* @__PURE__ */ jsx3(Text3, { children: messages.specsSearchPrompt }),
1371
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1372
+ messages.specsSearch,
1373
+ ":",
1374
+ " ",
1375
+ specsSearchInput,
1376
+ "_"
1377
+ ] })
1378
+ ] }) : /* @__PURE__ */ jsxs3(Text3, { children: [
1379
+ messages.filter,
1380
+ ":",
1381
+ " ",
1382
+ state.navigation[state.scope].filter,
1383
+ "_"
1384
+ ] })),
1385
+ state.scope === "history" && state.history.status === "loading" && /* @__PURE__ */ jsx3(Text3, { children: messages.historyLoading }),
1386
+ state.scope === "history" && state.history.status === "error" && /* @__PURE__ */ jsx3(Text3, { color: "red", children: formatHistoryDiagnostic(messages.historyRefreshFailed, state.history.error ?? "", state.width) }),
1387
+ state.scope === "history" && state.history.summary && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: truncateDisplay(`${state.history.summary.returned}/${state.history.summary.matched} ${messages.archivedChangesShown}${state.history.summary.hasMore ? ` \xB7 ${messages.historyBounded}` : ""}`, state.width) }),
1388
+ state.scope === "specs" && state.specs.status === "loading" && /* @__PURE__ */ jsx3(Text3, { children: messages.specsLoading }),
1389
+ state.scope === "specs" && (state.specs.status === "stale" || state.specs.status === "error") && /* @__PURE__ */ jsx3(Text3, { color: state.specs.status === "stale" ? "yellow" : "red", children: truncateDisplay(`${messages.specsRefreshFailed} ${state.specs.error ?? ""}${state.specs.status === "stale" ? ` [${messages.stale}]` : ""}`, state.width) }),
1390
+ state.scope === "specs" && state.specs.search.loading && /* @__PURE__ */ jsx3(Text3, { children: messages.specsSearchLoading }),
1391
+ state.scope === "specs" && state.specs.search.error && /* @__PURE__ */ jsx3(Text3, { color: "red", children: formatHistoryDiagnostic(messages.specsSearchFailed, state.specs.search.error, state.width) }),
1392
+ state.scope === "specs" && showSpecsSearchResults && state.specs.search.result && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1393
+ state.specs.search.result.summary.returned,
1394
+ "/",
1395
+ state.specs.search.result.summary.matched,
1396
+ " ",
1397
+ messages.specsSearch,
1398
+ state.specs.search.result.summary.hasMore ? ` \xB7 ${messages.truncated}` : ""
1399
+ ] }),
1400
+ state.scope === "work" && state.refresh.running && /* @__PURE__ */ jsx3(Text3, { children: messages.refreshing }),
1401
+ state.scope === "work" && state.refresh.error && /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
1402
+ messages.refreshFailed,
1403
+ " ",
1404
+ state.refresh.error
1405
+ ] }),
1406
+ state.scope === "work" && state.snapshot.diagnostics.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: "yellow", children: [
1407
+ messages.diagnostics,
1408
+ ":",
1409
+ " ",
1410
+ state.snapshot.diagnostics.map((item) => item.code).join(", ")
1411
+ ] }),
1412
+ state.mode === "detail" ? detailDocumentState === "loading" ? /* @__PURE__ */ jsx3(Text3, { children: messages.specsDetailLoading }) : detailDocumentState === "error" ? /* @__PURE__ */ jsx3(Text3, { color: "red", children: detailDocumentError }) : detailDocumentState === "document" && detailDocument ? /* @__PURE__ */ jsx3(MarkdownDocumentDetail, { kind: state.scope === "history" ? selectedHistory?.history.kind ?? "archive" : selected?.type ?? "work", messages, path: detailDocument.path, title: selected?.title ?? detailDocument.path, viewport: specsDetailViewport, width: state.width }) : state.scope === "history" ? selectedHistory && state.history.detail.record?.path === selectedHistory.history.path ? /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: state.width, children: [
1413
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: formatHistoryField(messages.detail, selectedHistory.workRef, Math.max(8, state.width - 1)) }),
1414
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.archiveDate, selectedHistory.history.date, Math.max(8, state.width - 1)) }),
1415
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.kind, selectedHistory.history.kind, Math.max(8, state.width - 1)) }),
1416
+ /* @__PURE__ */ jsx3(Text3, { children: formatHistoryField(messages.path, selectedHistory.history.path, Math.max(8, state.width - 1)) }),
1417
+ /* @__PURE__ */ jsx3(RenderedDetailViewport, { messages, viewport: specsDetailViewport })
1418
+ ] }) : /* @__PURE__ */ jsx3(HistoryDetail, { height: state.height, history: state.history, item: selectedHistory, messages, width: state.width }) : state.scope === "specs" ? /* @__PURE__ */ jsx3(SpecsDetail, { messages, specs: state.specs, viewport: specsDetailViewport, width: state.width }) : /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: state.width, children: [
1419
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
1420
+ messages.detail,
1421
+ ":",
1422
+ " ",
1423
+ selected?.workRef
1424
+ ] }),
1425
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1426
+ messages.path,
1427
+ ":",
1428
+ " ",
1429
+ selected?.type === "change" ? selected.record.output.path : selected?.type === "group" ? selected.group.path : ""
1430
+ ] }),
1431
+ /* @__PURE__ */ jsx3(RenderedDetailViewport, { messages, viewport: specsDetailViewport })
1432
+ ] }) : state.layout === "wide" ? /* @__PURE__ */ jsxs3(Box3, { flexGrow: 1, children: [
1433
+ activeList,
1434
+ state.scope === "history" ? /* @__PURE__ */ jsx3(HistorySummary, { item: selectedHistory, messages, width: detailWidth }) : state.scope === "specs" ? /* @__PURE__ */ jsx3(SpecsSummary, { match: selectedSpecsMatch, messages, row: selectedSpecsRow, width: detailWidth }) : /* @__PURE__ */ jsx3(WorkDetail, { expanded: false, item: selected, messages, snapshot: state.snapshot, width: detailWidth })
1435
+ ] }) : activeList,
1436
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: truncateDisplay(messages.help, state.width) })
1437
+ ] });
1438
+ }
1439
+
1440
+ // src/tui/history-source.ts
1441
+ var defaultDependencies = {
1442
+ inspect: inspectArchiveHistory,
1443
+ readDetail: readArchiveHistoryDetail,
1444
+ readDocument: readArchiveHistoryDocument
1445
+ };
1446
+ function createTuiHistorySource(dependencies = defaultDependencies) {
1447
+ let recordsByPath = /* @__PURE__ */ new Map();
1448
+ return {
1449
+ async list() {
1450
+ const inspection = await dependencies.inspect();
1451
+ if (!historyInspectionComplete(inspection)) {
1452
+ const first = inspection.diagnostics[0];
1453
+ throw new ArchiveHistoryError("archive_inspection_incomplete", `archive_inspection_incomplete${first ? `: ${first.path ?? "archive"} \u2014 ${first.message}` : ""}`);
1454
+ }
1455
+ const result = queryArchiveHistory(inspection.records);
1456
+ const returnedPaths = new Set(result.records.map((record) => record.path));
1457
+ const nextRecords = new Map(inspection.records.filter((record) => returnedPaths.has(record.path)).map((record) => [record.path, record]));
1458
+ recordsByPath = nextRecords;
1459
+ return result;
1460
+ },
1461
+ detail(path) {
1462
+ const record = recordsByPath.get(path);
1463
+ if (!record)
1464
+ throw new ArchiveHistoryError("archive_not_found", `archived Change is not present in the last successful bounded TUI result: ${path}`);
1465
+ return dependencies.readDetail(record);
1466
+ },
1467
+ async document(path) {
1468
+ const record = recordsByPath.get(path);
1469
+ if (!record)
1470
+ throw new ArchiveHistoryError("archive_not_found", `archived Change is not present in the last successful bounded TUI result: ${path}`);
1471
+ if (!dependencies.readDocument)
1472
+ throw new ArchiveHistoryError("archive_read_failed", `archive document reader is unavailable: ${path}`);
1473
+ return { path, content: (await dependencies.readDocument(record)).content };
1474
+ }
1475
+ };
1476
+ }
1477
+
1478
+ // src/tui/i18n/messages.ts
1479
+ var en = {
1480
+ title: "RSP dashboard",
1481
+ changes: "Changes",
1482
+ groups: "Groups",
1483
+ work: "Work",
1484
+ specs: "Specs",
1485
+ decisions: "Decisions",
1486
+ history: "History",
1487
+ focused: "focused",
1488
+ blocked: "blocked",
1489
+ ready: "ready",
1490
+ waiting: "waiting",
1491
+ resolved: "resolved",
1492
+ progress: "Progress",
1493
+ status: "Status",
1494
+ blockers: "Blockers",
1495
+ dependencies: "Prerequisites",
1496
+ dependencyDirection: "Dependency trees read from work to prerequisites.",
1497
+ reason: "Reason",
1498
+ shared: "shared",
1499
+ legend: "Legend \u25CE focused \u25CF ready \u25CB waiting",
1500
+ legendMore: " \u2713 resolved ! blocked",
1501
+ canonicalStates: "Canonical states: focused \xB7 ready \xB7 waiting \xB7 resolved \xB7 blocked",
1502
+ nextCommand: "Next command",
1503
+ nextAction: "Next action",
1504
+ awaitingOwnerDecision: "Awaiting owner decision",
1505
+ reviewGroupBrief: "Review Group Brief",
1506
+ none: "none",
1507
+ yes: "yes",
1508
+ noWork: "No open work.",
1509
+ noMatches: "No items match the filter.",
1510
+ compact: "Terminal is too small. Resize to at least 40 columns by 8 rows.",
1511
+ filter: "Filter",
1512
+ refreshing: "Refreshing\u2026",
1513
+ refreshFailed: "Refresh failed; showing the last valid snapshot.",
1514
+ help: "Tab: Work \u2192 Specs \u2192 History \xB7 \u2191/\u2193 or j/k move \xB7 Enter detail \xB7 v semantic/document \xB7 / filter \xB7 s Specs search \xB7 r refresh \xB7 ? help \xB7 q quit",
1515
+ helpTitle: "Keyboard help",
1516
+ closeHelp: "Press Esc or ? to close help.",
1517
+ diagnostics: "Diagnostics",
1518
+ detail: "Detail",
1519
+ archiveDate: "Archive date",
1520
+ kind: "Kind",
1521
+ summary: "Summary",
1522
+ path: "Path",
1523
+ scenarios: "Scenarios",
1524
+ tasks: "Tasks",
1525
+ verify: "Verify",
1526
+ truncated: "(truncated)",
1527
+ noHistory: "No archived Changes in the bounded result.",
1528
+ historyLoading: "Loading archive history\u2026",
1529
+ historyRefreshFailed: "Archive history refresh failed; showing the last valid result.",
1530
+ historyDetailLoading: "Loading structured archive detail\u2026",
1531
+ historyDetailFailed: "Archive detail failed:",
1532
+ archivedChangesShown: "archived Changes shown",
1533
+ historyBounded: "bounded result; use rsp history filters for older work",
1534
+ enterHistoryDetail: "Press Enter to load structured detail.",
1535
+ historyEvidenceHidden: "Resize the terminal to show bounded evidence items.",
1536
+ historyDetailCompact: "Resize to at least 12 rows to view History detail.",
1537
+ noSpecs: "No Specs or Decision Records.",
1538
+ specsLoading: "Loading Specs\u2026",
1539
+ specsRefreshFailed: "Specs refresh failed; showing the last valid result as stale.",
1540
+ specsDetailLoading: "Loading current Specs detail\u2026",
1541
+ specsDetailFailed: "Specs detail failed:",
1542
+ specsSearch: "Specs search",
1543
+ specsSearchLoading: "Searching current Specs\u2026",
1544
+ specsSearchFailed: "Specs search failed:",
1545
+ specsSearchPrompt: "Type a literal and press Enter; Esc cancels.",
1546
+ stale: "stale"
1547
+ };
1548
+ var zhCN = {
1549
+ title: "RSP \u4EEA\u8868\u76D8",
1550
+ changes: "\u53D8\u66F4",
1551
+ groups: "\u53D8\u66F4\u7EC4",
1552
+ work: "\u5DE5\u4F5C",
1553
+ specs: "Specs",
1554
+ decisions: "\u51B3\u7B56\u8BB0\u5F55",
1555
+ history: "\u5386\u53F2",
1556
+ focused: "\u5DF2\u805A\u7126",
1557
+ blocked: "\u963B\u585E",
1558
+ ready: "\u5C31\u7EEA",
1559
+ waiting: "\u7B49\u5F85",
1560
+ resolved: "\u5DF2\u89E3\u51B3",
1561
+ progress: "\u8FDB\u5EA6",
1562
+ status: "\u72B6\u6001",
1563
+ blockers: "\u963B\u585E\u9879",
1564
+ dependencies: "\u524D\u7F6E\u4F9D\u8D56",
1565
+ dependencyDirection: "\u4F9D\u8D56\u6811\u4ECE\u5F53\u524D\u5DE5\u4F5C\u6307\u5411\u6240\u9700\u524D\u7F6E\u9879\u3002",
1566
+ reason: "\u539F\u56E0",
1567
+ shared: "\u5171\u4EAB",
1568
+ legend: "\u56FE\u4F8B \u25CE \u805A\u7126 \u25CF \u5C31\u7EEA \u25CB \u7B49\u5F85",
1569
+ legendMore: " \u2713 \u5DF2\u89E3\u51B3 ! \u963B\u585E",
1570
+ canonicalStates: "\u89C4\u8303\u72B6\u6001\uFF1Afocused \xB7 ready \xB7 waiting \xB7 resolved \xB7 blocked",
1571
+ nextCommand: "\u4E0B\u4E00\u6761\u547D\u4EE4",
1572
+ nextAction: "\u4E0B\u4E00\u6B65",
1573
+ awaitingOwnerDecision: "\u7B49\u5F85\u7EF4\u62A4\u8005\u51B3\u7B56",
1574
+ reviewGroupBrief: "\u67E5\u770B\u53D8\u66F4\u7EC4\u8BF4\u660E",
1575
+ none: "\u65E0",
1576
+ yes: "\u662F",
1577
+ noWork: "\u6CA1\u6709\u5F00\u653E\u5DE5\u4F5C\u3002",
1578
+ noMatches: "\u6CA1\u6709\u7B26\u5408\u7B5B\u9009\u6761\u4EF6\u7684\u9879\u76EE\u3002",
1579
+ compact: "\u7EC8\u7AEF\u8FC7\u5C0F\uFF0C\u8BF7\u8C03\u6574\u5230\u81F3\u5C11 40 \u5217 \xD7 8 \u884C\u3002",
1580
+ filter: "\u7B5B\u9009",
1581
+ refreshing: "\u6B63\u5728\u5237\u65B0\u2026",
1582
+ refreshFailed: "\u5237\u65B0\u5931\u8D25\uFF0C\u7EE7\u7EED\u663E\u793A\u6700\u8FD1\u4E00\u6B21\u6709\u6548\u72B6\u6001\u3002",
1583
+ help: "Tab\uFF1A\u5DE5\u4F5C \u2192 Specs \u2192 \u5386\u53F2 \xB7 \u2191/\u2193 \u6216 j/k \u79FB\u52A8 \xB7 Enter \u8BE6\u60C5 \xB7 v \u8BED\u4E49/\u6587\u6863 \xB7 / \u7B5B\u9009 \xB7 s \u641C\u7D22 Specs \xB7 r \u5237\u65B0 \xB7 ? \u5E2E\u52A9 \xB7 q \u9000\u51FA",
1584
+ helpTitle: "\u5FEB\u6377\u952E\u5E2E\u52A9",
1585
+ closeHelp: "\u6309 Esc \u6216 ? \u5173\u95ED\u5E2E\u52A9\u3002",
1586
+ diagnostics: "\u8BCA\u65AD",
1587
+ detail: "\u8BE6\u60C5",
1588
+ archiveDate: "\u5F52\u6863\u65E5\u671F",
1589
+ kind: "\u7C7B\u578B",
1590
+ summary: "\u6458\u8981",
1591
+ path: "\u8DEF\u5F84",
1592
+ scenarios: "\u573A\u666F",
1593
+ tasks: "\u4EFB\u52A1",
1594
+ verify: "\u9A8C\u8BC1",
1595
+ truncated: "\uFF08\u5DF2\u622A\u65AD\uFF09",
1596
+ noHistory: "\u6709\u754C\u7ED3\u679C\u4E2D\u6CA1\u6709\u5DF2\u5F52\u6863\u53D8\u66F4\u3002",
1597
+ historyLoading: "\u6B63\u5728\u52A0\u8F7D\u5F52\u6863\u5386\u53F2\u2026",
1598
+ historyRefreshFailed: "\u5F52\u6863\u5386\u53F2\u5237\u65B0\u5931\u8D25\uFF0C\u7EE7\u7EED\u663E\u793A\u6700\u8FD1\u4E00\u6B21\u6709\u6548\u7ED3\u679C\u3002",
1599
+ historyDetailLoading: "\u6B63\u5728\u52A0\u8F7D\u7ED3\u6784\u5316\u5F52\u6863\u8BE6\u60C5\u2026",
1600
+ historyDetailFailed: "\u5F52\u6863\u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A",
1601
+ archivedChangesShown: "\u4E2A\u5DF2\u5F52\u6863\u53D8\u66F4",
1602
+ historyBounded: "\u5F53\u524D\u4E3A\u6709\u754C\u7ED3\u679C\uFF1B\u66F4\u65E9\u8BB0\u5F55\u8BF7\u4F7F\u7528 rsp history \u7B5B\u9009",
1603
+ enterHistoryDetail: "\u6309 Enter \u52A0\u8F7D\u7ED3\u6784\u5316\u8BE6\u60C5\u3002",
1604
+ historyEvidenceHidden: "\u8BF7\u589E\u5927\u7EC8\u7AEF\u4EE5\u663E\u793A\u6709\u754C\u8BC1\u636E\u6761\u76EE\u3002",
1605
+ historyDetailCompact: "\u8BF7\u5C06\u7EC8\u7AEF\u9AD8\u5EA6\u8C03\u6574\u5230\u81F3\u5C11 12 \u884C\u4EE5\u67E5\u770B\u5386\u53F2\u8BE6\u60C5\u3002",
1606
+ noSpecs: "\u6CA1\u6709 Specs \u6216\u51B3\u7B56\u8BB0\u5F55\u3002",
1607
+ specsLoading: "\u6B63\u5728\u52A0\u8F7D Specs\u2026",
1608
+ specsRefreshFailed: "Specs \u5237\u65B0\u5931\u8D25\uFF0C\u7EE7\u7EED\u663E\u793A\u6700\u8FD1\u4E00\u6B21\u6709\u6548\u7ED3\u679C\u5E76\u6807\u8BB0\u4E3A\u8FC7\u671F\u3002",
1609
+ specsDetailLoading: "\u6B63\u5728\u52A0\u8F7D\u5F53\u524D Specs \u8BE6\u60C5\u2026",
1610
+ specsDetailFailed: "Specs \u8BE6\u60C5\u52A0\u8F7D\u5931\u8D25\uFF1A",
1611
+ specsSearch: "Specs \u641C\u7D22",
1612
+ specsSearchLoading: "\u6B63\u5728\u641C\u7D22\u5F53\u524D Specs\u2026",
1613
+ specsSearchFailed: "Specs \u641C\u7D22\u5931\u8D25\uFF1A",
1614
+ specsSearchPrompt: "\u8F93\u5165\u5B57\u9762\u6587\u672C\u5E76\u6309 Enter\uFF0CEsc \u53D6\u6D88\u3002",
1615
+ stale: "\u5DF2\u8FC7\u671F"
1616
+ };
1617
+ var catalogs = {
1618
+ en,
1619
+ "zh-CN": zhCN
1620
+ };
1621
+
1622
+ // src/tui/specs-source.ts
1623
+ var defaultDependencies2 = {
1624
+ inspect: inspectSpecs,
1625
+ projectTree: projectSpecsTree,
1626
+ projectDetail: projectSpecsDetail,
1627
+ projectSearch: projectSpecsSearch
1628
+ };
1629
+ function createTuiSpecsSource(dependencies = defaultDependencies2) {
1630
+ let latestInspection = null;
1631
+ async function inspectComplete() {
1632
+ const inspection = await dependencies.inspect();
1633
+ const tree = dependencies.projectTree(inspection);
1634
+ latestInspection = inspection;
1635
+ return { inspection, tree };
1636
+ }
1637
+ async function currentInspection() {
1638
+ if (latestInspection)
1639
+ return latestInspection;
1640
+ return (await inspectComplete()).inspection;
1641
+ }
1642
+ return {
1643
+ async tree() {
1644
+ return (await inspectComplete()).tree;
1645
+ },
1646
+ async detail(path) {
1647
+ return dependencies.projectDetail(await currentInspection(), path);
1648
+ },
1649
+ async search(literal, options = {}) {
1650
+ return dependencies.projectSearch(await currentInspection(), literal, {
1651
+ limit: options.limit ?? SPECS_DEFAULT_RESULT_LIMIT,
1652
+ excerptCodePoints: options.excerptCodePoints ?? SPECS_SEARCH_EXCERPT_CODE_POINTS
1653
+ });
1654
+ }
1655
+ };
1656
+ }
1657
+
1658
+ // src/tui/work-source.ts
1659
+ import { Buffer } from "buffer";
1660
+ import { constants } from "fs";
1661
+ import { lstat, open, realpath } from "fs/promises";
1662
+ import { dirname, isAbsolute, relative, resolve, sep } from "path";
1663
+ var READ_NO_FOLLOW_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
1664
+ var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
1665
+ var WorkDocumentError = class extends Error {
1666
+ constructor(code, message) {
1667
+ super(message);
1668
+ this.code = code;
1669
+ this.name = "WorkDocumentError";
1670
+ }
1671
+ code;
1672
+ };
1673
+ function createTuiWorkSource(options = {}) {
1674
+ const changesDir = resolve(options.changesDir ?? CHANGES_DIR);
1675
+ const projectRoot = dirname(dirname(changesDir));
1676
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
1677
+ return {
1678
+ document: (path) => readWorkDocument(resolve(path), { changesDir, maxFileBytes, projectRoot })
1679
+ };
1680
+ }
1681
+ async function readWorkDocument(sourcePath, options) {
1682
+ const projectPath = normalizeLogicalPath(relative(options.projectRoot, sourcePath));
1683
+ try {
1684
+ const ref = resolveWorkRefPath(sourcePath, { changesDir: options.changesDir, mustExist: true });
1685
+ if (resolve(ref.path) !== sourcePath)
1686
+ throw unsafe(projectPath);
1687
+ } catch (error) {
1688
+ if (error instanceof WorkDocumentError)
1689
+ throw error;
1690
+ if (error instanceof WorkRefError)
1691
+ throw unsafe(projectPath);
1692
+ throw readFailed(projectPath);
1693
+ }
1694
+ let rootBefore;
1695
+ let sourceBefore;
1696
+ let realRoot;
1697
+ let realSource;
1698
+ try {
1699
+ rootBefore = await lstat(options.changesDir, { bigint: true });
1700
+ sourceBefore = await lstat(sourcePath, { bigint: true });
1701
+ realRoot = await realpath(options.changesDir);
1702
+ realSource = await realpath(sourcePath);
1703
+ const expectedRealSource = resolve(realRoot, relative(options.changesDir, sourcePath));
1704
+ if (!rootBefore.isDirectory() || !sourceBefore.isFile() || realSource !== expectedRealSource || !isContained(realRoot, realSource))
1705
+ throw unsafe(projectPath);
1706
+ } catch (error) {
1707
+ if (error instanceof WorkDocumentError)
1708
+ throw error;
1709
+ throw unsafe(projectPath);
1710
+ }
1711
+ let handle;
1712
+ try {
1713
+ handle = await open(sourcePath, READ_NO_FOLLOW_FLAGS);
1714
+ } catch {
1715
+ throw unsafe(projectPath);
1716
+ }
1717
+ try {
1718
+ const opened = await handle.stat({ bigint: true });
1719
+ if (!opened.isFile() || !sameIdentity(sourceBefore, opened) || !sameSnapshot(sourceBefore, opened))
1720
+ throw changed(projectPath);
1721
+ if (opened.size > BigInt(options.maxFileBytes))
1722
+ throw tooLarge(projectPath, options.maxFileBytes);
1723
+ const buffer = Buffer.allocUnsafe(options.maxFileBytes + 1);
1724
+ let bytes = 0;
1725
+ while (bytes < buffer.length) {
1726
+ const result = await handle.read(buffer, bytes, buffer.length - bytes, bytes);
1727
+ if (result.bytesRead === 0)
1728
+ break;
1729
+ bytes += result.bytesRead;
1730
+ }
1731
+ const final = await handle.stat({ bigint: true });
1732
+ if (bytes > options.maxFileBytes || final.size > BigInt(options.maxFileBytes))
1733
+ throw tooLarge(projectPath, options.maxFileBytes);
1734
+ if (!sameIdentity(opened, final) || !sameSnapshot(opened, final) || final.size !== BigInt(bytes))
1735
+ throw changed(projectPath);
1736
+ const rootAfter = await lstat(options.changesDir, { bigint: true });
1737
+ const sourceAfter = await lstat(sourcePath, { bigint: true });
1738
+ const realRootAfter = await realpath(options.changesDir);
1739
+ const realSourceAfter = await realpath(sourcePath);
1740
+ if (!sameIdentity(rootBefore, rootAfter) || realRootAfter !== realRoot || realSourceAfter !== realSource || !sameIdentity(sourceAfter, final) || !sameSnapshot(sourceAfter, final)) {
1741
+ throw changed(projectPath);
1742
+ }
1743
+ return { path: projectPath, content: buffer.subarray(0, bytes).toString("utf8") };
1744
+ } catch (error) {
1745
+ if (error instanceof WorkDocumentError)
1746
+ throw error;
1747
+ throw changed(projectPath);
1748
+ } finally {
1749
+ await handle.close();
1750
+ }
1751
+ }
1752
+ function sameIdentity(left, right) {
1753
+ return left.dev === right.dev && left.ino === right.ino;
1754
+ }
1755
+ function sameSnapshot(left, right) {
1756
+ return left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
1757
+ }
1758
+ function isContained(root, path) {
1759
+ const candidate = relative(root, path);
1760
+ return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${sep}`) && !isAbsolute(candidate);
1761
+ }
1762
+ function unsafe(path) {
1763
+ return new WorkDocumentError("work_document_unsafe", `Work document is not an exact readable regular file: ${path}`);
1764
+ }
1765
+ function changed(path) {
1766
+ return new WorkDocumentError("work_document_changed", `Work document changed during inspection: ${path}`);
1767
+ }
1768
+ function tooLarge(path, limit) {
1769
+ return new WorkDocumentError("work_document_too_large", `Work document exceeds the ${limit}-byte limit: ${path}`);
1770
+ }
1771
+ function readFailed(path) {
1772
+ return new WorkDocumentError("work_document_read_failed", `Unable to read Work document: ${path}`);
1773
+ }
1774
+
1775
+ // src/tui/entry.tsx
1776
+ var signalExitCodes = {
1777
+ SIGHUP: 129,
1778
+ SIGINT: 130,
1779
+ SIGTERM: 143
1780
+ };
1781
+ var defaultHistorySource = createTuiHistorySource();
1782
+ var defaultSpecsSource = createTuiSpecsSource();
1783
+ var defaultWorkSource = createTuiWorkSource();
1784
+ var defaultRuntime = {
1785
+ inspect: inspectProjectStatus,
1786
+ inspectHistory: defaultHistorySource.list,
1787
+ inspectHistoryDetail: defaultHistorySource.detail,
1788
+ inspectHistoryDocument: defaultHistorySource.document,
1789
+ specsSource: defaultSpecsSource,
1790
+ workSource: defaultWorkSource,
1791
+ render,
1792
+ openSession: openTerminalSession,
1793
+ host: process,
1794
+ reportError: (message) => console.error(message)
1795
+ };
1796
+ async function runTui(locale, runtime = defaultRuntime) {
1797
+ const screenReader = runtime.host.env.INK_SCREEN_READER === "true" || runtime.host.env.INK_SCREEN_READER === "1";
1798
+ const session = runtime.openSession(runtime.host.stdout, { screenReader });
1799
+ let instance;
1800
+ const listeners = /* @__PURE__ */ new Map();
1801
+ let cleaned = false;
1802
+ const cleanup = () => {
1803
+ if (cleaned)
1804
+ return;
1805
+ cleaned = true;
1806
+ for (const [signal, listener] of listeners)
1807
+ runtime.host.off(signal, listener);
1808
+ listeners.clear();
1809
+ instance?.unmount();
1810
+ session.cleanup();
1811
+ };
1812
+ try {
1813
+ const snapshot = await runtime.inspect();
1814
+ instance = runtime.render(React.createElement(DashboardApp, {
1815
+ initialSnapshot: snapshot,
1816
+ messages: catalogs[locale],
1817
+ inspectStatus: runtime.inspect,
1818
+ inspectHistory: runtime.inspectHistory,
1819
+ inspectHistoryDetail: runtime.inspectHistoryDetail,
1820
+ inspectHistoryDocument: runtime.inspectHistoryDocument,
1821
+ specsSource: runtime.specsSource,
1822
+ workSource: runtime.workSource
1823
+ }), {
1824
+ exitOnCtrlC: false,
1825
+ patchConsole: false
1826
+ });
1827
+ for (const signal of Object.keys(signalExitCodes)) {
1828
+ const listener = () => {
1829
+ cleanup();
1830
+ runtime.host.exit(signalExitCodes[signal]);
1831
+ };
1832
+ listeners.set(signal, listener);
1833
+ runtime.host.once(signal, listener);
1834
+ }
1835
+ await instance.waitUntilExit();
1836
+ cleanup();
1837
+ return 0;
1838
+ } catch (error) {
1839
+ cleanup();
1840
+ runtime.reportError(` Error: ${error instanceof Error ? error.message : String(error)}`);
1841
+ return 1;
1842
+ }
1843
+ }
1844
+ export {
1845
+ runTui
1846
+ };