@try-works/dsh-recursive-mode 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -405,64 +405,6 @@ window.__ModuleLoader__.load({
405
405
  })));
406
406
  }
407
407
  //#endregion
408
- //#region src/client/inspector.tsx
409
- /**
410
- * Drill-down inspector (SP2 R1 + run 15/16): centered modal-style detail panel
411
- * (Paper .detail shape) with back + close, data-theme (default light), theme
412
- * toggle, and solid status pills. All from the live host-route snapshot.
413
- * useBoardTheme is hoisted ABOVE the not-found early return.
414
- */
415
- function closeButton(onClose) {
416
- if (onClose === void 0) return null;
417
- return (0, react.createElement)("button", {
418
- type: "button",
419
- className: "rec-close",
420
- onClick: onClose,
421
- title: "Close",
422
- "aria-label": "Close"
423
- }, "×");
424
- }
425
- function solidPill(kind) {
426
- return (0, react.createElement)("span", {
427
- className: "rec-pill",
428
- "data-pill": kind
429
- }, PILL_LABELS[kind]);
430
- }
431
- function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle) {
432
- return (0, react.createElement)("div", {
433
- className: "rec-inspector",
434
- "data-theme": theme
435
- }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
436
- type: "button",
437
- className: "rec-back",
438
- onClick: onBackToToolDetails
439
- }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, (0, react.createElement)(ThemeToggle, {
440
- theme,
441
- toggle
442
- }), closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
443
- }
444
- function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
445
- const { theme, toggle } = useBoardTheme();
446
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
447
- if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId), theme, toggle);
448
- const facts = cardFacts(card);
449
- const rows = expandPhaseRows(card);
450
- return detailShell(runId, onBackToToolDetails, onClose, solidPill(cardPill(card)), (0, react.createElement)("section", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
451
- key: row.phase,
452
- className: "rec-phase-row"
453
- }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), solidPill(phaseStatusPill(row.status)), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8)))), facts.gateBlocked && (0, react.createElement)("div", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", { className: "rec-detail-text" }, "Blocked (" + (facts.gateKind ?? "") + ")"))), theme, toggle);
454
- }
455
- //#endregion
456
- //#region src/client/settings.tsx
457
- /**
458
- * Recursive settings page (Phase D R8, §11.8): a settings.section list entry
459
- * surfacing the host-owned enforcement config. The client is read-mostly; all
460
- * writes ride the host config path (never run files).
461
- */
462
- function RecursiveSettings({ close }) {
463
- return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("h2", {}, "Recursive"), (0, react.createElement)("p", {}, "Enforcement policy, scratch format, provider defaults, and preset install path are configured on the host. The client is read-only (§11.9)."), (0, react.createElement)("button", { onClick: close }, "Close"));
464
- }
465
- //#endregion
466
408
  //#region src/client/host-api.ts
