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/README.md +49 -6
- package/README.zh-CN.md +49 -6
- package/lib/client.js +948 -9
- package/lib/context-budget.js +348 -0
- package/lib/extract.js +25 -11
- package/lib/index.js +236 -63
- package/lib/markdown.js +0 -2
- package/lib/pins.js +66 -21
- package/lib/provenance.js +147 -0
- package/lib/recall-preview.js +128 -0
- package/lib/recall-trace.js +285 -0
- package/lib/revision-graph.js +103 -0
- package/lib/revisions.js +451 -0
- package/lib/schema.js +13 -1
- package/lib/server.js +204 -4
- package/lib/sidecar.js +122 -15
- package/lib/store.js +480 -62
- package/lib/tool-defs.js +93 -0
- package/lib/tool-output.js +88 -0
- package/lib/tools.js +69 -43
- package/lib/turn-ledger.js +184 -0
- package/lib/until.js +1 -13
- package/package.json +10 -4
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: {
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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: () =>
|
|
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.
|
|
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:
|
|
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
|
}
|