dsh-retrace 0.4.1 → 0.4.2

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.
@@ -83,6 +83,7 @@ return {
83
83
  "timeline.open": "\u65F6\u95F4\u7EBF",
84
84
  "timeline.openAria": "\u6253\u5F00\u4F1A\u8BDD\u65F6\u95F4\u7EBF",
85
85
  "timeline.title": "\u7248\u672C\u65F6\u95F4\u7EBF",
86
+ "view.retrace": "\u7248\u672C",
86
87
  "timeline.refresh": "\u5237\u65B0",
87
88
  "timeline.close": "\u5173\u95ED",
88
89
  "timeline.empty": "\u8FD8\u6CA1\u6709\u7248\u672C\u3002\u64A4\u56DE / \u7F16\u8F91 / \u91CD\u65B0\u751F\u6210\u4F1A\u5728\u6B64\u8BB0\u5F55\u7248\u672C\u3002",
@@ -115,6 +116,7 @@ return {
115
116
  "timeline.cancel": "\u53D6\u6D88",
116
117
  "timeline.busy": "\u56DE\u9000\u4E2D\u2026",
117
118
  "timeline.detail": "\u8BE6\u60C5",
119
+ "timeline.trajectory": "\u8F68\u8FF9\u53F0\u8D26",
118
120
  "timeline.jump": "\u8DF3\u8F6C",
119
121
  "timeline.jumpFailed": "\u8BE5\u7248\u672C\u5728\u8F83\u8FDC\u7684\u8FC7\u53BB\uFF08\u8D85\u51FA\u81EA\u52A8\u52A0\u8F7D\u9884\u7B97\uFF09\uFF0C\u65E0\u6CD5\u76F4\u63A5\u5B9A\u4F4D\u3002\u8BF7\u5411\u4E0A\u6EDA\u52A8\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F\u540E\u91CD\u8BD5\uFF1B\u6216\u7528\u300C\u8BE6\u60C5\u300D\u67E5\u770B\u8BE5\u7248\u672C\u5F53\u65F6\u7684\u4E8B\u4EF6\u539F\u6587\u3002",
120
122
  "timeline.gitRepo": "git \u4ED3\u5E93",
@@ -156,6 +158,7 @@ return {
156
158
  "timeline.open": "Timeline",
157
159
  "timeline.openAria": "Open the session timeline",
158
160
  "timeline.title": "Version timeline",
161
+ "view.retrace": "Versions",
159
162
  "timeline.refresh": "Refresh",
160
163
  "timeline.close": "Close",
161
164
  "timeline.empty": "No versions yet. Recall / edit / regenerate record a version here.",
@@ -188,6 +191,7 @@ return {
188
191
  "timeline.cancel": "Cancel",
189
192
  "timeline.busy": "Rolling back\u2026",
190
193
  "timeline.detail": "Details",
194
+ "timeline.trajectory": "Trajectory",
191
195
  "timeline.jump": "Jump",
192
196
  "timeline.jumpFailed": "This version lies too far back (beyond the auto-load budget) to locate directly. Scroll up to load earlier messages, or use Details to read the original event text of this version.",
193
197
  "timeline.gitRepo": "git repository",
@@ -731,17 +735,13 @@ return {
731
735
  .dsh-rt-retention-input{width:64px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:2px 6px;font:inherit;font-size:13px;outline:none;margin-top:1px}
732
736
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
733
737
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
734
- /* ---- P1 timeline ---- */
735
- .dsh-rt-timeline{position:relative}
736
- .dsh-rt-timeline-trigger{min-height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:transparent;border:none;border-radius:6px;display:inline-flex;align-items:center;gap:4px;padding:3px 6px;font-size:12px;line-height:18px}
737
- .dsh-rt-timeline-trigger:hover,.dsh-rt-timeline-trigger:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
738
- .dsh-rt-timeline-icon{font-size:13px}
739
- .dsh-rt-timeline-panel{position:fixed;top:calc(100% + 6px);right:8px;z-index:2147483000;width:min(520px,calc(100vw - 32px));max-height:min(70vh,640px);box-sizing:border-box;display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);border-radius:12px;padding:8px;overflow:hidden}
738
+ /* ---- P1 timeline (conversation view tab) ---- */
739
+ .dsh-rt-view{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;height:100%;min-height:0;padding:12px 16px 0}
740
740
  .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
741
741
  .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
742
742
  .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}
743
743
  .dsh-rt-timeline-git-text{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px}
744
- .dsh-rt-timeline-list{overflow-y:auto;flex:none;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
744
+ .dsh-rt-timeline-list{overflow-y:auto;flex:1;min-height:0;position:relative;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
745
745
  .dsh-rt-timeline-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:12px 4px;text-align:center}
746
746
  .dsh-rt-version{position:absolute;left:0;right:0;height:60px;box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;border:1px solid transparent;border-radius:10px;padding:6px 8px}
747
747
  .dsh-rt-version:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
@@ -811,8 +811,7 @@ return {
811
811
  deleted: counts.deleted ?? 0
812
812
  });
813
813
  }
814
- function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, ctx }) {
815
- const [open, setOpen] = (0, import_react.useState)(false);
814
+ function RetraceView({ sessionId, useProjection, t, actions, store }) {
816
815
  const [versions, setVersions] = (0, import_react.useState)(null);
817
816
  const [loading, setLoading] = (0, import_react.useState)(false);
818
817
  const [error, setError] = (0, import_react.useState)(null);
@@ -821,41 +820,11 @@ return {
821
820
  const [preview, setPreview] = (0, import_react.useState)(null);
822
821
  const [previewScope, setPreviewScope] = (0, import_react.useState)("both");
823
822
  const [rollbackBusy, setRollbackBusy] = (0, import_react.useState)(false);
824
- const [detail, setDetail] = (0, import_react.useState)(null);
825
- const [jumpTarget, setJumpTarget] = (0, import_react.useState)(null);
826
- const [jumpNotice, setJumpNotice] = (0, import_react.useState)(null);
827
823
  const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
828
- const [panelPos, setPanelPos] = (0, import_react.useState)(null);
829
- const rootRef = { current: null };
830
- const triggerRef = { current: null };
831
- const snapRef = { current: null };
832
824
  const projected = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
833
- useSession((snapshot) => {
834
- snapRef.current = snapshot;
835
- return snapshot.chat.nodes.size;
836
- });
837
825
  (0, import_react.useEffect)(() => {
838
826
  if (projected && Array.isArray(projected.versions)) setVersions(projected.versions);
839
827
  }, [projected]);
840
- const anchorPanel = () => {
841
- const rect = triggerRef.current?.getBoundingClientRect?.();
842
- if (!rect) return;
843
- setPanelPos({
844
- top: rect.bottom + 6,
845
- right: Math.max(8, window.innerWidth - rect.right)
846
- });
847
- };
848
- (0, import_react.useEffect)(() => {
849
- if (!open) return;
850
- anchorPanel();
851
- const onViewportChange = () => anchorPanel();
852
- window.addEventListener("resize", onViewportChange);
853
- window.addEventListener("scroll", onViewportChange, true);
854
- return () => {
855
- window.removeEventListener("resize", onViewportChange);
856
- window.removeEventListener("scroll", onViewportChange, true);
857
- };
858
- }, [open]);
859
828
  const refresh = () => {
860
829
  setLoading(true);
861
830
  setError(null);
@@ -868,77 +837,67 @@ return {
868
837
  if (getConfig().git !== true) return;
869
838
  timelineGet(`/git/status?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setGit(result?.ok === true ? result.value : null)).catch(() => setGit(null));
870
839
  };
871
- const toggle = () => {
872
- const next = !open;
873
- setOpen(next);
874
- if (next) {
875
- if (projected === void 0) refresh();
876
- refreshGit();
877
- }
878
- };
879
840
  (0, import_react.useEffect)(() => {
880
- if (!open) return;
881
- const onPointerDown = (event) => {
882
- if (rootRef.current && !rootRef.current.contains(event.target)) setOpen(false);
883
- };
884
- document.addEventListener("pointerdown", onPointerDown);
885
- return () => document.removeEventListener("pointerdown", onPointerDown);
886
- }, [open]);
841
+ if (projected === void 0) refresh();
842
+ refreshGit();
843
+ }, []);
887
844
  const JUMP_PAGE_BUDGET = 24;
888
845
  const jump = async (boundarySeq) => {
889
- setJumpNotice(null);
890
- setJumpTarget(boundarySeq);
891
- const binding = ctx?.get?.("sessions")?.binding?.(sessionId);
892
- const store = binding?.session;
893
- if (!store || typeof store.loadOlder !== "function") return;
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);
894
858
  let pages = 0;
895
- while (pages < JUMP_PAGE_BUDGET && store.hasMore) {
896
- const before = snapRef.current?.chat?.nodes?.size ?? 0;
897
- await store.loadOlder();
898
- const after = snapRef.current?.chat?.nodes?.size ?? 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;
899
863
  pages += 1;
900
864
  if (after === before) break;
865
+ key = keyOfSeq(boundarySeq);
901
866
  }
902
- await new Promise((resolve) => setTimeout(resolve, 60));
903
- if (!nodePresent(boundarySeq)) {
904
- setJumpNotice(t("timeline.jumpFailed"));
905
- setJumpTarget(null);
867
+ if (key === null) {
868
+ await new Promise((resolve) => setTimeout(resolve, 60));
869
+ key = keyOfSeq(boundarySeq);
906
870
  }
907
- };
908
- function nodePresent(seq) {
909
- const nodes = snapRef.current?.chat?.nodes;
910
- if (!nodes) return false;
911
- for (const node of nodes.values()) {
912
- if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return true;
913
- }
914
- return false;
915
- }
916
- const foundKey = useSession((snapshot) => {
917
- if (jumpTarget === null) return null;
918
- for (const node of snapshot.chat.nodes.values()) {
919
- if (typeof node.anchorSeq === "number" && node.anchorSeq === jumpTarget) return node.key;
920
- }
921
- return null;
922
- });
923
- (0, import_react.useEffect)(() => {
924
- if (foundKey === null || jumpTarget === null) return;
925
- const el = document.querySelector(`[data-chat-anchor-key=${JSON.stringify(foundKey)}]`);
926
- if (!el) {
927
- setJumpNotice(t("timeline.jumpFailed"));
928
- setJumpTarget(null);
871
+ if (key === null) {
872
+ console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
929
873
  return;
930
874
  }
875
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
876
+ if (el === null) return;
931
877
  el.scrollIntoView({ behavior: "smooth", block: "center" });
932
- const id = `dsh-rt-jump-${foundKey.replace(/[^a-z0-9]/gi, "-")}`;
933
- if (!document.querySelector(`style[data-plugin-css="${id}"]`)) {
934
- const tag = document.createElement("style");
935
- tag.dataset.pluginCss = id;
936
- tag.textContent = `[data-chat-anchor-key=${JSON.stringify(foundKey)}]{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)}}`;
937
- document.head.appendChild(tag);
938
- setTimeout(() => tag.remove(), 3400);
939
- }
940
- setJumpTarget(null);
941
- }, [foundKey, jumpTarget]);
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
+ }
942
901
  const requestPreview = (record) => {
943
902
  setPreviewScope("both");
944
903
  setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
@@ -960,12 +919,6 @@ return {
960
919
  refreshGit();
961
920
  });
962
921
  };
963
- const openDetail = (record) => {
964
- setDetail({ seq: record.boundarySeq, data: null, error: null });
965
- timelineGet(`/event?sessionId=${encodeURIComponent(sessionId)}&seq=${record.boundarySeq}&before=2&after=2`).then((result) => {
966
- setDetail((prev) => prev && prev.seq === record.boundarySeq ? { ...prev, data: result?.ok === true ? result.value : null, error: result?.ok === true ? null : result?.error?.message ?? null } : prev);
967
- });
968
- };
969
922
  const initGit = () => {
970
923
  if (!window.confirm(t("timeline.gitInitConfirm"))) return;
971
924
  setGitBusy(true);
@@ -976,89 +929,62 @@ return {
976
929
  };
977
930
  const ROW_H = 64;
978
931
  const list = versions ?? [];
979
- const visible = list.slice(Math.max(0, Math.floor(scrollTop / ROW_H) - 2), Math.min(list.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(420 / ROW_H) + 2));
980
- return (0, import_react.createElement)("div", { className: "dsh-rt-timeline", ref: rootRef }, [
981
- (0, import_react.createElement)("button", {
982
- key: "trigger",
983
- ref: triggerRef,
984
- type: "button",
985
- className: "dsh-rt-timeline-trigger",
986
- title: t("timeline.open"),
987
- "aria-label": t("timeline.openAria"),
988
- onClick: toggle
989
- }, [
990
- (0, import_react.createElement)("span", { key: "i", className: "dsh-rt-timeline-icon" }, "\u29C9"),
991
- (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-timeline-label" }, t("timeline.open"))
932
+ 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));
933
+ return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
934
+ (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
935
+ (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
936
+ (0, import_react.createElement)("button", {
937
+ key: "refresh",
938
+ type: "button",
939
+ className: "dsh-rt-chip",
940
+ onClick: refresh
941
+ }, t("timeline.refresh"))
992
942
  ]),
993
- open && (0, import_react.createElement)("div", {
994
- key: "panel",
995
- className: "dsh-rt-timeline-panel",
996
- style: panelPos ? { top: `${panelPos.top}px`, right: `${panelPos.right}px` } : void 0
943
+ git !== null && git !== void 0 && (0, import_react.createElement)("div", { key: "git", className: "dsh-rt-timeline-git" }, [
944
+ git.headHash ? [
945
+ (0, import_react.createElement)("span", { key: "r", className: "dsh-rt-timeline-git-text" }, `${t("timeline.gitRepo")} \xB7 ${t("timeline.gitHead", { hash: git.headHash.slice(0, 8) })}${git.dirty ? ` \xB7 ${t("timeline.gitDirty")}` : ""}`)
946
+ ] : (0, import_react.createElement)("button", {
947
+ key: "init",
948
+ type: "button",
949
+ className: "dsh-rt-chip",
950
+ disabled: gitBusy,
951
+ onClick: initGit,
952
+ title: t("timeline.gitInitDesc")
953
+ }, t("timeline.gitInit"))
954
+ ]),
955
+ error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
956
+ loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
957
+ !loading && list.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("timeline.empty")),
958
+ list.length > 0 && (0, import_react.createElement)("div", {
959
+ key: "list",
960
+ className: "dsh-rt-timeline-list",
961
+ onScroll: (event) => setScrollTop(event.target.scrollTop)
997
962
  }, [
998
- (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
999
- (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
1000
- (0, import_react.createElement)("button", {
1001
- key: "refresh",
1002
- type: "button",
1003
- className: "dsh-rt-chip",
1004
- onClick: refresh
1005
- }, t("timeline.refresh")),
1006
- (0, import_react.createElement)("button", {
1007
- key: "close",
1008
- type: "button",
1009
- className: "dsh-rt-chip",
1010
- onClick: () => setOpen(false)
1011
- }, t("timeline.close"))
1012
- ]),
1013
- git !== null && git !== void 0 && (0, import_react.createElement)("div", { key: "git", className: "dsh-rt-timeline-git" }, [
1014
- git.headHash ? [
1015
- (0, import_react.createElement)("span", { key: "r", className: "dsh-rt-timeline-git-text" }, `${t("timeline.gitRepo")} \xB7 ${t("timeline.gitHead", { hash: git.headHash.slice(0, 8) })}${git.dirty ? ` \xB7 ${t("timeline.gitDirty")}` : ""}`)
1016
- ] : (0, import_react.createElement)("button", {
1017
- key: "init",
1018
- type: "button",
1019
- className: "dsh-rt-chip",
1020
- disabled: gitBusy,
1021
- onClick: initGit,
1022
- title: t("timeline.gitInitDesc")
1023
- }, t("timeline.gitInit"))
1024
- ]),
1025
- error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
1026
- loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1027
- !loading && list.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("timeline.empty")),
1028
- list.length > 0 && (0, import_react.createElement)("div", {
1029
- key: "list",
1030
- className: "dsh-rt-timeline-list",
1031
- style: { height: `${Math.min(420, list.length * ROW_H)}px` },
1032
- onScroll: (event) => setScrollTop(event.target.scrollTop)
1033
- }, [
1034
- (0, import_react.createElement)("div", { key: "spacer", style: { height: `${list.length * ROW_H}px`, position: "relative" } }, [
1035
- visible.map((record) => (0, import_react.createElement)(VersionRow, {
1036
- key: record.versionId,
1037
- record,
1038
- t,
1039
- top: list.indexOf(record) * ROW_H,
1040
- onPreview: () => requestPreview(record),
1041
- onDetail: () => openDetail(record),
1042
- onJump: () => jump(record.boundarySeq)
1043
- }))
1044
- ])
1045
- ]),
1046
- preview && (0, import_react.createElement)(PreviewBox, {
1047
- key: "preview",
1048
- preview,
1049
- scope: previewScope,
1050
- setScope: setPreviewScope,
1051
- busy: rollbackBusy,
1052
- t,
1053
- onConfirm: confirmRollback,
1054
- onCancel: () => setPreview(null)
1055
- }),
1056
- detail && (0, import_react.createElement)(DetailBox, { key: "detail", detail, t, onClose: () => setDetail(null) }),
1057
- jumpNotice !== null && (0, import_react.createElement)("div", { key: "jump", className: "dsh-rt-error" }, jumpNotice)
1058
- ])
963
+ (0, import_react.createElement)("div", { key: "spacer", style: { height: `${list.length * ROW_H}px`, position: "relative" } }, [
964
+ visible.map((record) => (0, import_react.createElement)(VersionRow, {
965
+ key: record.versionId,
966
+ record,
967
+ t,
968
+ top: list.indexOf(record) * ROW_H,
969
+ onPreview: () => requestPreview(record),
970
+ onTrajectory: () => actions?.setView?.("trajectory"),
971
+ onJump: () => jump(record.boundarySeq)
972
+ }))
973
+ ])
974
+ ]),
975
+ preview && (0, import_react.createElement)(PreviewBox, {
976
+ key: "preview",
977
+ preview,
978
+ scope: previewScope,
979
+ setScope: setPreviewScope,
980
+ busy: rollbackBusy,
981
+ t,
982
+ onConfirm: confirmRollback,
983
+ onCancel: () => setPreview(null)
984
+ })
1059
985
  ]);
1060
986
  }
1061
- function VersionRow({ record, top, t, onPreview, onDetail, onJump }) {
987
+ function VersionRow({ record, top, t, onPreview, onTrajectory, onJump }) {
1062
988
  return (0, import_react.createElement)("div", { className: "dsh-rt-version", style: { top: `${top}px` } }, [
1063
989
  (0, import_react.createElement)("span", { key: "kind", className: `dsh-rt-version-kind dsh-rt-version-kind-${record.kind}`, title: kindLabel(record.kind, t) }, KIND_ICONS[record.kind] ?? "\u2022"),
1064
990
  (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-version-body" }, [
@@ -1071,8 +997,8 @@ return {
1071
997
  record.markerText ? (0, import_react.createElement)("div", { key: "text", className: "dsh-rt-version-text" }, record.markerText.length > 120 ? `${record.markerText.slice(0, 120)}\u2026` : record.markerText) : null
1072
998
  ]),
1073
999
  (0, import_react.createElement)("span", { key: "actions", className: "dsh-rt-version-actions" }, [
1074
- (0, import_react.createElement)("button", { key: "detail", type: "button", className: "dsh-rt-chip", onClick: onDetail }, t("timeline.detail")),
1075
1000
  (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump")),
1001
+ (0, import_react.createElement)("button", { key: "trajectory", type: "button", className: "dsh-rt-chip", onClick: onTrajectory, title: t("timeline.trajectory") }, t("timeline.trajectory")),
1076
1002
  (0, import_react.createElement)("button", { key: "preview", type: "button", className: "dsh-rt-chip dsh-rt-chip-danger", onClick: onPreview }, t("timeline.preview"))
1077
1003
  ])
1078
1004
  ]);
@@ -1124,21 +1050,11 @@ return {
1124
1050
  ])
1125
1051
  ]);
1126
1052
  }
1127
- function DetailBox({ detail, t, onClose }) {
1128
- return (0, import_react.createElement)("div", { className: "dsh-rt-modal" }, [
1129
- (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-modal-title" }, [
1130
- `${t("timeline.detail")} \xB7 seq ${detail.seq}`,
1131
- (0, import_react.createElement)("button", { key: "close", type: "button", className: "dsh-rt-chip", onClick: onClose }, t("timeline.close"))
1132
- ]),
1133
- detail.error && (0, import_react.createElement)("div", { key: "err", className: "dsh-rt-error" }, detail.error),
1134
- detail.data === null && !detail.error && (0, import_react.createElement)("div", { key: "wait", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1135
- detail.data && (0, import_react.createElement)("pre", { key: "json", className: "dsh-rt-modal-json" }, JSON.stringify(detail.data, null, 2))
1136
- ]);
1137
- }
1138
1053
  function apply(ctx) {
1139
1054
  const disposeStyle = ensureStyle();
1140
1055
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
1141
1056
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
1057
+ const t = ctx.locale.bind(NS);
1142
1058
  const conversationEvents = ctx.get("conversationEvents");
1143
1059
  if (conversationEvents) {
1144
1060
  conversationEvents.register(userActionsDefinition);
@@ -1166,12 +1082,17 @@ return {
1166
1082
  key: "recall-marker",
1167
1083
  locale: NS
1168
1084
  }, RecallMarkerRow));
1169
- ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1170
- name: "conversation.session.header.actions",
1171
- id: "retrace-timeline",
1172
- order: 30,
1173
- locale: NS
1174
- }, (props) => TimelinePanel({ ...props, ctx })));
1085
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1086
+ name: "conversation.view",
1087
+ id: "retrace",
1088
+ order: 20,
1089
+ locale: NS,
1090
+ label: () => t("view.retrace"),
1091
+ inject: (sessionId, actions) => ({
1092
+ actions,
1093
+ store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1094
+ })
1095
+ }, RetraceView));
1175
1096
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1176
1097
  name: "settings.general.item",
1177
1098
  id: "retrace",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-retrace",
3
3
  "description": "Retrace · 回溯 — Recall, edit-and-resend, regenerate, and conversation/artifact versioning (timeline, rollback, fork map) for DeepSeek Harness — Web and Desktop",
4
- "version": "0.4.1",
4
+ "version": "0.4.2",
5
5
  "packageManager": "pnpm@11.7.0",
6
6
  "type": "module",
7
7
  "main": "lib/index.js",