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.
@@ -37,7 +37,16 @@ var inject = ["slots", "locale", "conversationEvents"];
37
37
  var NS = "retrace";
38
38
  var ROUTE_BASE = "/api/plugins/retrace";
39
39
  var MARKER_PREFIX = "retrace";
40
+ var MARKER_PREFIXES = [MARKER_PREFIX, "message-editor"];
41
+ var LEGACY_MARKER_PREFIXES = ["message-editor"];
42
+ function isMarkerId(id) {
43
+ return typeof id === "string" && MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`));
44
+ }
45
+ function isLegacyMarkerId(id) {
46
+ return typeof id === "string" && LEGACY_MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`));
47
+ }
40
48
  var CONFIG_KEY = "dsh-retrace:config";
49
+ var LEGACY_CONFIG_KEYS = ["dsh-message-editor:config"];
41
50
  var zh = {
42
51
  "action.edit": "\u7F16\u8F91",
43
52
  "action.editAria": "\u7F16\u8F91\u8FD9\u6761\u6D88\u606F",
@@ -56,13 +65,60 @@ var zh = {
56
65
  "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",
57
66
  "options.title": "\u6D88\u606F\u7F16\u8F91\u63D2\u4EF6",
58
67
  "options.showOriginalInput": "\u7F16\u8F91\u540E\u663E\u793A\u539F\u63D0\u95EE\u5BF9\u7167",
59
- "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF09",
68
+ "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF0C\u9ED8\u8BA4\u5173\uFF09",
69
+ "options.hideShadowed": "\u6309\u6807\u8BB0\u9690\u85CF\u88AB\u7F16\u8F91/\u64A4\u56DE\u7684\u6D88\u606F",
70
+ "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",
60
71
  "options.versioning": "\u7248\u672C\u4E0E\u4EA7\u7269\u5FEB\u7167",
61
72
  "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",
62
73
  "options.git": "\u542F\u7528 git \u96C6\u6210",
63
74
  "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",
64
75
  "options.retention": "\u7248\u672C\u4FDD\u7559\u4E0A\u9650",
65
76
  "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",
77
+ "timeline.open": "\u65F6\u95F4\u7EBF",
78
+ "timeline.openAria": "\u6253\u5F00\u4F1A\u8BDD\u65F6\u95F4\u7EBF",
79
+ "timeline.title": "\u7248\u672C\u65F6\u95F4\u7EBF",
80
+ "view.retrace": "\u7248\u672C",
81
+ "timeline.refresh": "\u5237\u65B0",
82
+ "timeline.close": "\u5173\u95ED",
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",
84
+ "timeline.loading": "\u52A0\u8F7D\u4E2D\u2026",
85
+ "timeline.error": "\u65F6\u95F4\u7EBF\u52A0\u8F7D\u5931\u8D25",
86
+ "timeline.kind.recall": "\u64A4\u56DE",
87
+ "timeline.kind.edit": "\u7F16\u8F91\u91CD\u53D1",
88
+ "timeline.kind.regenerate": "\u91CD\u65B0\u751F\u6210",
89
+ "timeline.kind.restore": "\u6062\u590D",
90
+ "timeline.kind.compaction": "\u538B\u7F29",
91
+ "timeline.kind.replace": "\u66FF\u6362",
92
+ "timeline.messages": "{count} \u6761\u6D88\u606F",
93
+ "timeline.files": "{created} \u589E / {modified} \u6539 / {deleted} \u5220",
94
+ "timeline.filesNone": "\u65E0\u6587\u4EF6\u53D8\u66F4",
95
+ "timeline.preview": "\u56DE\u9000\u9884\u89C8",
96
+ "timeline.previewDesc": "\u5C06\u56DE\u9000\u5230\u7248\u672C {version}\uFF08{kind}\uFF09\u3002",
97
+ "timeline.contextOnly": "\u4EC5\u5BF9\u8BDD",
98
+ "timeline.contextOnlyDesc": "\u79FB\u9664\u8BE5\u7248\u672C\u4E4B\u540E\u7684\u6D88\u606F\uFF08\u4FDD\u7559\u65E5\u5FD7\u5BA1\u8BA1\u75D5\u8FF9\uFF09",
99
+ "timeline.artifactsOnly": "\u4EC5\u4EA7\u7269",
100
+ "timeline.artifactsOnlyDesc": "\u628A\u8BE5\u7248\u672C\u89E6\u78B0\u7684\u6587\u4EF6\u6062\u590D\u5230\u5F53\u65F6\u7684\u5167\u5BB9",
101
+ "timeline.both": "\u4E24\u8005",
102
+ "timeline.bothDesc": "\u5148\u56DE\u9000\u5BF9\u8BDD\uFF0C\u518D\u56DE\u9000\u4EA7\u7269",
103
+ "timeline.messagesRemoved": "\u5C06\u79FB\u9664 {count} \u6761\u6D88\u606F",
104
+ "timeline.noChanges": "\u5F53\u524D\u5DF2\u5728\u8BE5\u7248\u672C\u72B6\u6001\uFF0C\u65E0\u53D8\u5316",
105
+ "timeline.artifactsList": "\u4EA7\u7269\u52A8\u4F5C\uFF08{count}\uFF09",
106
+ "timeline.artifact.restore": "\u6062\u590D",
107
+ "timeline.artifact.delete": "\u5220\u9664",
108
+ "timeline.artifact.skip": "\u8DF3\u8FC7",
109
+ "timeline.confirm": "\u786E\u8BA4\u56DE\u9000",
110
+ "timeline.cancel": "\u53D6\u6D88",
111
+ "timeline.busy": "\u56DE\u9000\u4E2D\u2026",
112
+ "timeline.detail": "\u8BE6\u60C5",
113
+ "timeline.trajectory": "\u8F68\u8FF9\u53F0\u8D26",
114
+ "timeline.jump": "\u8DF3\u8F6C",
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",
116
+ "timeline.gitRepo": "git \u4ED3\u5E93",
117
+ "timeline.gitHead": "HEAD {hash}",
118
+ "timeline.gitDirty": "\u5DE5\u4F5C\u533A\u6709\u672A\u63D0\u4EA4\u6539\u52A8",
119
+ "timeline.gitInit": "\u542F\u7528 git \u7248\u672C\u7BA1\u7406",
120
+ "timeline.gitInitDesc": "\u5728\u5DE5\u4F5C\u533A\u6267\u884C git init\uFF08\u4EC5\u6DFB\u52A0 .gitignore \u4E0E\u4E00\u4E2A\u57FA\u7EBF\u63D0\u4EA4\uFF09\uFF0C\u7248\u672C\u56DE\u9000\u5C06\u4F18\u5148\u4F7F\u7528 git\u3002",
121
+ "timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
66
122
  "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
67
123
  "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
68
124
  };
@@ -84,13 +140,60 @@ var en = {
84
140
  "marker.referenceHint": "Click to expand the original input (reference only, never sent to the model)",
85
141
  "options.title": "Message editor plugin",
86
142
  "options.showOriginalInput": "Show the original input after editing",
87
- "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages)",
143
+ "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
144
+ "options.hideShadowed": "Hide shadowed messages per marker",
145
+ "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.",
88
146
  "options.versioning": "Version & artifact snapshots",
89
147
  "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).",
90
148
  "options.git": "Git integration",
91
149
  "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.",
92
150
  "options.retention": "Version retention limit",
93
151
  "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).",
152
+ "timeline.open": "Timeline",
153
+ "timeline.openAria": "Open the session timeline",
154
+ "timeline.title": "Version timeline",
155
+ "view.retrace": "Versions",
156
+ "timeline.refresh": "Refresh",
157
+ "timeline.close": "Close",
158
+ "timeline.empty": "No versions yet. Recall / edit / regenerate record a version here.",
159
+ "timeline.loading": "Loading\u2026",
160
+ "timeline.error": "Failed to load the timeline",
161
+ "timeline.kind.recall": "Recall",
162
+ "timeline.kind.edit": "Edit & resend",
163
+ "timeline.kind.regenerate": "Regenerate",
164
+ "timeline.kind.restore": "Restore",
165
+ "timeline.kind.compaction": "Compaction",
166
+ "timeline.kind.replace": "Replace",
167
+ "timeline.messages": "{count} messages",
168
+ "timeline.files": "{created} created / {modified} modified / {deleted} deleted",
169
+ "timeline.filesNone": "No file changes",
170
+ "timeline.preview": "Rollback preview",
171
+ "timeline.previewDesc": "Will roll back to version {version} ({kind}).",
172
+ "timeline.contextOnly": "Context only",
173
+ "timeline.contextOnlyDesc": "Remove messages after this version (the log audit trail stays)",
174
+ "timeline.artifactsOnly": "Artifacts only",
175
+ "timeline.artifactsOnlyDesc": "Restore the files this version touched to their state at that version",
176
+ "timeline.both": "Both",
177
+ "timeline.bothDesc": "Roll back the context first, then the artifacts",
178
+ "timeline.messagesRemoved": "{count} messages will be removed",
179
+ "timeline.noChanges": "Already at this version; nothing to change",
180
+ "timeline.artifactsList": "Artifact actions ({count})",
181
+ "timeline.artifact.restore": "restore",
182
+ "timeline.artifact.delete": "delete",
183
+ "timeline.artifact.skip": "skip",
184
+ "timeline.confirm": "Confirm rollback",
185
+ "timeline.cancel": "Cancel",
186
+ "timeline.busy": "Rolling back\u2026",
187
+ "timeline.detail": "Details",
188
+ "timeline.trajectory": "Trajectory",
189
+ "timeline.jump": "Jump",
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.",
191
+ "timeline.gitRepo": "git repository",
192
+ "timeline.gitHead": "HEAD {hash}",
193
+ "timeline.gitDirty": "working tree has uncommitted changes",
194
+ "timeline.gitInit": "Enable git versioning",
195
+ "timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
196
+ "timeline.gitInitConfirm": "Initialize git in this workspace?",
94
197
  "error.generic": "Operation failed; please try again",
95
198
  "error.busy": "Stop the current reply before recalling or editing"
96
199
  };
@@ -117,14 +220,49 @@ function retraceConfigHeaders() {
117
220
  const { versioning, git, retentionLimit } = getConfig();
118
221
  return { "x-retrace-config": JSON.stringify({ versioning, git, retentionLimit }) };
119
222
  }
120
- var CONFIG_DEFAULTS = { showOriginalInput: true, editFromScratch: true, versioning: true, git: true, retentionLimit: 50 };
223
+ var CONFIG_VERSION = 3;
224
+ var CONFIG_DEFAULTS = { version: CONFIG_VERSION, showOriginalInput: true, editFromScratch: false, hideShadowed: true, versioning: true, git: true, retentionLimit: 50, prewrite: true };
121
225
  var configListeners = /* @__PURE__ */ new Set();
122
226
  var configCache = readConfig();
123
227
  var editReferences = /* @__PURE__ */ new Map();
228
+ function migrateConfig(parsed) {
229
+ const version = typeof parsed.version === "number" ? parsed.version : 1;
230
+ if (version >= CONFIG_VERSION) return { ...CONFIG_DEFAULTS, ...parsed };
231
+ return {
232
+ ...CONFIG_DEFAULTS,
233
+ ...parsed,
234
+ version: CONFIG_VERSION,
235
+ editFromScratch: CONFIG_DEFAULTS.editFromScratch,
236
+ hideShadowed: CONFIG_DEFAULTS.hideShadowed
237
+ };
238
+ }
124
239
  function readConfig() {
125
240
  try {
126
241
  const raw = localStorage.getItem(CONFIG_KEY);
127
- return { ...CONFIG_DEFAULTS, ...raw ? JSON.parse(raw) : {} };
242
+ if (raw !== null) {
243
+ const parsed = raw ? JSON.parse(raw) : {};
244
+ const migrated = migrateConfig(parsed);
245
+ const storedVersion = typeof parsed.version === "number" ? parsed.version : 1;
246
+ if (storedVersion < CONFIG_VERSION) {
247
+ try {
248
+ localStorage.setItem(CONFIG_KEY, JSON.stringify(migrated));
249
+ } catch {
250
+ }
251
+ }
252
+ return migrated;
253
+ }
254
+ for (const legacyKey of LEGACY_CONFIG_KEYS) {
255
+ const legacyRaw = localStorage.getItem(legacyKey);
256
+ if (legacyRaw !== null) {
257
+ const migrated = migrateConfig(legacyRaw ? JSON.parse(legacyRaw) : {});
258
+ try {
259
+ localStorage.setItem(CONFIG_KEY, JSON.stringify(migrated));
260
+ } catch {
261
+ }
262
+ return migrated;
263
+ }
264
+ }
265
+ return { ...CONFIG_DEFAULTS };
128
266
  } catch {
129
267
  return { ...CONFIG_DEFAULTS };
130
268
  }
@@ -202,9 +340,11 @@ var userReferenceDefinition = {
202
340
  }
203
341
  };
204
342
  function markerOpFromId(id) {
205
- if (id.startsWith(`${MARKER_PREFIX}-recall-`)) return "recall";
206
- if (id.startsWith(`${MARKER_PREFIX}-edit-`)) return "edit";
207
- if (id.startsWith(`${MARKER_PREFIX}-regenerate-`)) return "regenerate";
343
+ for (const p of MARKER_PREFIXES) {
344
+ if (id.startsWith(`${p}-recall-`)) return "recall";
345
+ if (id.startsWith(`${p}-edit-`)) return "edit";
346
+ if (id.startsWith(`${p}-regenerate-`)) return "regenerate";
347
+ }
208
348
  return "edit";
209
349
  }
210
350
  var recallMarkerDefinition = {
@@ -213,16 +353,22 @@ var recallMarkerDefinition = {
213
353
  match: (event) => {
214
354
  if (event.type !== "assistant/message" || !isReplacementSurfaceEvent(event)) return null;
215
355
  const id = event.data?.message?.id;
216
- if (typeof id !== "string" || !id.startsWith(`${MARKER_PREFIX}-`)) return null;
356
+ if (!isMarkerId(id)) return null;
217
357
  return { id: `marker:${id}`, role: "start" };
218
358
  },
219
359
  start: (_context, match) => {
220
360
  const event = match.event;
361
+ const id = String(event.data.message.id);
362
+ const legacy = isLegacyMarkerId(id);
221
363
  return {
222
364
  seq: event.seq,
223
365
  time: event.time,
224
- op: markerOpFromId(String(event.data.message.id)),
225
- shadowedSeqs: Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
366
+ op: markerOpFromId(id),
367
+ legacy,
368
+ // Legacy markers never hide: treat their shadowed range as empty so the
369
+ // notice/reference render but no row is hidden and no action row is
370
+ // suppressed via useShadowed.
371
+ shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
226
372
  targetSeq: event.data?.editor?.targetSeq,
227
373
  text: event.data?.editor?.text
228
374
  };
@@ -258,12 +404,15 @@ function useShadowed(useSession, seq) {
258
404
  return false;
259
405
  });
260
406
  }
407
+ var SHADOW_SAFETY_RATIO = 0.4;
261
408
  function useHiddenKeys(useSession, shadowedSeqs) {
262
409
  return useSession((snapshot) => {
263
410
  if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
264
411
  const hidden = new Set(shadowedSeqs);
265
412
  const keys = [];
413
+ let rowCount = 0;
266
414
  for (const node of snapshot.chat.nodes.values()) {
415
+ if (typeof node.anchorSeq === "number") rowCount += 1;
267
416
  if (node.kind === "recall-marker") continue;
268
417
  if (node.kind === "turn-tail") {
269
418
  const closingSeq = node.data?.closing?.finalNode?.seq;
@@ -277,6 +426,7 @@ function useHiddenKeys(useSession, shadowedSeqs) {
277
426
  }
278
427
  if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
279
428
  }
429
+ if (keys.length > 0 && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) return null;
280
430
  return keys.length === 0 ? null : keys;
281
431
  });
282
432
  }
@@ -477,9 +627,9 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
477
627
  ]);
478
628
  }
