dsh-retrace 0.4.2 → 0.4.3
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.bundle.js +326 -80
- package/lib/client.js +434 -101
- package/lib/dynamic-client.js +326 -80
- package/lib/forkmap.js +106 -0
- package/lib/http.js +14 -0
- package/lib/projection/forkmap.js +69 -0
- package/lib/versioning.js +16 -5
- package/package.json +1 -1
package/lib/dynamic-client.js
CHANGED
|
@@ -126,7 +126,17 @@ return {
|
|
|
126
126
|
"timeline.gitInitDesc": "\u5728\u5DE5\u4F5C\u533A\u6267\u884C git init\uFF08\u4EC5\u6DFB\u52A0 .gitignore \u4E0E\u4E00\u4E2A\u57FA\u7EBF\u63D0\u4EA4\uFF09\uFF0C\u7248\u672C\u56DE\u9000\u5C06\u4F18\u5148\u4F7F\u7528 git\u3002",
|
|
127
127
|
"timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
|
|
128
128
|
"error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
|
|
129
|
-
"error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
|
|
129
|
+
"error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C",
|
|
130
|
+
"view.fork": "\u5206\u53C9",
|
|
131
|
+
"fork.title": "\u5206\u53C9\u56FE",
|
|
132
|
+
"fork.empty": "\u8FD8\u6CA1\u6709\u5206\u53C9\u3002\u64A4\u56DE / \u7F16\u8F91 / \u91CD\u65B0\u751F\u6210 / \u6062\u590D\u4F1A\u5728\u6B64\u4EA7\u751F\u5206\u53C9\u3002",
|
|
133
|
+
"fork.refresh": "\u5237\u65B0",
|
|
134
|
+
"fork.spine": "\u5F53\u524D\u8DEF\u5F84",
|
|
135
|
+
"fork.shadowed": "\u88AB\u906E\u853D {count} \u4E2A\u8282\u70B9",
|
|
136
|
+
"fork.node.user": "\u7528\u6237\u6D88\u606F",
|
|
137
|
+
"fork.node.assistant": "\u52A9\u624B\u56DE\u590D",
|
|
138
|
+
"fork.node.tool": "\u5DE5\u5177\u7ED3\u679C",
|
|
139
|
+
"fork.histTitle": "\u5386\u53F2\u5206\u53C9\u70B9"
|
|
130
140
|
};
|
|
131
141
|
var en = {
|
|
132
142
|
"action.edit": "Edit",
|
|
@@ -201,7 +211,17 @@ return {
|
|
|
201
211
|
"timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
|
|
202
212
|
"timeline.gitInitConfirm": "Initialize git in this workspace?",
|
|
203
213
|
"error.generic": "Operation failed; please try again",
|
|
204
|
-
"error.busy": "Stop the current reply before recalling or editing"
|
|
214
|
+
"error.busy": "Stop the current reply before recalling or editing",
|
|
215
|
+
"view.fork": "Fork",
|
|
216
|
+
"fork.title": "Fork map",
|
|
217
|
+
"fork.empty": "No forks yet. Recall / edit / regenerate / restore fork the path here.",
|
|
218
|
+
"fork.refresh": "Refresh",
|
|
219
|
+
"fork.spine": "Current path",
|
|
220
|
+
"fork.shadowed": "{count} shadowed nodes",
|
|
221
|
+
"fork.node.user": "User message",
|
|
222
|
+
"fork.node.assistant": "Assistant reply",
|
|
223
|
+
"fork.node.tool": "Tool result",
|
|
224
|
+
"fork.histTitle": "Historical fork points"
|
|
205
225
|
};
|
|
206
226
|
var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
|
|
207
227
|
function isReplacementSurfaceEvent(event) {
|
|
@@ -411,29 +431,51 @@ return {
|
|
|
411
431
|
});
|
|
412
432
|
}
|
|
413
433
|
var SHADOW_SAFETY_RATIO = 0.4;
|
|
414
|
-
function
|
|
434
|
+
function hiddenKeysFor(shadowedSeqs, nodes) {
|
|
435
|
+
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
|
|
436
|
+
const hidden = new Set(shadowedSeqs);
|
|
437
|
+
const keys = [];
|
|
438
|
+
for (const node of nodes.values()) {
|
|
439
|
+
if (node.kind === "recall-marker") continue;
|
|
440
|
+
if (node.kind === "turn-tail") {
|
|
441
|
+
const closingSeq = node.data?.closing?.finalNode?.seq;
|
|
442
|
+
if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (node.kind === "tool-call") {
|
|
446
|
+
const resultSeq = node.data?.root?.seq;
|
|
447
|
+
if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
|
|
451
|
+
}
|
|
452
|
+
return keys.length === 0 ? null : keys;
|
|
453
|
+
}
|
|
454
|
+
var EMPTY_HIDE_PLAN = { degraded: false, hiddenFor: () => null };
|
|
455
|
+
function useMarkerHidePlan(useSession) {
|
|
415
456
|
return useSession((snapshot) => {
|
|
416
|
-
|
|
417
|
-
const hidden = new Set(shadowedSeqs);
|
|
418
|
-
const keys = [];
|
|
457
|
+
const nodes = snapshot.chat.nodes;
|
|
419
458
|
let rowCount = 0;
|
|
420
|
-
|
|
459
|
+
const markers = [];
|
|
460
|
+
for (const node of nodes.values()) {
|
|
421
461
|
if (typeof node.anchorSeq === "number") rowCount += 1;
|
|
422
|
-
if (node.kind === "recall-marker")
|
|
423
|
-
|
|
424
|
-
const closingSeq = node.data?.closing?.finalNode?.seq;
|
|
425
|
-
if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
if (node.kind === "tool-call") {
|
|
429
|
-
const resultSeq = node.data?.root?.seq;
|
|
430
|
-
if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
|
|
431
|
-
continue;
|
|
462
|
+
if (node.kind === "recall-marker") {
|
|
463
|
+
markers.push({ key: node.key, shadowedSeqs: node.data?.shadowedSeqs });
|
|
432
464
|
}
|
|
433
|
-
if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
|
|
434
465
|
}
|
|
435
|
-
if (
|
|
436
|
-
|
|
466
|
+
if (markers.length === 0) return EMPTY_HIDE_PLAN;
|
|
467
|
+
const union = /* @__PURE__ */ new Set();
|
|
468
|
+
const perMarker = /* @__PURE__ */ new Map();
|
|
469
|
+
for (const marker of markers) {
|
|
470
|
+
const keys = hiddenKeysFor(marker.shadowedSeqs, nodes);
|
|
471
|
+
perMarker.set(marker.key, keys);
|
|
472
|
+
if (keys !== null) for (const key of keys) union.add(key);
|
|
473
|
+
}
|
|
474
|
+
const degraded = rowCount > 0 && union.size / rowCount > SHADOW_SAFETY_RATIO;
|
|
475
|
+
return {
|
|
476
|
+
degraded,
|
|
477
|
+
hiddenFor: (key) => degraded ? null : perMarker.get(key) ?? null
|
|
478
|
+
};
|
|
437
479
|
});
|
|
438
480
|
}
|
|
439
481
|
function useMarkerDismissed(useSession, markerSeq, op) {
|
|
@@ -635,7 +677,8 @@ return {
|
|
|
635
677
|
function RecallMarkerRow({ node, useSession, t }) {
|
|
636
678
|
const { seq, op, shadowedSeqs, legacy } = node.data;
|
|
637
679
|
const dismissed = useMarkerDismissed(useSession, seq, op);
|
|
638
|
-
const
|
|
680
|
+
const hidePlan = useMarkerHidePlan(useSession);
|
|
681
|
+
const hiddenKeys = legacy || !getConfig().hideShadowed ? null : hidePlan.hiddenFor(node.key);
|
|
639
682
|
const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
|
|
640
683
|
const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
|
|
641
684
|
const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
|
|
@@ -736,7 +779,8 @@ return {
|
|
|
736
779
|
.dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
|
|
737
780
|
.dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
|
|
738
781
|
/* ---- P1 timeline (conversation view tab) ---- */
|
|
739
|
-
|
|
782
|
+
|
|
783
|
+
.dsh-rt-view{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;flex:1 1 0%;min-height:0;overflow:hidden;padding:12px 16px 0}
|
|
740
784
|
.dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
|
|
741
785
|
.dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
|
|
742
786
|
.dsh-rt-timeline-git{display:flex;align-items:center;gap:6px;flex:none;border:1px dashed var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px}
|
|
@@ -773,7 +817,121 @@ return {
|
|
|
773
817
|
.dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
|
|
774
818
|
.dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
|
|
775
819
|
.dsh-rt-modal-json{margin:0;overflow:auto;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;font-family:var(--dsw-font-mono);font-size:11px;line-height:15px;white-space:pre-wrap;word-break:break-all}
|
|
820
|
+
.dsh-rt-fork-list{overflow-y:auto;flex:1;min-height:0;position:relative}
|
|
821
|
+
.dsh-rt-fork-spine-label{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;flex:none}
|
|
822
|
+
.dsh-rt-fork-row{position:absolute;left:0;right:0;box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:8px}
|
|
823
|
+
.dsh-rt-fork-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
824
|
+
.dsh-rt-fork-icon{flex:none;width:22px;height:22px;display:inline-flex;justify-content:center;align-items:center;font-size:13px;color:var(--dsw-alias-label-secondary)}
|
|
825
|
+
.dsh-rt-fork-boundary .dsh-rt-fork-icon{color:var(--dsw-alias-state-warning-primary)}
|
|
826
|
+
.dsh-rt-fork-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
827
|
+
.dsh-rt-fork-line{display:flex;align-items:baseline;gap:8px;min-width:0}
|
|
828
|
+
.dsh-rt-fork-label{font-size:12px;font-weight:500;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}
|
|
829
|
+
.dsh-rt-fork-seq{font-family:var(--dsw-font-mono);font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);flex:none}
|
|
830
|
+
.dsh-rt-fork-shadowed{font-size:11px;line-height:18px;color:var(--dsw-alias-state-warning-primary);flex:none}
|
|
831
|
+
.dsh-rt-fork-text{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
832
|
+
.dsh-rt-fork-boundary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated)}
|
|
833
|
+
.dsh-rt-fork-hist{display:flex;flex-direction:column;gap:4px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px;overflow-y:auto;flex:none}
|
|
834
|
+
.dsh-rt-fork-hist-row{display:flex;align-items:flex-start;gap:8px;padding:4px 8px;border-radius:8px}
|
|
835
|
+
.dsh-rt-fork-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
776
836
|
`;
|
|
837
|
+
var JUMP_PAGE_BUDGET = 24;
|
|
838
|
+
function switchToViewTab(viewId) {
|
|
839
|
+
const ORDER = { chat: 0, trajectory: 1, retrace: 2, "retrace-fork": 3 };
|
|
840
|
+
const index = ORDER[viewId];
|
|
841
|
+
if (index === void 0) {
|
|
842
|
+
console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
const buttons = [...document.querySelectorAll('[role="tablist"] [role="tab"]')];
|
|
846
|
+
const button = buttons[index];
|
|
847
|
+
if (!button) {
|
|
848
|
+
console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
button.click();
|
|
852
|
+
}
|
|
853
|
+
function waitForElement(selector, frames) {
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
let remaining = frames;
|
|
856
|
+
const probe = () => {
|
|
857
|
+
const el = document.querySelector(selector);
|
|
858
|
+
if (el !== null) return resolve(el);
|
|
859
|
+
if (remaining-- <= 0) return resolve(null);
|
|
860
|
+
requestAnimationFrame(probe);
|
|
861
|
+
};
|
|
862
|
+
requestAnimationFrame(probe);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
function flashKey(key) {
|
|
866
|
+
const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
|
|
867
|
+
if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
|
|
868
|
+
const tag = document.createElement("style");
|
|
869
|
+
tag.dataset.pluginCss = id;
|
|
870
|
+
tag.textContent = `[data-chat-anchor-key=${JSON.stringify(key)}]{animation:dsh-rt-flash 1.6s ease-out 2}@keyframes dsh-rt-flash{0%,100%{background:transparent}30%,70%{background:var(--dsw-alias-state-business-primary)}55%{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}}`;
|
|
871
|
+
document.head.appendChild(tag);
|
|
872
|
+
setTimeout(() => tag.remove(), 3400);
|
|
873
|
+
}
|
|
874
|
+
async function jumpToAnchor(store, anchorSeq) {
|
|
875
|
+
switchToViewTab("chat");
|
|
876
|
+
if (!store || typeof store.loadOlder !== "function" || typeof store.getSnapshot !== "function") return;
|
|
877
|
+
const keyOfSeq = (seq) => {
|
|
878
|
+
const nodes = store.getSnapshot()?.chat?.nodes;
|
|
879
|
+
if (!nodes) return null;
|
|
880
|
+
for (const node of nodes.values()) {
|
|
881
|
+
if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
|
|
882
|
+
}
|
|
883
|
+
return null;
|
|
884
|
+
};
|
|
885
|
+
let key = keyOfSeq(anchorSeq);
|
|
886
|
+
let pages = 0;
|
|
887
|
+
while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
|
|
888
|
+
const before = store.getSnapshot()?.chat?.nodes?.size ?? 0;
|
|
889
|
+
await store.loadOlder();
|
|
890
|
+
const after = store.getSnapshot()?.chat?.nodes?.size ?? 0;
|
|
891
|
+
pages += 1;
|
|
892
|
+
if (after === before) break;
|
|
893
|
+
key = keyOfSeq(anchorSeq);
|
|
894
|
+
}
|
|
895
|
+
if (key === null) {
|
|
896
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
897
|
+
key = keyOfSeq(anchorSeq);
|
|
898
|
+
}
|
|
899
|
+
if (key === null) {
|
|
900
|
+
console.warn(`[dsh-retrace] jump target seq ${anchorSeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
|
|
904
|
+
if (el === null) return;
|
|
905
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
906
|
+
flashKey(key);
|
|
907
|
+
}
|
|
908
|
+
var NODE_ICONS = { "user/message": "\u{1F464}", "assistant/message": "\u{1F916}", "tool/result": "\u{1F527}" };
|
|
909
|
+
function nodeTypeLabel(type, t) {
|
|
910
|
+
if (type === "user/message") return t("fork.node.user");
|
|
911
|
+
if (type === "assistant/message") return t("fork.node.assistant");
|
|
912
|
+
if (type === "tool/result") return t("fork.node.tool");
|
|
913
|
+
return type;
|
|
914
|
+
}
|
|
915
|
+
function bindListHeight(listEl) {
|
|
916
|
+
if (!listEl) return () => {
|
|
917
|
+
};
|
|
918
|
+
const measure = () => {
|
|
919
|
+
const top = listEl.getBoundingClientRect().top;
|
|
920
|
+
const height = Math.max(120, window.innerHeight - top - 16);
|
|
921
|
+
if (Math.abs(listEl.clientHeight - height) > 4) {
|
|
922
|
+
listEl.style.flex = "none";
|
|
923
|
+
listEl.style.height = `${height}px`;
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
measure();
|
|
927
|
+
const ro = typeof ResizeObserver === "function" ? new ResizeObserver(measure) : null;
|
|
928
|
+
ro?.observe(document.body);
|
|
929
|
+
window.addEventListener("resize", measure);
|
|
930
|
+
return () => {
|
|
931
|
+
ro?.disconnect();
|
|
932
|
+
window.removeEventListener("resize", measure);
|
|
933
|
+
};
|
|
934
|
+
}
|
|
777
935
|
function ensureStyle() {
|
|
778
936
|
if (typeof document === "undefined") return () => {
|
|
779
937
|
};
|
|
@@ -841,63 +999,7 @@ return {
|
|
|
841
999
|
if (projected === void 0) refresh();
|
|
842
1000
|
refreshGit();
|
|
843
1001
|
}, []);
|
|
844
|
-
const
|
|
845
|
-
const jump = async (boundarySeq) => {
|
|
846
|
-
actions?.setView?.("chat");
|
|
847
|
-
const target = store;
|
|
848
|
-
if (!target || typeof target.loadOlder !== "function" || typeof target.getSnapshot !== "function") return;
|
|
849
|
-
const keyOfSeq = (seq) => {
|
|
850
|
-
const nodes = target.getSnapshot()?.chat?.nodes;
|
|
851
|
-
if (!nodes) return null;
|
|
852
|
-
for (const node of nodes.values()) {
|
|
853
|
-
if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
|
|
854
|
-
}
|
|
855
|
-
return null;
|
|
856
|
-
};
|
|
857
|
-
let key = keyOfSeq(boundarySeq);
|
|
858
|
-
let pages = 0;
|
|
859
|
-
while (key === null && pages < JUMP_PAGE_BUDGET && target.hasMore) {
|
|
860
|
-
const before = target.getSnapshot()?.chat?.nodes?.size ?? 0;
|
|
861
|
-
await target.loadOlder();
|
|
862
|
-
const after = target.getSnapshot()?.chat?.nodes?.size ?? 0;
|
|
863
|
-
pages += 1;
|
|
864
|
-
if (after === before) break;
|
|
865
|
-
key = keyOfSeq(boundarySeq);
|
|
866
|
-
}
|
|
867
|
-
if (key === null) {
|
|
868
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
869
|
-
key = keyOfSeq(boundarySeq);
|
|
870
|
-
}
|
|
871
|
-
if (key === null) {
|
|
872
|
-
console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
|
|
876
|
-
if (el === null) return;
|
|
877
|
-
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
878
|
-
flashKey(key);
|
|
879
|
-
};
|
|
880
|
-
function waitForElement(selector, frames) {
|
|
881
|
-
return new Promise((resolve) => {
|
|
882
|
-
let remaining = frames;
|
|
883
|
-
const probe = () => {
|
|
884
|
-
const el = document.querySelector(selector);
|
|
885
|
-
if (el !== null) return resolve(el);
|
|
886
|
-
if (remaining-- <= 0) return resolve(null);
|
|
887
|
-
requestAnimationFrame(probe);
|
|
888
|
-
};
|
|
889
|
-
requestAnimationFrame(probe);
|
|
890
|
-
});
|
|
891
|
-
}
|
|
892
|
-
function flashKey(key) {
|
|
893
|
-
const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
|
|
894
|
-
if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
|
|
895
|
-
const tag = document.createElement("style");
|
|
896
|
-
tag.dataset.pluginCss = id;
|
|
897
|
-
tag.textContent = `[data-chat-anchor-key=${JSON.stringify(key)}]{animation:dsh-rt-flash 1.6s ease-out 2}@keyframes dsh-rt-flash{0%,100%{background:transparent}30%,70%{background:var(--dsw-alias-state-business-primary)}55%{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}}`;
|
|
898
|
-
document.head.appendChild(tag);
|
|
899
|
-
setTimeout(() => tag.remove(), 3400);
|
|
900
|
-
}
|
|
1002
|
+
const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq);
|
|
901
1003
|
const requestPreview = (record) => {
|
|
902
1004
|
setPreviewScope("both");
|
|
903
1005
|
setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
|
|
@@ -930,6 +1032,10 @@ return {
|
|
|
930
1032
|
const ROW_H = 64;
|
|
931
1033
|
const list = versions ?? [];
|
|
932
1034
|
const visible = list.slice(Math.max(0, Math.floor(scrollTop / ROW_H) - 2), Math.min(list.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2));
|
|
1035
|
+
(0, import_react.useEffect)(() => {
|
|
1036
|
+
if (list.length === 0) return void 0;
|
|
1037
|
+
return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-timeline-list"));
|
|
1038
|
+
}, [list.length]);
|
|
933
1039
|
return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
|
|
934
1040
|
(0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
|
|
935
1041
|
(0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
|
|
@@ -967,7 +1073,7 @@ return {
|
|
|
967
1073
|
t,
|
|
968
1074
|
top: list.indexOf(record) * ROW_H,
|
|
969
1075
|
onPreview: () => requestPreview(record),
|
|
970
|
-
onTrajectory: () =>
|
|
1076
|
+
onTrajectory: () => switchToViewTab("trajectory"),
|
|
971
1077
|
onJump: () => jump(record.boundarySeq)
|
|
972
1078
|
}))
|
|
973
1079
|
])
|
|
@@ -1050,6 +1156,135 @@ return {
|
|
|
1050
1156
|
])
|
|
1051
1157
|
]);
|
|
1052
1158
|
}
|
|
1159
|
+
function ForkView({ sessionId, useProjection, t, actions, store }) {
|
|
1160
|
+
const [fork, setFork] = (0, import_react.useState)(null);
|
|
1161
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
1162
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
1163
|
+
const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
|
|
1164
|
+
const projected = typeof useProjection === "function" ? useProjection("retrace/forkmap") : void 0;
|
|
1165
|
+
const versions = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
|
|
1166
|
+
const markerBySeq = new Map((versions?.versions ?? []).map((v) => [v.boundarySeq, v.markerText]));
|
|
1167
|
+
(0, import_react.useEffect)(() => {
|
|
1168
|
+
if (projected && Array.isArray(projected.nodes) && Array.isArray(projected.boundaries)) setFork(projected);
|
|
1169
|
+
}, [projected]);
|
|
1170
|
+
const refresh = () => {
|
|
1171
|
+
setLoading(true);
|
|
1172
|
+
setError(null);
|
|
1173
|
+
timelineGet(`/forkmap?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
|
|
1174
|
+
if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "forkmap failed");
|
|
1175
|
+
setFork(result.value?.nodes ? result.value : null);
|
|
1176
|
+
}).catch((cause) => setError(cause?.message ?? "forkmap error")).finally(() => setLoading(false));
|
|
1177
|
+
};
|
|
1178
|
+
(0, import_react.useEffect)(() => {
|
|
1179
|
+
if (projected === void 0) refresh();
|
|
1180
|
+
}, []);
|
|
1181
|
+
const nodes = fork?.nodes ?? [];
|
|
1182
|
+
const boundaries = fork?.boundaries ?? [];
|
|
1183
|
+
const boundaryBySeq = new Map(boundaries.map((b) => [b.seq, b]));
|
|
1184
|
+
const spineSeqSet = new Set(nodes.map((n) => n.seq));
|
|
1185
|
+
const offSpine = boundaries.filter((b) => !spineSeqSet.has(b.seq));
|
|
1186
|
+
(0, import_react.useEffect)(() => {
|
|
1187
|
+
if (nodes.length === 0) return void 0;
|
|
1188
|
+
return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-fork-list"));
|
|
1189
|
+
}, [nodes.length]);
|
|
1190
|
+
const ROW_H = 56;
|
|
1191
|
+
const visible = nodes.slice(
|
|
1192
|
+
Math.max(0, Math.floor(scrollTop / ROW_H) - 2),
|
|
1193
|
+
Math.min(nodes.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2)
|
|
1194
|
+
);
|
|
1195
|
+
return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
|
|
1196
|
+
(0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
|
|
1197
|
+
(0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("fork.title")),
|
|
1198
|
+
(0, import_react.createElement)("button", {
|
|
1199
|
+
key: "refresh",
|
|
1200
|
+
type: "button",
|
|
1201
|
+
className: "dsh-rt-chip",
|
|
1202
|
+
onClick: refresh
|
|
1203
|
+
}, t("fork.refresh"))
|
|
1204
|
+
]),
|
|
1205
|
+
error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
|
|
1206
|
+
loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
|
|
1207
|
+
!loading && nodes.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("fork.empty")),
|
|
1208
|
+
nodes.length > 0 && (0, import_react.createElement)("div", { key: "spine", className: "dsh-rt-fork-spine-label" }, t("fork.spine")),
|
|
1209
|
+
nodes.length > 0 && (0, import_react.createElement)("div", {
|
|
1210
|
+
key: "list",
|
|
1211
|
+
className: "dsh-rt-fork-list",
|
|
1212
|
+
onScroll: (event) => setScrollTop(event.target.scrollTop)
|
|
1213
|
+
}, [
|
|
1214
|
+
(0, import_react.createElement)("div", { key: "spacer", style: { height: `${nodes.length * ROW_H}px`, position: "relative" } }, [
|
|
1215
|
+
visible.map((node) => (0, import_react.createElement)(ForkRow, {
|
|
1216
|
+
key: node.seq,
|
|
1217
|
+
node,
|
|
1218
|
+
boundary: boundaryBySeq.get(node.seq),
|
|
1219
|
+
markerText: markerBySeq.get(node.seq),
|
|
1220
|
+
t,
|
|
1221
|
+
top: nodes.indexOf(node) * ROW_H,
|
|
1222
|
+
onJump: () => jumpToAnchor(store, node.seq)
|
|
1223
|
+
}))
|
|
1224
|
+
])
|
|
1225
|
+
]),
|
|
1226
|
+
offSpine.length > 0 && (0, import_react.createElement)("div", { key: "hist", className: "dsh-rt-fork-hist" }, [
|
|
1227
|
+
(0, import_react.createElement)("div", { key: "title", className: "dsh-rt-fork-spine-label" }, t("fork.histTitle")),
|
|
1228
|
+
offSpine.map((boundary) => (0, import_react.createElement)(ForkHistRow, {
|
|
1229
|
+
key: boundary.seq,
|
|
1230
|
+
boundary,
|
|
1231
|
+
markerText: markerBySeq.get(boundary.seq),
|
|
1232
|
+
t,
|
|
1233
|
+
onJump: () => jumpToAnchor(store, boundary.seq)
|
|
1234
|
+
}))
|
|
1235
|
+
])
|
|
1236
|
+
]);
|
|
1237
|
+
}
|
|
1238
|
+
function ForkRow({ node, boundary, markerText, t, top, onJump }) {
|
|
1239
|
+
const isBoundary = boundary !== void 0;
|
|
1240
|
+
const icon = isBoundary ? KIND_ICONS[boundary.kind] ?? "\u2022" : NODE_ICONS[node.type] ?? "\u2022";
|
|
1241
|
+
const label = isBoundary ? kindLabel(boundary.kind, t) : nodeTypeLabel(node.type, t);
|
|
1242
|
+
return (0, import_react.createElement)("div", {
|
|
1243
|
+
className: `dsh-rt-fork-row${isBoundary ? " dsh-rt-fork-boundary" : ""}`,
|
|
1244
|
+
style: { top: `${top}px` }
|
|
1245
|
+
}, [
|
|
1246
|
+
(0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, icon),
|
|
1247
|
+
(0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
|
|
1248
|
+
(0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
|
|
1249
|
+
(0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, label),
|
|
1250
|
+
(0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${node.seq}`),
|
|
1251
|
+
isBoundary && boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
|
|
1252
|
+
"span",
|
|
1253
|
+
{ key: "shadowed", className: "dsh-rt-fork-shadowed" },
|
|
1254
|
+
t("fork.shadowed", { count: boundary.replacedSeqs.length })
|
|
1255
|
+
)
|
|
1256
|
+
]),
|
|
1257
|
+
isBoundary && markerText && (0, import_react.createElement)(
|
|
1258
|
+
"div",
|
|
1259
|
+
{ key: "text", className: "dsh-rt-fork-text" },
|
|
1260
|
+
markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
|
|
1261
|
+
)
|
|
1262
|
+
]),
|
|
1263
|
+
(0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
|
|
1264
|
+
]);
|
|
1265
|
+
}
|
|
1266
|
+
function ForkHistRow({ boundary, markerText, t, onJump }) {
|
|
1267
|
+
return (0, import_react.createElement)("div", { className: "dsh-rt-fork-hist-row" }, [
|
|
1268
|
+
(0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, KIND_ICONS[boundary.kind] ?? "\u2022"),
|
|
1269
|
+
(0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
|
|
1270
|
+
(0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
|
|
1271
|
+
(0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, kindLabel(boundary.kind, t)),
|
|
1272
|
+
(0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${boundary.seq}`),
|
|
1273
|
+
boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
|
|
1274
|
+
"span",
|
|
1275
|
+
{ key: "shadowed", className: "dsh-rt-fork-shadowed" },
|
|
1276
|
+
t("fork.shadowed", { count: boundary.replacedSeqs.length })
|
|
1277
|
+
)
|
|
1278
|
+
]),
|
|
1279
|
+
markerText && (0, import_react.createElement)(
|
|
1280
|
+
"div",
|
|
1281
|
+
{ key: "text", className: "dsh-rt-fork-text" },
|
|
1282
|
+
markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
|
|
1283
|
+
)
|
|
1284
|
+
]),
|
|
1285
|
+
(0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
|
|
1286
|
+
]);
|
|
1287
|
+
}
|
|
1053
1288
|
function apply(ctx) {
|
|
1054
1289
|
const disposeStyle = ensureStyle();
|
|
1055
1290
|
ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
|
|
@@ -1093,6 +1328,17 @@ return {
|
|
|
1093
1328
|
store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
|
|
1094
1329
|
})
|
|
1095
1330
|
}, RetraceView));
|
|
1331
|
+
ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
1332
|
+
name: "conversation.view",
|
|
1333
|
+
id: "retrace-fork",
|
|
1334
|
+
order: 30,
|
|
1335
|
+
locale: NS,
|
|
1336
|
+
label: () => t("view.fork"),
|
|
1337
|
+
inject: (sessionId, actions) => ({
|
|
1338
|
+
actions,
|
|
1339
|
+
store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
|
|
1340
|
+
})
|
|
1341
|
+
}, ForkView));
|
|
1096
1342
|
ctx.slots.inject("settings.general.item", () => ctx.slots.register({
|
|
1097
1343
|
name: "settings.general.item",
|
|
1098
1344
|
id: "retrace",
|
package/lib/forkmap.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-retrace — `retrace/forkmap` pure fold (PLAN §5.2, P2.1).
|
|
3
|
+
*
|
|
4
|
+
* The fork map is the BRANCH-TOPOLOGY presentation layer ("how we got here"):
|
|
5
|
+
* every surface replacement — recall / edit / regenerate / restore markers,
|
|
6
|
+
* compaction checkpoints, and any other replace — is a FORK POINT that shadows
|
|
7
|
+
* a range of the old path and continues a new path. This module mirrors the
|
|
8
|
+
* official `foldSurface` state transition (the same one the versions unit
|
|
9
|
+
* uses) and additionally records each boundary with the seqs it shadowed, so
|
|
10
|
+
* the client can render:
|
|
11
|
+
*
|
|
12
|
+
* spine — the current-surface node flow (markers sit ON the spine),
|
|
13
|
+
* branches — at each boundary, the shadowed (old-path) node cluster.
|
|
14
|
+
*
|
|
15
|
+
* Data notes (verified against the official source, 2026-08-26):
|
|
16
|
+
* - replace `start`/`end` are EVENT SEQS (not surface indices); the shadowed
|
|
17
|
+
* seqs are the surface nodes the splice removed — exactly
|
|
18
|
+
* `foldSurface(...).replacements[].shadowedSeqs`.
|
|
19
|
+
* - official `traceEvent` does NOT link a marker to its new path
|
|
20
|
+
* (new-path events never cite the marker, so `derivedEventSeqs` is empty)
|
|
21
|
+
* — the new path IS the spine: the surface nodes after the boundary
|
|
22
|
+
* (PLAN.md:295 "标记→新路径无需自解析" corrected by research).
|
|
23
|
+
* - boundaries ARE the versions (every replacement closes a version); the
|
|
24
|
+
* wire stays lean (no markerText — the client joins it from
|
|
25
|
+
* `retrace/versions` by boundarySeq).
|
|
26
|
+
*
|
|
27
|
+
* The module is import-free except for the shared boundary predicates, stays
|
|
28
|
+
* transport-neutral, and is registered alongside `retrace/versions` in the
|
|
29
|
+
* versioning seam. No side effects: pure fold only.
|
|
30
|
+
*/
|
|
31
|
+
import {
|
|
32
|
+
classifyBoundaryKind,
|
|
33
|
+
isReplacementSurfaceEvent,
|
|
34
|
+
isSurfaceEvent,
|
|
35
|
+
} from './version-index.js'
|
|
36
|
+
|
|
37
|
+
/** Fork-map fold state (plain JSON — durable checkpoint rows validate it). */
|
|
38
|
+
export function createForkmapState() {
|
|
39
|
+
return {
|
|
40
|
+
/** Current surface nodes: `{ seq, type }` in fold order (mirror foldSurface). */
|
|
41
|
+
nodes: [],
|
|
42
|
+
/** Replacement boundaries in append order. */
|
|
43
|
+
boundaries: [],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fold one committed event into the fork-map state.
|
|
49
|
+
*
|
|
50
|
+
* Performance contract (load-bearing for the projection registry): when the
|
|
51
|
+
* event changes nothing — no surface move, no boundary — the SAME state
|
|
52
|
+
* reference is returned (`Object.is` gates the change feed).
|
|
53
|
+
*
|
|
54
|
+
* @param {ReturnType<typeof createForkmapState>} state
|
|
55
|
+
* @param {object} event
|
|
56
|
+
* @returns the next state (same reference when nothing changed).
|
|
57
|
+
*/
|
|
58
|
+
export function applyForkmap(state, event) {
|
|
59
|
+
if (!event || typeof event.seq !== 'number') return state
|
|
60
|
+
let next = state
|
|
61
|
+
let shadowed = null
|
|
62
|
+
|
|
63
|
+
// 1. Surface fold (mirror foldSurface / the versions unit).
|
|
64
|
+
if (isSurfaceEvent(event)) {
|
|
65
|
+
const op = event.surfaceOp
|
|
66
|
+
if (op === 'append') {
|
|
67
|
+
next = { ...next, nodes: [...next.nodes, { seq: event.seq, type: event.type }] }
|
|
68
|
+
} else if (op && op.op === 'replace') {
|
|
69
|
+
const startIdx = next.nodes.findIndex((node) => node.seq === op.start)
|
|
70
|
+
const endIdx = next.nodes.findIndex((node) => node.seq === op.end)
|
|
71
|
+
if (startIdx !== -1 && endIdx !== -1 && startIdx <= endIdx) {
|
|
72
|
+
shadowed = next.nodes.slice(startIdx, endIdx + 1).map((node) => node.seq)
|
|
73
|
+
const nodes = next.nodes.slice()
|
|
74
|
+
nodes.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, type: event.type })
|
|
75
|
+
next = { ...next, nodes }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Boundary (fork point). The shadowed range is the splice's removal when
|
|
81
|
+
// the span was live; otherwise fall back to the marker's own citation.
|
|
82
|
+
if (isReplacementSurfaceEvent(event)) {
|
|
83
|
+
const cited = Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs : []
|
|
84
|
+
const replacedSeqs = shadowed !== null ? shadowed : cited
|
|
85
|
+
const boundary = {
|
|
86
|
+
seq: event.seq,
|
|
87
|
+
kind: classifyBoundaryKind(event),
|
|
88
|
+
replacedSeqs,
|
|
89
|
+
}
|
|
90
|
+
next = { ...next, boundaries: [...next.boundaries, boundary] }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return next
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The client-visible wire value (what `session/projection` frames serve). */
|
|
97
|
+
export function viewForkmap(state) {
|
|
98
|
+
return {
|
|
99
|
+
nodes: state.nodes.map((node) => ({ seq: node.seq, type: node.type })),
|
|
100
|
+
boundaries: state.boundaries.map((boundary) => ({
|
|
101
|
+
seq: boundary.seq,
|
|
102
|
+
kind: boundary.kind,
|
|
103
|
+
replacedSeqs: boundary.replacedSeqs,
|
|
104
|
+
})),
|
|
105
|
+
}
|
|
106
|
+
}
|
package/lib/http.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* GET /api/plugins/retrace/versions?sessionId=
|
|
9
9
|
* — live projection snapshot (HTTP fallback channel; the push channel is
|
|
10
10
|
* `session/projection` frames via dsh-host-apiproxy).
|
|
11
|
+
* GET /api/plugins/retrace/forkmap?sessionId=
|
|
12
|
+
* — the fork-map projection snapshot (branch topology, P2.1; same
|
|
13
|
+
* push-frame + HTTP fallback dual channel as /versions).
|
|
11
14
|
* GET /api/plugins/retrace/event?sessionId=&seq=&before=&after=
|
|
12
15
|
* — one event + context window (sessionQuery.readEvent, lazy reads).
|
|
13
16
|
* GET /api/plugins/retrace/surface?sessionId=
|
|
@@ -87,6 +90,15 @@ export function createRetraceHttpHandler(ctx, { sessions, agents, seam, rollback
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
function handleForkmap(req, res, sessionId, config) {
|
|
94
|
+
seam.setConfig(sessionId, config)
|
|
95
|
+
try {
|
|
96
|
+
sendJson(res, 200, { ok: true, value: seam.snapshotForkmap(sessionId) })
|
|
97
|
+
} catch (error) {
|
|
98
|
+
sendError(res, error)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
90
102
|
async function handleEvent(req, res, searchParams) {
|
|
91
103
|
const sessionId = searchParams.get('sessionId') ?? ''
|
|
92
104
|
const seq = Number(searchParams.get('seq'))
|
|
@@ -249,6 +261,8 @@ export function createRetraceHttpHandler(ctx, { sessions, agents, seam, rollback
|
|
|
249
261
|
switch (op) {
|
|
250
262
|
case 'versions':
|
|
251
263
|
return handleVersions(req, res, sessionId, config)
|
|
264
|
+
case 'forkmap':
|
|
265
|
+
return handleForkmap(req, res, sessionId, config)
|
|
252
266
|
case 'event':
|
|
253
267
|
return void handleEvent(req, res, searchParams)
|
|
254
268
|
case 'surface':
|