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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ in the conversation — all **inside the same session**, no session-switching.
51
51
 
52
52
  **Versioning & rollback (live in 0.4.x)** — every rewind is also recorded as a **version**:
53
53
 
54
- - 🕘 **Timeline** — the conversation header gains a **时间线 / Timeline** entry: a floating panel of every version (type, time, message count, file-change badges, summary), pushed live via `session/projection` (no polling), windowed for long histories.
54
+ - 🕘 **Timeline** — a **Versions** tab in the conversation view (on par with the official 对话/轨迹 tabs, since 0.4.2): every version (type, time, message count, file-change badges, summary), pushed live via `session/projection` (no polling), windowed for long histories; event inspection reuses the official Trajectory ledger.
55
55
  - ↩️ **Artifact rollback** — each version offers **context-only / artifacts-only / both** rollback with a dry-run preview; git-first (commit-free checkout of the listed paths) with content-addressed snapshot fallback. The rollback itself is recorded as a new version (`restore`) — rollback of a rollback.
56
56
  - 🧭 **Jump-to-conversation** — one click from a version to that point in the conversation (auto-loads earlier history, anchor highlight).
57
57
  - 🧹 **Bounded storage** — file snapshots keep the most recent N versions (default 50); a throttled background sweep prunes snapshots of truncated versions, keeping long sessions bounded.
package/README.zh.md CHANGED
@@ -43,7 +43,7 @@ DeepSeek Harness 的对话是「只追加(append-only)」的事件日志,
43
43
 
44
44
  **版本化与回退(0.4.x 已上线)** —— 每次回退都会被记录为一个**版本**:
45
45
 
46
- - 🕘 **时间线** —— 会话头部新增「时间线」入口:浮层面板展示每个版本(类型/时间/消息数/文件变更徽标/摘要),经 `session/projection` 推送帧实时更新(零轮询),大列表窗口化渲染。
46
+ - 🕘 **时间线** —— 会话视图新增「版本」Tab(与官方「对话/轨迹」平级,0.4.2 起):展示每个版本(类型/时间/消息数/文件变更徽标/摘要),经 `session/projection` 推送帧实时更新(零轮询),大列表窗口化渲染;事件原文查看复用官方「轨迹」台账。
47
47
  - ↩️ **产物回退** —— 每个版本支持 仅对话 / 仅产物 / 两者 三种回退范围,先干跑预览再执行;git 优先(commit-free checkout 清单路径)+ 内容寻址快照兜底。回退本身记录为新版本(`restore`),可以再回退。
48
48
  - 🧭 **跳转对话** —— 从时间线一键跳转到对话对应位置(自动翻页加载更早历史 + 锚点高亮)。
49
49
  - 🧹 **存储有界** —— 文件快照只保留最近 N 个版本(默认 50);节流后台扫掠回收被截断版本的快照,长会话不膨胀。
