deepseek-harness-background 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,28 +4,84 @@ 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 agent task strip family (`
65
+ * --dsw-specific-tip`), the subagent lineage popover, the home hero
66
+ * "preview" badge, and the timeline rail (timeline-css.ts). Every glassed
67
+ * surface shares ONE recipe: fill from the painter's --dsw-specific-input-
68
+ * major token (same alpha curve + theme dimming), the full shared
69
+ * blur/saturate/brightness chain driven by the glass-blur slider, and the
70
+ * shared sheen vars — so no surface reads heavier than the composer card.
71
+ * Dialogs, the settings UI, menus, tooltips, toasts and every hover/accent
72
+ * fill keep the OFFICIAL opaque paints — they are reading surfaces and must
73
+ * stay legible.
74
+ *
75
+ * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
76
+ * `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
77
+ * SUFFIX conventions (`_bubble`, `_newSession`…) audited for collisions:
78
+ * `_bubble` excludes `role="tooltip"` (Tooltip.module.css shares the suffix),
79
+ * `_add` is scoped under `[data-composer-card]` (DiffBlock line markers share
80
+ * it), `_newSession` excludes `Label` (the button's inner label span) and
81
+ * `_toBottom` excludes `Slot` (the zero-height sticky wrapper) — both share
82
+ * the substring with the chrome control but must keep their own paint, and
83
+ * the subagent popover is `role="tree"` + `_menu` so the generic menu
84
+ * surface stays untouched.
29
85
  */
30
86
  const BACKGROUND_CSS = `
31
87
  .dsh-bg-layer {
@@ -58,22 +114,118 @@ window.__ModuleLoader__.load({
58
114
  --dsw-specific-sidebar-fill: transparent;
59
115
  }
60
116
 
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);
117
+ /* ---- Whitelisted frosted-glass sheet ---------------------------------
118
+ Fills come from the --dsw-* tokens the painter overrides on body ONLY for
119
+ this list's own surfaces (input/bubble/code/tip); this block adds the
120
+ shared wet-glass sheen + blur from --bg-glass-blur. Exposure is calibrated
121
+ per scheme (the painter writes --bg-glass-brightness / --bg-glass-sheen):
122
+ light glass slightly DIMS, dark glass keeps a slight lift. Gated on the
123
+ GLASS ATTR (wallpaper active AND panelOpacity < 1): maxing the panel
124
+ slider returns these surfaces to the exact official paints. */
125
+ body[data-dsh-bg-glass] [data-composer-card],
126
+ body[data-dsh-bg-glass] [class*="_bubble"]:not([role="tooltip"]),
127
+ body[data-dsh-bg-glass] .md-code-block,
128
+ body[data-dsh-bg-glass] [data-terminal],
129
+ body[data-dsh-bg-glass] [data-diff],
130
+ body[data-dsh-bg-glass] [data-read],
131
+ body[data-dsh-bg-glass] [data-search],
132
+ body[data-dsh-bg-glass] [data-web],
133
+ body[data-dsh-bg-glass] [class*="_ioCard"],
134
+ body[data-dsh-bg-glass] [class*="_instructionsCard"],
135
+ body[data-dsh-bg-glass] [class*="_markdown"] :not(pre) > code {
136
+ 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));
137
+ -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);
138
+ 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
139
  box-shadow:
73
- inset 0 1px 0 rgba(255, 255, 255, var(--bg-glass-highlight, 0.32)),
140
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
74
141
  inset 0 -1px 0 rgba(255, 255, 255, 0.08),
75
142
  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));
