deepseek-harness-background 0.2.1 → 0.3.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
@@ -4,28 +4,85 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ //#region \0rolldown/runtime.js
8
+ var __create = Object.create;
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __getProtoOf = Object.getPrototypeOf;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
+ key = keys[i];
17
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
18
+ get: ((k) => from[k]).bind(null, key),
19
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
20
+ });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
25
+ value: mod,
26
+ enumerable: true
27
+ }) : target, mod));
28
+ //#endregion
7
29
  let react = require("react");
30
+ react = __toESM(react, 1);
8
31
  let react_jsx_runtime = require("react/jsx-runtime");
32
+ let react_dom = require("react-dom");
33
+ react_dom = __toESM(react_dom, 1);
9
34
  //#region src/client/background-css.ts
10
35
  /**
11
36
  * Background CSS — the stylesheet injected once into the document when the
12
37
  * plugin first runs. It defines the behind-body wallpaper layer, the scrim,
13
- * and the frosted-glass panels, all driven by CSS variables the painter writes
14
- * onto `document.body`. Kept in a single idempotent `<style data-plugin-css>`
15
- * tag (mirrors the reference `dsh-wallpaper-engine` bundle).
38
+ * and the frosted-glass surfaces, all driven by CSS variables the painter
39
+ * writes onto `document.body`. Kept in a single idempotent
40
+ * `<style data-plugin-css>` tag (mirrors the reference `dsh-wallpaper-engine`
41
+ * bundle).
16
42
  */
17
43
  /** Unique id stamped on the injected style element (dedup key). */
18
44
  const BACKGROUND_CSS_TAG = "deepseek-harness-background/styles";
19
45
  /** Body attribute the painter sets while a wallpaper is active. */
20
46
  const ACTIVE_ATTR = "data-dsh-bg";
21
47
  /**
48
+ * Body attribute the painter sets only while the glass is actually on (a
49
+ * wallpaper is active AND panelOpacity < 1). The explicit-fill rules below —
50
+ * whitelisted buttons, popovers and badges whose official fills are not
51
+ * token-mediated — key on this gate so they return to the exact official
52
+ * paints when the user maxes the panel-opacity slider.
53
+ */
54
+ const GLASS_ATTR = "data-dsh-bg-glass";
55
+ /**
22
56
  * The backdrop CSS. The wallpaper is a fixed `z-index:-2` element (below the
23
- * scrim at `-1`, below the UI). While active, the official surface tokens are
24
- * made transparent (so the wallpaper shows through every column) and the
25
- * opaque input / bubble surfaces turn into translucent frosted glass.
26
- * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`)
27
- * and CSS-module SUFFIX conventions (`_bubble`) — the same stable anchors the
28
- * reference engine uses, since hashed prefixes change across shell rebuilds.
57
+ * scrim at `-1`, below the UI). While active, the app frame + sidebar fills
58
+ * turn transparent so all columns share one backdrop.
59
+ *
60
+ * Frosted glass is a WHITELIST, not a blanket: only the surfaces that float
61
+ * as small cards/buttons over the wallpaper are glassed — composer card,
62
+ * message bubbles, code surfaces (blocks, inline code, tool IO cards,
63
+ * skill/MCP call cards), the three chrome buttons (new session, composer +
64
+ * button, scroll-to-bottom), the load-earlier history button, the composer
65
+ * dock family (agent task strips: TodoPanel / GoalBar / QueueDock) and its
66
+ * takeover panels (approval, question, plan review), the subagent lineage
67
+ * popover, the home hero "preview" badge, the sidebar build badge, and the
68
+ * timeline rail (timeline-css.ts). Every glassed surface shares ONE recipe:
69
+ * fill from the painter's --dsw-specific-input-major token (same alpha
70
+ * curve + theme dimming), the full shared blur/saturate/brightness chain
71
+ * driven by the glass-blur slider, and the shared sheen vars — so no
72
+ * surface reads heavier than the composer card. Dialogs, the settings UI,
73
+ * menus, tooltips, toasts and every hover/accent fill keep the OFFICIAL
74
+ * opaque paints — they are reading surfaces and must stay legible.
75
+ *
76
+ * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
77
+ * `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
78
+ * SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions:
79
+ * `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
80
+ * `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
81
+ * it), `_newSession` excludes `Label` (the button's inner label span) and
82
+ * `_toBottom` excludes `Slot` (the zero-height sticky wrapper) — both share
83
+ * the substring with the chrome control but must keep their own paint, and
84
+ * the subagent popover is `role="tree"` + `_menu` so the generic menu
85
+ * surface stays untouched.
29
86
  */
30
87
  const BACKGROUND_CSS = `
31
88
  .dsh-bg-layer {
@@ -58,22 +115,165 @@ window.__ModuleLoader__.load({
58
115
  --dsw-specific-sidebar-fill: transparent;
59
116
  }
60
117
 
61
- /* Frosted glass over the opaque conversation surfaces (composer + bubbles).
62
- The surface translucency is driven by --dsw-specific-input-major /
63
- --dsw-specific-bubble, which the painter writes (theme-aware) so that a
64
- fully-opaque panel restores the official surfaces. The blur here comes
65
- from --bg-glass-blur; a faint top-weighted specular sheen makes the
66
- translucent surfaces read as wet glass. */
67
- body[data-dsh-bg] [data-composer-card],
68
- body[data-dsh-bg] [class*="_bubble"] {
69
- background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05) 38%, rgba(255, 255, 255, 0.02));
70
- -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
71
- backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.6)) brightness(var(--bg-glass-brightness, 1.04)) contrast(1.01);
118
+ /* ---- Whitelisted frosted-glass sheet ---------------------------------
119
+ Fills come from the --dsw-* tokens the painter overrides on body ONLY for
120
+ this list's own surfaces (input/bubble/code/tip); this block adds the
121
+ shared wet-glass sheen + blur from --bg-glass-blur. Exposure is calibrated
122
+ per scheme (the painter writes --bg-glass-brightness / --bg-glass-sheen):
123
+ light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
124
+ GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
125
+ slider returns these surfaces to the exact official paints. */
126
+ body[data-dsh-bg-glass] [data-composer-card],
127
+ body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
128
+ body[data-dsh-bg-glass] .md-code-block,
129
+ body[data-dsh-bg-glass] [data-terminal],
130
+ body[data-dsh-bg-glass] [data-diff],
131
+ body[data-dsh-bg-glass] [data-read],
132
+ body[data-dsh-bg-glass] [data-search],
133
+ body[data-dsh-bg-glass] [data-web],
134
+ body[data-dsh-bg-glass] [class*="_ioCard"],
135
+ body[data-dsh-bg-glass] [class*="_instructionsCard"],
136
+ body[data-dsh-bg-glass] [class*="_markdown"] :not(pre) > code {
137
+ 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));
138
+ -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);
139
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
72
140
  box-shadow:
73
- inset 0 1px 0 rgba(255, 255, 255, var(--bg-glass-highlight, 0.32)),
141
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
74
142
  inset 0 -1px 0 rgba(255, 255, 255, 0.08),
75
143
  inset 0 0 0 0.5px rgba(255, 255, 255, 0.08),
76
- 0 12px 40px rgba(0, 0, 0, var(--bg-glass-shadow, 0.12));
144
+ 0 12px 40px rgba(0, 0, 0, 0.12);
145
+ }
146
+
147
+ /* The sticky code-block header officially occludes scrolled code with
148
+ bg-base (transparent under a wallpaper): refill it with the banner token
149
+ (itself translucent glass) so code stays hidden beneath the sticky bar. */
150
+ body[data-dsh-bg] .md-code-block [class*="_bannerWrap"] {
151
+ background-color: var(--dsw-alias-markdown-code-block-banner);
152
+ }
153
+
154
+ /* The sidebar build badge paints with the label-ink token — a TEXT token we
155
+ must not override globally — so it gets a theme-aware translucent ink. */
156
+ body[data-dsh-bg] [class*="_buildRevision"] {
157
+ background-color: rgba(15, 17, 21, 0.62);
158
+ }
159
+ body[data-ds-dark-theme][data-dsh-bg] [class*="_buildRevision"] {
160
+ background-color: rgba(249, 250, 251, 0.62);
161
+ }
162
+
163
+ /* The empty-state hero glow asset would wash out the wallpaper behind the
164
+ welcome message — dim it so the art stays visible. */
165
+ body[data-dsh-bg] [class*="_heroGlow"] {
166
+ opacity: 0.2;
167
+ }
168
+
169
+ /* ---- Whitelisted chrome buttons --------------------------------------
170
+ These three buttons' official fills ride button-* tokens shared with
171
+ unrelated surfaces, so instead of overriding the tokens they get explicit
172
+ paints — but the SAME glass recipe as the composer card and bubbles:
173
+ fill = the painter's --dsw-specific-input-major token (the identical
174
+ translucent white + theme curve), filter = the sheet's exact
175
+ blur/saturate/brightness chain (blur follows the glass-blur slider, no
176
+ caps), sheen = the shared sheen vars. Hover = the same white glass at the
177
+ boosted alpha var. Gated on data-dsh-bg-glass (off when the panel is
178
+ fully opaque). */
179
+ body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]),
180
+ body[data-dsh-bg-glass] [data-composer-card] [class*="_add"],
181
+ body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]) {
182
+ background-color: var(--dsw-specific-input-major);
183
+ 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));
184
+ -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);
185
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
186
+ box-shadow:
187
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
188
+ inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
189
+ 0 4px 14px rgba(0, 0, 0, 0.08);
190
+ }
191
+ body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]):hover,
192
+ body[data-dsh-bg-glass] [data-composer-card] [class*="_add"]:hover:not(:disabled),
193
+ body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]):hover {
194
+ background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
195
+ }
196
+ /* Collapsed rail renders the new-session control as a bare icon — keep the
197
+ official transparent paint (and its official translucent hover). */
198
+ body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"],
199
+ body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"]:hover {
200
+ background-color: transparent;
201
+ background-image: none;
202
+ box-shadow: none;
203
+ }
204
+
205
+ /* ---- Subagent lineage popover (title-bar expanded list) --------------
206
+ Its official fill rides the menu token shared with every dropdown; glass
207
+ THIS popover explicitly via its role="tree" + _menu combination so real
208
+ menus stay officially opaque and legible. Same unified recipe: the
209
+ composer fill token + the full shared filter chain (the official
210
+ elevation shadow stays). */
211
+ body[data-dsh-bg-glass] [role="tree"][class*="_menu"] {
212
+ background-color: var(--dsw-specific-input-major);
213
+ 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));
214
+ -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);
215
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
216
+ }
217
+
218
+ /* ---- Home hero "preview" badge (top-right superscript pill) ----------
219
+ Officially the state-business-tertiary pastel; re-emit the same hue as
220
+ glass but with the UNIFIED alpha curve (color-mix against the painter's
221
+ --bg-glass-alpha) and the shared blur chain, so it sits on the wallpaper
222
+ with exactly the glass strength of the composer card. */
223
+ body[data-dsh-bg-glass] [class*="_previewBadge"] {
224
+ background-color: color-mix(in srgb, rgb(228 237 253) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
225
+ -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);
226
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
227
+ }
228
+ body[data-ds-dark-theme][data-dsh-bg-glass] [class*="_previewBadge"] {
229
+ background-color: color-mix(in srgb, rgb(52 65 91) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
230
+ }
231
+
232
+ /* ---- Composer dock family: task strips + takeover panels --------------
233
+ The agent task strips (TodoPanel, GoalBar, QueueDock — one dock column
234
+ above the composer) and the composer takeovers (approval, question,
235
+ plan review) paint with tokens the painter turns translucent
236
+ (--dsw-specific-tip / --dsw-specific-input-major) but their own styles
237
+ never blur anything: translucent-without-blur reads as bare
238
+ transparency, not frosted glass. Give each the shared sheen + filter
239
+ chain; their fills stay token-driven so the panel-opacity slider keeps
240
+ driving them. Anchors are the components' own stable data attributes
241
+ plus a structural child, so no generic class suffix is swept in. */
242
+ body[data-dsh-bg-glass] [data-testid="todo-panel"],
243
+ body[data-dsh-bg-glass] [data-goal-bar] > div,
244
+ body[data-dsh-bg-glass] [data-queue-dock] > div,
245
+ body[data-dsh-bg-glass] [data-approval-key] > div,
246
+ body[data-dsh-bg-glass] [data-question-key] > section,
247
+ body[data-dsh-bg-glass] [data-plan-review-key] > section {
248
+ 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));
249
+ -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);
250
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
251
+ }
252
+
253
+ /* ---- "Load earlier" history button (top of long conversations) --------
254
+ Its official fill is an opaque solid button token that is deliberately
255
+ NOT overridden, so unlike the tip family it needs an explicit glass
256
+ fill too — same recipe as the three chrome buttons above. */
257
+ body[data-dsh-bg-glass] [class*="_older"] button {
258
+ background-color: var(--dsw-specific-input-major);
259
+ 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));
260
+ -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);
261
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
262
+ box-shadow:
263
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
264
+ inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
265
+ 0 4px 14px rgba(0, 0, 0, 0.08);
266
+ }
267
+ body[data-dsh-bg-glass] [class*="_older"] button:hover:not(:disabled) {
268
+ background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
269
+ }
270
+
271
+ /* Sidebar build badge: translucent ink already, but it sat directly on
272
+ the wallpaper without frost. Join the shared blur chain under the same
273
+ gate (its theme-aware ink fills from the active block above). */
274
+ body[data-dsh-bg-glass] [class*="_buildRevision"] {
275
+ -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);
276
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
77
277
  }
78
278
  `;
