deepseek-harness-background 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +437 -73
- package/lib/client.js.map +1 -1
- package/lib/index.js +12 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -75,7 +75,13 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
*
|
|
76
76
|
* Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
|
|
77
77
|
* `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
|
|
78
|
-
* SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions
|
|
78
|
+
* SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions.
|
|
79
|
+
*
|
|
80
|
+
* MAINTENANCE NOTE: the suffix anchors are the host's only stable-enough
|
|
81
|
+
* integration surface today, but a host restyle that renames them silently
|
|
82
|
+
* drops the glass. Upgrade path: once the host ships stable data-* anchors
|
|
83
|
+
* for these surfaces, swap the selector lists below to them and delete the
|
|
84
|
+
* suffix entries.
|
|
79
85
|
* `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
|
|
80
86
|
* `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
|
|
81
87
|
* it), `_newSession` excludes `Label` (the button's inner label span) and
|
|
@@ -440,6 +446,8 @@ window.__ModuleLoader__.load({
|
|
|
440
446
|
observer;
|
|
441
447
|
active = false;
|
|
442
448
|
settings;
|
|
449
|
+
/** Whether the current section resolves to an actual image source. */
|
|
450
|
+
hasSource = false;
|
|
443
451
|
savedTokens = /* @__PURE__ */ new Map();
|
|
444
452
|
savedVars = /* @__PURE__ */ new Map();
|
|
445
453
|
/** Remember + hijack a property only once so dispose restores the original. */
|
|
@@ -468,6 +476,7 @@ window.__ModuleLoader__.load({
|
|
|
468
476
|
this.restoreFrameTokens();
|
|
469
477
|
this.applySurfaceGlass(settings, true);
|
|
470
478
|
this.active = false;
|
|
479
|
+
this.hasSource = false;
|
|
471
480
|
this.removeLayers();
|
|
472
481
|
document.body.removeAttribute(ACTIVE_ATTR);
|
|
473
482
|
return;
|
|
@@ -483,10 +492,20 @@ window.__ModuleLoader__.load({
|
|
|
483
492
|
this.img.className = IMAGE_CLASS;
|
|
484
493
|
this.img.alt = "";
|
|
485
494
|
this.img.referrerPolicy = "no-referrer";
|
|
495
|
+
this.img.onerror = () => {
|
|
496
|
+
if (this.img === null || this.img.dataset.failed === "1") return;
|
|
497
|
+
this.img.dataset.failed = "1";
|
|
498
|
+
this.img.style.visibility = "hidden";
|
|
499
|
+
console.warn("[deepseek-harness-background] wallpaper image failed to load:", this.img.getAttribute("src"));
|
|
500
|
+
};
|
|
486
501
|
this.layer.appendChild(this.img);
|
|
487
502
|
document.body.appendChild(this.layer);
|
|
488
503
|
}
|
|
489
|
-
if (this.img && this.img.getAttribute("src") !== url)
|
|
504
|
+
if (this.img && this.img.getAttribute("src") !== url) {
|
|
505
|
+
delete this.img.dataset.failed;
|
|
506
|
+
this.img.style.visibility = "";
|
|
507
|
+
this.img.src = url;
|
|
508
|
+
}
|
|
490
509
|
if (!this.scrim) {
|
|
491
510
|
this.scrim = document.createElement("div");
|
|
492
511
|
this.scrim.className = SCRIM_CLASS;
|
|
@@ -494,6 +513,7 @@ window.__ModuleLoader__.load({
|
|
|
494
513
|
}
|
|
495
514
|
document.body.setAttribute(ACTIVE_ATTR, "on");
|
|
496
515
|
this.active = true;
|
|
516
|
+
this.hasSource = true;
|
|
497
517
|
document.body.style;
|
|
498
518
|
this.setVar("--bg-object-fit", settings.fit);
|
|
499
519
|
this.setVar("--bg-opacity", String(settings.opacity));
|
|
@@ -546,6 +566,15 @@ window.__ModuleLoader__.load({
|
|
|
546
566
|
case "scrim":
|
|
547
567
|
this.setVar("--bg-scrim", String(value));
|
|
548
568
|
break;
|
|
569
|
+
case "panelOpacity": {
|
|
570
|
+
const next = {
|
|
571
|
+
...this.settings,
|
|
572
|
+
panelOpacity: Number(value)
|
|
573
|
+
};
|
|
574
|
+
this.settings = next;
|
|
575
|
+
this.applySurfaceGlass(next, !this.hasSource);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
549
578
|
case "blur":
|
|
550
579
|
this.setVar("--bg-glass-blur", `${value}px`);
|
|
551
580
|
this.setVar("--bg-glass-saturate", String(glassSaturate(Number(value))));
|
|
@@ -696,14 +725,22 @@ window.__ModuleLoader__.load({
|
|
|
696
725
|
const REGISTRY_CSS_TAG = "deepseek-harness-background/glass-registry";
|
|
697
726
|
/** Plugin identity stamped onto the registry <style> element. */
|
|
698
727
|
const PLUGIN_ID = "deepseek-harness-background";
|
|
699
|
-
/**
|
|
728
|
+
/**
|
|
729
|
+
* Characters whose presence would let a selector escape its rule context.
|
|
730
|
+
* ',' is refused too: inside the generated rule body
|
|
731
|
+
* (`body[gate] <selector> { … }`) a comma lets one registration broaden the
|
|
732
|
+
* rule to ARBITRARY extra subjects (`.x, html body`) — an unintended
|
|
733
|
+
* widening of the glass sheet beyond the registered surface. Multiple
|
|
734
|
+
* surfaces stay supported through the selectors ARRAY.
|
|
735
|
+
*/
|
|
700
736
|
const FORBIDDEN_SELECTOR_CHARS = [
|
|
701
737
|
"{",
|
|
702
738
|
"}",
|
|
703
739
|
";",
|
|
704
740
|
"@",
|
|
705
741
|
"<",
|
|
706
|
-
">"
|
|
742
|
+
">",
|
|
743
|
+
","
|
|
707
744
|
];
|
|
708
745
|
/** Hard cap on one selector's length (diagnostics-friendly, abuse-proof). */
|
|
709
746
|
const MAX_SELECTOR_LENGTH = 500;
|
|
@@ -916,7 +953,8 @@ window.__ModuleLoader__.load({
|
|
|
916
953
|
"timeline.marksOnly": "★ 只看标记",
|
|
917
954
|
"timeline.mark": "标记为重点",
|
|
918
955
|
"timeline.unmark": "取消重点标记",
|
|
919
|
-
"timeline.noMarks": "还没有标记的重点"
|
|
956
|
+
"timeline.noMarks": "还没有标记的重点",
|
|
957
|
+
"timeline.cannotJump": "无法跳转:这条历史消息缺少定位信息"
|
|
920
958
|
};
|
|
921
959
|
/** English copy. */
|
|
922
960
|
const en = {
|
|
@@ -952,7 +990,8 @@ window.__ModuleLoader__.load({
|
|
|
952
990
|
"timeline.marksOnly": "★ Marked only",
|
|
953
991
|
"timeline.mark": "Mark as key point",
|
|
954
992
|
"timeline.unmark": "Unmark key point",
|
|
955
|
-
"timeline.noMarks": "No marked key points"
|
|
993
|
+
"timeline.noMarks": "No marked key points",
|
|
994
|
+
"timeline.cannotJump": "Cannot jump: this history message lacks position info"
|
|
956
995
|
};
|
|
957
996
|
//#endregion
|
|
958
997
|
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -1087,40 +1126,40 @@ window.__ModuleLoader__.load({
|
|
|
1087
1126
|
document.head.appendChild(tag);
|
|
1088
1127
|
}
|
|
1089
1128
|
var SettingsRow_module_css_default = {
|
|
1090
|
-
"
|
|
1091
|
-
"preview": "-ozqdq_preview",
|
|
1092
|
-
"sliderLabel": "-ozqdq_sliderLabel",
|
|
1093
|
-
"segRow": "-ozqdq_segRow",
|
|
1094
|
-
"urlRow": "-ozqdq_urlRow",
|
|
1095
|
-
"previewImg": "-ozqdq_previewImg",
|
|
1096
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
1097
|
-
"hint": "-ozqdq_hint",
|
|
1098
|
-
"header": "-ozqdq_header",
|
|
1099
|
-
"slider": "-ozqdq_slider",
|
|
1100
|
-
"title": "-ozqdq_title",
|
|
1129
|
+
"switchKnob": "-ozqdq_switchKnob",
|
|
1101
1130
|
"segLabel": "-ozqdq_segLabel",
|
|
1102
|
-
"
|
|
1131
|
+
"btnDanger": "-ozqdq_btnDanger",
|
|
1132
|
+
"row": "-ozqdq_row",
|
|
1133
|
+
"previewGlassText": "-ozqdq_previewGlassText",
|
|
1134
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
1135
|
+
"urlRow": "-ozqdq_urlRow",
|
|
1136
|
+
"clearRow": "-ozqdq_clearRow",
|
|
1103
1137
|
"error": "-ozqdq_error",
|
|
1104
|
-
"
|
|
1105
|
-
"
|
|
1106
|
-
"
|
|
1107
|
-
"sectionLabel": "-ozqdq_sectionLabel",
|
|
1108
|
-
"switchKnob": "-ozqdq_switchKnob",
|
|
1109
|
-
"section": "-ozqdq_section",
|
|
1138
|
+
"sliderLabel": "-ozqdq_sliderLabel",
|
|
1139
|
+
"title": "-ozqdq_title",
|
|
1140
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
1110
1141
|
"sliderValue": "-ozqdq_sliderValue",
|
|
1111
|
-
"
|
|
1142
|
+
"titleRow": "-ozqdq_titleRow",
|
|
1143
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
1144
|
+
"segGroup": "-ozqdq_segGroup",
|
|
1145
|
+
"segBtn": "-ozqdq_segBtn",
|
|
1146
|
+
"hint": "-ozqdq_hint",
|
|
1147
|
+
"urlInput": "-ozqdq_urlInput",
|
|
1112
1148
|
"previewScrim": "-ozqdq_previewScrim",
|
|
1149
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
1150
|
+
"previewImg": "-ozqdq_previewImg",
|
|
1151
|
+
"slider": "-ozqdq_slider",
|
|
1113
1152
|
"desc": "-ozqdq_desc",
|
|
1114
|
-
"
|
|
1115
|
-
"clearRow": "-ozqdq_clearRow",
|
|
1153
|
+
"segRow": "-ozqdq_segRow",
|
|
1116
1154
|
"segActive": "-ozqdq_segActive",
|
|
1155
|
+
"section": "-ozqdq_section",
|
|
1156
|
+
"btnPrimary": "-ozqdq_btnPrimary",
|
|
1157
|
+
"preview": "-ozqdq_preview",
|
|
1158
|
+
"sectionLabel": "-ozqdq_sectionLabel",
|
|
1159
|
+
"header": "-ozqdq_header",
|
|
1117
1160
|
"sliderRow": "-ozqdq_sliderRow",
|
|
1161
|
+
"switch": "-ozqdq_switch",
|
|
1118
1162
|
"btn": "-ozqdq_btn",
|
|
1119
|
-
"titleRow": "-ozqdq_titleRow",
|
|
1120
|
-
"btnDanger": "-ozqdq_btnDanger",
|
|
1121
|
-
"previewGlassText": "-ozqdq_previewGlassText",
|
|
1122
|
-
"sourceRow": "-ozqdq_sourceRow",
|
|
1123
|
-
"segBtn": "-ozqdq_segBtn",
|
|
1124
1163
|
"previewEmpty": "-ozqdq_previewEmpty"
|
|
1125
1164
|
};
|
|
1126
1165
|
//#endregion
|
|
@@ -1320,8 +1359,7 @@ window.__ModuleLoader__.load({
|
|
|
1320
1359
|
};
|
|
1321
1360
|
draftRef.current = next;
|
|
1322
1361
|
markEditing();
|
|
1323
|
-
|
|
1324
|
-
else paintBackgroundKnob(key, value);
|
|
1362
|
+
paintBackgroundKnob(key, value);
|
|
1325
1363
|
if (previewRef.current) paintPreviewSurface(previewRef.current, next);
|
|
1326
1364
|
}, [markEditing]);
|
|
1327
1365
|
/** Persist one section through the host route and adopt it on success. */
|
|
@@ -1329,6 +1367,7 @@ window.__ModuleLoader__.load({
|
|
|
1329
1367
|
dirtyRef.current = true;
|
|
1330
1368
|
const result = await settingsClient.save(next);
|
|
1331
1369
|
if (result === "failed") {
|
|
1370
|
+
dirtyRef.current = false;
|
|
1332
1371
|
setError(t("background.saveFailed"));
|
|
1333
1372
|
return;
|
|
1334
1373
|
}
|
|
@@ -1877,6 +1916,8 @@ window.__ModuleLoader__.load({
|
|
|
1877
1916
|
display: flex; align-items: center; justify-content: flex-end;
|
|
1878
1917
|
}
|
|
1879
1918
|
.dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
|
|
1919
|
+
.dsbt-jump:disabled { cursor: default; opacity: .55; }
|
|
1920
|
+
.dsbt-jump:disabled .dsbt-title { text-decoration: line-through; text-decoration-color: currentColor; }
|
|
1880
1921
|
.dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
|
|
1881
1922
|
body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
|
|
1882
1923
|
body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
|
|
@@ -1973,9 +2014,15 @@ window.__ModuleLoader__.load({
|
|
|
1973
2014
|
* - Key-point bookmarks: star a question in the expanded panel, persisted per
|
|
1974
2015
|
* session in localStorage; marked rows show a golden tick in the capsule
|
|
1975
2016
|
* and a "marked only" filter narrows the list.
|
|
1976
|
-
* - Jump stabilization: the reading-position tracker freezes while a
|
|
1977
|
-
*
|
|
1978
|
-
* fallback), so the panel cannot jitter mid-jump.
|
|
2017
|
+
* - Jump stabilization: the reading-position tracker freezes while a jump
|
|
2018
|
+
* glide animates (cleared 150ms after the last scroll event, plus a 2s
|
|
2019
|
+
* fallback), so the panel cannot jitter mid-jump. The glide itself is a
|
|
2020
|
+
* rAF ease-in-out on the conversation scrollport: a ~26px bottom-follow
|
|
2021
|
+
* detach first settles the official at-bottom flag outside its 25px zone
|
|
2022
|
+
* (so a tip-moved re-render cannot yank the slide back to the floor — the
|
|
2023
|
+
* earlier instant+smooth double pass kept the jump working but made the
|
|
2024
|
+
* slide invisible), and wheel/touch/keyboard input cancels it so the
|
|
2025
|
+
* reader takes over instantly.
|
|
1979
2026
|
* - Rewind integration: messages withdrawn by a rewind command node are
|
|
1980
2027
|
* filtered out of the rail.
|
|
1981
2028
|
*
|
|
@@ -2048,16 +2095,73 @@ window.__ModuleLoader__.load({
|
|
|
2048
2095
|
else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
|
|
2049
2096
|
} catch {}
|
|
2050
2097
|
}
|
|
2098
|
+
/** Parse digits into a finite number (undefined otherwise). */
|
|
2099
|
+
function parseSeqDigits(digits) {
|
|
2100
|
+
if (digits === void 0) return void 0;
|
|
2101
|
+
const n = Number.parseInt(digits, 10);
|
|
2102
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2103
|
+
}
|
|
2104
|
+
/** Extract a seq from a string-ish field ('@42', 'seq 42', '#42', bare 42). */
|
|
2105
|
+
function seqFromStringish(value) {
|
|
2106
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2107
|
+
if (typeof value !== "string") return void 0;
|
|
2108
|
+
const m = /(?:@|seq\s*|#)?(\d+)/i.exec(value);
|
|
2109
|
+
return m === null ? void 0 : parseSeqDigits(m[1]);
|
|
2110
|
+
}
|
|
2051
2111
|
/**
|
|
2052
|
-
* Extract the rewind target seq from a rewind outcome text.
|
|
2112
|
+
* Extract the rewind target seq from a rewind outcome text. Multi-pattern
|
|
2113
|
+
* (ported from dsh-chat-timeline v0.1.5): an explicit `seq 42` declaration
|
|
2114
|
+
* wins, then `#42`, then the broad english/chinese verb forms — the old
|
|
2115
|
+
* single "rewound" regex silently missed real success texts like "已撤回
|
|
2116
|
+
* seq 42" / "Withdrawn seq 42" (upstream issue #6).
|
|
2053
2117
|
* @param text - the command outcome text.
|
|
2054
2118
|
*/
|
|
2055
2119
|
function rewindTargetOfOutcome(text) {
|
|
2056
2120
|
if (typeof text !== "string") return void 0;
|
|
2057
|
-
const
|
|
2058
|
-
if (
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2121
|
+
const seqMatch = /seq\s*(\d+)/i.exec(text);
|
|
2122
|
+
if (seqMatch !== null) return parseSeqDigits(seqMatch[1]);
|
|
2123
|
+
const hashMatch = /#(\d+)/.exec(text);
|
|
2124
|
+
if (hashMatch !== null) return parseSeqDigits(hashMatch[1]);
|
|
2125
|
+
const broadMatch = /(?:rewound|withdrawn|已撤回).*?(?:target\s+)?(\d+)/i.exec(text);
|
|
2126
|
+
if (broadMatch !== null) return parseSeqDigits(broadMatch[1]);
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Resolve the target of a rewind command: structured fields first
|
|
2130
|
+
* (outcome.targetSeq / outcome.target / args.targetSeq / args.target /
|
|
2131
|
+
* args.seq / args.raw / args[0]), then the outcome text — mirrors upstream
|
|
2132
|
+
* v0.1.5 so locale-independent producers keep parsing without text
|
|
2133
|
+
* heuristics (the args.seq guard skips the command's own seq).
|
|
2134
|
+
* @param command - the command payload.
|
|
2135
|
+
*/
|
|
2136
|
+
function rewindTargetOfCommand(command) {
|
|
2137
|
+
if (command === null || typeof command !== "object") return void 0;
|
|
2138
|
+
const rec = command;
|
|
2139
|
+
const outcome = rec.outcome;
|
|
2140
|
+
if (outcome !== null && typeof outcome === "object") {
|
|
2141
|
+
const o = outcome;
|
|
2142
|
+
if (typeof o.targetSeq === "number" && Number.isFinite(o.targetSeq)) return o.targetSeq;
|
|
2143
|
+
if (typeof o.target === "number" && Number.isFinite(o.target)) return o.target;
|
|
2144
|
+
}
|
|
2145
|
+
const args = rec.args;
|
|
2146
|
+
if (args !== null && typeof args === "object") {
|
|
2147
|
+
const record = args;
|
|
2148
|
+
if (typeof record.targetSeq === "number" && Number.isFinite(record.targetSeq)) return record.targetSeq;
|
|
2149
|
+
if (typeof record.target === "number" && Number.isFinite(record.target)) return record.target;
|
|
2150
|
+
if (typeof record.seq === "number" && Number.isFinite(record.seq) && record.seq !== rec.seq) return record.seq;
|
|
2151
|
+
const rawish = record.raw ?? record["0"] ?? (Array.isArray(args) ? args[0] : void 0);
|
|
2152
|
+
if (rawish !== void 0) {
|
|
2153
|
+
const n = seqFromStringish(rawish);
|
|
2154
|
+
if (n !== void 0) return n;
|
|
2155
|
+
}
|
|
2156
|
+
if (typeof record.target === "string") {
|
|
2157
|
+
const n = seqFromStringish(record.target);
|
|
2158
|
+
if (n !== void 0) return n;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
if (outcome !== null && typeof outcome === "object") {
|
|
2162
|
+
const text = outcome.text;
|
|
2163
|
+
if (typeof text === "string") return rewindTargetOfOutcome(text);
|
|
2164
|
+
}
|
|
2061
2165
|
}
|
|
2062
2166
|
/**
|
|
2063
2167
|
* Is this rewind command a preview/dry-run (marks the target hidden without
|
|
@@ -2077,6 +2181,7 @@ window.__ModuleLoader__.load({
|
|
|
2077
2181
|
*/
|
|
2078
2182
|
function hiddenSeqsOfChat(chat) {
|
|
2079
2183
|
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
2184
|
+
for (const seq of rewindMarkedSeqsOfChat(chat)) hidden.add(seq);
|
|
2080
2185
|
const spans = rewindSpansOfChat(chat);
|
|
2081
2186
|
if (spans.length === 0) return hidden;
|
|
2082
2187
|
const nodes = chat.nodes;
|
|
@@ -2090,6 +2195,32 @@ window.__ModuleLoader__.load({
|
|
|
2090
2195
|
return hidden;
|
|
2091
2196
|
}
|
|
2092
2197
|
/**
|
|
2198
|
+
* Seq numbers of nodes carrying a rewind-hidden marker (upstream v0.1.5):
|
|
2199
|
+
* host/plugin producers can tag the node record itself (rewindHidden), its
|
|
2200
|
+
* data payload, or the payload's attributes map.
|
|
2201
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
2202
|
+
*/
|
|
2203
|
+
function rewindMarkedSeqsOfChat(chat) {
|
|
2204
|
+
const marked = /* @__PURE__ */ new Set();
|
|
2205
|
+
const nodes = chat.nodes;
|
|
2206
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return marked;
|
|
2207
|
+
for (const node of nodes.values()) {
|
|
2208
|
+
if (node === null || typeof node !== "object") continue;
|
|
2209
|
+
const data = node.data;
|
|
2210
|
+
if (data === null || typeof data !== "object") continue;
|
|
2211
|
+
const record = data;
|
|
2212
|
+
const attrs = record.attributes;
|
|
2213
|
+
if (attrs !== null && typeof attrs === "object" && attrs["data-dsh-rewind-hidden"] === true) {
|
|
2214
|
+
if (typeof node.anchorSeq === "number") marked.add(node.anchorSeq);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
if (record["data-dsh-rewind-hidden"] === true || node.rewindHidden === true) {
|
|
2218
|
+
if (typeof node.anchorSeq === "number") marked.add(node.anchorSeq);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return marked;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2093
2224
|
* Seq numbers a rewind hid directly: successful commands hide their own row;
|
|
2094
2225
|
* preview commands hide their target.
|
|
2095
2226
|
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
@@ -2137,7 +2268,7 @@ window.__ModuleLoader__.load({
|
|
|
2137
2268
|
if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
|
|
2138
2269
|
const marker = outcome.sourceEventSeq;
|
|
2139
2270
|
if (typeof marker !== "number") continue;
|
|
2140
|
-
const target =
|
|
2271
|
+
const target = rewindTargetOfCommand(command);
|
|
2141
2272
|
if (target !== void 0) spans.push({
|
|
2142
2273
|
start: target,
|
|
2143
2274
|
end: marker
|
|
@@ -2175,6 +2306,19 @@ window.__ModuleLoader__.load({
|
|
|
2175
2306
|
out.sort((a, b) => a.seq - b.seq);
|
|
2176
2307
|
return out;
|
|
2177
2308
|
}
|
|
2309
|
+
/**
|
|
2310
|
+
* Count the user messages the loaded window holds WITHOUT collecting or
|
|
2311
|
+
* sorting them — the fast gate railMessages uses to skip the merge path on
|
|
2312
|
+
* the streaming hot path (see there).
|
|
2313
|
+
* @param chat - the loaded chat snapshot.
|
|
2314
|
+
*/
|
|
2315
|
+
function countWindowUserMessages(chat) {
|
|
2316
|
+
const nodes = chat?.nodes;
|
|
2317
|
+
if (nodes === void 0 || typeof nodes.values !== "function") return 0;
|
|
2318
|
+
let count = 0;
|
|
2319
|
+
for (const node of nodes.values()) if (node !== null && typeof node === "object" && node.kind === "user") count += 1;
|
|
2320
|
+
return count;
|
|
2321
|
+
}
|
|
2178
2322
|
/** Resolve the DOM anchor key of one entry (direct key form only). */
|
|
2179
2323
|
function resolveAnchorKey(m) {
|
|
2180
2324
|
return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
|
|
@@ -2219,24 +2363,45 @@ window.__ModuleLoader__.load({
|
|
|
2219
2363
|
return out;
|
|
2220
2364
|
}
|
|
2221
2365
|
/**
|
|
2222
|
-
* Rail messages
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2225
|
-
*
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2366
|
+
* Rail messages: the host projection (whole-session index) MERGED with the
|
|
2367
|
+
* loaded chat-node window. The projection normally wins outright, but a
|
|
2368
|
+
* projection that lost its baseline (e.g. the plugin was hot-reloaded after
|
|
2369
|
+
* the session's tail page seeded the client store) can miss early entries —
|
|
2370
|
+
* the window fills those gaps whenever it holds them, and window entries
|
|
2371
|
+
* lend their real anchor key to projected entries whose durable id was
|
|
2372
|
+
* absent (early events without one). Locally-known rewind hiding applies to
|
|
2373
|
+
* the merged list so a cut lands instantly, before the next projection push.
|
|
2374
|
+
* @param snapshot - the session conversation snapshot (node window).
|
|
2228
2375
|
* @param projected - the raw bgTimeline projection value.
|
|
2229
2376
|
*/
|
|
2230
2377
|
function railMessages(snapshot, projected) {
|
|
2231
2378
|
const projectedMessages = normalizeProjectedTimeline(projected);
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2379
|
+
const chat = snapshot?.chat;
|
|
2380
|
+
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
2381
|
+
const marked = rewindMarkedSeqsOfChat(chat);
|
|
2382
|
+
const spans = rewindSpansOfChat(chat);
|
|
2383
|
+
const isHidden = (seq) => hidden.has(seq) || marked.has(seq) || spans.some((span) => seq >= span.start && seq <= span.end);
|
|
2384
|
+
if (projectedMessages.length === 0) return collectMessages(snapshot);
|
|
2385
|
+
const windowUserCount = countWindowUserMessages(chat);
|
|
2386
|
+
const projectionLacksKeys = projectedMessages.some((m) => m.key === void 0);
|
|
2387
|
+
if (windowUserCount <= projectedMessages.length && !projectionLacksKeys) return marked.size === 0 && hidden.size === 0 && spans.length === 0 ? projectedMessages : projectedMessages.filter((m) => !isHidden(m.seq));
|
|
2388
|
+
const windowMessages = collectMessages(snapshot);
|
|
2389
|
+
const bySeq = /* @__PURE__ */ new Map();
|
|
2390
|
+
for (const m of projectedMessages) if (!isHidden(m.seq)) bySeq.set(m.seq, m);
|
|
2391
|
+
for (const m of windowMessages) {
|
|
2392
|
+
if (isHidden(m.seq)) continue;
|
|
2393
|
+
const existing = bySeq.get(m.seq);
|
|
2394
|
+
if (existing === void 0) {
|
|
2395
|
+
bySeq.set(m.seq, m);
|
|
2396
|
+
continue;
|
|
2397
|
+
}
|
|
2398
|
+
if (existing.key === void 0 && m.key !== void 0) bySeq.set(m.seq, {
|
|
2399
|
+
...existing,
|
|
2400
|
+
key: m.key,
|
|
2401
|
+
...existing.text === "" && m.text !== "" ? { text: m.text } : {}
|
|
2402
|
+
});
|
|
2238
2403
|
}
|
|
2239
|
-
return
|
|
2404
|
+
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
2240
2405
|
}
|
|
2241
2406
|
/**
|
|
2242
2407
|
* Outer rail height for a message count: rows + breathing room, clamped so
|
|
@@ -2270,29 +2435,221 @@ window.__ModuleLoader__.load({
|
|
|
2270
2435
|
}
|
|
2271
2436
|
return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
|
|
2272
2437
|
}
|
|
2273
|
-
/**
|
|
2274
|
-
|
|
2438
|
+
/** How long one jump may keep paging older history before giving up. */
|
|
2439
|
+
const JUMP_PAGE_DEADLINE_MS = 3e4;
|
|
2440
|
+
/** How long to wait for the chat view to commit the target row. */
|
|
2441
|
+
const JUMP_ROW_WAIT_MS = 3e3;
|
|
2442
|
+
/** How long the jump-stabilization lock may stay engaged without settling. */
|
|
2443
|
+
const JUMP_LOCK_FALLBACK_MS = 2e3;
|
|
2444
|
+
/**
|
|
2445
|
+
* Poll the conversation DOM until the target row is committed. The session
|
|
2446
|
+
* store updates BEFORE React renders the prepended page, so the old one-shot
|
|
2447
|
+
* query right after loadOlder() resolved could miss the row and silently
|
|
2448
|
+
* give up — the reported "click does nothing until the conversation is
|
|
2449
|
+
* nudged (scroll/click)" behavior.
|
|
2450
|
+
* @param key - chat anchor key to look for.
|
|
2451
|
+
* @param timeoutMs - polling budget.
|
|
2452
|
+
*/
|
|
2453
|
+
async function waitForChatRow(key, timeoutMs) {
|
|
2454
|
+
const scrollports = Array.from(document.querySelectorAll("[data-conversation-scroll]"));
|
|
2455
|
+
if (scrollports.length === 0) return null;
|
|
2456
|
+
const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
|
|
2457
|
+
const deadline = Date.now() + timeoutMs;
|
|
2458
|
+
for (;;) {
|
|
2459
|
+
for (const scrollport of scrollports) {
|
|
2460
|
+
const row = scrollport.querySelector(selector);
|
|
2461
|
+
if (row !== null) return row;
|
|
2462
|
+
}
|
|
2463
|
+
if (Date.now() >= deadline) return null;
|
|
2464
|
+
await delay(40);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/** Official conversation follow threshold: the view keeps bottom-following while within this distance of the floor (dsh-client-ui-conversation, 25px). */
|
|
2468
|
+
const CONVERSATION_FOLLOW_ZONE_PX = 25;
|
|
2469
|
+
/** Distances under this snap instead of animating. */
|
|
2470
|
+
const MIN_GLIDE_DISTANCE_PX = 8;
|
|
2471
|
+
/** Glide duration floor / cap — a far-boundary rail click stays a tangible slide. */
|
|
2472
|
+
const MIN_GLIDE_MS = 280;
|
|
2473
|
+
const MAX_GLIDE_MS = 900;
|
|
2474
|
+
/** Maximum scrollable offset of a conversation scrollport. */
|
|
2475
|
+
function scrollFloorOf(el) {
|
|
2476
|
+
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
2477
|
+
}
|
|
2478
|
+
/**
|
|
2479
|
+
* Destination scrollTop that centers `row` inside `el` — the same geometry
|
|
2480
|
+
* as scrollIntoView({ block: 'center' }), computed directly so the glide owns
|
|
2481
|
+
* the scroll position instead of the browser.
|
|
2482
|
+
* @param el - the conversation scrollport.
|
|
2483
|
+
* @param row - the target chat row.
|
|
2484
|
+
*/
|
|
2485
|
+
function centeredScrollTopFor(el, row) {
|
|
2486
|
+
const floor = scrollFloorOf(el);
|
|
2487
|
+
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
|
|
2488
|
+
if (!Number.isFinite(target)) return el.scrollTop;
|
|
2489
|
+
return Math.max(0, Math.min(floor, target));
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Glide duration for a distance: longer slides take longer, bounded to
|
|
2493
|
+
* [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history rail click reads as a
|
|
2494
|
+
* deliberate glide, not a sluggish race).
|
|
2495
|
+
* @param distance - px to travel.
|
|
2496
|
+
*/
|
|
2497
|
+
function glideDurationFor(distance) {
|
|
2498
|
+
if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
|
|
2499
|
+
return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Detach the official bottom-follow state when the reader currently sits
|
|
2503
|
+
* inside its follow zone: assign the scroll position just outside the 25px
|
|
2504
|
+
* boundary. That one synchronous scroll event settles at-bottom to false
|
|
2505
|
+
* (moved-by-reader + outside the zone) so a tip-moved re-render cannot yank
|
|
2506
|
+
* the glide back to the floor. No-op when already above the zone or when
|
|
2507
|
+
* the assignment cannot move (content shorter than the viewport).
|
|
2508
|
+
* @param el - the conversation scrollport.
|
|
2509
|
+
*/
|
|
2510
|
+
function detachBottomFollow(el) {
|
|
2511
|
+
const floor = scrollFloorOf(el);
|
|
2512
|
+
if (el.scrollTop < floor - CONVERSATION_FOLLOW_ZONE_PX) return false;
|
|
2513
|
+
const detached = Math.max(0, floor - CONVERSATION_FOLLOW_ZONE_PX - 1);
|
|
2514
|
+
if (Math.abs(el.scrollTop - detached) < 1) return false;
|
|
2515
|
+
el.scrollTop = detached;
|
|
2516
|
+
return true;
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Wait until a detach assignment's scroll event has been dispatched AND
|
|
2520
|
+
* consumed by the official scroll handler: scroll events fire on a later
|
|
2521
|
+
* frame than the assignment, and a paging commit's layout effect runs
|
|
2522
|
+
* before that dispatch — it would re-run the follow logic while the
|
|
2523
|
+
* at-bottom flag is still true and drag the viewport to the growing floor,
|
|
2524
|
+
* undoing the detach (two rAFs + a macrotask bracket the dispatch window).
|
|
2525
|
+
*/
|
|
2526
|
+
function settleScrollEvents() {
|
|
2527
|
+
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
|
2528
|
+
return new Promise((resolve) => {
|
|
2529
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.setTimeout(resolve, 0)));
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Animate el.scrollTop toward target with an ease-in-out cubic profile.
|
|
2534
|
+
* Wheel / touch / keyboard-scroll input cancels the glide so the reader
|
|
2535
|
+
* takes over immediately. Resolves true when the glide completed, false
|
|
2536
|
+
* when the reader cancelled it.
|
|
2537
|
+
* @param el - the conversation scrollport.
|
|
2538
|
+
* @param target - destination scrollTop.
|
|
2539
|
+
* @param durationMs - animation budget.
|
|
2540
|
+
*/
|
|
2541
|
+
function animateScrollTop(el, target, durationMs) {
|
|
2542
|
+
return new Promise((resolve) => {
|
|
2543
|
+
const start = el.scrollTop;
|
|
2544
|
+
const delta = target - start;
|
|
2545
|
+
let done = false;
|
|
2546
|
+
let frame = 0;
|
|
2547
|
+
const finish = (completed) => {
|
|
2548
|
+
if (done) return;
|
|
2549
|
+
done = true;
|
|
2550
|
+
el.removeEventListener("wheel", onWheel);
|
|
2551
|
+
el.removeEventListener("touchstart", onTouch);
|
|
2552
|
+
window.removeEventListener("keydown", onKey);
|
|
2553
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
2554
|
+
resolve(completed);
|
|
2555
|
+
};
|
|
2556
|
+
const onWheel = () => finish(false);
|
|
2557
|
+
const onTouch = () => finish(false);
|
|
2558
|
+
const onKey = (e) => {
|
|
2559
|
+
if ([
|
|
2560
|
+
"ArrowUp",
|
|
2561
|
+
"ArrowDown",
|
|
2562
|
+
"PageUp",
|
|
2563
|
+
"PageDown",
|
|
2564
|
+
"Home",
|
|
2565
|
+
"End",
|
|
2566
|
+
" "
|
|
2567
|
+
].includes(e.key)) finish(false);
|
|
2568
|
+
};
|
|
2569
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
2570
|
+
el.addEventListener("touchstart", onTouch, { passive: true });
|
|
2571
|
+
window.addEventListener("keydown", onKey);
|
|
2572
|
+
if (Math.abs(delta) < .5 || durationMs <= 0) {
|
|
2573
|
+
el.scrollTop = target;
|
|
2574
|
+
finish(true);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
const t0 = performance.now();
|
|
2578
|
+
const ease = (x) => x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
2579
|
+
const step = (now) => {
|
|
2580
|
+
if (done) return;
|
|
2581
|
+
const t = Math.min(1, (now - t0) / durationMs);
|
|
2582
|
+
el.scrollTop = start + delta * ease(t);
|
|
2583
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
2584
|
+
else finish(true);
|
|
2585
|
+
};
|
|
2586
|
+
frame = requestAnimationFrame(step);
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Ensure the target node is loaded, wait for the chat view to commit its row
|
|
2591
|
+
* into the DOM, then glide it into view. Paging uses a time budget instead
|
|
2592
|
+
* of the old fixed 120-iteration guard (each in-flight page burned 50ms of
|
|
2593
|
+
* that guard, so slow history could exhaust it and silently fail).
|
|
2594
|
+
*/
|
|
2595
|
+
async function jumpToMessage(sessionsService, sessionId, key, rowWaitMs = JUMP_ROW_WAIT_MS) {
|
|
2275
2596
|
const session = sessionsService.binding(sessionId)?.session;
|
|
2276
2597
|
if (session === void 0) return false;
|
|
2277
|
-
let
|
|
2278
|
-
|
|
2598
|
+
let detachedAny = false;
|
|
2599
|
+
for (const el of Array.from(document.querySelectorAll("[data-conversation-scroll]"))) if (detachBottomFollow(el)) {
|
|
2600
|
+
el.dispatchEvent(new Event("scroll"));
|
|
2601
|
+
detachedAny = true;
|
|
2602
|
+
}
|
|
2603
|
+
if (detachedAny) await settleScrollEvents();
|
|
2604
|
+
const deadline = Date.now() + JUMP_PAGE_DEADLINE_MS;
|
|
2605
|
+
for (;;) {
|
|
2279
2606
|
const snapshot = session.getSnapshot();
|
|
2280
2607
|
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
2281
|
-
if (snapshot?.hasMore !== true)
|
|
2608
|
+
if (snapshot?.hasMore !== true) break;
|
|
2609
|
+
if (Date.now() >= deadline) break;
|
|
2282
2610
|
if (snapshot.loadingOlder === true) {
|
|
2283
2611
|
await delay(50);
|
|
2284
2612
|
continue;
|
|
2285
2613
|
}
|
|
2286
|
-
|
|
2614
|
+
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
2615
|
+
await delay(100);
|
|
2616
|
+
continue;
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
await session.loadOlder();
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
console.warn("[deepseek-harness-background] timeline jump paging failed:", error);
|
|
2622
|
+
break;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
const row = await waitForChatRow(key, rowWaitMs);
|
|
2626
|
+
if (row === null) {
|
|
2627
|
+
console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${key}`);
|
|
2628
|
+
return false;
|
|
2287
2629
|
}
|
|
2288
|
-
const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
|
|
2289
|
-
if (row === null) return false;
|
|
2290
2630
|
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2291
|
-
row.
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2631
|
+
const scroller = row.closest("[data-conversation-scroll]");
|
|
2632
|
+
if (scroller === null) {
|
|
2633
|
+
row.scrollIntoView({
|
|
2634
|
+
behavior: "auto",
|
|
2635
|
+
block: "center"
|
|
2636
|
+
});
|
|
2637
|
+
if (!reducedMotion) row.scrollIntoView({
|
|
2638
|
+
behavior: "smooth",
|
|
2639
|
+
block: "center"
|
|
2640
|
+
});
|
|
2641
|
+
return true;
|
|
2642
|
+
}
|
|
2643
|
+
const target = centeredScrollTopFor(scroller, row);
|
|
2644
|
+
if (reducedMotion || Math.abs(target - scroller.scrollTop) < MIN_GLIDE_DISTANCE_PX) {
|
|
2645
|
+
scroller.scrollTop = target;
|
|
2646
|
+
return true;
|
|
2647
|
+
}
|
|
2648
|
+
if (detachBottomFollow(scroller)) {
|
|
2649
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
2650
|
+
await settleScrollEvents();
|
|
2651
|
+
}
|
|
2652
|
+
return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
|
|
2296
2653
|
}
|
|
2297
2654
|
/**
|
|
2298
2655
|
* Scroll only the rail itself so the active row stays visible (min distance).
|
|
@@ -2578,8 +2935,9 @@ window.__ModuleLoader__.load({
|
|
|
2578
2935
|
type: "button",
|
|
2579
2936
|
className: "dsbt-jump",
|
|
2580
2937
|
ref: isActive ? activeItemRef : void 0,
|
|
2581
|
-
|
|
2582
|
-
"
|
|
2938
|
+
disabled: key === void 0,
|
|
2939
|
+
title: key === void 0 ? t("timeline.cannotJump") : `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
|
|
2940
|
+
"aria-label": key === void 0 ? `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${t("timeline.cannotJump")}` : `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
|
|
2583
2941
|
"aria-current": isActive ? "location" : void 0,
|
|
2584
2942
|
onClick: () => {
|
|
2585
2943
|
if (key === void 0) return;
|
|
@@ -2588,8 +2946,14 @@ window.__ModuleLoader__.load({
|
|
|
2588
2946
|
jumpFallbackRef.current = window.setTimeout(() => {
|
|
2589
2947
|
jumpFallbackRef.current = void 0;
|
|
2590
2948
|
jumpPendingRef.current = false;
|
|
2591
|
-
},
|
|
2592
|
-
jumpToMessage(sessionsService, sessionId, key).catch(() => {})
|
|
2949
|
+
}, JUMP_LOCK_FALLBACK_MS);
|
|
2950
|
+
jumpToMessage(sessionsService, sessionId, key).catch(() => {}).finally(() => {
|
|
2951
|
+
if (jumpFallbackRef.current !== void 0) {
|
|
2952
|
+
clearTimeout(jumpFallbackRef.current);
|
|
2953
|
+
jumpFallbackRef.current = void 0;
|
|
2954
|
+
}
|
|
2955
|
+
jumpPendingRef.current = false;
|
|
2956
|
+
});
|
|
2593
2957
|
},
|
|
2594
2958
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2595
2959
|
className: "dsbt-title",
|