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/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) this.img.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
- /** Characters whose presence would let a selector escape its rule context. */
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
- "switch": "-ozqdq_switch",
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
- "urlInput": "-ozqdq_urlInput",
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
- "enabledBadge": "-ozqdq_enabledBadge",
1105
- "btnPrimary": "-ozqdq_btnPrimary",
1106
- "clearBtn": "-ozqdq_clearBtn",
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
- "row": "-ozqdq_row",
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
- "segGroup": "-ozqdq_segGroup",
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
- if (key === "panelOpacity") paintBackground(next);
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 smooth
1977
- * jump animates (cleared 150ms after the last scroll event, plus an 800ms
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 digits = (/rewound.*?(?:target\s+)?(\d+)/i.exec(text) ?? /#(\d+)/.exec(text))?.[1];
2058
- if (digits === void 0) return void 0;
2059
- const n = Number.parseInt(digits, 10);
2060
- return Number.isFinite(n) ? n : void 0;
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 = rewindTargetOfOutcome(outcome.text);
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 with the fastest source first: the host projection covers the
2223
- * WHOLE session (and already excludes rewind cuts), so it wins whenever it
2224
- * has anything; otherwise fall back to the loaded chat-node window. The
2225
- * projection path still applies locally-known rewind hiding so a cut lands
2226
- * instantly even before the next projection push arrives.
2227
- * @param snapshot - the session conversation snapshot (node fallback).
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
- if (projectedMessages.length > 0) {
2233
- const chat = snapshot?.chat;
2234
- const hidden = rewindHiddenSeqsOfChat(chat);
2235
- const spans = rewindSpansOfChat(chat);
2236
- if (hidden.size === 0 && spans.length === 0) return projectedMessages;
2237
- return projectedMessages.filter((m) => !hidden.has(m.seq) && !spans.some((span) => m.seq >= span.start && m.seq <= span.end));
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 collectMessages(snapshot);
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
- /** Ensure the target node is loaded, then scroll its row into view. */
2274
- async function jumpToMessage(sessionsService, sessionId, key) {
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 guard = 0;
2278
- while (guard++ < 120) {
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) return false;
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
- await session.loadOlder();
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.scrollIntoView({
2292
- behavior: reducedMotion ? "auto" : "smooth",
2293
- block: "center"
2294
- });
2295
- return true;
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
- title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2582
- "aria-label": `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
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
- }, 800);
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",