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.
@@ -120,7 +120,17 @@ var zh = {
120
120
  "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",
121
121
  "timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
122
122
  "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
123
- "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
123
+ "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C",
124
+ "view.fork": "\u5206\u53C9",
125
+ "fork.title": "\u5206\u53C9\u56FE",
126
+ "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",
127
+ "fork.refresh": "\u5237\u65B0",
128
+ "fork.spine": "\u5F53\u524D\u8DEF\u5F84",
129
+ "fork.shadowed": "\u88AB\u906E\u853D {count} \u4E2A\u8282\u70B9",
130
+ "fork.node.user": "\u7528\u6237\u6D88\u606F",
131
+ "fork.node.assistant": "\u52A9\u624B\u56DE\u590D",
132
+ "fork.node.tool": "\u5DE5\u5177\u7ED3\u679C",
133
+ "fork.histTitle": "\u5386\u53F2\u5206\u53C9\u70B9"
124
134
  };
125
135
  var en = {
126
136
  "action.edit": "Edit",
@@ -195,7 +205,17 @@ var en = {
195
205
  "timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
196
206
  "timeline.gitInitConfirm": "Initialize git in this workspace?",
197
207
  "error.generic": "Operation failed; please try again",
198
- "error.busy": "Stop the current reply before recalling or editing"
208
+ "error.busy": "Stop the current reply before recalling or editing",
209
+ "view.fork": "Fork",
210
+ "fork.title": "Fork map",
211
+ "fork.empty": "No forks yet. Recall / edit / regenerate / restore fork the path here.",
212
+ "fork.refresh": "Refresh",
213
+ "fork.spine": "Current path",
214
+ "fork.shadowed": "{count} shadowed nodes",
215
+ "fork.node.user": "User message",
216
+ "fork.node.assistant": "Assistant reply",
217
+ "fork.node.tool": "Tool result",
218
+ "fork.histTitle": "Historical fork points"
199
219
  };
200
220
  var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
201
221
  function isReplacementSurfaceEvent(event) {
@@ -405,29 +425,51 @@ function useShadowed(useSession, seq) {
405
425
  });
406
426
  }
407
427
  var SHADOW_SAFETY_RATIO = 0.4;