479
629
  function RecallMarkerRow({ node, useSession, t }) {
480
- const { seq, op, shadowedSeqs } = node.data;
630
+ const { seq, op, shadowedSeqs, legacy } = node.data;
481
631
  const dismissed = useMarkerDismissed(useSession, seq, op);
482
- const hiddenKeys = useHiddenKeys(useSession, shadowedSeqs);
632
+ const hiddenKeys = legacy || !getConfig().hideShadowed ? null : useHiddenKeys(useSession, shadowedSeqs);
483
633
  const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
484
634
  const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
485
635
  const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
@@ -516,6 +666,7 @@ function OptionsRow({ t }) {
516
666
  }),
517
667
  (0, import_react.createElement)("span", null, t("options.editFromScratch"))
518
668
  ]),
669
+ optionRow("hideShadowed", "options.hideShadowed", "options.hideShadowedDesc"),
519
670
  optionRow("versioning", "options.versioning", "options.versioningDesc"),
520
671
  optionRow("git", "options.git", "options.gitDesc"),
521
672
  (0, import_react.createElement)("div", { key: "retention", className: "dsh-rt-option dsh-rt-option-number" }, [
@@ -578,6 +729,44 @@ var CSS = `
578
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}
579
730
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
580
731
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
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
+ .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
735
+ .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
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
+ .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:1;min-height:0;position:relative;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2)}
739
+ .dsh-rt-timeline-empty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding:12px 4px;text-align:center}
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
+ .dsh-rt-version:hover{background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l2)}
742
+ .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}
743
+ .dsh-rt-version-kind-restore{background:var(--dsw-alias-state-success-bg);color:var(--dsw-alias-state-success-primary)}
744
+ .dsh-rt-version-kind-compaction{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-caption)}
745
+ .dsh-rt-version-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
746
+ .dsh-rt-version-line{display:flex;align-items:center;gap:8px;min-width:0;white-space:nowrap}
747
+ .dsh-rt-version-kind-label{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;line-height:16px}
748
+ .dsh-rt-version-time,.dsh-rt-version-msgs{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
749
+ .dsh-rt-version-files{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;overflow:hidden;text-overflow:ellipsis}
750
+ .dsh-rt-version-text{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:15px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
751
+ .dsh-rt-version-actions{flex:none;display:inline-flex;gap:4px;opacity:0;transition:opacity .1s}
752
+ .dsh-rt-version:hover .dsh-rt-version-actions{opacity:1}
753
+ .dsh-rt-chip-danger{color:var(--dsw-alias-state-error-primary)}
754
+ .dsh-rt-chip-danger:hover{color:var(--dsw-alias-state-error-primary)}
755
+ .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)}
756
+ .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}
757
+ .dsh-rt-modal-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400}
758
+ .dsh-rt-modal-body{display:flex;flex-direction:column;gap:6px;overflow-y:auto;min-height:0}
759
+ .dsh-rt-modal-line{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
760
+ .dsh-rt-modal-files{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;max-height:160px;overflow-y:auto}
761
+ .dsh-rt-modal-files li{display:flex;gap:8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
762
+ .dsh-rt-art-restore{color:var(--dsw-alias-state-success-primary);flex:none}
763
+ .dsh-rt-art-delete{color:var(--dsw-alias-state-error-primary);flex:none}
764
+ .dsh-rt-art-skip{color:var(--dsw-alias-label-caption);flex:none}
765
+ .dsh-rt-art-path{font-family:var(--dsw-font-mono);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
766
+ .dsh-rt-modal-scope{display:flex;flex-direction:column;gap:4px}
767
+ .dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
768
+ .dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
769
+ .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}
581
770
  `;
582
771
  function ensureStyle() {
583
772
  if (typeof document === "undefined") return () => {
@@ -593,10 +782,273 @@ function ensureStyle() {
593
782
  tag.remove();
594
783
  };
595
784
  }
785
+ var KIND_ICONS = { recall: "\u21A9", edit: "\u270E", regenerate: "\u21BB", restore: "\u27F2", compaction: "\u25A4", replace: "\u21C4" };
786
+ function kindLabel(kind, t) {
787
+ return t(`timeline.kind.${kind}`) || kind;
788
+ }
789
+ function timeLabel(ms) {
790
+ const date = new Date(ms);
791
+ return `${date.getMonth() + 1}-${date.getDate()} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
792
+ }
793
+ function timelineGet(path) {
794
+ return fetch(`${ROUTE_BASE}${path}`, { headers: retraceConfigHeaders() }).then((res) => {
795
+ if (res.status < 200 || res.status >= 300) throw new Error(`HTTP ${res.status}`);
796
+ return res.json();
797
+ });
798
+ }
799
+ function fileCountLabel(counts, t) {
800
+ const total = (counts?.created ?? 0) + (counts?.modified ?? 0) + (counts?.deleted ?? 0);
801
+ if (total === 0) return t("timeline.filesNone");
802
+ return t("timeline.files", {
803
+ created: counts.created ?? 0,
804
+ modified: counts.modified ?? 0,
805
+ deleted: counts.deleted ?? 0
806
+ });
807
+ }
808
+ function RetraceView({ sessionId, useProjection, t, actions, store }) {
809
+ const [versions, setVersions] = (0, import_react.useState)(null);
810
+ const [loading, setLoading] = (0, import_react.useState)(false);
811
+ const [error, setError] = (0, import_react.useState)(null);
812
+ const [git, setGit] = (0, import_react.useState)(null);
813
+ const [gitBusy, setGitBusy] = (0, import_react.useState)(false);
814
+ const [preview, setPreview] = (0, import_react.useState)(null);
815
+ const [previewScope, setPreviewScope] = (0, import_react.useState)("both");
816
+ const [rollbackBusy, setRollbackBusy] = (0, import_react.useState)(false);
817
+ const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
818
+ const projected = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
819
+ (0, import_react.useEffect)(() => {
820
+ if (projected && Array.isArray(projected.versions)) setVersions(projected.versions);
821
+ }, [projected]);
822
+ const refresh = () => {
823
+ setLoading(true);
824
+ setError(null);
825
+ timelineGet(`/versions?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
826
+ if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "versions failed");
827
+ setVersions(result.value?.versions ?? []);
828
+ }).catch((cause) => setError(cause?.message ?? "timeline error")).finally(() => setLoading(false));
829
+ };
830
+ const refreshGit = () => {
831
+ if (getConfig().git !== true) return;
832
+ timelineGet(`/git/status?sessionId=${encodeURIComponent(sessionId)}`).then((result) => setGit(result?.ok === true ? result.value : null)).catch(() => setGit(null));
833
+ };
834
+ (0, import_react.useEffect)(() => {
835
+ if (projected === void 0) refresh();
836
+ refreshGit();
837
+ }, []);
838
+ const JUMP_PAGE_BUDGET = 24;
839
+ const jump = async (boundarySeq) => {
840
+ actions?.setView?.("chat");
841
+ const target = store;
842
+ if (!target || typeof target.loadOlder !== "function" || typeof target.getSnapshot !== "function") return;
843
+ const keyOfSeq = (seq) => {
844
+ const nodes = target.getSnapshot()?.chat?.nodes;
845
+ if (!nodes) return null;
846
+ for (const node of nodes.values()) {
847
+ if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
848
+ }
849
+ return null;
850
+ };
851
+ let key = keyOfSeq(boundarySeq);
852
+ let pages = 0;
853
+ while (key === null && pages < JUMP_PAGE_BUDGET && target.hasMore) {
854
+ const before = target.getSnapshot()?.chat?.nodes?.size ?? 0;
855
+ await target.loadOlder();
856
+ const after = target.getSnapshot()?.chat?.nodes?.size ?? 0;
857
+ pages += 1;
858
+ if (after === before) break;
859
+ key = keyOfSeq(boundarySeq);
860
+ }
861
+ if (key === null) {
862
+ await new Promise((resolve) => setTimeout(resolve, 60));
863
+ key = keyOfSeq(boundarySeq);
864
+ }
865
+ if (key === null) {
866
+ console.warn(`[dsh-retrace] jump target seq ${boundarySeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
867
+ return;
868
+ }
869
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
870
+ if (el === null) return;
871
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
872
+ flashKey(key);
873
+ };
874
+ function waitForElement(selector, frames) {
875
+ return new Promise((resolve) => {
876
+ let remaining = frames;
877
+ const probe = () => {
878
+ const el = document.querySelector(selector);
879
+ if (el !== null) return resolve(el);
880
+ if (remaining-- <= 0) return resolve(null);
881
+ requestAnimationFrame(probe);
882
+ };
883
+ requestAnimationFrame(probe);
884
+ });
885
+ }
886
+ function flashKey(key) {
887
+ const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
888
+ if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
889
+ const tag = document.createElement("style");
890
+ tag.dataset.pluginCss = id;
891
+ tag.textContent = `[data-chat-anchor-key=${JSON.stringify(key)}]{animation:dsh-rt-flash 1.6s ease-out 2}@keyframes dsh-rt-flash{0%,100%{background:transparent}30%,70%{background:var(--dsw-alias-state-business-primary)}55%{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 30%,transparent)}}`;
892
+ document.head.appendChild(tag);
893
+ setTimeout(() => tag.remove(), 3400);
894
+ }
895
+ const requestPreview = (record) => {
896
+ setPreviewScope("both");
897
+ setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
898
+ callOp("rollback/preview", { sessionId, versionId: record.versionId, scope: "both" }).then((result) => {
899
+ 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);
900
+ });
901
+ };
902
+ const confirmRollback = () => {
903
+ if (!preview) return;
904
+ setRollbackBusy(true);
905
+ callOp("rollback", { sessionId, versionId: preview.versionId, scope: previewScope }).then((result) => {
906
+ setRollbackBusy(false);
907
+ if (!result || result.ok !== true) {
908
+ setPreview((prev) => prev && { ...prev, error: result?.error?.message ?? "rollback failed" });
909
+ return;
910
+ }
911
+ setPreview(null);
912
+ if (projected === void 0) refresh();
913
+ refreshGit();
914
+ });
915
+ };
916
+ const initGit = () => {
917
+ if (!window.confirm(t("timeline.gitInitConfirm"))) return;
918
+ setGitBusy(true);
919
+ callOp("git/init", { sessionId }).then((result) => {
920
+ setGitBusy(false);
921
+ refreshGit();
922
+ });
923
+ };
924
+ const ROW_H = 64;
925
+ const list = versions ?? [];
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"))
936
+ ]),
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)
956
+ }, [
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
+ })
979
+ ]);
980
+ }
981
+ function VersionRow({ record, top, t, onPreview, onTrajectory, onJump }) {
982
+ return (0, import_react.createElement)("div", { className: "dsh-rt-version", style: { top: `${top}px` } }, [
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"),
984
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-version-body" }, [
985
+ (0, import_react.createElement)("div", { key: "line1", className: "dsh-rt-version-line" }, [
986
+ (0, import_react.createElement)("span", { key: "kind", className: "dsh-rt-version-kind-label" }, kindLabel(record.kind, t)),
987
+ (0, import_react.createElement)("span", { key: "time", className: "dsh-rt-version-time" }, timeLabel(record.createdAt)),
988
+ (0, import_react.createElement)("span", { key: "msgs", className: "dsh-rt-version-msgs" }, t("timeline.messages", { count: record.messageCount })),
989
+ (0, import_react.createElement)("span", { key: "files", className: "dsh-rt-version-files" }, fileCountLabel(record.fileCounts, t))
990
+ ]),
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
992
+ ]),
993
+ (0, import_react.createElement)("span", { key: "actions", className: "dsh-rt-version-actions" }, [
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")),
996
+ (0, import_react.createElement)("button", { key: "preview", type: "button", className: "dsh-rt-chip dsh-rt-chip-danger", onClick: onPreview }, t("timeline.preview"))
997
+ ])
998
+ ]);
999
+ }
1000
+ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel }) {
1001
+ const data = preview.data;
1002
+ const scopes = [
1003
+ ["context", "timeline.contextOnly", "timeline.contextOnlyDesc"],
1004
+ ["artifacts", "timeline.artifactsOnly", "timeline.artifactsOnlyDesc"],
1005
+ ["both", "timeline.both", "timeline.bothDesc"]
1006
+ ];
1007
+ return (0, import_react.createElement)("div", { className: "dsh-rt-modal" }, [
1008
+ (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-modal-title" }, [
1009
+ t("timeline.preview"),
1010
+ (0, import_react.createElement)("span", { key: "ver", className: "dsh-rt-modal-sub" }, t("timeline.previewDesc", { version: preview.versionId, kind: kindLabel(preview.kind, t) }))
1011
+ ]),
1012
+ preview.error && (0, import_react.createElement)("div", { key: "err", className: "dsh-rt-error" }, preview.error),
1013
+ data === null && !preview.error && (0, import_react.createElement)("div", { key: "wait", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1014
+ data && (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-modal-body" }, [
1015
+ (0, import_react.createElement)(
1016
+ "div",
1017
+ { key: "ctx", className: "dsh-rt-modal-line" },
1018
+ data.context?.messages > 0 ? t("timeline.messagesRemoved", { count: data.context.messages }) : t("timeline.noChanges")
1019
+ ),
1020
+ (0, import_react.createElement)("div", { key: "art", className: "dsh-rt-modal-line" }, t("timeline.artifactsList", { count: data.artifacts?.rows?.length ?? 0 })),
1021
+ (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 }, [
1022
+ (0, import_react.createElement)(
1023
+ "span",
1024
+ { key: "a", className: `dsh-rt-art-${row.action}` },
1025
+ row.action === "skip" ? `${t("timeline.artifact.skip")} (${row.reason ?? ""})` : row.action === "delete" ? t("timeline.artifact.delete") : t("timeline.artifact.restore")
1026
+ ),
1027
+ (0, import_react.createElement)("span", { key: "p", className: "dsh-rt-art-path" }, row.path)
1028
+ ]))),
1029
+ (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" }, [
1030
+ (0, import_react.createElement)("input", { type: "radio", name: "rt-scope", checked: scope === value, onChange: () => setScope(value) }),
1031
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-text" }, [
1032
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-label" }, t(labelKey)),
1033
+ (0, import_react.createElement)("span", { className: "dsh-rt-option-desc" }, t(descKey))
1034
+ ])
1035
+ ])))
1036
+ ]),
1037
+ (0, import_react.createElement)("div", { key: "buttons", className: "dsh-rt-modal-buttons" }, [
1038
+ (0, import_react.createElement)("button", { key: "cancel", type: "button", className: "dsh-rt-editor-cancel", disabled: busy, onClick: onCancel }, t("timeline.cancel")),
1039
+ (0, import_react.createElement)(
1040
+ "button",
1041
+ { key: "confirm", type: "button", className: "dsh-rt-editor-send dsh-rt-confirm", disabled: busy || preview.error !== null, onClick: onConfirm },
1042
+ busy ? t("timeline.busy") : t("timeline.confirm")
1043
+ )
1044
+ ])
1045
+ ]);
1046
+ }
596
1047
  function apply(ctx) {
597
1048
  const disposeStyle = ensureStyle();
598
1049
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
599
1050
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-retrace: dictionaries");
1051
+ const t = ctx.locale.bind(NS);
600
1052
  const conversationEvents = ctx.get("conversationEvents");
601
1053
  if (conversationEvents) {
602
1054
  conversationEvents.register(userActionsDefinition);
@@ -624,6 +1076,17 @@ function apply(ctx) {
624
1076
  key: "recall-marker",
625
1077
  locale: NS
626
1078
  }, RecallMarkerRow));
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));
627
1090
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
628
1091
  name: "settings.general.item",
629
1092
  id: "retrace",