deepseek-harness-background 0.4.0 → 0.4.1

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,41 +1126,41 @@ window.__ModuleLoader__.load({
1087
1126
  document.head.appendChild(tag);
1088
1127
  }
1089
1128
  var SettingsRow_module_css_default = {
1090
- "switch": "-ozqdq_switch",
1129
+ "row": "-ozqdq_row",
1130
+ "segBtn": "-ozqdq_segBtn",
1091
1131
  "preview": "-ozqdq_preview",
1092
- "sliderLabel": "-ozqdq_sliderLabel",
1093
- "segRow": "-ozqdq_segRow",
1094
1132
  "urlRow": "-ozqdq_urlRow",
1133
+ "sliderValue": "-ozqdq_sliderValue",
1134
+ "titleRow": "-ozqdq_titleRow",
1135
+ "clearBtn": "-ozqdq_clearBtn",
1136
+ "segRow": "-ozqdq_segRow",
1137
+ "enabledBadge": "-ozqdq_enabledBadge",
1138
+ "title": "-ozqdq_title",
1095
1139
  "previewImg": "-ozqdq_previewImg",
1096
- "previewGlass": "-ozqdq_previewGlass",
1097
- "hint": "-ozqdq_hint",
1098
1140
  "header": "-ozqdq_header",
1141
+ "previewGlass": "-ozqdq_previewGlass",
1142
+ "sliderRow": "-ozqdq_sliderRow",
1143
+ "previewGlassText": "-ozqdq_previewGlassText",
1099
1144
  "slider": "-ozqdq_slider",
1100
- "title": "-ozqdq_title",
1101
- "segLabel": "-ozqdq_segLabel",
1102
- "urlInput": "-ozqdq_urlInput",
1145
+ "switch": "-ozqdq_switch",
1146
+ "sourceRow": "-ozqdq_sourceRow",
1147
+ "clearRow": "-ozqdq_clearRow",
1103
1148
  "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",
1110
- "sliderValue": "-ozqdq_sliderValue",
1111
- "row": "-ozqdq_row",
1149
+ "sliderLabel": "-ozqdq_sliderLabel",
1112
1150
  "previewScrim": "-ozqdq_previewScrim",
1151
+ "urlInput": "-ozqdq_urlInput",
1113
1152
  "desc": "-ozqdq_desc",
1153
+ "previewEmpty": "-ozqdq_previewEmpty",
1154
+ "section": "-ozqdq_section",
1155
+ "hint": "-ozqdq_hint",
1156
+ "btn": "-ozqdq_btn",
1114
1157
  "segGroup": "-ozqdq_segGroup",
1115
- "clearRow": "-ozqdq_clearRow",
1158
+ "segLabel": "-ozqdq_segLabel",
1159
+ "sectionLabel": "-ozqdq_sectionLabel",
1160
+ "btnPrimary": "-ozqdq_btnPrimary",
1116
1161
  "segActive": "-ozqdq_segActive",
1117
- "sliderRow": "-ozqdq_sliderRow",
1118
- "btn": "-ozqdq_btn",
1119
- "titleRow": "-ozqdq_titleRow",
1120
- "btnDanger": "-ozqdq_btnDanger",
1121
- "previewGlassText": "-ozqdq_previewGlassText",
1122
- "sourceRow": "-ozqdq_sourceRow",
1123
- "segBtn": "-ozqdq_segBtn",
1124
- "previewEmpty": "-ozqdq_previewEmpty"
1162
+ "switchKnob": "-ozqdq_switchKnob",
1163
+ "btnDanger": "-ozqdq_btnDanger"
1125
1164
  };
1126
1165
  //#endregion
1127
1166
  //#region src/client/SettingsRow.tsx
@@ -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); }
@@ -2175,6 +2216,19 @@ window.__ModuleLoader__.load({
2175
2216
  out.sort((a, b) => a.seq - b.seq);
2176
2217
  return out;
2177
2218
  }
2219
+ /**
2220
+ * Count the user messages the loaded window holds WITHOUT collecting or
2221
+ * sorting them — the fast gate railMessages uses to skip the merge path on
2222
+ * the streaming hot path (see there).
2223
+ * @param chat - the loaded chat snapshot.
2224
+ */
2225
+ function countWindowUserMessages(chat) {
2226
+ const nodes = chat?.nodes;
2227
+ if (nodes === void 0 || typeof nodes.values !== "function") return 0;
2228
+ let count = 0;
2229
+ for (const node of nodes.values()) if (node !== null && typeof node === "object" && node.kind === "user") count += 1;
2230
+ return count;
2231
+ }
2178
2232
  /** Resolve the DOM anchor key of one entry (direct key form only). */
2179
2233
  function resolveAnchorKey(m) {
2180
2234
  return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
@@ -2219,24 +2273,44 @@ window.__ModuleLoader__.load({
2219
2273
  return out;
2220
2274
  }
2221
2275
  /**
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).
2276
+ * Rail messages: the host projection (whole-session index) MERGED with the
2277
+ * loaded chat-node window. The projection normally wins outright, but a
2278
+ * projection that lost its baseline (e.g. the plugin was hot-reloaded after
2279
+ * the session's tail page seeded the client store) can miss early entries —
2280
+ * the window fills those gaps whenever it holds them, and window entries
2281
+ * lend their real anchor key to projected entries whose durable id was
2282
+ * absent (early events without one). Locally-known rewind hiding applies to
2283
+ * the merged list so a cut lands instantly, before the next projection push.
2284
+ * @param snapshot - the session conversation snapshot (node window).
2228
2285
  * @param projected - the raw bgTimeline projection value.
2229
2286
  */
2230
2287
  function railMessages(snapshot, projected) {
2231
2288
  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));
2289
+ const chat = snapshot?.chat;
2290
+ const hidden = rewindHiddenSeqsOfChat(chat);
2291
+ const spans = rewindSpansOfChat(chat);
2292
+ const isHidden = (seq) => hidden.has(seq) || spans.some((span) => seq >= span.start && seq <= span.end);
2293
+ if (projectedMessages.length === 0) return collectMessages(snapshot);
2294
+ const windowUserCount = countWindowUserMessages(chat);
2295
+ const projectionLacksKeys = projectedMessages.some((m) => m.key === void 0);
2296
+ if (windowUserCount <= projectedMessages.length && !projectionLacksKeys) return hidden.size === 0 && spans.length === 0 ? projectedMessages : projectedMessages.filter((m) => !isHidden(m.seq));
2297
+ const windowMessages = collectMessages(snapshot);
2298
+ const bySeq = /* @__PURE__ */ new Map();
2299
+ for (const m of projectedMessages) if (!isHidden(m.seq)) bySeq.set(m.seq, m);
2300
+ for (const m of windowMessages) {
2301
+ if (isHidden(m.seq)) continue;
2302
+ const existing = bySeq.get(m.seq);
2303
+ if (existing === void 0) {
2304
+ bySeq.set(m.seq, m);
2305
+ continue;
2306
+ }
2307
+ if (existing.key === void 0 && m.key !== void 0) bySeq.set(m.seq, {
2308
+ ...existing,
2309
+ key: m.key,
2310
+ ...existing.text === "" && m.text !== "" ? { text: m.text } : {}
2311
+ });
2238
2312
  }
2239
- return collectMessages(snapshot);
2313
+ return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
2240
2314
  }
2241
2315
  /**
2242
2316
  * Outer rail height for a message count: rows + breathing room, clamped so
@@ -2270,26 +2344,70 @@ window.__ModuleLoader__.load({
2270
2344
  }
2271
2345
  return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
2272
2346
  }
2273
- /** Ensure the target node is loaded, then scroll its row into view. */
2274
- async function jumpToMessage(sessionsService, sessionId, key) {
2347
+ /** How long one jump may keep paging older history before giving up. */
2348
+ const JUMP_PAGE_DEADLINE_MS = 3e4;
2349
+ /** How long to wait for the chat view to commit the target row. */
2350
+ const JUMP_ROW_WAIT_MS = 3e3;
2351
+ /**
2352
+ * Poll the conversation DOM until the target row is committed. The session
2353
+ * store updates BEFORE React renders the prepended page, so the old one-shot
2354
+ * query right after loadOlder() resolved could miss the row and silently
2355
+ * give up — the reported "click does nothing until the conversation is
2356
+ * nudged (scroll/click)" behavior.
2357
+ * @param key - chat anchor key to look for.
2358
+ * @param timeoutMs - polling budget.
2359
+ */
2360
+ async function waitForChatRow(key, timeoutMs) {
2361
+ const scrollports = Array.from(document.querySelectorAll("[data-conversation-scroll]"));
2362
+ if (scrollports.length === 0) return null;
2363
+ const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
2364
+ const deadline = Date.now() + timeoutMs;
2365
+ for (;;) {
2366
+ for (const scrollport of scrollports) {
2367
+ const row = scrollport.querySelector(selector);
2368
+ if (row !== null) return row;
2369
+ }
2370
+ if (Date.now() >= deadline) return null;
2371
+ await delay(40);
2372
+ }
2373
+ }
2374
+ /**
2375
+ * Ensure the target node is loaded, wait for the chat view to commit its row
2376
+ * into the DOM, then scroll it into view. Paging uses a time budget instead
2377
+ * of the old fixed 120-iteration guard (each in-flight page burned 50ms of
2378
+ * that guard, so slow history could exhaust it and silently fail).
2379
+ */
2380
+ async function jumpToMessage(sessionsService, sessionId, key, rowWaitMs = JUMP_ROW_WAIT_MS) {
2275
2381
  const session = sessionsService.binding(sessionId)?.session;
2276
2382
  if (session === void 0) return false;
2277
- let guard = 0;
2278
- while (guard++ < 120) {
2383
+ const deadline = Date.now() + JUMP_PAGE_DEADLINE_MS;
2384
+ for (;;) {
2279
2385
  const snapshot = session.getSnapshot();
2280
2386
  if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
2281
- if (snapshot?.hasMore !== true) return false;
2387
+ if (snapshot?.hasMore !== true) break;
2388
+ if (Date.now() >= deadline) break;
2282
2389
  if (snapshot.loadingOlder === true) {
2283
2390
  await delay(50);
2284
2391
  continue;
2285
2392
  }
2393
+ if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
2394
+ await delay(100);
2395
+ continue;
2396
+ }
2286
2397
  await session.loadOlder();
2287
2398
  }
2288
- const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
2289
- if (row === null) return false;
2399
+ const row = await waitForChatRow(key, rowWaitMs);
2400
+ if (row === null) {
2401
+ console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${key}`);
2402
+ return false;
2403
+ }
2290
2404
  const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2291
2405
  row.scrollIntoView({
2292
- behavior: reducedMotion ? "auto" : "smooth",
2406
+ behavior: "auto",
2407
+ block: "center"
2408
+ });
2409
+ if (!reducedMotion) row.scrollIntoView({
2410
+ behavior: "smooth",
2293
2411
  block: "center"
2294
2412
  });
2295
2413
  return true;
@@ -2578,8 +2696,9 @@ window.__ModuleLoader__.load({
2578
2696
  type: "button",
2579
2697
  className: "dsbt-jump",
2580
2698
  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")}`,
2699
+ disabled: key === void 0,
2700
+ title: key === void 0 ? t("timeline.cannotJump") : `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2701
+ "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
2702
  "aria-current": isActive ? "location" : void 0,
2584
2703
  onClick: () => {
2585
2704
  if (key === void 0) return;