weir-harness 1.1.5 → 1.1.7

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/icon.svg ADDED
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 160 160" xmlns="http://www.w3.org/2000/svg"><rect width="160" height="160" rx="36" fill="#0A0A0B"/><g transform="translate(80 80) scale(.72) translate(-80 -84)"><polygon points="16,48 30,48 56,126 80,56 104,126 130,48 144,48 144,144 16,144" fill="#ECECE6"/><rect x="16" y="24" width="128" height="10" fill="#CCFF00"/></g></svg>
@@ -13,8 +13,13 @@ window.__ModuleLoader__.load({
13
13
  // /api/weirBlackboard/<method> payload shape.
14
14
  //
15
15
  // WIRE CONTRACT (pinned by src/blackboard/remote.js): namespace
16
- // weirBlackboard, five methods, each taking the acting session as the
17
- // `agentId` lookup parameter plus one JSON `args` object. The channel
16
+ // weirBlackboard. The seven board verbs each take the acting session as
17
+ // the `agentId` lookup parameter plus one JSON `args` object. The eighth
18
+ // method, resolve, is the deliberate exception: the browsing session id
19
+ // crosses as a BARE JSON `agentId` (no lookup, no scope, no inner args)
20
+ // because the gateway's agent lookup refuses subagent-owned sessions
21
+ // before the service can run — resolve is how a subagent session page
22
+ // learns the conversation root whose board it binds. The channel
18
23
  // rides the RAW gateway carrier exactly like the capability reads (the
19
24
  // hand-written src-json contribution cannot mount through
20
25
  // ctx.remote.$mount — its strict-codec gate rejects src-json codecs),
@@ -99,6 +104,33 @@ window.__ModuleLoader__.load({
99
104
  };
100
105
  }
101
106
 
107
+ /**
108
+ * The panel's root resolution (slice 4): POST weirBlackboard/resolve
109
+ * with the browsing session id as a bare JSON `agentId` — NO inner
110
+ * args object, NO agent lookup (the gateway's lookup would refuse a
111
+ * subagent-owned session before the service runs). ok with a non-empty
112
+ * value.sessionId folds to { ok: true, sessionId }; every other shape
113
+ * (failure envelope, unusable payload, carrier throw) folds to
114
+ * { ok: false, error } so the panel can fall back to the browsing id.
115
+ */
116
+ function resolveBoardSession(rawCall, sessionId) {
117
+ let pending;
118
+ try {
119
+ pending = Promise.resolve(rawCall("weirBlackboard/resolve", { args: { agentId: sessionId } }));
120
+ } catch (cause) {
121
+ return Promise.resolve({ ok: false, error: { code: "call-threw", message: cause instanceof Error ? cause.message : String(cause) } });
122
+ }
123
+ return pending.then((result) => {
124
+ const folded = foldRemoteResult(result);
125
+ if (folded.ok === true) {
126
+ const root = folded.value?.sessionId;
127
+ if (typeof root === "string" && root !== "") return { ok: true, sessionId: root };
128
+ return { ok: false, error: { code: "bad-payload", message: "resolve returned no usable session id" } };
129
+ }
130
+ return { ok: false, error: folded.error };
131
+ }, (cause) => ({ ok: false, error: { code: "call-threw", message: cause instanceof Error ? cause.message : String(cause) } }));
132
+ }
133
+
102
134
  // ---- entries ----
103
135
 
104
136
  /** Defensive normalization of one wire row (list or read shape; content only present on reads). */
@@ -358,6 +390,7 @@ window.__ModuleLoader__.load({
358
390
  exports.WATCH_GIVE_UP_AFTER = WATCH_GIVE_UP_AFTER;
359
391
  exports.foldRemoteResult = foldRemoteResult;
360
392
  exports.createBlackboardChannel = createBlackboardChannel;
393
+ exports.resolveBoardSession = resolveBoardSession;
361
394
  exports.entryRowOf = entryRowOf;
362
395
  exports.listOutcomeOf = listOutcomeOf;
363
396
  exports.readOutcomeOf = readOutcomeOf;
@@ -168,6 +168,13 @@ window.__ModuleLoader__.load({
168
168
  minWidth: "16px",
169
169
  textAlign: "center",
170
170
  flex: "none"
171
+ },
172
+ parentNote: {
173
+ fontSize: "11px",
174
+ lineHeight: "16px",
175
+ color: MUTED,
176
+ flex: "none",
177
+ padding: "0 2px 4px"
171
178
  }
172
179
  },
173
180
  row: {
@@ -481,20 +488,35 @@ window.__ModuleLoader__.load({
481
488
  const [typeFilter, setTypeFilter] = react.useState("");
482
489
  const [query, setQuery] = react.useState("");
483
490
  const [promoting, setPromoting] = react.useState(false);
491
+ // The board the panel binds: null while resolveBoardSession runs, then
492
+ // the conversation root resolve names (a subagent session page cannot
493
+ // ride the gateway's agent lookup) or the browsing id itself when
494
+ // resolve fails — exactly the pre-resolve behavior.
495
+ const [boardSessionId, setBoardSessionId] = react.useState(null);
484
496
  // Mirrors for async continuations (polls and call settles read the
485
497
  // freshest state without stale-closure races).
486
498
  const detailRef = react.useRef(null);
487
499
  detailRef.current = detail;
488
500
  const watchRef = react.useRef(null);
489
501
  watchRef.current = watch;
490
- const channel = react.useMemo(() => model.createBlackboardChannel(props.callBoard, props.sessionId), [props.callBoard, props.sessionId]);
491
- const refreshList = () => channel.list().then((result) => {
492
- setBoard(model.listOutcomeOf(result));
493
- });
502
+ // The channel keys on the RESOLVED board session, never the browsing
503
+ // id: while resolution runs it does not exist, and every list/poll/
504
+ // detail effect below gates on it being non-null.
505
+ const channel = react.useMemo(
506
+ () => (boardSessionId === null ? null : model.createBlackboardChannel(props.callBoard, boardSessionId)),
507
+ [props.callBoard, boardSessionId],
508
+ );
509
+ const refreshList = () => {
510
+ if (channel === null) return Promise.resolve();
511
+ return channel.list().then((result) => {
512
+ setBoard(model.listOutcomeOf(result));
513
+ });
514
+ };
494
515
  const setDetailFor = (key, next) => {
495
516
  if (detailRef.current?.key === key) setDetail(next);
496
517
  };
497
518
  const loadDetail = (key) => {
519
+ if (channel === null) return;
498
520
  // Record the request synchronously: a fast-settling read must not
499
521
  // be dropped by the setDetailFor guard before the next render
500
522
  // re-mirrors the ref.
@@ -514,14 +536,32 @@ window.__ModuleLoader__.load({
514
536
  setRoute({ name: "detail", key });
515
537
  loadDetail(key);
516
538
  };
517
- // Initial listing + session switch.
539
+ // Root resolution precedes the channel: a subagent session page cannot
540
+ // pass the gateway's agent lookup, so the panel first asks resolve for
541
+ // the conversation root and binds THAT board; a failed resolve keeps
542
+ // the browsing id (the pre-resolve behavior). While the answer is
543
+ // pending the board resets to the loading state; the alive flag drops
544
+ // a superseded answer after a session switch.
545
+ react.useEffect(() => {
546
+ let alive = true;
547
+ setBoardSessionId(null);
548
+ setBoard(null);
549
+ model.resolveBoardSession(props.callBoard, props.sessionId).then((result) => {
550
+ if (!alive) return;
551
+ setBoardSessionId(result.ok === true ? result.sessionId : props.sessionId);
552
+ });
553
+ return () => { alive = false; };
554
+ }, [props.callBoard, props.sessionId]);
555
+ // Initial listing + (re)bind: runs once the resolved channel exists
556
+ // and again whenever the answer swapped it.
518
557
  react.useEffect(() => {
558
+ if (channel === null) return undefined;
519
559
  let alive = true;
520
560
  channel.list().then((result) => {
521
561
  if (alive) setBoard(model.listOutcomeOf(result));
522
562
  });
523
563
  return () => { alive = false; };
524
- }, [props.sessionId]);
564
+ }, [channel]);
525
565
  // Page visibility: polls pause while hidden (no server push in this
526
566
  // slice — the interval is the only refresh channel).
527
567
  react.useEffect(() => {
@@ -531,15 +571,15 @@ window.__ModuleLoader__.load({
531
571
  return () => document.removeEventListener("visibilitychange", onVisibility);
532
572
  }, []);
533
573
  react.useEffect(() => {
534
- if (!visible || !(pollMs > 0)) return undefined;
574
+ if (!visible || !(pollMs > 0) || channel === null) return undefined;
535
575
  const timer = setInterval(refreshList, pollMs);
536
576
  return () => clearInterval(timer);
537
- }, [visible, pollMs, props.sessionId]);
577
+ }, [visible, pollMs, channel]);
538
578
  // The contention watch: poll apply until the key frees (an acquired
539
579
  // flip means THIS session now holds the authority — the one-shot
540
580
  // subscription's client-side half).
541
581
  react.useEffect(() => {
542
- if (!visible || watch?.phase !== "watching" || !(watchMs > 0)) return undefined;
582
+ if (!visible || watch?.phase !== "watching" || !(watchMs > 0) || channel === null) return undefined;
543
583
  const timer = setInterval(() => {
544
584
  channel.apply(watch.key).then((result) => {
545
585
  const next = model.watchAfterPoll(watchRef.current, model.applyOutcomeOf(result));
@@ -587,6 +627,7 @@ window.__ModuleLoader__.load({
587
627
  setRoute({ name: "editor", mode: "create" });
588
628
  };
589
629
  const startEdit = (entry) => {
630
+ if (channel === null) return;
590
631
  if (entry.promoted) {
591
632
  setNotice({ tone: "warn", text: t("blackboard.promotedReadonly", "This entry is promoted and read-only.") });
592
633
  return;
@@ -611,6 +652,7 @@ window.__ModuleLoader__.load({
611
652
  };
612
653
  // ---- promotion request (3.1): the button pushes the evaluation brief into the main agent ----
613
654
  const requestPromotionNow = () => {
655
+ if (channel === null) return;
614
656
  if (promoting) return;
615
657
  setNotice(null);
616
658
  setPromoting(true);
@@ -626,6 +668,7 @@ window.__ModuleLoader__.load({
626
668
  });
627
669
  };
628
670
  const reacquire = () => {
671
+ if (channel === null) return;
629
672
  if (editor?.mode !== "update") return;
630
673
  const key = editor.key;
631
674
  setEditor({ ...editor, phase: "applying", error: null, contended: null });
@@ -643,6 +686,7 @@ window.__ModuleLoader__.load({
643
686
  });
644
687
  };
645
688
  const saveEditor = () => {
689
+ if (channel === null) return;
646
690
  if (editor === null || editor.draft === null || editor.phase === "saving" || editor.phase === "applying") return;
647
691
  const errors = model.editorErrorsOf(editor.draft);
648
692
  if (Object.keys(errors).length > 0) {
@@ -694,6 +738,7 @@ window.__ModuleLoader__.load({
694
738
  writeNow();
695
739
  };
696
740
  const startDelete = (key) => {
741
+ if (channel === null) return;
697
742
  setNotice(null);
698
743
  setConfirmDelete({ key, phase: "applying", error: null });
699
744
  channel.apply(key).then((result) => {
@@ -711,6 +756,7 @@ window.__ModuleLoader__.load({
711
756
  });
712
757
  };
713
758
  const confirmDeleteNow = () => {
759
+ if (channel === null) return;
714
760
  if (confirmDelete?.phase !== "confirm") return;
715
761
  const key = confirmDelete.key;
716
762
  setConfirmDelete({ key, phase: "removing", error: null });
@@ -1181,6 +1227,11 @@ window.__ModuleLoader__.load({
1181
1227
  ]
1182
1228
  })
1183
1229
  : null;
1230
+ // The parent-board note: the resolved root differs from the browsing
1231
+ // session — this page is a subagent session showing its parent's board.
1232
+ const parentBoardNote = route.name === "list" && boardSessionId !== null && boardSessionId !== props.sessionId
1233
+ ? react_jsx_runtime.jsx("div", { style: S.list.parentNote, children: t("blackboard.parentBoard", "Showing the parent conversation's board.") })
1234
+ : null;
1184
1235
  const scrollBody = route.name === "editor"
1185
1236
  ? react_jsx_runtime.jsx("div", { style: S.form.box, children: editorBody })
1186
1237
  : route.name === "detail"
@@ -1189,6 +1240,7 @@ window.__ModuleLoader__.load({
1189
1240
  return react_jsx_runtime.jsxs("div", { style: S.frame, children: [
1190
1241
  header,
1191
1242
  searchRow,
1243
+ parentBoardNote,
1192
1244
  scrollBody,
1193
1245
  footerChips.length > 0 ? react_jsx_runtime.jsx("div", { style: S.footer.bar, children: footerChips }) : null
1194
1246
  ] });
@@ -88,7 +88,7 @@ window.__ModuleLoader__.load({
88
88
  // Continue editing is one human action: restore authority, then confirm the
89
89
  // retained files (each through the ordinary per-file check).
90
90
  // confirm --all only when something waits, so no empty "Confirmed 0 of 0" is recorded.
91
- if (state === "stopped") return { verbs: view.files.some((file) => file.mine && file.status === "user-interrupted") ? ["resume", "confirm --all"] : ["resume"], label: "editLockResume" };
91
+ if (state === "stopped" || state === "revoked") return { verbs: view.files.some((file) => file.mine && file.status === "user-interrupted") ? ["resume", "confirm --all"] : ["resume"], label: "editLockResume" };
92
92
  if (state === "confirm") return { verb: "confirm --all", label: "editLockConfirmAll" };
93
93
  if (state === "holding" && view.ownCount > 0) return { verbs: view.files.filter((file) => file.mine && file.action === "release").map(rowCommand), label: "editLockReleaseAll" };
94
94
  return null;
@@ -209,7 +209,8 @@ window.__ModuleLoader__.load({
209
209
  ] }, "head"),
210
210
  view?.reason && !bindingRefusal ? jsx("div", { style: { ...errorStyle, wordBreak: "break-word" }, "data-weir-edit-lock-reason": "", children: view.reason }, "reason") : null,
211
211
  view?.bindingFailure ? jsx("div", { style: mutedStyle, "data-weir-edit-lock-binding": "", children: t(`editLockBinding_${view.bindingFailure.holderKind ?? "unknown"}`) }, "binding") : null,
212
- state === "revoked" ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-revoked": "", children: t("editLockRevokedHint") }, "revoked") : null,
212
+ state === "revoked" && !cold ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-revoked": "", children: t("editLockRevokedHint") }, "revoked") : null,
213
+ cold && state === "revoked" ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-cold": "", children: t(view.autoResume === false ? "editLockColdRevokedManual" : "editLockColdRevokedAuto") }, "cold") : null,
213
214
  cold && state === "stopped" ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-cold": "", children: t(view.autoResume === false ? "editLockColdResumeManual" : "editLockColdResumeAuto") }, "cold") : null,
214
215
  // Usage is historical; only the live driver's explicit deadline signals a pause.
215
216
  // Follow-up locale keys: editLockCleanupOnly, editLockRecoveryUsage,