467
409
  /**
468
410
  * Live recursive board/strip feed (SP2 R1): the browser half of the host
@@ -503,6 +445,27 @@ window.__ModuleLoader__.load({
503
445
  }
504
446
  }
505
447
  /**
448
+ * Fetch one per-phase run doc (0.2.4) via the lazy GET /.recursive/api/doc
449
+ * route. Raw markdown text; the host validates the root (known workspace) and
450
+ * guards the file path (phase-doc basename only, containment under the run
451
+ * dir). Same 15s AbortController timeout as fetchLiveState.
452
+ */
453
+ async function fetchPhaseDoc(req) {
454
+ const controller = new AbortController();
455
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
456
+ try {
457
+ const q = "root=" + encodeURIComponent(req.root) + "&runId=" + encodeURIComponent(req.runId) + "&file=" + encodeURIComponent(req.file);
458
+ const res = await fetch("/.recursive/api/doc?" + q, {
459
+ signal: controller.signal,
460
+ cache: "no-store"
461
+ });
462
+ if (!res.ok) throw new Error("recursive phase doc: HTTP " + res.status);
463
+ return await res.text();
464
+ } finally {
465
+ clearTimeout(timer);
466
+ }
467
+ }
468
+ /**
506
469
  * Subscribe to the SSE events feed. Returns a disposer. The host pushes a full
507
470
  * {revision, root, projection} frame on every fs change + a 15s heartbeat.
508
471
  *
@@ -536,6 +499,485 @@ window.__ModuleLoader__.load({
536
499
  };
537
500
  }
538
501
  //#endregion
502
+ //#region src/client/doc-viewer.tsx
503
+ /**
504
+ * Per-phase run-doc viewer (0.2.4): renders one .recursive/run/<runId>/<file>
505
+ * markdown doc read LAZILY via the host GET /.recursive/api/doc route, inside
506
+ * the Inspector run-detail panel. Read-only: NO approve / changes / comment /
507
+ * quit actions, NO session-answer writes, NO recursive/* session events.
508
+ *
509
+ * ATTRIBUTION — ported from @guillaumemeyer/dsh-plan-approval (MIT),
510
+ * https://github.com/guillaumemeyer/dsh-plan-approval:
511
+ * - the markdown line parser (parseDoc, based on parsePlan), and
512
+ * - the vim-nav + / search key handling (onKey/move/goTop/goBottom/
513
+ * computeMatches/searchNext/searchPrev/openSearch/closeSearch).
514
+ * Reused under MIT. NO planReviewOf / overlay / session-question logic is
515
+ * carried over — this viewer reads run artifacts.
516
+ */
517
+ /**
518
+ * Markdown -> line tokens. Base is parsePlan (blank/h1-h4/li/plain, MIT),
519
+ * extended for fenced code blocks (one code line per block) and pipe tables
520
+ * (one table line per block, header separator row dropped).
521
+ */
522
+ function parseDoc(plan) {
523
+ const raw = String(plan == null ? "" : plan).split("\n");
524
+ const out = [];
525
+ let i = 0;
526
+ while (i < raw.length) {
527
+ const line = raw[i];
528
+ if (/^\s*$/.test(line)) {
529
+ out.push({
530
+ kind: "blank",
531
+ text: ""
532
+ });
533
+ i += 1;
534
+ continue;
535
+ }
536
+ if (/^```\s*([\w+-]*)\s*$/.exec(line)) {
537
+ const code = [];
538
+ i += 1;
539
+ while (i < raw.length && !/^```\s*$/.test(raw[i])) {
540
+ code.push(raw[i]);
541
+ i += 1;
542
+ }
543
+ i += 1;
544
+ out.push({
545
+ kind: "code",
546
+ text: code.join("\n")
547
+ });
548
+ continue;
549
+ }
550
+ if (/^\s*\|/.test(line)) {
551
+ const rows = [];
552
+ while (i < raw.length && /^\s*\|/.test(raw[i])) {
553
+ const parts = raw[i].split("|").slice(1, -1).map((c) => c.trim());
554
+ rows.push(parts);
555
+ i += 1;
556
+ }
557
+ const data = rows.filter((r) => !r.every((c) => /^:?-{3,}:?$/.test(c)));
558
+ out.push({
559
+ kind: "table",
560
+ text: rows.map((r) => r.join(" | ")).join("\n"),
561
+ cells: data
562
+ });
563
+ continue;
564
+ }
565
+ const h = /^(#{1,4})\s+(.+?)\s*$/.exec(line);
566
+ if (h) {
567
+ let kind;
568
+ switch (h[1].length) {
569
+ case 1:
570
+ kind = "h1";
571
+ break;
572
+ case 2:
573
+ kind = "h2";
574
+ break;
575
+ case 3:
576
+ kind = "h3";
577
+ break;
578
+ default: kind = "h4";
579
+ }
580
+ out.push({
581
+ kind,
582
+ text: h[2]
583
+ });
584
+ i += 1;
585
+ continue;
586
+ }
587
+ const li = /^\s*[-*]\s+(.+)$/.exec(line);
588
+ if (li) {
589
+ out.push({
590
+ kind: "li",
591
+ text: li[1]
592
+ });
593
+ i += 1;
594
+ continue;
595
+ }
596
+ out.push({
597
+ kind: "plain",
598
+ text: line
599
+ });
600
+ i += 1;
601
+ }
602
+ return out;
603
+ }
604
+ /**
605
+ * Inline scanner: **bold**, `code`, [text](url); unmatched markers stay plain
606
+ * text. Used to build the React children of a line text.
607
+ */
608
+ function parseInline(text) {
609
+ const out = [];
610
+ const re = /(\*\*(.+?)\*\*)|(`([^`]+)`)|(\[([^\]]+)\]\(([^)]+)\))/g;
611
+ let last = 0;
612
+ let m;
613
+ while ((m = re.exec(text)) !== null) {
614
+ if (m.index > last) out.push({
615
+ type: "text",
616
+ text: text.slice(last, m.index)
617
+ });
618
+ if (m[1] !== void 0) out.push({
619
+ type: "bold",
620
+ text: m[2]
621
+ });
622
+ if (m[3] !== void 0) out.push({
623
+ type: "code",
624
+ text: m[4]
625
+ });
626
+ if (m[5] !== void 0) out.push({
627
+ type: "link",
628
+ text: m[6],
629
+ href: m[7]
630
+ });
631
+ last = re.lastIndex;
632
+ }
633
+ if (last < text.length) out.push({
634
+ type: "text",
635
+ text: text.slice(last)
636
+ });
637
+ return out;
638
+ }
639
+ const SEARCH_HINTS = [
640
+ ["j/k", "move"],
641
+ ["gg/G", "ends"],
642
+ ["/", "search"],
643
+ ["n/N", "next/prev"],
644
+ ["y", "copy"],
645
+ ["Esc", "close"]
646
+ ];
647
+ /** Inline segments -> ReactNodes (bold/code/link markup). */
648
+ function inlineNodes(segments, baseKey) {
649
+ return segments.map((seg, n) => {
650
+ const key = baseKey + "-seg-" + String(n);
651
+ if (seg.type === "bold") return (0, react.createElement)("strong", { key }, seg.text);
652
+ if (seg.type === "code") return (0, react.createElement)("code", {
653
+ key,
654
+ className: "rec-doc-inline-code"
655
+ }, seg.text);
656
+ if (seg.type === "link") return (0, react.createElement)("a", {
657
+ key,
658
+ href: seg.href,
659
+ target: "_blank",
660
+ rel: "noreferrer",
661
+ className: "rec-doc-inline-link"
662
+ }, seg.text);
663
+ return (0, react.createElement)("span", { key }, seg.text);
664
+ });
665
+ }
666
+ /**
667
+ * Render one parsed line as a React element. Headings/bullets get parsePlan
668
+ * sizing; code/table get block layout; inline markup applies to plain-ish text.
669
+ */
670
+ function lineElement(line, i, isCurrent) {
671
+ const cls = "rec-doc-line rec-doc-" + line.kind + (isCurrent ? " rec-doc-line-current" : "");
672
+ if (line.kind === "blank") return (0, react.createElement)("div", {
673
+ key: i,
674
+ "data-line": String(i),
675
+ className: cls
676
+ }, null);
677
+ if (line.kind === "code") return (0, react.createElement)("pre", {
678
+ key: i,
679
+ "data-line": String(i),
680
+ className: cls + " rec-doc-pre"
681
+ }, (0, react.createElement)("code", { className: "rec-doc-code" }, line.text));
682
+ if (line.kind === "table") {
683
+ const all = line.cells ?? [];
684
+ const header = all[0] ?? [];
685
+ const body = all.slice(1);
686
+ return (0, react.createElement)("div", {
687
+ key: i,
688
+ "data-line": String(i),
689
+ className: cls
690
+ }, (0, react.createElement)("table", { className: "rec-doc-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, header.map((c, n) => (0, react.createElement)("th", { key: "th-" + String(n) }, c)))), (0, react.createElement)("tbody", null, body.map((r, n) => (0, react.createElement)("tr", { key: "tr-" + String(n) }, r.map((c, m) => (0, react.createElement)("td", { key: "td-" + String(m) }, c)))))));
691
+ }
692
+ const nodes = inlineNodes(parseInline(line.text), String(i));
693
+ if (line.kind === "li") return (0, react.createElement)("div", {
694
+ key: i,
695
+ "data-line": String(i),
696
+ className: cls
697
+ }, (0, react.createElement)("span", { className: "rec-doc-bullet" }, "•"), (0, react.createElement)("span", { className: "rec-doc-li-text" }, nodes));
698
+ return (0, react.createElement)("div", {
699
+ key: i,
700
+ "data-line": String(i),
701
+ className: cls
702
+ }, nodes);
703
+ }
704
+ /**
705
+ * The per-phase doc viewer. Fetches the route on mount / fileName change.
706
+ * Vim nav + / search + n/N + Esc; y copies the doc. Esc closes search first,
707
+ * else the viewer. data-theme is passed down by the hoisting Inspector
708
+ * (0.1.10 invariant: useBoardTheme lives in the panel).
709
+ */
710
+ function DocViewer({ runId, worktreeRoot, fileName, theme, onClose }) {
711
+ const [text, setText] = (0, react.useState)(null);
712
+ const [error, setError] = (0, react.useState)(null);
713
+ const [notice, setNotice] = (0, react.useState)(null);
714
+ const [cursor, setCursor] = (0, react.useState)(0);
715
+ const [searchOpen, setSearchOpen] = (0, react.useState)(false);
716
+ const [query, setQuery] = (0, react.useState)("");
717
+ const [matches, setMatches] = (0, react.useState)([]);
718
+ const [activeMatch, setActiveMatch] = (0, react.useState)(0);
719
+ const ggArmed = (0, react.useRef)(false);
720
+ const rootRef = (0, react.useRef)(null);
721
+ const [copied, setCopied] = (0, react.useState)(false);
722
+ (0, react.useEffect)(() => {
723
+ let disposed = false;
724
+ setText(null);
725
+ setError(null);
726
+ setNotice(null);
727
+ setCopied(false);
728
+ setCursor(0);
729
+ fetchPhaseDoc({
730
+ root: worktreeRoot,
731
+ runId,
732
+ file: fileName
733
+ }).then((t) => {
734
+ if (!disposed) {
735
+ setText(t);
736
+ setCopied(false);
737
+ }
738
+ }).catch(() => {
739
+ if (!disposed) setError("Failed to load doc");
740
+ });
741
+ return () => {
742
+ disposed = true;
743
+ };
744
+ }, [
745
+ worktreeRoot,
746
+ runId,
747
+ fileName
748
+ ]);
749
+ const docLines = (0, react.useMemo)(() => text === null ? [] : parseDoc(text), [text]);
750
+ const move = (delta) => {
751
+ if (docLines.length === 0) return;
752
+ setCursor((c) => Math.max(0, Math.min(docLines.length - 1, c + delta)));
753
+ };
754
+ const goTop = () => setCursor(0);
755
+ const goBottom = () => setCursor(Math.max(0, docLines.length - 1));
756
+ const computeMatches = (q) => {
757
+ if (!q) return [];
758
+ const needle = q.toLowerCase();
759
+ const found = [];
760
+ for (let i = 0; i < docLines.length; i++) if (docLines[i].text.toLowerCase().indexOf(needle) >= 0) found.push(i);
761
+ return found;
762
+ };
763
+ const openSearch = () => {
764
+ setSearchOpen(true);
765
+ setError(null);
766
+ setNotice(null);
767
+ };
768
+ const closeSearch = () => {
769
+ setSearchOpen(false);
770
+ setQuery("");
771
+ setMatches([]);
772
+ setActiveMatch(0);
773
+ if (rootRef.current) rootRef.current.focus();
774
+ };
775
+ const onSearchChange = (e) => {
776
+ const q = e.target.value;
777
+ setQuery(q);
778
+ setMatches(computeMatches(q));
779
+ setActiveMatch(0);
780
+ };
781
+ const searchNext = () => {
782
+ if (matches.length === 0) return;
783
+ const next = (activeMatch + 1) % matches.length;
784
+ setActiveMatch(next);
785
+ setCursor(matches[next]);
786
+ };
787
+ const searchPrev = () => {
788
+ if (matches.length === 0) return;
789
+ const prev = (activeMatch - 1 + matches.length) % matches.length;
790
+ setActiveMatch(prev);
791
+ setCursor(matches[prev]);
792
+ };
793
+ const onSearchKey = (e) => {
794
+ if (e.key === "Enter" && e.shiftKey) {
795
+ e.preventDefault();
796
+ searchPrev();
797
+ } else if (e.key === "Enter") {
798
+ e.preventDefault();
799
+ searchNext();
800
+ } else if (e.key === "Escape") {
801
+ e.preventDefault();
802
+ closeSearch();
803
+ }
804
+ };
805
+ const copyDoc = () => {
806
+ const clip = globalThis.navigator?.clipboard;
807
+ if (clip === void 0 || clip.writeText === void 0) {
808
+ setError("Copy unavailable");
809
+ setNotice(null);
810
+ return;
811
+ }
812
+ if (clip && clip.writeText) {
813
+ const data = text ?? "";
814
+ clip.writeText(data).then(() => {
815
+ setNotice("Doc copied");
816
+ setError(null);
817
+ setCopied(true);
818
+ }).catch(() => {
819
+ setError("Copy failed");
820
+ setNotice(null);
821
+ });
822
+ } else {
823
+ setError("Copy unavailable");
824
+ setNotice(null);
825
+ }
826
+ };
827
+ const onKey = (e) => {
828
+ const tag = e.target && e.target.tagName || "";
829
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
830
+ const rawKey = e.key || "";
831
+ const key = rawKey.toLowerCase();
832
+ if (rawKey === "G") {
833
+ e.preventDefault();
834
+ ggArmed.current = false;
835
+ goBottom();
836
+ return;
837
+ }
838
+ if (rawKey === "N") {
839
+ e.preventDefault();
840
+ ggArmed.current = false;
841
+ searchPrev();
842
+ return;
843
+ }
844
+ if (key === "g") {
845
+ e.preventDefault();
846
+ if (ggArmed.current) {
847
+ ggArmed.current = false;
848
+ goTop();
849
+ } else ggArmed.current = true;
850
+ return;
851
+ }
852
+ ggArmed.current = false;
853
+ if (key === "j") {
854
+ e.preventDefault();
855
+ move(1);
856
+ } else if (key === "k") {
857
+ e.preventDefault();
858
+ move(-1);
859
+ } else if (key === "/") {
860
+ e.preventDefault();
861
+ openSearch();
862
+ } else if (key === "n") {
863
+ e.preventDefault();
864
+ searchNext();
865
+ } else if (key === "y") {
866
+ e.preventDefault();
867
+ copyDoc();
868
+ } else if (key === "escape") {
869
+ e.preventDefault();
870
+ if (searchOpen) closeSearch();
871
+ else onClose();
872
+ }
873
+ };
874
+ new Set(matches);
875
+ matches.length > 0 && matches[activeMatch];
876
+ const lineEls = docLines.map((line, i) => lineElement(line, i, i === cursor));
877
+ const searchBar = searchOpen ? (0, react.createElement)("div", { className: "rec-doc-search" }, (0, react.createElement)("input", {
878
+ className: "rec-doc-search-input",
879
+ value: query,
880
+ placeholder: "/ search doc…",
881
+ onChange: onSearchChange,
882
+ onKeyDown: onSearchKey
883
+ }), (0, react.createElement)("span", { className: "rec-doc-search-count" }, matches.length > 0 ? activeMatch + 1 + "/" + matches.length : query ? "0" : "")) : null;
884
+ const statusText = error || notice || docLines.length + " lines";
885
+ const statusCls = "rec-doc-status" + (error ? " rec-doc-status-error" : notice ? " rec-doc-status-ok" : "");
886
+ return (0, react.createElement)("div", {
887
+ className: "rec-doc",
888
+ "data-theme": theme,
889
+ onKeyDown: onKey,
890
+ ref: rootRef,
891
+ tabIndex: -1
892
+ }, (0, react.createElement)("header", { className: "rec-doc-header" }, (0, react.createElement)("div", { className: "rec-doc-heading" }, (0, react.createElement)("span", { className: "rec-doc-badge" }, "Phase doc"), (0, react.createElement)("h2", { className: "rec-doc-title" }, fileName), (0, react.createElement)("span", { className: "rec-doc-run" }, runId)), (0, react.createElement)("button", {
893
+ type: "button",
894
+ className: "rec-doc-btn rec-doc-copy",
895
+ onClick: copyDoc,
896
+ title: "Copy doc",
897
+ "aria-label": "Copy doc"
898
+ }, "Copy doc"), (0, react.createElement)("button", {
899
+ type: "button",
900
+ className: "rec-doc-btn rec-doc-close",
901
+ onClick: onClose,
902
+ title: "Close",
903
+ "aria-label": "Close"
904
+ }, "Close")), searchBar, (0, react.createElement)("div", { className: "rec-doc-body" }, text === null && error === null ? (0, react.createElement)("p", { className: "rec-doc-text" }, "Loading doc…") : null, error !== null ? (0, react.createElement)("p", { className: "rec-doc-text rec-doc-error" }, error) : null, text !== null ? lineEls : null), (0, react.createElement)("footer", { className: "rec-doc-footer" }, (0, react.createElement)("div", {
905
+ className: statusCls,
906
+ role: "status"
907
+ }, statusText), (0, react.createElement)("div", { className: "rec-doc-hints" }, SEARCH_HINTS.map((h, n) => (0, react.createElement)("span", { key: "hint-" + String(n) }, (0, react.createElement)("kbd", null, h[0]), " " + h[1] + (n < SEARCH_HINTS.length - 1 ? " |" : ""))))));
908
+ }
909
+ //#endregion
910
+ //#region src/client/inspector.tsx
911
+ /**
912
+ * Drill-down inspector (SP2 R1 + run 15/16): centered modal-style detail panel
913
+ * (Paper .detail shape) with back + close, data-theme (default light), theme
914
+ * toggle, and solid status pills. All from the live host-route snapshot.
915
+ * useBoardTheme is hoisted ABOVE the not-found early return.
916
+ */
917
+ function closeButton(onClose) {
918
+ if (onClose === void 0) return null;
919
+ return (0, react.createElement)("button", {
920
+ type: "button",
921
+ className: "rec-close",
922
+ onClick: onClose,
923
+ title: "Close",
924
+ "aria-label": "Close"
925
+ }, "×");
926
+ }
927
+ function solidPill(kind) {
928
+ return (0, react.createElement)("span", {
929
+ className: "rec-pill",
930
+ "data-pill": kind
931
+ }, PILL_LABELS[kind]);
932
+ }
933
+ function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle) {
934
+ return (0, react.createElement)("div", {
935
+ className: "rec-inspector",
936
+ "data-theme": theme
937
+ }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
938
+ type: "button",
939
+ className: "rec-back",
940
+ onClick: onBackToToolDetails
941
+ }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, (0, react.createElement)(ThemeToggle, {
942
+ theme,
943
+ toggle
944
+ }), closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
945
+ }
946
+ function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
947
+ const { theme, toggle } = useBoardTheme();
948
+ const [openFileName, setOpenFileName] = (0, react.useState)(null);
949
+ const card = snapshot?.projection?.[worktreeRoot]?.[runId];
950
+ if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId), theme, toggle);
951
+ const facts = cardFacts(card);
952
+ const rows = expandPhaseRows(card);
953
+ return detailShell(runId, onBackToToolDetails, onClose, solidPill(cardPill(card)), openFileName !== null ? (0, react.createElement)(DocViewer, {
954
+ runId,
955
+ worktreeRoot,
956
+ fileName: openFileName,
957
+ theme,
958
+ onClose: () => setOpenFileName(null)
959
+ }) : (0, react.createElement)("section", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Phases"), rows.map((row) => (0, react.createElement)("div", {
960
+ key: row.phase,
961
+ className: "rec-phase-row"
962
+ }, (0, react.createElement)("span", { className: "rec-phase-id" }, row.phase), (0, react.createElement)("span", { className: "rec-phase-name" }, row.fileName ?? "—"), solidPill(phaseStatusPill(row.status)), row.lockHash !== void 0 && (0, react.createElement)("code", { className: "rec-lockhash" }, "#" + row.lockHash.slice(0, 8)), row.present && row.fileName !== null && (0, react.createElement)("button", {
963
+ type: "button",
964
+ className: "rec-phase-view",
965
+ onClick: () => setOpenFileName(row.fileName),
966
+ title: "View phase doc",
967
+ "aria-label": "View phase doc"
968
+ }, "View phase"))), facts.gateBlocked && (0, react.createElement)("div", { className: "rec-detail-section" }, (0, react.createElement)("h3", {}, "Gate"), (0, react.createElement)("p", { className: "rec-detail-text" }, "Blocked (" + (facts.gateKind ?? "") + ")"))), theme, toggle);
969
+ }
970
+ //#endregion
971
+ //#region src/client/settings.tsx
972
+ /**
973
+ * Recursive settings page (Phase D R8, §11.8): a settings.section list entry
974
+ * surfacing the host-owned enforcement config. The client is read-mostly; all
975
+ * writes ride the host config path (never run files).
976
+ */
977
+ function RecursiveSettings({ close }) {
978
+ return (0, react.createElement)("div", { className: "rec-settings" }, (0, react.createElement)("h2", {}, "Recursive"), (0, react.createElement)("p", {}, "Enforcement policy, scratch format, provider defaults, and preset install path are configured on the host. The client is read-only (§11.9)."), (0, react.createElement)("button", { onClick: close }, "Close"));
979
+ }
980
+ //#endregion
539
981
  //#region src/client/use-live.ts
540
982
  /**
541
983
  * useLiveProjection(scope): the client bridge to the live host route (SP2 R1).
@@ -1178,6 +1620,316 @@ window.__ModuleLoader__.load({
1178
1620
  color: var(--board-muted-fg);
1179
1621
  }
1180
1622
 
1623
+ /* ===== Per-phase doc viewer (0.2.4, inside the inspector detail body) ===== */
1624
+
1625
+ /* View phase ghost button on each present phase row. */
1626
+ .rec-phase-view {
1627
+ flex: none;
1628
+ padding: 3px 10px;
1629
+ font-size: var(--board-text-xs);
1630
+ color: var(--board-info);
1631
+ background: transparent;
1632
+ border: 1px solid var(--board-border);
1633
+ border-radius: 999px;
1634
+ cursor: pointer;
1635
+ white-space: nowrap;
1636
+ }
1637
+
1638
+ .rec-phase-view:hover {
1639
+ background: var(--board-accent);
1640
+ }
1641
+
1642
+ /* Viewer shell: fills the inspector detail body (the panel owns data-theme). */
1643
+ .rec-doc {
1644
+ display: flex;
1645
+ flex-direction: column;
1646
+ min-height: 0;
1647
+ gap: var(--board-space-12);
1648
+ color: var(--board-fg);
1649
+ font-family: var(--board-font-sans);
1650
+ background: var(--board-bg);
1651
+ border: 1px solid var(--board-border);
1652
+ border-radius: var(--board-radius-xl);
1653
+ overflow: hidden;
1654
+ }
1655
+
1656
+ .rec-doc-header {
1657
+ display: flex;
1658
+ align-items: center;
1659
+ gap: var(--board-space-12);
1660
+ padding: var(--board-space-12) var(--board-space-16);
1661
+ border-bottom: 1px solid var(--board-border);
1662
+ flex: none;
1663
+ }
1664
+
1665
+ .rec-doc-heading {
1666
+ display: flex;
1667
+ align-items: baseline;
1668
+ gap: var(--board-space-12);
1669
+ flex: 1;
1670
+ min-width: 0;
1671
+ }
1672
+
1673
+ .rec-doc-badge {
1674
+ flex: none;
1675
+ padding: 2px 10px;
1676
+ font-size: 11px;
1677
+ font-weight: var(--board-fw-semibold);
1678
+ letter-spacing: 0.05em;
1679
+ text-transform: uppercase;
1680
+ border-radius: 999px;
1681
+ background: var(--board-accent);
1682
+ color: var(--board-muted-fg);
1683
+ }
1684
+
1685
+ .rec-doc-title {
1686
+ margin: 0;
1687
+ font-size: var(--board-text-sm);
1688
+ font-weight: var(--board-fw-semibold);
1689
+ letter-spacing: var(--board-tracking-tight);
1690
+ overflow: hidden;
1691
+ text-overflow: ellipsis;
1692
+ white-space: nowrap;
1693
+ }
1694
+
1695
+ .rec-doc-run {
1696
+ flex: none;
1697
+ font-family: var(--board-font-mono);
1698
+ font-size: var(--board-text-xs);
1699
+ color: var(--board-muted-fg);
1700
+ }
1701
+
1702
+ .rec-doc-btn {
1703
+ flex: none;
1704
+ padding: 5px 12px;
1705
+ font-size: var(--board-text-xs);
1706
+ color: var(--board-fg);
1707
+ background: transparent;
1708
+ border: 1px solid var(--board-border);
1709
+ border-radius: 999px;
1710
+ cursor: pointer;
1711
+ white-space: nowrap;
1712
+ }
1713
+
1714
+ .rec-doc-btn:hover {
1715
+ background: var(--board-accent);
1716
+ }
1717
+
1718
+ /* Search bar (/ to open). */
1719
+ .rec-doc-search {
1720
+ display: flex;
1721
+ align-items: center;
1722
+ gap: var(--board-space-12);
1723
+ padding: var(--board-space-8) var(--board-space-16);
1724
+ border-bottom: 1px solid var(--board-border);
1725
+ flex: none;
1726
+ }
1727
+
1728
+ .rec-doc-search-input {
1729
+ flex: 1 1 auto;
1730
+ background: var(--board-muted);
1731
+ color: var(--board-fg);
1732
+ border: 1px solid var(--board-border);
1733
+ border-radius: var(--board-radius-md);
1734
+ padding: 5px 10px;
1735
+ font-size: var(--board-text-xs);
1736
+ font-family: inherit;
1737
+ min-width: 0;
1738
+ }
1739
+
1740
+ .rec-doc-search-input::placeholder {
1741
+ color: var(--board-muted-fg);
1742
+ opacity: 1;
1743
+ }
1744
+
1745
+ .rec-doc-search-count {
1746
+ font-size: var(--board-text-xs);
1747
+ color: var(--board-muted-fg);
1748
+ white-space: nowrap;
1749
+ }
1750
+
1751
+ /* Body: scrollable line list. */
1752
+ .rec-doc-body {
1753
+ flex: 1 1 auto;
1754
+ min-height: 0;
1755
+ overflow-y: auto;
1756
+ padding: var(--board-space-12) var(--board-space-16) var(--board-space-16);
1757
+ }
1758
+
1759
+ .rec-doc-line {
1760
+ font-size: var(--board-text-sm);
1761
+ line-height: 1.7;
1762
+ padding: 0 6px;
1763
+ border-left: 2px solid transparent;
1764
+ }
1765
+
1766
+ .rec-doc-line-current {
1767
+ background: var(--board-muted);
1768
+ border-left-color: var(--board-info);
1769
+ }
1770
+
1771
+ .rec-doc-blank {
1772
+ height: 10px;
1773
+ }
1774
+
1775
+ .rec-doc-plain {
1776
+ white-space: pre-wrap;
1777
+ }
1778
+
1779
+ .rec-doc-h1 {
1780
+ font-size: 22px;
1781
+ font-weight: 700;
1782
+ margin: 8px 0 6px;
1783
+ line-height: 1.3;
1784
+ }
1785
+
1786
+ .rec-doc-h2 {
1787
+ font-size: 18px;
1788
+ font-weight: 600;
1789
+ margin: 12px 0 4px;
1790
+ line-height: 1.3;
1791
+ }
1792
+
1793
+ .rec-doc-h3 {
1794
+ font-size: 15px;
1795
+ font-weight: 600;
1796
+ margin: 10px 0 4px;
1797
+ line-height: 1.3;
1798
+ }
1799
+
1800
+ .rec-doc-h4 {
1801
+ font-size: 14px;
1802
+ font-weight: 600;
1803
+ margin: 8px 0 4px;
1804
+ line-height: 1.3;
1805
+ }
1806
+
1807
+ .rec-doc-li {
1808
+ display: flex;
1809
+ align-items: baseline;
1810
+ gap: 8px;
1811
+ font-size: var(--board-text-sm);
1812
+ line-height: 1.65;
1813
+ margin: 0 0 4px;
1814
+ }
1815
+
1816
+ .rec-doc-bullet {
1817
+ flex: none;
1818
+ color: var(--board-muted-fg);
1819
+ width: 14px;
1820
+ text-align: center;
1821
+ }
1822
+
1823
+ .rec-doc-li-text {
1824
+ flex: 1;
1825
+ min-width: 0;
1826
+ }
1827
+
1828
+ .rec-doc-pre {
1829
+ margin: 6px 0;
1830
+ padding: var(--board-space-12);
1831
+ background: var(--board-muted);
1832
+ border: 1px solid var(--board-border);
1833
+ border-radius: var(--board-radius-md);
1834
+ overflow-x: auto;
1835
+ }
1836
+
1837
+ .rec-doc-code,
1838
+ .rec-doc-inline-code {
1839
+ font-family: var(--board-font-mono);
1840
+ font-size: 12.5px;
1841
+ letter-spacing: var(--board-tracking-mono);
1842
+ }
1843
+
1844
+ .rec-doc-inline-code {
1845
+ background: var(--board-muted);
1846
+ border: 1px solid var(--board-border);
1847
+ border-radius: 4px;
1848
+ padding: 0 4px;
1849
+ }
1850
+
1851
+ .rec-doc-inline-link {
1852
+ color: var(--board-info);
1853
+ text-decoration: underline;
1854
+ cursor: pointer;
1855
+ }
1856
+
1857
+ .rec-doc-table {
1858
+ width: 100%;
1859
+ border-collapse: collapse;
1860
+ margin: 6px 0;
1861
+ font-size: var(--board-text-xs);
1862
+ }
1863
+
1864
+ .rec-doc-table th,
1865
+ .rec-doc-table td {
1866
+ border: 1px solid var(--board-border);
1867
+ padding: 4px 8px;
1868
+ text-align: left;
1869
+ }
1870
+
1871
+ .rec-doc-table th {
1872
+ background: var(--board-muted);
1873
+ color: var(--board-muted-fg);
1874
+ font-weight: var(--board-fw-semibold);
1875
+ }
1876
+
1877
+ /* Footer: status + key hints. */
1878
+ .rec-doc-footer {
1879
+ flex: none;
1880
+ display: flex;
1881
+ align-items: center;
1882
+ gap: var(--board-space-16);
1883
+ padding: var(--board-space-8) var(--board-space-16);
1884
+ border-top: 1px solid var(--board-border);
1885
+ flex-wrap: wrap;
1886
+ }
1887
+
1888
+ .rec-doc-status {
1889
+ font-size: var(--board-text-xs);
1890
+ color: var(--board-muted-fg);
1891
+ min-width: 120px;
1892
+ }
1893
+
1894
+ .rec-doc-status-error {
1895
+ color: var(--board-error);
1896
+ }
1897
+
1898
+ .rec-doc-status-ok {
1899
+ color: var(--board-success);
1900
+ }
1901
+
1902
+ .rec-doc-hints {
1903
+ display: flex;
1904
+ align-items: center;
1905
+ gap: 2px;
1906
+ flex-wrap: wrap;
1907
+ font-size: var(--board-text-xs);
1908
+ color: var(--board-muted-fg);
1909
+ }
1910
+
1911
+ .rec-doc-hints kbd {
1912
+ background: var(--board-muted);
1913
+ border: 1px solid var(--board-border);
1914
+ border-radius: 4px;
1915
+ padding: 1px 6px;
1916
+ font-family: inherit;
1917
+ font-size: 11px;
1918
+ font-weight: 600;
1919
+ color: var(--board-fg);
1920
+ margin: 0 3px 0 8px;
1921
+ }
1922
+
1923
+ .rec-doc-text {
1924
+ margin: 0;
1925
+ font-size: var(--board-text-sm);
1926
+ color: var(--board-muted-fg);
1927
+ }
1928
+
1929
+ .rec-doc-error {
1930
+ color: var(--board-error);
1931
+ }
1932
+
1181
1933
  /* ===== Strip (session dock) — keeps the shell --dsw-* theme ===== */
1182
1934
  .rec-badge {
1183
1935
  flex: none;
@@ -1407,6 +2159,7 @@ window.__ModuleLoader__.load({
1407
2159
  }
1408
2160
  //#endregion
1409
2161
  exports.Board = Board;
2162
+ exports.DocViewer = DocViewer;
1410
2163
  exports.Inspector = Inspector;
1411
2164
  exports.RecursiveSettings = RecursiveSettings;
1412
2165
  exports.RecursiveView = RecursiveView;
@@ -1417,6 +2170,8 @@ window.__ModuleLoader__.load({
1417
2170
  exports.inject = inject;
1418
2171
  exports.isRecursivePreset = isRecursivePreset;
1419
2172
  exports.listRuns = listRuns;
2173
+ exports.parseDoc = parseDoc;
2174
+ exports.parseInline = parseInline;
1420
2175
  exports.subscribeLiveEvents = subscribeLiveEvents;
1421
2176
  exports.useLiveProjection = useLiveProjection;
1422
2177
  return module.exports;