@try-works/dsh-recursive-mode 0.4.6 → 0.4.8
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/README.md +11 -4
- package/lib/client/contract.d.ts +10 -0
- package/lib/client/doc-viewer.d.ts +50 -0
- package/lib/client/spec-sheet-view.d.ts +237 -0
- package/lib/client/spec-sheet.d.ts +103 -0
- package/lib/client/use-live.d.ts +17 -1
- package/lib/client.js +988 -14
- package/lib/delegation.d.ts +35 -1
- package/lib/errors.d.ts +13 -0
- package/lib/index.js +164 -9
- package/lib/run-spec.d.ts +78 -0
- package/lib/run-start.d.ts +27 -0
- package/package.json +1 -1
- package/src/client/contract.ts +10 -0
- package/src/client/doc-viewer.tsx +131 -12
- package/src/client/slots.ts +12 -0
- package/src/client/spec-sheet-view.ts +325 -0
- package/src/client/spec-sheet.tsx +409 -0
- package/src/client/styles.ts +298 -1
- package/src/client/use-live.ts +23 -2
- package/src/delegation.ts +58 -4
- package/src/errors.ts +13 -0
- package/src/recursive_ask.tool.ts +28 -1
- package/src/run-spec.ts +148 -0
- package/src/run-start.ts +59 -0
- package/src/runtime.ts +78 -24
package/lib/client.js
CHANGED
|
@@ -522,9 +522,29 @@ window.__ModuleLoader__.load({
|
|
|
522
522
|
* carried over — this viewer reads run artifacts.
|
|
523
523
|
*/
|
|
524
524
|
/**
|
|
525
|
+
* A list item: the text AFTER its marker. The marker is consumed here exactly as the base parser consumed
|
|
526
|
+
* it — `text` is the item's CONTENT, and the renderer draws the `- ` / box back from `kind` + `checked`.
|
|
527
|
+
*/
|
|
528
|
+
const BULLET_RE = /^\s*[-*]\s+(.+)$/;
|
|
529
|
+
/**
|
|
530
|
+
* A task box (`[ ]`, `[x]`, `[X]`) at the front of a list item, split into its tick and the rest of the item.
|
|
531
|
+
*
|
|
532
|
+
* The tick is the one fact a reader of the document cannot recover from the text alone once the box has been
|
|
533
|
+
* recognised, so it travels as `checked`; everything after it is the item's content, as for any other bullet.
|
|
534
|
+
*/
|
|
535
|
+
const TASK_BOX_RE = /^\[([ xX])\]\s*(.*)$/;
|
|
536
|
+
/** A gate reading: `Coverage: FAIL` / `Approval: PASS`, as `run-spec.ts` reads the same lines. */
|
|
537
|
+
const GATE_RE = /^\s*(?:Coverage|Approval)\s*:\s*(PASS|FAIL)\s*$/i;
|
|
538
|
+
/**
|
|
525
539
|
* Markdown -> line tokens. Base is parsePlan (blank/h1-h4/li/plain, MIT),
|
|
526
540
|
* extended for fenced code blocks (one code line per block) and pipe tables
|
|
527
541
|
* (one table line per block, header separator row dropped).
|
|
542
|
+
*
|
|
543
|
+
* AND EXTENDED FOR WHAT THE RUN ARTIFACTS ACTUALLY CONTAIN — the task boxes and gate readings the
|
|
544
|
+
* scaffolded `00-requirements.md` ships (`- [ ] …`, `Coverage: FAIL`, `Approval: FAIL`), because a preview
|
|
545
|
+
* that renders an unticked box and a FAIL gate as generic body text hides the two marks a person who is
|
|
546
|
+
* being asked to approve the document most needs to see. Both are additive FIELDS on the existing `li` and
|
|
547
|
+
* `plain` kinds, so no line is retyped, no character is dropped, and every existing caller keeps working.
|
|
528
548
|
*/
|
|
529
549
|
function parseDoc(plan) {
|
|
530
550
|
const raw = String(plan == null ? "" : plan).split("\n");
|
|
@@ -591,11 +611,26 @@ window.__ModuleLoader__.load({
|
|
|
591
611
|
i += 1;
|
|
592
612
|
continue;
|
|
593
613
|
}
|
|
594
|
-
const
|
|
595
|
-
if (
|
|
596
|
-
|
|
614
|
+
const item = BULLET_RE.exec(line);
|
|
615
|
+
if (item) {
|
|
616
|
+
const box = TASK_BOX_RE.exec(item[1]);
|
|
617
|
+
out.push(box === null ? {
|
|
618
|
+
kind: "li",
|
|
619
|
+
text: item[1]
|
|
620
|
+
} : {
|
|
597
621
|
kind: "li",
|
|
598
|
-
text:
|
|
622
|
+
text: box[2],
|
|
623
|
+
checked: box[1] !== " "
|
|
624
|
+
});
|
|
625
|
+
i += 1;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const gate = GATE_RE.exec(line);
|
|
629
|
+
if (gate) {
|
|
630
|
+
out.push({
|
|
631
|
+
kind: "plain",
|
|
632
|
+
text: line,
|
|
633
|
+
gate: gate[1].toUpperCase() === "FAIL" ? "fail" : "pass"
|
|
599
634
|
});
|
|
600
635
|
i += 1;
|
|
601
636
|
continue;
|
|
@@ -671,11 +706,39 @@ window.__ModuleLoader__.load({
|
|
|
671
706
|
});
|
|
672
707
|
}
|
|
673
708
|
/**
|
|
709
|
+
* The mark of a task box.
|
|
710
|
+
*
|
|
711
|
+
* ⚠ THE MARK REPLACES `[ ]` / `[x]` IN PLACE, GLYPH FOR GLYPH. The parser hands over the item's content
|
|
712
|
+
* without the box, so what the reader sees is `- [ ] item` where the document says `- [x] item`: same line,
|
|
713
|
+
* same position, same length of reading — and the box is still legible as a box rather than as an assertion
|
|
714
|
+
* about the item.
|
|
715
|
+
*
|
|
716
|
+
* ⚠ AND THE TICK IS CARRIED TWICE — once as that glyph for the eye and once as text, visually hidden, for the
|
|
717
|
+
* ear — because the two bracket forms are read inconsistently by screen readers, and the whole point of the
|
|
718
|
+
* mark is that "this box is not ticked" survives every way of reading it. This span carries NO separator of
|
|
719
|
+
* its own: the item's text follows it directly, separated by the leading space on that text (see
|
|
720
|
+
* `lineElement`), so that neither side of the boundary has a trailing space to lose.
|
|
721
|
+
*/
|
|
722
|
+
function todoMark(line, key) {
|
|
723
|
+
const done = line.checked === true;
|
|
724
|
+
return (0, react.createElement)("span", {
|
|
725
|
+
key,
|
|
726
|
+
className: "rec-doc-todo-check" + (done ? " rec-doc-todo-check-on" : " rec-doc-todo-check-off"),
|
|
727
|
+
title: done ? "done" : "not done"
|
|
728
|
+
}, (0, react.createElement)("span", { "aria-hidden": "true" }, done ? "[x]" : "[ ]"), (0, react.createElement)("span", { className: "rec-doc-sr" }, done ? "done:" : "not done:"));
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
674
731
|
* Render one parsed line as a React element. Headings/bullets get parsePlan
|
|
675
732
|
* sizing; code/table get block layout; inline markup applies to plain-ish text.
|
|
733
|
+
*
|
|
734
|
+
* ⚠ ONE RENDERER, TWO READERS. `DocViewer` and the run-start spec sheet's preview both come through here,
|
|
735
|
+
* so a mark that means "unticked box" or "gate reads FAIL" cannot mean one thing in the phase-doc viewer and
|
|
736
|
+
* another in the document a person is approving. Exported for that reason alone.
|
|
676
737
|
*/
|
|
677
|
-
function lineElement(line, i, isCurrent) {
|
|
738
|
+
function lineElement(line, i, isCurrent = false) {
|
|
678
739
|
const cls = "rec-doc-line rec-doc-" + line.kind + (isCurrent ? " rec-doc-line-current" : "");
|
|
740
|
+
const gateCls = line.gate === void 0 ? "" : " rec-doc-gate rec-doc-gate-" + line.gate;
|
|
741
|
+
const todoCls = line.checked === void 0 ? "" : " rec-doc-todo" + (line.checked ? " rec-doc-todo-done" : " rec-doc-todo-open");
|
|
679
742
|
if (line.kind === "blank") return (0, react.createElement)("div", {
|
|
680
743
|
key: i,
|
|
681
744
|
"data-line": String(i),
|
|
@@ -697,18 +760,42 @@ window.__ModuleLoader__.load({
|
|
|
697
760
|
}, (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)))))));
|
|
698
761
|
}
|
|
699
762
|
const nodes = inlineNodes(parseInline(line.text), String(i));
|
|
763
|
+
const SPACER = "\xA0";
|
|
764
|
+
if (line.kind === "li" && line.checked !== void 0) return (0, react.createElement)("div", {
|
|
765
|
+
key: i,
|
|
766
|
+
"data-line": String(i),
|
|
767
|
+
className: cls + todoCls
|
|
768
|
+
}, (0, react.createElement)("span", { className: "rec-doc-bullet" }, "-"), todoMark(line, "todo-" + String(i)), (0, react.createElement)("span", { className: "rec-doc-li-text" }, SPACER, nodes));
|
|
700
769
|
if (line.kind === "li") return (0, react.createElement)("div", {
|
|
701
770
|
key: i,
|
|
702
771
|
"data-line": String(i),
|
|
703
772
|
className: cls
|
|
704
|
-
}, (0, react.createElement)("span", { className: "rec-doc-bullet" }, "
|
|
773
|
+
}, (0, react.createElement)("span", { className: "rec-doc-bullet" }, "-"), (0, react.createElement)("span", { className: "rec-doc-li-text" }, SPACER, nodes));
|
|
705
774
|
return (0, react.createElement)("div", {
|
|
706
775
|
key: i,
|
|
707
776
|
"data-line": String(i),
|
|
708
|
-
className: cls
|
|
777
|
+
className: cls + gateCls
|
|
709
778
|
}, nodes);
|
|
710
779
|
}
|
|
711
780
|
/**
|
|
781
|
+
* The parsed lines as elements — the preview built from `parseDoc` + `lineElement`, with no shell of its own.
|
|
782
|
+
*
|
|
783
|
+
* ⚠ THIS IS WHAT MAKES A SECOND RENDERER UNNECESSARY. Any surface that wants to show a run artifact as a
|
|
784
|
+
* PREVIEW (the phase-doc viewer's body, the run-start spec sheet's document body) renders these nodes inside
|
|
785
|
+
* whatever frame it owns, so the markdown is parsed and drawn exactly once in the plugin. `keyBase` namespaces
|
|
786
|
+
* the React keys when several of these are on screen at once; `current` is the vim cursor line, which the
|
|
787
|
+
* spec sheet never sets.
|
|
788
|
+
*/
|
|
789
|
+
function PreviewLines({ lines, keyBase = "doc", current = -1 }) {
|
|
790
|
+
return (0, react.createElement)("div", {
|
|
791
|
+
className: "rec-doc-lines",
|
|
792
|
+
"data-preview-lines": String(lines.length)
|
|
793
|
+
}, ...lines.map((line, i) => (0, react.createElement)("div", {
|
|
794
|
+
key: keyBase + "-line-" + String(i),
|
|
795
|
+
className: "rec-doc-line-wrap"
|
|
796
|
+
}, lineElement(line, i, i === current))));
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
712
799
|
* The per-phase doc viewer. Fetches the route on mount / fileName change.
|
|
713
800
|
* Vim nav + / search + n/N + Esc; y copies the doc. Esc closes search first,
|
|
714
801
|
* else the viewer. data-theme is passed down by the hoisting Inspector
|
|
@@ -878,9 +965,6 @@ window.__ModuleLoader__.load({
|
|
|
878
965
|
else onClose();
|
|
879
966
|
}
|
|
880
967
|
};
|
|
881
|
-
new Set(matches);
|
|
882
|
-
matches.length > 0 && matches[activeMatch];
|
|
883
|
-
const lineEls = docLines.map((line, i) => lineElement(line, i, i === cursor));
|
|
884
968
|
const searchBar = searchOpen ? (0, react.createElement)("div", { className: "rec-doc-search" }, (0, react.createElement)("input", {
|
|
885
969
|
className: "rec-doc-search-input",
|
|
886
970
|
value: query,
|
|
@@ -908,7 +992,11 @@ window.__ModuleLoader__.load({
|
|
|
908
992
|
onClick: onClose,
|
|
909
993
|
title: "Close",
|
|
910
994
|
"aria-label": "Close"
|
|
911
|
-
}, "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 ?
|
|
995
|
+
}, "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 ? (0, react.createElement)(PreviewLines, {
|
|
996
|
+
lines: docLines,
|
|
997
|
+
keyBase: "doc",
|
|
998
|
+
current: cursor
|
|
999
|
+
}) : null), (0, react.createElement)("footer", { className: "rec-doc-footer" }, (0, react.createElement)("div", {
|
|
912
1000
|
className: statusCls,
|
|
913
1001
|
role: "status"
|
|
914
1002
|
}, 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 ? " |" : ""))))));
|
|
@@ -998,10 +1086,18 @@ window.__ModuleLoader__.load({
|
|
|
998
1086
|
* Subscribe to the live route for one scope. Returns the current state. The SSE
|
|
999
1087
|
* feed pushes full frames on every fs change; a dropped stream (sleep/error)
|
|
1000
1088
|
* triggers one refetch so the board never serves a stale fold.
|
|
1089
|
+
*
|
|
1090
|
+
* @param scope - sessionId PRIMARY + cwd fallback hint (host resolves the root).
|
|
1091
|
+
* @param options - `enabled: false` clears the snapshot and never fetches.
|
|
1001
1092
|
*/
|
|
1002
|
-
function useLiveProjection(scope) {
|
|
1093
|
+
function useLiveProjection(scope, options = {}) {
|
|
1094
|
+
const { enabled = true } = options;
|
|
1003
1095
|
const [state, setState] = (0, react.useState)(null);
|
|
1004
1096
|
(0, react.useEffect)(() => {
|
|
1097
|
+
if (!enabled) {
|
|
1098
|
+
setState(null);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1005
1101
|
if (isEmpty(scope)) {
|
|
1006
1102
|
setState(null);
|
|
1007
1103
|
return;
|
|
@@ -1018,7 +1114,11 @@ window.__ModuleLoader__.load({
|
|
|
1018
1114
|
disposed = true;
|
|
1019
1115
|
disposeEvents();
|
|
1020
1116
|
};
|
|
1021
|
-
}, [
|
|
1117
|
+
}, [
|
|
1118
|
+
scope.sessionId,
|
|
1119
|
+
scope.cwd,
|
|
1120
|
+
enabled
|
|
1121
|
+
]);
|
|
1022
1122
|
return state;
|
|
1023
1123
|
}
|
|
1024
1124
|
//#endregion
|
|
@@ -1305,6 +1405,579 @@ window.__ModuleLoader__.load({
|
|
|
1305
1405
|
});
|
|
1306
1406
|
}
|
|
1307
1407
|
//#endregion
|
|
1408
|
+
//#region src/run-spec.ts
|
|
1409
|
+
/** The named evidence classes, so a reader can tell a placeholder from an unmet gate. */
|
|
1410
|
+
const ARTIFACT_MARKER_IDS = {
|
|
1411
|
+
placeholder: "placeholder",
|
|
1412
|
+
uncheckedTodo: "unchecked-todo",
|
|
1413
|
+
failedGate: "failed-gate"
|
|
1414
|
+
};
|
|
1415
|
+
/** Every marker, in the order they are reported for a single line. */
|
|
1416
|
+
const MARKER_PATTERNS = [
|
|
1417
|
+
{
|
|
1418
|
+
id: ARTIFACT_MARKER_IDS.placeholder,
|
|
1419
|
+
re: /(^\s*\.\.\.\s*$)|(\[[^[\]\n<>]{2,120}\])|(<[^<>\n]{2,120}>)/
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
id: ARTIFACT_MARKER_IDS.uncheckedTodo,
|
|
1423
|
+
re: /^\s*[-*]\s*\[ \]/
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
id: ARTIFACT_MARKER_IDS.failedGate,
|
|
1427
|
+
re: /^\s*(Coverage|Approval):\s*FAIL\b/i
|
|
1428
|
+
}
|
|
1429
|
+
];
|
|
1430
|
+
/**
|
|
1431
|
+
* Which marker a single line carries, or null.
|
|
1432
|
+
*
|
|
1433
|
+
* Exported because the client prints the marker NAMES beside the quoted lines, and a second classifier that
|
|
1434
|
+
* re-derived them would be a second answer to the same question.
|
|
1435
|
+
*/
|
|
1436
|
+
function markerIdsOnLine(line) {
|
|
1437
|
+
const ids = [];
|
|
1438
|
+
for (const pattern of MARKER_PATTERNS) if (pattern.re.test(line)) ids.push(pattern.id);
|
|
1439
|
+
return ids;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Classify one artifact's text.
|
|
1443
|
+
*
|
|
1444
|
+
* A verdict of `unfilled` means the document still carries the template's own placeholder text — the
|
|
1445
|
+
* evidence travels with it, line by line, so the refusal (and the client notice) can name what is missing
|
|
1446
|
+
* instead of asserting a state the reader cannot check.
|
|
1447
|
+
*/
|
|
1448
|
+
function classifyArtifact(text) {
|
|
1449
|
+
const lines = text.split(/\r?\n/);
|
|
1450
|
+
const hits = [];
|
|
1451
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1452
|
+
const line = lines[i];
|
|
1453
|
+
for (const id of markerIdsOnLine(line)) hits.push({
|
|
1454
|
+
id,
|
|
1455
|
+
line: i + 1,
|
|
1456
|
+
text: line.trim()
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
verdict: hits.some((hit) => hit.id === ARTIFACT_MARKER_IDS.placeholder) ? "unfilled" : "filled",
|
|
1461
|
+
hits
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
/** The unfilled evidence only (what a refusal names). */
|
|
1465
|
+
function unfilledEvidence(result) {
|
|
1466
|
+
return result.hits.filter((hit) => hit.id === ARTIFACT_MARKER_IDS.placeholder);
|
|
1467
|
+
}
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/client/spec-sheet-view.ts
|
|
1470
|
+
/**
|
|
1471
|
+
* Pure display model for the RUN-START SPEC SHEET.
|
|
1472
|
+
*
|
|
1473
|
+
* WHY THIS MODULE EXISTS. `src/client/spec-sheet.tsx` renders the Phase 0 document beside the run-start
|
|
1474
|
+
* question so a person can read what they are approving. Everything that DECIDES what the sheet says is
|
|
1475
|
+
* here instead: reading the tool call's own arguments, classifying the document, and choosing the fetch
|
|
1476
|
+
* state. It is pure (no React, no fs, no session window), which is what lets the spec drive every branch —
|
|
1477
|
+
* a stub, a filled document, a missing file, a failed route, a call with no runId — without a browser.
|
|
1478
|
+
*
|
|
1479
|
+
* ⚠ THE VERDICT COMES FROM THE TEXT, NEVER FROM THE FETCH. `loading`, `absent`, `error` and `loaded` are
|
|
1480
|
+
* four different facts, and a single "not loaded" would collapse them — which is how a client comes to
|
|
1481
|
+
* render an empty box for a failure and a failure for a document nobody has written yet.
|
|
1482
|
+
*
|
|
1483
|
+
* ⚠ AND IT IS NODE-FREE. This file is reached by the BROWSER bundle. Reading the artifact belongs to the
|
|
1484
|
+
* server half (`run-start.ts`, `recursive_ask.tool.ts`); this half only ever DISPLAYS what the read-only
|
|
1485
|
+
* `/doc` route returned.
|
|
1486
|
+
*/
|
|
1487
|
+
/**
|
|
1488
|
+
* The wire name of the tool whose call row the sheet replaces.
|
|
1489
|
+
*
|
|
1490
|
+
* `tool.call.toolview` is a KEYED seat dispatched by this exact string, so a typo renders nothing at all
|
|
1491
|
+
* rather than rendering wrongly — the failure mode the harness documents for a keyed seat.
|
|
1492
|
+
*/
|
|
1493
|
+
const RUN_START_TOOL_NAME = "recursive_ask";
|
|
1494
|
+
/**
|
|
1495
|
+
* The Phase 0 artifact the run-start decision is recorded in.
|
|
1496
|
+
*
|
|
1497
|
+
* ⚠ DUPLICATED DELIBERATELY, AND PINNED BY A TEST. `run-start.ts` holds the same string, but importing it
|
|
1498
|
+
* here would pull `node:fs` / `node:crypto` into the browser bundle through `status.ts` and take the page
|
|
1499
|
+
* down for a constant. `tests/spec-sheet.spec.ts` asserts this value EQUALS `RUN_START_ARTIFACT`, so the copy
|
|
1500
|
+
* cannot drift; the client only ever reads through it and never writes.
|
|
1501
|
+
*/
|
|
1502
|
+
const RUN_START_SPEC_FILE = "00-requirements.md";
|
|
1503
|
+
/** The gate id whose question this sheet accompanies. */
|
|
1504
|
+
const RUN_START_GATE_ID = "run-start";
|
|
1505
|
+
/** The all-null argument set: what a malformed, truncated, or absent payload yields. */
|
|
1506
|
+
const NO_ARGS = {
|
|
1507
|
+
gate: null,
|
|
1508
|
+
runId: null,
|
|
1509
|
+
artifact: null,
|
|
1510
|
+
answer: null
|
|
1511
|
+
};
|
|
1512
|
+
/**
|
|
1513
|
+
* Read the arguments out of the raw JSON the model dispatched.
|
|
1514
|
+
*
|
|
1515
|
+
* A malformed or empty payload yields all-nulls rather than throwing: a truncated mid-stream argument string
|
|
1516
|
+
* is a normal sight on this seat (the harness exposes preparing calls with no arguments at all), and a view
|
|
1517
|
+
* that threw on one would take the transcript down with it.
|
|
1518
|
+
*/
|
|
1519
|
+
function parseRecursiveAskArgs(argsRaw) {
|
|
1520
|
+
if (argsRaw === null || argsRaw.trim() === "") return { ...NO_ARGS };
|
|
1521
|
+
let parsed;
|
|
1522
|
+
try {
|
|
1523
|
+
parsed = JSON.parse(argsRaw);
|
|
1524
|
+
} catch {
|
|
1525
|
+
return { ...NO_ARGS };
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof parsed !== "object" || parsed === null) return { ...NO_ARGS };
|
|
1528
|
+
const record = parsed;
|
|
1529
|
+
const str = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
1530
|
+
return {
|
|
1531
|
+
gate: str(record.gate),
|
|
1532
|
+
runId: str(record.runId),
|
|
1533
|
+
artifact: str(record.artifact),
|
|
1534
|
+
answer: str(record.answer)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Read the call's arguments out of whichever phase the owner supplied.
|
|
1539
|
+
*
|
|
1540
|
+
* ⚠ THE THREE FAILURES ARE KEPT APART. "No block", "the arguments have not been dispatched yet" and "the
|
|
1541
|
+
* window truncated the call" are three different facts, and a client that collapsed them into one empty
|
|
1542
|
+
* argument set would tell a person the call named no run — a claim about the CALLER, made on evidence about
|
|
1543
|
+
* the CLIENT.
|
|
1544
|
+
*/
|
|
1545
|
+
function readCall(block) {
|
|
1546
|
+
if (block === null || block === void 0) return {
|
|
1547
|
+
ok: false,
|
|
1548
|
+
reason: "missing"
|
|
1549
|
+
};
|
|
1550
|
+
if (block.phase === "preparing") return {
|
|
1551
|
+
ok: false,
|
|
1552
|
+
reason: "preparing"
|
|
1553
|
+
};
|
|
1554
|
+
if (block.phase === "start") return {
|
|
1555
|
+
ok: true,
|
|
1556
|
+
args: parseRecursiveAskArgs(block.argsRaw)
|
|
1557
|
+
};
|
|
1558
|
+
const carried = block.call ?? null;
|
|
1559
|
+
return carried === null ? {
|
|
1560
|
+
ok: false,
|
|
1561
|
+
reason: "truncated"
|
|
1562
|
+
} : {
|
|
1563
|
+
ok: true,
|
|
1564
|
+
args: parseRecursiveAskArgs(carried.argsRaw)
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/** Is this the run-start gate? Everything else keeps the generic tool row. */
|
|
1568
|
+
function isRunStartCall(args) {
|
|
1569
|
+
return args.gate === RUN_START_GATE_ID;
|
|
1570
|
+
}
|
|
1571
|
+
/** Whether this call is one the sheet claims at all: the run-start gate, with a run named. */
|
|
1572
|
+
function isSpecSheetCall(read) {
|
|
1573
|
+
return read.ok && isRunStartCall(read.args) && read.args.runId !== null;
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Build the sheet's model from what the client actually has.
|
|
1577
|
+
*
|
|
1578
|
+
* ⚠ THE TEXT IS ONLY EVER TAKEN FROM A `loaded` FETCH. A stale body from a previous run is not shown against
|
|
1579
|
+
* a new run id, because "this is the document" is the one claim this seat exists to make truthfully.
|
|
1580
|
+
*/
|
|
1581
|
+
function specSheetModel(input) {
|
|
1582
|
+
const file = input.file ?? "00-requirements.md";
|
|
1583
|
+
const text = input.fetch.state === "loaded" ? input.fetch.text ?? null : null;
|
|
1584
|
+
const verdict = text === null ? null : classifyArtifact(text);
|
|
1585
|
+
return {
|
|
1586
|
+
runId: input.args.runId,
|
|
1587
|
+
root: input.root,
|
|
1588
|
+
file,
|
|
1589
|
+
state: input.fetch.state,
|
|
1590
|
+
error: input.fetch.error ?? null,
|
|
1591
|
+
text,
|
|
1592
|
+
verdict: verdict === null ? null : verdict.verdict,
|
|
1593
|
+
evidence: verdict === null ? [] : verdict.hits,
|
|
1594
|
+
args: input.args
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
/** The document's path, as it should be PRINTED (never as it is fetched). */
|
|
1598
|
+
function specPath(runId, root, file) {
|
|
1599
|
+
const at = ".recursive/run/" + (runId ?? "<runId>") + "/" + file;
|
|
1600
|
+
return root === null || root.trim() === "" ? at : at + " (in " + root + ")";
|
|
1601
|
+
}
|
|
1602
|
+
/** The mode a freshly mounted sheet opens in: the rendered document. */
|
|
1603
|
+
const SPEC_DEFAULT_MODE = "preview";
|
|
1604
|
+
/** The toggle's own label, ALWAYS naming the mode that is on screen — never the one a click would reach. */
|
|
1605
|
+
const SPEC_MODE_LABEL = {
|
|
1606
|
+
preview: "Showing: rendered preview",
|
|
1607
|
+
raw: "Showing: raw source"
|
|
1608
|
+
};
|
|
1609
|
+
/** What the toggle does next, in the imperative, so the control reads as an action and its state reads apart. */
|
|
1610
|
+
const SPEC_TOGGLE_LABEL = {
|
|
1611
|
+
preview: "View source",
|
|
1612
|
+
raw: "Back to preview"
|
|
1613
|
+
};
|
|
1614
|
+
/** One line describing what is on screen, in both modes, for the announced status. */
|
|
1615
|
+
const SPEC_MODE_NOTE = {
|
|
1616
|
+
preview: "The document is rendered here as a preview: headings, lists, code and tables are drawn as such.",
|
|
1617
|
+
raw: "The document is shown here VERBATIM — the exact bytes, with nothing rendered and nothing removed."
|
|
1618
|
+
};
|
|
1619
|
+
/** The other mode — what one press of the toggle reaches. */
|
|
1620
|
+
function otherMode(mode) {
|
|
1621
|
+
return mode === "preview" ? "raw" : "preview";
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Describe the toggle for a mode.
|
|
1625
|
+
*
|
|
1626
|
+
* ⚠ `pressed` IS DERIVED FROM THE MODE, NEVER STORED BESIDE IT. A second boolean that could disagree with the
|
|
1627
|
+
* mode is how a control comes to say "pressed" while the rendered document is on screen — and this is the one
|
|
1628
|
+
* control whose whole job is to tell the reader which of the two things they are looking at.
|
|
1629
|
+
*/
|
|
1630
|
+
function specViewToggle(mode) {
|
|
1631
|
+
return {
|
|
1632
|
+
mode,
|
|
1633
|
+
action: SPEC_TOGGLE_LABEL[mode],
|
|
1634
|
+
pressed: mode === "raw",
|
|
1635
|
+
announce: SPEC_MODE_LABEL[mode] + ". " + SPEC_MODE_NOTE[mode]
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Decide the body content for a document text and a mode.
|
|
1640
|
+
*
|
|
1641
|
+
* ⚠ NULL TEXT IS NOT `''`. A `null` document is one that was never loaded, and it must not be printed as an
|
|
1642
|
+
* empty document — "there is nothing here" and "the document is empty" are different claims, and this seat
|
|
1643
|
+
* exists to make claims a reader can trust.
|
|
1644
|
+
*/
|
|
1645
|
+
function specBodyContent(text, mode) {
|
|
1646
|
+
if (text === null) return null;
|
|
1647
|
+
return mode === "raw" ? {
|
|
1648
|
+
mode: "raw",
|
|
1649
|
+
text
|
|
1650
|
+
} : {
|
|
1651
|
+
mode: "preview",
|
|
1652
|
+
text
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
/** What the sheet says while the call is still preparing (its arguments are not dispatched yet). */
|
|
1656
|
+
const PREPARING_NOTICE = "This tool call has not been dispatched yet, so its arguments — and the run it names — are not available.";
|
|
1657
|
+
/** What the sheet says when the loaded window truncated the call head away. */
|
|
1658
|
+
const TRUNCATED_NOTICE = "The loaded window left this call's own arguments outside it, so the run spec it names cannot be resolved here.";
|
|
1659
|
+
/** What each offered label means, quoted from the gate's own option descriptions. */
|
|
1660
|
+
const RUN_START_OPTION_MEANING = "“Start run” records the approval and arms the run goal; “Hold” leaves the spec inert: no run goal, no autonomous rounds.";
|
|
1661
|
+
/**
|
|
1662
|
+
* The recorded decision, when this call carries one.
|
|
1663
|
+
*
|
|
1664
|
+
* ⚠ A `Hold` IS NOT AN APPROVAL and is not printed as one. The test is the same VALUE test the server makes
|
|
1665
|
+
* (`run-start.ts::isRunStartApproval` — the approving label, not the mere presence of a decision line), so
|
|
1666
|
+
* the sheet can never describe a hold as an approval.
|
|
1667
|
+
*/
|
|
1668
|
+
function decisionLine(answer) {
|
|
1669
|
+
if (answer === null) return null;
|
|
1670
|
+
return answer.trim() === "Start run" ? "This call records: " + answer + " — the approving label, which arms the run goal." : "This call records: " + answer + " — not the approving label, so no run goal is armed.";
|
|
1671
|
+
}
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region src/client/spec-sheet.tsx
|
|
1674
|
+
/**
|
|
1675
|
+
* THE RUN-START SPEC SHEET — the document beside the question it decides.
|
|
1676
|
+
*
|
|
1677
|
+
* THE DEFECT THIS ANSWERS. The owner asked for a run spec, the plugin scaffolded one and raised the
|
|
1678
|
+
* `run-start` gate — "start this run or hold?" — and NOBODY WAS SHOWN THE DOCUMENT:
|
|
1679
|
+
* *"the card ui for accepting the spec appeared, but i was never shown the spec before that so how could i
|
|
1680
|
+
* approve if i havent seen it"*. The gate was decidable before it was readable.
|
|
1681
|
+
*
|
|
1682
|
+
* ⚠ WHERE THIS RENDERS, AND WHY NOT IN `conversation.approval.detail`. That seat was the first candidate —
|
|
1683
|
+
* its catalog summary reads "Optional detail for the Tool call correlated with an approval request" — and it
|
|
1684
|
+
* is the WRONG one, for a reason that is structural rather than stylistic:
|
|
1685
|
+
*
|
|
1686
|
+
* 1. `conversation.composer` is a CHAIN slot: its entries' selectors run in order and the FIRST non-null
|
|
1687
|
+
* match renders. `ui-approval` claims it with `select: pendingInteraction instanceof PendingApproval`
|
|
1688
|
+
* and declares `conversation.approval.detail` as its child; `ui-user-questions` claims it with
|
|
1689
|
+
* `select: pendingInteraction instanceof PendingQuestion` and declares
|
|
1690
|
+
* `conversation.plan-review.actions` instead. They are two mutually exclusive cells of ONE chain.
|
|
1691
|
+
* 2. The run-start gate is asked through the USER-QUESTIONS channel (`askRunStartDirectly` →
|
|
1692
|
+
* `channel.ask(...)`, correlated by `wait: { callId: exec.callId }`), so while it is pending the
|
|
1693
|
+
* pending interaction IS a `PendingQuestion` — the question composer owns the composer, and
|
|
1694
|
+
* `conversation.approval.detail` is not mounted at all. A sheet registered there would render on the
|
|
1695
|
+
* one occasion it is not needed and never on the one it is.
|
|
1696
|
+
* 3. `tool.call.toolview` keyed by tool name is the seat that exists exactly while THIS tool call is on
|
|
1697
|
+
* screen, is unclaimed for `recursive_ask` (the harness's own `ask_user_question` IS claimed, which is
|
|
1698
|
+
* the parallel that matters), and hands the view its own `callId`, the session `cwd` and the frozen
|
|
1699
|
+
* call block — which is how the `runId` is read out of the call's own arguments.
|
|
1700
|
+
*
|
|
1701
|
+
* WHAT IT SHOWS — AND THE DEFECT THAT CHANGED IT. This seat first showed the ACTUAL bytes of
|
|
1702
|
+
* `.recursive/run/<runId>/00-requirements.md` as RAW TEXT, on the reasoning that verbatim is the honest
|
|
1703
|
+
* thing to print. The honest answer to "is that a preview?" was NO, and the owner was being asked to APPROVE
|
|
1704
|
+
* what they read: *reading `##` headings and pipe-table syntax is not reviewing a spec*. So the default is now
|
|
1705
|
+
* a RENDERED PREVIEW — headings, lists, fenced code and pipe tables drawn as such — and the verbatim text is
|
|
1706
|
+
* one press away behind "View source".
|
|
1707
|
+
*
|
|
1708
|
+
* ⚠ AND THE RAW MODE IS NOT DECORATION. A preview is an INTERPRETATION, and this seat asks a person to
|
|
1709
|
+
* approve a document on the strength of it. The raw view exists so that "the preview is not paraphrasing or
|
|
1710
|
+
* hiding anything" is a claim the reader can CHECK against the bytes rather than one they must take from the
|
|
1711
|
+
* renderer. Its state is carried by `aria-pressed` and repeated in a `role="status"` line that names the mode
|
|
1712
|
+
* on screen, so the mode is announced and not merely coloured.
|
|
1713
|
+
*
|
|
1714
|
+
* ⚠ ONE RENDERER, NOT TWO. The markdown is parsed and drawn by `doc-viewer.tsx` — `parseDoc` and its
|
|
1715
|
+
* `PreviewLines` — the same code path the phase-doc viewer uses. A second markdown renderer in one plugin
|
|
1716
|
+
* would be a second answer to "what does this document say", which is the defect this plugin exists to
|
|
1717
|
+
* refuse. What `parseDoc` could not carry for a real `00-requirements.md` (the task boxes and the gate
|
|
1718
|
+
* readings the template ships) was added THERE, for both readers, rather than forked here.
|
|
1719
|
+
*
|
|
1720
|
+
* WHAT IT STILL REFUSES TO DO. It is READ-ONLY (R9): the sheet has no approve/hold control, because a second
|
|
1721
|
+
* control that looks like the question card's would be a second way to answer one question — the sheet may
|
|
1722
|
+
* change HOW the document is shown, never WHETHER it is approved. And the honesty rule of
|
|
1723
|
+
* `settings-view.ts` is unchanged: when the artifact is still the unfilled template the rendered view says so
|
|
1724
|
+
* loudly and the unfilled marks stay visible AS unfilled (`☐ …` items and `Coverage: FAIL` gates are drawn as
|
|
1725
|
+
* exactly that), so a pretty preview of an empty form can never read as an approvable spec.
|
|
1726
|
+
*
|
|
1727
|
+
* ACCESSIBILITY. The outer element is a `region` named by its own heading (`aria-labelledby`); the document
|
|
1728
|
+
* body is a focusable (`tabIndex=0`) scrolling box with its own `aria-label` and `aria-readonly`, so the
|
|
1729
|
+
* keyboard reaches the text and can scroll it without the decision controls leaving the viewport (the cap is
|
|
1730
|
+
* CSS `max-height` with `overflow-y: auto`). The box is the SAME element in both modes, so toggling never
|
|
1731
|
+
* drops focus. Escape is swallowed deliberately, because on a dialog-ish surface Escape is the key that
|
|
1732
|
+
* dismisses and dismissing this sheet must never be read as a decision; and the one transition it has is none
|
|
1733
|
+
* under `prefers-reduced-motion` (see the `.rec-spec` rules in `styles.ts`). No modal is used: nothing here
|
|
1734
|
+
* needs a focus trap, and a trap would take the keyboard away from the decision controls.
|
|
1735
|
+
*/
|
|
1736
|
+
const EMPTY_WORKSPACES = {
|
|
1737
|
+
items: [],
|
|
1738
|
+
recentWorkspaceId: void 0
|
|
1739
|
+
};
|
|
1740
|
+
/**
|
|
1741
|
+
* A scope that resolves nothing.
|
|
1742
|
+
*
|
|
1743
|
+
* The hook stays UNCONDITIONAL (the run 13 lesson: a conditional hook changed the hook count across renders
|
|
1744
|
+
* and threw "Rendered more hooks than during the previous render"), so a row that is not a run-start call
|
|
1745
|
+
* passes this and the route is never asked for it.
|
|
1746
|
+
*/
|
|
1747
|
+
const NO_SCOPE = {
|
|
1748
|
+
sessionId: void 0,
|
|
1749
|
+
cwd: void 0
|
|
1750
|
+
};
|
|
1751
|
+
/**
|
|
1752
|
+
* Resolve the workspace root the `/doc` route will accept.
|
|
1753
|
+
*
|
|
1754
|
+
* ⚠ THE ROOT CANNOT BE THE SESSION CWD BY GUESSWORK. The route re-validates whatever root it is handed
|
|
1755
|
+
* against the host's own workspace registry (`live-route.ts`: `resolveRoot(undefined, root)` must return the
|
|
1756
|
+
* same canonical path), so the root used here is the one the live route ITSELF answered with; the workspace
|
|
1757
|
+
* path is the hydration hint every other seat passes, and the owner-supplied `cwd` is the last resort.
|
|
1758
|
+
*/
|
|
1759
|
+
function resolveSpecRoot(snapshotRoot, workspacePath, cwd) {
|
|
1760
|
+
if (typeof snapshotRoot === "string" && snapshotRoot.trim() !== "") return snapshotRoot;
|
|
1761
|
+
if (workspacePath.trim() !== "") return workspacePath;
|
|
1762
|
+
return cwd.trim() === "" ? null : cwd;
|
|
1763
|
+
}
|
|
1764
|
+
/** The route states the /doc fetch can produce, and what each one MEANS. */
|
|
1765
|
+
function readDoc(runId, root, file) {
|
|
1766
|
+
const [fetchState, setFetchState] = (0, react.useState)({ state: "loading" });
|
|
1767
|
+
(0, react.useEffect)(() => {
|
|
1768
|
+
if (runId === null || root === null) {
|
|
1769
|
+
setFetchState({ state: "loading" });
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
let disposed = false;
|
|
1773
|
+
setFetchState({ state: "loading" });
|
|
1774
|
+
fetchPhaseDoc({
|
|
1775
|
+
root,
|
|
1776
|
+
runId,
|
|
1777
|
+
file
|
|
1778
|
+
}).then((text) => {
|
|
1779
|
+
if (!disposed) setFetchState({
|
|
1780
|
+
state: "loaded",
|
|
1781
|
+
text
|
|
1782
|
+
});
|
|
1783
|
+
}).catch((err) => {
|
|
1784
|
+
if (disposed) return;
|
|
1785
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1786
|
+
const missing = /HTTP 404\b/.test(message);
|
|
1787
|
+
setFetchState({
|
|
1788
|
+
state: missing ? "absent" : "error",
|
|
1789
|
+
error: message
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
return () => {
|
|
1793
|
+
disposed = true;
|
|
1794
|
+
};
|
|
1795
|
+
}, [
|
|
1796
|
+
runId,
|
|
1797
|
+
root,
|
|
1798
|
+
file
|
|
1799
|
+
]);
|
|
1800
|
+
return fetchState;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* The seat component: renders the spec sheet for ONE `recursive_ask` tool call, and NOTHING for any other.
|
|
1804
|
+
*
|
|
1805
|
+
* Returning `null` is how an unclaimed key behaves, so a `tdd-mode` / `qa-signoff` / `gate-block` ask, or a
|
|
1806
|
+
* preparing call whose arguments have not been dispatched yet, keeps the generic tool row it has today.
|
|
1807
|
+
*/
|
|
1808
|
+
function RunStartSpecSheet({ block, cwd, useSessions, useWorkspaces }) {
|
|
1809
|
+
const sessions = useSessions === void 0 ? null : useSessions((s) => s);
|
|
1810
|
+
const workspaces = useWorkspaces === void 0 ? EMPTY_WORKSPACES : useWorkspaces((s) => s) ?? EMPTY_WORKSPACES;
|
|
1811
|
+
const workspacePath = sessions === null ? "" : currentWorkspacePath(workspaces, sessions);
|
|
1812
|
+
const sessionCwd = sessions === null ? "" : currentSessionCwd(sessions);
|
|
1813
|
+
const read = readCall(block);
|
|
1814
|
+
const claims = isSpecSheetCall(read);
|
|
1815
|
+
const runId = read.ok ? read.args.runId : null;
|
|
1816
|
+
const root = resolveSpecRoot(useLiveProjection(sessions === null ? NO_SCOPE : {
|
|
1817
|
+
sessionId: sessions.current,
|
|
1818
|
+
cwd: workspacePath !== "" ? workspacePath : sessionCwd
|
|
1819
|
+
}, { enabled: claims })?.root ?? null, workspacePath, cwd ?? "");
|
|
1820
|
+
const fetchState = readDoc(claims ? runId : null, root, RUN_START_SPEC_FILE);
|
|
1821
|
+
if (!read.ok) {
|
|
1822
|
+
if (read.reason === "missing") return null;
|
|
1823
|
+
const text = read.reason === "preparing" ? PREPARING_NOTICE : TRUNCATED_NOTICE;
|
|
1824
|
+
return specFrame((0, react.createElement)("p", {
|
|
1825
|
+
className: "rec-spec-notice",
|
|
1826
|
+
role: "status"
|
|
1827
|
+
}, text));
|
|
1828
|
+
}
|
|
1829
|
+
if (!claims) return null;
|
|
1830
|
+
if (read.args.runId === null) return specFrame((0, react.createElement)("p", {
|
|
1831
|
+
className: "rec-spec-notice",
|
|
1832
|
+
role: "status"
|
|
1833
|
+
}, "This `recursive_ask` call names no runId, so no run spec can be shown for it."));
|
|
1834
|
+
return (0, react.createElement)(RunStartSpecSheetBody, { model: specSheetModel({
|
|
1835
|
+
args: read.args,
|
|
1836
|
+
root,
|
|
1837
|
+
fetch: fetchState
|
|
1838
|
+
}) });
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* The body, from a MODEL — split out so the spec can drive every state (stub, filled, absent, error, a
|
|
1842
|
+
* recorded Hold, a recorded approval) without a route, a session, or a fetch.
|
|
1843
|
+
*
|
|
1844
|
+
* The MODE is local state and starts at the rendered preview (`SPEC_DEFAULT_MODE`); everything that decides
|
|
1845
|
+
* what the sheet SAYS still comes from the pure model, so a spec can assert the words and the marks without
|
|
1846
|
+
* mounting a component.
|
|
1847
|
+
*/
|
|
1848
|
+
function RunStartSpecSheetBody({ model }) {
|
|
1849
|
+
const headingId = "rec-spec-title-" + (model.runId ?? "unresolved");
|
|
1850
|
+
const [mode, setMode] = (0, react.useState)(SPEC_DEFAULT_MODE);
|
|
1851
|
+
const toggle = specViewToggle(mode);
|
|
1852
|
+
return (0, react.createElement)("section", {
|
|
1853
|
+
className: "rec-spec",
|
|
1854
|
+
role: "region",
|
|
1855
|
+
"aria-labelledby": headingId,
|
|
1856
|
+
"aria-readonly": "true",
|
|
1857
|
+
"data-verdict": model.verdict ?? model.state,
|
|
1858
|
+
"data-mode": mode,
|
|
1859
|
+
onKeyDown: (event) => {
|
|
1860
|
+
if (event.key !== "Escape") return;
|
|
1861
|
+
event.preventDefault?.();
|
|
1862
|
+
event.stopPropagation?.();
|
|
1863
|
+
}
|
|
1864
|
+
}, (0, react.createElement)("header", { className: "rec-spec-header" }, (0, react.createElement)("h3", {
|
|
1865
|
+
className: "rec-spec-title",
|
|
1866
|
+
id: headingId
|
|
1867
|
+
}, "Run spec — " + (model.runId ?? "runId not carried") + " / " + model.file), (0, react.createElement)("span", { className: "rec-spec-tag" }, "read-only")), (0, react.createElement)("p", { className: "rec-spec-path" }, specPath(model.runId, model.root, model.file)), notice(model), questionBlock(model), viewControls(toggle, model, () => {
|
|
1868
|
+
setMode(otherMode(toggle.mode));
|
|
1869
|
+
}), documentBody(model, mode));
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1872
|
+
* The mode control: one button, and the line that announces the state it just put the sheet in.
|
|
1873
|
+
*
|
|
1874
|
+
* ⚠ KEYBOARD-REACHABLE BY BEING A BUTTON. It is a real `<button type="button">`, so Tab reaches it and
|
|
1875
|
+
* Enter/Space press it with no key handler of our own to get wrong. `aria-pressed` carries the state to a
|
|
1876
|
+
* screen reader on the control, and the `role="status"` paragraph repeats it in words, naming the mode ON
|
|
1877
|
+
* SCREEN ("Showing: raw source") rather than the action — so neither a pointer user nor a reader has to infer
|
|
1878
|
+
* the current mode from the button's face.
|
|
1879
|
+
*/
|
|
1880
|
+
function viewControls(toggle, model, onToggle) {
|
|
1881
|
+
const shown = model.text !== null;
|
|
1882
|
+
return (0, react.createElement)("div", { className: "rec-spec-view" }, (0, react.createElement)("button", {
|
|
1883
|
+
type: "button",
|
|
1884
|
+
className: "rec-spec-view-toggle",
|
|
1885
|
+
"aria-pressed": toggle.pressed,
|
|
1886
|
+
title: toggle.announce,
|
|
1887
|
+
"aria-disabled": !shown,
|
|
1888
|
+
onClick: () => {
|
|
1889
|
+
if (shown) onToggle();
|
|
1890
|
+
}
|
|
1891
|
+
}, toggle.action), (0, react.createElement)("span", {
|
|
1892
|
+
className: "rec-spec-view-state",
|
|
1893
|
+
role: "status"
|
|
1894
|
+
}, toggle.announce));
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* The document body: the rendered preview, or the raw bytes — and only ever the document's OWN text.
|
|
1898
|
+
*
|
|
1899
|
+
* Both modes are built from the same string, so neither is a second reading of the file: `raw` prints the
|
|
1900
|
+
* characters, `preview` hands those same characters to the one parser this plugin has.
|
|
1901
|
+
*/
|
|
1902
|
+
function documentBody(model, mode) {
|
|
1903
|
+
const content = specBodyContent(model.text, mode);
|
|
1904
|
+
const box = {
|
|
1905
|
+
className: "rec-spec-body-scroll" + (content !== null && content.mode === "raw" ? " rec-spec-body-raw" : ""),
|
|
1906
|
+
tabIndex: 0,
|
|
1907
|
+
role: "group",
|
|
1908
|
+
"aria-label": "Document text of " + (model.runId ?? "the run") + " / " + model.file + ", read-only, " + (mode === "raw" ? "raw source" : "rendered preview")
|
|
1909
|
+
};
|
|
1910
|
+
if (content === null) return (0, react.createElement)("div", box, (0, react.createElement)("p", { className: "rec-spec-empty" }, "No document text to show."));
|
|
1911
|
+
if (content.mode === "raw") return (0, react.createElement)("div", box, (0, react.createElement)("pre", { className: "rec-spec-text" }, content.text));
|
|
1912
|
+
return (0, react.createElement)("div", box, (0, react.createElement)(SpecPreview, {
|
|
1913
|
+
text: content.text,
|
|
1914
|
+
runId: model.runId,
|
|
1915
|
+
file: model.file
|
|
1916
|
+
}));
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* The rendered document.
|
|
1920
|
+
*
|
|
1921
|
+
* The parsing is memoised on the TEXT, so a toggle back and forth does not re-parse the document, and the
|
|
1922
|
+
* elements come from `doc-viewer.tsx`'s `PreviewLines` — the one place in this plugin where a `DocLine`
|
|
1923
|
+
* becomes markup.
|
|
1924
|
+
*/
|
|
1925
|
+
function SpecPreview({ text, runId, file }) {
|
|
1926
|
+
const lines = (0, react.useMemo)(() => parseDoc(text), [text]);
|
|
1927
|
+
return (0, react.createElement)(PreviewLines, {
|
|
1928
|
+
lines,
|
|
1929
|
+
keyBase: "rec-spec-" + (runId ?? "run") + "-" + file
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
/** The verdict notice: what the document IS, decided from its own text. */
|
|
1933
|
+
function notice(model) {
|
|
1934
|
+
if (model.state === "loading") return (0, react.createElement)("p", {
|
|
1935
|
+
className: "rec-spec-notice",
|
|
1936
|
+
role: "status"
|
|
1937
|
+
}, "Reading the document…");
|
|
1938
|
+
if (model.state === "absent") return (0, react.createElement)("p", {
|
|
1939
|
+
className: "rec-spec-notice rec-spec-notice-error",
|
|
1940
|
+
role: "status"
|
|
1941
|
+
}, "That document does not exist yet, so there is nothing here to read and nothing to approve.");
|
|
1942
|
+
if (model.state === "error") return (0, react.createElement)("p", {
|
|
1943
|
+
className: "rec-spec-notice rec-spec-notice-error",
|
|
1944
|
+
role: "status"
|
|
1945
|
+
}, "The document could not be read: " + (model.error ?? "the route gave no reason"));
|
|
1946
|
+
if (model.verdict === "unfilled") {
|
|
1947
|
+
const strong = unfilledEvidence({
|
|
1948
|
+
verdict: "unfilled",
|
|
1949
|
+
hits: model.evidence
|
|
1950
|
+
});
|
|
1951
|
+
const context = model.evidence.filter((hit) => hit.id !== "placeholder");
|
|
1952
|
+
return (0, react.createElement)("div", {
|
|
1953
|
+
className: "rec-spec-notice rec-spec-notice-unfilled",
|
|
1954
|
+
role: "status"
|
|
1955
|
+
}, (0, react.createElement)("p", { className: "rec-spec-unfilled-lead" }, "This document is still the UNFILLED TEMPLATE. There is no spec to approve yet."), (0, react.createElement)("p", { className: "rec-spec-unfilled-why" }, "The template's own placeholder text is still in it:"), (0, react.createElement)("ul", { className: "rec-spec-evidence" }, strong.slice(0, 8).map((hit, n) => (0, react.createElement)("li", {
|
|
1956
|
+
key: "placeholder-" + String(n),
|
|
1957
|
+
className: "rec-spec-evidence-item"
|
|
1958
|
+
}, "line " + String(hit.line) + ": " + hit.text))), strong.length > 8 ? (0, react.createElement)("p", { className: "rec-spec-unfilled-why" }, "…and " + String(strong.length - 8) + " more placeholder lines.") : null, context.length > 0 ? (0, react.createElement)("p", { className: "rec-spec-unfilled-why" }, "It also carries " + String(context.length) + " unfinished marker(s) of its own: " + context.slice(0, 3).map((hit) => "line " + String(hit.line) + ": " + hit.text).join(" | ")) : null, (0, react.createElement)("p", { className: "rec-spec-unfilled-next" }, "Fill the document in first. Approving here would record a decision about a spec that does not exist yet."));
|
|
1959
|
+
}
|
|
1960
|
+
if (model.verdict === "filled") return (0, react.createElement)("p", {
|
|
1961
|
+
className: "rec-spec-notice rec-spec-notice-filled",
|
|
1962
|
+
role: "status"
|
|
1963
|
+
}, "The document carries real content: no template placeholder remains in it.");
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
/** The decision under way, in the gate's own words, plus the fact that the controls live elsewhere. */
|
|
1967
|
+
function questionBlock(model) {
|
|
1968
|
+
const recorded = decisionLine(model.args.answer);
|
|
1969
|
+
return (0, react.createElement)("div", { className: "rec-spec-question" }, (0, react.createElement)("p", { className: "rec-spec-question-lead" }, "The question about this document is: “Approve phase 0 and start this run? Approving creates an armed goal the harness will keep driving.”"), (0, react.createElement)("p", { className: "rec-spec-question-options" }, RUN_START_OPTION_MEANING), (0, react.createElement)("p", { className: "rec-spec-question-where" }, "The decision controls are the question card's. This panel is read-only: it casts no vote and writes nothing."), recorded === null ? null : (0, react.createElement)("p", { className: "rec-spec-decision" }, recorded));
|
|
1970
|
+
}
|
|
1971
|
+
/** A minimal named frame for the states that precede a document (no call, no run id). */
|
|
1972
|
+
function specFrame(...children) {
|
|
1973
|
+
return (0, react.createElement)("section", {
|
|
1974
|
+
className: "rec-spec",
|
|
1975
|
+
role: "region",
|
|
1976
|
+
"aria-label": "Run spec",
|
|
1977
|
+
"aria-readonly": "true"
|
|
1978
|
+
}, ...children);
|
|
1979
|
+
}
|
|
1980
|
+
//#endregion
|
|
1308
1981
|
//#region src/client/open-state.ts
|
|
1309
1982
|
/**
|
|
1310
1983
|
* Shared board open-state (run 08 R1): module-level store behind the launcher,
|
|
@@ -1382,7 +2055,7 @@ window.__ModuleLoader__.load({
|
|
|
1382
2055
|
/* ===== dsh-recursive Paper theme (run 16) ===== */
|
|
1383
2056
|
|
|
1384
2057
|
/* Raw tokens: light (default) + dark + shared, scoped to the board/inspector. */
|
|
1385
|
-
.rec-board, .rec-inspector {
|
|
2058
|
+
.rec-board, .rec-inspector, .rec-spec {
|
|
1386
2059
|
/* light */
|
|
1387
2060
|
--rm3-light-background: #FFFFFF;
|
|
1388
2061
|
--rm3-light-foreground: #111111;
|
|
@@ -2041,6 +2714,9 @@ window.__ModuleLoader__.load({
|
|
|
2041
2714
|
line-height: 1.7;
|
|
2042
2715
|
padding: 0 6px;
|
|
2043
2716
|
border-left: 2px solid transparent;
|
|
2717
|
+
/* Containing block for the visually-hidden text that carries a task box's tick to a screen reader, so it
|
|
2718
|
+
cannot be positioned against the page instead of against its own line. */
|
|
2719
|
+
position: relative;
|
|
2044
2720
|
}
|
|
2045
2721
|
|
|
2046
2722
|
.rec-doc-line-current {
|
|
@@ -2465,8 +3141,302 @@ dl.rec-settings-rows {
|
|
|
2465
3141
|
overflow-wrap: anywhere;
|
|
2466
3142
|
}
|
|
2467
3143
|
|
|
3144
|
+
/* ===== Run-start spec sheet (tool.call.toolview key 'recursive_ask') =====
|
|
3145
|
+
The document beside the run-start question. LIGHT is the only theme here: the sheet renders inline in a
|
|
3146
|
+
transcript row, not on the board, so it does not own a data-theme attribute and must not inherit the
|
|
3147
|
+
board's. The body is the ONLY scrolling box (a max-height cap with overflow-y: auto) so the decision
|
|
3148
|
+
controls below it can never be pushed out of reach by a long document. */
|
|
3149
|
+
.rec-spec {
|
|
3150
|
+
box-sizing: border-box;
|
|
3151
|
+
margin: 6px 0;
|
|
3152
|
+
padding: var(--board-space-12);
|
|
3153
|
+
border: 1px solid var(--board-border);
|
|
3154
|
+
border-radius: var(--board-radius-lg);
|
|
3155
|
+
background: var(--board-card);
|
|
3156
|
+
color: var(--board-fg);
|
|
3157
|
+
font-family: var(--board-font-sans);
|
|
3158
|
+
font-size: var(--board-text-sm);
|
|
3159
|
+
/* The one transition this sheet has. It is disabled outright under prefers-reduced-motion below. */
|
|
3160
|
+
transition: border-color 160ms ease;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
.rec-spec-header {
|
|
3164
|
+
display: flex;
|
|
3165
|
+
align-items: baseline;
|
|
3166
|
+
justify-content: space-between;
|
|
3167
|
+
gap: var(--board-space-8);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.rec-spec-title {
|
|
3171
|
+
margin: 0;
|
|
3172
|
+
font-size: var(--board-text-sm);
|
|
3173
|
+
font-weight: var(--board-fw-semibold);
|
|
3174
|
+
letter-spacing: var(--board-tracking-tight);
|
|
3175
|
+
overflow-wrap: anywhere;
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
.rec-spec-tag {
|
|
3179
|
+
flex: none;
|
|
3180
|
+
font-size: var(--board-text-xs);
|
|
3181
|
+
color: var(--board-muted-fg);
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
.rec-spec-path {
|
|
3185
|
+
margin: 2px 0 var(--board-space-8);
|
|
3186
|
+
font-family: var(--board-font-mono);
|
|
3187
|
+
font-size: var(--board-text-xs);
|
|
3188
|
+
color: var(--board-muted-fg);
|
|
3189
|
+
overflow-wrap: anywhere;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
.rec-spec-notice {
|
|
3193
|
+
margin: 0 0 var(--board-space-8);
|
|
3194
|
+
padding: var(--board-space-8);
|
|
3195
|
+
border: 1px solid var(--board-border);
|
|
3196
|
+
border-radius: var(--board-radius-md);
|
|
3197
|
+
background: var(--board-muted);
|
|
3198
|
+
font-size: var(--board-text-xs);
|
|
3199
|
+
line-height: 1.5;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/* An UNFILLED template is stated as a warning, never as a neutral fact: the whole point of this seat is that
|
|
3203
|
+
nobody is nudged toward approving a hollow document. */
|
|
3204
|
+
.rec-spec-notice-unfilled {
|
|
3205
|
+
border-color: var(--board-warning);
|
|
3206
|
+
background: color-mix(in srgb, var(--board-warning) 10%, var(--board-card));
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
.rec-spec-unfilled-lead {
|
|
3210
|
+
margin: 0 0 6px;
|
|
3211
|
+
font-weight: var(--board-fw-semibold);
|
|
3212
|
+
color: var(--board-warning);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
.rec-spec-unfilled-why {
|
|
3216
|
+
margin: 0 0 4px;
|
|
3217
|
+
color: var(--board-muted-fg);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
.rec-spec-unfilled-next {
|
|
3221
|
+
margin: 6px 0 0;
|
|
3222
|
+
font-weight: var(--board-fw-medium);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
.rec-spec-evidence {
|
|
3226
|
+
margin: 0 0 4px;
|
|
3227
|
+
padding-left: 18px;
|
|
3228
|
+
font-family: var(--board-font-mono);
|
|
3229
|
+
font-size: var(--board-text-xs);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
.rec-spec-evidence-item {
|
|
3233
|
+
overflow-wrap: anywhere;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
.rec-spec-notice-filled {
|
|
3237
|
+
border-color: var(--board-success);
|
|
3238
|
+
background: color-mix(in srgb, var(--board-success) 8%, var(--board-card));
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
.rec-spec-notice-error {
|
|
3242
|
+
border-color: var(--board-error);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
.rec-spec-question {
|
|
3246
|
+
margin: 0 0 var(--board-space-8);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
.rec-spec-question-lead,
|
|
3250
|
+
.rec-spec-question-options,
|
|
3251
|
+
.rec-spec-question-where {
|
|
3252
|
+
margin: 0 0 4px;
|
|
3253
|
+
font-size: var(--board-text-xs);
|
|
3254
|
+
line-height: 1.5;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
.rec-spec-question-where {
|
|
3258
|
+
color: var(--board-muted-fg);
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
.rec-spec-decision {
|
|
3262
|
+
margin: 0;
|
|
3263
|
+
font-size: var(--board-text-xs);
|
|
3264
|
+
font-weight: var(--board-fw-medium);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/* The document body: the one bounded scroller. Focusable (tabIndex=0) for the keyboard, and the border
|
|
3268
|
+
marks the focus ring's home rather than relying on a colour change alone. */
|
|
3269
|
+
.rec-spec-body-scroll {
|
|
3270
|
+
max-height: 420px;
|
|
3271
|
+
overflow-y: auto;
|
|
3272
|
+
overscroll-behavior: contain;
|
|
3273
|
+
border: 1px solid var(--board-border);
|
|
3274
|
+
border-radius: var(--board-radius-md);
|
|
3275
|
+
background: var(--board-muted);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
.rec-spec-body-scroll:focus-visible {
|
|
3279
|
+
outline: 2px solid var(--board-info);
|
|
3280
|
+
outline-offset: 2px;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
/* The rendered preview inside that scroller (PreviewLines from doc-viewer.tsx). The line blocks carry their
|
|
3284
|
+
own padding so the marks — task boxes, gate readings — line up in a column down the left edge. */
|
|
3285
|
+
.rec-spec-body-scroll .rec-doc-lines {
|
|
3286
|
+
padding: var(--board-space-12);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
.rec-spec-body-scroll .rec-doc-line {
|
|
3290
|
+
padding: 0;
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
/* Raw source is monospace, wrapping, and NOT reflowed: the same characters the preview was built from. */
|
|
3294
|
+
.rec-spec-text {
|
|
3295
|
+
margin: 0;
|
|
3296
|
+
padding: var(--board-space-12);
|
|
3297
|
+
font-family: var(--board-font-mono);
|
|
3298
|
+
font-size: var(--board-text-xs);
|
|
3299
|
+
line-height: 1.55;
|
|
3300
|
+
/* The document is shown VERBATIM: only wrapping is normalised, never the characters. */
|
|
3301
|
+
white-space: pre-wrap;
|
|
3302
|
+
overflow-wrap: anywhere;
|
|
3303
|
+
tab-size: 2;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/* ===== The mode control (preview <-> raw source) =====
|
|
3307
|
+
One real button, so Tab reaches it and Enter/Space press it; aria-pressed carries its state to a screen
|
|
3308
|
+
reader and the role=status line beside it names the mode on screen in words. */
|
|
3309
|
+
.rec-spec-view {
|
|
3310
|
+
display: flex;
|
|
3311
|
+
align-items: center;
|
|
3312
|
+
flex-wrap: wrap;
|
|
3313
|
+
gap: var(--board-space-8);
|
|
3314
|
+
margin: 0 0 var(--board-space-8);
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
.rec-spec-view-toggle {
|
|
3318
|
+
flex: none;
|
|
3319
|
+
padding: 4px 12px;
|
|
3320
|
+
font-family: inherit;
|
|
3321
|
+
font-size: var(--board-text-xs);
|
|
3322
|
+
color: var(--board-fg);
|
|
3323
|
+
background: var(--board-card);
|
|
3324
|
+
border: 1px solid var(--board-border);
|
|
3325
|
+
border-radius: 999px;
|
|
3326
|
+
cursor: pointer;
|
|
3327
|
+
white-space: nowrap;
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
.rec-spec-view-toggle:hover {
|
|
3331
|
+
background: var(--board-accent);
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
.rec-spec-view-toggle:focus-visible {
|
|
3335
|
+
outline: 2px solid var(--board-info);
|
|
3336
|
+
outline-offset: 2px;
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
/* A pressed toggle says "the verbatim source is what you are reading" — marked, and not by colour alone. */
|
|
3340
|
+
.rec-spec-view-toggle[aria-pressed='true'] {
|
|
3341
|
+
border-color: var(--board-info);
|
|
3342
|
+
color: var(--board-info);
|
|
3343
|
+
font-weight: var(--board-fw-semibold);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
.rec-spec-view-toggle[aria-disabled='true'] {
|
|
3347
|
+
opacity: 0.55;
|
|
3348
|
+
cursor: default;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
.rec-spec-view-state {
|
|
3352
|
+
flex: 1 1 auto;
|
|
3353
|
+
min-width: 0;
|
|
3354
|
+
font-size: var(--board-text-xs);
|
|
3355
|
+
color: var(--board-muted-fg);
|
|
3356
|
+
overflow-wrap: anywhere;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
/* ===== Marks the PREVIEW must not hide (shared with the phase-doc viewer) =====
|
|
3360
|
+
The scaffolded Phase 0 template ships unticked boxes and two FAIL gates; a preview that drew them as
|
|
3361
|
+
generic body text would make an unfilled form look like a finished spec. */
|
|
3362
|
+
.rec-doc-sr {
|
|
3363
|
+
position: absolute;
|
|
3364
|
+
width: 1px;
|
|
3365
|
+
height: 1px;
|
|
3366
|
+
margin: -1px;
|
|
3367
|
+
padding: 0;
|
|
3368
|
+
border: 0;
|
|
3369
|
+
overflow: hidden;
|
|
3370
|
+
clip: rect(0 0 0 0);
|
|
3371
|
+
white-space: nowrap;
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
.rec-doc-todo {
|
|
3375
|
+
display: flex;
|
|
3376
|
+
align-items: baseline;
|
|
3377
|
+
gap: 8px;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
.rec-doc-todo-check {
|
|
3381
|
+
flex: none;
|
|
3382
|
+
font-family: var(--board-font-mono);
|
|
3383
|
+
font-size: 12.5px;
|
|
3384
|
+
line-height: 1.7;
|
|
3385
|
+
letter-spacing: var(--board-tracking-mono);
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
.rec-doc-todo-check-off {
|
|
3389
|
+
color: var(--board-warning);
|
|
3390
|
+
font-weight: var(--board-fw-semibold);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
.rec-doc-todo-check-on {
|
|
3394
|
+
color: var(--board-success);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
/* An unticked box is stated, not merely greyed: the warning colour AND the mark together. */
|
|
3398
|
+
.rec-doc-todo-open .rec-doc-li-text {
|
|
3399
|
+
color: var(--board-fg);
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
.rec-doc-todo-done .rec-doc-li-text {
|
|
3403
|
+
color: var(--board-muted-fg);
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
.rec-doc-gate {
|
|
3407
|
+
font-family: var(--board-font-mono);
|
|
3408
|
+
font-size: var(--board-text-xs);
|
|
3409
|
+
font-weight: var(--board-fw-semibold);
|
|
3410
|
+
letter-spacing: var(--board-tracking-mono);
|
|
3411
|
+
margin: 4px 0;
|
|
3412
|
+
padding: 3px 8px;
|
|
3413
|
+
border: 1px solid var(--board-border);
|
|
3414
|
+
border-radius: var(--board-radius-md);
|
|
3415
|
+
display: inline-block;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
.rec-doc-gate-fail {
|
|
3419
|
+
color: var(--board-error);
|
|
3420
|
+
border-color: var(--board-error);
|
|
3421
|
+
background: color-mix(in srgb, var(--board-error) 8%, var(--board-card));
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
.rec-doc-gate-pass {
|
|
3425
|
+
color: var(--board-success);
|
|
3426
|
+
border-color: var(--board-success);
|
|
3427
|
+
background: color-mix(in srgb, var(--board-success) 8%, var(--board-card));
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
.rec-spec-empty {
|
|
3431
|
+
margin: 0;
|
|
3432
|
+
padding: var(--board-space-12);
|
|
3433
|
+
color: var(--board-muted-fg);
|
|
3434
|
+
font-size: var(--board-text-xs);
|
|
3435
|
+
}
|
|
3436
|
+
|
|
2468
3437
|
@media (prefers-reduced-motion: reduce) {
|
|
2469
3438
|
.rec-card, .rec-back, .rec-close, .rec-theme-toggle { transition: none; }
|
|
3439
|
+
.rec-spec { transition: none; }
|
|
2470
3440
|
}
|
|
2471
3441
|
`;
|
|
2472
3442
|
/**
|
|
@@ -2605,6 +3575,10 @@ dl.rec-settings-rows {
|
|
|
2605
3575
|
useWorkspaces: props?.useWorkspaces
|
|
2606
3576
|
});
|
|
2607
3577
|
})));
|
|
3578
|
+
disposers.push(ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
|
|
3579
|
+
name: "tool.call.toolview",
|
|
3580
|
+
key: RUN_START_TOOL_NAME
|
|
3581
|
+
}, RunStartSpecSheet)));
|
|
2608
3582
|
disposers.push(ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
2609
3583
|
name: "settings.section",
|
|
2610
3584
|
id: "recursive",
|