@@ -77,6 +77,7 @@ var zh = {
77
77
  "timeline.open": "\u65F6\u95F4\u7EBF",
78
78
  "timeline.openAria": "\u6253\u5F00\u4F1A\u8BDD\u65F6\u95F4\u7EBF",
79
79
  "timeline.title": "\u7248\u672C\u65F6\u95F4\u7EBF",
80
+ "view.retrace": "\u7248\u672C",
80
81
  "timeline.refresh": "\u5237\u65B0",
81
82
  "timeline.close": "\u5173\u95ED",
82
83
  "timeline.empty": "\u8FD8\u6CA1\u6709\u7248\u672C\u3002\u64A4\u56DE / \u7F16\u8F91 / \u91CD\u65B0\u751F\u6210\u4F1A\u5728\u6B64\u8BB0\u5F55\u7248\u672C\u3002",
@@ -109,6 +110,7 @@ var zh = {
109
110
  "timeline.cancel": "\u53D6\u6D88",
110
111
  "timeline.busy": "\u56DE\u9000\u4E2D\u2026",
111
112
  "timeline.detail": "\u8BE6\u60C5",
113
+ "timeline.trajectory": "\u8F68\u8FF9\u53F0\u8D26",
112
114
  "timeline.jump": "\u8DF3\u8F6C",
113
115
  "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",
114
116
  "timeline.gitRepo": "git \u4ED3\u5E93",
@@ -150,6 +152,7 @@ var en = {
150
152
  "timeline.open": "Timeline",
151
153
  "timeline.openAria": "Open the session timeline",
152
154
  "timeline.title": "Version timeline",
155
+ "view.retrace": "Versions",
153
156
  "timeline.refresh": "Refresh",
154
157
  "timeline.close": "Close",
155
158
  "timeline.empty": "No versions yet. Recall / edit / regenerate record a version here.",
@@ -182,6 +185,7 @@ var en = {
182
185
  "timeline.cancel": "Cancel",
183
186
  "timeline.busy": "Rolling back\u2026",
184
187
  "timeline.detail": "Details",
188
+ "timeline.trajectory": "Trajectory",
185
189
  "timeline.jump": "Jump",
186
190
  "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.",
187
191
  "timeline.gitRepo": "git repository",
@@ -725,17 +729,13 @@ var CSS = `
725
729
  .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}
726
730
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
727
731
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
728
- /* ---- P1 timeline ---- */
729
- .dsh-rt-timeline{position:relative}
730
- .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}
731
- .dsh-rt-timeline-trigger:hover,.dsh-rt-timeline-trigger:focus-visible{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}
732
- .dsh-rt-timeline-icon{font-size:13px}
733
- .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}
732
+ /* ---- 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}
734
734
  .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
735
735
  .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
736
736
  .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}
737
737
  .dsh-rt-timeline-git-text{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px}
738
- .dsh-rt-timeline-list{overflow-y:auto;flex:none;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
738
+ .dsh-rt-timeline-list{overflow-y:auto;flex:1;min-height:0;position:relative;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
739
739
  .dsh-rt-timeline-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:12px 4px;text-align:center}
740
740
  .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}
741
741
  .dsh-rt-version:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
@@ -805,8 +805,7 @@ function fileCountLabel(counts, t) {
805
805
  deleted: counts.deleted ?? 0
806
806
  });
807
807
  }
808
- function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, ctx }) {
809
- const [open, setOpen] = (0, import_react.useState)(false);
808
+ function RetraceView({ sessionId, useProjection, t, actions, store }) {
810
809
  const [versions, setVersions] = (0, import_react.useState)(null);
811
810
  const [loading, setLoading] = (0, import_react.useState)(false);
812
811
  const [error, setError] = (0, import_react.useState)(null);
@@ -815,41 +814,11 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
815
814
  const [preview, setPreview] = (0, import_react.useState)(null);
816
815
  const [previewScope, setPreviewScope] = (0, import_react.useState)("both");
817
816
  const [rollbackBusy, setRollbackBusy] = (0, import_react.useState)(false);
818
- const [detail, setDetail] = (0, import_react.useState)(null);
819
- const [jumpTarget, setJumpTarget] = (0, import_react.useState)(null);
820
- const [jumpNotice, setJumpNotice] = (0, import_react.useState)(null);
821
817
  const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
822
- const [panelPos, setPanelPos] = (0, import_react.useState)(null);
823
- const rootRef = { current: null };
824
- const triggerRef = { current: null };
825
- const snapRef = { current: null };
826
818
  const projected = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
827
- useSession((snapshot) => {
828
- snapRef.current = snapshot;
829
- return snapshot.chat.nodes.size;
830
- });
831
819
  (0, import_react.useEffect)(() => {
832
820
  if (projected && Array.isArray(projected.versions)) setVersions(projected.versions);
833
821
  }, [projected]);
834
- const anchorPanel = () => {
835
- const rect = triggerRef.current?.getBoundingClientRect?.();
836
- if (!rect) return;
837
- setPanelPos({
838
- top: rect.bottom + 6,
839
- right: Math.max(8, window.innerWidth - rect.right)
840
- });
841
- };
842
- (0, import_react.useEffect)(() => {
843
- if (!open) return;
844
- anchorPanel();
845
- const onViewportChange = () => anchorPanel();
846
- window.addEventListener("resize", onViewportChange);
847
- window.addEventListener("scroll", onViewportChange, true);
848
- return () => {
849
- window.removeEventListener("resize", onViewportChange);
850
- window.removeEventListener("scroll", onViewportChange, true);
851
- };
852
- }, [open]);
853
822
  const refresh = () => {
854
823
  setLoading(true);
855
824
  setError(null);
@@ -862,77 +831,67 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
862
831
  if (getConfig().git !== true) return;
863
832
  timelineGet(`/git/status?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setGit(result?.ok === true ? result.value : null)).catch(() => setGit(null));
864
833
  };
865
- const toggle = () => {
866
- const next = !open;
867
- setOpen(next);
868
- if (next) {
869
- if (projected === void 0) refresh();
870
- refreshGit();
871
- }
872
- };
873
834
  (0, import_react.useEffect)(() => {
874
- if (!open) return;
875
- const onPointerDown = (event) => {
876
- if (rootRef.current && !rootRef.current.contains(event.target)) setOpen(false);
877
- };
878
- document.addEventListener("pointerdown", onPointerDown);
879
- return () => document.removeEventListener("pointerdown", onPointerDown);
880
- }, [open]);
835
+ if (projected === void 0) refresh();
836
+ refreshGit();
837
+ }, []);
881
838
  const JUMP_PAGE_BUDGET = 24;
