dsh-retrace 0.4.3 → 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.
- package/lib/client.bundle.js +134 -49
- package/lib/client.js +229 -60
- package/lib/dynamic-client.js +134 -49
- package/lib/versioning.js +5 -1
- package/package.json +1 -1
package/lib/client.bundle.js
CHANGED
|
@@ -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",
|
|
@@ -148,6 +150,8 @@ var en = {
|
|
|
148
150
|
"marker.regenerate": "Reply regenerated",
|
|
149
151
|
"marker.originalLabel": "Original input",
|
|
150
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.',
|
|
151
155
|
"options.title": "Message editor plugin",
|
|
152
156
|
"options.showOriginalInput": "Show the original input after editing",
|
|
153
157
|
"options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
|
|
@@ -371,23 +375,32 @@ var recallMarkerDefinition = {
|
|
|
371
375
|
kind: "recall-marker",
|
|
372
376
|
target: "chat",
|
|
373
377
|
match: (event) => {
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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;
|
|
378
388
|
},
|
|
379
389
|
start: (_context, match) => {
|
|
380
390
|
const event = match.event;
|
|
381
|
-
const
|
|
382
|
-
const
|
|
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);
|
|
383
394
|
return {
|
|
384
395
|
seq: event.seq,
|
|
385
396
|
time: event.time,
|
|
386
|
-
op: markerOpFromId(id),
|
|
397
|
+
op: compact ? "compaction" : markerOpFromId(id),
|
|
387
398
|
legacy,
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
//
|
|
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).
|
|
391
404
|
shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
|
|
392
405
|
targetSeq: event.data?.editor?.targetSeq,
|
|
393
406
|
text: event.data?.editor?.text
|
|
@@ -399,6 +412,9 @@ var recallMarkerDefinition = {
|
|
|
399
412
|
return chatNodeLike(context, "recall-marker", context.state.seq, context.state);
|
|
400
413
|
}
|
|
401
414
|
};
|
|
415
|
+
function isCompactCheckpoint(source) {
|
|
416
|
+
return Boolean(source) && source.kind === "plugin" && source.plugin === "compact";
|
|
417
|
+
}
|
|
402
418
|
function textOf(content) {
|
|
403
419
|
if (!Array.isArray(content)) return "";
|
|
404
420
|
return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
@@ -413,17 +429,6 @@ function useMessageSeq(useSession, messageId) {
|
|
|
413
429
|
return void 0;
|
|
414
430
|
});
|
|
415
431
|
}
|
|
416
|
-
function useShadowed(useSession, seq) {
|
|
417
|
-
return useSession((snapshot) => {
|
|
418
|
-
if (seq === void 0 || seq === null) return false;
|
|
419
|
-
for (const node of snapshot.chat.nodes.values()) {
|
|
420
|
-
if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return false;
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
432
|
var SHADOW_SAFETY_RATIO = 0.4;
|
|
428
433
|
function hiddenKeysFor(shadowedSeqs, nodes) {
|
|
429
434
|
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
|
|
@@ -441,35 +446,94 @@ function hiddenKeysFor(shadowedSeqs, nodes) {
|
|
|
441
446
|
if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
|
|
442
447
|
continue;
|
|
443
448
|
}
|
|
444
|
-
if (typeof node.anchorSeq === "number"
|
|
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
|
+
}
|
|
445
453
|
}
|
|
446
454
|
return keys.length === 0 ? null : keys;
|
|
447
455
|
}
|
|
448
|
-
var EMPTY_HIDE_PLAN = {
|
|
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;
|
|
449
472
|
function useMarkerHidePlan(useSession) {
|
|
450
473
|
return useSession((snapshot) => {
|
|
474
|
+
if (hidePlanCacheSnapshot === snapshot) return hidePlanCacheValue;
|
|
451
475
|
const nodes = snapshot.chat.nodes;
|
|
452
|
-
|
|
476
|
+
const rowCount = realRowCount(nodes);
|
|
453
477
|
const markers = [];
|
|
454
478
|
for (const node of nodes.values()) {
|
|
455
|
-
if (
|
|
456
|
-
if (node.kind === "recall-marker") {
|
|
457
|
-
markers.push({ key: node.key, shadowedSeqs: node.data?.shadowedSeqs });
|
|
458
|
-
}
|
|
479
|
+
if (node.kind === "recall-marker" && !node.data?.compact) markers.push(node);
|
|
459
480
|
}
|
|
460
|
-
if (markers.length === 0)
|
|
481
|
+
if (markers.length === 0) {
|
|
482
|
+
hidePlanCacheSnapshot = snapshot;
|
|
483
|
+
hidePlanCacheValue = EMPTY_HIDE_PLAN;
|
|
484
|
+
return hidePlanCacheValue;
|
|
485
|
+
}
|
|
486
|
+
const plans = /* @__PURE__ */ new Map();
|
|
461
487
|
const union = /* @__PURE__ */ new Set();
|
|
462
|
-
const perMarker = /* @__PURE__ */ new Map();
|
|
463
488
|
for (const marker of markers) {
|
|
464
|
-
const keys = hiddenKeysFor(marker.shadowedSeqs, nodes);
|
|
465
|
-
|
|
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 });
|
|
466
492
|
if (keys !== null) for (const key of keys) union.add(key);
|
|
467
493
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
hiddenFor: (key) =>
|
|
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
|
|
472
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) {
|
|
518
|
+
return useSession((snapshot) => {
|
|
519
|
+
if (seq === void 0 || seq === null) return false;
|
|
520
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
521
|
+
if (node.kind !== "recall-marker" && typeof node.anchorSeq === "number" && node.anchorSeq === seq) {
|
|
522
|
+
return rowHiddenByKey(snapshot, node.key);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function useShadowed(useSession, seq) {
|
|
529
|
+
return useSession((snapshot) => {
|
|
530
|
+
if (seq === void 0 || seq === null) return false;
|
|
531
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
532
|
+
if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
473
537
|
});
|
|
474
538
|
}
|
|
475
539
|
function useMarkerDismissed(useSession, markerSeq, op) {
|
|
@@ -506,10 +570,11 @@ function useEditReference(useSession, mySeq) {
|
|
|
506
570
|
}
|
|
507
571
|
function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
508
572
|
const seq = useMessageSeq(useSession, messageId);
|
|
573
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
509
574
|
const shadowed = useShadowed(useSession, seq);
|
|
510
575
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
511
576
|
const [failure, setFailure] = (0, import_react.useState)(null);
|
|
512
|
-
if (shadowed || seq === void 0) return null;
|
|
577
|
+
if (hidden || shadowed || seq === void 0) return null;
|
|
513
578
|
const run = (op) => {
|
|
514
579
|
setBusy(true);
|
|
515
580
|
setFailure(null);
|
|
@@ -552,11 +617,11 @@ function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
|
552
617
|
}
|
|
553
618
|
function ReferenceRow({ node, useSession, t }) {
|
|
554
619
|
const { seq, messageId } = node.data;
|
|
555
|
-
const
|
|
620
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
556
621
|
const markerRef = useEditReference(useSession, seq);
|
|
557
622
|
const referenceText = editReferences.get(messageId) ?? markerRef;
|
|
558
623
|
const config = useConfig();
|
|
559
|
-
if (
|
|
624
|
+
if (hidden) return null;
|
|
560
625
|
if (referenceText === null || !config.showOriginalInput) return null;
|
|
561
626
|
return (0, import_react.createElement)("div", { className: "dsh-rt-user-row" }, [
|
|
562
627
|
(0, import_react.createElement)("details", { className: "dsh-rt-reference" }, [
|
|
@@ -571,12 +636,13 @@ function ReferenceRow({ node, useSession, t }) {
|
|
|
571
636
|
}
|
|
572
637
|
function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
573
638
|
const { seq, messageId, content } = node.data;
|
|
639
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
574
640
|
const shadowed = useShadowed(useSession, seq);
|
|
575
641
|
const [editing, setEditing] = (0, import_react.useState)(false);
|
|
576
642
|
const [draft, setDraft] = (0, import_react.useState)("");
|
|
577
643
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
578
644
|
const [failure, setFailure] = (0, import_react.useState)(null);
|
|
579
|
-
if (shadowed) return null;
|
|
645
|
+
if (hidden || shadowed) return null;
|
|
580
646
|
const openEditor = () => {
|
|
581
647
|
setDraft(textOf(content));
|
|
582
648
|
setFailure(null);
|
|
@@ -669,16 +735,26 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
|
669
735
|
]);
|
|
670
736
|
}
|
|
671
737
|
function RecallMarkerRow({ node, useSession, t }) {
|
|
672
|
-
const { seq, op, shadowedSeqs, legacy } = node.data;
|
|
738
|
+
const { seq, op, shadowedSeqs, legacy, compact } = node.data;
|
|
739
|
+
if (compact) return null;
|
|
673
740
|
const dismissed = useMarkerDismissed(useSession, seq, op);
|
|
674
741
|
const hidePlan = useMarkerHidePlan(useSession);
|
|
675
|
-
const
|
|
742
|
+
const plan = hidePlan.planFor(node.key);
|
|
743
|
+
const hiddenKeys = legacy || !getConfig().hideShadowed ? null : plan?.keys ?? null;
|
|
676
744
|
const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
|
|
677
745
|
const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
|
|
678
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;
|
|
679
753
|
return (0, import_react.createElement)("div", { className: "dsh-rt-marker-block", "data-dismissed": dismissed || void 0 }, [
|
|
680
754
|
css !== null && (0, import_react.createElement)("style", { key: "hide", dangerouslySetInnerHTML: { __html: css } }),
|
|
681
|
-
!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
|
|
682
758
|
]);
|
|
683
759
|
}
|
|
684
760
|
function OptionsRow({ t }) {
|
|
@@ -755,6 +831,7 @@ var CSS = `
|
|
|
755
831
|
.dsh-rt-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;max-width:min(525px,82%)}
|
|
756
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}
|
|
757
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}
|
|
758
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}
|
|
759
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}
|
|
760
837
|
.dsh-rt-reference summary::-webkit-details-marker{display:none}
|
|
@@ -836,7 +913,7 @@ function switchToViewTab(viewId) {
|
|
|
836
913
|
console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
|
|
837
914
|
return;
|
|
838
915
|
}
|
|
839
|
-
const buttons = [...document.querySelectorAll('[role="tablist"] [role="tab"]')];
|
|
916
|
+
const buttons = [...document.querySelectorAll('[role="tablist"]')].filter((tablist) => tablist.getBoundingClientRect().width > 0).flatMap((tablist) => [...tablist.querySelectorAll('[role="tab"]')]);
|
|
840
917
|
const button = buttons[index];
|
|
841
918
|
if (!button) {
|
|
842
919
|
console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
|
|
@@ -844,6 +921,11 @@ function switchToViewTab(viewId) {
|
|
|
844
921
|
}
|
|
845
922
|
button.click();
|
|
846
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
|
+
}
|
|
847
929
|
function waitForElement(selector, frames) {
|
|
848
930
|
return new Promise((resolve) => {
|
|
849
931
|
let remaining = frames;
|
|
@@ -879,9 +961,9 @@ async function jumpToAnchor(store, anchorSeq) {
|
|
|
879
961
|
let key = keyOfSeq(anchorSeq);
|
|
880
962
|
let pages = 0;
|
|
881
963
|
while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
|
|
882
|
-
const before = store.getSnapshot()?.chat?.nodes
|
|
964
|
+
const before = nodeCountOf(store.getSnapshot()?.chat?.nodes);
|
|
883
965
|
await store.loadOlder();
|
|
884
|
-
const after = store.getSnapshot()?.chat?.nodes
|
|
966
|
+
const after = nodeCountOf(store.getSnapshot()?.chat?.nodes);
|
|
885
967
|
pages += 1;
|
|
886
968
|
if (after === before) break;
|
|
887
969
|
key = keyOfSeq(anchorSeq);
|
|
@@ -1286,9 +1368,12 @@ function apply(ctx) {
|
|
|
1286
1368
|
const t = ctx.locale.bind(NS);
|
|
1287
1369
|
const conversationEvents = ctx.get("conversationEvents");
|
|
1288
1370
|
if (conversationEvents) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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");
|
|
1292
1377
|
}
|
|
1293
1378
|
ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
1294
1379
|
name: "conversation.chat.assistant-actions",
|
package/lib/client.js
CHANGED
|
@@ -71,6 +71,8 @@ const zh = {
|
|
|
71
71
|
'marker.regenerate': '已重新生成回复',
|
|
72
72
|
'marker.originalLabel': '原输入',
|
|
73
73
|
'marker.referenceHint': '点击展开查看原提问(仅作对照,不会进入模型上下文)',
|
|
74
|
+
'marker.degradedHint': '此操作涉及大范围对话,为保护历史未隐藏内容(日志完好)。',
|
|
75
|
+
'marker.unionHint': '已累积隐藏约 {count}% 的历史消息;可在 设置→通用 关闭「按标记隐藏」查看完整历史。',
|
|
74
76
|
'options.title': '消息编辑插件',
|
|
75
77
|
'options.showOriginalInput': '编辑后显示原提问对照',
|
|
76
78
|
'options.editFromScratch': '编辑后从新对话开始(隐藏此前的消息,默认关)',
|
|
@@ -157,6 +159,8 @@ const en = {
|
|
|
157
159
|
'marker.regenerate': 'Reply regenerated',
|
|
158
160
|
'marker.originalLabel': 'Original input',
|
|
159
161
|
'marker.referenceHint': 'Click to expand the original input (reference only, never sent to the model)',
|
|
162
|
+
'marker.degradedHint': 'This operation spans a large part of the conversation; content stays visible to protect your history (the log is intact).',
|
|
163
|
+
'marker.unionHint': 'About {count}% of the history is hidden in total; disable "Hide shadowed messages" in Settings → General to review the full history.',
|
|
160
164
|
'options.title': 'Message editor plugin',
|
|
161
165
|
'options.showOriginalInput': 'Show the original input after editing',
|
|
162
166
|
'options.editFromScratch': 'Start a fresh conversation after editing (hide earlier messages, default off)',
|
|
@@ -453,23 +457,38 @@ const recallMarkerDefinition = {
|
|
|
453
457
|
kind: 'recall-marker',
|
|
454
458
|
target: 'chat',
|
|
455
459
|
match: (event) => {
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
+
if (!isReplacementSurfaceEvent(event)) return null
|
|
461
|
+
if (event.type === 'assistant/message') {
|
|
462
|
+
const id = event.data?.message?.id
|
|
463
|
+
if (!isMarkerId(id)) return null
|
|
464
|
+
return { id: `marker:${id}`, role: 'start' }
|
|
465
|
+
}
|
|
466
|
+
// Compaction checkpoints are user/message replaces with the official
|
|
467
|
+
// `plugin: compact` source. They are NOT our markers, but their shadow
|
|
468
|
+
// range tells us which messages were compacted away — used ONLY to hide
|
|
469
|
+
// the edit/recall entries for those messages (compacted rows are handled
|
|
470
|
+
// by the engine itself, so the checkpoint marker renders nothing and
|
|
471
|
+
// never injects hide rules).
|
|
472
|
+
if (event.type === 'user/message' && isCompactCheckpoint(event.data?.source)) {
|
|
473
|
+
return { id: `marker:compact:${event.seq}`, role: 'start' }
|
|
474
|
+
}
|
|
475
|
+
return null
|
|
460
476
|
},
|
|
461
477
|
start: (_context, match) => {
|
|
462
478
|
const event = match.event
|
|
463
|
-
const
|
|
464
|
-
const
|
|
479
|
+
const compact = event.type === 'user/message' && isCompactCheckpoint(event.data?.source)
|
|
480
|
+
const id = compact ? '' : String(event.data.message.id)
|
|
481
|
+
const legacy = !compact && isLegacyMarkerId(id)
|
|
465
482
|
return {
|
|
466
483
|
seq: event.seq,
|
|
467
484
|
time: event.time,
|
|
468
|
-
op: markerOpFromId(id),
|
|
485
|
+
op: compact ? 'compaction' : markerOpFromId(id),
|
|
469
486
|
legacy,
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
//
|
|
487
|
+
compact,
|
|
488
|
+
// Legacy/compact markers never hide: their shadowed range is kept for
|
|
489
|
+
// the action-row suppression check (useShadowed) but empty for legacy
|
|
490
|
+
// so no row is hidden and no action row is suppressed for legacy
|
|
491
|
+
// markers (rename must never make visible content disappear).
|
|
473
492
|
shadowedSeqs: legacy ? [] : (Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : []),
|
|
474
493
|
targetSeq: event.data?.editor?.targetSeq,
|
|
475
494
|
text: event.data?.editor?.text,
|
|
@@ -482,6 +501,11 @@ const recallMarkerDefinition = {
|
|
|
482
501
|
},
|
|
483
502
|
}
|
|
484
503
|
|
|
504
|
+
/** Official compaction checkpoint source: `{kind:'plugin', plugin:'compact'}`. */
|
|
505
|
+
function isCompactCheckpoint(source) {
|
|
506
|
+
return Boolean(source) && source.kind === 'plugin' && source.plugin === 'compact'
|
|
507
|
+
}
|
|
508
|
+
|
|
485
509
|
// ---------------------------------------------------------------------------
|
|
486
510
|
// Shared selector helpers
|
|
487
511
|
// ---------------------------------------------------------------------------
|
|
@@ -505,25 +529,18 @@ function useMessageSeq(useSession, messageId) {
|
|
|
505
529
|
})
|
|
506
530
|
}
|
|
507
531
|
|
|
508
|
-
/** True when `seq` was shadowed by any recall/edit/regenerate marker. */
|
|
509
|
-
function useShadowed(useSession, seq) {
|
|
510
|
-
return useSession((snapshot) => {
|
|
511
|
-
if (seq === undefined || seq === null) return false
|
|
512
|
-
for (const node of snapshot.chat.nodes.values()) {
|
|
513
|
-
if (node.kind === 'recall-marker' && Array.isArray(node.data?.shadowedSeqs)
|
|
514
|
-
&& node.data.shadowedSeqs.includes(seq)) {
|
|
515
|
-
return true
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return false
|
|
519
|
-
})
|
|
520
|
-
}
|
|
521
|
-
|
|
522
532
|
/**
|
|
523
|
-
*
|
|
524
|
-
* rows
|
|
525
|
-
*
|
|
526
|
-
*
|
|
533
|
+
* Safety guard: a SINGLE marker that would hide more than this share of the
|
|
534
|
+
* conversation's rows is refused (the marker renders as a notice only, and an
|
|
535
|
+
* explicit hint explains why). Ordinary recalls/edits shadow a few rows and
|
|
536
|
+
* always hide — the guard only ever trips on whole-surface operations such as
|
|
537
|
+
* "edit, start a fresh conversation". History must never silently vanish.
|
|
538
|
+
*
|
|
539
|
+
* Note (0.4.3 regression, fixed): the previous UNION-wide guard degraded EVERY
|
|
540
|
+
* marker (including fresh recalls) once the session's markers collectively
|
|
541
|
+
* covered >40% of the rows — recall/edit silently stopped hiding. The guard is
|
|
542
|
+
* per-marker again; a stacked-edit session still hides each replaced round,
|
|
543
|
+
* and a visible hint reports how much history is hidden in total.
|
|
527
544
|
*/
|
|
528
545
|
const SHADOW_SAFETY_RATIO = 0.4
|
|
529
546
|
|
|
@@ -553,43 +570,141 @@ function hiddenKeysFor(shadowedSeqs, nodes) {
|
|
|
553
570
|
if (typeof resultSeq === 'number' && hidden.has(resultSeq)) keys.push(node.key)
|
|
554
571
|
continue
|
|
555
572
|
}
|
|
556
|
-
if (typeof node.anchorSeq === 'number'
|
|
573
|
+
if (typeof node.anchorSeq === 'number') {
|
|
574
|
+
// Pseudo rows anchor at HALF seqs to order before their message
|
|
575
|
+
// (retrace-reference uses `seq - 0.5`); the shadow set holds whole
|
|
576
|
+
// seqs, so map the anchor back to its integer seq before matching —
|
|
577
|
+
// otherwise the original-input reference survives the message it
|
|
578
|
+
// belongs to (visible residue after an edit).
|
|
579
|
+
const anchored = node.anchorSeq % 1 === 0 ? node.anchorSeq : Math.ceil(node.anchorSeq)
|
|
580
|
+
if (hidden.has(anchored)) keys.push(node.key)
|
|
581
|
+
}
|
|
557
582
|
}
|
|
558
583
|
return keys.length === 0 ? null : keys
|
|
559
584
|
}
|
|
560
585
|
|
|
561
586
|
/**
|
|
562
|
-
*
|
|
563
|
-
* keys and applies the safety guard to
|
|
564
|
-
* hides its replaced round (a few rows ≪ the
|
|
565
|
-
*
|
|
566
|
-
*
|
|
587
|
+
* Hide plan (0.4.3 → per-marker): one snapshot pass computes every marker's
|
|
588
|
+
* hidden keys and applies the safety guard to each marker INDEPENDENTLY. A
|
|
589
|
+
* single normal recall/edit hides its replaced round (a few rows ≪ the
|
|
590
|
+
* threshold); only a marker that would hide most of the history by itself
|
|
591
|
+
* (e.g. "start a fresh conversation") degrades to notice-only. The plan also
|
|
592
|
+
* reports the collective ratio so the UI can hint when stacked edits hide a
|
|
593
|
+
* large share of the conversation.
|
|
567
594
|
*/
|
|
568
|
-
const EMPTY_HIDE_PLAN = {
|
|
595
|
+
const EMPTY_HIDE_PLAN = Object.freeze({
|
|
596
|
+
hiddenFor: () => null,
|
|
597
|
+
planFor: () => null,
|
|
598
|
+
unionRatio: 0,
|
|
599
|
+
firstMarkerKey: null,
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Plugin pseudo-node kinds never represent a real conversation row; they must
|
|
604
|
+
* not count toward the safety-ratio denominator (0.4.x review: counting them
|
|
605
|
+
* diluted the 40% guard to ~55-68% of real rows).
|
|
606
|
+
*/
|
|
607
|
+
const PLUGIN_PSEUDO_KINDS = new Set(['user-actions', 'retrace-reference', 'recall-marker'])
|
|
608
|
+
|
|
609
|
+
/** Count only REAL conversation rows (excludes the plugin's pseudo nodes). */
|
|
610
|
+
function realRowCount(nodes) {
|
|
611
|
+
let count = 0
|
|
612
|
+
for (const node of nodes.values()) {
|
|
613
|
+
if (typeof node.anchorSeq === 'number' && !PLUGIN_PSEUDO_KINDS.has(node.kind)) count += 1
|
|
614
|
+
}
|
|
615
|
+
return count
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Module-level memo: the conversation snapshot reference is stable between
|
|
619
|
+
// events, so the hide plan (O(nodes) per pass) is computed once per snapshot
|
|
620
|
+
// and shared by every marker row — same object reference → no re-render storm
|
|
621
|
+
// (0.4.x review: each marker row recomputed the whole table every snapshot).
|
|
622
|
+
let hidePlanCacheSnapshot = null
|
|
623
|
+
let hidePlanCacheValue = null
|
|
569
624
|
function useMarkerHidePlan(useSession) {
|
|
570
625
|
return useSession((snapshot) => {
|
|
626
|
+
if (hidePlanCacheSnapshot === snapshot) return hidePlanCacheValue
|
|
571
627
|
const nodes = snapshot.chat.nodes
|
|
572
|
-
|
|
628
|
+
const rowCount = realRowCount(nodes)
|
|
573
629
|
const markers = []
|
|
574
630
|
for (const node of nodes.values()) {
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
631
|
+
if (node.kind === 'recall-marker' && !node.data?.compact) markers.push(node)
|
|
632
|
+
}
|
|
633
|
+
if (markers.length === 0) {
|
|
634
|
+
hidePlanCacheSnapshot = snapshot
|
|
635
|
+
hidePlanCacheValue = EMPTY_HIDE_PLAN
|
|
636
|
+
return hidePlanCacheValue
|
|
579
637
|
}
|
|
580
|
-
|
|
638
|
+
const plans = new Map()
|
|
581
639
|
const union = new Set()
|
|
582
|
-
const perMarker = new Map()
|
|
583
640
|
for (const marker of markers) {
|
|
584
|
-
const keys = hiddenKeysFor(marker.shadowedSeqs, nodes)
|
|
585
|
-
|
|
641
|
+
const keys = hiddenKeysFor(marker.data?.shadowedSeqs, nodes)
|
|
642
|
+
const degraded = keys !== null && rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO
|
|
643
|
+
plans.set(marker.key, { keys: degraded ? null : keys, degraded })
|
|
586
644
|
if (keys !== null) for (const key of keys) union.add(key)
|
|
587
645
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
hiddenFor: (key) =>
|
|
646
|
+
hidePlanCacheSnapshot = snapshot
|
|
647
|
+
hidePlanCacheValue = {
|
|
648
|
+
planFor: (key) => plans.get(key) ?? null,
|
|
649
|
+
hiddenFor: (key) => plans.get(key)?.keys ?? null,
|
|
650
|
+
unionRatio: rowCount > 0 ? union.size / rowCount : 0,
|
|
651
|
+
firstMarkerKey: markers[0].key,
|
|
652
|
+
}
|
|
653
|
+
return hidePlanCacheValue
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* True when the chat row `key` is actually hidden right now — i.e. some
|
|
659
|
+
* marker that is NOT degraded includes it in its hide rules. This mirrors the
|
|
660
|
+
* CSS reality: a degraded marker hides nothing, so rows it shadowed stay
|
|
661
|
+
* visible AND stay operable (their action rows must not vanish).
|
|
662
|
+
*/
|
|
663
|
+
function rowHiddenByKey(snapshot, rowKey) {
|
|
664
|
+
if (rowKey === undefined || rowKey === null) return false
|
|
665
|
+
const nodes = snapshot.chat.nodes
|
|
666
|
+
const rowCount = realRowCount(nodes)
|
|
667
|
+
for (const node of nodes.values()) {
|
|
668
|
+
if (node.kind !== 'recall-marker' || node.data?.compact) continue // compact markers never hide rows
|
|
669
|
+
const keys = hiddenKeysFor(node.data?.shadowedSeqs, nodes)
|
|
670
|
+
if (keys === null) continue
|
|
671
|
+
if (rowCount > 0 && keys.length / rowCount > SHADOW_SAFETY_RATIO) continue // degraded: hides nothing
|
|
672
|
+
if (keys.includes(rowKey)) return true
|
|
673
|
+
}
|
|
674
|
+
return false
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Same as rowHiddenByKey, but resolves the row key from a surface seq. */
|
|
678
|
+
function useSeqHidden(useSession, seq) {
|
|
679
|
+
return useSession((snapshot) => {
|
|
680
|
+
if (seq === undefined || seq === null) return false
|
|
681
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
682
|
+
if (node.kind !== 'recall-marker' && typeof node.anchorSeq === 'number' && node.anchorSeq === seq) {
|
|
683
|
+
return rowHiddenByKey(snapshot, node.key)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return false
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* True when `seq` was shadowed by ANY recall/edit/regenerate/compaction
|
|
692
|
+
* marker — the OPERATION-FEASIBILITY dimension, distinct from visual hiding:
|
|
693
|
+
* a shadowed message can never be edited/recalled again (the host rejects it
|
|
694
|
+
* with target-shadowed), so its action entries must be hidden even when the
|
|
695
|
+
* row itself stays visible (guard-degraded or compacted).
|
|
696
|
+
*/
|
|
697
|
+
function useShadowed(useSession, seq) {
|
|
698
|
+
return useSession((snapshot) => {
|
|
699
|
+
if (seq === undefined || seq === null) return false
|
|
700
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
701
|
+
// Include compact markers: compacted messages are also un-editable.
|
|
702
|
+
if (node.kind === 'recall-marker' && Array.isArray(node.data?.shadowedSeqs)
|
|
703
|
+
&& node.data.shadowedSeqs.includes(seq)) {
|
|
704
|
+
return true
|
|
705
|
+
}
|
|
592
706
|
}
|
|
707
|
+
return false
|
|
593
708
|
})
|
|
594
709
|
}
|
|
595
710
|
|
|
@@ -644,10 +759,14 @@ function useEditReference(useSession, mySeq) {
|
|
|
644
759
|
/** 撤回 / 重新生成 strip inside a finalized assistant reply's IconActions row. */
|
|
645
760
|
function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
646
761
|
const seq = useMessageSeq(useSession, messageId)
|
|
762
|
+
const hidden = useSeqHidden(useSession, seq)
|
|
647
763
|
const shadowed = useShadowed(useSession, seq)
|
|
648
764
|
const [busy, setBusy] = useState(false)
|
|
649
765
|
const [failure, setFailure] = useState(null)
|
|
650
|
-
|
|
766
|
+
// Hidden (visually) or shadowed (un-editable: recalled/edited/compacted
|
|
767
|
+
// away) messages must not offer recall/regenerate — the host would reject
|
|
768
|
+
// with target-shadowed.
|
|
769
|
+
if (hidden || shadowed || seq === undefined) return null
|
|
651
770
|
|
|
652
771
|
const run = (op) => {
|
|
653
772
|
setBusy(true)
|
|
@@ -694,11 +813,15 @@ function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
|
694
813
|
/** 原输入 reference block, rendered just above the re-sent message. */
|
|
695
814
|
function ReferenceRow({ node, useSession, t }) {
|
|
696
815
|
const { seq, messageId } = node.data
|
|
697
|
-
|
|
816
|
+
// The reference node anchors at seq-0.5 (above the message) and never
|
|
817
|
+
// appears in any hide rule — judge by the REAL message seq so a shadowed
|
|
818
|
+
// re-send's reference disappears with it (0.4.4 regression: judging by the
|
|
819
|
+
// node key left stale "original input" blocks after a second edit).
|
|
820
|
+
const hidden = useSeqHidden(useSession, seq)
|
|
698
821
|
const markerRef = useEditReference(useSession, seq)
|
|
699
822
|
const referenceText = editReferences.get(messageId) ?? markerRef
|
|
700
823
|
const config = useConfig()
|
|
701
|
-
if (
|
|
824
|
+
if (hidden) return null
|
|
702
825
|
if (referenceText === null || !config.showOriginalInput) return null
|
|
703
826
|
return createElement('div', { className: 'dsh-rt-user-row' }, [
|
|
704
827
|
createElement('details', { className: 'dsh-rt-reference' }, [
|
|
@@ -712,12 +835,18 @@ function ReferenceRow({ node, useSession, t }) {
|
|
|
712
835
|
/** 编辑 / 撤回 action row under one user message; recall echoes into the composer. */
|
|
713
836
|
function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
714
837
|
const { seq, messageId, content } = node.data
|
|
838
|
+
// Two independent dimensions: visual hiding (guard-protected, row stays
|
|
839
|
+
// visible when degraded) vs operation feasibility (a shadowed message can
|
|
840
|
+
// never be edited again — the host rejects with target-shadowed). Hide the
|
|
841
|
+
// edit/recall entries when EITHER applies, so compacted or recalled rows
|
|
842
|
+
// that remain visible don't offer operations that would just fail.
|
|
843
|
+
const hidden = useSeqHidden(useSession, seq)
|
|
715
844
|
const shadowed = useShadowed(useSession, seq)
|
|
716
845
|
const [editing, setEditing] = useState(false)
|
|
717
846
|
const [draft, setDraft] = useState('')
|
|
718
847
|
const [busy, setBusy] = useState(false)
|
|
719
848
|
const [failure, setFailure] = useState(null)
|
|
720
|
-
if (shadowed) return null
|
|
849
|
+
if (hidden || shadowed) return null
|
|
721
850
|
|
|
722
851
|
const openEditor = () => {
|
|
723
852
|
setDraft(textOf(content))
|
|
@@ -818,10 +947,14 @@ function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
|
818
947
|
|
|
819
948
|
/** The transient notice row: hides shadowed content, dismissed after the user keeps typing. */
|
|
820
949
|
function RecallMarkerRow({ node, useSession, t }) {
|
|
821
|
-
const { seq, op, shadowedSeqs, legacy } = node.data
|
|
950
|
+
const { seq, op, shadowedSeqs, legacy, compact } = node.data
|
|
951
|
+
// Compaction checkpoints render nothing here: their only role is feeding
|
|
952
|
+
// useShadowed so compacted messages lose their edit/recall entries.
|
|
953
|
+
if (compact) return null
|
|
822
954
|
const dismissed = useMarkerDismissed(useSession, seq, op)
|
|
823
955
|
const hidePlan = useMarkerHidePlan(useSession)
|
|
824
|
-
const
|
|
956
|
+
const plan = hidePlan.planFor(node.key)
|
|
957
|
+
const hiddenKeys = legacy || !getConfig().hideShadowed ? null : (plan?.keys ?? null)
|
|
825
958
|
|
|
826
959
|
// The hide rules must stay mounted even after the notice is dismissed,
|
|
827
960
|
// otherwise the recalled message would reappear. Legacy markers and the
|
|
@@ -833,10 +966,21 @@ function RecallMarkerRow({ node, useSession, t }) {
|
|
|
833
966
|
const label = op === 'recall'
|
|
834
967
|
? (count > 1 ? t('marker.recallMany', { count }) : t('marker.recallOne'))
|
|
835
968
|
: op === 'regenerate' ? t('marker.regenerate') : t('marker.edit')
|
|
969
|
+
// A per-marker safety-guard trip (e.g. "start a fresh conversation") and a
|
|
970
|
+
// collective-hide hint appear once, on the first marker row.
|
|
971
|
+
const degradedHint = !legacy && plan?.degraded === true
|
|
972
|
+
? createElement('div', { key: 'degraded', className: 'dsh-rt-marker-hint' }, t('marker.degradedHint'))
|
|
973
|
+
: null
|
|
974
|
+
const unionHint = !legacy && hidePlan.firstMarkerKey === node.key && hidePlan.unionRatio > SHADOW_SAFETY_RATIO
|
|
975
|
+
? createElement('div', { key: 'union', className: 'dsh-rt-marker-hint' },
|
|
976
|
+
t('marker.unionHint', { count: Math.round(hidePlan.unionRatio * 100) }))
|
|
977
|
+
: null
|
|
836
978
|
|
|
837
979
|
return createElement('div', { className: 'dsh-rt-marker-block', 'data-dismissed': dismissed || undefined }, [
|
|
838
980
|
css !== null && createElement('style', { key: 'hide', dangerouslySetInnerHTML: { __html: css } }),
|
|
839
981
|
!dismissed && createElement('div', { key: 'label', className: 'dsh-rt-marker', role: 'status' }, label),
|
|
982
|
+
!dismissed && degradedHint,
|
|
983
|
+
!dismissed && unionHint,
|
|
840
984
|
])
|
|
841
985
|
}
|
|
842
986
|
|
|
@@ -919,6 +1063,7 @@ const CSS = `
|
|
|
919
1063
|
.dsh-rt-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;max-width:min(525px,82%)}
|
|
920
1064
|
.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}
|
|
921
1065
|
.dsh-rt-marker{text-align:center;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}
|
|
1066
|
+
.dsh-rt-marker-hint{text-align:center;color:var(--dsw-alias-state-warning-primary);font-size:11px;line-height:16px;margin-top:2px}
|
|
922
1067
|
.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}
|
|
923
1068
|
.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}
|
|
924
1069
|
.dsh-rt-reference summary::-webkit-details-marker{display:none}
|
|
@@ -1015,7 +1160,12 @@ function switchToViewTab(viewId) {
|
|
|
1015
1160
|
console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`)
|
|
1016
1161
|
return
|
|
1017
1162
|
}
|
|
1018
|
-
|
|
1163
|
+
// Only VISIBLE tablists count — hidden tabbars (settings etc.) must not
|
|
1164
|
+
// shift the index (0.4.x review: a document-wide query could click the
|
|
1165
|
+
// wrong control). The conversation view's tab bar is the visible one.
|
|
1166
|
+
const buttons = [...document.querySelectorAll('[role="tablist"]')]
|
|
1167
|
+
.filter((tablist) => tablist.getBoundingClientRect().width > 0)
|
|
1168
|
+
.flatMap((tablist) => [...tablist.querySelectorAll('[role="tab"]')])
|
|
1019
1169
|
const button = buttons[index]
|
|
1020
1170
|
if (!button) {
|
|
1021
1171
|
console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`)
|
|
@@ -1024,6 +1174,13 @@ function switchToViewTab(viewId) {
|
|
|
1024
1174
|
button.click()
|
|
1025
1175
|
}
|
|
1026
1176
|
|
|
1177
|
+
/** Count chat nodes regardless of store shape (Map or iterator-only). */
|
|
1178
|
+
function nodeCountOf(nodes) {
|
|
1179
|
+
if (!nodes) return 0
|
|
1180
|
+
if (typeof nodes.size === 'number') return nodes.size
|
|
1181
|
+
return [...nodes.values()].length
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1027
1184
|
/** rAF poll for an element that appears after a view switch + render pass. */
|
|
1028
1185
|
function waitForElement(selector, frames) {
|
|
1029
1186
|
return new Promise((resolve) => {
|
|
@@ -1071,9 +1228,13 @@ async function jumpToAnchor(store, anchorSeq) {
|
|
|
1071
1228
|
let key = keyOfSeq(anchorSeq)
|
|
1072
1229
|
let pages = 0
|
|
1073
1230
|
while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
|
|
1074
|
-
|
|
1231
|
+
// Map/iterator-agnostic node count: `.size` may be absent on the
|
|
1232
|
+
// snapshot's node store and `values()` yields an iterator with no
|
|
1233
|
+
// `.length` — spread it (0.4.x review: the loop used to break after one
|
|
1234
|
+
// page because the count was always undefined).
|
|
1235
|
+
const before = nodeCountOf(store.getSnapshot()?.chat?.nodes)
|
|
1075
1236
|
await store.loadOlder()
|
|
1076
|
-
const after = store.getSnapshot()?.chat?.nodes
|
|
1237
|
+
const after = nodeCountOf(store.getSnapshot()?.chat?.nodes)
|
|
1077
1238
|
pages += 1
|
|
1078
1239
|
if (after === before) break // page returned nothing
|
|
1079
1240
|
key = keyOfSeq(anchorSeq)
|
|
@@ -1604,9 +1765,17 @@ export function apply(ctx) {
|
|
|
1604
1765
|
|
|
1605
1766
|
const conversationEvents = ctx.get('conversationEvents')
|
|
1606
1767
|
if (conversationEvents) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1768
|
+
// register() returns a disposer that removes the definition from the
|
|
1769
|
+
// registry service's context. It MUST be wired into ctx.effect — otherwise
|
|
1770
|
+
// the definition survives plugin unload/hot-reload and the next apply
|
|
1771
|
+
// throws "already registered" (0.4.x review: the plugin would crash after
|
|
1772
|
+
// one toggle in Settings).
|
|
1773
|
+
const disposeDefinitions = [
|
|
1774
|
+
conversationEvents.register(userActionsDefinition),
|
|
1775
|
+
conversationEvents.register(userReferenceDefinition),
|
|
1776
|
+
conversationEvents.register(recallMarkerDefinition),
|
|
1777
|
+
]
|
|
1778
|
+
ctx.effect(() => () => disposeDefinitions.forEach((dispose) => dispose()), 'dsh-retrace: conversation definitions')
|
|
1610
1779
|
}
|
|
1611
1780
|
|
|
1612
1781
|
ctx.slots.inject('conversation.chat.assistant-actions', () => ctx.slots.register({
|
package/lib/dynamic-client.js
CHANGED
|
@@ -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",
|
|
@@ -154,6 +156,8 @@ return {
|
|
|
154
156
|
"marker.regenerate": "Reply regenerated",
|
|
155
157
|
"marker.originalLabel": "Original input",
|
|
156
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.',
|
|
157
161
|
"options.title": "Message editor plugin",
|
|
158
162
|
"options.showOriginalInput": "Show the original input after editing",
|
|
159
163
|
"options.editFromScratch": "Start a fresh conversation after editing (hide earlier messages, default off)",
|
|
@@ -377,23 +381,32 @@ return {
|
|
|
377
381
|
kind: "recall-marker",
|
|
378
382
|
target: "chat",
|
|
379
383
|
match: (event) => {
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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;
|
|
384
394
|
},
|
|
385
395
|
start: (_context, match) => {
|
|
386
396
|
const event = match.event;
|
|
387
|
-
const
|
|
388
|
-
const
|
|
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);
|
|
389
400
|
return {
|
|
390
401
|
seq: event.seq,
|
|
391
402
|
time: event.time,
|
|
392
|
-
op: markerOpFromId(id),
|
|
403
|
+
op: compact ? "compaction" : markerOpFromId(id),
|
|
393
404
|
legacy,
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
//
|
|
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).
|
|
397
410
|
shadowedSeqs: legacy ? [] : Array.isArray(event.sourceEventSeqs) ? event.sourceEventSeqs.slice() : [],
|
|
398
411
|
targetSeq: event.data?.editor?.targetSeq,
|
|
399
412
|
text: event.data?.editor?.text
|
|
@@ -405,6 +418,9 @@ return {
|
|
|
405
418
|
return chatNodeLike(context, "recall-marker", context.state.seq, context.state);
|
|
406
419
|
}
|
|
407
420
|
};
|
|
421
|
+
function isCompactCheckpoint(source) {
|
|
422
|
+
return Boolean(source) && source.kind === "plugin" && source.plugin === "compact";
|
|
423
|
+
}
|
|
408
424
|
function textOf(content) {
|
|
409
425
|
if (!Array.isArray(content)) return "";
|
|
410
426
|
return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
@@ -419,17 +435,6 @@ return {
|
|
|
419
435
|
return void 0;
|
|
420
436
|
});
|
|
421
437
|
}
|
|
422
|
-
function useShadowed(useSession, seq) {
|
|
423
|
-
return useSession((snapshot) => {
|
|
424
|
-
if (seq === void 0 || seq === null) return false;
|
|
425
|
-
for (const node of snapshot.chat.nodes.values()) {
|
|
426
|
-
if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
|
|
427
|
-
return true;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return false;
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
438
|
var SHADOW_SAFETY_RATIO = 0.4;
|
|
434
439
|
function hiddenKeysFor(shadowedSeqs, nodes) {
|
|
435
440
|
if (!Array.isArray(shadowedSeqs) || shadowedSeqs.length === 0) return null;
|
|
@@ -447,35 +452,94 @@ return {
|
|
|
447
452
|
if (typeof resultSeq === "number" && hidden.has(resultSeq)) keys.push(node.key);
|
|
448
453
|
continue;
|
|
449
454
|
}
|
|
450
|
-
if (typeof node.anchorSeq === "number"
|
|
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
|
+
}
|
|
451
459
|
}
|
|
452
460
|
return keys.length === 0 ? null : keys;
|
|
453
461
|
}
|
|
454
|
-
var EMPTY_HIDE_PLAN = {
|
|
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;
|
|
455
478
|
function useMarkerHidePlan(useSession) {
|
|
456
479
|
return useSession((snapshot) => {
|
|
480
|
+
if (hidePlanCacheSnapshot === snapshot) return hidePlanCacheValue;
|
|
457
481
|
const nodes = snapshot.chat.nodes;
|
|
458
|
-
|
|
482
|
+
const rowCount = realRowCount(nodes);
|
|
459
483
|
const markers = [];
|
|
460
484
|
for (const node of nodes.values()) {
|
|
461
|
-
if (
|
|
462
|
-
if (node.kind === "recall-marker") {
|
|
463
|
-
markers.push({ key: node.key, shadowedSeqs: node.data?.shadowedSeqs });
|
|
464
|
-
}
|
|
485
|
+
if (node.kind === "recall-marker" && !node.data?.compact) markers.push(node);
|
|
465
486
|
}
|
|
466
|
-
if (markers.length === 0)
|
|
487
|
+
if (markers.length === 0) {
|
|
488
|
+
hidePlanCacheSnapshot = snapshot;
|
|
489
|
+
hidePlanCacheValue = EMPTY_HIDE_PLAN;
|
|
490
|
+
return hidePlanCacheValue;
|
|
491
|
+
}
|
|
492
|
+
const plans = /* @__PURE__ */ new Map();
|
|
467
493
|
const union = /* @__PURE__ */ new Set();
|
|
468
|
-
const perMarker = /* @__PURE__ */ new Map();
|
|
469
494
|
for (const marker of markers) {
|
|
470
|
-
const keys = hiddenKeysFor(marker.shadowedSeqs, nodes);
|
|
471
|
-
|
|
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 });
|
|
472
498
|
if (keys !== null) for (const key of keys) union.add(key);
|
|
473
499
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
hiddenFor: (key) =>
|
|
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
|
|
478
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) {
|
|
524
|
+
return useSession((snapshot) => {
|
|
525
|
+
if (seq === void 0 || seq === null) return false;
|
|
526
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
527
|
+
if (node.kind !== "recall-marker" && typeof node.anchorSeq === "number" && node.anchorSeq === seq) {
|
|
528
|
+
return rowHiddenByKey(snapshot, node.key);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
function useShadowed(useSession, seq) {
|
|
535
|
+
return useSession((snapshot) => {
|
|
536
|
+
if (seq === void 0 || seq === null) return false;
|
|
537
|
+
for (const node of snapshot.chat.nodes.values()) {
|
|
538
|
+
if (node.kind === "recall-marker" && Array.isArray(node.data?.shadowedSeqs) && node.data.shadowedSeqs.includes(seq)) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
479
543
|
});
|
|
480
544
|
}
|
|
481
545
|
function useMarkerDismissed(useSession, markerSeq, op) {
|
|
@@ -512,10 +576,11 @@ return {
|
|
|
512
576
|
}
|
|
513
577
|
function AssistantActions({ messageId, sessionId, useSession, t }) {
|
|
514
578
|
const seq = useMessageSeq(useSession, messageId);
|
|
579
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
515
580
|
const shadowed = useShadowed(useSession, seq);
|
|
516
581
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
517
582
|
const [failure, setFailure] = (0, import_react.useState)(null);
|
|
518
|
-
if (shadowed || seq === void 0) return null;
|
|
583
|
+
if (hidden || shadowed || seq === void 0) return null;
|
|
519
584
|
const run = (op) => {
|
|
520
585
|
setBusy(true);
|
|
521
586
|
setFailure(null);
|
|
@@ -558,11 +623,11 @@ return {
|
|
|
558
623
|
}
|
|
559
624
|
function ReferenceRow({ node, useSession, t }) {
|
|
560
625
|
const { seq, messageId } = node.data;
|
|
561
|
-
const
|
|
626
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
562
627
|
const markerRef = useEditReference(useSession, seq);
|
|
563
628
|
const referenceText = editReferences.get(messageId) ?? markerRef;
|
|
564
629
|
const config = useConfig();
|
|
565
|
-
if (
|
|
630
|
+
if (hidden) return null;
|
|
566
631
|
if (referenceText === null || !config.showOriginalInput) return null;
|
|
567
632
|
return (0, import_react.createElement)("div", { className: "dsh-rt-user-row" }, [
|
|
568
633
|
(0, import_react.createElement)("details", { className: "dsh-rt-reference" }, [
|
|
@@ -577,12 +642,13 @@ return {
|
|
|
577
642
|
}
|
|
578
643
|
function UserActionsRow({ node, sessionId, useSession, inputActions, t }) {
|
|
579
644
|
const { seq, messageId, content } = node.data;
|
|
645
|
+
const hidden = useSeqHidden(useSession, seq);
|
|
580
646
|
const shadowed = useShadowed(useSession, seq);
|
|
581
647
|
const [editing, setEditing] = (0, import_react.useState)(false);
|
|
582
648
|
const [draft, setDraft] = (0, import_react.useState)("");
|
|
583
649
|
const [busy, setBusy] = (0, import_react.useState)(false);
|
|
584
650
|
const [failure, setFailure] = (0, import_react.useState)(null);
|
|
585
|
-
if (shadowed) return null;
|
|
651
|
+
if (hidden || shadowed) return null;
|
|
586
652
|
const openEditor = () => {
|
|
587
653
|
setDraft(textOf(content));
|
|
588
654
|
setFailure(null);
|
|
@@ -675,16 +741,26 @@ return {
|
|
|
675
741
|
]);
|
|
676
742
|
}
|
|
677
743
|
function RecallMarkerRow({ node, useSession, t }) {
|
|
678
|
-
const { seq, op, shadowedSeqs, legacy } = node.data;
|
|
744
|
+
const { seq, op, shadowedSeqs, legacy, compact } = node.data;
|
|
745
|
+
if (compact) return null;
|
|
679
746
|
const dismissed = useMarkerDismissed(useSession, seq, op);
|
|
680
747
|
const hidePlan = useMarkerHidePlan(useSession);
|
|
681
|
-
const
|
|
748
|
+
const plan = hidePlan.planFor(node.key);
|
|
749
|
+
const hiddenKeys = legacy || !getConfig().hideShadowed ? null : plan?.keys ?? null;
|
|
682
750
|
const css = hiddenKeys === null ? null : hiddenKeys.map((key) => `[data-chat-anchor-key=${JSON.stringify(key)}]{display:none!important}`).join("");
|
|
683
751
|
const count = Array.isArray(shadowedSeqs) ? shadowedSeqs.length : 0;
|
|
684
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;
|
|
685
759
|
return (0, import_react.createElement)("div", { className: "dsh-rt-marker-block", "data-dismissed": dismissed || void 0 }, [
|
|
686
760
|
css !== null && (0, import_react.createElement)("style", { key: "hide", dangerouslySetInnerHTML: { __html: css } }),
|
|
687
|
-
!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
|
|
688
764
|
]);
|
|
689
765
|
}
|
|
690
766
|
function OptionsRow({ t }) {
|
|
@@ -761,6 +837,7 @@ return {
|
|
|
761
837
|
.dsh-rt-error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;max-width:min(525px,82%)}
|
|
762
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}
|
|
763
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}
|
|
764
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}
|
|
765
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}
|
|
766
843
|
.dsh-rt-reference summary::-webkit-details-marker{display:none}
|
|
@@ -842,7 +919,7 @@ return {
|
|
|
842
919
|
console.warn(`[dsh-retrace] no tab order registered for "${viewId}"`);
|
|
843
920
|
return;
|
|
844
921
|
}
|
|
845
|
-
const buttons = [...document.querySelectorAll('[role="tablist"] [role="tab"]')];
|
|
922
|
+
const buttons = [...document.querySelectorAll('[role="tablist"]')].filter((tablist) => tablist.getBoundingClientRect().width > 0).flatMap((tablist) => [...tablist.querySelectorAll('[role="tab"]')]);
|
|
846
923
|
const button = buttons[index];
|
|
847
924
|
if (!button) {
|
|
848
925
|
console.warn(`[dsh-retrace] tab "${viewId}" (index ${index}) not found in the conversation tab bar`);
|
|
@@ -850,6 +927,11 @@ return {
|
|
|
850
927
|
}
|
|
851
928
|
button.click();
|
|
852
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
|
+
}
|
|
853
935
|
function waitForElement(selector, frames) {
|
|
854
936
|
return new Promise((resolve) => {
|
|
855
937
|
let remaining = frames;
|
|
@@ -885,9 +967,9 @@ return {
|
|
|
885
967
|
let key = keyOfSeq(anchorSeq);
|
|
886
968
|
let pages = 0;
|
|
887
969
|
while (key === null && pages < JUMP_PAGE_BUDGET && store.hasMore) {
|
|
888
|
-
const before = store.getSnapshot()?.chat?.nodes
|
|
970
|
+
const before = nodeCountOf(store.getSnapshot()?.chat?.nodes);
|
|
889
971
|
await store.loadOlder();
|
|
890
|
-
const after = store.getSnapshot()?.chat?.nodes
|
|
972
|
+
const after = nodeCountOf(store.getSnapshot()?.chat?.nodes);
|
|
891
973
|
pages += 1;
|
|
892
974
|
if (after === before) break;
|
|
893
975
|
key = keyOfSeq(anchorSeq);
|
|
@@ -1292,9 +1374,12 @@ return {
|
|
|
1292
1374
|
const t = ctx.locale.bind(NS);
|
|
1293
1375
|
const conversationEvents = ctx.get("conversationEvents");
|
|
1294
1376
|
if (conversationEvents) {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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");
|
|
1298
1383
|
}
|
|
1299
1384
|
ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
1300
1385
|
name: "conversation.chat.assistant-actions",
|
package/lib/versioning.js
CHANGED
|
@@ -246,7 +246,11 @@ export function createVersioningSeam(ctx, log = () => {}, options = {}) {
|
|
|
246
246
|
const global = opened.global.get()
|
|
247
247
|
if (global) {
|
|
248
248
|
// Seed session configs with the durable defaults (per-request overrides win).
|
|
249
|
-
|
|
249
|
+
// The domain schema spells the git switch `gitEnabled` (0.4.x review:
|
|
250
|
+
// it was merged in but never read — consumers use `config.git`).
|
|
251
|
+
const seeded = { ...global, git: global.gitEnabled ?? configs.get('')?.git ?? true }
|
|
252
|
+
delete seeded.gitEnabled
|
|
253
|
+
for (const [id, config] of configs) setConfig(id, { ...config, ...seeded })
|
|
250
254
|
}
|
|
251
255
|
})
|
|
252
256
|
.catch((error) => log(`retrace: versioning disabled: ${String(error)}`))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-retrace",
|
|
3
3
|
"description": "Retrace · 回溯 — Recall, edit-and-resend, regenerate, and conversation/artifact versioning (timeline, rollback, fork map) for DeepSeek Harness — Web and Desktop",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"packageManager": "pnpm@11.7.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|