@try-works/dsh-recursive-mode 0.2.2 → 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
@@ -335,8 +335,8 @@ window.__ModuleLoader__.load({
335
335
  for (const worktreeRoot of Object.keys(projection)) for (const runId of Object.keys(projection[worktreeRoot])) runs.push(projection[worktreeRoot][runId]);
336
336
  return runs;
337
337
  }
338
- function closeButton$1(onClose) {
339
- if (onClose === void 0) return null;
338
+ function closeButton$1(onClose, variant) {
339
+ if (onClose === void 0 || variant === "view") return null;
340
340
  return (0, react.createElement)("button", {
341
341
  type: "button",
342
342
  className: "rec-close",
@@ -356,7 +356,7 @@ window.__ModuleLoader__.load({
356
356
  "data-pill": kind
357
357
  }, PILL_LABELS[kind]);
358
358
  }
359
- function Board({ snapshot, workspacePath, onOpenInspector, onClose }) {
359
+ function Board({ snapshot, workspacePath, onOpenInspector, onClose, variant = "overlay" }) {
360
360
  const { theme, toggle } = useBoardTheme();
361
361
  if (snapshot === null || snapshot.root === null) return null;
362
362
  const runs = workspacePath !== void 0 && workspacePath !== "" && !sameWorkspacePath(workspacePath, snapshot.root) ? [] : listRuns(snapshot.projection);
@@ -365,21 +365,22 @@ window.__ModuleLoader__.load({
365
365
  worktreeRoot: run.worktreeRoot,
366
366
  runId: run.runId
367
367
  });
368
+ const rootCls = variant === "view" ? "rec-board rec-board-view" : "rec-board";
368
369
  if (runs.length === 0) return (0, react.createElement)("div", {
369
- className: "rec-board",
370
+ className: rootCls,
370
371
  "data-theme": theme,
371
372
  "data-empty": true
372
373
  }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, "0 runs"), (0, react.createElement)(ThemeToggle, {
373
374
  theme,
374
375
  toggle
375
- }), closeButton$1(onClose)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
376
+ }), closeButton$1(onClose, variant)), (0, react.createElement)("p", { className: "rec-board-empty" }, "No recursive runs in this workspace yet."));
376
377
  return (0, react.createElement)("div", {
377
- className: "rec-board",
378
+ className: rootCls,
378
379
  "data-theme": theme
379
380
  }, (0, react.createElement)("header", { className: "rec-board-header" }, (0, react.createElement)("h2", { className: "rec-board-title" }, "Recursive runs"), (0, react.createElement)("span", { className: "rec-board-path" }, headerPath), (0, react.createElement)("span", { className: "rec-board-count" }, runs.length + " runs"), (0, react.createElement)(ThemeToggle, {
380
381
  theme,
381
382
  toggle
382
- }), closeButton$1(onClose)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
383
+ }), closeButton$1(onClose, variant)), (0, react.createElement)("div", { className: "rec-columns" }, KANBAN_LANES.map((lane) => {
383
384
  const laneRuns = runs.filter((r) => columnForRun(r) === lane.id);
384
385
  return (0, react.createElement)("section", {
385
386
  key: lane.id,
@@ -404,64 +405,6 @@ window.__ModuleLoader__.load({
404
405
  })));
405
406
  }
406
407
  //#endregion
407
- //#region src/client/inspector.tsx
408
- /**
409
- * Drill-down inspector (SP2 R1 + run 15/16): centered modal-style detail panel
410
- * (Paper .detail shape) with back + close, data-theme (default light), theme
411
- * toggle, and solid status pills. All from the live host-route snapshot.
412
- * useBoardTheme is hoisted ABOVE the not-found early return.
413
- */
414
- function closeButton(onClose) {
415
- if (onClose === void 0) return null;
416
- return (0, react.createElement)("button", {
417
- type: "button",
418
- className: "rec-close",
419
- onClick: onClose,
420
- title: "Close",
421
- "aria-label": "Close"
422
- }, "×");
423
- }
424
- function solidPill(kind) {
425
- return (0, react.createElement)("span", {
426
- className: "rec-pill",
427
- "data-pill": kind
428
- }, PILL_LABELS[kind]);
429
- }
430
- function detailShell(runId, onBackToToolDetails, onClose, headerExtra, body, theme, toggle) {
431
- return (0, react.createElement)("div", {
432
- className: "rec-inspector",
433
- "data-theme": theme
434
- }, (0, react.createElement)("div", { className: "rec-detail" }, (0, react.createElement)("div", { className: "rec-detail-header" }, (0, react.createElement)("button", {
435
- type: "button",
436
- className: "rec-back",
437
- onClick: onBackToToolDetails
438
- }, "← Back"), (0, react.createElement)("h2", { className: "rec-detail-title" }, runId), headerExtra, (0, react.createElement)(ThemeToggle, {
439
- theme,
440
- toggle
441
- }), closeButton(onClose)), (0, react.createElement)("div", { className: "rec-detail-body" }, body)));
442
- }
443
- function Inspector({ runId, worktreeRoot, snapshot, onBackToToolDetails, onClose }) {
444
- const { theme, toggle } = useBoardTheme();
445
- const card = snapshot?.projection?.[worktreeRoot]?.[runId];
446
- if (card === void 0) return detailShell(runId, onBackToToolDetails, onClose, null, (0, react.createElement)("p", { className: "rec-detail-text" }, "Run not found: " + runId), theme, toggle);
447
- const facts = cardFacts(card);
448
- const rows = expandPhaseRows(card);
449
- 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", {
450
- key: row.phase,
451
- className: "rec-phase-row"
452
- }, (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);
453
- }
454
- //#endregion
455
- //#region src/client/settings.tsx
456
- /**
457
- * Recursive settings page (Phase D R8, §11.8): a settings.section list entry
458
- * surfacing the host-owned enforcement config. The client is read-mostly; all
459
- * writes ride the host config path (never run files).
460
- */
461
- function RecursiveSettings({ close }) {
462
- 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"));
463
- }
464
- //#endregion
465
408
  //#region src/client/host-api.ts
466
409
  /**
467
410
  * Live recursive board/strip feed (SP2 R1): the browser half of the host
@@ -502,6 +445,27 @@ window.__ModuleLoader__.load({
502
445
  }
503
446
  }
504
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
+ /**
505
469
  * Subscribe to the SSE events feed. Returns a disposer. The host pushes a full
506
470
  * {revision, root, projection} frame on every fs change + a 15s heartbeat.
507
471
  *
@@ -535,6 +499,485 @@ window.__ModuleLoader__.load({
535
499
  };
536
500
  }
537
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
538
981
  //#region src/client/use-live.ts
539
982
  /**
540
983
  * useLiveProjection(scope): the client bridge to the live host route (SP2 R1).
@@ -781,6 +1224,17 @@ window.__ModuleLoader__.load({
781
1224
  overflow: hidden;
782
1225
  }
783
1226
 
1227
+ /* Inline board (conversation.view tab): fills its parent view area, not fixed. */
1228
+ .rec-board-view {
1229
+ position: relative;
1230
+ inset: auto;
1231
+ z-index: auto;
1232
+ width: 100%;
1233
+ height: 100%;
1234
+ min-height: 0;
1235
+ flex: 1;
1236
+ }
1237
+
784
1238
  .rec-board-header {
785
1239
  display: flex;
786
1240
  align-items: center;
@@ -1166,6 +1620,316 @@ window.__ModuleLoader__.load({
1166
1620
  color: var(--board-muted-fg);
1167
1621
  }
1168
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
+
1169
1933
  /* ===== Strip (session dock) — keeps the shell --dsw-* theme ===== */
1170
1934
  .rec-badge {
1171
1935
  flex: none;
@@ -1257,9 +2021,51 @@ window.__ModuleLoader__.load({
1257
2021
  onClick: () => boardState.openBoard()
1258
2022
  }, "⧉");
1259
2023
  }
2024
+ /**
2025
+ * RecursiveView: the conversation.view tab body. Renders the run board INLINE
2026
+ * (fills the view area) keyed on the CURRENT workspace, and swaps to the
2027
+ * inspector modal when a run is opened. Read-only (R9): uses the live host
2028
+ * route. No preset gate — the tab is discoverable in any session.
2029
+ */
2030
+ function RecursiveView({ useSessions, useWorkspaces }) {
2031
+ const board = useBoardState();
2032
+ const sessions = useRecursiveSessions(useSessions);
2033
+ const wsPath = currentWorkspacePath(useWorkspaces((s) => s) ?? {
2034
+ items: [],
2035
+ recentWorkspaceId: void 0
2036
+ }, sessions);
2037
+ const snapshot = useLiveProjection({ cwd: wsPath });
2038
+ if (board.selection !== null) return (0, react.createElement)(Inspector, {
2039
+ runId: board.selection.runId,
2040
+ worktreeRoot: board.selection.worktreeRoot,
2041
+ snapshot,
2042
+ onBackToToolDetails: () => boardState.backToBoard(),
2043
+ onClose: () => boardState.close()
2044
+ });
2045
+ return (0, react.createElement)(Board, {
2046
+ snapshot,
2047
+ workspacePath: wsPath,
2048
+ variant: "view",
2049
+ onOpenInspector: (sel) => boardState.openInspector(sel)
2050
+ });
2051
+ }
1260
2052
  function registerSlots(ctx) {
1261
2053
  const disposers = [];
1262
2054
  disposers.push(injectBoardStyles());
2055
+ disposers.push(ctx.slots.inject("conversation.view", () => ctx.slots.register({
2056
+ name: "conversation.view",
2057
+ id: "recursive",
2058
+ order: 20,
2059
+ label: "Recursive"
2060
+ }, (props) => {
2061
+ const useSessions = props?.useSessions;
2062
+ const useWorkspaces = props?.useWorkspaces;
2063
+ if (useSessions === void 0 || useWorkspaces === void 0) return null;
2064
+ return (0, react.createElement)(RecursiveView, {
2065
+ useSessions,
2066
+ useWorkspaces
2067
+ });
2068
+ })));
1263
2069
  disposers.push(ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1264
2070
  name: "sidebar.footer.action",
1265
2071
  id: "recursive",
@@ -1353,8 +2159,10 @@ window.__ModuleLoader__.load({
1353
2159
  }
1354
2160
  //#endregion
1355
2161
  exports.Board = Board;
2162
+ exports.DocViewer = DocViewer;
1356
2163
  exports.Inspector = Inspector;
1357
2164
  exports.RecursiveSettings = RecursiveSettings;
2165
+ exports.RecursiveView = RecursiveView;
1358
2166
  exports.apply = apply;
1359
2167
  exports.currentSessionCwd = currentSessionCwd;
1360
2168
  exports.currentWorkspacePath = currentWorkspacePath;
@@ -1362,6 +2170,8 @@ window.__ModuleLoader__.load({
1362
2170
  exports.inject = inject;
1363
2171
  exports.isRecursivePreset = isRecursivePreset;
1364
2172
  exports.listRuns = listRuns;
2173
+ exports.parseDoc = parseDoc;
2174
+ exports.parseInline = parseInline;
1365
2175
  exports.subscribeLiveEvents = subscribeLiveEvents;
1366
2176
  exports.useLiveProjection = useLiveProjection;
1367
2177
  return module.exports;