143
+ 0 12px 40px rgba(0, 0, 0, 0.12);
144
+ }
145
+
146
+ /* The sticky code-block header officially occludes scrolled code with
147
+ bg-base (transparent under a wallpaper): refill it with the banner token
148
+ (itself translucent glass) so code stays hidden beneath the sticky bar. */
149
+ body[data-dsh-bg] .md-code-block [class*="_bannerWrap"] {
150
+ background-color: var(--dsw-alias-markdown-code-block-banner);
151
+ }
152
+
153
+ /* The sidebar build badge paints with the label-ink token — a TEXT token we
154
+ must not override globally — so it gets a theme-aware translucent ink. */
155
+ body[data-dsh-bg] [class*="_buildRevision"] {
156
+ background-color: rgba(15, 17, 21, 0.62);
157
+ }
158
+ body[data-ds-dark-theme][data-dsh-bg] [class*="_buildRevision"] {
159
+ background-color: rgba(249, 250, 251, 0.62);
160
+ }
161
+
162
+ /* The empty-state hero glow asset would wash out the wallpaper behind the
163
+ welcome message — dim it so the art stays visible. */
164
+ body[data-dsh-bg] [class*="_heroGlow"] {
165
+ opacity: 0.2;
166
+ }
167
+
168
+ /* ---- Whitelisted chrome buttons --------------------------------------
169
+ These three buttons' official fills ride button-* tokens shared with
170
+ unrelated surfaces, so instead of overriding the tokens they get explicit
171
+ paints — but the SAME glass recipe as the composer card and bubbles:
172
+ fill = the painter's --dsw-specific-input-major token (the identical
173
+ translucent white + theme curve), filter = the sheet's exact
174
+ blur/saturate/brightness chain (blur follows the glass-blur slider, no
175
+ caps), sheen = the shared sheen vars. Hover = the same white glass at the
176
+ boosted alpha var. Gated on data-dsh-bg-glass (off when the panel is
177
+ fully opaque). */
178
+ body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]),
179
+ body[data-dsh-bg-glass] [data-composer-card] [class*="_add"],
180
+ body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]) {
181
+ background-color: var(--dsw-specific-input-major);
182
+ 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));
183
+ -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);
184
+ 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
+ box-shadow:
186
+ inset 0 1px 0 rgba(255, 255, 255, 0.32),
187
+ inset 0 0 0 0.5px rgba(255, 255, 255, 0.12),
188
+ 0 4px 14px rgba(0, 0, 0, 0.08);
189
+ }
190
+ body[data-dsh-bg-glass] [class*="_newSession"]:not([class*="Label"]):hover,
191
+ body[data-dsh-bg-glass] [data-composer-card] [class*="_add"]:hover:not(:disabled),
192
+ body[data-dsh-bg-glass] [class*="_toBottom"]:not([class*="Slot"]):hover {
193
+ background-color: color-mix(in srgb, rgb(255 255 255) calc(var(--bg-glass-alpha-strong, 0.8) * 100%), transparent);
194
+ }
195
+ /* Collapsed rail renders the new-session control as a bare icon — keep the
196
+ official transparent paint (and its official translucent hover). */
197
+ body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"],
198
+ body[data-dsh-bg-glass] [class*="_collapsed"] [class*="_newSession"]:hover {
199
+ background-color: transparent;
200
+ background-image: none;
201
+ box-shadow: none;
202
+ }
203
+
204
+ /* ---- Subagent lineage popover (title-bar expanded list) --------------
205
+ Its official fill rides the menu token shared with every dropdown; glass
206
+ THIS popover explicitly via its role="tree" + _menu combination so real
207
+ menus stay officially opaque and legible. Same unified recipe: the
208
+ composer fill token + the full shared filter chain (the official
209
+ elevation shadow stays). */
210
+ body[data-dsh-bg-glass] [role="tree"][class*="_menu"] {
211
+ background-color: var(--dsw-specific-input-major);
212
+ 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));
213
+ -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);
214
+ 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
+ }
216
+
217
+ /* ---- Home hero "preview" badge (top-right superscript pill) ----------
218
+ Officially the state-business-tertiary pastel; re-emit the same hue as
219
+ glass but with the UNIFIED alpha curve (color-mix against the painter's
220
+ --bg-glass-alpha) and the shared blur chain, so it sits on the wallpaper
221
+ with exactly the glass strength of the composer card. */
222
+ body[data-dsh-bg-glass] [class*="_previewBadge"] {
223
+ background-color: color-mix(in srgb, rgb(228 237 253) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
224
+ -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);
225
+ 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
+ }
227
+ body[data-ds-dark-theme][data-dsh-bg-glass] [class*="_previewBadge"] {
228
+ background-color: color-mix(in srgb, rgb(52 65 91) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
77
229
  }
78
230
  `;
79
231
  /**
@@ -117,22 +269,88 @@ window.__ModuleLoader__.load({
117
269
  const LAYER_CLASS = "dsh-bg-layer";
118
270
  const IMAGE_CLASS = "dsh-bg-image";
119
271
  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"];
272
+ /**
273
+ * White-glass tokens — the WHITELISTED surfaces only. Everything that floats
274
+ * as a small card over the wallpaper: the composer input stack, message
275
+ * bubbles, the markdown code surfaces (blocks + banner + inline code), and
276
+ * the agent task strip family (`--dsw-specific-tip`: TodoPanel + GoalBar +
277
+ * QueueDock share one dock column and one fill token). Menus, dialogs, the
278
+ * settings UI, tooltips, toasts, hover fills and every accent keep the
279
+ * OFFICIAL token values — they are reading surfaces and must stay legible.
280
+ * `factor` scales the alpha relative to the input surface.
281
+ */
282
+ const GLASS_SURFACE_TOKENS = [
283
+ {
284
+ token: "--dsw-specific-input-major",
285
+ factor: 1
286
+ },
287
+ {
288
+ token: "--dsw-specific-bubble",
289
+ factor: .8
290
+ },
291
+ {
292
+ token: "--dsw-alias-markdown-code-block",
293
+ factor: 1
294
+ },
295
+ {
296
+ token: "--dsw-alias-markdown-code-block-banner",
297
+ factor: 1
298
+ },
299
+ {
300
+ token: "--dsw-alias-markdown-inline-code",
301
+ factor: .9
302
+ },
303
+ {
304
+ token: "--dsw-specific-tip",
305
+ factor: 1
306
+ }
307
+ ];
308
+ /** Every token the glass owns (save/restore key set). */
309
+ const GLASS_TOKENS = GLASS_SURFACE_TOKENS.map(({ token }) => token);
122
310
  /** Fraction of the official white surface alpha kept at the most transparent panel setting. */
123
311
  const GLASS_MIN_ALPHA = .05;
312
+ /** Base white-glass alpha for one panelOpacity (the curve every surface shares). */
313
+ function glassAlpha(panelOpacity) {
314
+ const alpha = GLASS_MIN_ALPHA + panelOpacity * .85;
315
+ return Math.max(0, Math.min(.9, alpha));
316
+ }
317
+ /**
318
+ * Saturation boost that rides the blur radius. Gentler than the reference
319
+ * engine (1.1 + blur * 0.02, capped at 1.6): stacking a hot saturate on top of
320
+ * the brightness/sheen layers pushed bright wallpapers into clipping.
321
+ */
322
+ function glassSaturate(blur) {
323
+ if (blur <= 0) return 1;
324
+ return Math.round(Math.min(1.6, 1.1 + blur * .02) * 1e3) / 1e3;
325
+ }
326
+ /**
327
+ * Exposure calibration per scheme. Light glass neither boosts brightness nor
328
+ * stacks a heavy sheen (mainstream frosted recipes — macOS vibrancy, Windows
329
+ * acrylic, common web glass — blur + saturate + tint without a positive
330
+ * brightness gain); dark glass keeps the reference engine's slight lift, which
331
+ * stops the white-glass fills from turning muddy.
332
+ */
333
+ function glassExposure(inDark) {
334
+ return inDark ? {
335
+ brightness: "1.04",
336
+ sheen: "0.16",
337
+ sheenMid: "0.05"
338
+ } : {
339
+ brightness: "0.98",
340
+ sheen: "0.07",
341
+ sheenMid: "0.02"
342
+ };
343
+ }
124
344
  /**
125
345
  * The theme-aware glass surface alphas for one panelOpacity: the input-surface
126
346
  * token and the bubble token share one curve, dimmed per scheme. Shared by the
127
347
  * live painter and the settings-row preview card so the two cannot drift.
128
348
  */
129
349
  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;
350
+ const alpha = glassAlpha(panelOpacity) * (inDark ? .4 : .8);
133
351
  return {
134
- major: `rgba(255, 255, 255, ${(clamped * factor).toFixed(3)})`,
135
- bubble: `rgba(255, 255, 255, ${(clamped * factor * .8).toFixed(3)})`
352
+ major: `rgba(255, 255, 255, ${alpha.toFixed(3)})`,
353
+ bubble: `rgba(255, 255, 255, ${(alpha * .8).toFixed(3)})`
136
354
  };
137
355
  }
138
356
  /**
@@ -160,7 +378,7 @@ window.__ModuleLoader__.load({
160
378
  rememberOnce(prop) {
161
379
  if (this.savedTokens.has(prop) || this.savedVars.has(prop)) return;
162
380
  const value = document.body.style.getPropertyValue(prop);
163
- if (SURFACE_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
381
+ if (GLASS_TOKENS.includes(prop)) this.savedTokens.set(prop, value);
164
382
  else this.savedVars.set(prop, value);
165
383
  }
166
384
  /**
@@ -214,7 +432,7 @@ window.__ModuleLoader__.load({
214
432
  this.setVar("--bg-wallpaper-scale", (1 + settings.wallpaperBlur * .006).toFixed(4));
215
433
  this.setVar("--bg-scrim", String(settings.scrim));
216
434
  this.setVar("--bg-glass-blur", `${settings.blur}px`);
217
- this.setVar("--bg-glass-saturate", String(1.15 + settings.blur * .03));
435
+ this.setVar("--bg-glass-saturate", String(glassSaturate(settings.blur)));
218
436
  this.applySurfaceGlass(settings, false);
219
437
  if (!this.observer) {
220
438
  this.observer = new MutationObserver(() => this.applySurfaceGlass(this.settings, false));
@@ -247,7 +465,7 @@ window.__ModuleLoader__.load({
247
465
  break;
248
466
  case "blur":
249
467
  this.setVar("--bg-glass-blur", `${value}px`);
250
- this.setVar("--bg-glass-saturate", String(1.15 + Number(value) * .03));
468
+ this.setVar("--bg-glass-saturate", String(glassSaturate(Number(value))));
251
469
  break;
252
470
  case "wallpaperBlur":
253
471
  this.setVar("--bg-wallpaper-blur", `${value}px`);
@@ -266,9 +484,9 @@ window.__ModuleLoader__.load({
266
484
  applySurfaceGlass(settings, forceRestore) {
267
485
  const s = document.body.style;
268
486
  const inDark = document.body.dataset.dsDarkTheme !== void 0;
269
- for (const token of SURFACE_TOKENS) this.rememberOnce(token);
487
+ for (const token of GLASS_TOKENS) this.rememberOnce(token);
270
488
  const restore = () => {
271
- for (const token of SURFACE_TOKENS) {
489
+ for (const token of GLASS_TOKENS) {
272
490
  const original = this.savedTokens.get(token);
273
491
  if (original !== void 0 && original !== "") s.setProperty(token, original);
274
492
  else s.removeProperty(token);
@@ -276,12 +494,22 @@ window.__ModuleLoader__.load({
276
494
  };
277
495
  if (forceRestore || settings === void 0 || settings.panelOpacity >= 1) {
278
496
  restore();
497
+ document.body.removeAttribute(GLASS_ATTR);
279
498
  this.setVar("--bg-glass-blur", "0px");
499
+ this.setVar("--bg-glass-brightness", "1");
500
+ s.removeProperty("--bg-glass-alpha");
501
+ s.removeProperty("--bg-glass-alpha-strong");
280
502
  return;
281
503
  }
282
- const { major, bubble } = glassSurfaceAlphas(settings.panelOpacity, inDark);
283
- s.setProperty("--dsw-specific-input-major", major);
284
- s.setProperty("--dsw-specific-bubble", bubble);
504
+ document.body.setAttribute(GLASS_ATTR, "on");
505
+ const exposure = glassExposure(inDark);
506
+ this.setVar("--bg-glass-brightness", exposure.brightness);
507
+ this.setVar("--bg-glass-sheen", exposure.sheen);
508
+ this.setVar("--bg-glass-sheen-mid", exposure.sheenMid);
509
+ const base = glassAlpha(settings.panelOpacity) * (inDark ? .4 : .8);
510
+ for (const { token, factor } of GLASS_SURFACE_TOKENS) s.setProperty(token, `rgba(255, 255, 255, ${(base * factor).toFixed(3)})`);
511
+ this.setVar("--bg-glass-alpha", base.toFixed(3));
512
+ this.setVar("--bg-glass-alpha-strong", Math.min(.9, base * 1.5).toFixed(3));
285
513
  }
286
514
  /** Remove the wallpaper + scrim layers and clear the active attribute. */
287
515
  removeLayers() {
@@ -302,6 +530,7 @@ window.__ModuleLoader__.load({
302
530
  this.removeLayers();
303
531
  const s = document.body.style;
304
532
  document.body.removeAttribute(ACTIVE_ATTR);
533
+ document.body.removeAttribute(GLASS_ATTR);
305
534
  for (const [prop, value] of this.savedTokens) s.setProperty(prop, value);
306
535
  for (const [prop, value] of this.savedVars) s.setProperty(prop, value);
307
536
  this.savedTokens.clear();
@@ -343,6 +572,7 @@ window.__ModuleLoader__.load({
343
572
  "--bg-wallpaper-scale",
344
573
  "--bg-glass-blur",
345
574
  "--bg-glass-saturate",
575
+ "--bg-glass-brightness",
346
576
  "--bg-preview-glass"
347
577
  ];
348
578
  /**
@@ -362,11 +592,13 @@ window.__ModuleLoader__.load({
362
592
  const inDark = document.body.dataset.dsDarkTheme !== void 0;
363
593
  if (settings.panelOpacity >= 1) {
364
594
  s.setProperty("--bg-glass-blur", "0px");
595
+ s.setProperty("--bg-glass-brightness", "1");
365
596
  s.setProperty("--bg-preview-glass", "var(--dsw-alias-bg-layer-1)");
366
597
  return;
367
598
  }
368
599
  s.setProperty("--bg-glass-blur", `${settings.blur}px`);
369
- s.setProperty("--bg-glass-saturate", String(1.15 + settings.blur * .03));
600
+ s.setProperty("--bg-glass-saturate", String(glassSaturate(settings.blur)));
601
+ s.setProperty("--bg-glass-brightness", glassExposure(inDark).brightness);
370
602
  s.setProperty("--bg-preview-glass", glassSurfaceAlphas(settings.panelOpacity, inDark).bubble);
371
603
  }
372
604
  /** Remove every preview variable from an element (restores CSS defaults). */
@@ -400,7 +632,16 @@ window.__ModuleLoader__.load({
400
632
  "background.contain": "完整",
401
633
  "background.clear": "清除背景",
402
634
  "background.uploadFailed": "上传失败,请重试",
403
- "background.saveFailed": "保存失败,请重试"
635
+ "background.saveFailed": "保存失败,请重试",
636
+ "background.timeline": "会话时间线",
637
+ "background.timelineHint": "右侧消息导航轨,悬停展开问题列表(DeepSeek 官网风格)",
638
+ "timeline.railLabel": "对话时间线",
639
+ "timeline.roleUser": "用户",
640
+ "timeline.noText": "(无文本内容)",
641
+ "timeline.marksOnly": "★ 只看标记",
642
+ "timeline.mark": "标记为重点",
643
+ "timeline.unmark": "取消重点标记",
644
+ "timeline.noMarks": "还没有标记的重点"
404
645
  };
405
646
  /** English copy. */
406
647
  const en = {
@@ -427,7 +668,16 @@ window.__ModuleLoader__.load({
427
668
  "background.contain": "Contain",
428
669
  "background.clear": "Clear background",
429
670
  "background.uploadFailed": "Upload failed, try again",
430
- "background.saveFailed": "Save failed, try again"
671
+ "background.saveFailed": "Save failed, try again",
672
+ "background.timeline": "Conversation timeline",
673
+ "background.timelineHint": "Message rail at the right edge; hover to expand the question list (DeepSeek web style)",
674
+ "timeline.railLabel": "Chat timeline",
675
+ "timeline.roleUser": "User",
676
+ "timeline.noText": "(no text)",
677
+ "timeline.marksOnly": "★ Marked only",
678
+ "timeline.mark": "Mark as key point",
679
+ "timeline.unmark": "Unmark key point",
680
+ "timeline.noMarks": "No marked key points"
431
681
  };
432
682
  //#endregion
433
683
  //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -549,7 +799,7 @@ window.__ModuleLoader__.load({
549
799
  const settingsClient = new SettingsClient();
550
800
  //#endregion
551
801
  //#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}}";
802
+ 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:#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}.-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
803
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
554
804
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
555
805
  const tag = document.createElement("style");
@@ -559,39 +809,41 @@ window.__ModuleLoader__.load({
559
809
  document.head.appendChild(tag);
560
810
  }
561
811
  var SettingsRow_module_css_default = {
812
+ "sourceRow": "-ozqdq_sourceRow",
813
+ "previewScrim": "-ozqdq_previewScrim",
814
+ "segBtn": "-ozqdq_segBtn",
815
+ "header": "-ozqdq_header",
816
+ "clearRow": "-ozqdq_clearRow",
817
+ "switchKnob": "-ozqdq_switchKnob",
818
+ "switch": "-ozqdq_switch",
819
+ "segGroup": "-ozqdq_segGroup",
562
820
  "segActive": "-ozqdq_segActive",
563
- "error": "-ozqdq_error",
564
- "sliderLabel": "-ozqdq_sliderLabel",
821
+ "segRow": "-ozqdq_segRow",
822
+ "btn": "-ozqdq_btn",
565
823
  "previewEmpty": "-ozqdq_previewEmpty",
824
+ "previewGlass": "-ozqdq_previewGlass",
566
825
  "row": "-ozqdq_row",
567
- "urlRow": "-ozqdq_urlRow",
568
- "slider": "-ozqdq_slider",
569
- "sliderValue": "-ozqdq_sliderValue",
570
- "clearRow": "-ozqdq_clearRow",
571
- "preview": "-ozqdq_preview",
572
- "clearBtn": "-ozqdq_clearBtn",
573
- "btn": "-ozqdq_btn",
574
- "segLabel": "-ozqdq_segLabel",
575
- "previewGlassText": "-ozqdq_previewGlassText",
576
- "btnPrimary": "-ozqdq_btnPrimary",
577
- "header": "-ozqdq_header",
578
- "title": "-ozqdq_title",
579
- "previewScrim": "-ozqdq_previewScrim",
580
- "sectionLabel": "-ozqdq_sectionLabel",
581
- "enabledBadge": "-ozqdq_enabledBadge",
582
826
  "titleRow": "-ozqdq_titleRow",
583
- "previewGlass": "-ozqdq_previewGlass",
827
+ "btnDanger": "-ozqdq_btnDanger",
584
828
  "desc": "-ozqdq_desc",
585
829
  "sliderRow": "-ozqdq_sliderRow",
586
- "previewImg": "-ozqdq_previewImg",
830
+ "segLabel": "-ozqdq_segLabel",
831
+ "title": "-ozqdq_title",
832
+ "urlRow": "-ozqdq_urlRow",
833
+ "enabledBadge": "-ozqdq_enabledBadge",
834
+ "sliderLabel": "-ozqdq_sliderLabel",
587
835
  "hint": "-ozqdq_hint",
588
- "segBtn": "-ozqdq_segBtn",
589
- "segGroup": "-ozqdq_segGroup",
836
+ "slider": "-ozqdq_slider",
590
837
  "urlInput": "-ozqdq_urlInput",
591
- "btnDanger": "-ozqdq_btnDanger",
592
- "segRow": "-ozqdq_segRow",
593
- "sourceRow": "-ozqdq_sourceRow",
594
- "section": "-ozqdq_section"
838
+ "btnPrimary": "-ozqdq_btnPrimary",
839
+ "clearBtn": "-ozqdq_clearBtn",
840
+ "error": "-ozqdq_error",
841
+ "previewImg": "-ozqdq_previewImg",
842
+ "sectionLabel": "-ozqdq_sectionLabel",
843
+ "section": "-ozqdq_section",
844
+ "preview": "-ozqdq_preview",
845
+ "sliderValue": "-ozqdq_sliderValue",
846
+ "previewGlassText": "-ozqdq_previewGlassText"
595
847
  };
596
848
  //#endregion
597
849
  //#region src/client/SettingsRow.tsx
@@ -623,7 +875,8 @@ window.__ModuleLoader__.load({
623
875
  panelOpacity: DEFAULT_PANEL_OPACITY,
624
876
  blur: 16,
625
877
  wallpaperBlur: 0,
626
- fit: DEFAULT_FIT
878
+ fit: DEFAULT_FIT,
879
+ timeline: true
627
880
  };
628
881
  /** Per-field slider geometry: 5% steps for ratios, fine px steps for radii so
629
882
  * the two blur sliders feel smooth near small values. Bounds come from the
@@ -671,7 +924,7 @@ window.__ModuleLoader__.load({
671
924
  }
672
925
  /** Field-wise equality for distinguishing our own saves from external changes. */
673
926
  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;
927
+ 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
928
  }
676
929
  /**
677
930
  * A stepped range control. While dragging it re-renders only itself: the value
@@ -909,6 +1162,16 @@ window.__ModuleLoader__.load({
909
1162
  paintLive(next);
910
1163
  saveNow(next);
911
1164
  };
1165
+ const handleTimelineToggle = () => {
1166
+ markEditing();
1167
+ const next = {
1168
+ ...draftRef.current,
1169
+ timeline: !draftRef.current.timeline
1170
+ };
1171
+ draftRef.current = next;
1172
+ setDraft(next);
1173
+ saveNow(next);
1174
+ };
912
1175
  const sourceUrl = draft.uploadId ? `${BACKGROUND_API_PREFIX}/image/${draft.uploadId}` : draft.url;
913
1176
  const hasImage = sourceUrl !== "";
914
1177
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -1071,6 +1334,29 @@ window.__ModuleLoader__.load({
1071
1334
  children: t(mode === "cover" ? "background.cover" : "background.contain")
1072
1335
  }, mode))
1073
1336
  })]
1337
+ }),
1338
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1339
+ className: SettingsRow_module_css_default.segRow,
1340
+ children: [
1341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1342
+ className: SettingsRow_module_css_default.segLabel,
1343
+ children: t("background.timeline")
1344
+ }),
1345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1346
+ type: "button",
1347
+ role: "switch",
1348
+ "aria-checked": draft.timeline,
1349
+ "aria-label": t("background.timeline"),
1350
+ title: t("background.timelineHint"),
1351
+ className: SettingsRow_module_css_default.switch,
1352
+ onClick: handleTimelineToggle,
1353
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsRow_module_css_default.switchKnob })
1354
+ }),
1355
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1356
+ className: SettingsRow_module_css_default.hint,
1357
+ children: t("background.timelineHint")
1358
+ })
1359
+ ]
1074
1360
  })
1075
1361
  ]
1076
1362
  }),
@@ -1092,17 +1378,862 @@ window.__ModuleLoader__.load({
1092
1378
  });
1093
1379
  }
1094
1380
  //#endregion
1381
+ //#region src/client/timeline-css.ts
1382
+ /**
1383
+ * Timeline CSS — the conversation scroll-navigation rail (DeepSeek web
1384
+ * ScrollNav structure, rebuilt on this plugin's frosted-glass system).
1385
+ *
1386
+ * Structure and metrics mirror the official chat.deepseek.com stylesheet
1387
+ * (module map extracted from the shipped bundle; v0.1.4 of the reference port
1388
+ * dsh-chat-timeline widens the panel to 260px and adds bookmarks):
1389
+ * nav 34px-wide rail, fixed right edge, vertically centered; collapsed
1390
+ * blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
1391
+ * whose scroll area holds 30px rows (tick indicator + fading-in title);
1392
+ * 32px gradient fade veils top/bottom shown while that side clips.
1393
+ *
1394
+ * Glass integration: the official chat.deepseek.com paints (light
1395
+ * rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
1396
+ * 5px collapsed / 16px expanded blur) remain as the NO-GLASS fallback.
1397
+ * While the plugin's glass system is on (`body[data-dsh-bg-glass]`), the
1398
+ * rail joins the SAME unified recipe as the composer card and bubbles:
1399
+ * fill = the painter's `--dsw-specific-input-major` token, blur/saturate/
1400
+ * brightness = the shared `--bg-glass-*` variables, i.e. the glass-blur
1401
+ * slider. Class prefix `dsbt-` deliberately avoids every
1402
+ * `[class*="_xxx"]` anchor of the plugin's glass sheet (underscores only).
1403
+ */
1404
+ /** Unique id stamped on the injected style element (dedup key). */
1405
+ const TIMELINE_CSS_TAG = "deepseek-harness-background/timeline";
1406
+ /** Inject the timeline stylesheet once. Idempotent (dedup key on the tag). */
1407
+ function injectTimelineCss() {
1408
+ if (typeof document === "undefined") return;
1409
+ if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
1410
+ const tag = document.createElement("style");
1411
+ tag.dataset.plugin = "deepseek-harness-background";
1412
+ tag.dataset.pluginCss = TIMELINE_CSS_TAG;
1413
+ tag.textContent = TIMELINE_CSS;
1414
+ document.head.appendChild(tag);
1415
+ }
1416
+ const TIMELINE_CSS = `
1417
+ .dsbt-nav {
1418
+ user-select: none; -webkit-user-select: none;
1419
+ z-index: 90; align-items: center; width: 34px;
1420
+ height: var(--dsbt-h, 300px);
1421
+ transition: right .2s ease, opacity .2s ease;
1422
+ display: flex; position: fixed; top: 50%; right: 16px;
1423
+ transform: translateY(-50%); pointer-events: auto;
1424
+ }
1425
+ @media (max-width: 767px) { .dsbt-nav { display: none; } }
1426
+
1427
+ /* Collapsed capsule — frosted strip behind the tick marks. Official paint:
1428
+ light rgba(255,255,255,.8), dark rgba(21,21,23,.6), 5px blur. */
1429
+ .dsbt-bg {
1430
+ background-color: rgba(255, 255, 255, .8);
1431
+ -webkit-backdrop-filter: blur(5px);
1432
+ backdrop-filter: blur(5px);
1433
+ box-shadow:
1434
+ inset 0 1px 0 rgba(255, 255, 255, .28),
1435
+ inset 0 0 0 .5px rgba(255, 255, 255, .1),
1436
+ 0 6px 18px rgba(0, 0, 0, .07);
1437
+ border-radius: 16px; width: 34px;
1438
+ height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
1439
+ position: absolute; top: 50%; right: 0; transform: translateY(-50%);
1440
+ transition: opacity .2s ease; pointer-events: none;
1441
+ }
1442
+ body[data-ds-dark-theme] .dsbt-bg {
1443
+ background-color: rgba(21, 21, 23, .6);
1444
+ box-shadow:
1445
+ inset 0 1px 0 rgba(255, 255, 255, .09),
1446
+ inset 0 0 0 .5px rgba(255, 255, 255, .05),
1447
+ 0 6px 18px rgba(0, 0, 0, .3);
1448
+ }
1449
+ .dsbt-bg.dsbt-bg-hide { opacity: 0; }
1450
+
1451
+ /* Expanding panel — exactly the capsule's box (identical height in both
1452
+ states), width animates 34px -> measured fit (max 260px). Official
1453
+ hover-state paint: white/black frosted glass at ~94% alpha, 16px blur. */
1454
+ .dsbt-wrap {
1455
+ width: 34px;
1456
+ height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
1457
+ border: 1px solid transparent; border-radius: 16px;
1458
+ flex-direction: column; align-items: stretch;
1459
+ transition: width .22s cubic-bezier(.4, 0, .2, 1), background-color .2s ease,
1460
+ box-shadow .2s ease, border-color .2s ease;
1461
+ display: flex; position: absolute; top: 50%; right: 0;
1462
+ transform: translateY(-50%);
1463
+ overflow: hidden; box-sizing: border-box; background: transparent;
1464
+ pointer-events: none;
1465
+ }
1466
+ .dsbt-wrap.dsbt-show {
1467
+ pointer-events: auto;
1468
+ width: min(var(--dsbt-w, 260px), 260px);
1469
+ background-color: rgba(255, 255, 255, .94);
1470
+ -webkit-backdrop-filter: blur(16px);
1471
+ backdrop-filter: blur(16px);
1472
+ border-color: rgba(0, 0, 0, .06);
1473
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .08), 0 2px 8px rgba(0, 0, 0, .04);
1474
+ }
1475
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show {
1476
+ background-color: rgba(28, 28, 32, .95);
1477
+ border-color: rgba(255, 255, 255, .08);
1478
+ box-shadow: 0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .25);
1479
+ }
1480
+
1481
+ /* Content fade veils — the official ::before/::after pair rebuilt as
1482
+ elements: a bar of the panel's OWN fill color masked to nothing over
1483
+ 32px (solid -> mask == gradient-to-transparent, but works with the
1484
+ translucent fill). Shown only while that side actually clips. */
1485
+ .dsbt-fade {
1486
+ z-index: 2; pointer-events: none; opacity: 0;
1487
+ background-color: rgba(255, 255, 255, .94);
1488
+ -webkit-mask-image: linear-gradient(#000 20.19%, transparent 100%);
1489
+ mask-image: linear-gradient(#000 20.19%, transparent 100%);
1490
+ width: 100%; height: 32px; transition: opacity .2s ease;
1491
+ position: absolute; left: 0; top: 0;
1492
+ }
1493
+ body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
1494
+ .dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
1495
+ .dsbt-fade.dsbt-fade-on { opacity: 1; }
1496
+
1497
+ /* Filter bar — "marked only" toggle pinned above the scroll area. */
1498
+ .dsbt-filterbar {
1499
+ padding: 8px 12px 4px; display: flex; justify-content: flex-end;
1500
+ align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
1501
+ box-sizing: border-box; flex: none; transition: opacity .2s ease;
1502
+ }
1503
+ .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar { opacity: 0; }
1504
+ body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
1505
+ .dsbt-filterbtn {
1506
+ font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
1507
+ border: 1px solid rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .03);
1508
+ color: rgba(0, 0, 0, .65); cursor: pointer; transition: all .15s ease;
1509
+ display: inline-flex; align-items: center; gap: 3px; font-family: inherit;
1510
+ }
1511
+ .dsbt-filterbtn:hover { background: rgba(0, 0, 0, .07); color: rgba(0, 0, 0, .9); }
1512
+ .dsbt-filterbtn.dsbt-filteron {
1513
+ background: rgba(245, 158, 11, .12); border-color: #f59e0b;
1514
+ color: #b45309; font-weight: 600;
1515
+ }
1516
+ body[data-ds-dark-theme] .dsbt-filterbtn {
1517
+ border-color: rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .05);
1518
+ color: rgba(255, 255, 255, .7);
1519
+ }
1520
+ body[data-ds-dark-theme] .dsbt-filterbtn:hover { background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .95); }
1521
+ body[data-ds-dark-theme] .dsbt-filterbtn.dsbt-filteron {
1522
+ background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
1523
+ }
1524
+
1525
+ /* Scroll area — rows hug the right edge; scrolls only when overfull. */
1526
+ .dsbt-page {
1527
+ padding: 15px 0 15px 24px; box-sizing: border-box;
1528
+ overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
1529
+ flex-direction: column; align-items: flex-end; display: flex;
1530
+ position: relative; width: 100%; overflow: hidden;
1531
+ }
1532
+ .dsbt-wrap.dsbt-show .dsbt-page {
1533
+ overflow-y: auto; overflow-x: hidden;
1534
+ scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
1535
+ }
1536
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-page { scrollbar-color: rgba(255, 255, 255, .25) transparent; }
1537
+ .dsbt-page::-webkit-scrollbar { width: 4px; }
1538
+ .dsbt-page::-webkit-scrollbar-track { background: transparent; }
1539
+ .dsbt-page::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, .15); border-radius: 4px; }
1540
+ body[data-ds-dark-theme] .dsbt-page::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .25); }
1541
+
1542
+ /* Empty state (filtered list with no marks). */
1543
+ .dsbt-empty {
1544
+ font-size: 12px; line-height: 18px; padding: 16px 12px;
1545
+ color: rgba(0, 0, 0, .45); text-align: center; width: 100%; box-sizing: border-box;
1546
+ }
1547
+ body[data-ds-dark-theme] .dsbt-empty { color: rgba(255, 255, 255, .45); }
1548
+
1549
+ /* One message row: 30px, star bookmark + tick indicator at the right,
1550
+ title fading in. */
1551
+ .dsbt-item {
1552
+ cursor: pointer; height: 30px; min-height: 30px; flex-shrink: 0;
1553
+ justify-content: flex-end; align-items: center;
1554
+ width: calc(100% - 4px); margin-right: 4px; line-height: 20px;
1555
+ display: flex; background: none; border: none; font: inherit;
1556
+ text-align: right; box-sizing: border-box; padding: 0;
1557
+ color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
1558
+ transition: color .15s ease;
1559
+ }
1560
+ .dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
1561
+ body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
1562
+ body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
1563
+ .dsbt-title {
1564
+ font-size: 13px; line-height: 20px; flex: 1 1 auto; min-width: 0;
1565
+ text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
1566
+ margin-right: 6px; text-align: right; color: inherit;
1567
+ opacity: 0; transition: opacity .12s ease, color .15s ease;
1568
+ }
1569
+ .dsbt-show .dsbt-title { opacity: 1; }
1570
+
1571
+ /* Star bookmark — appears on row hover while the panel is expanded; gold
1572
+ when marked. Keyboard-operable (span[role=button]). */
1573
+ .dsbt-star {
1574
+ font-size: 13px; line-height: 16px; width: 18px; height: 18px;
1575
+ display: inline-flex; align-items: center; justify-content: center;
1576
+ opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
1577
+ flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
1578
+ -webkit-user-select: none;
1579
+ }
1580
+ .dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
1581
+ .dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
1582
+ .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
1583
+ body[data-ds-dark-theme] .dsbt-star { color: rgba(255, 255, 255, .25); }
1584
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star:hover { color: #fde047; }
1585
+ body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #fbbf24; }
1586
+
1587
+ .dsbt-ind { flex-shrink: 0; justify-content: center; align-items: center; width: 16px; height: 20px; display: flex; }
1588
+ .dsbt-line {
1589
+ background-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, .16));
1590
+ border-radius: 4px; flex-shrink: 0; width: 8px; height: 2px;
1591
+ transition: background-color .2s ease, transform .2s ease, width .2s ease, height .2s ease;
1592
+ }
1593
+ .dsbt-item:hover .dsbt-line { background-color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .9)); }
1594
+ .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
1595
+ .dsbt-item.dsbt-active .dsbt-title { color: var(--dsw-alias-state-business-primary, #4d6bfe); font-weight: 500; }
1596
+ .dsbt-item.dsbt-active .dsbt-line {
1597
+ background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
1598
+ transform-origin: 50%; transform: scale(1.5);
1599
+ }
1600
+ body[data-ds-dark-theme] .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
1601
+ body[data-ds-dark-theme] .dsbt-line { background-color: rgba(255, 255, 255, .2); }
1602
+ body[data-ds-dark-theme] .dsbt-item:hover .dsbt-line { background-color: rgba(255, 255, 255, .9); }
1603
+
1604
+ /* Marked (key-point) rows: a wider golden tick in both themes; the active
1605
+ state keeps its brand-blue scale so reading position always wins. */
1606
+ .dsbt-item.dsbt-marked .dsbt-line { background-color: #d97706; width: 10px; height: 2.5px; }
1607
+ .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #b45309; }
1608
+ .dsbt-item.dsbt-marked.dsbt-active .dsbt-line {
1609
+ background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
1610
+ transform-origin: 50%; transform: scale(1.5);
1611
+ }
1612
+ body[data-ds-dark-theme] .dsbt-item.dsbt-marked .dsbt-line { background-color: #fbbf24; width: 10px; height: 2.5px; }
1613
+ body[data-ds-dark-theme] .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #fde047; }
1614
+
1615
+ /* ---- Unified glass integration -----------------------------------------
1616
+ Glass system on (wallpaper active + panelOpacity < 1): the rail joins
1617
+ the composer/bubble recipe — the same translucent fill token and the
1618
+ same blur/saturate/brightness chain (the glass-blur slider drives it,
1619
+ no fixed 5/16px). Placed after the dark-theme rules so equal-specificity
1620
+ overrides win in both schemes. */
1621
+ body[data-dsh-bg-glass] .dsbt-bg,
1622
+ body[data-dsh-bg-glass] .dsbt-wrap.dsbt-show {
1623
+ background-color: var(--dsw-specific-input-major);
1624
+ -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);
1625
+ backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
1626
+ }
1627
+ body[data-dsh-bg-glass] .dsbt-fade {
1628
+ background-color: var(--dsw-specific-input-major);
1629
+ }
1630
+
1631
+ @media (prefers-reduced-motion: reduce) {
1632
+ .dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
1633
+ }
1634
+ `;
1635
+ //#endregion
1636
+ //#region src/client/timeline.tsx
1637
+ /**
1638
+ * Conversation timeline rail — DeepSeek web ScrollNav rebuilt on this
1639
+ * plugin's frosted-glass system (styles: ./timeline-css.ts).
1640
+ *
1641
+ * Collapsed: a fixed 34px-wide frosted capsule at the right edge, vertically
1642
+ * centered, one tick per user message (active tick scaled + brand blue,
1643
+ * key-point bookmarks golden). Hovering expands the SAME box into a frosted
1644
+ * panel listing every user question (title fades in; >8 messages widen to
1645
+ * 260px, otherwise the width fits the longest title). Both states share one
1646
+ * computed outer height, so expanding never jumps, and 32px fade veils mask
1647
+ * the clipped edge on each side while that side actually scrolls.
1648
+ *
1649
+ * Enhancements ported from dsh-chat-timeline v0.1.4 (MIT):
1650
+ * - Key-point bookmarks: star a question in the expanded panel, persisted per
1651
+ * session in localStorage; marked rows show a golden tick in the capsule
1652
+ * and a "marked only" filter narrows the list.
1653
+ * - Jump stabilization: the reading-position tracker freezes while a smooth
1654
+ * jump animates (cleared 150ms after the last scroll event, plus an 800ms
1655
+ * fallback), so the panel cannot jitter mid-jump.
1656
+ * - Rewind integration: messages withdrawn by a rewind command node are
1657
+ * filtered out of the rail.
1658
+ *
1659
+ * Data source (client-side only): loaded chat nodes -> background loadOlder
1660
+ * loop until complete. Clicking a row loads older history on demand, scrolls
1661
+ * the conversation to the target row, and keeps the rail's own active row
1662
+ * visible.
1663
+ *
1664
+ * The rail reads the shared settings transport: it renders unless
1665
+ * `timeline: false`. Its glass paints are self-contained official values
1666
+ * (see timeline-css.ts), independent of the plugin's token overrides.
1667
+ */
1668
+ if (typeof document !== "undefined") injectTimelineCss();
1669
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1670
+ /** Extract preview text from a user message's ContentBlock list. */
1671
+ function userTextOf(content) {
1672
+ if (!Array.isArray(content)) return "";
1673
+ let out = "";
1674
+ for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
1675
+ const text = block.text;
1676
+ if (typeof text === "string") out += text;
1677
+ }
1678
+ return out.trim().slice(0, 80);
1679
+ }
1680
+ /**
1681
+ * Stable bookmark key for one entry: the durable message key, falling back to
1682
+ * the anchor seq — so marks survive history reloads. Entries with neither are
1683
+ * not markable (empty key).
1684
+ * @param m - the entry (or any shaped object).
1685
+ */
1686
+ function markKeyOf(m) {
1687
+ if (m === null || typeof m !== "object") return "";
1688
+ const rec = m;
1689
+ if (typeof rec.key === "string" && rec.key !== "") return `key:${rec.key}`;
1690
+ return typeof rec.seq === "number" ? `seq:${rec.seq}` : "";
1691
+ }
1692
+ /** localStorage key prefix for the per-session marked-key lists. */
1693
+ const MARKS_STORAGE_PREFIX = "dsbt_marks_";
1694
+ /**
1695
+ * Read the marked keys for one session from localStorage.
1696
+ * @param sessionId - the session the marks belong to.
1697
+ */
1698
+ function readMarks(sessionId) {
1699
+ if (typeof window === "undefined" || !sessionId) return [];
1700
+ try {
1701
+ const raw = window.localStorage.getItem(MARKS_STORAGE_PREFIX + sessionId);
1702
+ if (!raw) return [];
1703
+ const parsed = JSON.parse(raw);
1704
+ return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
1705
+ } catch {
1706
+ return [];
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Persist the marked keys for one session (removes the key when empty).
1711
+ * @param sessionId - the session the marks belong to.
1712
+ * @param marks - the marked-key list.
1713
+ */
1714
+ function writeMarks(sessionId, marks) {
1715
+ if (typeof window === "undefined" || !sessionId) return;
1716
+ try {
1717
+ if (!Array.isArray(marks) || marks.length === 0) window.localStorage.removeItem(MARKS_STORAGE_PREFIX + sessionId);
1718
+ else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
1719
+ } catch {}
1720
+ }
1721
+ /**
1722
+ * Extract the rewind target seq from a rewind outcome text.
1723
+ * @param text - the command outcome text.
1724
+ */
1725
+ function rewindTargetOfOutcome(text) {
1726
+ if (typeof text !== "string") return void 0;
1727
+ const digits = (/rewound.*?(?:target\s+)?(\d+)/i.exec(text) ?? /#(\d+)/.exec(text))?.[1];
1728
+ if (digits === void 0) return void 0;
1729
+ const n = Number.parseInt(digits, 10);
1730
+ return Number.isFinite(n) ? n : void 0;
1731
+ }
1732
+ /**
1733
+ * Is this rewind command a preview/dry-run (marks the target hidden without
1734
+ * an outcome)?
1735
+ * @param command - the command payload.
1736
+ */
1737
+ function isRewindPreviewCommand(command) {
1738
+ if (command.args === null || typeof command.args !== "object") return false;
1739
+ const args = command.args;
1740
+ return args.preview === true || args.dryRun === true;
1741
+ }
1742
+ /**
1743
+ * Collect the seq numbers a rewind hid: preview commands hide their target
1744
+ * directly; successful commands hide their own row plus every node anchored
1745
+ * inside the rewound span [target, sourceEventSeq].
1746
+ * @param chat - a chat snapshot (loosely typed on purpose).
1747
+ */
1748
+ function hiddenSeqsOfChat(chat) {
1749
+ const hidden = /* @__PURE__ */ new Set();
1750
+ if (chat === null || typeof chat !== "object") return hidden;
1751
+ const nodes = chat.nodes;
1752
+ if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1753
+ const all = [...nodes.values()];
1754
+ const spans = [];
1755
+ for (const node of all) {
1756
+ if (node === null || typeof node !== "object") continue;
1757
+ if (node.kind !== "command") continue;
1758
+ const command = node.data;
1759
+ if (command === null || typeof command !== "object") continue;
1760
+ if (command.name !== "rewind") continue;
1761
+ if (isRewindPreviewCommand(command)) {
1762
+ if (typeof command.seq === "number") hidden.add(command.seq);
1763
+ continue;
1764
+ }
1765
+ const outcome = command.outcome;
1766
+ if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1767
+ const marker = outcome.sourceEventSeq;
1768
+ if (typeof marker !== "number") continue;
1769
+ if (typeof command.seq === "number") hidden.add(command.seq);
1770
+ const target = rewindTargetOfOutcome(outcome.text);
1771
+ if (target !== void 0) spans.push({
1772
+ start: target,
1773
+ end: marker
1774
+ });
1775
+ }
1776
+ if (spans.length === 0) return hidden;
1777
+ for (const node of all) {
1778
+ if (node === null || typeof node !== "object") continue;
1779
+ const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
1780
+ if (anchor === void 0) continue;
1781
+ if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
1782
+ }
1783
+ return hidden;
1784
+ }
1785
+ /**
1786
+ * Enumerate user messages from the loaded chat-node snapshot (the client-side
1787
+ * collector; sorted by anchor sequence, rewind-hidden entries dropped).
1788
+ * @param snapshot - a session snapshot (loosely typed on purpose).
1789
+ */
1790
+ function collectMessages(snapshot) {
1791
+ const chat = snapshot?.chat;
1792
+ const nodes = chat?.nodes;
1793
+ if (nodes === void 0 || typeof nodes.values !== "function") return [];
1794
+ const hidden = hiddenSeqsOfChat(chat);
1795
+ const out = [];
1796
+ for (const node of nodes.values()) {
1797
+ if (node === null || typeof node !== "object") continue;
1798
+ if (node.kind !== "user") continue;
1799
+ const data = node.data;
1800
+ if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
1801
+ const key = typeof node.key === "string" ? node.key : void 0;
1802
+ if (key === void 0) continue;
1803
+ const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
1804
+ if (hidden.has(seq)) continue;
1805
+ out.push({
1806
+ seq,
1807
+ time: data.time,
1808
+ text: userTextOf(data.content),
1809
+ key
1810
+ });
1811
+ }
1812
+ out.sort((a, b) => a.seq - b.seq);
1813
+ return out;
1814
+ }
1815
+ /** Resolve the DOM anchor key of one entry (direct key form only). */
1816
+ function resolveAnchorKey(m) {
1817
+ return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
1818
+ }
1819
+ /**
1820
+ * Outer rail height for a message count: rows + breathing room, clamped so
1821
+ * both states always share the exact same height (never jumps on expand).
1822
+ * @param count - number of user messages.
1823
+ */
1824
+ function railHeightFor(count) {
1825
+ return Math.max(140, Math.min(300, count * 30 + 38));
1826
+ }
1827
+ /** Longest-title width estimate bounds. */
1828
+ const RAIL_MIN_WIDTH = 96;
1829
+ const RAIL_MAX_WIDTH = 260;
1830
+ /**
1831
+ * Measure the expanded-panel width for the given titles (canvas text metrics;
1832
+ * falls back to the max width when measurement is unavailable). Accounts for
1833
+ * the star column so a marked row never clips its tick.
1834
+ * @param texts - the message previews.
1835
+ */
1836
+ function railWidthFor(texts) {
1837
+ if (texts.length > 8) return RAIL_MAX_WIDTH;
1838
+ let widest = 0;
1839
+ if (typeof document !== "undefined") {
1840
+ const ctx = document.createElement("canvas").getContext("2d");
1841
+ if (ctx) {
1842
+ ctx.font = "13px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
1843
+ for (const t of texts) {
1844
+ const w = ctx.measureText(t === "" ? "…" : t).width;
1845
+ if (w > widest) widest = w;
1846
+ }
1847
+ }
1848
+ }
1849
+ return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
1850
+ }
1851
+ /** Ensure the target node is loaded, then scroll its row into view. */
1852
+ async function jumpToMessage(sessionsService, sessionId, key) {
1853
+ const session = sessionsService.binding(sessionId)?.session;
1854
+ if (session === void 0) return false;
1855
+ let guard = 0;
1856
+ while (guard++ < 120) {
1857
+ const snapshot = session.getSnapshot();
1858
+ if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
1859
+ if (snapshot?.hasMore !== true) return false;
1860
+ if (snapshot.loadingOlder === true) {
1861
+ await delay(50);
1862
+ continue;
1863
+ }
1864
+ await session.loadOlder();
1865
+ }
1866
+ const row = document.querySelector("[data-conversation-scroll]")?.querySelector(`[data-chat-anchor-key="${CSS.escape(key)}"]`) ?? null;
1867
+ if (row === null) return false;
1868
+ const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1869
+ row.scrollIntoView({
1870
+ behavior: reducedMotion ? "auto" : "smooth",
1871
+ block: "center"
1872
+ });
1873
+ return true;
1874
+ }
1875
+ /**
1876
+ * Scroll only the rail itself so the active row stays visible (min distance).
1877
+ * offsetTop math instead of getBoundingClientRect: stable while the panel's
1878
+ * own smooth scroll animates (no feedback loop with live rects).
1879
+ */
1880
+ function scrollTimelineItemIntoView(page, item) {
1881
+ const pageTop = page.scrollTop;
1882
+ const pageBottom = pageTop + page.clientHeight;
1883
+ const itemTop = item.offsetTop;
1884
+ const itemBottom = itemTop + item.offsetHeight;
1885
+ if (itemTop < pageTop) page.scrollTop = Math.max(0, itemTop - 10);
1886
+ else if (itemBottom > pageBottom) page.scrollTop = itemBottom - page.clientHeight + 10;
1887
+ }
1888
+ /** Is another timeline rail already mounted (e.g. dsh-chat-timeline)? */
1889
+ function otherRailPresent() {
1890
+ return document.querySelector(".dsct_nav") !== null;
1891
+ }
1892
+ /**
1893
+ * The timeline rail component. Portaled to body; registered into the
1894
+ * conversation input dock purely to bind per-session lifecycle.
1895
+ */
1896
+ function TimelineRail(props) {
1897
+ const { sessionId, sessionsService, t } = props;
1898
+ const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
1899
+ const timelineUnknown = settings.status === "loading";
1900
+ const timelineDisabled = settings.value?.timeline === false;
1901
+ const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
1902
+ const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
1903
+ const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
1904
+ const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
1905
+ const messages = react.default.useMemo(() => collectMessages(nodeSnapshot), [nodeSnapshot]);
1906
+ const [loadedAll, setLoadedAll] = react.default.useState(false);
1907
+ const [activeIndex, setActiveIndex] = react.default.useState(-1);
1908
+ const [show, setShow] = react.default.useState(false);
1909
+ const [rightOffset, setRightOffset] = react.default.useState(16);
1910
+ const [railWidth, setRailWidth] = react.default.useState(RAIL_MAX_WIDTH);
1911
+ const [fades, setFades] = react.default.useState({
1912
+ top: false,
1913
+ bottom: false
1914
+ });
1915
+ const [hidden, setHidden] = react.default.useState(() => otherRailPresent());
1916
+ const [marks, setMarks] = react.default.useState(() => readMarks(sessionId));
1917
+ const [marksOnly, setMarksOnly] = react.default.useState(false);
1918
+ const [isNarrow, setIsNarrow] = react.default.useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches);
1919
+ const pageRef = react.default.useRef(null);
1920
+ const activeItemRef = react.default.useRef(null);
1921
+ const jumpPendingRef = react.default.useRef(false);
1922
+ const jumpFallbackRef = react.default.useRef(void 0);
1923
+ react.default.useEffect(() => {
1924
+ setMarks(readMarks(sessionId));
1925
+ setMarksOnly(false);
1926
+ }, [sessionId]);
1927
+ const markedSet = react.default.useMemo(() => new Set(marks), [marks]);
1928
+ const marksRef = react.default.useRef(marks);
1929
+ marksRef.current = marks;
1930
+ const toggleMark = react.default.useCallback((m) => {
1931
+ const k = markKeyOf(m);
1932
+ if (k === "") return;
1933
+ const prev = marksRef.current;
1934
+ const next = prev.includes(k) ? prev.filter((x) => x !== k) : [...prev, k];
1935
+ marksRef.current = next;
1936
+ setMarks(next);
1937
+ writeMarks(sessionId, next);
1938
+ }, [sessionId]);
1939
+ react.default.useEffect(() => {
1940
+ if (typeof window.matchMedia !== "function") return;
1941
+ const mql = window.matchMedia("(max-width: 767px)");
1942
+ const onChange = (e) => setIsNarrow(e.matches);
1943
+ mql.addEventListener("change", onChange);
1944
+ return () => mql.removeEventListener("change", onChange);
1945
+ }, []);
1946
+ react.default.useEffect(() => {
1947
+ if (session === void 0 || isNarrow || sessionId === void 0 || timelineUnknown || timelineDisabled || hidden) return;
1948
+ setLoadedAll(false);
1949
+ let cancelled = false;
1950
+ const run = async () => {
1951
+ let guard = 0;
1952
+ while (!cancelled && guard++ < 120) {
1953
+ const snap = session.getSnapshot();
1954
+ if (snap?.hasMore !== true) break;
1955
+ if (snap.loadingOlder === true) {
1956
+ await delay(50);
1957
+ continue;
1958
+ }
1959
+ await session.loadOlder();
1960
+ }
1961
+ if (!cancelled) setLoadedAll(true);
1962
+ };
1963
+ run().catch(() => {
1964
+ if (!cancelled) setLoadedAll(true);
1965
+ });
1966
+ return () => {
1967
+ cancelled = true;
1968
+ };
1969
+ }, [
1970
+ sessionId,
1971
+ isNarrow,
1972
+ session,
1973
+ timelineUnknown,
1974
+ timelineDisabled,
1975
+ hidden
1976
+ ]);
1977
+ react.default.useEffect(() => {
1978
+ if (!loadedAll && messages.length > 0) return;
1979
+ setRailWidth(railWidthFor(messages.map((m) => m.text)));
1980
+ }, [loadedAll, messages]);
1981
+ react.default.useEffect(() => {
1982
+ if (messages.length === 0) return;
1983
+ const indexByKey = /* @__PURE__ */ new Map();
1984
+ messages.forEach((m, i) => {
1985
+ const key = resolveAnchorKey(m);
1986
+ if (key !== void 0) indexByKey.set(key, i);
1987
+ });
1988
+ const updateActive = () => {
1989
+ if (jumpPendingRef.current) return;
1990
+ const sp = document.querySelector("[data-conversation-scroll]");
1991
+ if (sp === null) return;
1992
+ const rect = sp.getBoundingClientRect();
1993
+ if (rect.height === 0) return;
1994
+ const line = rect.top + rect.height * .4;
1995
+ const rows = sp.querySelectorAll("[data-chat-anchor-key]");
1996
+ let best = -1;
1997
+ let bestDist = Infinity;
1998
+ for (const row of rows) {
1999
+ const key = row.getAttribute("data-chat-anchor-key");
2000
+ if (key === null) continue;
2001
+ const idx = indexByKey.get(key) ?? -1;
2002
+ if (idx === -1) continue;
2003
+ const r = row.getBoundingClientRect();
2004
+ const dist = Math.abs(r.top + r.height / 2 - line);
2005
+ if (dist < bestDist) {
2006
+ bestDist = dist;
2007
+ best = idx;
2008
+ }
2009
+ }
2010
+ setActiveIndex(best);
2011
+ };
2012
+ updateActive();
2013
+ const el = document.querySelector("[data-conversation-scroll]");
2014
+ let timer;
2015
+ let settleTimer;
2016
+ const onScroll = () => {
2017
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
2018
+ settleTimer = window.setTimeout(() => {
2019
+ settleTimer = void 0;
2020
+ if (jumpPendingRef.current) {
2021
+ jumpPendingRef.current = false;
2022
+ if (jumpFallbackRef.current !== void 0) {
2023
+ clearTimeout(jumpFallbackRef.current);
2024
+ jumpFallbackRef.current = void 0;
2025
+ }
2026
+ updateActive();
2027
+ }
2028
+ }, 150);
2029
+ if (timer !== void 0) return;
2030
+ timer = window.setTimeout(() => {
2031
+ timer = void 0;
2032
+ updateActive();
2033
+ }, 60);
2034
+ };
2035
+ el?.addEventListener("scroll", onScroll, { passive: true });
2036
+ const interval = window.setInterval(updateActive, 2e3);
2037
+ return () => {
2038
+ if (timer !== void 0) clearTimeout(timer);
2039
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
2040
+ if (jumpFallbackRef.current !== void 0) {
2041
+ clearTimeout(jumpFallbackRef.current);
2042
+ jumpFallbackRef.current = void 0;
2043
+ }
2044
+ el?.removeEventListener("scroll", onScroll);
2045
+ clearInterval(interval);
2046
+ };
2047
+ }, [messages]);
2048
+ const syncFades = react.default.useCallback(() => {
2049
+ const page = pageRef.current;
2050
+ if (page === null) {
2051
+ setFades({
2052
+ top: false,
2053
+ bottom: false
2054
+ });
2055
+ return;
2056
+ }
2057
+ const canTop = page.scrollTop > 2;
2058
+ const canBot = page.scrollTop + page.clientHeight < page.scrollHeight - 2;
2059
+ setFades((prev) => prev.top === canTop && prev.bottom === canBot ? prev : {
2060
+ top: canTop,
2061
+ bottom: canBot
2062
+ });
2063
+ }, []);
2064
+ react.default.useEffect(syncFades, [
2065
+ syncFades,
2066
+ show,
2067
+ messages.length,
2068
+ loadedAll,
2069
+ marksOnly
2070
+ ]);
2071
+ react.default.useLayoutEffect(() => {
2072
+ if (!show) return;
2073
+ const page = pageRef.current;
2074
+ const item = activeItemRef.current;
2075
+ if (page === null || item === null) return;
2076
+ scrollTimelineItemIntoView(page, item);
2077
+ syncFades();
2078
+ }, [
2079
+ activeIndex,
2080
+ messages.length,
2081
+ marksOnly,
2082
+ show,
2083
+ syncFades
2084
+ ]);
2085
+ react.default.useEffect(() => {
2086
+ let raf = 0;
2087
+ const measure = () => {
2088
+ raf = 0;
2089
+ const sp = document.querySelector("[data-conversation-scroll]");
2090
+ if (sp === null) return;
2091
+ const rect = sp.getBoundingClientRect();
2092
+ if (rect.width === 0 && rect.height === 0) return;
2093
+ const next = Math.max(8, Math.round(window.innerWidth - rect.right + 12));
2094
+ setRightOffset((prev) => Math.abs(prev - next) > .5 ? next : prev);
2095
+ };
2096
+ const schedule = () => {
2097
+ if (raf === 0) raf = window.requestAnimationFrame(measure);
2098
+ };
2099
+ const sp = document.querySelector("[data-conversation-scroll]");
2100
+ const ro = typeof ResizeObserver === "function" && sp !== null ? new ResizeObserver(schedule) : null;
2101
+ if (ro !== null && sp !== null) ro.observe(sp);
2102
+ window.addEventListener("resize", schedule);
2103
+ const guard = window.setInterval(() => setHidden(otherRailPresent()), 2e3);
2104
+ measure();
2105
+ return () => {
2106
+ if (raf !== 0) cancelAnimationFrame(raf);
2107
+ ro?.disconnect();
2108
+ window.removeEventListener("resize", schedule);
2109
+ clearInterval(guard);
2110
+ };
2111
+ }, []);
2112
+ if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length < 2) return null;
2113
+ const height = railHeightFor(messages.length);
2114
+ const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
2115
+ const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
2116
+ return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2117
+ className: "dsbt-nav",
2118
+ style: {
2119
+ right: rightOffset,
2120
+ ["--dsbt-h"]: `${height}px`,
2121
+ ["--dsbt-w"]: `${railWidth}px`
2122
+ },
2123
+ role: "navigation",
2124
+ "aria-label": t("timeline.railLabel"),
2125
+ onMouseEnter: () => setShow(true),
2126
+ onMouseLeave: () => setShow(false),
2127
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: show ? "dsbt-bg dsbt-bg-hide" : "dsbt-bg" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2128
+ className: show ? "dsbt-wrap dsbt-show" : "dsbt-wrap",
2129
+ children: [
2130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2131
+ className: "dsbt-filterbar",
2132
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2133
+ type: "button",
2134
+ className: marksOnly ? "dsbt-filterbtn dsbt-filteron" : "dsbt-filterbtn",
2135
+ "aria-pressed": marksOnly,
2136
+ onClick: () => setMarksOnly((v) => !v),
2137
+ children: [t("timeline.marksOnly"), marks.length > 0 ? ` (${marks.length})` : ""]
2138
+ })
2139
+ }),
2140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2141
+ className: fades.top ? "dsbt-fade dsbt-fade-on" : "dsbt-fade",
2142
+ "aria-hidden": true
2143
+ }),
2144
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2145
+ className: fades.bottom ? "dsbt-fade dsbt-fade-bot dsbt-fade-on" : "dsbt-fade dsbt-fade-bot",
2146
+ "aria-hidden": true
2147
+ }),
2148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2149
+ className: "dsbt-page",
2150
+ ref: pageRef,
2151
+ onScroll: syncFades,
2152
+ children: displayMessages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2153
+ className: "dsbt-empty",
2154
+ children: t("timeline.noMarks")
2155
+ }) : displayMessages.map((m) => {
2156
+ const key = resolveAnchorKey(m);
2157
+ const mk = markKeyOf(m);
2158
+ const marked = markedSet.has(mk);
2159
+ const isActive = activeKey !== "" && mk === activeKey;
2160
+ const cls = `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`;
2161
+ const label = `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`;
2162
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2163
+ type: "button",
2164
+ ref: isActive ? activeItemRef : void 0,
2165
+ className: cls,
2166
+ title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2167
+ "aria-label": label,
2168
+ "aria-current": isActive ? "location" : void 0,
2169
+ onClick: () => {
2170
+ if (key === void 0) return;
2171
+ jumpPendingRef.current = true;
2172
+ if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
2173
+ jumpFallbackRef.current = window.setTimeout(() => {
2174
+ jumpFallbackRef.current = void 0;
2175
+ jumpPendingRef.current = false;
2176
+ }, 800);
2177
+ jumpToMessage(sessionsService, sessionId, key).catch(() => {});
2178
+ },
2179
+ children: [
2180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
+ className: "dsbt-title",
2182
+ children: m.text === "" ? t("timeline.noText") : m.text
2183
+ }),
2184
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2185
+ className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
2186
+ role: "button",
2187
+ tabIndex: 0,
2188
+ "aria-pressed": marked,
2189
+ "aria-label": marked ? t("timeline.unmark") : t("timeline.mark"),
2190
+ onMouseDown: (e) => {
2191
+ e.stopPropagation();
2192
+ e.preventDefault();
2193
+ },
2194
+ onClick: (e) => {
2195
+ e.stopPropagation();
2196
+ toggleMark(m);
2197
+ },
2198
+ onKeyDown: (e) => {
2199
+ if (e.key === "Enter" || e.key === " ") {
2200
+ e.preventDefault();
2201
+ e.stopPropagation();
2202
+ toggleMark(m);
2203
+ }
2204
+ },
2205
+ children: "★"
2206
+ }),
2207
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2208
+ className: "dsbt-ind",
2209
+ "aria-hidden": true,
2210
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsbt-line" })
2211
+ })
2212
+ ]
2213
+ }, m.seq);
2214
+ })
2215
+ })
2216
+ ]
2217
+ })]
2218
+ }), document.body);
2219
+ }
2220
+ //#endregion
1095
2221
  //#region src/client/index.ts
1096
2222
  /**
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.
2223
+ * Service injections this entry declares: `slots` + `locale` for the settings
2224
+ * surface and the timeline slot seat, and `sessions` so the timeline rail can
2225
+ * enumerate user messages and jump between them. The settings surface itself
2226
+ * talks to the plugin's own host route over plain same-origin fetch.
1100
2227
  */
1101
- const inject = ["slots", "locale"];
2228
+ const inject = [
2229
+ "slots",
2230
+ "locale",
2231
+ "sessions"
2232
+ ];
1102
2233
  /**
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).
2234
+ * Client plugin body: paint the persisted background live, register the
2235
+ * General-settings row, and mount the conversation timeline rail.
2236
+ * @param ctx - client cordis context (slots/locale/sessions injected).
1106
2237
  */
1107
2238
  function apply(ctx) {
1108
2239
  const paint = () => {
@@ -1126,6 +2257,13 @@ window.__ModuleLoader__.load({
1126
2257
  order: 600,
1127
2258
  locale: "ui-background"
1128
2259
  }, BackgroundSettingsRow));
2260
+ ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
2261
+ name: "conversation.input.dock",
2262
+ id: "deepseek-harness-background.timeline",
2263
+ order: 45,
2264
+ locale: "ui-background",
2265
+ inject: () => ({ sessionsService: ctx.sessions })
2266
+ }, TimelineRail));
1129
2267
  ctx.effect(() => () => {
1130
2268
  unsubscribe();
1131
2269
  backgroundPainter.dispose();