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.
@@ -63,6 +63,8 @@ var zh = {
63
63
  "marker.regenerate": "\u5DF2\u91CD\u65B0\u751F\u6210\u56DE\u590D",
64
64
  "marker.originalLabel": "\u539F\u8F93\u5165",
65
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",
66
+ "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",
67
+ "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",
66
68
  "options.title": "\u6D88\u606F\u7F16\u8F91\u63D2\u4EF6",
67
69
  "options.showOriginalInput": "\u7F16\u8F91\u540E\u663E\u793A\u539F\u63D0\u95EE\u5BF9\u7167",
68
70
  "options.editFromScratch": "\u7F16\u8F91\u540E\u4ECE\u65B0\u5BF9\u8BDD\u5F00\u59CB\uFF08\u9690\u85CF\u6B64\u524D\u7684\u6D88\u606F\uFF0C\u9ED8\u8BA4\u5173\uFF09",
@@ -120,7 +122,17 @@ var zh = {
120
122
  "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
123
  "timeline.gitInitConfirm": "\u786E\u5B9A\u8981\u521D\u59CB\u5316 git \u5417\uFF1F",
122
124
  "error.generic": "\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
123
- "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C"
125
+ "error.busy": "\u8BF7\u5148\u505C\u6B62\u5F53\u524D\u56DE\u590D\u518D\u64CD\u4F5C",
126
+ "view.fork": "\u5206\u53C9",
127
+ "fork.title": "\u5206\u53C9\u56FE",
128
+ "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",
129
+ "fork.refresh": "\u5237\u65B0",
130
+ "fork.spine": "\u5F53\u524D\u8DEF\u5F84",
131
+ "fork.shadowed": "\u88AB\u906E\u853D {count} \u4E2A\u8282\u70B9",
132
+ "fork.node.user": "\u7528\u6237\u6D88\u606F",
133
+ "fork.node.assistant": "\u52A9\u624B\u56DE\u590D",
134
+ "fork.node.tool": "\u5DE5\u5177\u7ED3\u679C",
135
+ "fork.histTitle": "\u5386\u53F2\u5206\u53C9\u70B9"
124
136
  };
125
137
  var en = {
126
138
  "action.edit": "Edit",
@@ -138,6 +150,8 @@ var en = {
138
150
  "marker.regenerate": "Reply regenerated",
139
151
  "marker.originalLabel": "Original input",
140
152
  "marker.referenceHint": "Click to expand the original input (reference only, never sent to the model)",
153
+ "marker.degradedHint": "This operation spans a large part of the conversation; content stays visible to protect your history (the log is intact).",
154
+ "marker.unionHint": 'About {count}% of the history is hidden in total; disable "Hide shadowed messages" in Settings \u2192 General to review the full history.',
141
155
  "options.title": "Message editor plugin",
142
156
  "options.showOriginalInput": "Show the original input after editing",
143
157
  "options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
@@ -195,7 +209,17 @@ var en = {
195
209
  "timeline.gitInitDesc": "Runs git init in the workspace (adds a minimal .gitignore and a baseline commit); rollback will prefer git.",
196
210
  "timeline.gitInitConfirm": "Initialize git in this workspace?",
197
211
  "error.generic": "Operation failed; please try again",
198
- "error.busy": "Stop the current reply before recalling or editing"
212
+ "error.busy": "Stop the current reply before recalling or editing",
213
+ "view.fork": "Fork",
214
+ "fork.title": "Fork map",
215
+ "fork.empty": "No forks yet. Recall / edit / regenerate / restore fork the path here.",
216
+ "fork.refresh": "Refresh",
217
+ "fork.spine": "Current path",
218
+ "fork.shadowed": "{count} shadowed nodes",
219
+ "fork.node.user": "User message",
220
+ "fork.node.assistant": "Assistant reply",
221
+ "fork.node.tool": "Tool result",
222
+ "fork.histTitle": "Historical fork points"
199
223
  };
200
224
  var SURFACE_TYPES = /* @__PURE__ */ new Set(["user/message", "assistant/message", "tool/result"]);
201
225
  function isReplacementSurfaceEvent(event) {
@@ -351,23 +375,32 @@ var recallMarkerDefinition = {
351
375
  kind: "recall-marker",
352
376
  target: "chat",
353
377
  match: (event) => {
354
- if (event.type !== "assistant/message" || !isReplacementSurfaceEvent(event)) return null;
355
- const id = event.data?.message?.id;
356
- if (!isMarkerId(id)) return null;
357
- return { id: `marker:${id}`, role: "start" };
378
+ if (!isReplacementSurfaceEvent(event)) return null;
379
+ if (event.type === "assistant/message") {
380
+ const id = event.data?.message?.id;
381
+ if (!isMarkerId(id)) return null;
382
+ return { id: `marker:${id}`, role: "start" };
383
+ }
384
+ if (event.type === "user/message" && isCompactCheckpoint(event.data?.source)) {
385
+ return { id: `marker:compact:${event.seq}`, role: "start" };
386
+ }
387
+ return null;
358
388
  },
359
389
  start: (_context, match) => {
360
390
  const event = match.event;
361
- const id = String(event.data.message.id);
362
- const legacy = isLegacyMarkerId(id);
391
+ const compact = event.type === "user/message" && isCompactCheckpoint(event.data?.source);
392
+ const id = compact ? "" : String(event.data.message.id);
393
+ const legacy = !compact && isLegacyMarkerId(id);
363
394
  return {
364
395
  seq: event.seq,
365
396
  time: event.time,
366
- op: markerOpFromId(id),
397
+ op: compact ? "compaction" : markerOpFromId(id),
367
398
  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.
399
+ compact,
400
+ // Legacy/compact markers never hide: their shadowed range is kept for
401
+ // the action-row suppression check (useShadowed) but empty for legacy
402
+ // so no row is hidden and no action row is suppressed for legacy
403
+ // markers (rename must never make visible content disappear).
371
404
  shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
372
405
  targetSeq: event.data?.editor?.targetSeq,
373
406
  text: event.data?.editor?.text
@@ -379,6 +412,9 @@ var recallMarkerDefinition = {
379
412
  return chatNodeLike(context, "recall-marker", context.state.seq, context.state);
380
413
  }
381
414
  };
415
+ function isCompactCheckpoint(source) {
416
+ return Boolean(source) && source.kind === "plugin" && source.plugin === "compact";
417
+ }
382
418
  function textOf(content) {
383
419
  if (!Array.isArray(content)) return "";
384
420
  return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
@@ -393,41 +429,111 @@ function useMessageSeq(useSession, messageId) {
393
429
  return void 0;
394
430
  });
395
431
  }
396
- function useShadowed(useSession, seq) {
432
+ var SHADOW_SAFETY_RATIO = 0.4;
433
+ function hiddenKeysFor(shadowedSeqs, nodes) {
434
+ if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
435
+ const hidden = new Set(shadowedSeqs);
436
+ const keys = [];
437
+ for (const node of nodes.values()) {
438
+ if (node.kind === "recall-marker") continue;
439
+ if (node.kind === "turn-tail") {
440
+ const closingSeq = node.data?.closing?.finalNode?.seq;
441
+ if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
442
+ continue;
443
+ }
444
+ if (node.kind === "tool-call") {
445
+ const resultSeq = node.data?.root?.seq;
446
+ if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
447
+ continue;
448
+ }
449
+ if (typeof node.anchorSeq === "number") {
450
+ const anchored = node.anchorSeq % 1 === 0 ? node.anchorSeq : Math.ceil(node.anchorSeq);
451
+ if (hidden.has(anchored)) keys.push(node.key);
452
+ }
453
+ }
454
+ return keys.length === 0 ? null : keys;
455
+ }
456
+ var EMPTY_HIDE_PLAN = Object.freeze({
457
+ hiddenFor: () => null,
458
+ planFor: () => null,
459
+ unionRatio: 0,
460
+ firstMarkerKey: null
461
+ });
462
+ var PLUGIN_PSEUDO_KINDS = /* @__PURE__ */ new Set(["user-actions", "retrace-reference", "recall-marker"]);
463
+ function realRowCount(nodes) {
464
+ let count = 0;
465
+ for (const node of nodes.values()) {
466
+ if (typeof node.anchorSeq === "number" && !PLUGIN_PSEUDO_KINDS.has(node.kind)) count += 1;
467
+ }
468
+ return count;
469
+ }
470
+ var hidePlanCacheSnapshot = null;
471
+ var hidePlanCacheValue = null;
472
+ function useMarkerHidePlan(useSession) {
473
+ return useSession((snapshot) => {
474
+ if (hidePlanCacheSnapshot === snapshot) return hidePlanCacheValue;
475
+ const nodes = snapshot.chat.nodes;
476
+ const rowCount = realRowCount(nodes);
477
+ const markers = [];
478
+ for (const node of nodes.values()) {
479
+ if (node.kind === "recall-marker" && !node.data?.compact) markers.push(node);
480
+ }
481
+ if (markers.length === 0) {
482
+ hidePlanCacheSnapshot = snapshot;
483
+ hidePlanCacheValue = EMPTY_HIDE_PLAN;
484
+ return hidePlanCacheValue;
485
+ }
486
+ const plans = /* @__PURE__ */ new Map();
487
+ const union = /* @__PURE__ */ new Set();
488
+ for (const marker of markers) {
489
+ const keys = hiddenKeysFor(marker.data?.shadowedSeqs, nodes);
490
+ const degraded = keys !== null && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO;
491
+ plans.set(marker.key, { keys: degraded ? null : keys, degraded });
492
+ if (keys !== null) for (const key of keys) union.add(key);
493
+ }
494
+ hidePlanCacheSnapshot = snapshot;
495
+ hidePlanCacheValue = {
496
+ planFor: (key) => plans.get(key) ?? null,
497
+ hiddenFor: (key) => plans.get(key)?.keys ?? null,
498
+ unionRatio: rowCount > 0 ? union.size / rowCount : 0,
499
+ firstMarkerKey: markers[0].key
500
+ };
501
+ return hidePlanCacheValue;
502
+ });
503
+ }
504
+ function rowHiddenByKey(snapshot, rowKey) {
505
+ if (rowKey === void 0 || rowKey === null) return false;
506
+ const nodes = snapshot.chat.nodes;
507
+ const rowCount = realRowCount(nodes);
508
+ for (const node of nodes.values()) {
509
+ if (node.kind !== "recall-marker" || node.data?.compact) continue;
510
+ const keys = hiddenKeysFor(node.data?.shadowedSeqs, nodes);
511
+ if (keys === null) continue;
512
+ if (rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) continue;
513
+ if (keys.includes(rowKey)) return true;
514
+ }
515
+ return false;
516
+ }
517
+ function useSeqHidden(useSession, seq) {
397
518
  return useSession((snapshot) => {
398
519
  if (seq === void 0 || seq === null) return false;
399
520
  for (const node of snapshot.chat.nodes.values()) {
400
- if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
401
- return true;
521
+ if (node.kind !== "recall-marker" && typeof node.anchorSeq === "number" && node.anchorSeq === seq) {
522
+ return rowHiddenByKey(snapshot, node.key);
402
523
  }
403
524
  }
404
525
  return false;
405
526
  });
406
527
  }
407
- var SHADOW_SAFETY_RATIO = 0.4;
408
- function useHiddenKeys(useSession, shadowedSeqs) {
528
+ function useShadowed(useSession, seq) {
409
529
  return useSession((snapshot) => {
410
- if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
411
- const hidden = new Set(shadowedSeqs);
412
- const keys = [];
413
- let rowCount = 0;
530
+ if (seq === void 0 || seq === null) return false;
414
531
  for (const node of snapshot.chat.nodes.values()) {
415
- if (typeof node.anchorSeq === "number") rowCount += 1;
416
- if (node.kind === "recall-marker") continue;
417
- if (node.kind === "turn-tail") {
418
- const closingSeq = node.data?.closing?.finalNode?.seq;
419
- if (typeof closingSeq === "number" && hidden.has(closingSeq)) keys.push(node.key);
420
- continue;
421
- }
422
- if (node.kind === "tool-call") {
423
- const resultSeq = node.data?.root?.seq;
424
- if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
425
- continue;
532
+ if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
533
+ return true;
426
534
  }
427
- if (typeof node.anchorSeq === "number" && hidden.has(node.anchorSeq)) keys.push(node.key);
428
535
  }
429
- if (keys.length > 0 && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) return null;
430
- return keys.length === 0 ? null : keys;
536
+ return false;
431
537
  });
432
538
  }
433
539
  function useMarkerDismissed(useSession, markerSeq, op) {
@@ -464,10 +570,11 @@ function useEditReference(useSession, mySeq) {
464
570
  }
465
571
  function AssistantActions({ messageId, sessionId, useSession, t }) {
466
572
  const seq = useMessageSeq(useSession, messageId);
573
+ const hidden = useSeqHidden(useSession, seq);
467
574
  const shadowed = useShadowed(useSession, seq);
468
575
  const [busy, setBusy] = (0, import_react.useState)(false);
469
576
  const [failure, setFailure] = (0, import_react.useState)(null);
470
- if (shadowed || seq === void 0) return null;
577
+ if (hidden || shadowed || seq === void 0) return null;
471
578
  const run = (op) => {
472
579
  setBusy(true);
473
580
  setFailure(null);
@@ -510,11 +617,11 @@ function AssistantActions({ messageId, sessionId, useSession, t }) {
510
617
  }
511
618
  function ReferenceRow({ node, useSession, t }) {
512
619
  const { seq, messageId } = node.data;
513
- const shadowed = useShadowed(useSession, seq);
620
+ const hidden = useSeqHidden(useSession, seq);
514
621
  const markerRef = useEditReference(useSession, seq);
515
622
  const referenceText = editReferences.get(messageId) ?? markerRef;
516
623
  const config = useConfig();
517
- if (shadowed) return null;
624
+ if (hidden) return null;
518
625
  if (referenceText === null || !config.showOriginalInput) return null;
519
626
  return (0, import_react.createElement)("div", { className: "dsh-rt-user-row" }, [
520
627
  (0, import_react.createElement)("details", { className: "dsh-rt-reference" }, [
@@ -529,12 +636,13 @@ function ReferenceRow({ node, useSession, t }) {
529
636
  }
530
637
  function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
531
638
  const { seq, messageId, content } = node.data;
639
+ const hidden = useSeqHidden(useSession, seq);
532
640
  const shadowed = useShadowed(useSession, seq);
533
641
  const [editing, setEditing] = (0, import_react.useState)(false);
534
642
  const [draft, setDraft] = (0, import_react.useState)("");
535
643
  const [busy, setBusy] = (0, import_react.useState)(false);
536
644
  const [failure, setFailure] = (0, import_react.useState)(null);
537
- if (shadowed) return null;
645
+ if (hidden || shadowed) return null;
538
646
  const openEditor = () => {
539
647
  setDraft(textOf(content));
540
648
  setFailure(null);
@@ -627,15 +735,26 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
627
735
  ]);
628
736
  }
629
737
  function RecallMarkerRow({ node, useSession, t }) {
630
- const { seq, op, shadowedSeqs, legacy } = node.data;
738
+ const { seq, op, shadowedSeqs, legacy, compact } = node.data;
739
+ if (compact) return null;
631
740
  const dismissed = useMarkerDismissed(useSession, seq, op);
632
- const hiddenKeys = legacy || !getConfig().hideShadowed ? null : useHiddenKeys(useSession, shadowedSeqs);
741
+ const hidePlan = useMarkerHidePlan(useSession);
742
+ const plan = hidePlan.planFor(node.key);
743
+ const hiddenKeys = legacy || !getConfig().hideShadowed ? null : plan?.keys ?? null;
633
744
  const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
634
745
  const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
635
746
  const label = op === "recall" ? count > 1 ? t("marker.recallMany", { count }) : t("marker.recallOne") : op === "regenerate" ? t("marker.regenerate") : t("marker.edit");
747
+ const degradedHint = !legacy && plan?.degraded === true ? (0, import_react.createElement)("div", { key: "degraded", className: "dsh-rt-marker-hint" }, t("marker.degradedHint")) : null;
748
+ const unionHint = !legacy && hidePlan.firstMarkerKey === node.key && hidePlan.unionRatio > SHADOW_SAFETY_RATIO ? (0, import_react.createElement)(
749
+ "div",
750
+ { key: "union", className: "dsh-rt-marker-hint" },
751
+ t("marker.unionHint", { count: Math.round(hidePlan.unionRatio * 100) })
752
+ ) : null;
636
753
  return (0, import_react.createElement)("div", { className: "dsh-rt-marker-block", "data-dismissed": dismissed || void 0 }, [
637
754
  css !== null && (0, import_react.createElement)("style", { key: "hide", dangerouslySetInnerHTML: { __html: css } }),
638
- !dismissed && (0, import_react.createElement)("div", { key: "label", className: "dsh-rt-marker", role: "status" }, label)
755
+ !dismissed && (0, import_react.createElement)("div", { key: "label", className: "dsh-rt-marker", role: "status" }, label),
756
+ !dismissed && degradedHint,
757
+ !dismissed && unionHint
639
758
  ]);
640
759
  }
641
760
  function OptionsRow({ t }) {
@@ -712,6 +831,7 @@ var CSS = `
712
831
  .dsh-rt-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;max-width:min(525px,82%)}
713
832
  .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}
714
833
  .dsh-rt-marker{text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}
834
+ .dsh-rt-marker-hint{text-align:center;color:var(--dsw-alias-state-warning-primary);font-size:11px;line-height:16px;margin-top:2px}
715
835
  .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}
716
836
  .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}
717
837
  .dsh-rt-reference summary::-webkit-details-marker{display:none}
@@ -730,7 +850,8 @@ var CSS = `
730
850
  .dsh-rt-retention-input:focus{box-shadow:0 0 0 2px var(--dsw-alias-state-business-primary)}
731
851
  .dsh-rt-option input{accent-color:var(--dsw-alias-state-business-primary)}
732
852
  /* ---- 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}
853
+
854
+ .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}
734
855
  .dsh-rt-timeline-head{display:flex;align-items:center;gap:8px;flex:none}
735
856
  .dsh-rt-timeline-title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:20px;flex:1}
736
857
  .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}
@@ -767,7 +888,126 @@ var CSS = `
767
888
  .dsh-rt-modal-buttons{display:flex;justify-content:flex-end;gap:8px;flex:none}
768
889
  .dsh-rt-confirm{background:var(--dsw-alias-state-error-primary)}
769
890
  .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}
891
+ .dsh-rt-fork-list{overflow-y:auto;flex:1;min-height:0;position:relative}
892
+ .dsh-rt-fork-spine-label{color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;flex:none}
893
+ .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}
894
+ .dsh-rt-fork-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
895
+ .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)}
896
+ .dsh-rt-fork-boundary .dsh-rt-fork-icon{color:var(--dsw-alias-state-warning-primary)}
897
+ .dsh-rt-fork-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
898
+ .dsh-rt-fork-line{display:flex;align-items:baseline;gap:8px;min-width:0}
899
+ .dsh-rt-fork-label{font-size:12px;font-weight:500;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}
900
+ .dsh-rt-fork-seq{font-family:var(--dsw-font-mono);font-size:11px;line-height:18px;color:var(--dsw-alias-label-caption);flex:none}
901
+ .dsh-rt-fork-shadowed{font-size:11px;line-height:18px;color:var(--dsw-alias-state-warning-primary);flex:none}
902
+ .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}
903
+ .dsh-rt-fork-boundary{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-elevated)}
904
+ .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}
905
+ .dsh-rt-fork-hist-row{display:flex;align-items:flex-start;gap:8px;padding:4px 8px;border-radius:8px}
906
+ .dsh-rt-fork-hist-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
770
907
  `;
908
+ var JUMP_PAGE_BUDGET = 24;
909
+ function switchToViewTab(viewId) {
910
+ const ORDER = { chat: 0, trajectory: 1, retrace: 2, "retrace-fork": 3 };
911
+ const index = ORDER[viewId];
912
+ if (index === void 0) {
913
+ console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
914
+ return;
915
+ }
916
+ const buttons = [...document.querySelectorAll('[role="tablist"]')].filter((tablist) => tablist.getBoundingClientRect().width > 0).flatMap((tablist) => [...tablist.querySelectorAll('[role="tab"]')]);
917
+ const button = buttons[index];
918
+ if (!button) {
919
+ console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
920
+ return;
921
+ }
922
+ button.click();
923
+ }
924
+ function nodeCountOf(nodes) {
925
+ if (!nodes) return 0;
926
+ if (typeof nodes.size === "number") return nodes.size;
927
+ return [...nodes.values()].length;
928
+ }
929
+ function waitForElement(selector, frames) {
930
+ return new Promise((resolve) => {
931
+ let remaining = frames;
932
+ const probe = () => {
933
+ const el = document.querySelector(selector);
934
+ if (el !== null) return resolve(el);
935
+ if (remaining-- <= 0) return resolve(null);
936
+ requestAnimationFrame(probe);
937
+ };
938
+ requestAnimationFrame(probe);
939
+ });
940
+ }
941
+ function flashKey(key) {
942
+ const id = `dsh-rt-jump-${key.replace(/[^a-z0-9]/gi, "-")}`;
943
+ if (document.querySelector(`style[data-plugin-css="${id}"]`)) return;
944
+ const tag = document.createElement("style");
945
+ tag.dataset.pluginCss = id;
946
+ 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)}}`;
947
+ document.head.appendChild(tag);
948
+ setTimeout(() => tag.remove(), 3400);
949
+ }
950
+ async function jumpToAnchor(store, anchorSeq) {
951
+ switchToViewTab("chat");
952
+ if (!store || typeof store.loadOlder !== "function" || typeof store.getSnapshot !== "function") return;
953
+ const keyOfSeq = (seq) => {
954
+ const nodes = store.getSnapshot()?.chat?.nodes;
955
+ if (!nodes) return null;
956
+ for (const node of nodes.values()) {
957
+ if (typeof node.anchorSeq === "number" && node.anchorSeq === seq) return node.key;
958
+ }
959
+ return null;
960
+ };
961
+ let key = keyOfSeq(anchorSeq);
962
+ let pages = 0;
963
+ while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
964
+ const before = nodeCountOf(store.getSnapshot()?.chat?.nodes);
965
+ await store.loadOlder();
966
+ const after = nodeCountOf(store.getSnapshot()?.chat?.nodes);
967
+ pages += 1;
968
+ if (after === before) break;
969
+ key = keyOfSeq(anchorSeq);
970
+ }
971
+ if (key === null) {
972
+ await new Promise((resolve) => setTimeout(resolve, 60));
973
+ key = keyOfSeq(anchorSeq);
974
+ }
975
+ if (key === null) {
976
+ console.warn(`[dsh-retrace] jump target seq ${anchorSeq} not reached within ${JUMP_PAGE_BUDGET} pages`);
977
+ return;
978
+ }
979
+ const el = await waitForElement(`[data-chat-anchor-key=${JSON.stringify(key)}]`, 90);
980
+ if (el === null) return;
981
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
982
+ flashKey(key);
983
+ }
984
+ var NODE_ICONS = { "user/message": "\u{1F464}", "assistant/message": "\u{1F916}", "tool/result": "\u{1F527}" };
985
+ function nodeTypeLabel(type, t) {
986
+ if (type === "user/message") return t("fork.node.user");
987
+ if (type === "assistant/message") return t("fork.node.assistant");
988
+ if (type === "tool/result") return t("fork.node.tool");
989
+ return type;
990
+ }
991
+ function bindListHeight(listEl) {
992
+ if (!listEl) return () => {
993
+ };
994
+ const measure = () => {
995
+ const top = listEl.getBoundingClientRect().top;
996
+ const height = Math.max(120, window.innerHeight - top - 16);
997
+ if (Math.abs(listEl.clientHeight - height) > 4) {
998
+ listEl.style.flex = "none";
999
+ listEl.style.height = `${height}px`;
1000
+ }
1001
+ };
1002
+ measure();
1003
+ const ro = typeof ResizeObserver === "function" ? new ResizeObserver(measure) : null;
1004
+ ro?.observe(document.body);
1005
+ window.addEventListener("resize", measure);
1006
+ return () => {
1007
+ ro?.disconnect();
1008
+ window.removeEventListener("resize", measure);
1009
+ };
1010
+ }
771
1011
  function ensureStyle() {
772
1012
  if (typeof document === "undefined") return () => {
773
1013
  };
@@ -835,63 +1075,7 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
835
1075
  if (projected === void 0) refresh();
836
1076
  refreshGit();
837
1077
  }, []);
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
- }
1078
+ const jump = (boundarySeq) => jumpToAnchor(store, boundarySeq);
895
1079
  const requestPreview = (record) => {
896
1080
  setPreviewScope("both");
897
1081
  setPreview({ versionId: record.versionId, kind: record.kind, boundarySeq: record.boundarySeq, data: null, error: null });
@@ -924,6 +1108,10 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
924
1108
  const ROW_H = 64;
925
1109
  const list = versions ?? [];
926
1110
  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));
1111
+ (0, import_react.useEffect)(() => {
1112
+ if (list.length === 0) return void 0;
1113
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-timeline-list"));
1114
+ }, [list.length]);
927
1115
  return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
928
1116
  (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
929
1117
  (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("timeline.title")),
@@ -961,7 +1149,7 @@ function RetraceView({ sessionId, useProjection, t, actions, store }) {
961
1149
  t,
962
1150
  top: list.indexOf(record) * ROW_H,
963
1151
  onPreview: () => requestPreview(record),
964
- onTrajectory: () => actions?.setView?.("trajectory"),
1152
+ onTrajectory: () => switchToViewTab("trajectory"),
965
1153
  onJump: () => jump(record.boundarySeq)
966
1154
  }))
967
1155
  ])
@@ -1044,6 +1232,135 @@ function PreviewBox({ preview, scope, setScope, busy, t, onConfirm, onCancel })
1044
1232
  ])
1045
1233
  ]);
1046
1234
  }
1235
+ function ForkView({ sessionId, useProjection, t, actions, store }) {
1236
+ const [fork, setFork] = (0, import_react.useState)(null);
1237
+ const [loading, setLoading] = (0, import_react.useState)(false);
1238
+ const [error, setError] = (0, import_react.useState)(null);
1239
+ const [scrollTop, setScrollTop] = (0, import_react.useState)(0);
1240
+ const projected = typeof useProjection === "function" ? useProjection("retrace/forkmap") : void 0;
1241
+ const versions = typeof useProjection === "function" ? useProjection("retrace/versions") : void 0;
1242
+ const markerBySeq = new Map((versions?.versions ?? []).map((v) => [v.boundarySeq, v.markerText]));
1243
+ (0, import_react.useEffect)(() => {
1244
+ if (projected && Array.isArray(projected.nodes) && Array.isArray(projected.boundaries)) setFork(projected);
1245
+ }, [projected]);
1246
+ const refresh = () => {
1247
+ setLoading(true);
1248
+ setError(null);
1249
+ timelineGet(`/forkmap?sessionId=${encodeURIComponent(sessionId)}`).then((result) => {
1250
+ if (!result || result.ok !== true) throw new Error(result?.error?.message ?? "forkmap failed");
1251
+ setFork(result.value?.nodes ? result.value : null);
1252
+ }).catch((cause) => setError(cause?.message ?? "forkmap error")).finally(() => setLoading(false));
1253
+ };
1254
+ (0, import_react.useEffect)(() => {
1255
+ if (projected === void 0) refresh();
1256
+ }, []);
1257
+ const nodes = fork?.nodes ?? [];
1258
+ const boundaries = fork?.boundaries ?? [];
1259
+ const boundaryBySeq = new Map(boundaries.map((b) => [b.seq, b]));
1260
+ const spineSeqSet = new Set(nodes.map((n) => n.seq));
1261
+ const offSpine = boundaries.filter((b) => !spineSeqSet.has(b.seq));
1262
+ (0, import_react.useEffect)(() => {
1263
+ if (nodes.length === 0) return void 0;
1264
+ return bindListHeight(document.querySelector(".dsh-rt-view .dsh-rt-fork-list"));
1265
+ }, [nodes.length]);
1266
+ const ROW_H = 56;
1267
+ const visible = nodes.slice(
1268
+ Math.max(0, Math.floor(scrollTop / ROW_H) - 2),
1269
+ Math.min(nodes.length, Math.ceil(scrollTop / ROW_H) + Math.ceil(640 / ROW_H) + 2)
1270
+ );
1271
+ return (0, import_react.createElement)("div", { className: "dsh-rt-view" }, [
1272
+ (0, import_react.createElement)("div", { key: "head", className: "dsh-rt-timeline-head" }, [
1273
+ (0, import_react.createElement)("span", { key: "title", className: "dsh-rt-timeline-title" }, t("fork.title")),
1274
+ (0, import_react.createElement)("button", {
1275
+ key: "refresh",
1276
+ type: "button",
1277
+ className: "dsh-rt-chip",
1278
+ onClick: refresh
1279
+ }, t("fork.refresh"))
1280
+ ]),
1281
+ error !== null && (0, import_react.createElement)("div", { key: "error", className: "dsh-rt-error" }, error),
1282
+ loading && (0, import_react.createElement)("div", { key: "loading", className: "dsh-rt-timeline-empty" }, t("timeline.loading")),
1283
+ !loading && nodes.length === 0 && (0, import_react.createElement)("div", { key: "empty", className: "dsh-rt-timeline-empty" }, t("fork.empty")),
1284
+ nodes.length > 0 && (0, import_react.createElement)("div", { key: "spine", className: "dsh-rt-fork-spine-label" }, t("fork.spine")),
1285
+ nodes.length > 0 && (0, import_react.createElement)("div", {
1286
+ key: "list",
1287
+ className: "dsh-rt-fork-list",
1288
+ onScroll: (event) => setScrollTop(event.target.scrollTop)
1289
+ }, [
1290
+ (0, import_react.createElement)("div", { key: "spacer", style: { height: `${nodes.length * ROW_H}px`, position: "relative" } }, [
1291
+ visible.map((node) => (0, import_react.createElement)(ForkRow, {
1292
+ key: node.seq,
1293
+ node,
1294
+ boundary: boundaryBySeq.get(node.seq),
1295
+ markerText: markerBySeq.get(node.seq),
1296
+ t,
1297
+ top: nodes.indexOf(node) * ROW_H,
1298
+ onJump: () => jumpToAnchor(store, node.seq)
1299
+ }))
1300
+ ])
1301
+ ]),
1302
+ offSpine.length > 0 && (0, import_react.createElement)("div", { key: "hist", className: "dsh-rt-fork-hist" }, [
1303
+ (0, import_react.createElement)("div", { key: "title", className: "dsh-rt-fork-spine-label" }, t("fork.histTitle")),
1304
+ offSpine.map((boundary) => (0, import_react.createElement)(ForkHistRow, {
1305
+ key: boundary.seq,
1306
+ boundary,
1307
+ markerText: markerBySeq.get(boundary.seq),
1308
+ t,
1309
+ onJump: () => jumpToAnchor(store, boundary.seq)
1310
+ }))
1311
+ ])
1312
+ ]);
1313
+ }
1314
+ function ForkRow({ node, boundary, markerText, t, top, onJump }) {
1315
+ const isBoundary = boundary !== void 0;
1316
+ const icon = isBoundary ? KIND_ICONS[boundary.kind] ?? "\u2022" : NODE_ICONS[node.type] ?? "\u2022";
1317
+ const label = isBoundary ? kindLabel(boundary.kind, t) : nodeTypeLabel(node.type, t);
1318
+ return (0, import_react.createElement)("div", {
1319
+ className: `dsh-rt-fork-row${isBoundary ? " dsh-rt-fork-boundary" : ""}`,
1320
+ style: { top: `${top}px` }
1321
+ }, [
1322
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, icon),
1323
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1324
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1325
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, label),
1326
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${node.seq}`),
1327
+ isBoundary && boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1328
+ "span",
1329
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1330
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1331
+ )
1332
+ ]),
1333
+ isBoundary && markerText && (0, import_react.createElement)(
1334
+ "div",
1335
+ { key: "text", className: "dsh-rt-fork-text" },
1336
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1337
+ )
1338
+ ]),
1339
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1340
+ ]);
1341
+ }
1342
+ function ForkHistRow({ boundary, markerText, t, onJump }) {
1343
+ return (0, import_react.createElement)("div", { className: "dsh-rt-fork-hist-row" }, [
1344
+ (0, import_react.createElement)("span", { key: "icon", className: "dsh-rt-fork-icon" }, KIND_ICONS[boundary.kind] ?? "\u2022"),
1345
+ (0, import_react.createElement)("div", { key: "body", className: "dsh-rt-fork-body" }, [
1346
+ (0, import_react.createElement)("div", { key: "line", className: "dsh-rt-fork-line" }, [
1347
+ (0, import_react.createElement)("span", { key: "label", className: "dsh-rt-fork-label" }, kindLabel(boundary.kind, t)),
1348
+ (0, import_react.createElement)("span", { key: "seq", className: "dsh-rt-fork-seq" }, `#${boundary.seq}`),
1349
+ boundary.replacedSeqs.length > 0 && (0, import_react.createElement)(
1350
+ "span",
1351
+ { key: "shadowed", className: "dsh-rt-fork-shadowed" },
1352
+ t("fork.shadowed", { count: boundary.replacedSeqs.length })
1353
+ )
1354
+ ]),
1355
+ markerText && (0, import_react.createElement)(
1356
+ "div",
1357
+ { key: "text", className: "dsh-rt-fork-text" },
1358
+ markerText.length > 140 ? `${markerText.slice(0, 140)}\u2026` : markerText
1359
+ )
1360
+ ]),
1361
+ (0, import_react.createElement)("button", { key: "jump", type: "button", className: "dsh-rt-chip", onClick: onJump }, t("timeline.jump"))
1362
+ ]);
1363
+ }
1047
1364
  function apply(ctx) {
1048
1365
  const disposeStyle = ensureStyle();
1049
1366
  ctx.effect(() => () => disposeStyle(), "dsh-retrace: styles");
@@ -1051,9 +1368,12 @@ function apply(ctx) {
1051
1368
  const t = ctx.locale.bind(NS);
1052
1369
  const conversationEvents = ctx.get("conversationEvents");
1053
1370
  if (conversationEvents) {
1054
- conversationEvents.register(userActionsDefinition);
1055
- conversationEvents.register(userReferenceDefinition);
1056
- conversationEvents.register(recallMarkerDefinition);
1371
+ const disposeDefinitions = [
1372
+ conversationEvents.register(userActionsDefinition),
1373
+ conversationEvents.register(userReferenceDefinition),
1374
+ conversationEvents.register(recallMarkerDefinition)
1375
+ ];
1376
+ ctx.effect(() => () => disposeDefinitions.forEach((dispose) => dispose()), "dsh-retrace: conversation definitions");
1057
1377
  }
1058
1378
  ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
1059
1379
  name: "conversation.chat.assistant-actions",
@@ -1087,6 +1407,17 @@ function apply(ctx) {
1087
1407
  store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1088
1408
  })
1089
1409
  }, RetraceView));
1410
+ ctx.slots.inject("conversation.view", () => ctx.slots.register({
1411
+ name: "conversation.view",
1412
+ id: "retrace-fork",
1413
+ order: 30,
1414
+ locale: NS,
1415
+ label: () => t("view.fork"),
1416
+ inject: (sessionId, actions) => ({
1417
+ actions,
1418
+ store: ctx.get?.("sessions")?.binding?.(sessionId)?.session
1419
+ })
1420
+ }, ForkView));
1090
1421
  ctx.slots.inject("settings.general.item", () => ctx.slots.register({
1091
1422
  name: "settings.general.item",
1092
1423
  id: "retrace",