dsh-retrace 0.4.2 → 0.4.5

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.
@@ -69,6 +69,8 @@ return {
69
69
  "marker.regenerate": "\u5DF2\u91CD\u65B0\u751F\u6210\u56DE\u590D",
70
70
  "marker.originalLabel": "\u539F\u8F93\u5165",
71
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",
72
+ "marker.degradedHint": "\u6B64\u64CD\u4F5C\u6D89\u53CA\u5927\u8303\u56F4\u5BF9\u8BDD\uFF0C\u4E3A\u4FDD\u62A4\u5386\u53F2\u672A\u9690\u85CF\u5185\u5BB9\uFF08\u65E5\u5FD7\u5B8C\u597D\uFF09\u3002",
73
+ "marker.unionHint": "\u5DF2\u7D2F\u79EF\u9690\u85CF\u7EA6 {count}% \u7684\u5386\u53F2\u6D88\u606F\uFF1B\u53EF\u5728 \u8BBE\u7F6E\u2192\u901A\u7528 \u5173\u95ED\u300C\u6309\u6807\u8BB0\u9690\u85CF\u300D\u67E5\u770B\u5B8C\u6574\u5386\u53F2\u3002",
72
74
  "options.title": "\u6D88\u606F\u7F16\u8F91\u63D2\u4EF6",
73
75
  "options.showOriginalInput": "\u7F16\u8F91\u540E\u663E\u793A\u539F\u63D0\u95EE\u5BF9\u7167",
74
76
  "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF0C\u9ED8\u8BA4\u5173\uFF09",
@@ -126,7 +128,17 @@ return {
126
128
  "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
129
  "timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
128
130
  "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
129
- "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
131
+ "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C",
132
+ "view.fork": "\u5206\u53C9",
133
+ "fork.title": "\u5206\u53C9\u56FE",
134
+ "fork.empty": "\u8FD8\u6CA1\u6709\u5206\u53C9\u3002\u64A4\u56DE / \u7F16\u8F91 / \u91CD\u65B0\u751F\u6210 / \u6062\u590D\u4F1A\u5728\u6B64\u4EA7\u751F\u5206\u53C9\u3002",
135
+ "fork.refresh": "\u5237\u65B0",
136
+ "fork.spine": "\u5F53\u524D\u8DEF\u5F84",
137
+ "fork.shadowed": "\u88AB\u906E\u853D {count} \u4E2A\u8282\u70B9",
138
+ "fork.node.user": "\u7528\u6237\u6D88\u606F",
139
+ "fork.node.assistant": "\u52A9\u624B\u56DE\u590D",
140
+ "fork.node.tool": "\u5DE5\u5177\u7ED3\u679C",
141
+ "fork.histTitle": "\u5386\u53F2\u5206\u53C9\u70B9"
130
142
  };
131
143
  var en = {
132
144
  "action.edit": "Edit",
@@ -144,6 +156,8 @@ return {
144
156
  "marker.regenerate": "Reply regenerated",
145
157
  "marker.originalLabel": "Original input",
146
158
  "marker.referenceHint": "Click to expand the original input (reference only, never sent to the model)",
159
+ "marker.degradedHint": "This operation spans a large part of the conversation; content stays visible to protect your history (the log is intact).",
160
+ "marker.unionHint": 'About {count}% of the history is hidden in total; disable "Hide shadowed messages" in Settings \u2192 General to review the full history.',
147
161
  "options.title": "Message editor plugin",
148
162
  "options.showOriginalInput": "Show the original input after editing",
149
163
  "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
@@ -201,7 +215,17 @@ return {
201
215
  "timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
202
216
  "timeline.gitInitConfirm": "Initialize git in this workspace?",
203
217
  "error.generic": "Operation failed; please try again",
204
- "error.busy": "Stop the current reply before recalling or editing"
218
+ "error.busy": "Stop the current reply before recalling or editing",
219
+ "view.fork": "Fork",
220
+ "fork.title": "Fork map",
221
+ "fork.empty": "No forks yet. Recall / edit / regenerate / restore fork the path here.",
222
+ "fork.refresh": "Refresh",
223
+ "fork.spine": "Current path",
224
+ "fork.shadowed": "{count} shadowed nodes",
225
+ "fork.node.user": "User message",
226
+ "fork.node.assistant": "Assistant reply",
227
+ "fork.node.tool": "Tool result",
228
+ "fork.histTitle": "Historical fork points"
205
229
  };
206
230
  var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
207
231
  function isReplacementSurfaceEvent(event) {
@@ -357,23 +381,32 @@ return {
357
381
  kind: "recall-marker",
358
382
  target: "chat",
359
383
  match: (event) => {
360
- if (event.type !== "assistant/message" || !isReplacementSurfaceEvent(event)) return null;
361
- const id = event.data?.message?.id;
362
- if (!isMarkerId(id)) return null;
363
- return { id: `marker:${id}`, role: "start" };
384
+ if (!isReplacementSurfaceEvent(event)) return null;
385
+ if (event.type === "assistant/message") {
386
+ const id = event.data?.message?.id;
387
+ if (!isMarkerId(id)) return null;
388
+ return { id: `marker:${id}`, role: "start" };
389
+ }
390
+ if (event.type === "user/message" && isCompactCheckpoint(event.data?.source)) {
391
+ return { id: `marker:compact:${event.seq}`, role: "start" };
392
+ }
393
+ return null;
364
394
  },
365
395
  start: (_context, match) => {
366
396
  const event = match.event;
367
- const id = String(event.data.message.id);
368
- const legacy = isLegacyMarkerId(id);
397
+ const compact = event.type === "user/message" && isCompactCheckpoint(event.data?.source);
398
+ const id = compact ? "" : String(event.data.message.id);
399
+ const legacy = !compact && isLegacyMarkerId(id);
369
400
  return {
370
401
  seq: event.seq,
371
402
  time: event.time,
372
- op: markerOpFromId(id),
403
+ op: compact ? "compaction" : markerOpFromId(id),
373
404
  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.
405
+ compact,
406
+ // Legacy/compact markers never hide: their shadowed range is kept for
407
+ // the action-row suppression check (useShadowed) but empty for legacy
408
+ // so no row is hidden and no action row is suppressed for legacy
409
+ // markers (rename must never make visible content disappear).
377
410
  shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
378
411
  targetSeq: event.data?.editor?.targetSeq,
379
412
  text: event.data?.editor?.text
@@ -385,6 +418,9 @@ return {
385
418
  return chatNodeLike(context, "recall-marker", context.state.seq, context.state);
386
419
  }
387
420
  };
421
+ function isCompactCheckpoint(source) {
422
+ return Boolean(source) && source.kind === "plugin" && source.plugin === "compact";
423
+ }
388
424
  function textOf(content) {
389
425
  if (!Array.isArray(content)) return "";
390
426
  return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
@@ -399,41 +435,111 @@ return {
399
435
  return void 0;
400
436
  });
401
437
  }
402
- function useShadowed(useSession, seq) {
438
+ var SHADOW_SAFETY_RATIO = 0.4;
439
+ function hiddenKeysFor(shadowedSeqs, nodes) {
440
+ if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
441
+ const hidden = new Set(shadowedSeqs);
442
+ const keys = [];
443
+ for (const node of nodes.values()) {
444
+ if (node.kind === "recall-marker") continue;
445
+ if (node.kind === "turn-tail") {
446
+ const closingSeq = node.data?.closing?.finalNode?.seq;
447
+ if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
448
+ continue;
449
+ }
450
+ if (node.kind === "tool-call") {
451
+ const resultSeq = node.data?.root?.seq;
452
+ if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
453
+ continue;
454
+ }
455
+ if (typeof node.anchorSeq === "number") {
456
+ const anchored = node.anchorSeq % 1 === 0 ? node.anchorSeq : Math.ceil(node.anchorSeq);
457
+ if (hidden.has(anchored)) keys.push(node.key);
458
+ }
459
+ }
460
+ return keys.length === 0 ? null : keys;
461
+ }
462
+ var EMPTY_HIDE_PLAN = Object.freeze({
463
+ hiddenFor: () => null,
464
+ planFor: () => null,
465
+ unionRatio: 0,
466
+ firstMarkerKey: null
467
+ });
468
+ var PLUGIN_PSEUDO_KINDS = /* @__PURE__ */ new Set(["user-actions", "retrace-reference", "recall-marker"]);
469
+ function realRowCount(nodes) {
470
+ let count = 0;
471
+ for (const node of nodes.values()) {
472
+ if (typeof node.anchorSeq === "number" && !PLUGIN_PSEUDO_KINDS.has(node.kind)) count += 1;
473
+ }
474
+ return count;
475
+ }
476
+ var hidePlanCacheSnapshot = null;
477
+ var hidePlanCacheValue = null;
478
+ function useMarkerHidePlan(useSession) {
479
+ return useSession((snapshot) => {
480
+ if (hidePlanCacheSnapshot === snapshot) return hidePlanCacheValue;
481
+ const nodes = snapshot.chat.nodes;
482
+ const rowCount = realRowCount(nodes);
483
+ const markers = [];
484
+ for (const node of nodes.values()) {
485
+ if (node.kind === "recall-marker" && !node.data?.compact) markers.push(node);
486
+ }
487
+ if (markers.length === 0) {
488
+ hidePlanCacheSnapshot = snapshot;
489
+ hidePlanCacheValue = EMPTY_HIDE_PLAN;
490
+ return hidePlanCacheValue;
491
+ }
492
+ const plans = /* @__PURE__ */ new Map();
493
+ const union = /* @__PURE__ */ new Set();
494
+ for (const marker of markers) {
495
+ const keys = hiddenKeysFor(marker.data?.shadowedSeqs, nodes);
496
+ const degraded = keys !== null && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO;
497
+ plans.set(marker.key, { keys: degraded ? null : keys, degraded });
498
+ if (keys !== null) for (const key of keys) union.add(key);
499
+ }
500
+ hidePlanCacheSnapshot = snapshot;
501
+ hidePlanCacheValue = {
502
+ planFor: (key) => plans.get(key) ?? null,
503
+ hiddenFor: (key) => plans.get(key)?.keys ?? null,
504
+ unionRatio: rowCount > 0 ? union.size / rowCount : 0,
505
+ firstMarkerKey: markers[0].key
506
+ };
507
+ return hidePlanCacheValue;
508
+ });
509
+ }
510
+ function rowHiddenByKey(snapshot, rowKey) {
511
+ if (rowKey === void 0 || rowKey === null) return false;
512
+ const nodes = snapshot.chat.nodes;
513
+ const rowCount = realRowCount(nodes);
514
+ for (const node of nodes.values()) {
515
+ if (node.kind !== "recall-marker" || node.data?.compact) continue;
516
+ const keys = hiddenKeysFor(node.data?.shadowedSeqs, nodes);
517
+ if (keys === null) continue;
518
+ if (rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) continue;
519
+ if (keys.includes(rowKey)) return true;
520
+ }
521
+ return false;
522
+ }
523
+ function useSeqHidden(useSession, seq) {
403
524
  return useSession((snapshot) => {
404
525
  if (seq === void 0 || seq === null) return false;
405
526
  for (const node of snapshot.chat.nodes.values()) {
406
- if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
407
- return true;
527
+ if (node.kind !== "recall-marker" && typeof node.anchorSeq === "number" && node.anchorSeq === seq) {
528
+ return rowHiddenByKey(snapshot, node.key);
408
529
  }
409
530
  }
410
531
  return false;
411
532
  });
412
533
  }
413
- var SHADOW_SAFETY_RATIO = 0.4;
414
- function useHiddenKeys(useSession, shadowedSeqs) {
534
+ function useShadowed(useSession, seq) {
415
535
  return useSession((snapshot) => {
416
- if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
417
- const hidden = new Set(shadowedSeqs);
418
- const keys = [];
419
- let rowCount = 0;
536
+ if (seq === void 0 || seq === null) return false;
420
537
  for (const node of snapshot.chat.nodes.values()) {
421
- if (typeof node.anchorSeq === "number") rowCount += 1;
422
- if (node.kind === "recall-marker") continue;
423
- if (node.kind === "turn-tail") {
424
- const closingSeq = node.data?.closing?.finalNode?.seq;
425
- if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
426
- continue;
427
- }
428
- if (node.kind === "tool-call") {
429
- const resultSeq = node.data?.root?.seq;
430
- if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
431
- continue;
538
+ if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
539
+ return true;
432
540
  }
433
- if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
434
541
  }
435
- if (keys.length > 0 && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) return null;
436
- return keys.length === 0 ? null : keys;
542
+ return false;
437
543
  });
438
544
  }
439
545
  function useMarkerDismissed(useSession, markerSeq, op) {
@@ -470,10 +576,11 @@ return {
470
576
  }
471
577
  function AssistantActions({ messageId, sessionId, useSession, t }) {
472
578
  const seq = useMessageSeq(useSession, messageId);
579
+ const hidden = useSeqHidden(useSession, seq);
473
580
  const shadowed = useShadowed(useSession, seq);
474
581
  const [busy, setBusy] = (0, import_react.useState)(false);
475
582
  const [failure, setFailure] = (0, import_react.useState)(null);
476
- if (shadowed || seq === void 0) return null;
583
+ if (hidden || shadowed || seq === void 0) return null;
477
584
  const run = (op) => {
478
585
  setBusy(true);
479
586
  setFailure(null);
@@ -516,11 +623,11 @@ return {
516
623
  }
517
624
  function ReferenceRow({ node, useSession, t }) {
518
625
  const { seq, messageId } = node.data;
519
- const shadowed = useShadowed(useSession, seq);
626
+ const hidden = useSeqHidden(useSession, seq);
520
627
  const markerRef = useEditReference(useSession, seq);
521
628
  const referenceText = editReferences.get(messageId) ?? markerRef;
522
629
  const config = useConfig();
523
- if (shadowed) return null;
630
+ if (hidden) return null;
524
631
  if (referenceText === null || !config.showOriginalInput) return null;
525
632
  return (0, import_react.createElement)("div", { className: "dsh-rt-user-row" }, [
526
633
  (0, import_react.createElement)("details", { className: "dsh-rt-reference" }, [
@@ -535,12 +642,13 @@ return {
535
642
  }
536
643
  function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
537
644
  const { seq, messageId, content } = node.data;
645
+ const hidden = useSeqHidden(useSession, seq);
538
646
  const shadowed = useShadowed(useSession, seq);
539
647
  const [editing, setEditing] = (0, import_react.useState)(false);
540
648
  const [draft, setDraft] = (0, import_react.useState)("");
541
649
  const [busy, setBusy] = (0, import_react.useState)(false);
542
650
  const [failure, setFailure] = (0, import_react.useState)(null);
543
- if (shadowed) return null;
651
+ if (hidden || shadowed) return null;
544
652
  const openEditor = () => {
545
653
  setDraft(textOf(content));
546
654
  setFailure(null);
@@ -633,15 +741,26 @@ return {
633
741
  ]);
634
742
  }
635
743
  function RecallMarkerRow({ node, useSession, t }) {
636
- const { seq, op, shadowedSeqs, legacy } = node.data;
744
+ const { seq, op, shadowedSeqs, legacy, compact } = node.data;
745
+ if (compact) return null;
637
746
  const dismissed = useMarkerDismissed(useSession, seq, op);
638
- const hiddenKeys = legacy || !getConfig().hideShadowed ? null : useHiddenKeys(useSession, shadowedSeqs);
747
+ const hidePlan = useMarkerHidePlan(useSession);
748
+ const plan = hidePlan.planFor(node.key);
749
+ const hiddenKeys = legacy || !getConfig().hideShadowed ? null : plan?.keys ?? null;
639
750
  const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
640
751
  const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
641
752
  const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
753
+ const degradedHint = !legacy && plan?.degraded === true ? (0, import_react.createElement)("div", { key: "degraded", className: "dsh-rt-marker-hint" }, t("marker.degradedHint")) : null;
754
+ const unionHint = !legacy && hidePlan.firstMarkerKey === node.key && hidePlan.unionRatio > SHADOW_SAFETY_RATIO ? (0, import_react.createElement)(
755
+ "div",
756
+ { key: "union", className: "dsh-rt-marker-hint" },
757
+ t("marker.unionHint", { count: Math.round(hidePlan.unionRatio * 100) })
758
+ ) : null;
642
759
  return (0, import_react.createElement)("div", { className: "dsh-rt-marker-block", "data-dismissed": dismissed || void 0 }, [
643
760
  css !== null && (0, import_react.createElement)("style", { key: "hide", dangerouslySetInnerHTML: { __html: css } }),
644
- !dismissed && (0, import_react.createElement)("div", { key: "label", className: "dsh-rt-marker", role: "status" }, label)
761
+ !dismissed && (0, import_react.createElement)("div", { key: "label", className: "dsh-rt-marker", role: "status" }, label),
762
+ !dismissed && degradedHint,
763
+ !dismissed && unionHint
645
764
  ]);
646
765
  }
647
766
  function OptionsRow({ t }) {
@@ -718,6 +837,7 @@ return {
718
837
  .dsh-rt-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;max-width:min(525px,82%)}
719
838
  .dsh-rt-marker-block{display:flex;flex-direction:column;align-items:center;gap:4px;width:100%;max-width:var(--dsh-chat-content-width);box-sizing:border-box;margin:0 auto;padding:2px 0}
720
839
  .dsh-rt-marker{text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}
840
+ .dsh-rt-marker-hint{text-align:center;color:var(--dsw-alias-state-warning-primary);font-size:11px;line-height:16px;margin-top:2px}
721
841
  .dsh-rt-reference{width:min(525px,82%);box-sizing:border-box;border:1px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated);border-radius:10px;padding:2px 12px}
722
842
  .dsh-rt-reference summary{color:var(--dsw-alias-label-caption);cursor:pointer;user-select:none;font-size:12px;line-height:22px;list-style:none;display:inline-flex;align-items:center;gap:6px;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
723
843
  .dsh-rt-reference summary::-webkit-details-marker{display:none}
@@ -736,7 +856,8 @@ return {
736
856
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
737
857
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
738
858
  /* ---- 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}
859
+
860
+ .dsh-rt-view{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;flex:1 1 0%;min-height:0;overflow:hidden;padding:12px 16px 0}
740
861
  .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
741
862
  .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
742
863
  .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}
@@ -773,7 +894,126 @@ return {
773
894
  .dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
774
895
  .dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
775
896
  .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}
897
+ .dsh-rt-fork-list{overflow-y:auto;flex:1;min-height:0;position:relative}
898
+ .dsh-rt-fork-spine-label{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;flex:none}
899
+ .dsh-rt-fork-row{position:absolute;left:0;right:0;box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;padding:6px 8px;border-radius:8px}
900
+ .dsh-rt-fork-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
901
+ .dsh-rt-fork-icon{flex:none;width:22px;height:22px;display:inline-flex;justify-content:center;align-items:center;font-size:13px;color:var(--dsw-alias-label-secondary)}
902
+ .dsh-rt-fork-boundary .dsh-rt-fork-icon{color:var(--dsw-alias-state-warning-primary)}
903
+ .dsh-rt-fork-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
904
+ .dsh-rt-fork-line{display:flex;align-items:baseline;gap:8px;min-width:0}
905
+ .dsh-rt-fork-label{font-size:12px;font-weight:500;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}
906
+ .dsh-rt-fork-seq{font-family:var(--dsw-font-mono);font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);flex:none}
907
+ .dsh-rt-fork-shadowed{font-size:11px;line-height:18px;color:var(--dsw-alias-state-warning-primary);flex:none}
908
+ .dsh-rt-fork-text{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
909
+ .dsh-rt-fork-boundary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated)}
910
+ .dsh-rt-fork-hist{display:flex;flex-direction:column;gap:4px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:8px;overflow-y:auto;flex:none}
911
+ .dsh-rt-fork-hist-row{display:flex;align-items:flex-start;gap:8px;padding:4px 8px;border-radius:8px}
912
+ .dsh-rt-fork-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
776
913
  `;
914
+ var JUMP_PAGE_BUDGET = 24;
915
+ function switchToViewTab(viewId) {
916
+ const ORDER = { chat: 0, trajectory: 1, retrace: 2, "retrace-fork": 3 };
917
+ const index = ORDER[viewId];
918
+ if (index === void 0) {
919
+ console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
920
+ return;
921
+ }
922
+ const buttons = [...document.querySelectorAll('[role="tablist"]')].filter((tablist) => tablist.getBoundingClientRect().width > 0).flatMap((tablist) => [...tablist.querySelectorAll('[role="tab"]')]);
923
+ const button = buttons[index];
924
+ if (!button) {
925
+ console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
926
+ return;
927
+ }
928
+ button.click();
929
+ }
930
+ function nodeCountOf(nodes) {
931
+ if (!nodes) return 0;
932
+ if (typeof nodes.size === "number") return nodes.size;
933
+ return [...nodes.values()].length;
934
+ }
935
+ function waitForElement(selector, frames) {
936
+ return new Promise((resolve) => {
937
+ let remaining = frames;
938
+ const probe = () => {
939
+ const el = document.querySelector(selector);
940
+ if (el !== null) return resolve(el);
941
+ if (remaining-- <= 0) return resolve(null);
942
+ requestAnimationFrame(probe);
943
+ };
944
+ requestAnimationFrame(probe);
945
+ });
946
+ }
947
+ function flashKey(key) {
948
+ const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
949
+ if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
950
+ const tag = document.createElement("style");
951
+ tag.dataset.pluginCss = id;
952
+ 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)}}`;
953
+ document.head.appendChild(tag);
954
+ setTimeout(() => tag.remove(), 3400);
955
+ }
956
+ async function jumpToAnchor(store, anchorSeq) {
957
+ switchToViewTab("chat");
958
+ if (!store || typeof store.loadOlder !== "function" || typeof store.getSnapshot !== "function") return;
959
+ const keyOfSeq = (seq) => {
960
+ const nodes = store.getSnapshot()?.chat?.nodes;
961
+ if (!nodes) return null;
962
+ for (const node of nodes.values()) {
963
+ if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
964
+ }
965
+ return null;
966
+ };
967
+ let key = keyOfSeq(anchorSeq);
968
+ let pages = 0;
969
+ while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
970
+ const before = nodeCountOf(store.getSnapshot()?.chat?.nodes);
971
+ await store.loadOlder();
972
+ const after = nodeCountOf(store.getSnapshot()?.chat?.nodes);
973
+ pages += 1;
974
+ if (after === before) break;
975
+ key = keyOfSeq(anchorSeq);
976
+ }
977
+ if (key === null) {
978
+ await new Promise((resolve) => setTimeout(resolve, 60));
979
+ key = keyOfSeq(anchorSeq);
980
+ }
981
+ if (key === null) {
982
+ console.warn(`[dsh-retrace] jump target seq ${anchorSeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
983
+ return;
984
+ }
985
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
986
+ if (el === null) return;
987
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
988
+ flashKey(key);
989
+ }
990
+ var NODE_ICONS = { "user/message": "\u{1F464}", "assistant/message": "\u{1F916}", "tool/result": "\u{1F527}" };
991
+ function nodeTypeLabel(type, t) {
992
+ if (type === "user/message") return t("fork.node.user");
993
+ if (type === "assistant/message") return t("fork.node.assistant");
994
+ if (type === "tool/result") return t("fork.node.tool");
995
+ return type;
996
+ }
997
+ function bindListHeight(listEl) {
998
+ if (!listEl) return () => {
999
+ };
1000
+ const measure = () => {
1001
+ const top = listEl.getBoundingClientRect().top;
1002
+ const height = Math.max(120, window.innerHeight - top - 16);
1003
+ if (Math.abs(listEl.clientHeight - height) > 4) {
1004
+ listEl.style.flex = "none";
1005
+ listEl.style.height = `${height}px`;
1006
+ }
1007
+ };
1008
+ measure();
1009
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(measure) : null;
1010
+ ro?.observe(document.body);
1011
+ window.addEventListener("resize", measure);
1012
+ return () => {
1013
+ ro?.disconnect();
1014
+ window.removeEventListener("resize", measure);
1015
+ };
1016
+ }
777
1017
  function ensureStyle() {
778
1018
  if (typeof document === "undefined") return () => {
779
1019
  };
@@ -841,63 +1081,7 @@ return {
841
1081
  if (projected === void 0) refresh();
842
1082
  refreshGit();
843
1083
  }, []);
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
- }
1084
+ const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq);
901
1085
  const requestPreview = (record) => {
902
1086
  setPreviewScope("both");
903
1087
  setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
@@ -930,6 +1114,10 @@ return {
930
1114
  const ROW_H = 64;
931
1115
  const list = versions ?? [];
932
1116
  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));
1117
+ (0, import_react.useEffect)(() => {
1118
+ if (list.length === 0) return void 0;
1119
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-timeline-list"));
1120
+ }, [list.length]);
933
1121
  return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
934
1122
  (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
935
1123
  (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
@@ -967,7 +1155,7 @@ return {
967
1155
  t,
968
1156
  top: list.indexOf(record) * ROW_H,
969
1157
  onPreview: () => requestPreview(record),
970
- onTrajectory: () => actions?.setView?.("trajectory"),
1158
+ onTrajectory: () => switchToViewTab("trajectory"),
971
1159
  onJump: () => jump(record.boundarySeq)
972
1160
  }))
973
1161
  ])
@@ -1050,6 +1238,135 @@ return {
1050
1238
  ])
1051
1239
  ]);
1052
1240
  }
1241
+ function ForkView({ sessionId, useProjection, t, actions, store }) {
1242
+ const [fork, setFork] = (0, import_react.useState)(null);
1243
+ const [loading, setLoading] = (0, import_react.useState)(false);
1244
+ const [error, setError] = (0, import_react.useState)(null);
1245
+ const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
1246
+ const projected = typeof useProjection === "function" ? useProjection("retrace/forkmap") : void 0;
1247
+ const versions = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
1248
+ const markerBySeq = new Map((versions?.versions ?? []).map((v) => [v.boundarySeq, v.markerText]));
1249
+ (0, import_react.useEffect)(() => {
1250
+ if (projected && Array.isArray(projected.nodes) && Array.isArray(projected.boundaries)) setFork(projected);
1251
+ }, [projected]);
1252
+ const refresh = () => {
1253
+ setLoading(true);
1254
+ setError(null);
1255
+ timelineGet(`/forkmap?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
1256
+ if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "forkmap failed");
1257
+ setFork(result.value?.nodes ? result.value : null);
1258
+ }).catch((cause) => setError(cause?.message ?? "forkmap error")).finally(() => setLoading(false));
1259
+ };
1260
+ (0, import_react.useEffect)(() => {
1261
+ if (projected === void 0) refresh();
1262
+ }, []);
1263
+ const nodes = fork?.nodes ?? [];
1264
+ const boundaries = fork?.boundaries ?? [];
1265
+ const boundaryBySeq = new Map(boundaries.map((b) => [b.seq, b]));
1266
+ const spineSeqSet = new Set(nodes.map((n) => n.seq));
1267
+ const offSpine = boundaries.filter((b) => !spineSeqSet.has(b.seq));
1268
+ (0, import_react.useEffect)(() => {
1269
+ if (nodes.length === 0) return void 0;
1270
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-fork-list"));
1271
+ }, [nodes.length]);
1272
+ const ROW_H = 56;
1273
+ const visible = nodes.slice(
1274
+ Math.max(0, Math.floor(scrollTop / ROW_H) - 2),
1275
+ Math.min(nodes.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2)
1276
+ );
1277
+ return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
1278
+ (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
1279
+ (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("fork.title")),
1280
+ (0, import_react.createElement)("button", {
1281
+ key: "refresh",
1282
+ type: "button",
1283
+ className: "dsh-rt-chip",
1284
+ onClick: refresh
1285
+ }, t("fork.refresh"))
1286
+ ]),
1287
+ error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
1288
+ loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1289
+ !loading && nodes.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("fork.empty")),
1290
+ nodes.length > 0 && (0, import_react.createElement)("div", { key: "spine", className: "dsh-rt-fork-spine-label" }, t("fork.spine")),
1291
+ nodes.length > 0 && (0, import_react.createElement)("div", {
1292
+ key: "list",
1293
+ className: "dsh-rt-fork-list",
1294
+ onScroll: (event) => setScrollTop(event.target.scrollTop)
1295
+ }, [
1296
+ (0, import_react.createElement)("div", { key: "spacer", style: { height: `${nodes.length * ROW_H}px`, position: "relative" } }, [
1297
+ visible.map((node) => (0, import_react.createElement)(ForkRow, {
1298
+ key: node.seq,
1299
+ node,
1300
+ boundary: boundaryBySeq.get(node.seq),
1301
+ markerText: markerBySeq.get(node.seq),
1302
+ t,
1303
+ top: nodes.indexOf(node) * ROW_H,
1304
+ onJump: () => jumpToAnchor(store, node.seq)
1305
+ }))
1306
+ ])
1307
+ ]),
1308
+ offSpine.length > 0 && (0, import_react.createElement)("div", { key: "hist", className: "dsh-rt-fork-hist" }, [
1309
+ (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-fork-spine-label" }, t("fork.histTitle")),
1310
+ offSpine.map((boundary) => (0, import_react.createElement)(ForkHistRow, {
1311
+ key: boundary.seq,
1312
+ boundary,
1313
+ markerText: markerBySeq.get(boundary.seq),
1314
+ t,
1315
+ onJump: () => jumpToAnchor(store, boundary.seq)
1316
+ }))
1317
+ ])
1318
+ ]);
1319
+ }
1320
+ function ForkRow({ node, boundary, markerText, t, top, onJump }) {
1321
+ const isBoundary = boundary !== void 0;
1322
+ const icon = isBoundary ? KIND_ICONS[boundary.kind] ?? "\u2022" : NODE_ICONS[node.type] ?? "\u2022";
1323
+ const label = isBoundary ? kindLabel(boundary.kind, t) : nodeTypeLabel(node.type, t);
1324
+ return (0, import_react.createElement)("div", {
1325
+ className: `dsh-rt-fork-row${isBoundary ? " dsh-rt-fork-boundary" : ""}`,
1326
+ style: { top: `${top}px` }
1327
+ }, [
1328
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, icon),
1329
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1330
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1331
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, label),
1332
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${node.seq}`),
1333
+ isBoundary && boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1334
+ "span",
1335
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1336
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1337
+ )
1338
+ ]),
1339
+ isBoundary && markerText && (0, import_react.createElement)(
1340
+ "div",
1341
+ { key: "text", className: "dsh-rt-fork-text" },
1342
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1343
+ )
1344
+ ]),
1345
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1346
+ ]);
1347
+ }
1348
+ function ForkHistRow({ boundary, markerText, t, onJump }) {
1349
+ return (0, import_react.createElement)("div", { className: "dsh-rt-fork-hist-row" }, [
1350
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, KIND_ICONS[boundary.kind] ?? "\u2022"),
1351
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1352
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1353
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, kindLabel(boundary.kind, t)),
1354
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${boundary.seq}`),
1355
+ boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1356
+ "span",
1357
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1358
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1359
+ )
1360
+ ]),
1361
+ markerText && (0, import_react.createElement)(
1362
+ "div",
1363
+ { key: "text", className: "dsh-rt-fork-text" },
1364
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1365
+ )
1366
+ ]),
1367
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1368
+ ]);
1369
+ }
1053
1370
  function apply(ctx) {
1054
1371
  const disposeStyle = ensureStyle();
1055
1372
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
@@ -1057,9 +1374,12 @@ return {
1057
1374
  const t = ctx.locale.bind(NS);
1058
1375
  const conversationEvents = ctx.get("conversationEvents");
1059
1376
  if (conversationEvents) {
1060
- conversationEvents.register(userActionsDefinition);
1061
- conversationEvents.register(userReferenceDefinition);
1062
- conversationEvents.register(recallMarkerDefinition);
1377
+ const disposeDefinitions = [
1378
+ conversationEvents.register(userActionsDefinition),
1379
+ conversationEvents.register(userReferenceDefinition),
1380
+ conversationEvents.register(recallMarkerDefinition)
1381
+ ];
1382
+ ctx.effect(() => () => disposeDefinitions.forEach((dispose) => dispose()), "dsh-retrace: conversation definitions");
1063
1383
  }
1064
1384
  ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
1065
1385
  name: "conversation.chat.assistant-actions",
@@ -1093,6 +1413,17 @@ return {
1093
1413
  store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1094
1414
  })
1095
1415
  }, RetraceView));
1416
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1417
+ name: "conversation.view",
1418
+ id: "retrace-fork",
1419
+ order: 30,
1420
+ locale: NS,
1421
+ label: () => t("view.fork"),
1422
+ inject: (sessionId, actions) => ({
1423
+ actions,
1424
+ store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1425
+ })
1426
+ }, ForkView));
1096
1427
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1097
1428
  name: "settings.general.item",
1098
1429
  id: "retrace",