408
- function useHiddenKeys(useSession, shadowedSeqs) {
428
+ function hiddenKeysFor(shadowedSeqs, nodes) {
429
+ if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
430
+ const hidden = new Set(shadowedSeqs);
431
+ const keys = [];
432
+ for (const node of nodes.values()) {
433
+ if (node.kind === "recall-marker") continue;
434
+ if (node.kind === "turn-tail") {
435
+ const closingSeq = node.data?.closing?.finalNode?.seq;
436
+ if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
437
+ continue;
438
+ }
439
+ if (node.kind === "tool-call") {
440
+ const resultSeq = node.data?.root?.seq;
441
+ if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
442
+ continue;
443
+ }
444
+ if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
445
+ }
446
+ return keys.length === 0 ? null : keys;
447
+ }
448
+ var EMPTY_HIDE_PLAN = { degraded: false, hiddenFor: () => null };
449
+ function useMarkerHidePlan(useSession) {
409
450
  return useSession((snapshot) => {
410
- if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
411
- const hidden = new Set(shadowedSeqs);
412
- const keys = [];
451
+ const nodes = snapshot.chat.nodes;
413
452
  let rowCount = 0;
414
- for (const node of snapshot.chat.nodes.values()) {
453
+ const markers = [];
454
+ for (const node of nodes.values()) {
415
455
  if (typeof node.anchorSeq === "number") rowCount += 1;
416
- if (node.kind === "recall-marker") continue;
417
- if (node.kind === "turn-tail") {
418
- const closingSeq = node.data?.closing?.finalNode?.seq;
419
- if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
420
- continue;
421
- }
422
- if (node.kind === "tool-call") {
423
- const resultSeq = node.data?.root?.seq;
424
- if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
425
- continue;
456
+ if (node.kind === "recall-marker") {
457
+ markers.push({ key: node.key, shadowedSeqs: node.data?.shadowedSeqs });
426
458
  }
427
- if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
428
459
  }
429
- if (keys.length > 0 && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) return null;
430
- return keys.length === 0 ? null : keys;
460
+ if (markers.length === 0) return EMPTY_HIDE_PLAN;
461
+ const union = /* @__PURE__ */ new Set();
462
+ const perMarker = /* @__PURE__ */ new Map();
463
+ for (const marker of markers) {
464
+ const keys = hiddenKeysFor(marker.shadowedSeqs, nodes);
465
+ perMarker.set(marker.key, keys);
466
+ if (keys !== null) for (const key of keys) union.add(key);
467
+ }
468
+ const degraded = rowCount > 0 && union.size / rowCount > SHADOW_SAFETY_RATIO;
469
+ return {
470
+ degraded,
471
+ hiddenFor: (key) => degraded ? null : perMarker.get(key) ?? null
472
+ };
431
473
  });
432
474
  }
433
475
  function useMarkerDismissed(useSession, markerSeq, op) {
@@ -629,7 +671,8 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
629
671
  function RecallMarkerRow({ node, useSession, t }) {
630
672
  const { seq, op, shadowedSeqs, legacy } = node.data;
631
673
  const dismissed = useMarkerDismissed(useSession, seq, op);
632
- const hiddenKeys = legacy || !getConfig().hideShadowed ? null : useHiddenKeys(useSession, shadowedSeqs);
674
+ const hidePlan = useMarkerHidePlan(useSession);
675
+ const hiddenKeys = legacy || !getConfig().hideShadowed ? null : hidePlan.hiddenFor(node.key);
633
676
  const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
634
677
  const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
635
678
  const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
@@ -730,7 +773,8 @@ var CSS = `
730
773
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
731
774
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
732
775
  /* ---- P1 timeline (conversation view tab) ---- */
733
- .dsh-rt-view{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;height:100%;min-height:0;padding:12px 16px 0}
776
+
777
+ .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}
734
778
  .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
735
779
  .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
736
780
  .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}
@@ -767,7 +811,121 @@ var CSS = `
767
811
  .dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
768
812
  .dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
769
813
  .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}
814
+ .dsh-rt-fork-list{overflow-y:auto;flex:1;min-height:0;position:relative}
815
+ .dsh-rt-fork-spine-label{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;flex:none}
816
+ .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}
817
+ .dsh-rt-fork-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
818
+ .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)}
819
+ .dsh-rt-fork-boundary .dsh-rt-fork-icon{color:var(--dsw-alias-state-warning-primary)}
820
+ .dsh-rt-fork-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
821
+ .dsh-rt-fork-line{display:flex;align-items:baseline;gap:8px;min-width:0}
822
+ .dsh-rt-fork-label{font-size:12px;font-weight:500;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}
823
+ .dsh-rt-fork-seq{font-family:var(--dsw-font-mono);font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);flex:none}
824
+ .dsh-rt-fork-shadowed{font-size:11px;line-height:18px;color:var(--dsw-alias-state-warning-primary);flex:none}
825
+ .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}
826
+ .dsh-rt-fork-boundary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated)}
827
+ .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}
828
+ .dsh-rt-fork-hist-row{display:flex;align-items:flex-start;gap:8px;padding:4px 8px;border-radius:8px}
829
+ .dsh-rt-fork-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
770
830
  `;
831
+ var JUMP_PAGE_BUDGET = 24;
832
+ function switchToViewTab(viewId) {
833
+ const ORDER = { chat: 0, trajectory: 1, retrace: 2, "retrace-fork": 3 };
834
+ const index = ORDER[viewId];
835
+ if (index === void 0) {
836
+ console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
837
+ return;
838
+ }
839
+ const buttons = [...document.querySelectorAll('[role="tablist"] [role="tab"]')];
840
+ const button = buttons[index];
841
+ if (!button) {
842
+ console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
843
+ return;
844
+ }
845
+ button.click();
846
+ }
847
+ function waitForElement(selector, frames) {
848
+ return new Promise((resolve) => {
849
+ let remaining = frames;
850
+ const probe = () => {
851
+ const el = document.querySelector(selector);
852
+ if (el !== null) return resolve(el);
853
+ if (remaining-- <= 0) return resolve(null);
854
+ requestAnimationFrame(probe);
855
+ };
856
+ requestAnimationFrame(probe);
857
+ });
858
+ }
859
+ function flashKey(key) {
860
+ const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
861
+ if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
862
+ const tag = document.createElement("style");
863
+ tag.dataset.pluginCss = id;
864
+ 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)}}`;
865
+ document.head.appendChild(tag);
866
+ setTimeout(() => tag.remove(), 3400);
867
+ }
868
+ async function jumpToAnchor(store, anchorSeq) {
869
+ switchToViewTab("chat");
870
+ if (!store || typeof store.loadOlder !== "function" || typeof store.getSnapshot !== "function") return;
871
+ const keyOfSeq = (seq) => {
872
+ const nodes = store.getSnapshot()?.chat?.nodes;
873
+ if (!nodes) return null;
874
+ for (const node of nodes.values()) {
875
+ if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
876
+ }
877
+ return null;
878
+ };
879
+ let key = keyOfSeq(anchorSeq);
880
+ let pages = 0;
881
+ while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
882
+ const before = store.getSnapshot()?.chat?.nodes?.size ?? 0;
883
+ await store.loadOlder();
884
+ const after = store.getSnapshot()?.chat?.nodes?.size ?? 0;
885
+ pages += 1;
886
+ if (after === before) break;
887
+ key = keyOfSeq(anchorSeq);
888
+ }
889
+ if (key === null) {
890
+ await new Promise((resolve) => setTimeout(resolve, 60));
891
+ key = keyOfSeq(anchorSeq);
892
+ }
893
+ if (key === null) {
894
+ console.warn(`[dsh-retrace] jump target seq ${anchorSeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
895
+ return;
896
+ }
897
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
898
+ if (el === null) return;
899
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
900
+ flashKey(key);
901
+ }
902
+ var NODE_ICONS = { "user/message": "\u{1F464}", "assistant/message": "\u{1F916}", "tool/result": "\u{1F527}" };
903
+ function nodeTypeLabel(type, t) {
904
+ if (type === "user/message") return t("fork.node.user");
905
+ if (type === "assistant/message") return t("fork.node.assistant");
906
+ if (type === "tool/result") return t("fork.node.tool");
907
+ return type;
908
+ }
909
+ function bindListHeight(listEl) {
910
+ if (!listEl) return () => {
911
+ };
912
+ const measure = () => {
913
+ const top = listEl.getBoundingClientRect().top;
914
+ const height = Math.max(120, window.innerHeight - top - 16);
915
+ if (Math.abs(listEl.clientHeight - height) > 4) {
916
+ listEl.style.flex = "none";
917
+ listEl.style.height = `${height}px`;
918
+ }
919
+ };
920
+ measure();
921
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(measure) : null;
922
+ ro?.observe(document.body);
923
+ window.addEventListener("resize", measure);
924
+ return () => {
925
+ ro?.disconnect();
926
+ window.removeEventListener("resize", measure);
927
+ };
928
+ }
771
929
  function ensureStyle() {
772
930
  if (typeof document === "undefined") return () => {
773
931
  };
@@ -835,63 +993,7 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
835
993
  if (projected === void 0) refresh();
836
994
  refreshGit();
837
995
  }, []);
838
- const JUMP_PAGE_BUDGET = 24;
839
- const jump = async (boundarySeq) => {
840
- actions?.setView?.("chat");
841
- const target = store;
842
- if (!target || typeof target.loadOlder !== "function" || typeof target.getSnapshot !== "function") return;
843
- const keyOfSeq = (seq) => {
844
- const nodes = target.getSnapshot()?.chat?.nodes;
845
- if (!nodes) return null;
846
- for (const node of nodes.values()) {
847
- if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
848
- }
849
- return null;
850
- };
851
- let key = keyOfSeq(boundarySeq);
852
- let pages = 0;
853
- while (key === null && pages < JUMP_PAGE_BUDGET && target.hasMore) {
854
- const before = target.getSnapshot()?.chat?.nodes?.size ?? 0;
855
- await target.loadOlder();
856
- const after = target.getSnapshot()?.chat?.nodes?.size ?? 0;
857
- pages += 1;
858
- if (after === before) break;
859
- key = keyOfSeq(boundarySeq);
860
- }
861
- if (key === null) {
862
- await new Promise((resolve) => setTimeout(resolve, 60));
863
- key = keyOfSeq(boundarySeq);
864
- }
865
- if (key === null) {
866
- console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
867
- return;
868
- }
869
- const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
870
- if (el === null) return;
871
- el.scrollIntoView({ behavior: "smooth", block: "center" });
872
- flashKey(key);
873
- };
874
- function waitForElement(selector, frames) {
875
- return new Promise((resolve) => {
876
- let remaining = frames;
877
- const probe = () => {
878
- const el = document.querySelector(selector);
879
- if (el !== null) return resolve(el);
880
- if (remaining-- <= 0) return resolve(null);
881
- requestAnimationFrame(probe);
882
- };
883
- requestAnimationFrame(probe);
884
- });
885
- }
886
- function flashKey(key) {
887
- const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
888
- if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
889
- const tag = document.createElement("style");
890
- tag.dataset.pluginCss = id;
891
- 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)}}`;
892
- document.head.appendChild(tag);
893
- setTimeout(() => tag.remove(), 3400);
894
- }
996
+ const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq);
895
997
  const requestPreview = (record) => {
896
998
  setPreviewScope("both");
897
999
  setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
@@ -924,6 +1026,10 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
924
1026
  const ROW_H = 64;
925
1027
  const list = versions ?? [];
926
1028
  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));
