dsh-retrace 0.3.0 → 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.
@@ -43,7 +43,16 @@ return {
43
43
  var NS = "retrace";
44
44
  var ROUTE_BASE = "/api/plugins/retrace";
45
45
  var MARKER_PREFIX = "retrace";
46
+ var MARKER_PREFIXES = [MARKER_PREFIX, "message-editor"];
47
+ var LEGACY_MARKER_PREFIXES = ["message-editor"];
48
+ function isMarkerId(id) {
49
+ return typeof id === "string" && MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`));
50
+ }
51
+ function isLegacyMarkerId(id) {
52
+ return typeof id === "string" && LEGACY_MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`));
53
+ }
46
54
  var CONFIG_KEY = "dsh-retrace:config";
55
+ var LEGACY_CONFIG_KEYS = ["dsh-message-editor:config"];
47
56
  var zh = {
48
57
  "action.edit": "\u7F16\u8F91",
49
58
  "action.editAria": "\u7F16\u8F91\u8FD9\u6761\u6D88\u606F",
@@ -62,13 +71,60 @@ return {
62
71
  "marker.referenceHint": "\u70B9\u51FB\u5C55\u5F00\u67E5\u770B\u539F\u63D0\u95EE\uFF08\u4EC5\u4F5C\u5BF9\u7167\uFF0C\u4E0D\u4F1A\u8FDB\u5165\u6A21\u578B\u4E0A\u4E0B\u6587\uFF09",
63
72
  "options.title": "\u6D88\u606F\u7F16\u8F91\u63D2\u4EF6",
64
73
  "options.showOriginalInput": "\u7F16\u8F91\u540E\u663E\u793A\u539F\u63D0\u95EE\u5BF9\u7167",
65
- "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF09",
74
+ "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF0C\u9ED8\u8BA4\u5173\uFF09",
75
+ "options.hideShadowed": "\u6309\u6807\u8BB0\u9690\u85CF\u88AB\u7F16\u8F91/\u64A4\u56DE\u7684\u6D88\u606F",
76
+ "options.hideShadowedDesc": "\u5F00\uFF08\u9ED8\u8BA4\uFF09\uFF1A\u64A4\u56DE/\u7F16\u8F91/\u91CD\u65B0\u751F\u6210\u6309\u6807\u8BB0\u9690\u85CF\u88AB\u66FF\u6362\u7684\u90A3\u4E00\u8F6E\u6D88\u606F\u3002\u5173\uFF1A\u6240\u6709\u6D88\u606F\u4FDD\u6301\u53EF\u89C1\uFF0C\u6807\u8BB0\u4EC5\u663E\u793A\u63D0\u793A\u4E0E\u5BF9\u7167\uFF08\u67E5\u770B\u5B8C\u6574\u5386\u53F2\u7528\uFF09\u3002\u4E00\u6B21\u64CD\u4F5C\u8981\u9690\u85CF\u8D85\u8FC7 40% \u7684\u5BF9\u8BDD\u65F6\u81EA\u52A8\u964D\u7EA7\u4E3A\u4E0D\u9690\u85CF\u3002",
66
77
  "options.versioning": "\u7248\u672C\u4E0E\u4EA7\u7269\u5FEB\u7167",
67
78
  "options.versioningDesc": "\u5F00\uFF1A\u6BCF\u6B21\u64A4\u56DE/\u7F16\u8F91\u8BB0\u5F55\u4E00\u4E2A\u7248\u672C\uFF08\u6D88\u606F\u4E0E\u89E6\u78B0\u6587\u4EF6\uFF09\uFF0C\u63D0\u4F9B\u65F6\u95F4\u7EBF\u4E0E\u4EA7\u7269\u56DE\u9000\uFF1B\u5173\uFF1A\u4EC5\u56DE\u9000\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u8BB0\u5F55\u7248\u672C\u3001\u4E0D\u8FFD\u8E2A\u4EA7\u7269\uFF08\u6700\u7701\u8D44\u6E90\uFF09\u3002",
68
79
  "options.git": "\u542F\u7528 git \u96C6\u6210",
69
80
  "options.gitDesc": "\u5F00\uFF1A\u5DE5\u4F5C\u533A\u662F git \u4ED3\u5E93\u65F6\u7528 git \u8BB0\u5F55\u4E0E\u56DE\u9000\uFF08\u4E0D\u81EA\u52A8\u63D0\u4EA4\u3001\u4E0D\u52A8\u4F60\u7684\u5206\u652F\uFF09\uFF0C\u975E\u4ED3\u5E93\u53EF\u5728\u65F6\u95F4\u7EBF\u91CC\u4E00\u952E\u542F\u7528\uFF1B\u5173\uFF1A\u4E00\u5F8B\u7528\u5185\u7F6E\u5FEB\u7167\uFF08\u5B58\u4E8E ~/.dsh\uFF09\uFF0C\u4E0D\u89E6\u78B0\u5DE5\u4F5C\u533A git \u72B6\u6001\uFF0C\u529F\u80FD\u7B49\u4EF7\u3002",
70
81
  "options.retention": "\u7248\u672C\u4FDD\u7559\u4E0A\u9650",
71
82
  "options.retentionDesc": "\u6587\u4EF6\u5FEB\u7167\u53EA\u4FDD\u7559\u6700\u8FD1 N \u4E2A\u7248\u672C\uFF0C\u8D85\u51FA\u81EA\u52A8\u6E05\u7406\u6700\u65E7\u7684\uFF1B\u65F6\u95F4\u7EBF\u8BB0\u5F55\u4E0E\u5BA1\u8BA1\u75D5\u8FF9\u59CB\u7EC8\u4FDD\u7559\u3002",
83
+ "timeline.open": "\u65F6\u95F4\u7EBF",
84
+ "timeline.openAria": "\u6253\u5F00\u4F1A\u8BDD\u65F6\u95F4\u7EBF",
85
+ "timeline.title": "\u7248\u672C\u65F6\u95F4\u7EBF",
86
+ "view.retrace": "\u7248\u672C",
87
+ "timeline.refresh": "\u5237\u65B0",
88
+ "timeline.close": "\u5173\u95ED",
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",
90
+ "timeline.loading": "\u52A0\u8F7D\u4E2D\u2026",
91
+ "timeline.error": "\u65F6\u95F4\u7EBF\u52A0\u8F7D\u5931\u8D25",
92
+ "timeline.kind.recall": "\u64A4\u56DE",
93
+ "timeline.kind.edit": "\u7F16\u8F91\u91CD\u53D1",
94
+ "timeline.kind.regenerate": "\u91CD\u65B0\u751F\u6210",
95
+ "timeline.kind.restore": "\u6062\u590D",
96
+ "timeline.kind.compaction": "\u538B\u7F29",
97
+ "timeline.kind.replace": "\u66FF\u6362",
98
+ "timeline.messages": "{count} \u6761\u6D88\u606F",
99
+ "timeline.files": "{created} \u589E / {modified} \u6539 / {deleted} \u5220",
100
+ "timeline.filesNone": "\u65E0\u6587\u4EF6\u53D8\u66F4",
101
+ "timeline.preview": "\u56DE\u9000\u9884\u89C8",
102
+ "timeline.previewDesc": "\u5C06\u56DE\u9000\u5230\u7248\u672C {version}\uFF08{kind}\uFF09\u3002",
103
+ "timeline.contextOnly": "\u4EC5\u5BF9\u8BDD",
104
+ "timeline.contextOnlyDesc": "\u79FB\u9664\u8BE5\u7248\u672C\u4E4B\u540E\u7684\u6D88\u606F\uFF08\u4FDD\u7559\u65E5\u5FD7\u5BA1\u8BA1\u75D5\u8FF9\uFF09",
105
+ "timeline.artifactsOnly": "\u4EC5\u4EA7\u7269",
106
+ "timeline.artifactsOnlyDesc": "\u628A\u8BE5\u7248\u672C\u89E6\u78B0\u7684\u6587\u4EF6\u6062\u590D\u5230\u5F53\u65F6\u7684\u5167\u5BB9",
107
+ "timeline.both": "\u4E24\u8005",
108
+ "timeline.bothDesc": "\u5148\u56DE\u9000\u5BF9\u8BDD\uFF0C\u518D\u56DE\u9000\u4EA7\u7269",
109
+ "timeline.messagesRemoved": "\u5C06\u79FB\u9664 {count} \u6761\u6D88\u606F",
110
+ "timeline.noChanges": "\u5F53\u524D\u5DF2\u5728\u8BE5\u7248\u672C\u72B6\u6001\uFF0C\u65E0\u53D8\u5316",
111
+ "timeline.artifactsList": "\u4EA7\u7269\u52A8\u4F5C\uFF08{count}\uFF09",
112
+ "timeline.artifact.restore": "\u6062\u590D",
113
+ "timeline.artifact.delete": "\u5220\u9664",
114
+ "timeline.artifact.skip": "\u8DF3\u8FC7",
115
+ "timeline.confirm": "\u786E\u8BA4\u56DE\u9000",
116
+ "timeline.cancel": "\u53D6\u6D88",
117
+ "timeline.busy": "\u56DE\u9000\u4E2D\u2026",
118
+ "timeline.detail": "\u8BE6\u60C5",
119
+ "timeline.trajectory": "\u8F68\u8FF9\u53F0\u8D26",
120
+ "timeline.jump": "\u8DF3\u8F6C",
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",
122
+ "timeline.gitRepo": "git \u4ED3\u5E93",
123
+ "timeline.gitHead": "HEAD {hash}",
124
+ "timeline.gitDirty": "\u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u6539\u52A8",
125
+ "timeline.gitInit": "\u542F\u7528 git \u7248\u672C\u7BA1\u7406",
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
+ "timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
72
128
  "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
73
129
  "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
74
130
  };
@@ -90,13 +146,60 @@ return {
90
146
  "marker.referenceHint": "Click to expand the original input (reference only, never sent to the model)",
91
147
  "options.title": "Message editor plugin",
92
148
  "options.showOriginalInput": "Show the original input after editing",
93
- "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages)",
149
+ "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
150
+ "options.hideShadowed": "Hide shadowed messages per marker",
151
+ "options.hideShadowedDesc": "On (default): recall/edit/regenerate hide the replaced round per their markers. Off: every message stays visible; markers only show the notice and reference (use to review full history). A single op that would hide more than 40% of the conversation degrades to notice-only automatically.",
94
152
  "options.versioning": "Version & artifact snapshots",
95
153
  "options.versioningDesc": "On: every recall/edit records a version (messages and touched files) powering the timeline and artifact rollback. Off: only rewinds context \u2014 no version records, no artifact tracking (lightest).",
96
154
  "options.git": "Git integration",
97
155
  "options.gitDesc": "On: uses git to record and roll back when the workspace is a repository (never auto-commits, never touches your branches); non-repo workspaces can enable git from the timeline. Off: built-in snapshots under ~/.dsh only \u2014 the plugin never touches the workspace git state; equivalent features.",
98
156
  "options.retention": "Version retention limit",
99
157
  "options.retentionDesc": "File snapshots are kept for the most recent N versions; older ones are pruned automatically (timeline records and the audit trail are always kept).",
158
+ "timeline.open": "Timeline",
159
+ "timeline.openAria": "Open the session timeline",
160
+ "timeline.title": "Version timeline",
161
+ "view.retrace": "Versions",
162
+ "timeline.refresh": "Refresh",
163
+ "timeline.close": "Close",
164
+ "timeline.empty": "No versions yet. Recall / edit / regenerate record a version here.",
165
+ "timeline.loading": "Loading\u2026",
166
+ "timeline.error": "Failed to load the timeline",
167
+ "timeline.kind.recall": "Recall",
168
+ "timeline.kind.edit": "Edit & resend",
169
+ "timeline.kind.regenerate": "Regenerate",
170
+ "timeline.kind.restore": "Restore",
171
+ "timeline.kind.compaction": "Compaction",
172
+ "timeline.kind.replace": "Replace",
173
+ "timeline.messages": "{count} messages",
174
+ "timeline.files": "{created} created / {modified} modified / {deleted} deleted",
175
+ "timeline.filesNone": "No file changes",
176
+ "timeline.preview": "Rollback preview",
177
+ "timeline.previewDesc": "Will roll back to version {version} ({kind}).",
178
+ "timeline.contextOnly": "Context only",
179
+ "timeline.contextOnlyDesc": "Remove messages after this version (the log audit trail stays)",
180
+ "timeline.artifactsOnly": "Artifacts only",
181
+ "timeline.artifactsOnlyDesc": "Restore the files this version touched to their state at that version",
182
+ "timeline.both": "Both",
183
+ "timeline.bothDesc": "Roll back the context first, then the artifacts",
184
+ "timeline.messagesRemoved": "{count} messages will be removed",
185
+ "timeline.noChanges": "Already at this version; nothing to change",
186
+ "timeline.artifactsList": "Artifact actions ({count})",
187
+ "timeline.artifact.restore": "restore",
188
+ "timeline.artifact.delete": "delete",
189
+ "timeline.artifact.skip": "skip",
190
+ "timeline.confirm": "Confirm rollback",
191
+ "timeline.cancel": "Cancel",
192
+ "timeline.busy": "Rolling back\u2026",
193
+ "timeline.detail": "Details",
194
+ "timeline.trajectory": "Trajectory",
195
+ "timeline.jump": "Jump",
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.",
197
+ "timeline.gitRepo": "git repository",
198
+ "timeline.gitHead": "HEAD {hash}",
199
+ "timeline.gitDirty": "working tree has uncommitted changes",
200
+ "timeline.gitInit": "Enable git versioning",
201
+ "timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
202
+ "timeline.gitInitConfirm": "Initialize git in this workspace?",
100
203
  "error.generic": "Operation failed; please try again",
101
204
  "error.busy": "Stop the current reply before recalling or editing"
102
205
  };
@@ -123,14 +226,49 @@ return {
123
226
  const { versioning, git, retentionLimit } = getConfig();
124
227
  return { "x-retrace-config": JSON.stringify({ versioning, git, retentionLimit }) };
125
228
  }
126
- var CONFIG_DEFAULTS = { showOriginalInput: true, editFromScratch: true, versioning: true, git: true, retentionLimit: 50 };
229
+ var CONFIG_VERSION = 3;
230
+ var CONFIG_DEFAULTS = { version: CONFIG_VERSION, showOriginalInput: true, editFromScratch: false, hideShadowed: true, versioning: true, git: true, retentionLimit: 50, prewrite: true };
127
231
  var configListeners = /* @__PURE__ */ new Set();
128
232
  var configCache = readConfig();
129
233
  var editReferences = /* @__PURE__ */ new Map();
234
+ function migrateConfig(parsed) {
235
+ const version = typeof parsed.version === "number" ? parsed.version : 1;
236
+ if (version >= CONFIG_VERSION) return { ...CONFIG_DEFAULTS, ...parsed };
237
+ return {
238
+ ...CONFIG_DEFAULTS,
239
+ ...parsed,
240
+ version: CONFIG_VERSION,
241
+ editFromScratch: CONFIG_DEFAULTS.editFromScratch,
242
+ hideShadowed: CONFIG_DEFAULTS.hideShadowed
243
+ };
244
+ }
130
245
  function readConfig() {
131
246
  try {
132
247
  const raw = localStorage.getItem(CONFIG_KEY);
133
- return { ...CONFIG_DEFAULTS, ...raw ? JSON.parse(raw) : {} };
248
+ if (raw !== null) {
249
+ const parsed = raw ? JSON.parse(raw) : {};
250
+ const migrated = migrateConfig(parsed);
251
+ const storedVersion = typeof parsed.version === "number" ? parsed.version : 1;
252
+ if (storedVersion < CONFIG_VERSION) {
253
+ try {
254
+ localStorage.setItem(CONFIG_KEY, JSON.stringify(migrated));
255
+ } catch {
256
+ }
257
+ }
258
+ return migrated;
259
+ }
260
+ for (const legacyKey of LEGACY_CONFIG_KEYS) {
261
+ const legacyRaw = localStorage.getItem(legacyKey);
262
+ if (legacyRaw !== null) {
263
+ const migrated = migrateConfig(legacyRaw ? JSON.parse(legacyRaw) : {});
264
+ try {
265
+ localStorage.setItem(CONFIG_KEY, JSON.stringify(migrated));
266
+ } catch {
267
+ }
268
+ return migrated;
269
+ }
270
+ }
271
+ return { ...CONFIG_DEFAULTS };
134
272
  } catch {
135
273
  return { ...CONFIG_DEFAULTS };
136
274
  }
@@ -208,9 +346,11 @@ return {
208
346
  }
209
347
  };
210
348
  function markerOpFromId(id) {
211
- if (id.startsWith(`${MARKER_PREFIX}-recall-`)) return "recall";
212
- if (id.startsWith(`${MARKER_PREFIX}-edit-`)) return "edit";
213
- if (id.startsWith(`${MARKER_PREFIX}-regenerate-`)) return "regenerate";
349
+ for (const p of MARKER_PREFIXES) {
350
+ if (id.startsWith(`${p}-recall-`)) return "recall";
351
+ if (id.startsWith(`${p}-edit-`)) return "edit";
352
+ if (id.startsWith(`${p}-regenerate-`)) return "regenerate";
353
+ }
214
354
  return "edit";
215
355
  }
216
356
  var recallMarkerDefinition = {
@@ -219,16 +359,22 @@ return {
219
359
  match: (event) => {
220
360
  if (event.type !== "assistant/message" || !isReplacementSurfaceEvent(event)) return null;
221
361
  const id = event.data?.message?.id;
222
- if (typeof id !== "string" || !id.startsWith(`${MARKER_PREFIX}-`)) return null;
362
+ if (!isMarkerId(id)) return null;
223
363
  return { id: `marker:${id}`, role: "start" };
224
364
  },
225
365
  start: (_context, match) => {
226
366
  const event = match.event;
367
+ const id = String(event.data.message.id);
368
+ const legacy = isLegacyMarkerId(id);
227
369
  return {
228
370
  seq: event.seq,
229
371
  time: event.time,
230
- op: markerOpFromId(String(event.data.message.id)),
231
- shadowedSeqs: Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
372
+ op: markerOpFromId(id),
373
+ legacy,
374
+ // Legacy markers never hide: treat their shadowed range as empty so the
375
+ // notice/reference render but no row is hidden and no action row is
376
+ // suppressed via useShadowed.
377
+ shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
232
378
  targetSeq: event.data?.editor?.targetSeq,
233
379
  text: event.data?.editor?.text
234
380
  };
@@ -264,12 +410,15 @@ return {
264
410
  return false;
265
411
  });
266
412
  }
413
+ var SHADOW_SAFETY_RATIO = 0.4;
267
414
  function useHiddenKeys(useSession, shadowedSeqs) {
268
415
  return useSession((snapshot) => {
269
416
  if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
270
417
  const hidden = new Set(shadowedSeqs);
271
418
  const keys = [];
419
+ let rowCount = 0;
272
420
  for (const node of snapshot.chat.nodes.values()) {
421
+ if (typeof node.anchorSeq === "number") rowCount += 1;
273
422
  if (node.kind === "recall-marker") continue;
274
423
  if (node.kind === "turn-tail") {
275
424
  const closingSeq = node.data?.closing?.finalNode?.seq;
@@ -283,6 +432,7 @@ return {
283
432
  }
284
433
  if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
285
434
  }
435
+ if (keys.length > 0 && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) return null;
286
436
  return keys.length === 0 ? null : keys;
287
437
  });
288
438
  }
@@ -483,9 +633,9 @@ return {
483
633
  ]);
484
634
  }
485
635
  function RecallMarkerRow({ node, useSession, t }) {
486
- const { seq, op, shadowedSeqs } = node.data;
636
+ const { seq, op, shadowedSeqs, legacy } = node.data;
487
637
  const dismissed = useMarkerDismissed(useSession, seq, op);
488
- const hiddenKeys = useHiddenKeys(useSession, shadowedSeqs);
638
+ const hiddenKeys = legacy || !getConfig().hideShadowed ? null : useHiddenKeys(useSession, shadowedSeqs);
489
639
  const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
490
640
  const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
491
641
  const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
@@ -522,6 +672,7 @@ return {
522
672
  }),
523
673
  (0, import_react.createElement)("span", null, t("options.editFromScratch"))
524
674
  ]),