79
279
  /**
@@ -96,6 +296,12 @@ window.__ModuleLoader__.load({
96
296
  const DEFAULT_SCRIM = .25;
97
297
  /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
98
298
  const DEFAULT_PANEL_OPACITY = .15;
299
+ /**
300
+ * Session-projection key this plugin owns in the client-visible value map —
301
+ * the host fold registers it (src/projection.ts) and the rail reads it
302
+ * (src/client/timeline.tsx); one shared constant so the two can never drift.
303
+ */
304
+ const TIMELINE_PROJECTION_KEY = "bgTimeline";
99
305
  /** Default background rendering mode. */
100
306
  const DEFAULT_FIT = "cover";
101
307
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
@@ -117,22 +323,102 @@ window.__ModuleLoader__.load({
117
323
  const LAYER_CLASS = "dsh-bg-layer";
118
324
  const IMAGE_CLASS = "dsh-bg-image";
119
325
  const SCRIM_CLASS = "dsh-bg-scrim";
120
- /** Surface tokens this painter overwrites (restored on dispose). */
121
- const SURFACE_TOKENS = ["--dsw-specific-input-major", "--dsw-specific-bubble"];
326
+ /** Body style properties the painter owns while a wallpaper paints: the app
327
+ * frame and sidebar fills go transparent so every column shares one art.
328
+ * Inline values outrank any stylesheet rule (the body[data-dsh-bg] gate only
329
+ * covers the CSS copy), so a sourceless section must actively restore them. */
330
+ const FRAME_TOKENS = ["--dsw-alias-bg-base", "--dsw-specific-sidebar-fill"];
331
+ /**
332
+ * White-glass tokens — the WHITELISTED surfaces only. Everything that floats
333
+ * as a small card over the wallpaper: the composer input stack, message
334
+ * bubbles, the markdown code surfaces (blocks + banner + inline code), and
335
+ * the agent task strip family (`--dsw-specific-tip`: TodoPanel + GoalBar +
336
+ * QueueDock share one dock column and one fill token). Menus, dialogs, the
337
+ * settings UI, tooltips, toasts, hover fills and every accent keep the
338
+ * OFFICIAL token values — they are reading surfaces and must stay legible.
339
+ * `factor` scales the alpha relative to the input surface.
340
+ */
341
+ const GLASS_SURFACE_TOKENS = [
342
+ {
343
+ token: "--dsw-specific-input-major",
344
+ factor: 1
345
+ },
346
+ {
347
+ token: "--dsw-specific-bubble",
348
+ factor: .8
349
+ },
350
+ {
351
+ token: "--dsw-alias-markdown-code-block",
352
+ factor: 1
353
+ },
354
+ {
355
+ token: "--dsw-alias-markdown-code-block-banner",
356
+ factor: 1
357
+ },
358
+ {
359
+ token: "--dsw-alias-markdown-inline-code",
360
+ factor: .9
361
+ },
362
+ {
363
+ token: "--dsw-specific-tip",
364
+ factor: 1
365
+ }
366
+ ];
367
+ /** Every token the glass owns (save/restore key set). */
368
+ const GLASS_TOKENS = GLASS_SURFACE_TOKENS.map(({ token }) => token);
122
369
  /** Fraction of the official white surface alpha kept at the most transparent panel setting. */
123
370
  const GLASS_MIN_ALPHA = .05;
371
+ /** Base white-glass alpha for one panelOpacity (the curve every surface shares). */
372
+ function glassAlpha(panelOpacity) {
373
+ const alpha = GLASS_MIN_ALPHA + panelOpacity * .85;
374
+ return Math.max(0, Math.min(.9, alpha));
375
+ }
376
+ /**
377
+ * Saturation boost that rides the blur radius. Gentler than the reference
378
+ * engine (1.1 + blur * 0.02, capped at 1.6): stacking a hot saturate on top of
379
+ * the brightness/sheen layers pushed bright wallpapers into clipping.
380
+ */
381
+ function glassSaturate(blur) {
382
+ if (blur <= 0) return 1;
383
+ return Math.round(Math.min(1.6, 1.1 + blur * .02) * 1e3) / 1e3;
384
+ }
385
+ /**
386
+ * Exposure calibration per scheme. Light glass neither boosts brightness nor
387
+ * stacks a heavy sheen (mainstream frosted recipes — macOS vibrancy, Windows
388
+ * acrylic, common web glass — blur + saturate + tint without a positive
389
+ * brightness gain); dark glass keeps the reference engine's slight lift, which
390
+ * stops the white-glass fills from turning muddy.
391
+ */
392
+ function glassExposure(inDark) {
393
+ return inDark ? {
394
+ brightness: "1.04",
395
+ sheen: "0.16",
396
+ sheenMid: "0.05"
397
+ } : {
398
+ brightness: "0.98",
399
+ sheen: "0.07",
400
+ sheenMid: "0.02"
401
+ };
402
+ }
403
+ /**
404
+ * The theme-aware base alpha for one panelOpacity — THE curve every glass
405
+ * surface shares (surface tokens, bare alpha knobs and the settings preview
406
+ * included). Single-sourced here so the live painter and the preview card
407
+ * cannot drift.
408
+ */
409
+ function glassBaseAlpha(panelOpacity, inDark) {
410
+ return glassAlpha(panelOpacity) * (inDark ? .4 : .8);
411
+ }
124
412
  /**
125
413
  * The theme-aware glass surface alphas for one panelOpacity: the input-surface
126
414
  * token and the bubble token share one curve, dimmed per scheme. Shared by the
127
415
  * live painter and the settings-row preview card so the two cannot drift.
128
416
  */
129
417
  function glassSurfaceAlphas(panelOpacity, inDark) {
130
- const alpha = GLASS_MIN_ALPHA + panelOpacity * .85;
131
- const clamped = Math.max(0, Math.min(.9, alpha));
132
- const factor = inDark ? .4 : .8;
418
+ const alpha = glassBaseAlpha(panelOpacity, inDark);
133
419
  return {
134
- major: `rgba(255, 255, 255, ${(clamped * factor).toFixed(3)})`,
135
- bubble: `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`
420
+ major: `rgba(255, 255, 255, ${alpha.toFixed(3)})`,
421
+ bubble: `rgba(255, 255, 255, ${(alpha * .8).toFixed(3)})`
136
422
  };
137
423
  }
138
424
  /**
@@ -160,7 +446,7 @@ window.__ModuleLoader__.load({
160
446
  rememberOnce(prop) {
161
447
  if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
162
448
  const value = document.body.style.getPropertyValue(prop);
163
- if (SURFACE_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
449
+ if (GLASS_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
164
450
  else this.savedVars.set(prop, value);
165
451
  }
166
452
  /**
@@ -175,20 +461,21 @@ window.__ModuleLoader__.load({
175
461
  return;
176
462
  }
177
463
  injectBackgroundCss();
178
- this.rememberOnce("--dsw-alias-bg-base");
179
- this.rememberOnce("--dsw-specific-sidebar-fill");
180
- document.body.style.setProperty("--dsw-alias-bg-base", "transparent");
181
- document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
182
464
  const url = backgroundImageUrl(settings, uploadBase);
183
465
  if (url === "") {
184
466
  this.observer?.disconnect();
185
467
  this.observer = void 0;
468
+ this.restoreFrameTokens();
186
469
  this.applySurfaceGlass(settings, true);
187
470
  this.active = false;
188
471
  this.removeLayers();
189
472
  document.body.removeAttribute(ACTIVE_ATTR);
190
473
  return;
191
474
  }
475
+ for (const prop of FRAME_TOKENS) {
476
+ this.rememberOnce(prop);
477
+ document.body.style.setProperty(prop, "transparent");
478
+ }
192
479
  if (!this.layer) {
193
480
  this.layer = document.createElement("div");
194
481
  this.layer.className = LAYER_CLASS;
@@ -214,7 +501,7 @@ window.__ModuleLoader__.load({
214
501
  this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
215
502
  this.setVar("--bg-scrim", String(settings.scrim));
216
503
  this.setVar("--bg-glass-blur", `${settings.blur}px`);
217
- this.setVar("--bg-glass-saturate", String(1.15 + settings.blur * .03));
504
+ this.setVar("--bg-glass-saturate", String(glassSaturate(settings.blur)));
218
505
  this.applySurfaceGlass(settings, false);
219
506
  if (!this.observer) {
220
507
  this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
@@ -230,6 +517,20 @@ window.__ModuleLoader__.load({
230
517
  document.body.style.setProperty(name, value);
231
518
  }
232
519
  /**
520
+ * Return the owned frame-transparency properties to their saved state (or
521
+ * remove them when this session never overrode them). dispose() restores
522
+ * everything wholesale; this targeted variant serves the sourceless path,
523
+ * where the painter keeps running for a later source.
524
+ */
525
+ restoreFrameTokens() {
526
+ for (const prop of FRAME_TOKENS) {
527
+ this.rememberOnce(prop);
528
+ const original = this.savedVars.get(prop);
529
+ if (original !== void 0 && original !== "") document.body.style.setProperty(prop, original);
530
+ else document.body.style.removeProperty(prop);
531
+ }
532
+ }
533
+ /**
233
534
  * Update one effect knob without re-running the full apply. This is the hot
234
535
  * path for slider drags (many events per second): only the single CSS
235
536
  * variable changes, the DOM layers are untouched.
@@ -247,7 +548,7 @@ window.__ModuleLoader__.load({
247
548
  break;
248
549
  case "blur":
249
550
  this.setVar("--bg-glass-blur", `${value}px`);
250
- this.setVar("--bg-glass-saturate", String(1.15 + Number(value) * .03));
551
+ this.setVar("--bg-glass-saturate", String(glassSaturate(Number(value))));
251
552
  break;
252
553
  case "wallpaperBlur":
253
554
  this.setVar("--bg-wallpaper-blur", `${value}px`);
@@ -266,9 +567,9 @@ window.__ModuleLoader__.load({
266
567
  applySurfaceGlass(settings, forceRestore) {
267
568
  const s = document.body.style;
268
569
  const inDark = document.body.dataset.dsDarkTheme !== void 0;
269
- for (const token of SURFACE_TOKENS) this.rememberOnce(token);
570
+ for (const token of GLASS_TOKENS) this.rememberOnce(token);
270
571
  const restore = () => {
271
- for (const token of SURFACE_TOKENS) {
572
+ for (const token of GLASS_TOKENS) {
272
573
  const original = this.savedTokens.get(token);
273
574
  if (original !== void 0 && original !== "") s.setProperty(token, original);
274
575
  else s.removeProperty(token);
@@ -276,12 +577,22 @@ window.__ModuleLoader__.load({
276
577
  };
277
578
  if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
278
579
  restore();
580
+ document.body.removeAttribute(GLASS_ATTR);
279
581
  this.setVar("--bg-glass-blur", "0px");
582
+ this.setVar("--bg-glass-brightness", "1");
583
+ s.removeProperty("--bg-glass-alpha");
584
+ s.removeProperty("--bg-glass-alpha-strong");
280
585
  return;
281
586
  }
282
- const { major, bubble } = glassSurfaceAlphas(settings.panelOpacity, inDark);
283
- s.setProperty("--dsw-specific-input-major", major);
284
- s.setProperty("--dsw-specific-bubble", bubble);
587
+ document.body.setAttribute(GLASS_ATTR, "on");
588
+ const exposure = glassExposure(inDark);
589
+ this.setVar("--bg-glass-brightness", exposure.brightness);
590
+ this.setVar("--bg-glass-sheen", exposure.sheen);
591
+ this.setVar("--bg-glass-sheen-mid", exposure.sheenMid);
592
+ const base = glassBaseAlpha(settings.panelOpacity, inDark);
593
+ for (const { token, factor } of GLASS_SURFACE_TOKENS) s.setProperty(token, `rgba(255, 255, 255, ${(base * factor).toFixed(3)})`);
594
+ this.setVar("--bg-glass-alpha", base.toFixed(3));
595
+ this.setVar("--bg-glass-alpha-strong", Math.min(.9, base * 1.5).toFixed(3));
285
596
  }
286
597
  /** Remove the wallpaper + scrim layers and clear the active attribute. */
287
598
  removeLayers() {
@@ -302,6 +613,7 @@ window.__ModuleLoader__.load({
302
613
  this.removeLayers();
303
614
  const s = document.body.style;
304
615
  document.body.removeAttribute(ACTIVE_ATTR);
616
+ document.body.removeAttribute(GLASS_ATTR);
305
617
  for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
306
618
  for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
307
619
  this.savedTokens.clear();
@@ -343,6 +655,7 @@ window.__ModuleLoader__.load({
343
655
  "--bg-wallpaper-scale",
344
656
  "--bg-glass-blur",
345
657
  "--bg-glass-saturate",
658
+ "--bg-glass-brightness",
346
659
  "--bg-preview-glass"
347
660
  ];
348
661
  /**
@@ -362,11 +675,13 @@ window.__ModuleLoader__.load({
362
675
  const inDark = document.body.dataset.dsDarkTheme !== void 0;
363
676
  if (settings.panelOpacity >= 1) {
364
677
  s.setProperty("--bg-glass-blur", "0px");
678
+ s.setProperty("--bg-glass-brightness", "1");
365
679
  s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
366
680
  return;
367
681
  }
368
682
  s.setProperty("--bg-glass-blur", `${settings.blur}px`);
369
- s.setProperty("--bg-glass-saturate", String(1.15 + settings.blur * .03));
683
+ s.setProperty("--bg-glass-saturate", String(glassSaturate(settings.blur)));
684
+ s.setProperty("--bg-glass-brightness", glassExposure(inDark).brightness);
370
685
  s.setProperty("--bg-preview-glass", glassSurfaceAlphas(settings.panelOpacity, inDark).bubble);
371
686
  }
372
687
  /** Remove every preview variable from an element (restores CSS defaults). */
@@ -400,7 +715,16 @@ window.__ModuleLoader__.load({
400
715
  "background.contain": "完整",
401
716
  "background.clear": "清除背景",
402
717
  "background.uploadFailed": "上传失败,请重试",
403
- "background.saveFailed": "保存失败,请重试"
718
+ "background.saveFailed": "保存失败,请重试",
719
+ "background.timeline": "会话时间线",
720
+ "background.timelineHint": "右侧消息导航轨,悬停展开问题列表(DeepSeek 官网风格)",
721
+ "timeline.railLabel": "对话时间线",
722
+ "timeline.roleUser": "用户",
723
+ "timeline.noText": "(无文本内容)",
724
+ "timeline.marksOnly": "★ 只看标记",
725
+ "timeline.mark": "标记为重点",
726
+ "timeline.unmark": "取消重点标记",
727
+ "timeline.noMarks": "还没有标记的重点"
404
728
  };
405
729
  /** English copy. */
406
730
  const en = {
@@ -427,7 +751,16 @@ window.__ModuleLoader__.load({
427
751
  "background.contain": "Contain",
428
752
  "background.clear": "Clear background",
429
753
  "background.uploadFailed": "Upload failed, try again",
430
- "background.saveFailed": "Save failed, try again"
754
+ "background.saveFailed": "Save failed, try again",
755
+ "background.timeline": "Conversation timeline",
756
+ "background.timelineHint": "Message rail at the right edge; hover to expand the question list (DeepSeek web style)",
757
+ "timeline.railLabel": "Chat timeline",
758
+ "timeline.roleUser": "User",
759
+ "timeline.noText": "(no text)",
760
+ "timeline.marksOnly": "★ Marked only",
761
+ "timeline.mark": "Mark as key point",
762
+ "timeline.unmark": "Unmark key point",
763
+ "timeline.noMarks": "No marked key points"
431
764
  };
432
765
  //#endregion
433
766
  //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -522,6 +855,9 @@ window.__ModuleLoader__.load({
522
855
  };
523
856
  const promise = run();
524
857
  this.pending = promise;
858
+ promise.then(() => {
859
+ if (this.pending === promise) this.pending = void 0;
860
+ });
525
861
  return promise;
526
862
  }
527
863
  /**
@@ -549,7 +885,7 @@ window.__ModuleLoader__.load({
549
885
  const settingsClient = new SettingsClient();
550
886
  //#endregion
551
887
  //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
552
- const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.6));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#fff;opacity:.8;text-shadow:0 1px 2px #00000059;white-space:nowrap;font-size:11px;line-height:1}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}@media (prefers-reduced-motion:reduce){.-ozqdq_btn{transition:none}}";
888
+ const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.42)) brightness(var(--bg-glass-brightness,1));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#0f172ac7;text-shadow:0 1px 2px #fff6;white-space:nowrap;font-size:11px;line-height:1}body[data-ds-dark-theme] .-ozqdq_previewGlassText{color:#ffffffd9;text-shadow:0 1px 2px #00000059}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}.-ozqdq_switch{background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:11px;flex:none;width:40px;height:22px;padding:0;transition:background-color .15s;position:relative}.-ozqdq_switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}.-ozqdq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_switchKnob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px;box-shadow:0 1px 3px #00000047}.-ozqdq_switch[aria-checked=true] .-ozqdq_switchKnob{transform:translate(18px)}@media (prefers-reduced-motion:reduce){.-ozqdq_btn,.-ozqdq_switch,.-ozqdq_switchKnob{transition:none}}";
553
889
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
554
890
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
555
891
  const tag = document.createElement("style");
@@ -559,38 +895,40 @@ window.__ModuleLoader__.load({
559
895
  document.head.appendChild(tag);
560
896
  }
561
897
  var SettingsRow_module_css_default = {
562
- "segActive": "-ozqdq_segActive",
563
898
  "error": "-ozqdq_error",
564
- "sliderLabel": "-ozqdq_sliderLabel",
565
- "previewEmpty": "-ozqdq_previewEmpty",
566
- "row": "-ozqdq_row",
567
899
  "urlRow": "-ozqdq_urlRow",
568
- "slider": "-ozqdq_slider",
569
- "sliderValue": "-ozqdq_sliderValue",
900
+ "switch": "-ozqdq_switch",
901
+ "hint": "-ozqdq_hint",
902
+ "titleRow": "-ozqdq_titleRow",
570
903
  "clearRow": "-ozqdq_clearRow",
571
- "preview": "-ozqdq_preview",
572
- "clearBtn": "-ozqdq_clearBtn",
573
- "btn": "-ozqdq_btn",
574
- "segLabel": "-ozqdq_segLabel",
904
+ "previewGlass": "-ozqdq_previewGlass",
905
+ "previewEmpty": "-ozqdq_previewEmpty",
906
+ "sourceRow": "-ozqdq_sourceRow",
907
+ "urlInput": "-ozqdq_urlInput",
908
+ "segGroup": "-ozqdq_segGroup",
909
+ "previewScrim": "-ozqdq_previewScrim",
575
910
  "previewGlassText": "-ozqdq_previewGlassText",
576
- "btnPrimary": "-ozqdq_btnPrimary",
577
- "header": "-ozqdq_header",
578
911
  "title": "-ozqdq_title",
579
- "previewScrim": "-ozqdq_previewScrim",
580
- "sectionLabel": "-ozqdq_sectionLabel",
581
- "enabledBadge": "-ozqdq_enabledBadge",
582
- "titleRow": "-ozqdq_titleRow",
583
- "previewGlass": "-ozqdq_previewGlass",
912
+ "sliderValue": "-ozqdq_sliderValue",
913
+ "sliderLabel": "-ozqdq_sliderLabel",
914
+ "switchKnob": "-ozqdq_switchKnob",
915
+ "btn": "-ozqdq_btn",
584
916
  "desc": "-ozqdq_desc",
585
917
  "sliderRow": "-ozqdq_sliderRow",
918
+ "segRow": "-ozqdq_segRow",
919
+ "sectionLabel": "-ozqdq_sectionLabel",
920
+ "enabledBadge": "-ozqdq_enabledBadge",
921
+ "segActive": "-ozqdq_segActive",
922
+ "btnDanger": "-ozqdq_btnDanger",
586
923
  "previewImg": "-ozqdq_previewImg",
587
- "hint": "-ozqdq_hint",
588
924
  "segBtn": "-ozqdq_segBtn",
589
- "segGroup": "-ozqdq_segGroup",
590
- "urlInput": "-ozqdq_urlInput",
591
- "btnDanger": "-ozqdq_btnDanger",
592
- "segRow": "-ozqdq_segRow",
593
- "sourceRow": "-ozqdq_sourceRow",
925
+ "clearBtn": "-ozqdq_clearBtn",
926
+ "preview": "-ozqdq_preview",
927
+ "row": "-ozqdq_row",
928
+ "segLabel": "-ozqdq_segLabel",
929
+ "header": "-ozqdq_header",
930
+ "slider": "-ozqdq_slider",
931
+ "btnPrimary": "-ozqdq_btnPrimary",
594
932
  "section": "-ozqdq_section"
595
933
  };
596
934
  //#endregion
@@ -623,7 +961,8 @@ window.__ModuleLoader__.load({
623
961
  panelOpacity: DEFAULT_PANEL_OPACITY,
624
962
  blur: 16,
625
963
  wallpaperBlur: 0,
626
- fit: DEFAULT_FIT
964
+ fit: DEFAULT_FIT,
965
+ timeline: true
627
966
  };
628
967
  /** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
629
968
  * the two blur sliders feel smooth near small values. Bounds come from the
@@ -671,7 +1010,7 @@ window.__ModuleLoader__.load({
671
1010
  }
672
1011
  /** Field-wise equality for distinguishing our own saves from external changes. */
673
1012
  function sameSettings(a, b) {
674
- return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit;
1013
+ return a.enabled === b.enabled && a.uploadId === b.uploadId && a.url === b.url && a.opacity === b.opacity && a.scrim === b.scrim && a.panelOpacity === b.panelOpacity && a.blur === b.blur && a.wallpaperBlur === b.wallpaperBlur && a.fit === b.fit && a.timeline === b.timeline;
675
1014
  }
676
1015
  /**
677
1016
  * A stepped range control. While dragging it re-renders only itself: the value
@@ -909,6 +1248,16 @@ window.__ModuleLoader__.load({
909
1248
  paintLive(next);
910
1249
  saveNow(next);
911
1250
  };
1251
+ const handleTimelineToggle = () => {
1252
+ markEditing();
1253
+ const next = {
1254
+ ...draftRef.current,
1255
+ timeline: !draftRef.current.timeline
1256
+ };
1257
+ draftRef.current = next;
1258
+ setDraft(next);
1259
+ saveNow(next);
1260
+ };
912
1261
  const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
913
1262
  const hasImage = sourceUrl !== "";
914
1263
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1071,6 +1420,29 @@ window.__ModuleLoader__.load({
1071
1420
  children: t(mode === "cover" ? "background.cover" : "background.contain")
1072
1421
  }, mode))
1073
1422
  })]
1423
+ }),
1424
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1425
+ className: SettingsRow_module_css_default.segRow,
1426
+ children: [
1427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1428
+ className: SettingsRow_module_css_default.segLabel,
1429
+ children: t("background.timeline")
1430
+ }),
1431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1432
+ type: "button",
1433
+ role: "switch",
1434
+ "aria-checked": draft.timeline,
1435
+ "aria-label": t("background.timeline"),
1436
+ title: t("background.timelineHint"),
1437
+ className: SettingsRow_module_css_default.switch,
1438
+ onClick: handleTimelineToggle,
1439
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsRow_module_css_default.switchKnob })
1440
+ }),
1441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1442
+ className: SettingsRow_module_css_default.hint,
1443
+ children: t("background.timelineHint")
1444
+ })
1445
+ ]
1074
1446
  })
1075
1447
  ]
1076
1448
  }),
@@ -1092,17 +1464,984 @@ window.__ModuleLoader__.load({
1092
1464
  });
1093
1465
  }
1094
1466
  //#endregion
1467
+ //#region src/client/timeline-css.ts
1468
+ /**
1469
+ * Timeline CSS — the conversation scroll-navigation rail (DeepSeek web
1470
+ * ScrollNav structure, rebuilt on this plugin's frosted-glass system).
1471
+ *
1472
+ * Structure and metrics mirror the official chat.deepseek.com stylesheet
1473
+ * (module map extracted from the shipped bundle; v0.1.4 of the reference port
1474
+ * dsh-chat-timeline widens the panel to 260px and adds bookmarks):
1475
+ * nav 34px-wide rail, fixed right edge, vertically centered; collapsed
1476
+ * blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
1477
+ * whose scroll area holds 30px rows (tick indicator + fading-in title);
1478
+ * 32px gradient fade veils top/bottom, expanded-only, shown while that
1479
+ * side actually clips.
1480
+ *
1481
+ * Deviations from the reference port, all fidelity fixes:
1482
+ * - The filter bar collapses to zero height while idle instead of staying
1483
+ * in layout invisibly (it pushed every tick ~33px down).
1484
+ * - Short stacks center vertically; overflowing stacks are bottom-pinned
1485
+ * by the component so the newest tick hugs the capsule's bottom edge
1486
+ * (the official idle strategy — the reference port flowed ticks from
1487
+ * the top and clipped the newest away).
1488
+ *
1489
+ * Glass integration: the official chat.deepseek.com paints (light
1490
+ * rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
1491
+ * 5px collapsed / 16px expanded blur) remain as the NO-GLASS fallback.
1492
+ * While the plugin's glass system is on (`body[data-dsh-bg-glass]`), the
1493
+ * rail joins the SAME unified recipe as the composer card and bubbles:
1494
+ * fill = the painter's `--dsw-specific-input-major` token, blur/saturate/
1495
+ * brightness = the shared `--bg-glass-*` variables, i.e. the glass-blur
1496
+ * slider. Class prefix `dsbt-` deliberately avoids every
1497
+ * `[class*="_xxx"]` anchor of the plugin's glass sheet (underscores only).
1498
+ */
1499
+ /** Unique id stamped on the injected style element (dedup key). */
1500
+ const TIMELINE_CSS_TAG = "deepseek-harness-background/timeline";
1501
+ /** Inject the timeline stylesheet once. Idempotent (dedup key on the tag). */
1502
+ function injectTimelineCss() {
1503
+ if (typeof document === "undefined") return;
1504
+ if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
1505
+ const tag = document.createElement("style");
1506
+ tag.dataset.plugin = "deepseek-harness-background";
1507
+ tag.dataset.pluginCss = TIMELINE_CSS_TAG;
1508
+ tag.textContent = TIMELINE_CSS;
1509
+ document.head.appendChild(tag);
1510
+ }
1511
+ const TIMELINE_CSS = `
1512
+ .dsbt-nav {
1513
+ user-select: none; -webkit-user-select: none;
1514
+ /* Filter-bar chrome height shared by the bar itself and the top veil's
1515
+ expanded offset. */
1516
+ --dsbt-bar-h: 33px;
1517
+ z-index: 90; align-items: center; width: 34px;
1518
+ height: var(--dsbt-h, 300px);
1519
+ transition: right .2s ease, opacity .2s ease;
1520
+ display: flex; position: fixed; top: 50%; right: 16px;
1521
+ transform: translateY(-50%); pointer-events: auto;
1522
+ }
1523
+ @media (max-width: 767px) { .dsbt-nav { display: none; } }
1524
+
1525
+ /* Collapsed capsule — frosted strip behind the tick marks. Official paint:
1526
+ light rgba(255,255,255,.8), dark rgba(21,21,23,.6), 5px blur. */
1527
+ .dsbt-bg {
1528
+ background-color: rgba(255, 255, 255, .8);
1529
+ -webkit-backdrop-filter: blur(5px);
1530
+ backdrop-filter: blur(5px);
1531
+ box-shadow:
1532
+ inset 0 1px 0 rgba(255, 255, 255, .28),
1533
+ inset 0 0 0 .5px rgba(255, 255, 255, .1),
1534
+ 0 6px 18px rgba(0, 0, 0, .07);
1535
+ border-radius: 16px; width: 34px;
1536
+ height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
1537
+ position: absolute; top: 50%; right: 0; transform: translateY(-50%);
1538
+ transition: opacity .2s ease; pointer-events: none;
1539
+ }
1540
+ body[data-ds-dark-theme] .dsbt-bg {
1541
+ background-color: rgba(21, 21, 23, .6);
1542
+ box-shadow:
1543
+ inset 0 1px 0 rgba(255, 255, 255, .09),
1544
+ inset 0 0 0 .5px rgba(255, 255, 255, .05),
1545
+ 0 6px 18px rgba(0, 0, 0, .3);
1546
+ }
1547
+ .dsbt-bg.dsbt-bg-hide { opacity: 0; }
1548
+
1549
+ /* Expanding panel — exactly the capsule's box (identical height in both
1550
+ states), width animates 34px -> measured fit (max 260px). Official
1551
+ hover-state paint: white/black frosted glass at ~94% alpha, 16px blur. */
1552
+ .dsbt-wrap {
1553
+ width: 34px;
1554
+ height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
1555
+ border: 1px solid transparent; border-radius: 16px;
1556
+ flex-direction: column; align-items: stretch;
1557
+ transition: width .22s cubic-bezier(.4, 0, .2, 1), background-color .2s ease,
1558
+ box-shadow .2s ease, border-color .2s ease;
1559
+ display: flex; position: absolute; top: 50%; right: 0;
1560
+ transform: translateY(-50%);
1561
+ overflow: hidden; box-sizing: border-box; background: transparent;
1562
+ pointer-events: none;
1563
+ }
1564
+ .dsbt-wrap.dsbt-show {
1565
+ pointer-events: auto;
1566
+ width: min(var(--dsbt-w, 260px), 260px);
1567
+ background-color: rgba(255, 255, 255, .94);
1568
+ -webkit-backdrop-filter: blur(16px);
1569
+ backdrop-filter: blur(16px);
1570
+ border-color: rgba(0, 0, 0, .06);
1571
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .08), 0 2px 8px rgba(0, 0, 0, .04);
1572
+ }
1573
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show {
1574
+ background-color: rgba(28, 28, 32, .95);
1575
+ border-color: rgba(255, 255, 255, .08);
1576
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .25);
1577
+ }
1578
+
1579
+ /* Content fade veils — the official ::before/::after pair rebuilt as
1580
+ elements: a bar of the panel's OWN fill color masked to nothing over
1581
+ 32px (solid -> mask == gradient-to-transparent, but works with the
1582
+ translucent fill). They belong to the EXPANDED panel only — the idle
1583
+ capsule hard-clips its ticks at the rounded ends like the official
1584
+ strip — and while expanded the top veil starts BELOW the filter bar,
1585
+ not over it. Shown only while that side actually clips. */
1586
+ .dsbt-fade {
1587
+ z-index: 2; pointer-events: none; opacity: 0;
1588
+ background-color: rgba(255, 255, 255, .94);
1589
+ -webkit-mask-image: linear-gradient(#000 20.19%, transparent 100%);
1590
+ mask-image: linear-gradient(#000 20.19%, transparent 100%);
1591
+ width: 100%; height: 32px; transition: opacity .2s ease;
1592
+ position: absolute; left: 0; top: 0;
1593
+ }
1594
+ body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
1595
+ .dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
1596
+ .dsbt-wrap.dsbt-show .dsbt-fade.dsbt-fade-on { opacity: 1; }
1597
+ .dsbt-wrap.dsbt-show .dsbt-fade:not(.dsbt-fade-bot) { top: var(--dsbt-bar-h, 33px); }
1598
+
1599
+ /* Filter bar — "marked only" toggle pinned above the scroll area while
1600
+ expanded. Collapsed, it collapses to ZERO height (not just opacity): a
1601
+ transparent-but-in-layout bar pushed every tick ~33px down, one of the
1602
+ reasons the idle capsule read as off-alignment. */
1603
+ .dsbt-filterbar {
1604
+ height: var(--dsbt-bar-h, 33px);
1605
+ padding: 8px 12px 4px; display: flex; justify-content: flex-end;
1606
+ align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
1607
+ box-sizing: border-box; flex: none; overflow: hidden;
1608
+ transition: opacity .2s ease, height .2s ease, padding .2s ease,
1609
+ border-color .2s ease;
1610
+ }
1611
+ .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar {
1612
+ height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
1613
+ border-bottom-color: transparent;
1614
+ }
1615
+ body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
1616
+ .dsbt-filterbtn {
1617
+ font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
1618
+ border: 1px solid rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .03);
1619
+ color: rgba(0, 0, 0, .65); cursor: pointer; transition: all .15s ease;
1620
+ display: inline-flex; align-items: center; gap: 3px; font-family: inherit;
1621
+ }
1622
+ .dsbt-filterbtn:hover { background: rgba(0, 0, 0, .07); color: rgba(0, 0, 0, .9); }
1623
+ .dsbt-filterbtn.dsbt-filteron {
1624
+ background: rgba(245, 158, 11, .12); border-color: #f59e0b;
1625
+ color: #b45309; font-weight: 600;
1626
+ }
1627
+ body[data-ds-dark-theme] .dsbt-filterbtn {
1628
+ border-color: rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .05);
1629
+ color: rgba(255, 255, 255, .7);
1630
+ }
1631
+ body[data-ds-dark-theme] .dsbt-filterbtn:hover { background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .95); }
1632
+ body[data-ds-dark-theme] .dsbt-filterbtn.dsbt-filteron {
1633
+ background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
1634
+ }
1635
+
1636
+ /* Scroll area — rows hug the right edge; scrolls only when overfull. The
1637
+ auto-margin pseudo pair vertically centers the stack when it fits the
1638
+ capsule (short conversations), and resolves to zero free space when it
1639
+ overflows, so bottom-pinned scrolling behaves like a plain column. */
1640
+ .dsbt-page {
1641
+ padding: 15px 0 15px 24px; box-sizing: border-box;
1642
+ overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
1643
+ flex-direction: column; align-items: flex-end; display: flex;
1644
+ position: relative; width: 100%; overflow: hidden;
1645
+ }
1646
+ .dsbt-page::before, .dsbt-page::after {
1647
+ content: ''; flex: none; margin-block: auto;
1648
+ }
1649
+ .dsbt-wrap.dsbt-show .dsbt-page {
1650
+ overflow-y: auto; overflow-x: hidden;
1651
+ scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
1652
+ }
1653
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-page { scrollbar-color: rgba(255, 255, 255, .25) transparent; }
1654
+ .dsbt-page::-webkit-scrollbar { width: 4px; }
1655
+ .dsbt-page::-webkit-scrollbar-track { background: transparent; }
1656
+ .dsbt-page::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, .15); border-radius: 4px; }
1657
+ body[data-ds-dark-theme] .dsbt-page::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .25); }
1658
+
1659
+ /* Empty state (filtered list with no marks). */
1660
+ .dsbt-empty {
1661
+ font-size: 12px; line-height: 18px; padding: 16px 12px;
1662
+ color: rgba(0, 0, 0, .45); text-align: center; width: 100%; box-sizing: border-box;
1663
+ }
1664
+ body[data-ds-dark-theme] .dsbt-empty { color: rgba(255, 255, 255, .45); }
1665
+
1666
+ /* One message row: 30px, star bookmark + tick indicator at the right,
1667
+ title fading in. */
1668
+ /* Row shell: layout + shared ink only — the interactive hit areas are the
1669
+ sibling .dsbt-jump / .dsbt-star buttons below. */
1670
+ .dsbt-item {
1671
+ height: 30px; min-height: 30px; flex-shrink: 0;
1672
+ justify-content: flex-end; align-items: center;
1673
+ width: calc(100% - 4px); margin-right: 4px;
1674
+ display: flex; box-sizing: border-box;
1675
+ color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
1676
+ transition: color .15s ease;
1677
+ }
1678
+
1679
+ /* Jump target: a real button so Enter/Space work natively without nesting
1680
+ an interactive element inside another. */
1681
+ .dsbt-jump {
1682
+ cursor: pointer; background: none; border: none; margin: 0; padding: 0;
1683
+ font: inherit; line-height: 20px; text-align: right; color: inherit;
1684
+ flex: 1 1 auto; min-width: 0; height: 100%;
1685
+ display: flex; align-items: center; justify-content: flex-end;
1686
+ }
1687
+ .dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
1688
+ .dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
1689
+ body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
1690
+ body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
1691
+ .dsbt-title {
1692
+ font-size: 13px; line-height: 20px; flex: 1 1 auto; min-width: 0;
1693
+ text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
1694
+ margin-right: 6px; text-align: right; color: inherit;
1695
+ opacity: 0; transition: opacity .12s ease, color .15s ease;
1696
+ }
1697
+ .dsbt-show .dsbt-title { opacity: 1; }
1698
+
1699
+ /* Star bookmark — appears on row hover while the panel is expanded; gold
1700
+ when marked. A real sibling button next to the jump target, so marking
1701
+ stays keyboard-operable without nested interactive elements. */
1702
+ .dsbt-star {
1703
+ font-size: 13px; line-height: 16px; width: 18px; height: 18px;
1704
+ display: inline-flex; align-items: center; justify-content: center;
1705
+ opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
1706
+ flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
1707
+ -webkit-user-select: none;
1708
+ background: none; border: none; padding: 0; font-family: inherit;
1709
+ }
1710
+ .dsbt-star:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; opacity: .9; }
1711
+ .dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
1712
+ .dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
1713
+ .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
1714
+ body[data-ds-dark-theme] .dsbt-star { color: rgba(255, 255, 255, .25); }
1715
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star:hover { color: #fde047; }
1716
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #fbbf24; }
1717
+
1718
+ .dsbt-ind { flex-shrink: 0; justify-content: center; align-items: center; width: 16px; height: 20px; display: flex; }
1719
+ .dsbt-line {
1720
+ background-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, .16));
1721
+ border-radius: 4px; flex-shrink: 0; width: 8px; height: 2px;
1722
+ transition: background-color .2s ease, transform .2s ease, width .2s ease, height .2s ease;
1723
+ }
1724
+ .dsbt-item:hover .dsbt-line { background-color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .9)); }
1725
+ .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
1726
+ .dsbt-item.dsbt-active .dsbt-title { color: var(--dsw-alias-state-business-primary, #4d6bfe); font-weight: 500; }
1727
+ .dsbt-item.dsbt-active .dsbt-line {
1728
+ background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
1729
+ transform-origin: 50%; transform: scale(1.5);
1730
+ }
1731
+ body[data-ds-dark-theme] .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
1732
+ body[data-ds-dark-theme] .dsbt-line { background-color: rgba(255, 255, 255, .2); }
1733
+ body[data-ds-dark-theme] .dsbt-item:hover .dsbt-line { background-color: rgba(255, 255, 255, .9); }
1734
+
1735
+ /* Marked (key-point) rows: a wider golden tick in both themes; the active
1736
+ state keeps its brand-blue scale so reading position always wins. */
1737
+ .dsbt-item.dsbt-marked .dsbt-line { background-color: #d97706; width: 10px; height: 2.5px; }
1738
+ .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #b45309; }
1739
+ .dsbt-item.dsbt-marked.dsbt-active .dsbt-line {
1740
+ background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
1741
+ transform-origin: 50%; transform: scale(1.5);
1742
+ }
1743
+ body[data-ds-dark-theme] .dsbt-item.dsbt-marked .dsbt-line { background-color: #fbbf24; width: 10px; height: 2.5px; }
1744
+ body[data-ds-dark-theme] .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #fde047; }
1745
+
1746
+ /* ---- Unified glass integration -----------------------------------------
1747
+ Glass system on (wallpaper active + panelOpacity < 1): the rail joins
1748
+ the composer/bubble recipe — the same translucent fill token and the
1749
+ same blur/saturate/brightness chain (the glass-blur slider drives it,
1750
+ no fixed 5/16px). Placed after the dark-theme rules so equal-specificity
1751
+ overrides win in both schemes. */
1752
+ body[data-dsh-bg-glass] .dsbt-bg,
1753
+ body[data-dsh-bg-glass] .dsbt-wrap.dsbt-show {
1754
+ background-color: var(--dsw-specific-input-major);
1755
+ -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);
1756
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
1757
+ }
1758
+ body[data-dsh-bg-glass] .dsbt-fade {
1759
+ background-color: var(--dsw-specific-input-major);
1760
+ }
1761
+
1762
+ @media (prefers-reduced-motion: reduce) {
1763
+ .dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-jump, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
1764
+ }
1765
+ `;
1766
+ //#endregion
1767
+ //#region src/client/timeline.tsx
1768
+ /**
1769
+ * Conversation timeline rail — DeepSeek web ScrollNav rebuilt on this
1770
+ * plugin's frosted-glass system (styles: ./timeline-css.ts).
1771
+ *
1772
+ * Collapsed: a fixed 34px-wide frosted capsule at the right edge, vertically
1773
+ * centered, one tick per user message (active tick scaled + brand blue,
1774
+ * key-point bookmarks golden). Hovering expands the SAME box into a frosted
1775
+ * panel listing every user question (title fades in; >8 messages widen to
1776
+ * 260px, otherwise the width fits the longest title). Both states share one
1777
+ * computed outer height, so expanding never jumps, and 32px fade veils mask
1778
+ * the clipped edge on each side while that side actually scrolls.
1779
+ *
1780
+ * Enhancements ported from dsh-chat-timeline v0.1.4 (MIT):
1781
+ * - Key-point bookmarks: star a question in the expanded panel, persisted per
1782
+ * session in localStorage; marked rows show a golden tick in the capsule
1783
+ * and a "marked only" filter narrows the list.
1784
+ * - Jump stabilization: the reading-position tracker freezes while a smooth
1785
+ * jump animates (cleared 150ms after the last scroll event, plus an 800ms
1786
+ * fallback), so the panel cannot jitter mid-jump.
1787
+ * - Rewind integration: messages withdrawn by a rewind command node are
1788
+ * filtered out of the rail.
1789
+ *
1790
+ * Data source, fastest first: the host-side `bgTimeline` session projection
1791
+ * (src/projection.ts — a complete enumeration of the session's user messages
1792
+ * delivered through the projection baseline/push channel) -> the loaded chat
1793
+ * node window. The conversation's own lazy loading is never driven from here;
1794
+ * clicking a row pages older history on demand, scrolls the conversation to
1795
+ * the target row, and keeps the rail's own active row visible.
1796
+ *
1797
+ * Collapsed idle strategy (official ScrollNav behavior): the tick stack is
1798
+ * pinned to the BOTTOM inner edge — the newest question's tick always sits at
1799
+ * the bottom of the capsule, older ticks clip away at the top. Hovering
1800
+ * expands the SAME box and scrolls the active row into view instead.
1801
+ *
1802
+ * The rail reads the shared settings transport: it renders unless
1803
+ * `timeline: false`. Its glass paints are self-contained official values
1804
+ * (see timeline-css.ts), independent of the plugin's token overrides.
1805
+ */
1806
+ if (typeof document !== "undefined") injectTimelineCss();
1807
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1808
+ /** Extract preview text from a user message's ContentBlock list. */
1809
+ function userTextOf(content) {
1810
+ if (!Array.isArray(content)) return "";
1811
+ let out = "";
1812
+ for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
1813
+ const text = block.text;
1814
+ if (typeof text === "string") out += text;
1815
+ }
1816
+ return out.trim().slice(0, 80);
1817
+ }
1818
+ /**
1819
+ * Stable bookmark key for one entry: the durable message key, falling back to
1820
+ * the anchor seq — so marks survive history reloads. Entries with neither are
1821
+ * not markable (empty key).
1822
+ * @param m - the entry (or any shaped object).
1823
+ */
1824
+ function markKeyOf(m) {
1825
+ if (m === null || typeof m !== "object") return "";
1826
+ const rec = m;
1827
+ if (typeof rec.key === "string" && rec.key !== "") return `key:${rec.key}`;
1828
+ return typeof rec.seq === "number" ? `seq:${rec.seq}` : "";
1829
+ }
1830
+ /** localStorage key prefix for the per-session marked-key lists. */
1831
+ const MARKS_STORAGE_PREFIX = "dsbt_marks_";
1832
+ /**
1833
+ * Read the marked keys for one session from localStorage.
1834
+ * @param sessionId - the session the marks belong to.
1835
+ */
1836
+ function readMarks(sessionId) {
1837
+ if (typeof window === "undefined" || !sessionId) return [];
1838
+ try {
1839
+ const raw = window.localStorage.getItem(MARKS_STORAGE_PREFIX + sessionId);
1840
+ if (!raw) return [];
1841
+ const parsed = JSON.parse(raw);
1842
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
1843
+ } catch {
1844
+ return [];
1845
+ }
1846
+ }
1847
+ /**
1848
+ * Persist the marked keys for one session (removes the key when empty).
1849
+ * @param sessionId - the session the marks belong to.
1850
+ * @param marks - the marked-key list.
1851
+ */
1852
+ function writeMarks(sessionId, marks) {
1853
+ if (typeof window === "undefined" || !sessionId) return;
1854
+ try {
1855
+ if (!Array.isArray(marks) || marks.length === 0) window.localStorage.removeItem(MARKS_STORAGE_PREFIX + sessionId);
1856
+ else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
1857
+ } catch {}
1858
+ }
1859
+ /**
1860
+ * Extract the rewind target seq from a rewind outcome text.
1861
+ * @param text - the command outcome text.
1862
+ */
1863
+ function rewindTargetOfOutcome(text) {
1864
+ if (typeof text !== "string") return void 0;
1865
+ const digits = (/rewound.*?(?:target\s+)?(\d+)/i.exec(text) ?? /#(\d+)/.exec(text))?.[1];
1866
+ if (digits === void 0) return void 0;
1867
+ const n = Number.parseInt(digits, 10);
1868
+ return Number.isFinite(n) ? n : void 0;
1869
+ }
1870
+ /**
1871
+ * Is this rewind command a preview/dry-run (marks the target hidden without
1872
+ * an outcome)?
1873
+ * @param command - the command payload.
1874
+ */
1875
+ function isRewindPreviewCommand(command) {
1876
+ if (command.args === null || typeof command.args !== "object") return false;
1877
+ const args = command.args;
1878
+ return args.preview === true || args.dryRun === true;
1879
+ }
1880
+ /**
1881
+ * Collect the seq numbers a rewind hid: preview commands hide their target
1882
+ * directly; successful commands hide their own row plus every node anchored
1883
+ * inside the rewound span [target, sourceEventSeq].
1884
+ * @param chat - a chat snapshot (loosely typed on purpose).
1885
+ */
1886
+ function hiddenSeqsOfChat(chat) {
1887
+ const hidden = rewindHiddenSeqsOfChat(chat);
1888
+ const spans = rewindSpansOfChat(chat);
1889
+ if (spans.length === 0) return hidden;
1890
+ const nodes = chat.nodes;
1891
+ if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1892
+ for (const node of nodes.values()) {
1893
+ if (node === null || typeof node !== "object") continue;
1894
+ const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
1895
+ if (anchor === void 0) continue;
1896
+ if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
1897
+ }
1898
+ return hidden;
1899
+ }
1900
+ /**
1901
+ * Seq numbers a rewind hid directly: successful commands hide their own row;
1902
+ * preview commands hide their target.
1903
+ * @param chat - a chat snapshot (loosely typed on purpose).
1904
+ */
1905
+ function rewindHiddenSeqsOfChat(chat) {
1906
+ const hidden = /* @__PURE__ */ new Set();
1907
+ if (chat === null || typeof chat !== "object") return hidden;
1908
+ const nodes = chat.nodes;
1909
+ if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1910
+ for (const node of nodes.values()) {
1911
+ if (node === null || typeof node !== "object") continue;
1912
+ if (node.kind !== "command") continue;
1913
+ const command = node.data;
1914
+ if (command === null || typeof command !== "object") continue;
1915
+ if (command.name !== "rewind") continue;
1916
+ if (isRewindPreviewCommand(command)) {
1917
+ if (typeof command.seq === "number") hidden.add(command.seq);
1918
+ continue;
1919
+ }
1920
+ const outcome = command.outcome;
1921
+ if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1922
+ if (typeof command.seq === "number") hidden.add(command.seq);
1923
+ }
1924
+ return hidden;
1925
+ }
1926
+ /**
1927
+ * The [target, sourceEventSeq] spans successful rewind commands cut — the
1928
+ * range form of hiding, needed to filter sources that are NOT chat nodes
1929
+ * (the host projection) by plain seq membership.
1930
+ * @param chat - a chat snapshot (loosely typed on purpose).
1931
+ */
1932
+ function rewindSpansOfChat(chat) {
1933
+ const spans = [];
1934
+ if (chat === null || typeof chat !== "object") return spans;
1935
+ const nodes = chat.nodes;
1936
+ if (nodes === void 0 || typeof nodes.values !== "function") return spans;
1937
+ for (const node of nodes.values()) {
1938
+ if (node === null || typeof node !== "object") continue;
1939
+ if (node.kind !== "command") continue;
1940
+ const command = node.data;
1941
+ if (command === null || typeof command !== "object") continue;
1942
+ if (command.name !== "rewind") continue;
1943
+ if (isRewindPreviewCommand(command)) continue;
1944
+ const outcome = command.outcome;
1945
+ if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1946
+ const marker = outcome.sourceEventSeq;
1947
+ if (typeof marker !== "number") continue;
1948
+ const target = rewindTargetOfOutcome(outcome.text);
1949
+ if (target !== void 0) spans.push({
1950
+ start: target,
1951
+ end: marker
1952
+ });
1953
+ }
1954
+ return spans;
1955
+ }
1956
+ /**
1957
+ * Enumerate user messages from the loaded chat-node snapshot (the client-side
1958
+ * collector; sorted by anchor sequence, rewind-hidden entries dropped).
1959
+ * @param snapshot - a session snapshot (loosely typed on purpose).
1960
+ */
1961
+ function collectMessages(snapshot) {
1962
+ const chat = snapshot?.chat;
1963
+ const nodes = chat?.nodes;
1964
+ if (nodes === void 0 || typeof nodes.values !== "function") return [];
1965
+ const hidden = hiddenSeqsOfChat(chat);
1966
+ const out = [];
1967
+ for (const node of nodes.values()) {
1968
+ if (node === null || typeof node !== "object") continue;
1969
+ if (node.kind !== "user") continue;
1970
+ const data = node.data;
1971
+ if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
1972
+ const key = typeof node.key === "string" ? node.key : void 0;
1973
+ if (key === void 0) continue;
1974
+ const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
1975
+ if (hidden.has(seq)) continue;
1976
+ out.push({
1977
+ seq,
1978
+ time: data.time,
1979
+ text: userTextOf(data.content),
1980
+ key
1981
+ });
1982
+ }
1983
+ out.sort((a, b) => a.seq - b.seq);
1984
+ return out;
1985
+ }
1986
+ /** Resolve the DOM anchor key of one entry (direct key form only). */
1987
+ function resolveAnchorKey(m) {
1988
+ return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
1989
+ }
1990
+ /** The user-message node definition kind in ui-conversation. */
1991
+ const INPUT_MESSAGE_KIND = "input-message";
1992
+ /**
1993
+ * Rebuild a chat row's DOM anchor key from the durable message id — the same
1994
+ * engine formula as conversationContextKey(kind, id) for the input-message
1995
+ * definition (`13:input-message<id>`), so projected entries can jump even
1996
+ * when their chat node is not loaded yet.
1997
+ * @param id - the durable message id carried by the projection entry.
1998
+ */
1999
+ function inputAnchorKeyOf(id) {
2000
+ return `13:${INPUT_MESSAGE_KIND}${id}`;
2001
+ }
2002
+ /**
2003
+ * Validate one projected wire value into rail entries (defensive: the value
2004
+ * crosses the wire and may be absent, partial, or stale).
2005
+ * @param value - the raw useProjection(TIMELINE_PROJECTION_KEY) snapshot.
2006
+ */
2007
+ function normalizeProjectedTimeline(value) {
2008
+ if (value === null || typeof value !== "object") return [];
2009
+ const raw = value.messages;
2010
+ if (!Array.isArray(raw)) return [];
2011
+ const seen = /* @__PURE__ */ new Set();
2012
+ const out = [];
2013
+ for (const item of raw) {
2014
+ if (item === null || typeof item !== "object") continue;
2015
+ const rec = item;
2016
+ if (typeof rec.seq !== "number" || !Number.isFinite(rec.seq)) continue;
2017
+ if (seen.has(rec.seq)) continue;
2018
+ seen.add(rec.seq);
2019
+ out.push({
2020
+ seq: rec.seq,
2021
+ time: typeof rec.time === "number" ? rec.time : 0,
2022
+ text: typeof rec.text === "string" ? rec.text.slice(0, 80) : "",
2023
+ ...typeof rec.id === "string" && rec.id !== "" ? { key: inputAnchorKeyOf(rec.id) } : {}
2024
+ });
2025
+ }
2026
+ out.sort((a, b) => a.seq - b.seq);
2027
+ return out;
2028
+ }
2029
+ /**
2030
+ * Rail messages with the fastest source first: the host projection covers the
2031
+ * WHOLE session (and already excludes rewind cuts), so it wins whenever it
2032
+ * has anything; otherwise fall back to the loaded chat-node window. The
2033
+ * projection path still applies locally-known rewind hiding so a cut lands
2034
+ * instantly even before the next projection push arrives.
2035
+ * @param snapshot - the session conversation snapshot (node fallback).
2036
+ * @param projected - the raw bgTimeline projection value.
2037
+ */
2038
+ function railMessages(snapshot, projected) {
2039
+ const projectedMessages = normalizeProjectedTimeline(projected);
2040
+ if (projectedMessages.length > 0) {
2041
+ const chat = snapshot?.chat;
2042
+ const hidden = rewindHiddenSeqsOfChat(chat);
2043
+ const spans = rewindSpansOfChat(chat);
2044
+ if (hidden.size === 0 && spans.length === 0) return projectedMessages;
2045
+ return projectedMessages.filter((m) => !hidden.has(m.seq) && !spans.some((span) => m.seq >= span.start && m.seq <= span.end));
2046
+ }
2047
+ return collectMessages(snapshot);
2048
+ }
2049
+ /**
2050
+ * Outer rail height for a message count: rows + breathing room, clamped so
2051
+ * both states always share the exact same height (never jumps on expand).
2052
+ * @param count - number of user messages.
2053
+ */
2054
+ function railHeightFor(count) {
2055
+ return Math.max(140, Math.min(300, count * 30 + 38));
2056
+ }
2057
+ /** Longest-title width estimate bounds. */
2058
+ const RAIL_MIN_WIDTH = 96;
2059
+ const RAIL_MAX_WIDTH = 260;
2060
+ /**
2061
+ * Measure the expanded-panel width for the given titles (canvas text metrics;
2062
+ * falls back to the max width when measurement is unavailable). Accounts for
2063
+ * the star column so a marked row never clips its tick.
2064
+ * @param texts - the message previews.
2065
+ */
2066
+ function railWidthFor(texts) {
2067
+ if (texts.length > 8) return RAIL_MAX_WIDTH;
2068
+ let widest = 0;
2069
+ if (typeof document !== "undefined") {
2070
+ const ctx = document.createElement("canvas").getContext("2d");
2071
+ if (ctx) {
2072
+ ctx.font = `13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
2073
+ for (const t of texts) {
2074
+ const w = ctx.measureText(t === "" ? "…" : t).width;
2075
+ if (w > widest) widest = w;
2076
+ }
2077
+ }
2078
+ }
2079
+ return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
2080
+ }
2081
+ /** Ensure the target node is loaded, then scroll its row into view. */
2082
+ async function jumpToMessage(sessionsService, sessionId, key) {
2083
+ const session = sessionsService.binding(sessionId)?.session;
2084
+ if (session === void 0) return false;
2085
+ let guard = 0;
2086
+ while (guard++ < 120) {
2087
+ const snapshot = session.getSnapshot();
2088
+ if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
2089
+ if (snapshot?.hasMore !== true) return false;
2090
+ if (snapshot.loadingOlder === true) {
2091
+ await delay(50);
2092
+ continue;
2093
+ }
2094
+ await session.loadOlder();
2095
+ }
2096
+ const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
2097
+ if (row === null) return false;
2098
+ const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2099
+ row.scrollIntoView({
2100
+ behavior: reducedMotion ? "auto" : "smooth",
2101
+ block: "center"
2102
+ });
2103
+ return true;
2104
+ }
2105
+ /**
2106
+ * Scroll only the rail itself so the active row stays visible (min distance).
2107
+ * offsetTop math instead of getBoundingClientRect: stable while the panel's
2108
+ * own smooth scroll animates (no feedback loop with live rects).
2109
+ */
2110
+ function scrollTimelineItemIntoView(page, item) {
2111
+ const pageTop = page.scrollTop;
2112
+ const pageBottom = pageTop + page.clientHeight;
2113
+ const itemTop = item.offsetTop;
2114
+ const itemBottom = itemTop + item.offsetHeight;
2115
+ if (itemTop < pageTop) page.scrollTop = Math.max(0, itemTop - 10);
2116
+ else if (itemBottom > pageBottom) page.scrollTop = itemBottom - page.clientHeight + 10;
2117
+ }
2118
+ /** Is another timeline rail already mounted (e.g. dsh-chat-timeline)? */
2119
+ function otherRailPresent() {
2120
+ return document.querySelector(".dsct_nav") !== null;
2121
+ }
2122
+ /**
2123
+ * The timeline rail component. Portaled to body; registered into the
2124
+ * conversation input dock purely to bind per-session lifecycle.
2125
+ */
2126
+ function TimelineRail(props) {
2127
+ const { sessionId, sessionsService, useProjection, t } = props;
2128
+ const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
2129
+ const timelineUnknown = settings.status === "loading";
2130
+ const timelineDisabled = settings.value?.timeline === false;
2131
+ const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
2132
+ const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
2133
+ const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
2134
+ const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
2135
+ const projected = useProjection !== void 0 ? useProjection(TIMELINE_PROJECTION_KEY) : void 0;
2136
+ const messages = react.default.useMemo(() => railMessages(nodeSnapshot, projected), [nodeSnapshot, projected]);
2137
+ const [activeIndex, setActiveIndex] = react.default.useState(-1);
2138
+ const [show, setShow] = react.default.useState(false);
2139
+ const [rightOffset, setRightOffset] = react.default.useState(16);
2140
+ const [railWidth, setRailWidth] = react.default.useState(RAIL_MAX_WIDTH);
2141
+ const [fades, setFades] = react.default.useState({
2142
+ top: false,
2143
+ bottom: false
2144
+ });
2145
+ const [hidden, setHidden] = react.default.useState(() => otherRailPresent());
2146
+ const [marks, setMarks] = react.default.useState(() => readMarks(sessionId));
2147
+ const [marksOnly, setMarksOnly] = react.default.useState(false);
2148
+ const [isNarrow, setIsNarrow] = react.default.useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches);
2149
+ const pageRef = react.default.useRef(null);
2150
+ const activeItemRef = react.default.useRef(null);
2151
+ const jumpPendingRef = react.default.useRef(false);
2152
+ const jumpFallbackRef = react.default.useRef(void 0);
2153
+ react.default.useEffect(() => {
2154
+ setMarks(readMarks(sessionId));
2155
+ setMarksOnly(false);
2156
+ }, [sessionId]);
2157
+ const markedSet = react.default.useMemo(() => new Set(marks), [marks]);
2158
+ const marksRef = react.default.useRef(marks);
2159
+ marksRef.current = marks;
2160
+ const toggleMark = react.default.useCallback((m) => {
2161
+ const k = markKeyOf(m);
2162
+ if (k === "") return;
2163
+ const prev = marksRef.current;
2164
+ const next = prev.includes(k) ? prev.filter((x) => x !== k) : [...prev, k];
2165
+ marksRef.current = next;
2166
+ setMarks(next);
2167
+ writeMarks(sessionId, next);
2168
+ }, [sessionId]);
2169
+ react.default.useEffect(() => {
2170
+ if (typeof window.matchMedia !== "function") return;
2171
+ const mql = window.matchMedia("(max-width: 767px)");
2172
+ const onChange = (e) => setIsNarrow(e.matches);
2173
+ mql.addEventListener("change", onChange);
2174
+ return () => mql.removeEventListener("change", onChange);
2175
+ }, []);
2176
+ react.default.useEffect(() => {
2177
+ setRailWidth(railWidthFor(messages.map((m) => m.text)));
2178
+ }, [messages]);
2179
+ react.default.useEffect(() => {
2180
+ if (messages.length === 0) return;
2181
+ const indexByKey = /* @__PURE__ */ new Map();
2182
+ messages.forEach((m, i) => {
2183
+ const key = resolveAnchorKey(m);
2184
+ if (key !== void 0) indexByKey.set(key, i);
2185
+ });
2186
+ const sp = document.querySelector("[data-conversation-scroll]");
2187
+ if (sp === null) return;
2188
+ const rows = sp.querySelectorAll("[data-chat-anchor-key]");
2189
+ let settleTimer;
2190
+ let frame = 0;
2191
+ const nearRows = /* @__PURE__ */ new Set();
2192
+ const updateActive = () => {
2193
+ if (jumpPendingRef.current) return;
2194
+ const rect = sp.getBoundingClientRect();
2195
+ if (rect.height === 0) return;
2196
+ const candidates = nearRows.size > 0 ? nearRows : rows;
2197
+ const line = rect.top + rect.height * .4;
2198
+ let best = -1;
2199
+ let bestDist = Infinity;
2200
+ for (const row of candidates) {
2201
+ const key = row.getAttribute("data-chat-anchor-key");
2202
+ if (key === null) continue;
2203
+ const idx = indexByKey.get(key) ?? -1;
2204
+ if (idx === -1) continue;
2205
+ const r = row.getBoundingClientRect();
2206
+ const dist = Math.abs(r.top + r.height / 2 - line);
2207
+ if (dist < bestDist) {
2208
+ bestDist = dist;
2209
+ best = idx;
2210
+ }
2211
+ }
2212
+ setActiveIndex(best);
2213
+ };
2214
+ const scheduleMeasure = () => {
2215
+ if (frame !== 0) return;
2216
+ frame = window.requestAnimationFrame(() => {
2217
+ frame = 0;
2218
+ updateActive();
2219
+ });
2220
+ };
2221
+ let io;
2222
+ if (typeof IntersectionObserver === "function") {
2223
+ io = new IntersectionObserver((entries) => {
2224
+ for (const entry of entries) if (entry.isIntersecting) nearRows.add(entry.target);
2225
+ else nearRows.delete(entry.target);
2226
+ scheduleMeasure();
2227
+ }, { root: sp });
2228
+ rows.forEach((row) => io?.observe(row));
2229
+ }
2230
+ const onScroll = () => {
2231
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
2232
+ settleTimer = window.setTimeout(() => {
2233
+ settleTimer = void 0;
2234
+ if (jumpPendingRef.current) {
2235
+ jumpPendingRef.current = false;
2236
+ if (jumpFallbackRef.current !== void 0) {
2237
+ clearTimeout(jumpFallbackRef.current);
2238
+ jumpFallbackRef.current = void 0;
2239
+ }
2240
+ updateActive();
2241
+ }
2242
+ }, 150);
2243
+ scheduleMeasure();
2244
+ };
2245
+ sp.addEventListener("scroll", onScroll, { passive: true });
2246
+ const interval = window.setInterval(scheduleMeasure, 2e3);
2247
+ scheduleMeasure();
2248
+ return () => {
2249
+ io?.disconnect();
2250
+ if (frame !== 0) cancelAnimationFrame(frame);
2251
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
2252
+ if (jumpFallbackRef.current !== void 0) {
2253
+ clearTimeout(jumpFallbackRef.current);
2254
+ jumpFallbackRef.current = void 0;
2255
+ }
2256
+ sp.removeEventListener("scroll", onScroll);
2257
+ clearInterval(interval);
2258
+ };
2259
+ }, [messages]);
2260
+ const syncFades = react.default.useCallback(() => {
2261
+ const page = pageRef.current;
2262
+ if (page === null) {
2263
+ setFades({
2264
+ top: false,
2265
+ bottom: false
2266
+ });
2267
+ return;
2268
+ }
2269
+ const canTop = page.scrollTop > 2;
2270
+ const canBot = page.scrollTop + page.clientHeight < page.scrollHeight - 2;
2271
+ setFades((prev) => prev.top === canTop && prev.bottom === canBot ? prev : {
2272
+ top: canTop,
2273
+ bottom: canBot
2274
+ });
2275
+ }, []);
2276
+ react.default.useEffect(syncFades, [
2277
+ syncFades,
2278
+ show,
2279
+ messages.length,
2280
+ marksOnly
2281
+ ]);
2282
+ react.default.useLayoutEffect(() => {
2283
+ if (!show) return;
2284
+ const page = pageRef.current;
2285
+ const item = activeItemRef.current;
2286
+ if (page === null || item === null) return;
2287
+ scrollTimelineItemIntoView(page, item);
2288
+ syncFades();
2289
+ }, [
2290
+ activeIndex,
2291
+ messages.length,
2292
+ marksOnly,
2293
+ show,
2294
+ syncFades
2295
+ ]);
2296
+ const height = railHeightFor(messages.length);
2297
+ react.default.useLayoutEffect(() => {
2298
+ if (show) return;
2299
+ const page = pageRef.current;
2300
+ if (page === null) return;
2301
+ page.scrollTop = page.scrollHeight;
2302
+ }, [
2303
+ show,
2304
+ messages.length,
2305
+ marksOnly,
2306
+ height
2307
+ ]);
2308
+ react.default.useEffect(() => {
2309
+ let raf = 0;
2310
+ const measure = () => {
2311
+ raf = 0;
2312
+ const sp = document.querySelector("[data-conversation-scroll]");
2313
+ if (sp === null) return;
2314
+ const rect = sp.getBoundingClientRect();
2315
+ if (rect.width === 0 && rect.height === 0) return;
2316
+ const next = Math.max(8, Math.round(window.innerWidth - rect.right + 12));
2317
+ setRightOffset((prev) => Math.abs(prev - next) > .5 ? next : prev);
2318
+ };
2319
+ const schedule = () => {
2320
+ if (raf === 0) raf = window.requestAnimationFrame(measure);
2321
+ };
2322
+ const sp = document.querySelector("[data-conversation-scroll]");
2323
+ const ro = typeof ResizeObserver === "function" && sp !== null ? new ResizeObserver(schedule) : null;
2324
+ if (ro !== null && sp !== null) ro.observe(sp);
2325
+ window.addEventListener("resize", schedule);
2326
+ const guard = window.setInterval(() => setHidden(otherRailPresent()), 2e3);
2327
+ measure();
2328
+ return () => {
2329
+ if (raf !== 0) cancelAnimationFrame(raf);
2330
+ ro?.disconnect();
2331
+ window.removeEventListener("resize", schedule);
2332
+ clearInterval(guard);
2333
+ };
2334
+ }, []);
2335
+ if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length === 0) return null;
2336
+ const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
2337
+ const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
2338
+ return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2339
+ className: "dsbt-nav",
2340
+ style: {
2341
+ right: rightOffset,
2342
+ ["--dsbt-h"]: `${height}px`,
2343
+ ["--dsbt-w"]: `${railWidth}px`
2344
+ },
2345
+ role: "navigation",
2346
+ "aria-label": t("timeline.railLabel"),
2347
+ onMouseEnter: () => setShow(true),
2348
+ onMouseLeave: () => setShow(false),
2349
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: show ? "dsbt-bg dsbt-bg-hide" : "dsbt-bg" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2350
+ className: show ? "dsbt-wrap dsbt-show" : "dsbt-wrap",
2351
+ children: [
2352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2353
+ className: "dsbt-filterbar",
2354
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2355
+ type: "button",
2356
+ className: marksOnly ? "dsbt-filterbtn dsbt-filteron" : "dsbt-filterbtn",
2357
+ "aria-pressed": marksOnly,
2358
+ onClick: () => setMarksOnly((v) => !v),
2359
+ children: [t("timeline.marksOnly"), marks.length > 0 ? ` (${marks.length})` : ""]
2360
+ })
2361
+ }),
2362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2363
+ className: fades.top ? "dsbt-fade dsbt-fade-on" : "dsbt-fade",
2364
+ "aria-hidden": true
2365
+ }),
2366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2367
+ className: fades.bottom ? "dsbt-fade dsbt-fade-bot dsbt-fade-on" : "dsbt-fade dsbt-fade-bot",
2368
+ "aria-hidden": true
2369
+ }),
2370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2371
+ className: "dsbt-page",
2372
+ ref: pageRef,
2373
+ onScroll: syncFades,
2374
+ children: displayMessages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2375
+ className: "dsbt-empty",
2376
+ children: t("timeline.noMarks")
2377
+ }) : displayMessages.map((m) => {
2378
+ const key = resolveAnchorKey(m);
2379
+ const mk = markKeyOf(m);
2380
+ const marked = markedSet.has(mk);
2381
+ const isActive = activeKey !== "" && mk === activeKey;
2382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2383
+ className: `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`,
2384
+ children: [
2385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2386
+ type: "button",
2387
+ className: "dsbt-jump",
2388
+ ref: isActive ? activeItemRef : void 0,
2389
+ title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2390
+ "aria-label": `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
2391
+ "aria-current": isActive ? "location" : void 0,
2392
+ onClick: () => {
2393
+ if (key === void 0) return;
2394
+ jumpPendingRef.current = true;
2395
+ if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
2396
+ jumpFallbackRef.current = window.setTimeout(() => {
2397
+ jumpFallbackRef.current = void 0;
2398
+ jumpPendingRef.current = false;
2399
+ }, 800);
2400
+ jumpToMessage(sessionsService, sessionId, key).catch(() => {});
2401
+ },
2402
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2403
+ className: "dsbt-title",
2404
+ children: m.text === "" ? t("timeline.noText") : m.text
2405
+ })
2406
+ }),
2407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2408
+ type: "button",
2409
+ className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
2410
+ "aria-pressed": marked,
2411
+ "aria-label": marked ? t("timeline.unmark") : t("timeline.mark"),
2412
+ onClick: () => toggleMark(m),
2413
+ children: "★"
2414
+ }),
2415
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2416
+ className: "dsbt-ind",
2417
+ "aria-hidden": true,
2418
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsbt-line" })
2419
+ })
2420
+ ]
2421
+ }, m.seq);
2422
+ })
2423
+ })
2424
+ ]
2425
+ })]
2426
+ }), document.body);
2427
+ }
2428
+ //#endregion
1095
2429
  //#region src/client/index.ts