1029
+ (0, import_react.useEffect)(() => {
1030
+ if (list.length === 0) return void 0;
1031
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-timeline-list"));
1032
+ }, [list.length]);
927
1033
  return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
928
1034
  (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
929
1035
  (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
@@ -961,7 +1067,7 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
961
1067
  t,
962
1068
  top: list.indexOf(record) * ROW_H,
963
1069
  onPreview: () => requestPreview(record),
964
- onTrajectory: () => actions?.setView?.("trajectory"),
1070
+ onTrajectory: () => switchToViewTab("trajectory"),
965
1071
  onJump: () => jump(record.boundarySeq)
966
1072
  }))
967
1073
  ])
@@ -1044,6 +1150,135 @@ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel })
1044
1150
  ])
1045
1151
  ]);
1046
1152
  }
1153
+ function ForkView({ sessionId, useProjection, t, actions, store }) {
1154
+ const [fork, setFork] = (0, import_react.useState)(null);
1155
+ const [loading, setLoading] = (0, import_react.useState)(false);
1156
+ const [error, setError] = (0, import_react.useState)(null);
1157
+ const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
1158
+ const projected = typeof useProjection === "function" ? useProjection("retrace/forkmap") : void 0;
1159
+ const versions = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
1160
+ const markerBySeq = new Map((versions?.versions ?? []).map((v) => [v.boundarySeq, v.markerText]));
1161
+ (0, import_react.useEffect)(() => {
1162
+ if (projected && Array.isArray(projected.nodes) && Array.isArray(projected.boundaries)) setFork(projected);
1163
+ }, [projected]);
1164
+ const refresh = () => {
1165
+ setLoading(true);
1166
+ setError(null);
1167
+ timelineGet(`/forkmap?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
1168
+ if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "forkmap failed");
1169
+ setFork(result.value?.nodes ? result.value : null);
1170
+ }).catch((cause) => setError(cause?.message ?? "forkmap error")).finally(() => setLoading(false));
1171
+ };
1172
+ (0, import_react.useEffect)(() => {
1173
+ if (projected === void 0) refresh();
1174
+ }, []);
1175
+ const nodes = fork?.nodes ?? [];
1176
+ const boundaries = fork?.boundaries ?? [];
1177
+ const boundaryBySeq = new Map(boundaries.map((b) => [b.seq, b]));
1178
+ const spineSeqSet = new Set(nodes.map((n) => n.seq));
1179
+ const offSpine = boundaries.filter((b) => !spineSeqSet.has(b.seq));
1180
+ (0, import_react.useEffect)(() => {
1181
+ if (nodes.length === 0) return void 0;
1182
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-fork-list"));
1183
+ }, [nodes.length]);
1184
+ const ROW_H = 56;
1185
+ const visible = nodes.slice(
1186
+ Math.max(0, Math.floor(scrollTop / ROW_H) - 2),
1187
+ Math.min(nodes.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2)
1188
+ );
1189
+ return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
1190
+ (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
1191
+ (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("fork.title")),
1192
+ (0, import_react.createElement)("button", {
1193
+ key: "refresh",
1194
+ type: "button",
1195
+ className: "dsh-rt-chip",
1196
+ onClick: refresh
1197
+ }, t("fork.refresh"))
1198
+ ]),
1199
+ error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
1200
+ loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1201
+ !loading && nodes.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("fork.empty")),
1202
+ nodes.length > 0 && (0, import_react.createElement)("div", { key: "spine", className: "dsh-rt-fork-spine-label" }, t("fork.spine")),
1203
+ nodes.length > 0 && (0, import_react.createElement)("div", {
1204
+ key: "list",
1205
+ className: "dsh-rt-fork-list",
1206
+ onScroll: (event) => setScrollTop(event.target.scrollTop)
1207
+ }, [
1208
+ (0, import_react.createElement)("div", { key: "spacer", style: { height: `${nodes.length * ROW_H}px`, position: "relative" } }, [
1209
+ visible.map((node) => (0, import_react.createElement)(ForkRow, {
1210
+ key: node.seq,
1211
+ node,
1212
+ boundary: boundaryBySeq.get(node.seq),
1213
+ markerText: markerBySeq.get(node.seq),
1214
+ t,
1215
+ top: nodes.indexOf(node) * ROW_H,
1216
+ onJump: () => jumpToAnchor(store, node.seq)
1217
+ }))
1218
+ ])
1219
+ ]),
1220
+ offSpine.length > 0 && (0, import_react.createElement)("div", { key: "hist", className: "dsh-rt-fork-hist" }, [
1221
+ (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-fork-spine-label" }, t("fork.histTitle")),
1222
+ offSpine.map((boundary) => (0, import_react.createElement)(ForkHistRow, {
1223
+ key: boundary.seq,
1224
+ boundary,
1225
+ markerText: markerBySeq.get(boundary.seq),
1226
+ t,
1227
+ onJump: () => jumpToAnchor(store, boundary.seq)
1228
+ }))
1229
+ ])
1230
+ ]);
1231
+ }
1232
+ function ForkRow({ node, boundary, markerText, t, top, onJump }) {
1233
+ const isBoundary = boundary !== void 0;
1234
+ const icon = isBoundary ? KIND_ICONS[boundary.kind] ?? "\u2022" : NODE_ICONS[node.type] ?? "\u2022";
1235
+ const label = isBoundary ? kindLabel(boundary.kind, t) : nodeTypeLabel(node.type, t);
1236
+ return (0, import_react.createElement)("div", {
1237
+ className: `dsh-rt-fork-row${isBoundary ? " dsh-rt-fork-boundary" : ""}`,
1238
+ style: { top: `${top}px` }
1239
+ }, [
1240
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, icon),
1241
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1242
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1243
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, label),
1244
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${node.seq}`),
1245
+ isBoundary && boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1246
+ "span",
1247
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1248
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1249
+ )
1250
+ ]),
1251
+ isBoundary && markerText && (0, import_react.createElement)(
1252
+ "div",
1253
+ { key: "text", className: "dsh-rt-fork-text" },
1254
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1255
+ )
1256
+ ]),
1257
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1258
+ ]);
1259
+ }
1260
+ function ForkHistRow({ boundary, markerText, t, onJump }) {
1261
+ return (0, import_react.createElement)("div", { className: "dsh-rt-fork-hist-row" }, [
1262
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, KIND_ICONS[boundary.kind] ?? "\u2022"),
1263
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1264
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1265
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, kindLabel(boundary.kind, t)),
1266
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${boundary.seq}`),
1267
+ boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1268
+ "span",
1269
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1270
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1271
+ )
1272
+ ]),
1273
+ markerText && (0, import_react.createElement)(
1274
+ "div",
1275
+ { key: "text", className: "dsh-rt-fork-text" },
1276
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1277
+ )
1278
+ ]),
1279
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1280
+ ]);
1281
+ }
1047
1282
  function apply(ctx) {
1048
1283
  const disposeStyle = ensureStyle();
1049
1284
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
@@ -1087,6 +1322,17 @@ function apply(ctx) {
1087
1322
  store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1088
1323
  })
1089
1324
  }, RetraceView));
1325
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1326
+ name: "conversation.view",
1327
+ id: "retrace-fork",
1328
+ order: 30,
1329
+ locale: NS,
1330
+ label: () => t("view.fork"),
1331
+ inject: (sessionId, actions) => ({
1332
+ actions,
1333
+ store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1334
+ })
1335
+ }, ForkView));
1090
1336
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1091
1337
  name: "settings.general.item",
1092
1338
  id: "retrace",