882
839
  const jump = async (boundarySeq) => {
883
- setJumpNotice(null);
884
- setJumpTarget(boundarySeq);
885
- const binding = ctx?.get?.("sessions")?.binding?.(sessionId);
886
- const store = binding?.session;
887
- if (!store || typeof store.loadOlder !== "function") return;
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);
888
852
  let pages = 0;
889
- while (pages < JUMP_PAGE_BUDGET && store.hasMore) {
890
- const before = snapRef.current?.chat?.nodes?.size ?? 0;
891
- await store.loadOlder();
892
- const after = snapRef.current?.chat?.nodes?.size ?? 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;
893
857
  pages += 1;
894
858
  if (after === before) break;
859
+ key = keyOfSeq(boundarySeq);
895
860
  }
896
- await new Promise((resolve) => setTimeout(resolve, 60));
897
- if (!nodePresent(boundarySeq)) {
898
- setJumpNotice(t("timeline.jumpFailed"));
899
- setJumpTarget(null);
861
+ if (key === null) {
862
+ await new Promise((resolve) => setTimeout(resolve, 60));
863
+ key = keyOfSeq(boundarySeq);
900
864
  }
901
- };
902
- function nodePresent(seq) {
903
- const nodes = snapRef.current?.chat?.nodes;
904
- if (!nodes) return false;
905
- for (const node of nodes.values()) {
906
- if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return true;
907
- }
908
- return false;
909
- }
910
- const foundKey = useSession((snapshot) => {
911
- if (jumpTarget === null) return null;
912
- for (const node of snapshot.chat.nodes.values()) {
913
- if (typeof node.anchorSeq === "number" && node.anchorSeq === jumpTarget) return node.key;
914
- }
915
- return null;
916
- });
917
- (0, import_react.useEffect)(() => {
918
- if (foundKey === null || jumpTarget === null) return;
919
- const el = document.querySelector(`[data-chat-anchor-key=${JSON.stringify(foundKey)}]`);
920
- if (!el) {
921
- setJumpNotice(t("timeline.jumpFailed"));
922
- setJumpTarget(null);
865
+ if (key === null) {
866
+ console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
923
867
  return;
924
868
  }
869
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
870
+ if (el === null) return;
925
871
  el.scrollIntoView({ behavior: "smooth", block: "center" });
926
- const id = `dsh-rt-jump-${foundKey.replace(/[^a-z0-9]/gi, "-")}`;
927
- if (!document.querySelector(`style[data-plugin-css="${id}"]`)) {
928
- const tag = document.createElement("style");
929
- tag.dataset.pluginCss = id;
930
- 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)}}`;
931
- document.head.appendChild(tag);
932
- setTimeout(() => tag.remove(), 3400);
933
- }
934
- setJumpTarget(null);
935
- }, [foundKey, jumpTarget]);
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
+ }
936
895
  const requestPreview = (record) => {
937
896
  setPreviewScope("both");
938
897
  setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
@@ -954,12 +913,6 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
954
913
  refreshGit();
955
914
  });
956
915
  };
957
- const openDetail = (record) => {
958
- setDetail({ seq: record.boundarySeq, data: null, error: null });
959
- timelineGet(`/event?sessionId=${encodeURIComponent(sessionId)}&seq=${record.boundarySeq}&before=2&after=2`).then((result) => {
960
- 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);
961
- });
962
- };
963
916
  const initGit = () => {
964
917
  if (!window.confirm(t("timeline.gitInitConfirm"))) return;
965
918
  setGitBusy(true);
@@ -970,89 +923,62 @@ function TimelinePanel({ sessionId, useSession, useProjection, useSessions, t, c
970
923
  };
971
924
  const ROW_H = 64;
972
925
  const list = versions ?? [];
973
- 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));
974
- return (0, import_react.createElement)("div", { className: "dsh-rt-timeline", ref: rootRef }, [
975
- (0, import_react.createElement)("button", {
976
- key: "trigger",
977
- ref: triggerRef,
978
- type: "button",
979
- className: "dsh-rt-timeline-trigger",
980
- title: t("timeline.open"),
981
- "aria-label": t("timeline.openAria"),
982
- onClick: toggle
983
- }, [
984
- (0, import_react.createElement)("span", { key: "i", className: "dsh-rt-timeline-icon" }, "\u29C9"),
985
- (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-timeline-label" }, t("timeline.open"))
926
+ 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));
927
+ return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
928
+ (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
929
+ (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
930
+ (0, import_react.createElement)("button", {
931
+ key: "refresh",
932
+ type: "button",
933
+ className: "dsh-rt-chip",
934
+ onClick: refresh
935
+ }, t("timeline.refresh"))
986
936
  ]),
987
- open && (0, import_react.createElement)("div", {
988
- key: "panel",
989
- className: "dsh-rt-timeline-panel",
990
- style: panelPos ? { top: `${panelPos.top}px`, right: `${panelPos.right}px` } : void 0
937
+ git !== null && git !== void 0 && (0, import_react.createElement)("div", { key: "git", className: "dsh-rt-timeline-git" }, [
938
+ git.headHash ? [
939
+ (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")}` : ""}`)
940
+ ] : (0, import_react.createElement)("button", {
941
+ key: "init",
942
+ type: "button",
943
+ className: "dsh-rt-chip",
944
+ disabled: gitBusy,
945
+ onClick: initGit,
946
+ title: t("timeline.gitInitDesc")
947
+ }, t("timeline.gitInit"))
948
+ ]),
949
+ error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
950
+ loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
951
+ !loading && list.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("timeline.empty")),
952
+ list.length > 0 && (0, import_react.createElement)("div", {
953
+ key: "list",
954
+ className: "dsh-rt-timeline-list",
955
+ onScroll: (event) => setScrollTop(event.target.scrollTop)
991
956
  }, [
992
- (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
993
- (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
994
- (0, import_react.createElement)("button", {
995
- key: "refresh",
996
- type: "button",
997
- className: "dsh-rt-chip",
998
- onClick: refresh
999
- }, t("timeline.refresh")),
1000
- (0, import_react.createElement)("button", {
1001
- key: "close",
1002
- type: "button",
1003
- className: "dsh-rt-chip",
1004
- onClick: () => setOpen(false)
1005
- }, t("timeline.close"))
1006
- ]),
1007
- git !== null && git !== void 0 && (0, import_react.createElement)("div", { key: "git", className: "dsh-rt-timeline-git" }, [
1008
- git.headHash ? [
1009
- (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")}` : ""}`)
1010
- ] : (0, import_react.createElement)("button", {
1011
- key: "init",
1012
- type: "button",
1013
- className: "dsh-rt-chip",
1014
- disabled: gitBusy,
1015
- onClick: initGit,
1016
- title: t("timeline.gitInitDesc")
1017
- }, t("timeline.gitInit"))
1018
- ]),
1019
- error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
1020
- loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1021
- !loading && list.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("timeline.empty")),
1022
- list.length > 0 && (0, import_react.createElement)("div", {
1023
- key: "list",
1024
- className: "dsh-rt-timeline-list",
1025
- style: { height: `${Math.min(420, list.length * ROW_H)}px` },
1026
- onScroll: (event) => setScrollTop(event.target.scrollTop)
1027
- }, [
1028
- (0, import_react.createElement)("div", { key: "spacer", style: { height: `${list.length * ROW_H}px`, position: "relative" } }, [
1029
- visible.map((record) => (0, import_react.createElement)(VersionRow, {
1030
- key: record.versionId,
1031
- record,
1032
- t,
1033
- top: list.indexOf(record) * ROW_H,
1034
- onPreview: () => requestPreview(record),
1035
- onDetail: () => openDetail(record),
1036
- onJump: () => jump(record.boundarySeq)
1037
- }))
1038
- ])
1039
- ]),
1040
- preview && (0, import_react.createElement)(PreviewBox, {
1041
- key: "preview",
1042
- preview,
1043
- scope: previewScope,
1044
- setScope: setPreviewScope,
1045
- busy: rollbackBusy,
1046
- t,
1047
- onConfirm: confirmRollback,
1048
- onCancel: () => setPreview(null)
1049
- }),
1050
- detail && (0, import_react.createElement)(DetailBox, { key: "detail", detail, t, onClose: () => setDetail(null) }),
1051
- jumpNotice !== null && (0, import_react.createElement)("div", { key: "jump", className: "dsh-rt-error" }, jumpNotice)
1052
- ])
957
+ (0, import_react.createElement)("div", { key: "spacer", style: { height: `${list.length * ROW_H}px`, position: "relative" } }, [
958
+ visible.map((record) => (0, import_react.createElement)(VersionRow, {
959
+ key: record.versionId,
960
+ record,
961
+ t,
962
+ top: list.indexOf(record) * ROW_H,
963
+ onPreview: () => requestPreview(record),
964
+ onTrajectory: () => actions?.setView?.("trajectory"),
965
+ onJump: () => jump(record.boundarySeq)
966
+ }))
967
+ ])
968
+ ]),
969
+ preview && (0, import_react.createElement)(PreviewBox, {
970
+ key: "preview",
971
+ preview,
972
+ scope: previewScope,
973
+ setScope: setPreviewScope,
974
+ busy: rollbackBusy,
975
+ t,
976
+ onConfirm: confirmRollback,
977
+ onCancel: () => setPreview(null)
978
+ })
1053
979
  ]);
1054
980
  }
1055
- function VersionRow({ record, top, t, onPreview, onDetail, onJump }) {
981
+ function VersionRow({ record, top, t, onPreview, onTrajectory, onJump }) {
1056
982
  return (0, import_react.createElement)("div", { className: "dsh-rt-version", style: { top: `${top}px` } }, [
1057
983
  (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"),
1058
984
  (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-version-body" }, [
@@ -1065,8 +991,8 @@ function VersionRow({ record, top, t, onPreview, onDetail, onJump }) {
1065
991
  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
1066
992
  ]),
1067
993
  (0, import_react.createElement)("span", { key: "actions", className: "dsh-rt-version-actions" }, [
1068
- (0, import_react.createElement)("button", { key: "detail", type: "button", className: "dsh-rt-chip", onClick: onDetail }, t("timeline.detail")),
1069
994
  (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump")),
995
+ (0, import_react.createElement)("button", { key: "trajectory", type: "button", className: "dsh-rt-chip", onClick: onTrajectory, title: t("timeline.trajectory") }, t("timeline.trajectory")),
1070
996
  (0, import_react.createElement)("button", { key: "preview", type: "button", className: "dsh-rt-chip dsh-rt-chip-danger", onClick: onPreview }, t("timeline.preview"))
1071
997
  ])
1072
998
  ]);
@@ -1118,21 +1044,11 @@ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel })
1118
1044
  ])
1119
1045
  ]);
1120
1046
  }
1121
- function DetailBox({ detail, t, onClose }) {
1122
- return (0, import_react.createElement)("div", { className: "dsh-rt-modal" }, [
1123
- (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-modal-title" }, [
1124
- `${t("timeline.detail")} \xB7 seq ${detail.seq}`,
1125
- (0, import_react.createElement)("button", { key: "close", type: "button", className: "dsh-rt-chip", onClick: onClose }, t("timeline.close"))
1126
- ]),
1127
- detail.error && (0, import_react.createElement)("div", { key: "err", className: "dsh-rt-error" }, detail.error),
1128
- detail.data === null && !detail.error && (0, import_react.createElement)("div", { key: "wait", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1129
- detail.data && (0, import_react.createElement)("pre", { key: "json", className: "dsh-rt-modal-json" }, JSON.stringify(detail.data, null, 2))
1130
- ]);
1131
- }
1132
1047
  function apply(ctx) {
1133
1048
  const disposeStyle = ensureStyle();
1134
1049
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
1135
1050
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
1051
+ const t = ctx.locale.bind(NS);
1136
1052
  const conversationEvents = ctx.get("conversationEvents");
1137
1053
  if (conversationEvents) {
1138
1054
  conversationEvents.register(userActionsDefinition);
@@ -1160,12 +1076,17 @@ function apply(ctx) {
1160
1076
  key: "recall-marker",
1161
1077
  locale: NS
1162
1078
  }, RecallMarkerRow));
1163
- ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1164
- name: "conversation.session.header.actions",
1165
- id: "retrace-timeline",
1166
- order: 30,
1167
- locale: NS
1168
- }, (props) => TimelinePanel({ ...props, ctx })));
1079
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1080
+ name: "conversation.view",
1081
+ id: "retrace",
1082
+ order: 20,
1083
+ locale: NS,
1084
+ label: () => t("view.retrace"),
1085
+ inject: (sessionId, actions) => ({
1086
+ actions,
1087
+ store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1088
+ })
1089
+ }, RetraceView));
1169
1090
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1170
1091
  name: "settings.general.item",
1171
1092
  id: "retrace",