675
+ optionRow("hideShadowed", "options.hideShadowed", "options.hideShadowedDesc"),
525
676
  optionRow("versioning", "options.versioning", "options.versioningDesc"),
526
677
  optionRow("git", "options.git", "options.gitDesc"),
527
678
  (0, import_react.createElement)("div", { key: "retention", className: "dsh-rt-option dsh-rt-option-number" }, [
@@ -584,6 +735,44 @@ return {
584
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}
585
736
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
586
737
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
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
+ .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
741
+ .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
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
+ .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:1;min-height:0;position:relative;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
745
+ .dsh-rt-timeline-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:12px 4px;text-align:center}
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
+ .dsh-rt-version:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
748
+ .dsh-rt-version-kind{flex:none;width:22px;height:22px;display:inline-flex;justify-content:center;align-items:center;border-radius:6px;background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:1px}
749
+ .dsh-rt-version-kind-restore{background:var(--dsw-alias-state-success-bg);color:var(--dsw-alias-state-success-primary)}
750
+ .dsh-rt-version-kind-compaction{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-caption)}
751
+ .dsh-rt-version-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
752
+ .dsh-rt-version-line{display:flex;align-items:center;gap:8px;min-width:0;white-space:nowrap}
753
+ .dsh-rt-version-kind-label{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:16px}
754
+ .dsh-rt-version-time,.dsh-rt-version-msgs{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
755
+ .dsh-rt-version-files{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis}
756
+ .dsh-rt-version-text{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
757
+ .dsh-rt-version-actions{flex:none;display:inline-flex;gap:4px;opacity:0;transition:opacity .1s}
758
+ .dsh-rt-version:hover .dsh-rt-version-actions{opacity:1}
759
+ .dsh-rt-chip-danger{color:var(--dsw-alias-state-error-primary)}
760
+ .dsh-rt-chip-danger:hover{color:var(--dsw-alias-state-error-primary)}
761
+ .dsh-rt-modal{position:absolute;inset:0;z-index:130;display:flex;flex-direction:column;gap:8px;box-sizing:border-box;border-radius:12px;background:var(--dsw-specific-menu);padding:10px;box-shadow:var(--dsw-shadow-lv3)}
762
+ .dsh-rt-modal-title{display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px}
763
+ .dsh-rt-modal-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}
764
+ .dsh-rt-modal-body{display:flex;flex-direction:column;gap:6px;overflow-y:auto;min-height:0}
765
+ .dsh-rt-modal-line{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
766
+ .dsh-rt-modal-files{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;max-height:160px;overflow-y:auto}
767
+ .dsh-rt-modal-files li{display:flex;gap:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
768
+ .dsh-rt-art-restore{color:var(--dsw-alias-state-success-primary);flex:none}
769
+ .dsh-rt-art-delete{color:var(--dsw-alias-state-error-primary);flex:none}
770
+ .dsh-rt-art-skip{color:var(--dsw-alias-label-caption);flex:none}
771
+ .dsh-rt-art-path{font-family:var(--dsw-font-mono);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
772
+ .dsh-rt-modal-scope{display:flex;flex-direction:column;gap:4px}
773
+ .dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
774
+ .dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
775
+ .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}
587
776
  `;
588
777
  function ensureStyle() {
589
778
  if (typeof document === "undefined") return () => {
@@ -599,10 +788,273 @@ return {
599
788
  tag.remove();
600
789
  };
601
790
  }
791
+ var KIND_ICONS = { recall: "\u21A9", edit: "\u270E", regenerate: "\u21BB", restore: "\u27F2", compaction: "\u25A4", replace: "\u21C4" };
792
+ function kindLabel(kind, t) {
793
+ return t(`timeline.kind.${kind}`) || kind;
794
+ }
795
+ function timeLabel(ms) {
796
+ const date = new Date(ms);
797
+ return `${date.getMonth() + 1}-${date.getDate()} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
798
+ }
799
+ function timelineGet(path) {
800
+ return fetch(`${ROUTE_BASE}${path}`, { headers: retraceConfigHeaders() }).then((res) => {
801
+ if (res.status < 200 || res.status >= 300) throw new Error(`HTTP ${res.status}`);
802
+ return res.json();
803
+ });
804
+ }
805
+ function fileCountLabel(counts, t) {
806
+ const total = (counts?.created ?? 0) + (counts?.modified ?? 0) + (counts?.deleted ?? 0);
807
+ if (total === 0) return t("timeline.filesNone");
808
+ return t("timeline.files", {
809
+ created: counts.created ?? 0,
810
+ modified: counts.modified ?? 0,
811
+ deleted: counts.deleted ?? 0
812
+ });
813
+ }
814
+ function RetraceView({ sessionId, useProjection, t, actions, store }) {
815
+ const [versions, setVersions] = (0, import_react.useState)(null);
816
+ const [loading, setLoading] = (0, import_react.useState)(false);
817
+ const [error, setError] = (0, import_react.useState)(null);
818
+ const [git, setGit] = (0, import_react.useState)(null);
819
+ const [gitBusy, setGitBusy] = (0, import_react.useState)(false);
820
+ const [preview, setPreview] = (0, import_react.useState)(null);
821
+ const [previewScope, setPreviewScope] = (0, import_react.useState)("both");
822
+ const [rollbackBusy, setRollbackBusy] = (0, import_react.useState)(false);
823
+ const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
824
+ const projected = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
825
+ (0, import_react.useEffect)(() => {
826
+ if (projected && Array.isArray(projected.versions)) setVersions(projected.versions);
827
+ }, [projected]);
828
+ const refresh = () => {
829
+ setLoading(true);
830
+ setError(null);
831
+ timelineGet(`/versions?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
832
+ if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "versions failed");
833
+ setVersions(result.value?.versions ?? []);
834
+ }).catch((cause) => setError(cause?.message ?? "timeline error")).finally(() => setLoading(false));
835
+ };
836
+ const refreshGit = () => {
837
+ if (getConfig().git !== true) return;
838
+ timelineGet(`/git/status?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setGit(result?.ok === true ? result.value : null)).catch(() => setGit(null));
839
+ };
840
+ (0, import_react.useEffect)(() => {
841
+ if (projected === void 0) refresh();
842
+ refreshGit();
843
+ }, []);
844
+ const JUMP_PAGE_BUDGET = 24;
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
+ }
901
+ const requestPreview = (record) => {
902
+ setPreviewScope("both");
903
+ setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
904
+ callOp("rollback/preview", { sessionId, versionId: record.versionId, scope: "both" }).then((result) => {
905
+ setPreview((prev) => prev && prev.versionId === record.versionId ? { ...prev, data: result?.ok === true ? result.value : null, error: result?.ok === true ? null : result?.error?.message ?? null } : prev);
906
+ });
907
+ };
908
+ const confirmRollback = () => {
909
+ if (!preview) return;
910
+ setRollbackBusy(true);
911
+ callOp("rollback", { sessionId, versionId: preview.versionId, scope: previewScope }).then((result) => {
912
+ setRollbackBusy(false);
913
+ if (!result || result.ok !== true) {
914
+ setPreview((prev) => prev && { ...prev, error: result?.error?.message ?? "rollback failed" });
915
+ return;
916
+ }
917
+ setPreview(null);
918
+ if (projected === void 0) refresh();
919
+ refreshGit();
920
+ });
921
+ };
922
+ const initGit = () => {
923
+ if (!window.confirm(t("timeline.gitInitConfirm"))) return;
924
+ setGitBusy(true);
925
+ callOp("git/init", { sessionId }).then((result) => {
926
+ setGitBusy(false);
927
+ refreshGit();
928
+ });
929
+ };
930
+ const ROW_H = 64;
931
+ const list = versions ?? [];
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"))
942
+ ]),
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)
962
+ }, [
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
+ })
985
+ ]);
986
+ }
987
+ function VersionRow({ record, top, t, onPreview, onTrajectory, onJump }) {
988
+ return (0, import_react.createElement)("div", { className: "dsh-rt-version", style: { top: `${top}px` } }, [
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"),
990
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-version-body" }, [
991
+ (0, import_react.createElement)("div", { key: "line1", className: "dsh-rt-version-line" }, [
992
+ (0, import_react.createElement)("span", { key: "kind", className: "dsh-rt-version-kind-label" }, kindLabel(record.kind, t)),
993
+ (0, import_react.createElement)("span", { key: "time", className: "dsh-rt-version-time" }, timeLabel(record.createdAt)),
994
+ (0, import_react.createElement)("span", { key: "msgs", className: "dsh-rt-version-msgs" }, t("timeline.messages", { count: record.messageCount })),
995
+ (0, import_react.createElement)("span", { key: "files", className: "dsh-rt-version-files" }, fileCountLabel(record.fileCounts, t))
996
+ ]),
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
998
+ ]),
999
+ (0, import_react.createElement)("span", { key: "actions", className: "dsh-rt-version-actions" }, [
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")),
1002
+ (0, import_react.createElement)("button", { key: "preview", type: "button", className: "dsh-rt-chip dsh-rt-chip-danger", onClick: onPreview }, t("timeline.preview"))
1003
+ ])
1004
+ ]);
1005
+ }
1006
+ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel }) {
1007
+ const data = preview.data;
1008
+ const scopes = [
1009
+ ["context", "timeline.contextOnly", "timeline.contextOnlyDesc"],
1010
+ ["artifacts", "timeline.artifactsOnly", "timeline.artifactsOnlyDesc"],
1011
+ ["both", "timeline.both", "timeline.bothDesc"]
1012
+ ];
1013
+ return (0, import_react.createElement)("div", { className: "dsh-rt-modal" }, [
1014
+ (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-modal-title" }, [
1015
+ t("timeline.preview"),
1016
+ (0, import_react.createElement)("span", { key: "ver", className: "dsh-rt-modal-sub" }, t("timeline.previewDesc", { version: preview.versionId, kind: kindLabel(preview.kind, t) }))
1017
+ ]),
1018
+ preview.error && (0, import_react.createElement)("div", { key: "err", className: "dsh-rt-error" }, preview.error),
1019
+ data === null && !preview.error && (0, import_react.createElement)("div", { key: "wait", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1020
+ data && (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-modal-body" }, [
1021
+ (0, import_react.createElement)(
1022
+ "div",
1023
+ { key: "ctx", className: "dsh-rt-modal-line" },
1024
+ data.context?.messages > 0 ? t("timeline.messagesRemoved", { count: data.context.messages }) : t("timeline.noChanges")
1025
+ ),
1026
+ (0, import_react.createElement)("div", { key: "art", className: "dsh-rt-modal-line" }, t("timeline.artifactsList", { count: data.artifacts?.rows?.length ?? 0 })),
1027
+ (0, import_react.createElement)("ul", { key: "files", className: "dsh-rt-modal-files" }, (data.artifacts?.rows ?? []).slice(0, 12).map((row) => (0, import_react.createElement)("li", { key: row.path }, [
1028
+ (0, import_react.createElement)(
1029
+ "span",
1030
+ { key: "a", className: `dsh-rt-art-${row.action}` },
1031
+ row.action === "skip" ? `${t("timeline.artifact.skip")} (${row.reason ?? ""})` : row.action === "delete" ? t("timeline.artifact.delete") : t("timeline.artifact.restore")
1032
+ ),
1033
+ (0, import_react.createElement)("span", { key: "p", className: "dsh-rt-art-path" }, row.path)
1034
+ ]))),
1035
+ (0, import_react.createElement)("div", { key: "scope", className: "dsh-rt-modal-scope" }, scopes.map(([value, labelKey, descKey]) => (0, import_react.createElement)("label", { key: value, className: "dsh-rt-option" }, [
1036
+ (0, import_react.createElement)("input", { type: "radio", name: "rt-scope", checked: scope === value, onChange: () => setScope(value) }),
1037
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-text" }, [
1038
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-label" }, t(labelKey)),
1039
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-desc" }, t(descKey))
1040
+ ])
1041
+ ])))
1042
+ ]),
1043
+ (0, import_react.createElement)("div", { key: "buttons", className: "dsh-rt-modal-buttons" }, [
1044
+ (0, import_react.createElement)("button", { key: "cancel", type: "button", className: "dsh-rt-editor-cancel", disabled: busy, onClick: onCancel }, t("timeline.cancel")),
1045
+ (0, import_react.createElement)(
1046
+ "button",
1047
+ { key: "confirm", type: "button", className: "dsh-rt-editor-send dsh-rt-confirm", disabled: busy || preview.error !== null, onClick: onConfirm },
1048
+ busy ? t("timeline.busy") : t("timeline.confirm")
1049
+ )
1050
+ ])
1051
+ ]);
1052
+ }
602
1053
  function apply(ctx) {
603
1054
  const disposeStyle = ensureStyle();
604
1055
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
605
1056
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
1057
+ const t = ctx.locale.bind(NS);
606
1058
  const conversationEvents = ctx.get("conversationEvents");
607
1059
  if (conversationEvents) {
608
1060
  conversationEvents.register(userActionsDefinition);
@@ -630,6 +1082,17 @@ return {
630
1082
  key: "recall-marker",
631
1083
  locale: NS
632
1084
  }, RecallMarkerRow));
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));
633
1096
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
634
1097
  name: "settings.general.item",
635
1098
  id: "retrace",