@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/board.d.ts +7 -1
- package/lib/client/doc-viewer.d.ts +40 -0
- package/lib/client/host-api.d.ts +13 -0
- package/lib/client/index.d.ts +2 -0
- package/lib/client/slots.d.ts +10 -0
- package/lib/client.js +875 -65
- package/lib/index.js +142 -59
- package/lib/live-route.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/board.tsx +14 -7
- package/src/client/doc-viewer.tsx +306 -0
- package/src/client/host-api.ts +28 -0
- package/src/client/index.ts +2 -0
- package/src/client/inspector.tsx +13 -2
- package/src/client/slots.ts +43 -0
- package/src/client/styles.ts +321 -0
- package/src/live-route.ts +69 -2
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:
|
|
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:
|
|
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;
|