1096
2430
  /**
1097
- * Service injections this entry declares: the `slots` (runtime) and `locale`
1098
- * services only — the settings surface talks to the plugin's own host route
1099
- * over plain same-origin fetch, so no settings/connection service is needed.
2431
+ * Service injections this entry declares: `slots` + `locale` for the settings
2432
+ * surface and the timeline slot seat, and `sessions` so the timeline rail can
2433
+ * enumerate user messages and jump between them. The settings surface itself
2434
+ * talks to the plugin's own host route over plain same-origin fetch.
1100
2435
  */
1101
- const inject = ["slots", "locale"];
2436
+ const inject = [
2437
+ "slots",
2438
+ "locale",
2439
+ "sessions"
2440
+ ];
1102
2441
  /**
1103
- * Client plugin body: paint the persisted background live and register the
1104
- * General-settings row.
1105
- * @param ctx - client cordis context (slots/locale injected).
2442
+ * Client plugin body: paint the persisted background live, register the
2443
+ * General-settings row, and mount the conversation timeline rail.
2444
+ * @param ctx - client cordis context (slots/locale/sessions injected).
1106
2445
  */
1107
2446
  function apply(ctx) {
1108
2447
  const paint = () => {
@@ -1126,6 +2465,13 @@ window.__ModuleLoader__.load({
1126
2465
  order: 600,
1127
2466
  locale: "ui-background"
1128
2467
  }, BackgroundSettingsRow));
2468
+ ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
2469
+ name: "conversation.input.dock",
2470
+ id: "deepseek-harness-background.timeline",
2471
+ order: 45,
2472
+ locale: "ui-background",
2473
+ inject: () => ({ sessionsService: ctx.sessions })
2474
+ }, TimelineRail));
1129
2475
  ctx.effect(() => () => {
1130
2476
  unsubscribe();
1131
2477
  backgroundPainter.dispose();