deepseek-harness-background 0.3.2 → 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/README.md +3 -0
- package/README.zh.md +3 -0
- package/lib/client.js +369 -56
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ The look (fixed wallpaper layer + theme-aware scrim + translucent glass panels d
|
|
|
36
36
|
- **Fit modes** — `cover` (fill, crop) or `contain` (whole image).
|
|
37
37
|
- **Theme-aware scrim** — the light theme uses a white veil (lifts the art so dark text keeps contrast); the dark theme automatically switches to a black veil (dims the art so light text keeps contrast).
|
|
38
38
|
- **Frosted glass (whitelisted)** — while a background is active, only the surfaces that float as small cards over the wallpaper turn into translucent glass (specular sheen + `backdrop-filter`): the composer card and message bubbles, code blocks / terminal / diff / tool-IO cards / skill & MCP call cards and inline code, the agent task strips and their takeover panels (approval / question / plan review), the chrome buttons (new session, composer plus, scroll-to-bottom), the load-earlier history button, the subagent lineage popover, the sidebar build badge, and the home hero "preview" badge — every glassed surface carries the full recipe (fill + sheen + blur), never translucency without frost. Reading surfaces — dialogs, the settings UI, menus, tooltips, toasts, hover fills and every accent (the send button stays blue) — keep their **official opaque paints** so nothing legible turns washy. The blur radius is driven by the glass-blur slider; `panelOpacity` at 100% restores the official paints on the whitelisted list too.
|
|
39
|
+
- **Third-party glass registry** — any plugin can register its own panels into the same frosted-glass system through the published bridge (`window.__DSH_BACKGROUND_GLASS__` global + the `dsh-background-glass:ready` event): *token* mode adds the missing sheen + blur chain when the panel already fills with an overridden `--dsw-*` token, *fill* mode takes the fill over as well; every rule sits under the `data-dsh-bg-glass` gate so it toggles with the glass automatically. Consumers take zero dependencies and degrade gracefully when this plugin is absent; the whole bridge tears down with it. See [docs/GLASS_API.md](docs/GLASS_API.md).
|
|
39
40
|
- **Conversation timeline** — a DeepSeek-web-style scroll-navigation rail at the right edge of long conversations: one tick per user message on a frosted capsule; hovering expands it into a frosted panel listing every question (active one highlighted in brand blue); clicking jumps the chat to that message. **Key-point bookmarks** — star a question in the expanded panel (persisted per session): marked questions show a golden tick in the collapsed capsule and a "★ marked only" filter narrows the list. Jumps freeze the reading-position tracker until scrolling settles (no mid-jump jitter), and messages withdrawn by a rewind are dropped from the rail automatically. Collapsed and expanded share **one identical height** (no jump); idle ticks are **bottom-pinned** — the newest question's tick hugs the capsule's bottom edge like the official nav, fed by the full-session history regardless of how much of the conversation the chat view has paged in — and the expanded panel's clipped edges get the official **32px fade veils**. While the background glass is on, the rail joins the **same unified glass recipe** as the composer card and bubbles (fill follows panel opacity, blur follows the glass-blur slider); without glass it keeps the official DeepSeek frosted paints. Toggle it off with the timeline switch in the row. If the third-party dsh-chat-timeline plugin is also installed, this rail steps aside instead of doubling it.
|
|
40
41
|
- **Persisted in the official settings document** (`$DSH_HOME/settings.yaml`), waits out restarts.
|
|
41
42
|
- **Clean teardown** — disabling, clearing or uninstalling restores the original background exactly; the plugin only ever removes what it wrote.
|
|
@@ -97,6 +98,7 @@ dsh --profile web
|
|
|
97
98
|
- The plugin's own host routes (`/api/bg-wallpaper/*`: `settings`, `upload`, `image/<id>`) read/write the section and serve uploads with same-origin + size caps + MIME/signature checks + a path-escape fence. A custom route family is used because the api-proxy settings allowlist does not expose third-party namespaces over the settings RPC.
|
|
98
99
|
- The background is drawn as a fixed `z-index:-2` wallpaper layer plus a `z-index:-1` scrim on `body`, toggled by the `data-dsh-bg` attribute; the scrim switches white/black by `data-ds-dark-theme` in the injected stylesheet. Frosted glass is whitelist-scoped: only the whitelisted surfaces get `--dsw-*` surface-token overrides, while everything else is painted by explicit `data-dsh-bg-glass`-gated rules carrying the FULL recipe (fill + sheen + blur chain): the three chrome buttons, the load-earlier history button, the composer dock family (agent task strips TodoPanel / GoalBar / QueueDock and their takeover panels — approval, question, plan review — whose tokens turn translucent and need the blur added), the subagent lineage popover, the home hero preview badge and the sidebar build badge — every other official token and reading surface (menus, dialogs, tooltips, toasts) stays untouched.
|
|
99
100
|
- The **timeline rail** is registered into the `conversation.input.dock` slot (per-session lifecycle) and portals to `body`. Its data comes, fastest first, from a host-side session projection (`bgTimeline`, registered in `src/projection.ts`) that enumerates every user message of the whole session — including turns the conversation has not paged in yet — with the loaded chat-node window as fallback. The conversation's own lazy loading is never driven by the rail (jumping to an old message pages history on demand, exactly like clicking "load earlier"); bookmarks persist in localStorage per session. Idle (collapsed), the tick stack is pinned to the capsule's bottom edge — the newest question's tick always hugs the bottom, older ones clip at the top — matching the official ScrollNav; hovering expands and follows the reading position instead. The official DeepSeek frosted paints are the no-glass fallback — under `data-dsh-bg-glass` the rail joins the plugin's unified recipe (composer fill token + the glass-blur slider chain), exactly like the chrome buttons, code cards and the composer itself.
|
|
101
|
+
- The **third-party glass registry** (`src/client/glass-registry.ts`) publishes its bridge on client apply — before any repaint can race it: `register({ plugin, selectors, mode })` is idempotent per (plugin, mode, selector) triple and synthesizes explicit `body[data-dsh-bg-glass]`-gated recipe rules into a dedicated `<style data-plugin-css>` tag; selectors are structurally validated first (no `{ } ; @ < >`, no backslashes, 500-char cap) with per-selector warn-and-drop that never blocks valid siblings; disposing the client fiber retracts the whole bridge (stylesheet, entries, window key). Contract docs in `docs/GLASS_API.md` / `GLASS_API.zh.md`; behavior locked by `tests/glass-registry.spec.ts`.
|
|
100
102
|
- Uploads live under `$DSH_HOME/deepseek-harness-background/` (content-addressed ids). Switching to a new image or clearing the background deletes the superseded upload file, so the directory does not accumulate dead images in normal use. (An upload that is never saved into the section — e.g. the tab closes right after an upload — can leave one orphaned file behind.) Disable / uninstall leaves nothing behind.
|
|
101
103
|
|
|
102
104
|
## Development
|
|
@@ -124,6 +126,7 @@ deepseek-harness-background/ # the plugin repo (package name stays the
|
|
|
124
126
|
│ ├── index.ts # painter lifecycle + settings row registration
|
|
125
127
|
│ ├── backdrop.ts # fixed wallpaper layer + scrim + glass surface + preview vars
|
|
126
128
|
│ ├── background-css.ts # injected stylesheet (layers, glass, light/dark scrim, variables)
|
|
129
|
+
│ ├── glass-registry.ts # third-party glass registry (window bridge + gated rule synthesis)
|
|
127
130
|
│ ├── timeline.tsx # conversation timeline rail (ScrollNav port on the glass system)
|
|
128
131
|
│ ├── timeline-css.ts # timeline stylesheet (dsbt- prefixed, official metrics)
|
|
129
132
|
│ ├── SettingsRow.tsx # the General-settings row (preview surface + stepped sliders)
|
package/README.zh.md
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
- **填充方式** —— `cover`(铺满、裁剪)或 `contain`(完整、留白)。
|
|
37
37
|
- **主题自适应遮罩** —— 浅色主题用白色纱帘(把图片提亮保持深色文字对比度),深色主题自动换成黑色纱帘(压暗图片保持浅色文字对比度)。
|
|
38
38
|
- **毛玻璃(白名单制)** —— 启用背景后,只有以小卡片/小按钮形态悬浮在壁纸上的表面才会变成半透明玻璃(顶部白色高光渐变 + `backdrop-filter`):输入框卡片与消息气泡、代码块 / 终端 / diff / 工具 IO 卡 / 技能与 MCP 调用卡与行内代码、agent 任务条及其接管面板(审批 / 提问 / 计划评审)、三个铬件按钮(新会话、输入框加号、回到底部)、「加载更早」历史按钮、标题栏展开的子代理列表面板、侧栏构建徽标,以及首页右上角的「预览版」徽标——每块玻璃面都带完整配方(填充 + 高光 + 模糊),绝无"只透明不磨砂"的残缺面。阅读型表面——对话框、设置界面、菜单、Tooltip、Toast、悬停填充与所有强调色(发送键保持品牌蓝)——一律保留**官方不透明样式**,保证可读性。模糊半径由「毛玻璃模糊」滑块驱动;「面板不透明度」调至 100% 时白名单表面也恢复官方原样。
|
|
39
|
+
- **第三方毛玻璃接口** —— 内置毛玻璃注册表(`window.__DSH_BACKGROUND_GLASS__` 全局 + `dsh-background-glass:ready` 事件):任何插件都可把自家面板的选择器注册进来,套上与内置表面完全一致的配方——`token` 模式为已使用被覆盖 `--dsw-*` 填充的面板补齐高光+模糊链,`fill` 模式连填充一并接管;规则统一挂在 `data-dsh-bg-glass` 门控下随玻璃自动开关。消费方零依赖、未安装本插件时优雅降级,本插件卸载时整桥拆除。详见 [docs/GLASS_API.zh.md](docs/GLASS_API.zh.md)。
|
|
39
40
|
- **会话时间线** —— 长会话右缘的 DeepSeek 官网风格滚动导航轨:毛玻璃胶囊上每条用户消息一枚指示刻度;悬停展开为列出全部提问的毛玻璃面板(当前阅读位置品牌蓝高亮);点击跳转到对应消息。**重点书签**——展开面板内点击 ★ 标记重点提问(按会话持久化):已标记项在折叠胶囊上显示金色加宽刻度,「★ 只看标记」一键筛选;跳转期间冻结高亮跟踪(消除中途乱跳),被回退撤回的消息自动从轨道剔除。折叠态与展开态**共用同一高度**(零跳变);折叠刻度**贴底锚定**——最新提问的横杠始终贴住胶囊底缘(与官网一致),且由全量会话历史驱动、不受对话区分页加载进度影响;展开面板的裁切边缘带官方同款 **32px 淡化渐变**。开启背景毛玻璃后,导航轨与输入框、消息气泡共用**同一套玻璃配方**(透明度随「面板不透明度」、模糊随「毛玻璃模糊」滑块调节,不比其他表面更重);未开启背景时保持 DeepSeek 官网同款毛玻璃配色。可在设置行内用「会话时间线」开关关闭;若同时安装了第三方 dsh-chat-timeline 插件,本轨道会自动让位避免重叠。
|
|
40
41
|
- **持久化到官方设置文档** —— 存于 `$DSH_HOME/settings.yaml`,跨重启保留。
|
|
41
42
|
- **干净卸载** —— 关闭、清除或卸载后完整恢复原背景;插件只移除自己写过的内容。
|
|
@@ -97,6 +98,7 @@ dsh --profile web
|
|
|
97
98
|
- **会话时间线**注册进 `conversation.input.dock` 槽位(绑定每会话生命周期),portal 渲染到 `body`。数据按速度优先:host 侧会话投影 `bgTimeline`(见 `src/projection.ts`,完整枚举整个会话的全部用户消息——包括对话区尚未分页加载的早期内容),已加载聊天节点窗口作为兜底;轨道从不驱动对话区的懒加载策略(点击旧消息跳转时才按需补页,与点「加载更早」一致)。折叠态刻度列贴底锚定——最新一条提问的横杠始终贴住胶囊底缘、更早的向顶部裁掉,与官网 ScrollNav 一致;悬停展开则改为跟随阅读位置。书签存于 localStorage(按会话隔离)。官网同款毛玻璃配色仅作为未开背景时的兜底:开启玻璃后(`data-dsh-bg-glass`)轨道并入插件统一配方——与输入框一致的 token 填充 + 由「毛玻璃模糊」滑块驱动的同一滤镜链。
|
|
98
99
|
- 插件自有的 host 路由(`/api/bg-wallpaper/*`:`settings`、`upload`、`image/<id>`)负责读写设置与提供上传图片,带同源校验、大小上限、MIME/签名校验与路径穿越防护。使用自定义路由族,是因为 api-proxy 的 settings 白名单不向第三方命名空间开放 settings RPC。
|
|
99
100
|
- 背景以 `body` 上一张固定的 `z-index:-2` 壁纸层 + `z-index:-1` 遮罩绘制,由 `data-dsh-bg` 属性开关;遮罩在注入样式表里按 `data-ds-dark-theme` 切换白/黑纱帘。毛玻璃为白名单制:仅对输入/气泡/代码/任务条等白名单表面覆盖 `--dsw-*` surface token,其余表面通过 `data-dsh-bg-glass` 门控的显式规则补齐整套配方(填充 + 高光 + 模糊滤镜链):三个 chrome 按钮、「加载更早」历史按钮、composer 坞列家族——agent 任务条(TodoPanel / GoalBar / QueueDock)与接管面板(审批 / 提问 / 计划评审,token 变半透明后由这里补上模糊)、子代理列表弹出层、首页「预览版」徽标、侧栏构建徽章——全部官方 token 与阅读型界面(菜单/对话框/tooltip/toast)保持原样。
|
|
101
|
+
- **第三方玻璃注册表**(`src/client/glass-registry.ts`):客户端 apply 时即在 `window.__DSH_BACKGROUND_GLASS__` 发布注册 api 并派发 `dsh-background-glass:ready`;`register({ plugin, selectors, mode })` 按 `(plugin, mode, selector)` 三元组幂等登记,把 `body[data-dsh-bg-glass]` 门控的显式配方规则合成进独立的 `<style data-plugin-css>` 标签;选择器先做结构性校验(禁 `{} ; @ < >` 与反斜杠、500 字符上限),违规条目逐条警告并丢弃、不影响兄弟条目;fiber dispose 时整桥拆除(样式表、条目、window 键全清)。契约文档见 `docs/GLASS_API.md` / `GLASS_API.zh.md`,行为由 `tests/glass-registry.spec.ts` 锁定。
|
|
100
102
|
- 上传文件存放在 `$DSH_HOME/deepseek-harness-background/`(内容寻址 id)。切换新图片或清除背景时,被替换的旧上传文件会被自动回收,正常使用下目录不会堆积死图片。(例外:上传后从未保存进设置——例如上传后立刻关闭标签页——会留下一个孤儿文件。)关闭 / 卸载后不留残留。
|
|
101
103
|
|
|
102
104
|
## 开发
|
|
@@ -124,6 +126,7 @@ deepseek-harness-background/ # 插件仓库(包名保留 npm 风格 i
|
|
|
124
126
|
│ ├── index.ts # painter 生命周期 + 设置行注册
|
|
125
127
|
│ ├── backdrop.ts # 固定壁纸层 + 遮罩 + 玻璃表面 + 预览变量
|
|
126
128
|
│ ├── background-css.ts # 注入的样式表(层、玻璃、明暗遮罩、变量)
|
|
129
|
+
│ ├── glass-registry.ts # 第三方毛玻璃注册表(window 桥 + 门控规则合成)
|
|
127
130
|
│ ├── timeline.tsx # 会话时间线导航轨(官方 ScrollNav 结构 × 本插件玻璃体系)
|
|
128
131
|
│ ├── timeline-css.ts # 时间线样式(dsbt- 前缀,官方度量)
|
|
129
132
|
│ ├── SettingsRow.tsx # 通用设置中的设置行(预览卡 + 阻尼滑块)
|
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))));
|
|
@@ -688,6 +717,206 @@ window.__ModuleLoader__.load({
|
|
|
688
717
|
function clearPreviewSurface(el) {
|
|
689
718
|
for (const name of PREVIEW_VARS) el.style.removeProperty(name);
|
|
690
719
|
}
|
|
720
|
+
/** Property name on window where the api is published. */
|
|
721
|
+
const GLASS_BRIDGE_GLOBAL = "__DSH_BACKGROUND_GLASS__";
|
|
722
|
+
/** DOM event announced once the api becomes available (detail = the api). */
|
|
723
|
+
const GLASS_READY_EVENT = "dsh-background-glass:ready";
|
|
724
|
+
/** Style-tag dedup key of the registry stylesheet (data-plugin-css). */
|
|
725
|
+
const REGISTRY_CSS_TAG = "deepseek-harness-background/glass-registry";
|
|
726
|
+
/** Plugin identity stamped onto the registry <style> element. */
|
|
727
|
+
const PLUGIN_ID = "deepseek-harness-background";
|
|
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
|
+
*/
|
|
736
|
+
const FORBIDDEN_SELECTOR_CHARS = [
|
|
737
|
+
"{",
|
|
738
|
+
"}",
|
|
739
|
+
";",
|
|
740
|
+
"@",
|
|
741
|
+
"<",
|
|
742
|
+
">",
|
|
743
|
+
","
|
|
744
|
+
];
|
|
745
|
+
/** Hard cap on one selector's length (diagnostics-friendly, abuse-proof). */
|
|
746
|
+
const MAX_SELECTOR_LENGTH = 500;
|
|
747
|
+
/** Hard cap on selectors per registration call. */
|
|
748
|
+
const MAX_SELECTORS_PER_SPEC = 64;
|
|
749
|
+
/**
|
|
750
|
+
* Validate one raw selector string. Structural checks only: anything that
|
|
751
|
+
* could escape the generated rule body ({}, ;, @, <, >) is rejected, and a
|
|
752
|
+
* backslash is refused too (escape sequences have no business in an anchor
|
|
753
|
+
* selector); ordinary CSS mistakes (a typo'd pseudo-class) are left to the
|
|
754
|
+
* browser, which drops just that one rule — a safe failure mode.
|
|
755
|
+
* @returns an error message, or null when the selector is acceptable.
|
|
756
|
+
*/
|
|
757
|
+
function selectorProblem(selector) {
|
|
758
|
+
if (typeof selector !== "string") return "must be a string";
|
|
759
|
+
const trimmed = selector.trim();
|
|
760
|
+
if (trimmed === "") return "must not be empty";
|
|
761
|
+
if (trimmed.length > MAX_SELECTOR_LENGTH) return "exceeds the 500-character limit (" + trimmed.length + ")";
|
|
762
|
+
for (const ch of FORBIDDEN_SELECTOR_CHARS) if (trimmed.includes(ch)) return "contains a forbidden character (" + JSON.stringify(ch) + ")";
|
|
763
|
+
if (trimmed.includes(String.fromCharCode(92))) return "contains a backslash (escape sequences are not accepted)";
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
/** Normalize a spec into validated (plugin, mode, selector) triples.
|
|
767
|
+
* @returns the accepted triples plus one warning line per rejected selector. */
|
|
768
|
+
function normalizeSpec(spec) {
|
|
769
|
+
const plugin = typeof spec?.plugin === "string" && spec.plugin.trim() !== "" ? spec.plugin.trim() : "(anonymous)";
|
|
770
|
+
const mode = spec?.mode === "fill" ? "fill" : "token";
|
|
771
|
+
const raw = Array.isArray(spec?.selectors) ? [...spec.selectors] : [spec?.selectors];
|
|
772
|
+
const rows = [];
|
|
773
|
+
const warnings = [];
|
|
774
|
+
if (raw.length > MAX_SELECTORS_PER_SPEC) warnings.push("[glass-registry] " + plugin + ": " + raw.length + " selectors exceeds the 64-selector cap; extra entries dropped.");
|
|
775
|
+
for (const item of raw.slice(0, MAX_SELECTORS_PER_SPEC)) {
|
|
776
|
+
const problem = selectorProblem(item);
|
|
777
|
+
if (typeof item !== "string" || problem !== null) {
|
|
778
|
+
warnings.push("[glass-registry] " + plugin + ": rejected selector " + JSON.stringify(item) + " — " + (problem ?? "must be a string") + ".");
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
rows.push({
|
|
782
|
+
plugin,
|
|
783
|
+
mode,
|
|
784
|
+
selector: item.trim()
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
rows,
|
|
789
|
+
warnings
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Render one registered surface as a gated CSS rule — the same recipes the
|
|
794
|
+
* built-in sheets emit for their explicit-fill families.
|
|
795
|
+
*/
|
|
796
|
+
function ruleFor(row) {
|
|
797
|
+
const fill = row.mode === "fill" ? "background-color: var(--dsw-specific-input-major); background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01)); -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01); backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);" : "background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01)); -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01); backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);";
|
|
798
|
+
return "body[data-dsh-bg-glass] " + row.selector + " { " + fill + " }";
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Owns the registered surfaces and the registry stylesheet.
|
|
802
|
+
*/
|
|
803
|
+
var GlassRegistry = class {
|
|
804
|
+
entries = /* @__PURE__ */ new Map();
|
|
805
|
+
styleEl = null;
|
|
806
|
+
disposed = false;
|
|
807
|
+
/**
|
|
808
|
+
* Register one spec. See BackgroundGlassApi.register.
|
|
809
|
+
* @param spec - the surfaces to glass.
|
|
810
|
+
* @returns the unregister function (idempotent).
|
|
811
|
+
*/
|
|
812
|
+
register(spec) {
|
|
813
|
+
if (this.disposed || publishedRegistry !== void 0 && publishedRegistry !== this) {
|
|
814
|
+
console.warn("[glass-registry] registration ignored: this bridge is no longer the live publication");
|
|
815
|
+
return () => {};
|
|
816
|
+
}
|
|
817
|
+
let normalized;
|
|
818
|
+
try {
|
|
819
|
+
normalized = normalizeSpec(spec);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
console.warn("[glass-registry] spec rejected: " + (error instanceof Error ? error.message : String(error)));
|
|
822
|
+
normalized = {
|
|
823
|
+
rows: [],
|
|
824
|
+
warnings: []
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const { rows, warnings } = normalized;
|
|
828
|
+
for (const message of warnings) console.warn(message);
|
|
829
|
+
const owner = {};
|
|
830
|
+
const keys = [];
|
|
831
|
+
for (const row of rows) {
|
|
832
|
+
const key = JSON.stringify([
|
|
833
|
+
row.plugin,
|
|
834
|
+
row.mode,
|
|
835
|
+
row.selector
|
|
836
|
+
]);
|
|
837
|
+
this.entries.set(key, {
|
|
838
|
+
row,
|
|
839
|
+
owner
|
|
840
|
+
});
|
|
841
|
+
keys.push(key);
|
|
842
|
+
}
|
|
843
|
+
this.sync();
|
|
844
|
+
let unregistered = false;
|
|
845
|
+
return () => {
|
|
846
|
+
if (unregistered) return;
|
|
847
|
+
unregistered = true;
|
|
848
|
+
for (const key of keys) {
|
|
849
|
+
const entry = this.entries.get(key);
|
|
850
|
+
if (entry !== void 0 && entry.owner === owner) this.entries.delete(key);
|
|
851
|
+
}
|
|
852
|
+
this.sync();
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
/** Rebuild the registry stylesheet from the live entries. */
|
|
856
|
+
sync() {
|
|
857
|
+
if (this.entries.size === 0) {
|
|
858
|
+
this.styleEl?.remove();
|
|
859
|
+
this.styleEl = null;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const seen = /* @__PURE__ */ new Set();
|
|
863
|
+
const rules = [];
|
|
864
|
+
for (const { row } of this.entries.values()) {
|
|
865
|
+
const rule = ruleFor(row);
|
|
866
|
+
if (!seen.has(rule)) {
|
|
867
|
+
seen.add(rule);
|
|
868
|
+
rules.push(rule);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (this.styleEl === null) {
|
|
872
|
+
this.styleEl = document.createElement("style");
|
|
873
|
+
this.styleEl.dataset.plugin = PLUGIN_ID;
|
|
874
|
+
this.styleEl.dataset.pluginCss = REGISTRY_CSS_TAG;
|
|
875
|
+
document.head.appendChild(this.styleEl);
|
|
876
|
+
}
|
|
877
|
+
this.styleEl.textContent = rules.join(String.fromCharCode(10));
|
|
878
|
+
}
|
|
879
|
+
/** Tear everything down: entries, stylesheet, and the window publication. */
|
|
880
|
+
dispose() {
|
|
881
|
+
this.disposed = true;
|
|
882
|
+
this.entries.clear();
|
|
883
|
+
this.sync();
|
|
884
|
+
const w = window;
|
|
885
|
+
if (w["__DSH_BACKGROUND_GLASS__"] === this.api) delete w[GLASS_BRIDGE_GLOBAL];
|
|
886
|
+
if (publishedRegistry === this) publishedRegistry = void 0;
|
|
887
|
+
}
|
|
888
|
+
/** The published api face over this registry. */
|
|
889
|
+
api = {
|
|
890
|
+
version: 1,
|
|
891
|
+
bridgeId: "deepseek-harness-background",
|
|
892
|
+
isActive: () => typeof document !== "undefined" && document.body !== null && document.body.hasAttribute("data-dsh-bg-glass"),
|
|
893
|
+
register: (spec) => this.register(spec)
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
/** The registry whose api is CURRENTLY published on window. Module-level on
|
|
897
|
+
* purpose: the published api face deliberately carries no dispose member
|
|
898
|
+
* (internal teardown stays unreachable for consumers), so reinstall-time
|
|
899
|
+
* cleanup needs a handle the window key cannot provide. */
|
|
900
|
+
let publishedRegistry;
|
|
901
|
+
/**
|
|
902
|
+
* Publish the glass registry bridge onto window and announce it. Safe to
|
|
903
|
+
* call repeatedly: a previous publication's REGISTRY is disposed first, so
|
|
904
|
+
* exactly one bridge — and one stylesheet — is ever live.
|
|
905
|
+
* @returns the bridge handle — call .dispose() when this plugin's client
|
|
906
|
+
* fiber tears down.
|
|
907
|
+
*/
|
|
908
|
+
function installGlassBridge() {
|
|
909
|
+
publishedRegistry?.dispose();
|
|
910
|
+
document.querySelector("style[data-plugin-css=\"deepseek-harness-background/glass-registry\"]")?.remove();
|
|
911
|
+
const registry = new GlassRegistry();
|
|
912
|
+
publishedRegistry = registry;
|
|
913
|
+
window[GLASS_BRIDGE_GLOBAL] = registry.api;
|
|
914
|
+
window.dispatchEvent(new CustomEvent(GLASS_READY_EVENT, { detail: registry.api }));
|
|
915
|
+
return {
|
|
916
|
+
api: registry.api,
|
|
917
|
+
dispose: () => registry.dispose()
|
|
918
|
+
};
|
|
919
|
+
}
|
|
691
920
|
//#endregion
|
|
692
921
|
//#region src/client/locales.ts
|
|
693
922
|
/** Chinese copy. */
|
|
@@ -724,7 +953,8 @@ window.__ModuleLoader__.load({
|
|
|
724
953
|
"timeline.marksOnly": "★ 只看标记",
|
|
725
954
|
"timeline.mark": "标记为重点",
|
|
726
955
|
"timeline.unmark": "取消重点标记",
|
|
727
|
-
"timeline.noMarks": "还没有标记的重点"
|
|
956
|
+
"timeline.noMarks": "还没有标记的重点",
|
|
957
|
+
"timeline.cannotJump": "无法跳转:这条历史消息缺少定位信息"
|
|
728
958
|
};
|
|
729
959
|
/** English copy. */
|
|
730
960
|
const en = {
|
|
@@ -760,7 +990,8 @@ window.__ModuleLoader__.load({
|
|
|
760
990
|
"timeline.marksOnly": "★ Marked only",
|
|
761
991
|
"timeline.mark": "Mark as key point",
|
|
762
992
|
"timeline.unmark": "Unmark key point",
|
|
763
|
-
"timeline.noMarks": "No marked key points"
|
|
993
|
+
"timeline.noMarks": "No marked key points",
|
|
994
|
+
"timeline.cannotJump": "Cannot jump: this history message lacks position info"
|
|
764
995
|
};
|
|
765
996
|
//#endregion
|
|
766
997
|
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -895,41 +1126,41 @@ window.__ModuleLoader__.load({
|
|
|
895
1126
|
document.head.appendChild(tag);
|
|
896
1127
|
}
|
|
897
1128
|
var SettingsRow_module_css_default = {
|
|
898
|
-
"
|
|
1129
|
+
"row": "-ozqdq_row",
|
|
1130
|
+
"segBtn": "-ozqdq_segBtn",
|
|
1131
|
+
"preview": "-ozqdq_preview",
|
|
899
1132
|
"urlRow": "-ozqdq_urlRow",
|
|
900
|
-
"switch": "-ozqdq_switch",
|
|
901
|
-
"hint": "-ozqdq_hint",
|
|
902
|
-
"titleRow": "-ozqdq_titleRow",
|
|
903
|
-
"clearRow": "-ozqdq_clearRow",
|
|
904
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
905
|
-
"previewEmpty": "-ozqdq_previewEmpty",
|
|
906
|
-
"sourceRow": "-ozqdq_sourceRow",
|
|
907
|
-
"urlInput": "-ozqdq_urlInput",
|
|
908
|
-
"segGroup": "-ozqdq_segGroup",
|
|
909
|
-
"previewScrim": "-ozqdq_previewScrim",
|
|
910
|
-
"previewGlassText": "-ozqdq_previewGlassText",
|
|
911
|
-
"title": "-ozqdq_title",
|
|
912
1133
|
"sliderValue": "-ozqdq_sliderValue",
|
|
913
|
-
"
|
|
914
|
-
"
|
|
915
|
-
"btn": "-ozqdq_btn",
|
|
916
|
-
"desc": "-ozqdq_desc",
|
|
917
|
-
"sliderRow": "-ozqdq_sliderRow",
|
|
1134
|
+
"titleRow": "-ozqdq_titleRow",
|
|
1135
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
918
1136
|
"segRow": "-ozqdq_segRow",
|
|
919
|
-
"sectionLabel": "-ozqdq_sectionLabel",
|
|
920
1137
|
"enabledBadge": "-ozqdq_enabledBadge",
|
|
921
|
-
"
|
|
922
|
-
"btnDanger": "-ozqdq_btnDanger",
|
|
1138
|
+
"title": "-ozqdq_title",
|
|
923
1139
|
"previewImg": "-ozqdq_previewImg",
|
|
924
|
-
"segBtn": "-ozqdq_segBtn",
|
|
925
|
-
"clearBtn": "-ozqdq_clearBtn",
|
|
926
|
-
"preview": "-ozqdq_preview",
|
|
927
|
-
"row": "-ozqdq_row",
|
|
928
|
-
"segLabel": "-ozqdq_segLabel",
|
|
929
1140
|
"header": "-ozqdq_header",
|
|
1141
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
1142
|
+
"sliderRow": "-ozqdq_sliderRow",
|
|
1143
|
+
"previewGlassText": "-ozqdq_previewGlassText",
|
|
930
1144
|
"slider": "-ozqdq_slider",
|
|
1145
|
+
"switch": "-ozqdq_switch",
|
|
1146
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
1147
|
+
"clearRow": "-ozqdq_clearRow",
|
|
1148
|
+
"error": "-ozqdq_error",
|
|
1149
|
+
"sliderLabel": "-ozqdq_sliderLabel",
|
|
1150
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
1151
|
+
"urlInput": "-ozqdq_urlInput",
|
|
1152
|
+
"desc": "-ozqdq_desc",
|
|
1153
|
+
"previewEmpty": "-ozqdq_previewEmpty",
|
|
1154
|
+
"section": "-ozqdq_section",
|
|
1155
|
+
"hint": "-ozqdq_hint",
|
|
1156
|
+
"btn": "-ozqdq_btn",
|
|
1157
|
+
"segGroup": "-ozqdq_segGroup",
|
|
1158
|
+
"segLabel": "-ozqdq_segLabel",
|
|
1159
|
+
"sectionLabel": "-ozqdq_sectionLabel",
|
|
931
1160
|
"btnPrimary": "-ozqdq_btnPrimary",
|
|
932
|
-
"
|
|
1161
|
+
"segActive": "-ozqdq_segActive",
|
|
1162
|
+
"switchKnob": "-ozqdq_switchKnob",
|
|
1163
|
+
"btnDanger": "-ozqdq_btnDanger"
|
|
933
1164
|
};
|
|
934
1165
|
//#endregion
|
|
935
1166
|
//#region src/client/SettingsRow.tsx
|
|
@@ -1128,8 +1359,7 @@ window.__ModuleLoader__.load({
|
|
|
1128
1359
|
};
|
|
1129
1360
|
draftRef.current = next;
|
|
1130
1361
|
markEditing();
|
|
1131
|
-
|
|
1132
|
-
else paintBackgroundKnob(key, value);
|
|
1362
|
+
paintBackgroundKnob(key, value);
|
|
1133
1363
|
if (previewRef.current) paintPreviewSurface(previewRef.current, next);
|
|
1134
1364
|
}, [markEditing]);
|
|
1135
1365
|
/** Persist one section through the host route and adopt it on success. */
|
|
@@ -1137,6 +1367,7 @@ window.__ModuleLoader__.load({
|
|
|
1137
1367
|
dirtyRef.current = true;
|
|
1138
1368
|
const result = await settingsClient.save(next);
|
|
1139
1369
|
if (result === "failed") {
|
|
1370
|
+
dirtyRef.current = false;
|
|
1140
1371
|
setError(t("background.saveFailed"));
|
|
1141
1372
|
return;
|
|
1142
1373
|
}
|
|
@@ -1685,6 +1916,8 @@ window.__ModuleLoader__.load({
|
|
|
1685
1916
|
display: flex; align-items: center; justify-content: flex-end;
|
|
1686
1917
|
}
|
|
1687
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; }
|
|
1688
1921
|
.dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
|
|
1689
1922
|
body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
|
|
1690
1923
|
body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
|
|
@@ -1983,6 +2216,19 @@ window.__ModuleLoader__.load({
|
|
|
1983
2216
|
out.sort((a, b) => a.seq - b.seq);
|
|
1984
2217
|
return out;
|
|
1985
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
|
+
}
|
|
1986
2232
|
/** Resolve the DOM anchor key of one entry (direct key form only). */
|
|
1987
2233
|
function resolveAnchorKey(m) {
|
|
1988
2234
|
return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
|
|
@@ -2027,24 +2273,44 @@ window.__ModuleLoader__.load({
|
|
|
2027
2273
|
return out;
|
|
2028
2274
|
}
|
|
2029
2275
|
/**
|
|
2030
|
-
* Rail messages
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2033
|
-
*
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
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).
|
|
2036
2285
|
* @param projected - the raw bgTimeline projection value.
|
|
2037
2286
|
*/
|
|
2038
2287
|
function railMessages(snapshot, projected) {
|
|
2039
2288
|
const projectedMessages = normalizeProjectedTimeline(projected);
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
+
});
|
|
2046
2312
|
}
|
|
2047
|
-
return
|
|
2313
|
+
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
2048
2314
|
}
|
|
2049
2315
|
/**
|
|
2050
2316
|
* Outer rail height for a message count: rows + breathing room, clamped so
|
|
@@ -2078,26 +2344,70 @@ window.__ModuleLoader__.load({
|
|
|
2078
2344
|
}
|
|
2079
2345
|
return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
|
|
2080
2346
|
}
|
|
2081
|
-
/**
|
|
2082
|
-
|
|
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) {
|
|
2083
2381
|
const session = sessionsService.binding(sessionId)?.session;
|
|
2084
2382
|
if (session === void 0) return false;
|
|
2085
|
-
|
|
2086
|
-
|
|
2383
|
+
const deadline = Date.now() + JUMP_PAGE_DEADLINE_MS;
|
|
2384
|
+
for (;;) {
|
|
2087
2385
|
const snapshot = session.getSnapshot();
|
|
2088
2386
|
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
2089
|
-
if (snapshot?.hasMore !== true)
|
|
2387
|
+
if (snapshot?.hasMore !== true) break;
|
|
2388
|
+
if (Date.now() >= deadline) break;
|
|
2090
2389
|
if (snapshot.loadingOlder === true) {
|
|
2091
2390
|
await delay(50);
|
|
2092
2391
|
continue;
|
|
2093
2392
|
}
|
|
2393
|
+
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
2394
|
+
await delay(100);
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2094
2397
|
await session.loadOlder();
|
|
2095
2398
|
}
|
|
2096
|
-
const row =
|
|
2097
|
-
if (row === null)
|
|
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
|
+
}
|
|
2098
2404
|
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2099
2405
|
row.scrollIntoView({
|
|
2100
|
-
behavior:
|
|
2406
|
+
behavior: "auto",
|
|
2407
|
+
block: "center"
|
|
2408
|
+
});
|
|
2409
|
+
if (!reducedMotion) row.scrollIntoView({
|
|
2410
|
+
behavior: "smooth",
|
|
2101
2411
|
block: "center"
|
|
2102
2412
|
});
|
|
2103
2413
|
return true;
|
|
@@ -2386,8 +2696,9 @@ window.__ModuleLoader__.load({
|
|
|
2386
2696
|
type: "button",
|
|
2387
2697
|
className: "dsbt-jump",
|
|
2388
2698
|
ref: isActive ? activeItemRef : void 0,
|
|
2389
|
-
|
|
2390
|
-
"
|
|
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")}`,
|
|
2391
2702
|
"aria-current": isActive ? "location" : void 0,
|
|
2392
2703
|
onClick: () => {
|
|
2393
2704
|
if (key === void 0) return;
|
|
@@ -2444,6 +2755,7 @@ window.__ModuleLoader__.load({
|
|
|
2444
2755
|
* @param ctx - client cordis context (slots/locale/sessions injected).
|
|
2445
2756
|
*/
|
|
2446
2757
|
function apply(ctx) {
|
|
2758
|
+
const glassBridge = installGlassBridge();
|
|
2447
2759
|
const paint = () => {
|
|
2448
2760
|
const snapshot = settingsClient.getSnapshot();
|
|
2449
2761
|
const value = snapshot.status === "ready" ? snapshot.value : void 0;
|
|
@@ -2474,6 +2786,7 @@ window.__ModuleLoader__.load({
|
|
|
2474
2786
|
}, TimelineRail));
|
|
2475
2787
|
ctx.effect(() => () => {
|
|
2476
2788
|
unsubscribe();
|
|
2789
|
+
glassBridge.dispose();
|
|
2477
2790
|
backgroundPainter.dispose();
|
|
2478
2791
|
}, "deepseek-harness-background: background surface");
|
|
2479
2792
|
}
|