dsh-trivium 0.4.21 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -73,6 +73,77 @@ window.__ModuleLoader__.load({
73
73
  embedKeyHint: "已保存则留空不改。也可设环境变量 DSH_TRIVIUM_EMBED_KEY。",
74
74
  embedBackfill: "回填已有节点向量",
75
75
  list: "记忆条目",
76
+ tabList: "记忆条目",
77
+ tabRecall: "召回检查",
78
+ nodeTabContent: "内容",
79
+ nodeTabSource: "来源",
80
+ nodeTabHistory: "决策历史",
81
+ estimate: "估算",
82
+ recallRun: "运行只读检查",
83
+ recallQueryPh: "输入一个问题,看这一轮会注入什么",
84
+ recallSemanticOff: "语义通道未运行:只读检查不请求 embedding。",
85
+ recallBudget: "本轮预算",
86
+ recallBudgetHint: "估算 token,不是模型计费。芯片、更正与召回共用这 300。",
87
+ recallWillSend: "将发送给模型的文本",
88
+ recallEmptyText: "本轮没有可注入的内容。",
89
+ recallReasons: "入选与过滤原因",
90
+ recallRecent: "最近召回诊断",
91
+ recallNoTrace: "还没有召回记录。打开自动召回或芯片后,发一条消息再看。",
92
+ recallExpired: "诊断已过期或不存在。",
93
+ recallCandidates: "候选",
94
+ recallSelected: "入选",
95
+ recallDropped: "未入选",
96
+ recallDeferred: "留待下一轮生效",
97
+ recallSemantic: "语义",
98
+ recallMode: "通道",
99
+ reasonBudget: "预算不足",
100
+ reasonStale: "已过期",
101
+ reasonArchived: "已归档",
102
+ reasonHygiene: "卫生检查",
103
+ reasonDuplicate: "重复",
104
+ reasonRevision: "版本",
105
+ reasonInvalid: "无效",
106
+ sourceMissing: "此条目没有可定位的来源。",
107
+ sourceUnavailable: "会话已不可用,保留现有摘录。",
108
+ sourceNoJump: "宿主没有提供会话跳转接口,因此不提供跳转。",
109
+ sourceQuote: "摘录",
110
+ sourceKind: "来源类型",
111
+ sourceSession: "会话",
112
+ sourceCaptured: "记录时间",
113
+ sourceImport: "导入路径",
114
+ sourceOpen: "查看来源",
115
+ historyNone: "没有版本历史,这条就是当前版本。",
116
+ historyCurrent: "当前版本",
117
+ historySuperseded: "已被替代",
118
+ historyOlder: "旧版本",
119
+ historyUnresolved: "版本链无法解析",
120
+ historyReason: "说明",
121
+ historyViewCurrent: "查看当前版本",
122
+ expired: "已过期",
123
+ noDeadline: "无期限",
124
+ detailLoading: "加载中…",
125
+ detailFailed: "读取失败",
126
+ showSuperseded: "显示已替代",
127
+ supersede: "替代…",
128
+ supersedeTitle: "确认替代关系",
129
+ supersedeHint: "选一条已有决策来替代这条。旧版本的正文与期限都保留,不会被归档。",
130
+ supersedePick: "选择替代它的决策",
131
+ supersedeReason: "说明(可选,最多 240 字)",
132
+ supersedeConfirm: "确认替代关系",
133
+ supersedeCancel: "取消",
134
+ supersedeOld: "将被替代",
135
+ supersedeNew: "新决策",
136
+ supersedeSaving: "正在保存…",
137
+ supersedeConflict: "内容已在别处变更(409)。你的选择已保留,刷新后可以重试。",
138
+ supersedeRefresh: "刷新并重试",
139
+ supersedeFailed: "替代失败",
140
+ supersedeOk: "已建立替代关系",
141
+ revoke: "撤销替代",
142
+ revokeHint: "只删除这一条替代关系,其它边保留。",
143
+ revokeOk: "已撤销替代关系",
144
+ revokeFailed: "撤销失败",
145
+ noSuccessor: "没有可选的决策。先新建一条决策,再回来建立替代关系。",
146
+ revisionLinkedBlocked: "这条决策在替代链上,请先处理替代关系。",
76
147
  filter: "类型",
77
148
  search: "搜索",
78
149
  all: "全部",
@@ -229,6 +300,77 @@ window.__ModuleLoader__.load({
229
300
  embedKeyHint: "Leave blank to keep the saved key. Or set DSH_TRIVIUM_EMBED_KEY.",
230
301
  embedBackfill: "Backfill vectors",
231
302
  list: "Memory entries",
303
+ tabList: "Memory entries",
304
+ tabRecall: "Recall check",
305
+ nodeTabContent: "Content",
306
+ nodeTabSource: "Source",
307
+ nodeTabHistory: "Decision history",
308
+ estimate: "estimated",
309
+ recallRun: "Run read-only check",
310
+ recallQueryPh: "Type a question to see what this turn would inject",
311
+ recallSemanticOff: "Semantic channel did not run: the read-only check never requests an embedding.",
312
+ recallBudget: "Turn budget",
313
+ recallBudgetHint: "Estimated tokens, not model billing. Chips, corrections and recall share this 300.",
314
+ recallWillSend: "Text that would be sent to the model",
315
+ recallEmptyText: "Nothing would be injected this turn.",
316
+ recallReasons: "Selected and filtered reasons",
317
+ recallRecent: "Recent recall diagnostics",
318
+ recallNoTrace: "No recall recorded yet. Turn on auto recall or chips, send a message, then look again.",
319
+ recallExpired: "This diagnostic expired or does not exist.",
320
+ recallCandidates: "Candidates",
321
+ recallSelected: "Selected",
322
+ recallDropped: "Not selected",
323
+ recallDeferred: "Deferred to the next turn",
324
+ recallSemantic: "Semantic",
325
+ recallMode: "Channel",
326
+ reasonBudget: "over budget",
327
+ reasonStale: "expired",
328
+ reasonArchived: "archived",
329
+ reasonHygiene: "write hygiene",
330
+ reasonDuplicate: "duplicate",
331
+ reasonRevision: "revision",
332
+ reasonInvalid: "invalid",
333
+ sourceMissing: "This entry has no locatable source.",
334
+ sourceUnavailable: "The session is gone; the stored excerpt is kept.",
335
+ sourceNoJump: "The host exposes no session locator, so no jump is offered.",
336
+ sourceQuote: "Excerpt",
337
+ sourceKind: "Source kind",
338
+ sourceSession: "Session",
339
+ sourceCaptured: "Captured at",
340
+ sourceImport: "Import path",
341
+ sourceOpen: "View source",
342
+ historyNone: "No version history; this is the current version.",
343
+ historyCurrent: "Current version",
344
+ historySuperseded: "Superseded",
345
+ historyOlder: "Older version",
346
+ historyUnresolved: "Version chain could not be resolved",
347
+ historyReason: "Reason",
348
+ historyViewCurrent: "View current version",
349
+ expired: "Expired",
350
+ noDeadline: "No deadline",
351
+ detailLoading: "Loading…",
352
+ detailFailed: "Could not load",
353
+ showSuperseded: "Show replaced",
354
+ supersede: "Replace…",
355
+ supersedeTitle: "Confirm replacement",
356
+ supersedeHint: "Pick an existing decision that replaces this one. The old body and deadline are kept, and nothing is archived.",
357
+ supersedePick: "Decision that replaces this one",
358
+ supersedeReason: "Reason (optional, up to 240 characters)",
359
+ supersedeConfirm: "Confirm replacement",
360
+ supersedeCancel: "Cancel",
361
+ supersedeOld: "Will be replaced",
362
+ supersedeNew: "New decision",
363
+ supersedeSaving: "Saving…",
364
+ supersedeConflict: "Content changed elsewhere (409). Your selection is kept; refresh and try again.",
365
+ supersedeRefresh: "Refresh and retry",
366
+ supersedeFailed: "Replacement failed",
367
+ supersedeOk: "Replacement recorded",
368
+ revoke: "Undo replacement",
369
+ revokeHint: "Removes only this replacement relation; other edges stay.",
370
+ revokeOk: "Replacement undone",
371
+ revokeFailed: "Undo failed",
372
+ noSuccessor: "No candidate decision. Create one first, then come back.",
373
+ revisionLinkedBlocked: "This decision is in a replacement chain; handle the relation first.",
232
374
  filter: "Type",
233
375
  search: "Search",
234
376
  all: "All",
@@ -409,7 +551,18 @@ window.__ModuleLoader__.load({
409
551
  const TYPES = ["", "entity", "preference", "decision", "experience", "workspace"];
410
552
 
411
553
  const ui = {
412
- page: { display: "flex", flexDirection: "column", gap: 14, padding: "4px 0 16px", maxWidth: 760 },
554
+ page: {
555
+ display: "flex",
556
+ flexDirection: "column",
557
+ gap: 14,
558
+ padding: "4px 0 16px",
559
+ maxWidth: 760,
560
+ // The host modal keeps its own nav column; without these the card is
561
+ // squeezed to a one-character-wide column instead of shrinking evenly.
562
+ width: "100%",
563
+ minWidth: 0,
564
+ boxSizing: "border-box",
565
+ },
413
566
  header: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 },
414
567
  title: { margin: 0, fontSize: 20, lineHeight: "32px", fontWeight: 650 },
415
568
  actions: { display: "flex", alignItems: "center", gap: 8, flexShrink: 0 },
@@ -441,6 +594,46 @@ window.__ModuleLoader__.load({
441
594
  toolbar: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 10 },
442
595
  error: { color: "#c44", fontSize: 13 },
443
596
  saved: { fontSize: 12, opacity: 0.75 },
597
+ /*
598
+ * Everything below uses currentColor / inherited surfaces only: the panel
599
+ * has to work in the host light theme and dark theme without a second
600
+ * palette, and the prototype's fixed colours stay in the prototype.
601
+ */
602
+ tabRow: { display: "flex", flexWrap: "wrap", gap: 6, alignItems: "center" },
603
+ tab: {
604
+ appearance: "none",
605
+ border: "1px solid rgba(127,127,127,0.35)",
606
+ background: "transparent",
607
+ color: "inherit",
608
+ borderRadius: 999,
609
+ padding: "3px 12px",
610
+ fontSize: 12,
611
+ lineHeight: "20px",
612
+ cursor: "pointer",
613
+ },
614
+ tabActive: {
615
+ borderColor: "currentColor",
616
+ fontWeight: 650,
617
+ background: "rgba(127,127,127,0.16)",
618
+ },
619
+ bar: { height: 6, borderRadius: 3, background: "rgba(127,127,127,0.22)", overflow: "hidden" },
620
+ barFill: { height: "100%", background: "currentColor", opacity: 0.55 },
621
+ tag: {
622
+ fontSize: 11,
623
+ padding: "0 6px",
624
+ borderRadius: 4,
625
+ border: "1px solid rgba(127,127,127,0.35)",
626
+ whiteSpace: "nowrap",
627
+ },
628
+ mono: {
629
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
630
+ fontSize: 11,
631
+ whiteSpace: "pre-wrap",
632
+ wordBreak: "break-word",
633
+ },
634
+ stack: { display: "flex", flexDirection: "column", gap: 6 },
635
+ muted: { fontSize: 12, opacity: 0.7, lineHeight: 1.4 },
636
+ focusable: { outlineOffset: 2 },
444
637
  };
445
638
 
446
639
  const chipUi = { ctx: null, sessions: null, dispose: null };
@@ -545,6 +738,673 @@ window.__ModuleLoader__.load({
545
738
  const [updateBusy, setUpdateBusy] = react.useState(false);
546
739
  const persistSeq = react.useRef(0);
547
740
  const loadSeq = react.useRef(0);
741
+ const [tab, setTab] = react.useState("list");
742
+ const [nodeTab, setNodeTab] = react.useState("content");
743
+ const [detail, setDetail] = react.useState({});
744
+ const [sourceInfo, setSourceInfo] = react.useState({});
745
+ const [historyInfo, setHistoryInfo] = react.useState({});
746
+ const [preview, setPreview] = react.useState(null);
747
+ const [previewQuery, setPreviewQuery] = react.useState("");
748
+ const [previewBusy, setPreviewBusy] = react.useState(false);
749
+ const [traces, setTraces] = react.useState([]);
750
+ const [traceError, setTraceError] = react.useState("");
751
+ const [showSuperseded, setShowSuperseded] = react.useState(false);
752
+ const [revisionDialog, setRevisionDialog] = react.useState(null);
753
+ const [revisionBusy, setRevisionBusy] = react.useState(false);
754
+ const [revisionNote, setRevisionNote] = react.useState("");
755
+ const dialogRef = react.useRef(null);
756
+
757
+ /*
758
+ * Everything below is read-on-demand. Opening the source, history or
759
+ * recall pane issues at most one HTTP GET for the node in front of the
760
+ * user — it never asks for an embedding, a model call, or an injection.
761
+ */
762
+ const cwdQuery = () => (status && status.cwd ? "cwd=" + encodeURIComponent(status.cwd) : "");
763
+
764
+ /**
765
+ * Like api(), but keeps the status and the server's `code` so the panel can
766
+ * tell a 409 conflict apart from a generic failure.
767
+ */
768
+ const apiRaw = async (path, opts) => {
769
+ const resp = await fetch(path, opts);
770
+ let data = null;
771
+ try {
772
+ data = await resp.json();
773
+ } catch {
774
+ data = null;
775
+ }
776
+ return { ok: resp.ok && !(data && data.ok === false), status: resp.status, data };
777
+ };
778
+
779
+ const newOperationId = () =>
780
+ (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function"
781
+ ? globalThis.crypto.randomUUID()
782
+ : "op-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10));
783
+
784
+ const openSupersede = (oldId) => {
785
+ setRevisionNote("");
786
+ setRevisionDialog({ oldId: Number(oldId), successorId: "", reason: "", error: "", conflict: false, operationId: newOperationId() });
787
+ };
788
+
789
+ const closeSupersede = () => {
790
+ if (revisionBusy) return;
791
+ setRevisionDialog(null);
792
+ };
793
+
794
+ const submitSupersede = async (refreshOnly = false) => {
795
+ const dialog = revisionDialog;
796
+ if (!dialog || revisionBusy || !dialog.successorId) return;
797
+ setRevisionBusy(true);
798
+ setRevisionDialog({ ...dialog, error: "", conflict: false });
799
+ try {
800
+ const q = cwdQuery();
801
+ const suffix = q ? "?" + q : "";
802
+ // ETags are re-read on every attempt, including a retry after 409.
803
+ const [oldDetail, newDetail] = await Promise.all([
804
+ apiRaw("/api/dsh-trivium/nodes/" + dialog.oldId + "/detail" + suffix),
805
+ apiRaw("/api/dsh-trivium/nodes/" + dialog.successorId + "/detail" + suffix),
806
+ ]);
807
+ if (!oldDetail.ok || !newDetail.ok) throw new Error(t("detailFailed"));
808
+ if (refreshOnly) {
809
+ setRevisionDialog((prev) => (prev ? { ...prev, error: "", conflict: false } : prev));
810
+ setDetail((prev) => ({ ...prev, [dialog.oldId]: oldDetail.data, [dialog.successorId]: newDetail.data }));
811
+ setRevisionBusy(false);
812
+ return;
813
+ }
814
+ const result = await apiRaw("/api/dsh-trivium/nodes/" + dialog.oldId + "/supersede" + suffix, {
815
+ method: "POST",
816
+ headers: { "content-type": "application/json" },
817
+ body: JSON.stringify({
818
+ successorId: Number(dialog.successorId),
819
+ reason: dialog.reason,
820
+ // The same operationId is reused on retry, so a lost response
821
+ // cannot create a second edge.
822
+ operationId: dialog.operationId,
823
+ expectedEtags: { old: oldDetail.data.etag, new: newDetail.data.etag },
824
+ }),
825
+ });
826
+ if (!result.ok) {
827
+ const conflict = result.status === 409;
828
+ const code = (result.data && result.data.code) || "";
829
+ setRevisionDialog((prev) =>
830
+ prev
831
+ ? {
832
+ ...prev,
833
+ // The friendly conflict line already says "changed elsewhere,
834
+ // refresh"; keep the server's own wording only when it adds a
835
+ // different reason (a cycle, a linear-chain limit, ...).
836
+ errorCode: code,
837
+ error: (result.data && (result.data.detail || result.data.message)) || t("supersedeFailed"),
838
+ conflict,
839
+ }
840
+ : prev,
841
+ );
842
+ setRevisionBusy(false);
843
+ return;
844
+ }
845
+ setRevisionNote(t("supersedeOk"));
846
+ setRevisionDialog(null);
847
+ setRevisionBusy(false);
848
+ await load({ keepFlags: true });
849
+ } catch (err) {
850
+ setRevisionDialog((prev) => (prev ? { ...prev, error: String(err.message || err), conflict: false } : prev));
851
+ setRevisionBusy(false);
852
+ }
853
+ };
854
+
855
+ const revokeSupersession = async (oldId, successorId) => {
856
+ setRevisionBusy(true);
857
+ setRevisionNote("");
858
+ try {
859
+ const q = cwdQuery();
860
+ const suffix = q ? "?" + q : "";
861
+ const [oldDetail, newDetail] = await Promise.all([
862
+ apiRaw("/api/dsh-trivium/nodes/" + oldId + "/detail" + suffix),
863
+ apiRaw("/api/dsh-trivium/nodes/" + successorId + "/detail" + suffix),
864
+ ]);
865
+ const result = await apiRaw("/api/dsh-trivium/nodes/" + oldId + "/supersession/revoke" + suffix, {
866
+ method: "POST",
867
+ headers: { "content-type": "application/json" },
868
+ body: JSON.stringify({
869
+ successorId: Number(successorId),
870
+ operationId: newOperationId(),
871
+ expectedEtags: {
872
+ old: oldDetail.data ? oldDetail.data.etag : undefined,
873
+ new: newDetail.data ? newDetail.data.etag : undefined,
874
+ },
875
+ }),
876
+ });
877
+ setRevisionNote(
878
+ result.ok
879
+ ? t("revokeOk")
880
+ : (result.data && (result.data.detail || result.data.message)) || t("revokeFailed"),
881
+ );
882
+ if (result.ok) {
883
+ setHistoryInfo((prev) => {
884
+ const next = { ...prev };
885
+ delete next[oldId];
886
+ delete next[successorId];
887
+ return next;
888
+ });
889
+ await load({ keepFlags: true });
890
+ }
891
+ } catch (err) {
892
+ setRevisionNote(t("revokeFailed") + ": " + String(err.message || err));
893
+ } finally {
894
+ setRevisionBusy(false);
895
+ }
896
+ };
897
+
898
+ const loadNodeInfo = react.useCallback(
899
+ async (id, which) => {
900
+ const q = cwdQuery();
901
+ const base = "/api/dsh-trivium/nodes/" + id;
902
+ const settle = (setter, row) => setter((prev) => ({ ...prev, [id]: row }));
903
+ const setter = which === "detail" ? setDetail : which === "source" ? setSourceInfo : setHistoryInfo;
904
+ try {
905
+ if (which === "detail") settle(setter, await api(base + "/detail" + (q ? "?" + q : "")));
906
+ else if (which === "source") settle(setter, await api(base + "/provenance" + (q ? "?" + q : "")));
907
+ else if (which === "history") settle(setter, await api(base + "/history?" + (q ? q + "&" : "") + "limit=20"));
908
+ } catch (err) {
909
+ settle(setter, { error: String(err.message || err), status: 0 });
910
+ }
911
+ },
912
+ [status],
913
+ );
914
+
915
+ const pickNodeTab = (id, next) => {
916
+ setNodeTab(next);
917
+ if (next === "source" && !sourceInfo[id]) loadNodeInfo(id, "source");
918
+ if (next === "history" && !historyInfo[id]) loadNodeInfo(id, "history");
919
+ };
920
+
921
+ const loadTraces = react.useCallback(async () => {
922
+ setTraceError("");
923
+ try {
924
+ const q = cwdQuery();
925
+ const data = await api("/api/dsh-trivium/recall-traces" + (q ? "?" + q : ""));
926
+ setTraces((data && data.traces) || []);
927
+ } catch (err) {
928
+ setTraceError(String(err.message || err));
929
+ setTraces([]);
930
+ }
931
+ }, [status]);
932
+
933
+ const runPreview = async () => {
934
+ setPreviewBusy(true);
935
+ setTraceError("");
936
+ try {
937
+ const q = cwdQuery();
938
+ const sessionId = traces.length ? traces[0].sessionId || "" : "";
939
+ const data = await api("/api/dsh-trivium/recall-preview" + (q ? "?" + q : ""), {
940
+ method: "POST",
941
+ headers: { "content-type": "application/json" },
942
+ body: JSON.stringify({ query: previewQuery, sessionId, mode: status ? status.recallMode : "auto" }),
943
+ });
944
+ setPreview(data);
945
+ loadTraces();
946
+ } catch (err) {
947
+ setTraceError(String(err.message || err));
948
+ } finally {
949
+ setPreviewBusy(false);
950
+ }
951
+ };
952
+
953
+ react.useEffect(() => {
954
+ if (tab === "recall") loadTraces();
955
+ }, [tab, loadTraces]);
956
+
957
+ /*
958
+ * Dialog accessibility: focus moves into the panel, Escape closes it, and
959
+ * focus goes back to whatever opened it. The cleanup runs on unmount too,
960
+ * so a relayout cannot strand focus inside a removed node.
961
+ */
962
+ const dialogOpen = Boolean(revisionDialog);
963
+ react.useEffect(() => {
964
+ if (!dialogOpen) return undefined;
965
+ const previous = typeof document !== "undefined" ? document.activeElement : null;
966
+ const node = dialogRef.current;
967
+ if (node && typeof node.focus === "function") node.focus();
968
+ const onKey = (event) => {
969
+ if (event.key === "Escape") closeSupersede();
970
+ };
971
+ if (typeof document !== "undefined") document.addEventListener("keydown", onKey);
972
+ return () => {
973
+ if (typeof document !== "undefined") document.removeEventListener("keydown", onKey);
974
+ if (previous && typeof previous.focus === "function") previous.focus();
975
+ };
976
+ }, [dialogOpen]);
977
+
978
+ const reasonLabel = (code) => {
979
+ const map = {
980
+ budget: t("reasonBudget"),
981
+ "over-budget": t("reasonBudget"),
982
+ stale: t("reasonStale"),
983
+ archived: t("reasonArchived"),
984
+ hygiene: t("reasonHygiene"),
985
+ dirty: t("reasonHygiene"),
986
+ duplicate: t("reasonDuplicate"),
987
+ "duplicate-node": t("reasonDuplicate"),
988
+ "duplicate-in-turn": t("reasonDuplicate"),
989
+ "already-in-map": t("reasonDuplicate"),
990
+ revision: t("reasonRevision"),
991
+ "no-current": t("reasonRevision"),
992
+ invalid: t("reasonInvalid"),
993
+ empty: t("reasonHygiene"),
994
+ };
995
+ return map[code] || code;
996
+ };
997
+
998
+ const tabButton = (key, label, active, onPick) =>
999
+ jsx(
1000
+ "button",
1001
+ {
1002
+ type: "button",
1003
+ role: "tab",
1004
+ "aria-selected": active ? "true" : "false",
1005
+ style: active ? { ...ui.tab, ...ui.tabActive } : ui.tab,
1006
+ onClick: onPick,
1007
+ children: label,
1008
+ },
1009
+ key,
1010
+ );
1011
+
1012
+ const tabBar = (active, onPick, items) =>
1013
+ jsx("div", {
1014
+ style: ui.tabRow,
1015
+ role: "tablist",
1016
+ children: items.map((item) => tabButton(item.key, item.label, active === item.key, () => onPick(item.key))),
1017
+ });
1018
+
1019
+ const budgetBar = (tokens, budget) => {
1020
+ const cap = Math.max(1, Number(budget) || 300);
1021
+ const pct = Math.min(100, Math.round((Number(tokens) || 0) / cap * 100));
1022
+ return jsxs("div", {
1023
+ style: ui.stack,
1024
+ children: [
1025
+ jsxs("div", {
1026
+ style: { ...ui.muted, display: "flex", justifyContent: "space-between", gap: 8 },
1027
+ children: [
1028
+ jsx("span", { children: L.recallBudget }),
1029
+ jsx("span", {
1030
+ children:
1031
+ (Number(tokens) || 0) + " / " + cap + " " + t("estimate"),
1032
+ }),
1033
+ ],
1034
+ }),
1035
+ jsx("div", {
1036
+ style: ui.bar,
1037
+ role: "progressbar",
1038
+ "aria-valuenow": pct,
1039
+ "aria-valuemin": 0,
1040
+ "aria-valuemax": 100,
1041
+ children: jsx("div", { style: { ...ui.barFill, width: pct + "%" } }),
1042
+ }),
1043
+ ],
1044
+ });
1045
+ };
1046
+
1047
+ const sourceSection = (n) => {
1048
+ const info = sourceInfo[n.id];
1049
+ if (!info) return jsx("div", { style: ui.muted, children: L.detailLoading });
1050
+ if (info.error) return jsx("div", { style: ui.error, children: L.detailFailed + ": " + info.error });
1051
+ if (info.status === "missing") {
1052
+ return jsx("div", {
1053
+ style: ui.stack,
1054
+ children: [
1055
+ jsx("div", { style: ui.muted, children: L.sourceMissing }),
1056
+ jsx("div", { style: ui.muted, children: L.sourceNoJump }),
1057
+ ],
1058
+ });
1059
+ }
1060
+ const src = info.source || {};
1061
+ const rows = [
1062
+ [L.sourceKind, src.kind || src.via || ""],
1063
+ [L.sourceSession, src.sessionId || ""],
1064
+ [L.sourceCaptured, src.capturedAt || ""],
1065
+ [L.sourceImport, src.importPath || ""],
1066
+ ].filter((row) => row[1]);
1067
+ return jsxs("div", {
1068
+ style: ui.stack,
1069
+ children: [
1070
+ info.status === "unavailable"
1071
+ ? jsx("div", { style: ui.tag, children: L.sourceUnavailable })
1072
+ : null,
1073
+ ...rows.map((row) =>
1074
+ jsxs(
1075
+ "div",
1076
+ {
1077
+ style: { ...ui.muted, display: "flex", gap: 8, flexWrap: "wrap" },
1078
+ children: [
1079
+ jsx("span", { style: { opacity: 0.8 }, children: row[0] }),
1080
+ jsx("span", { children: row[1] }),
1081
+ ],
1082
+ },
1083
+ "src-" + row[0],
1084
+ ),
1085
+ ),
1086
+ src.quote
1087
+ ? jsxs("div", {
1088
+ style: ui.stack,
1089
+ children: [
1090
+ jsx("div", { style: ui.fieldLabel, children: L.sourceQuote }),
1091
+ // Plain text node: stored markup is shown, never executed.
1092
+ jsx("div", { style: { ...ui.muted, whiteSpace: "pre-wrap" }, children: src.quote }),
1093
+ ],
1094
+ })
1095
+ : null,
1096
+ jsx("div", { style: ui.muted, children: info.jump ? "" : L.sourceNoJump }),
1097
+ ],
1098
+ });
1099
+ };
1100
+
1101
+ const historySection = (n) => {
1102
+ const info = historyInfo[n.id];
1103
+ if (!info) return jsx("div", { style: ui.muted, children: L.detailLoading });
1104
+ if (info.error) return jsx("div", { style: ui.error, children: L.detailFailed + ": " + info.error });
1105
+ if (!info.nodes || !info.nodes.length) return jsx("div", { style: ui.muted, children: L.historyNone });
1106
+ return jsxs("div", {
1107
+ style: ui.stack,
1108
+ children: [
1109
+ jsx("div", { style: ui.muted, children: L.historyNone }),
1110
+ ...info.nodes.map((row) =>
1111
+ jsxs(
1112
+ "div",
1113
+ {
1114
+ style: {
1115
+ display: "flex",
1116
+ flexDirection: "column",
1117
+ gap: 2,
1118
+ paddingBottom: 6,
1119
+ borderBottom: "1px solid rgba(127,127,127,0.2)",
1120
+ },
1121
+ children: [
1122
+ jsxs("div", {
1123
+ style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center", fontSize: 12 },
1124
+ children: [
1125
+ jsx("span", { style: ui.tag, children: row.isCurrent ? L.historyCurrent : L.historyOlder }),
1126
+ jsx("strong", { children: "#" + row.id + " " + row.type }),
1127
+ row.expiryState === "expired" ? jsx("span", { style: ui.tag, children: L.expired }) : null,
1128
+ row.relation && row.relation.reason
1129
+ ? jsx("span", { style: ui.muted, children: L.historyReason + ": " + row.relation.reason })
1130
+ : null,
1131
+ ],
1132
+ }),
1133
+ jsx("div", { style: ui.muted, children: row.l0 }),
1134
+ ],
1135
+ },
1136
+ "hist-" + row.id,
1137
+ ),
1138
+ ),
1139
+ ],
1140
+ });
1141
+ };
1142
+
1143
+ const supersedeDialog = () => {
1144
+ const dialog = revisionDialog;
1145
+ if (!dialog) return null;
1146
+ const oldNode = nodes.find((x) => x.id === dialog.oldId) || {};
1147
+ const candidates = nodes.filter(
1148
+ (x) => x.type === "decision" && x.id !== dialog.oldId && (x.revision || "current") === "current",
1149
+ );
1150
+ const chosen = candidates.find((x) => String(x.id) === String(dialog.successorId));
1151
+ return jsx("div", {
1152
+ style: {
1153
+ position: "fixed",
1154
+ inset: 0,
1155
+ background: "rgba(0,0,0,0.42)",
1156
+ display: "flex",
1157
+ alignItems: "center",
1158
+ justifyContent: "center",
1159
+ padding: 16,
1160
+ zIndex: 60,
1161
+ },
1162
+ onClick: (event) => {
1163
+ if (event.target === event.currentTarget) closeSupersede();
1164
+ },
1165
+ children: jsxs("div", {
1166
+ ref: dialogRef,
1167
+ role: "dialog",
1168
+ "aria-modal": "true",
1169
+ "aria-label": L.supersedeTitle,
1170
+ tabIndex: -1,
1171
+ style: {
1172
+ background: "Canvas",
1173
+ color: "inherit",
1174
+ border: "1px solid rgba(127,127,127,0.35)",
1175
+ borderRadius: 12,
1176
+ padding: 16,
1177
+ width: "100%",
1178
+ maxWidth: "min(560px, 100%)",
1179
+ boxSizing: "border-box",
1180
+ maxHeight: "86vh",
1181
+ overflow: "auto",
1182
+ display: "flex",
1183
+ flexDirection: "column",
1184
+ gap: 10,
1185
+ },
1186
+ children: [
1187
+ jsx("div", { style: { fontSize: 15, fontWeight: 650 }, children: L.supersedeTitle }),
1188
+ jsx("div", { style: ui.hint, children: L.supersedeHint }),
1189
+ !candidates.length
1190
+ ? jsx("div", { style: ui.muted, children: L.noSuccessor })
1191
+ : labeledInput(
1192
+ L.supersedePick,
1193
+ jsx("select", {
1194
+ value: dialog.successorId,
1195
+ onChange: (event) =>
1196
+ setRevisionDialog((prev) => (prev ? { ...prev, successorId: event.target.value, error: "", conflict: false } : prev)),
1197
+ disabled: revisionBusy,
1198
+ style: ui.input,
1199
+ children: [
1200
+ jsx("option", { value: "", children: L.supersedePick }, "none"),
1201
+ ...candidates.map((row) =>
1202
+ jsx(
1203
+ "option",
1204
+ { value: String(row.id), children: "#" + row.id + " " + (row.name || row.text || "").slice(0, 48) },
1205
+ String(row.id),
1206
+ ),
1207
+ ),
1208
+ ],
1209
+ }),
1210
+ ),
1211
+ // The two existing decisions, side by side, never a generated one.
1212
+ jsxs("div", {
1213
+ style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 10 },
1214
+ children: [
1215
+ jsxs("div", {
1216
+ style: { ...ui.stack, border: "1px solid rgba(127,127,127,0.28)", borderRadius: 8, padding: 10 },
1217
+ children: [
1218
+ jsx("div", { style: ui.fieldLabel, children: L.supersedeOld }),
1219
+ jsx("div", { style: { fontSize: 13 }, children: "#" + oldNode.id + " " + (oldNode.name || "") }),
1220
+ jsx("div", { style: ui.muted, children: oldNode.text || "" }),
1221
+ ],
1222
+ }),
1223
+ jsxs("div", {
1224
+ style: { ...ui.stack, border: "1px solid rgba(127,127,127,0.28)", borderRadius: 8, padding: 10 },
1225
+ children: [
1226
+ jsx("div", { style: ui.fieldLabel, children: L.supersedeNew }),
1227
+ jsx("div", { style: { fontSize: 13 }, children: chosen ? "#" + chosen.id + " " + (chosen.name || "") : "—" }),
1228
+ jsx("div", { style: ui.muted, children: chosen ? chosen.text || "" : "" }),
1229
+ ],
1230
+ }),
1231
+ ],
1232
+ }),
1233
+ labeledInput(
1234
+ L.supersedeReason,
1235
+ jsx("textarea", {
1236
+ value: dialog.reason,
1237
+ maxLength: 240,
1238
+ rows: 2,
1239
+ disabled: revisionBusy,
1240
+ onChange: (event) => setRevisionDialog((prev) => (prev ? { ...prev, reason: event.target.value } : prev)),
1241
+ style: { ...ui.input, minHeight: 56, resize: "vertical" },
1242
+ }),
1243
+ ),
1244
+ dialog.error
1245
+ ? jsx("div", {
1246
+ style: ui.error,
1247
+ role: "alert",
1248
+ children: dialog.conflict
1249
+ ? dialog.errorCode === "etag" || dialog.errorCode === ""
1250
+ ? L.supersedeConflict
1251
+ : L.supersedeConflict + " " + dialog.error
1252
+ : L.supersedeFailed + " " + dialog.error,
1253
+ })
1254
+ : null,
1255
+ jsxs("div", {
1256
+ style: { display: "flex", justifyContent: "flex-end", gap: 8, flexWrap: "wrap" },
1257
+ children: [
1258
+ dialog.conflict
1259
+ ? jsx(Button, {
1260
+ variant: "outline",
1261
+ size: "sm",
1262
+ disabled: revisionBusy,
1263
+ onClick: () => submitSupersede(true),
1264
+ children: L.supersedeRefresh,
1265
+ })
1266
+ : null,
1267
+ jsx(Button, { variant: "outline", size: "sm", disabled: revisionBusy, onClick: closeSupersede, children: L.supersedeCancel }),
1268
+ jsx(Button, {
1269
+ variant: "primary",
1270
+ size: "sm",
1271
+ // Submitting twice would be two confirmations of one intent.
1272
+ disabled: revisionBusy || !dialog.successorId,
1273
+ onClick: () => submitSupersede(false),
1274
+ children: revisionBusy ? L.supersedeSaving : L.supersedeConfirm,
1275
+ }),
1276
+ ],
1277
+ }),
1278
+ ],
1279
+ }),
1280
+ });
1281
+ };
1282
+
1283
+ const recallPanel = () => {
1284
+ const plan = status && status.lastContextPlan;
1285
+ return jsxs("div", {
1286
+ style: ui.card,
1287
+ children: [
1288
+ jsx("div", { style: ui.cardTitle, children: L.tabRecall }),
1289
+ jsx("div", { style: ui.hint, children: L.recallBudgetHint }),
1290
+ budgetBar(plan ? plan.tokens : 0, plan ? plan.budget : status ? status.turnTokenBudget : 300),
1291
+ jsxs("div", {
1292
+ style: ui.toolbar,
1293
+ children: [
1294
+ jsx("input", {
1295
+ value: previewQuery,
1296
+ onChange: (e) => setPreviewQuery(e.target.value),
1297
+ placeholder: L.recallQueryPh,
1298
+ style: { ...ui.input, flex: "1 1 180px", minWidth: 0 },
1299
+ maxLength: 500,
1300
+ }),
1301
+ jsx(Button, {
1302
+ variant: "outline",
1303
+ size: "sm",
1304
+ disabled: previewBusy,
1305
+ onClick: runPreview,
1306
+ children: previewBusy ? L.loading : L.recallRun,
1307
+ }),
1308
+ ],
1309
+ }),
1310
+ jsx("div", { style: ui.muted, children: L.recallSemanticOff }),
1311
+ traceError ? jsx("div", { style: ui.error, children: traceError }) : null,
1312
+ !plan
1313
+ ? jsx("div", { style: ui.muted, children: L.recallNoTrace })
1314
+ : jsxs("div", {
1315
+ style: ui.stack,
1316
+ children: [
1317
+ jsx("div", { style: ui.fieldLabel, children: L.recallSelected }),
1318
+ jsxs("div", {
1319
+ style: { display: "flex", flexWrap: "wrap", gap: 6 },
1320
+ children: plan.records.length
1321
+ ? plan.records.map((row) =>
1322
+ jsx(
1323
+ "span",
1324
+ {
1325
+ style: ui.tag,
1326
+ children:
1327
+ "#" + row.id + " " + row.type + " · " + (row.channels || [row.channel]).join("+"),
1328
+ },
1329
+ "plan-" + row.id,
1330
+ ),
1331
+ )
1332
+ : jsx("span", { style: ui.muted, children: L.recallEmptyText }),
1333
+ }),
1334
+ plan.deferred && plan.deferred.length
1335
+ ? jsx("div", { style: ui.muted, children: L.recallDeferred + ": #" + plan.deferred.map((d) => d.id).join(", #") })
1336
+ : null,
1337
+ ],
1338
+ }),
1339
+ preview
1340
+ ? jsxs("div", {
1341
+ style: ui.stack,
1342
+ children: [
1343
+ budgetBar(preview.tokens, preview.budget),
1344
+ jsx("div", { style: ui.fieldLabel, children: L.recallSelected }),
1345
+ jsxs("div", {
1346
+ style: { display: "flex", flexWrap: "wrap", gap: 6 },
1347
+ children: preview.records.length
1348
+ ? preview.records.map((row) =>
1349
+ jsx(
1350
+ "span",
1351
+ { style: ui.tag, children: "#" + row.id + " " + row.type + " · " + row.channel },
1352
+ "pv-" + row.id,
1353
+ ),
1354
+ )
1355
+ : jsx("span", { style: ui.muted, children: L.recallEmptyText }),
1356
+ }),
1357
+ preview.dropped.length
1358
+ ? jsxs("div", {
1359
+ style: ui.stack,
1360
+ children: [
1361
+ jsx("div", { style: ui.fieldLabel, children: L.recallDropped }),
1362
+ jsxs("div", {
1363
+ style: { display: "flex", flexWrap: "wrap", gap: 6 },
1364
+ children: preview.dropped.map((row, i) =>
1365
+ jsx(
1366
+ "span",
1367
+ { style: ui.tag, children: "#" + (row.id ?? "?") + " " + reasonLabel(row.reason) },
1368
+ "drop-" + i,
1369
+ ),
1370
+ ),
1371
+ }),
1372
+ ],
1373
+ })
1374
+ : null,
1375
+ jsx("div", { style: ui.fieldLabel, children: L.recallWillSend }),
1376
+ jsx("pre", {
1377
+ style: { ...ui.mono, margin: 0, padding: 8, border: "1px solid rgba(127,127,127,0.28)", borderRadius: 6, maxHeight: 220, overflow: "auto" },
1378
+ children: (preview.text.pins + "\n" + preview.text.recall).trim() || L.recallEmptyText,
1379
+ }),
1380
+ ],
1381
+ })
1382
+ : null,
1383
+ jsx("div", { style: ui.fieldLabel, children: L.recallRecent }),
1384
+ !traces.length
1385
+ ? jsx("div", { style: ui.muted, children: L.recallNoTrace })
1386
+ : jsxs("div", {
1387
+ style: ui.stack,
1388
+ children: traces.map((row) =>
1389
+ jsxs(
1390
+ "div",
1391
+ {
1392
+ style: { ...ui.muted, display: "flex", gap: 8, flexWrap: "wrap" },
1393
+ children: [
1394
+ jsx("span", { style: ui.tag, children: row.semantic === "ran" ? "semantic" : "semantic: off" }),
1395
+ jsx("span", { children: row.query || "(empty)" }),
1396
+ jsx("span", { children: (row.stages || []).map((s) => s.stage).join(" → ") || "—" }),
1397
+ jsx("span", { children: row.tokens + " / " + row.budget + " " + t("estimate") }),
1398
+ ],
1399
+ },
1400
+ row.traceId,
1401
+ ),
1402
+ ),
1403
+ }),
1404
+ ],
1405
+ });
1406
+ };
1407
+
548
1408
 
549
1409
  const load = react.useCallback(async (opts = {}) => {
550
1410
  const seq = ++loadSeq.current;
@@ -571,6 +1431,9 @@ window.__ModuleLoader__.load({
571
1431
  if (type) params.set("type", type);
572
1432
  if (q.trim()) params.set("q", q.trim());
573
1433
  if (showStale) params.set("stale", "1");
1434
+ // Independent switch: revealing replaced versions is not the same
1435
+ // question as revealing expired ones.
1436
+ if (showSuperseded) params.set("superseded", "1");
574
1437
  if (aboutId) params.set("about", String(aboutId));
575
1438
  const qs = params.toString();
576
1439
  const list = await api("/api/dsh-trivium/nodes" + (qs ? "?" + qs : ""));
@@ -580,7 +1443,7 @@ window.__ModuleLoader__.load({
580
1443
  } catch (err) {
581
1444
  setError(String(err.message || err));
582
1445
  }
583
- }, [type, q, showStale, aboutId]);
1446
+ }, [type, q, showStale, showSuperseded, aboutId]);
584
1447
 
585
1448
  react.useEffect(() => {
586
1449
  load();
@@ -1246,7 +2109,7 @@ window.__ModuleLoader__.load({
1246
2109
  }),
1247
2110
  ),
1248
2111
  jsxs("div", {
1249
- style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 },
2112
+ style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 10 },
1250
2113
  children: [
1251
2114
  labeledInput(
1252
2115
  L.embedModel,
@@ -1277,7 +2140,13 @@ window.__ModuleLoader__.load({
1277
2140
  : null,
1278
2141
  ],
1279
2142
  }),
1280
- jsxs("div", {
2143
+ tabBar(tab, setTab, [
2144
+ { key: "list", label: L.tabList },
2145
+ { key: "recall", label: L.tabRecall },
2146
+ ]),
2147
+ tab === "recall"
2148
+ ? recallPanel()
2149
+ : jsxs("div", {
1281
2150
  style: ui.card,
1282
2151
  children: [
1283
2152
  jsx("div", { style: ui.cardTitle, children: L.list }),
@@ -1291,7 +2160,7 @@ window.__ModuleLoader__.load({
1291
2160
  jsx("select", {
1292
2161
  value: type,
1293
2162
  onChange: (e) => setType(e.target.value),
1294
- style: { ...ui.input, width: "auto", minWidth: 120 },
2163
+ style: { ...ui.input, width: "auto", minWidth: 0, maxWidth: 160 },
1295
2164
  children: TYPES.map((t) =>
1296
2165
  jsx("option", { value: t, children: t || L.all }, t || "all"),
1297
2166
  ),
@@ -1302,7 +2171,7 @@ window.__ModuleLoader__.load({
1302
2171
  value: q,
1303
2172
  onChange: (e) => setQ(e.target.value),
1304
2173
  placeholder: L.search + " " + L.searchPh,
1305
- style: { ...ui.input, flex: 1, minWidth: 160 },
2174
+ style: { ...ui.input, flex: "1 1 160px", minWidth: 0 },
1306
2175
  }),
1307
2176
  jsxs("label", {
1308
2177
  style: { display: "flex", alignItems: "center", gap: 6, fontSize: 13, whiteSpace: "nowrap" },
@@ -1315,6 +2184,17 @@ window.__ModuleLoader__.load({
1315
2184
  jsx("span", { children: L.showStale }),
1316
2185
  ],
1317
2186
  }),
2187
+ jsxs("label", {
2188
+ style: { display: "flex", alignItems: "center", gap: 6, fontSize: 13, whiteSpace: "nowrap" },
2189
+ children: [
2190
+ jsx("input", {
2191
+ type: "checkbox",
2192
+ checked: showSuperseded,
2193
+ onChange: (e) => setShowSuperseded(e.target.checked),
2194
+ }),
2195
+ jsx("span", { children: L.showSuperseded }),
2196
+ ],
2197
+ }),
1318
2198
  ],
1319
2199
  }),
1320
2200
  aboutId
@@ -1332,6 +2212,7 @@ window.__ModuleLoader__.load({
1332
2212
  })
1333
2213
  : null,
1334
2214
  jsx("div", { style: ui.hint, children: L.archiveHint }),
2215
+ revisionNote ? jsx("div", { style: ui.saved, role: "status", children: revisionNote }) : null,
1335
2216
  !nodes.length
1336
2217
  ? jsx("div", {
1337
2218
  style: { opacity: 0.7, fontSize: 13 },
@@ -1359,16 +2240,29 @@ window.__ModuleLoader__.load({
1359
2240
  display: "flex",
1360
2241
  flexDirection: "column",
1361
2242
  gap: 8,
2243
+ minWidth: 0,
2244
+ overflowWrap: "anywhere",
1362
2245
  },
1363
2246
  children: [
1364
2247
  jsxs("div", {
1365
2248
  style: { fontWeight: 600, cursor: "pointer", fontSize: 13 },
1366
- onClick: () => setExpanded(open ? null : n.id),
2249
+ onClick: () => {
2250
+ setExpanded(open ? null : n.id);
2251
+ // Detail is a small header: one request on expand,
2252
+ // never on list load.
2253
+ if (!open && !detail[n.id]) loadNodeInfo(n.id, "detail");
2254
+ },
1367
2255
  children: [
1368
2256
  "#" + n.id + " " + n.type,
1369
2257
  n.name ? " · " + n.name : "",
1370
2258
  n.until ? " · " + L.until + " " + n.until : "",
1371
- n.stale ? " · " + L.stale : "",
2259
+ n.stale ? " · " + L.expired : "",
2260
+ // Superseded is a different axis from expired and
2261
+ // gets its own label, never the expired one.
2262
+ n.revision === "superseded" ? " · " + L.historySuperseded : "",
2263
+ detail[n.id] && detail[n.id].revisionState === "unresolved"
2264
+ ? " · " + L.historyUnresolved
2265
+ : "",
1372
2266
  n.dirty ? " · " + L.dirty : "",
1373
2267
  ],
1374
2268
  }),
@@ -1383,6 +2277,17 @@ window.__ModuleLoader__.load({
1383
2277
  ? jsxs("div", {
1384
2278
  style: { display: "flex", flexDirection: "column", gap: 8 },
1385
2279
  children: [
2280
+ tabBar(
2281
+ nodeTab,
2282
+ (next) => pickNodeTab(n.id, next),
2283
+ [
2284
+ { key: "content", label: L.nodeTabContent },
2285
+ { key: "source", label: L.nodeTabSource },
2286
+ { key: "history", label: L.nodeTabHistory },
2287
+ ],
2288
+ ),
2289
+ ...(nodeTab === "content"
2290
+ ? [
1386
2291
  labeledInput(
1387
2292
  L.name,
1388
2293
  jsx("input", {
@@ -1454,7 +2359,7 @@ window.__ModuleLoader__.load({
1454
2359
  value: mergeFrom[n.id] || "",
1455
2360
  onChange: (ev) =>
1456
2361
  setMergeFrom((prev) => ({ ...prev, [n.id]: ev.target.value })),
1457
- style: { ...ui.input, width: "auto", minWidth: 160, flex: 1 },
2362
+ style: { ...ui.input, width: "auto", minWidth: 0, flex: "1 1 140px" },
1458
2363
  children: [
1459
2364
  jsx("option", { value: "", children: L.mergePick }, "none"),
1460
2365
  ...peers.map((p) =>
@@ -1482,6 +2387,10 @@ window.__ModuleLoader__.load({
1482
2387
  : null,
1483
2388
  ],
1484
2389
  }),
2390
+ ]
2391
+ : []),
2392
+ ...(nodeTab === "source" ? [sourceSection(n)] : []),
2393
+ ...(nodeTab === "history" ? [historySection(n)] : []),
1485
2394
  ],
1486
2395
  })
1487
2396
  : null,
@@ -1527,6 +2436,35 @@ window.__ModuleLoader__.load({
1527
2436
  : jsxs("div", {
1528
2437
  style: { display: "flex", gap: 8, flexWrap: "wrap" },
1529
2438
  children: [
2439
+ n.type === "decision"
2440
+ ? jsx(Button, {
2441
+ variant: "outline",
2442
+ size: "sm",
2443
+ onClick: () => openSupersede(n.id),
2444
+ children: L.supersede,
2445
+ })
2446
+ : null,
2447
+ n.type === "decision" && n.replacesId
2448
+ ? jsx(Button, {
2449
+ variant: "outline",
2450
+ size: "sm",
2451
+ disabled: revisionBusy,
2452
+ onClick: () => revokeSupersession(n.replacesId, n.id),
2453
+ children: L.revoke,
2454
+ })
2455
+ : null,
2456
+ n.supersededBy
2457
+ ? jsx(Button, {
2458
+ variant: "outline",
2459
+ size: "sm",
2460
+ onClick: () => {
2461
+ setExpanded(n.supersededBy);
2462
+ setNodeTab("content");
2463
+ loadNodeInfo(n.supersededBy, "detail");
2464
+ },
2465
+ children: L.historyViewCurrent,
2466
+ })
2467
+ : null,
1530
2468
  n.type === "entity"
1531
2469
  ? jsx(Button, {
1532
2470
  variant: "outline",
@@ -1557,6 +2495,7 @@ window.__ModuleLoader__.load({
1557
2495
  }),
1558
2496
  ],
1559
2497
  }),
2498
+ revisionDialog ? supersedeDialog() : null,
1560
2499
  ],
1561
2500
  });
1562
2501
  }