deepseek-harness-background 0.3.1 → 0.4.0

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
@@ -61,16 +61,17 @@ window.__ModuleLoader__.load({
61
61
  * as small cards/buttons over the wallpaper are glassed — composer card,
62
62
  * message bubbles, code surfaces (blocks, inline code, tool IO cards,
63
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.
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.
74
75
  *
75
76
  * Selectors target authored attributes (`data-dsh-bg`, `data-composer-card`,
76
77
  * `data-terminal`…), the `:global` `.md-code-block` anchor, and CSS-module
@@ -227,6 +228,53 @@ window.__ModuleLoader__.load({
227
228
  body[data-ds-dark-theme][data-dsh-bg-glass] [class*="_previewBadge"] {
228
229
  background-color: color-mix(in srgb, rgb(52 65 91) calc(var(--bg-glass-alpha, 0.72) * 100%), transparent);
229
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);
277
+ }
230
278
  `;
231
279
  /**
232
280
  * Inject the backdrop stylesheet once. Safe to call repeatedly; a second call
@@ -248,6 +296,12 @@ window.__ModuleLoader__.load({
248
296
  const DEFAULT_SCRIM = .25;
249
297
  /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
250
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";
251
305
  /** Default background rendering mode. */
252
306
  const DEFAULT_FIT = "cover";
253
307
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
@@ -269,6 +323,11 @@ window.__ModuleLoader__.load({
269
323
  const LAYER_CLASS = "dsh-bg-layer";
270
324
  const IMAGE_CLASS = "dsh-bg-image";
271
325
  const SCRIM_CLASS = "dsh-bg-scrim";
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"];
272
331
  /**
273
332
  * White-glass tokens — the WHITELISTED surfaces only. Everything that floats
274
333
  * as a small card over the wallpaper: the composer input stack, message
@@ -342,12 +401,21 @@ window.__ModuleLoader__.load({
342
401
  };
343
402
  }
344
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
+ }
412
+ /**
345
413
  * The theme-aware glass surface alphas for one panelOpacity: the input-surface
346
414
  * token and the bubble token share one curve, dimmed per scheme. Shared by the
347
415
  * live painter and the settings-row preview card so the two cannot drift.
348
416
  */
349
417
  function glassSurfaceAlphas(panelOpacity, inDark) {
350
- const alpha = glassAlpha(panelOpacity) * (inDark ? .4 : .8);
418
+ const alpha = glassBaseAlpha(panelOpacity, inDark);
351
419
  return {
352
420
  major: `rgba(255, 255, 255, ${alpha.toFixed(3)})`,
353
421
  bubble: `rgba(255, 255, 255, ${(alpha * .8).toFixed(3)})`
@@ -393,20 +461,21 @@ window.__ModuleLoader__.load({
393
461
  return;
394
462
  }
395
463
  injectBackgroundCss();
396
- this.rememberOnce("--dsw-alias-bg-base");
397
- this.rememberOnce("--dsw-specific-sidebar-fill");
398
- document.body.style.setProperty("--dsw-alias-bg-base", "transparent");
399
- document.body.style.setProperty("--dsw-specific-sidebar-fill", "transparent");
400
464
  const url = backgroundImageUrl(settings, uploadBase);
401
465
  if (url === "") {
402
466
  this.observer?.disconnect();
403
467
  this.observer = void 0;
468
+ this.restoreFrameTokens();
404
469
  this.applySurfaceGlass(settings, true);
405
470
  this.active = false;
406
471
  this.removeLayers();
407
472
  document.body.removeAttribute(ACTIVE_ATTR);
408
473
  return;
409
474
  }
475
+ for (const prop of FRAME_TOKENS) {
476
+ this.rememberOnce(prop);
477
+ document.body.style.setProperty(prop, "transparent");
478
+ }
410
479
  if (!this.layer) {
411
480
  this.layer = document.createElement("div");
412
481
  this.layer.className = LAYER_CLASS;
@@ -448,6 +517,20 @@ window.__ModuleLoader__.load({
448
517
  document.body.style.setProperty(name, value);
449
518
  }
450
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
+ /**
451
534
  * Update one effect knob without re-running the full apply. This is the hot
452
535
  * path for slider drags (many events per second): only the single CSS
453
536
  * variable changes, the DOM layers are untouched.
@@ -506,7 +589,7 @@ window.__ModuleLoader__.load({
506
589
  this.setVar("--bg-glass-brightness", exposure.brightness);
507
590
  this.setVar("--bg-glass-sheen", exposure.sheen);
508
591
  this.setVar("--bg-glass-sheen-mid", exposure.sheenMid);
509
- const base = glassAlpha(settings.panelOpacity) * (inDark ? .4 : .8);
592
+ const base = glassBaseAlpha(settings.panelOpacity, inDark);
510
593
  for (const { token, factor } of GLASS_SURFACE_TOKENS) s.setProperty(token, `rgba(255, 255, 255, ${(base * factor).toFixed(3)})`);
511
594
  this.setVar("--bg-glass-alpha", base.toFixed(3));
512
595
  this.setVar("--bg-glass-alpha-strong", Math.min(.9, base * 1.5).toFixed(3));
@@ -605,6 +688,198 @@ window.__ModuleLoader__.load({
605
688
  function clearPreviewSurface(el) {
606
689
  for (const name of PREVIEW_VARS) el.style.removeProperty(name);
607
690
  }
691
+ /** Property name on window where the api is published. */
692
+ const GLASS_BRIDGE_GLOBAL = "__DSH_BACKGROUND_GLASS__";
693
+ /** DOM event announced once the api becomes available (detail = the api). */
694
+ const GLASS_READY_EVENT = "dsh-background-glass:ready";
695
+ /** Style-tag dedup key of the registry stylesheet (data-plugin-css). */
696
+ const REGISTRY_CSS_TAG = "deepseek-harness-background/glass-registry";
697
+ /** Plugin identity stamped onto the registry <style> element. */
698
+ const PLUGIN_ID = "deepseek-harness-background";
699
+ /** Characters whose presence would let a selector escape its rule context. */
700
+ const FORBIDDEN_SELECTOR_CHARS = [
701
+ "{",
702
+ "}",
703
+ ";",
704
+ "@",
705
+ "<",
706
+ ">"
707
+ ];
708
+ /** Hard cap on one selector's length (diagnostics-friendly, abuse-proof). */
709
+ const MAX_SELECTOR_LENGTH = 500;
710
+ /** Hard cap on selectors per registration call. */
711
+ const MAX_SELECTORS_PER_SPEC = 64;
712
+ /**
713
+ * Validate one raw selector string. Structural checks only: anything that
714
+ * could escape the generated rule body ({}, ;, @, <, >) is rejected, and a
715
+ * backslash is refused too (escape sequences have no business in an anchor
716
+ * selector); ordinary CSS mistakes (a typo'd pseudo-class) are left to the
717
+ * browser, which drops just that one rule — a safe failure mode.
718
+ * @returns an error message, or null when the selector is acceptable.
719
+ */
720
+ function selectorProblem(selector) {
721
+ if (typeof selector !== "string") return "must be a string";
722
+ const trimmed = selector.trim();
723
+ if (trimmed === "") return "must not be empty";
724
+ if (trimmed.length > MAX_SELECTOR_LENGTH) return "exceeds the 500-character limit (" + trimmed.length + ")";
725
+ for (const ch of FORBIDDEN_SELECTOR_CHARS) if (trimmed.includes(ch)) return "contains a forbidden character (" + JSON.stringify(ch) + ")";
726
+ if (trimmed.includes(String.fromCharCode(92))) return "contains a backslash (escape sequences are not accepted)";
727
+ return null;
728
+ }
729
+ /** Normalize a spec into validated (plugin, mode, selector) triples.
730
+ * @returns the accepted triples plus one warning line per rejected selector. */
731
+ function normalizeSpec(spec) {
732
+ const plugin = typeof spec?.plugin === "string" && spec.plugin.trim() !== "" ? spec.plugin.trim() : "(anonymous)";
733
+ const mode = spec?.mode === "fill" ? "fill" : "token";
734
+ const raw = Array.isArray(spec?.selectors) ? [...spec.selectors] : [spec?.selectors];
735
+ const rows = [];
736
+ const warnings = [];
737
+ if (raw.length > MAX_SELECTORS_PER_SPEC) warnings.push("[glass-registry] " + plugin + ": " + raw.length + " selectors exceeds the 64-selector cap; extra entries dropped.");
738
+ for (const item of raw.slice(0, MAX_SELECTORS_PER_SPEC)) {
739
+ const problem = selectorProblem(item);
740
+ if (typeof item !== "string" || problem !== null) {
741
+ warnings.push("[glass-registry] " + plugin + ": rejected selector " + JSON.stringify(item) + " — " + (problem ?? "must be a string") + ".");
742
+ continue;
743
+ }
744
+ rows.push({
745
+ plugin,
746
+ mode,
747
+ selector: item.trim()
748
+ });
749
+ }
750
+ return {
751
+ rows,
752
+ warnings
753
+ };
754
+ }
755
+ /**
756
+ * Render one registered surface as a gated CSS rule — the same recipes the
757
+ * built-in sheets emit for their explicit-fill families.
758
+ */
759
+ function ruleFor(row) {
760
+ const fill = row.mode === "fill" ? "background-color: var(--dsw-specific-input-major); background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01)); -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01); backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);" : "background-image: linear-gradient(180deg, rgba(255, 255, 255, var(--bg-glass-sheen, 0.07)), rgba(255, 255, 255, var(--bg-glass-sheen-mid, 0.02)) 38%, rgba(255, 255, 255, 0.01)); -webkit-backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01); backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);";
761
+ return "body[data-dsh-bg-glass] " + row.selector + " { " + fill + " }";
762
+ }
763
+ /**
764
+ * Owns the registered surfaces and the registry stylesheet.
765
+ */
766
+ var GlassRegistry = class {
767
+ entries = /* @__PURE__ */ new Map();
768
+ styleEl = null;
769
+ disposed = false;
770
+ /**
771
+ * Register one spec. See BackgroundGlassApi.register.
772
+ * @param spec - the surfaces to glass.
773
+ * @returns the unregister function (idempotent).
774
+ */
775
+ register(spec) {
776
+ if (this.disposed || publishedRegistry !== void 0 && publishedRegistry !== this) {
777
+ console.warn("[glass-registry] registration ignored: this bridge is no longer the live publication");
778
+ return () => {};
779
+ }
780
+ let normalized;
781
+ try {
782
+ normalized = normalizeSpec(spec);
783
+ } catch (error) {
784
+ console.warn("[glass-registry] spec rejected: " + (error instanceof Error ? error.message : String(error)));
785
+ normalized = {
786
+ rows: [],
787
+ warnings: []
788
+ };
789
+ }
790
+ const { rows, warnings } = normalized;
791
+ for (const message of warnings) console.warn(message);
792
+ const owner = {};
793
+ const keys = [];
794
+ for (const row of rows) {
795
+ const key = JSON.stringify([
796
+ row.plugin,
797
+ row.mode,
798
+ row.selector
799
+ ]);
800
+ this.entries.set(key, {
801
+ row,
802
+ owner
803
+ });
804
+ keys.push(key);
805
+ }
806
+ this.sync();
807
+ let unregistered = false;
808
+ return () => {
809
+ if (unregistered) return;
810
+ unregistered = true;
811
+ for (const key of keys) {
812
+ const entry = this.entries.get(key);
813
+ if (entry !== void 0 && entry.owner === owner) this.entries.delete(key);
814
+ }
815
+ this.sync();
816
+ };
817
+ }
818
+ /** Rebuild the registry stylesheet from the live entries. */
819
+ sync() {
820
+ if (this.entries.size === 0) {
821
+ this.styleEl?.remove();
822
+ this.styleEl = null;
823
+ return;
824
+ }
825
+ const seen = /* @__PURE__ */ new Set();
826
+ const rules = [];
827
+ for (const { row } of this.entries.values()) {
828
+ const rule = ruleFor(row);
829
+ if (!seen.has(rule)) {
830
+ seen.add(rule);
831
+ rules.push(rule);
832
+ }
833
+ }
834
+ if (this.styleEl === null) {
835
+ this.styleEl = document.createElement("style");
836
+ this.styleEl.dataset.plugin = PLUGIN_ID;
837
+ this.styleEl.dataset.pluginCss = REGISTRY_CSS_TAG;
838
+ document.head.appendChild(this.styleEl);
839
+ }
840
+ this.styleEl.textContent = rules.join(String.fromCharCode(10));
841
+ }
842
+ /** Tear everything down: entries, stylesheet, and the window publication. */
843
+ dispose() {
844
+ this.disposed = true;
845
+ this.entries.clear();
846
+ this.sync();
847
+ const w = window;
848
+ if (w["__DSH_BACKGROUND_GLASS__"] === this.api) delete w[GLASS_BRIDGE_GLOBAL];
849
+ if (publishedRegistry === this) publishedRegistry = void 0;
850
+ }
851
+ /** The published api face over this registry. */
852
+ api = {
853
+ version: 1,
854
+ bridgeId: "deepseek-harness-background",
855
+ isActive: () => typeof document !== "undefined" && document.body !== null && document.body.hasAttribute("data-dsh-bg-glass"),
856
+ register: (spec) => this.register(spec)
857
+ };
858
+ };
859
+ /** The registry whose api is CURRENTLY published on window. Module-level on
860
+ * purpose: the published api face deliberately carries no dispose member
861
+ * (internal teardown stays unreachable for consumers), so reinstall-time
862
+ * cleanup needs a handle the window key cannot provide. */
863
+ let publishedRegistry;
864
+ /**
865
+ * Publish the glass registry bridge onto window and announce it. Safe to
866
+ * call repeatedly: a previous publication's REGISTRY is disposed first, so
867
+ * exactly one bridge — and one stylesheet — is ever live.
868
+ * @returns the bridge handle — call .dispose() when this plugin's client
869
+ * fiber tears down.
870
+ */
871
+ function installGlassBridge() {
872
+ publishedRegistry?.dispose();
873
+ document.querySelector("style[data-plugin-css=\"deepseek-harness-background/glass-registry\"]")?.remove();
874
+ const registry = new GlassRegistry();
875
+ publishedRegistry = registry;
876
+ window[GLASS_BRIDGE_GLOBAL] = registry.api;
877
+ window.dispatchEvent(new CustomEvent(GLASS_READY_EVENT, { detail: registry.api }));
878
+ return {
879
+ api: registry.api,
880
+ dispose: () => registry.dispose()
881
+ };
882
+ }
608
883
  //#endregion
609
884
  //#region src/client/locales.ts
610
885
  /** Chinese copy. */
@@ -772,6 +1047,9 @@ window.__ModuleLoader__.load({
772
1047
  };
773
1048
  const promise = run();
774
1049
  this.pending = promise;
1050
+ promise.then(() => {
1051
+ if (this.pending === promise) this.pending = void 0;
1052
+ });
775
1053
  return promise;
776
1054
  }
777
1055
  /**
@@ -799,7 +1077,7 @@ window.__ModuleLoader__.load({
799
1077
  const settingsClient = new SettingsClient();
800
1078
  //#endregion
801
1079
  //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
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}}";
1080
+ 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}}";
803
1081
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
804
1082
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
805
1083
  const tag = document.createElement("style");
@@ -809,41 +1087,41 @@ window.__ModuleLoader__.load({
809
1087
  document.head.appendChild(tag);
810
1088
  }
811
1089
  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
1090
  "switch": "-ozqdq_switch",
819
- "segGroup": "-ozqdq_segGroup",
820
- "segActive": "-ozqdq_segActive",
1091
+ "preview": "-ozqdq_preview",
1092
+ "sliderLabel": "-ozqdq_sliderLabel",
821
1093
  "segRow": "-ozqdq_segRow",
822
- "btn": "-ozqdq_btn",
823
- "previewEmpty": "-ozqdq_previewEmpty",
824
- "previewGlass": "-ozqdq_previewGlass",
825
- "row": "-ozqdq_row",
826
- "titleRow": "-ozqdq_titleRow",
827
- "btnDanger": "-ozqdq_btnDanger",
828
- "desc": "-ozqdq_desc",
829
- "sliderRow": "-ozqdq_sliderRow",
830
- "segLabel": "-ozqdq_segLabel",
831
- "title": "-ozqdq_title",
832
1094
  "urlRow": "-ozqdq_urlRow",
833
- "enabledBadge": "-ozqdq_enabledBadge",
834
- "sliderLabel": "-ozqdq_sliderLabel",
1095
+ "previewImg": "-ozqdq_previewImg",
1096
+ "previewGlass": "-ozqdq_previewGlass",
835
1097
  "hint": "-ozqdq_hint",
1098
+ "header": "-ozqdq_header",
836
1099
  "slider": "-ozqdq_slider",
1100
+ "title": "-ozqdq_title",
1101
+ "segLabel": "-ozqdq_segLabel",
837
1102
  "urlInput": "-ozqdq_urlInput",
1103
+ "error": "-ozqdq_error",
1104
+ "enabledBadge": "-ozqdq_enabledBadge",
838
1105
  "btnPrimary": "-ozqdq_btnPrimary",
839
1106
  "clearBtn": "-ozqdq_clearBtn",
840
- "error": "-ozqdq_error",
841
- "previewImg": "-ozqdq_previewImg",
842
1107
  "sectionLabel": "-ozqdq_sectionLabel",
1108
+ "switchKnob": "-ozqdq_switchKnob",
843
1109
  "section": "-ozqdq_section",
844
- "preview": "-ozqdq_preview",
845
1110
  "sliderValue": "-ozqdq_sliderValue",
846
- "previewGlassText": "-ozqdq_previewGlassText"
1111
+ "row": "-ozqdq_row",
1112
+ "previewScrim": "-ozqdq_previewScrim",
1113
+ "desc": "-ozqdq_desc",
1114
+ "segGroup": "-ozqdq_segGroup",
1115
+ "clearRow": "-ozqdq_clearRow",
1116
+ "segActive": "-ozqdq_segActive",
1117
+ "sliderRow": "-ozqdq_sliderRow",
1118
+ "btn": "-ozqdq_btn",
1119
+ "titleRow": "-ozqdq_titleRow",
1120
+ "btnDanger": "-ozqdq_btnDanger",
1121
+ "previewGlassText": "-ozqdq_previewGlassText",
1122
+ "sourceRow": "-ozqdq_sourceRow",
1123
+ "segBtn": "-ozqdq_segBtn",
1124
+ "previewEmpty": "-ozqdq_previewEmpty"
847
1125
  };
848
1126
  //#endregion
849
1127
  //#region src/client/SettingsRow.tsx
@@ -1389,7 +1667,16 @@ window.__ModuleLoader__.load({
1389
1667
  * nav 34px-wide rail, fixed right edge, vertically centered; collapsed
1390
1668
  * blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
1391
1669
  * whose scroll area holds 30px rows (tick indicator + fading-in title);
1392
- * 32px gradient fade veils top/bottom shown while that side clips.
1670
+ * 32px gradient fade veils top/bottom, expanded-only, shown while that
1671
+ * side actually clips.
1672
+ *
1673
+ * Deviations from the reference port, all fidelity fixes:
1674
+ * - The filter bar collapses to zero height while idle instead of staying
1675
+ * in layout invisibly (it pushed every tick ~33px down).
1676
+ * - Short stacks center vertically; overflowing stacks are bottom-pinned
1677
+ * by the component so the newest tick hugs the capsule's bottom edge
1678
+ * (the official idle strategy — the reference port flowed ticks from
1679
+ * the top and clipped the newest away).
1393
1680
  *
1394
1681
  * Glass integration: the official chat.deepseek.com paints (light
1395
1682
  * rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
@@ -1416,6 +1703,9 @@ window.__ModuleLoader__.load({
1416
1703
  const TIMELINE_CSS = `
1417
1704
  .dsbt-nav {
1418
1705
  user-select: none; -webkit-user-select: none;
1706
+ /* Filter-bar chrome height shared by the bar itself and the top veil's
1707
+ expanded offset. */
1708
+ --dsbt-bar-h: 33px;
1419
1709
  z-index: 90; align-items: center; width: 34px;
1420
1710
  height: var(--dsbt-h, 300px);
1421
1711
  transition: right .2s ease, opacity .2s ease;
@@ -1481,7 +1771,10 @@ window.__ModuleLoader__.load({
1481
1771
  /* Content fade veils — the official ::before/::after pair rebuilt as
1482
1772
  elements: a bar of the panel's OWN fill color masked to nothing over
1483
1773
  32px (solid -> mask == gradient-to-transparent, but works with the
1484
- translucent fill). Shown only while that side actually clips. */
1774
+ translucent fill). They belong to the EXPANDED panel only — the idle
1775
+ capsule hard-clips its ticks at the rounded ends like the official
1776
+ strip — and while expanded the top veil starts BELOW the filter bar,
1777
+ not over it. Shown only while that side actually clips. */
1485
1778
  .dsbt-fade {
1486
1779
  z-index: 2; pointer-events: none; opacity: 0;
1487
1780
  background-color: rgba(255, 255, 255, .94);
@@ -1492,15 +1785,25 @@ window.__ModuleLoader__.load({
1492
1785
  }
1493
1786
  body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
1494
1787
  .dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
1495
- .dsbt-fade.dsbt-fade-on { opacity: 1; }
1788
+ .dsbt-wrap.dsbt-show .dsbt-fade.dsbt-fade-on { opacity: 1; }
1789
+ .dsbt-wrap.dsbt-show .dsbt-fade:not(.dsbt-fade-bot) { top: var(--dsbt-bar-h, 33px); }
1496
1790
 
1497
- /* Filter bar — "marked only" toggle pinned above the scroll area. */
1791
+ /* Filter bar — "marked only" toggle pinned above the scroll area while
1792
+ expanded. Collapsed, it collapses to ZERO height (not just opacity): a
1793
+ transparent-but-in-layout bar pushed every tick ~33px down, one of the
1794
+ reasons the idle capsule read as off-alignment. */
1498
1795
  .dsbt-filterbar {
1796
+ height: var(--dsbt-bar-h, 33px);
1499
1797
  padding: 8px 12px 4px; display: flex; justify-content: flex-end;
1500
1798
  align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
1501
- box-sizing: border-box; flex: none; transition: opacity .2s ease;
1799
+ box-sizing: border-box; flex: none; overflow: hidden;
1800
+ transition: opacity .2s ease, height .2s ease, padding .2s ease,
1801
+ border-color .2s ease;
1802
+ }
1803
+ .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar {
1804
+ height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
1805
+ border-bottom-color: transparent;
1502
1806
  }
1503
- .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar { opacity: 0; }
1504
1807
  body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
1505
1808
  .dsbt-filterbtn {
1506
1809
  font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
@@ -1522,13 +1825,19 @@ window.__ModuleLoader__.load({
1522
1825
  background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
1523
1826
  }
1524
1827
 
1525
- /* Scroll area — rows hug the right edge; scrolls only when overfull. */
1828
+ /* Scroll area — rows hug the right edge; scrolls only when overfull. The
1829
+ auto-margin pseudo pair vertically centers the stack when it fits the
1830
+ capsule (short conversations), and resolves to zero free space when it
1831
+ overflows, so bottom-pinned scrolling behaves like a plain column. */
1526
1832
  .dsbt-page {
1527
1833
  padding: 15px 0 15px 24px; box-sizing: border-box;
1528
1834
  overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
1529
1835
  flex-direction: column; align-items: flex-end; display: flex;
1530
1836
  position: relative; width: 100%; overflow: hidden;
1531
1837
  }
1838
+ .dsbt-page::before, .dsbt-page::after {
1839
+ content: ''; flex: none; margin-block: auto;
1840
+ }
1532
1841
  .dsbt-wrap.dsbt-show .dsbt-page {
1533
1842
  overflow-y: auto; overflow-x: hidden;
1534
1843
  scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
@@ -1548,15 +1857,26 @@ window.__ModuleLoader__.load({
1548
1857
 
1549
1858
  /* One message row: 30px, star bookmark + tick indicator at the right,
1550
1859
  title fading in. */
1860
+ /* Row shell: layout + shared ink only — the interactive hit areas are the
1861
+ sibling .dsbt-jump / .dsbt-star buttons below. */
1551
1862
  .dsbt-item {
1552
- cursor: pointer; height: 30px; min-height: 30px; flex-shrink: 0;
1863
+ height: 30px; min-height: 30px; flex-shrink: 0;
1553
1864
  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;
1865
+ width: calc(100% - 4px); margin-right: 4px;
1866
+ display: flex; box-sizing: border-box;
1557
1867
  color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
1558
1868
  transition: color .15s ease;
1559
1869
  }
1870
+
1871
+ /* Jump target: a real button so Enter/Space work natively without nesting
1872
+ an interactive element inside another. */
1873
+ .dsbt-jump {
1874
+ cursor: pointer; background: none; border: none; margin: 0; padding: 0;
1875
+ font: inherit; line-height: 20px; text-align: right; color: inherit;
1876
+ flex: 1 1 auto; min-width: 0; height: 100%;
1877
+ display: flex; align-items: center; justify-content: flex-end;
1878
+ }
1879
+ .dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
1560
1880
  .dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
1561
1881
  body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
1562
1882
  body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
@@ -1569,14 +1889,17 @@ window.__ModuleLoader__.load({
1569
1889
  .dsbt-show .dsbt-title { opacity: 1; }
1570
1890
 
1571
1891
  /* Star bookmark — appears on row hover while the panel is expanded; gold
1572
- when marked. Keyboard-operable (span[role=button]). */
1892
+ when marked. A real sibling button next to the jump target, so marking
1893
+ stays keyboard-operable without nested interactive elements. */
1573
1894
  .dsbt-star {
1574
1895
  font-size: 13px; line-height: 16px; width: 18px; height: 18px;
1575
1896
  display: inline-flex; align-items: center; justify-content: center;
1576
1897
  opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
1577
1898
  flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
1578
1899
  -webkit-user-select: none;
1900
+ background: none; border: none; padding: 0; font-family: inherit;
1579
1901
  }
1902
+ .dsbt-star:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; opacity: .9; }
1580
1903
  .dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
1581
1904
  .dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
1582
1905
  .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
@@ -1629,7 +1952,7 @@ window.__ModuleLoader__.load({
1629
1952
  }
1630
1953
 
1631
1954
  @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; }
1955
+ .dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-jump, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
1633
1956
  }
1634
1957
  `;
1635
1958
  //#endregion
@@ -1656,10 +1979,17 @@ window.__ModuleLoader__.load({
1656
1979
  * - Rewind integration: messages withdrawn by a rewind command node are
1657
1980
  * filtered out of the rail.
1658
1981
  *
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.
1982
+ * Data source, fastest first: the host-side `bgTimeline` session projection
1983
+ * (src/projection.ts — a complete enumeration of the session's user messages
1984
+ * delivered through the projection baseline/push channel) -> the loaded chat
1985
+ * node window. The conversation's own lazy loading is never driven from here;
1986
+ * clicking a row pages older history on demand, scrolls the conversation to
1987
+ * the target row, and keeps the rail's own active row visible.
1988
+ *
1989
+ * Collapsed idle strategy (official ScrollNav behavior): the tick stack is
1990
+ * pinned to the BOTTOM inner edge — the newest question's tick always sits at
1991
+ * the bottom of the capsule, older ticks clip away at the top. Hovering
1992
+ * expands the SAME box and scrolls the active row into view instead.
1663
1993
  *
1664
1994
  * The rail reads the shared settings transport: it renders unless
1665
1995
  * `timeline: false`. Its glass paints are self-contained official values
@@ -1746,13 +2076,30 @@ window.__ModuleLoader__.load({
1746
2076
  * @param chat - a chat snapshot (loosely typed on purpose).
1747
2077
  */
1748
2078
  function hiddenSeqsOfChat(chat) {
2079
+ const hidden = rewindHiddenSeqsOfChat(chat);
2080
+ const spans = rewindSpansOfChat(chat);
2081
+ if (spans.length === 0) return hidden;
2082
+ const nodes = chat.nodes;
2083
+ if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
2084
+ for (const node of nodes.values()) {
2085
+ if (node === null || typeof node !== "object") continue;
2086
+ const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
2087
+ if (anchor === void 0) continue;
2088
+ if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
2089
+ }
2090
+ return hidden;
2091
+ }
2092
+ /**
2093
+ * Seq numbers a rewind hid directly: successful commands hide their own row;
2094
+ * preview commands hide their target.
2095
+ * @param chat - a chat snapshot (loosely typed on purpose).
2096
+ */
2097
+ function rewindHiddenSeqsOfChat(chat) {
1749
2098
  const hidden = /* @__PURE__ */ new Set();
1750
2099
  if (chat === null || typeof chat !== "object") return hidden;
1751
2100
  const nodes = chat.nodes;
1752
2101
  if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1753
- const all = [...nodes.values()];
1754
- const spans = [];
1755
- for (const node of all) {
2102
+ for (const node of nodes.values()) {
1756
2103
  if (node === null || typeof node !== "object") continue;
1757
2104
  if (node.kind !== "command") continue;
1758
2105
  const command = node.data;
@@ -1764,23 +2111,39 @@ window.__ModuleLoader__.load({
1764
2111
  }
1765
2112
  const outcome = command.outcome;
1766
2113
  if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
2114
+ if (typeof command.seq === "number") hidden.add(command.seq);
2115
+ }
2116
+ return hidden;
2117
+ }
2118
+ /**
2119
+ * The [target, sourceEventSeq] spans successful rewind commands cut — the
2120
+ * range form of hiding, needed to filter sources that are NOT chat nodes
2121
+ * (the host projection) by plain seq membership.
2122
+ * @param chat - a chat snapshot (loosely typed on purpose).
2123
+ */
2124
+ function rewindSpansOfChat(chat) {
2125
+ const spans = [];
2126
+ if (chat === null || typeof chat !== "object") return spans;
2127
+ const nodes = chat.nodes;
2128
+ if (nodes === void 0 || typeof nodes.values !== "function") return spans;
2129
+ for (const node of nodes.values()) {
2130
+ if (node === null || typeof node !== "object") continue;
2131
+ if (node.kind !== "command") continue;
2132
+ const command = node.data;
2133
+ if (command === null || typeof command !== "object") continue;
2134
+ if (command.name !== "rewind") continue;
2135
+ if (isRewindPreviewCommand(command)) continue;
2136
+ const outcome = command.outcome;
2137
+ if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1767
2138
  const marker = outcome.sourceEventSeq;
1768
2139
  if (typeof marker !== "number") continue;
1769
- if (typeof command.seq === "number") hidden.add(command.seq);
1770
2140
  const target = rewindTargetOfOutcome(outcome.text);
1771
2141
  if (target !== void 0) spans.push({
1772
2142
  start: target,
1773
2143
  end: marker
1774
2144
  });
1775
2145
  }
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;
2146
+ return spans;
1784
2147
  }
1785
2148
  /**
1786
2149
  * Enumerate user messages from the loaded chat-node snapshot (the client-side
@@ -1816,6 +2179,65 @@ window.__ModuleLoader__.load({
1816
2179
  function resolveAnchorKey(m) {
1817
2180
  return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
1818
2181
  }
2182
+ /** The user-message node definition kind in ui-conversation. */
2183
+ const INPUT_MESSAGE_KIND = "input-message";
2184
+ /**
2185
+ * Rebuild a chat row's DOM anchor key from the durable message id — the same
2186
+ * engine formula as conversationContextKey(kind, id) for the input-message
2187
+ * definition (`13:input-message<id>`), so projected entries can jump even
2188
+ * when their chat node is not loaded yet.
2189
+ * @param id - the durable message id carried by the projection entry.
2190
+ */
2191
+ function inputAnchorKeyOf(id) {
2192
+ return `13:${INPUT_MESSAGE_KIND}${id}`;
2193
+ }
2194
+ /**
2195
+ * Validate one projected wire value into rail entries (defensive: the value
2196
+ * crosses the wire and may be absent, partial, or stale).
2197
+ * @param value - the raw useProjection(TIMELINE_PROJECTION_KEY) snapshot.
2198
+ */
2199
+ function normalizeProjectedTimeline(value) {
2200
+ if (value === null || typeof value !== "object") return [];
2201
+ const raw = value.messages;
2202
+ if (!Array.isArray(raw)) return [];
2203
+ const seen = /* @__PURE__ */ new Set();
2204
+ const out = [];
2205
+ for (const item of raw) {
2206
+ if (item === null || typeof item !== "object") continue;
2207
+ const rec = item;
2208
+ if (typeof rec.seq !== "number" || !Number.isFinite(rec.seq)) continue;
2209
+ if (seen.has(rec.seq)) continue;
2210
+ seen.add(rec.seq);
2211
+ out.push({
2212
+ seq: rec.seq,
2213
+ time: typeof rec.time === "number" ? rec.time : 0,
2214
+ text: typeof rec.text === "string" ? rec.text.slice(0, 80) : "",
2215
+ ...typeof rec.id === "string" && rec.id !== "" ? { key: inputAnchorKeyOf(rec.id) } : {}
2216
+ });
2217
+ }
2218
+ out.sort((a, b) => a.seq - b.seq);
2219
+ return out;
2220
+ }
2221
+ /**
2222
+ * Rail messages with the fastest source first: the host projection covers the
2223
+ * WHOLE session (and already excludes rewind cuts), so it wins whenever it
2224
+ * has anything; otherwise fall back to the loaded chat-node window. The
2225
+ * projection path still applies locally-known rewind hiding so a cut lands
2226
+ * instantly even before the next projection push arrives.
2227
+ * @param snapshot - the session conversation snapshot (node fallback).
2228
+ * @param projected - the raw bgTimeline projection value.
2229
+ */
2230
+ function railMessages(snapshot, projected) {
2231
+ const projectedMessages = normalizeProjectedTimeline(projected);
2232
+ if (projectedMessages.length > 0) {
2233
+ const chat = snapshot?.chat;
2234
+ const hidden = rewindHiddenSeqsOfChat(chat);
2235
+ const spans = rewindSpansOfChat(chat);
2236
+ if (hidden.size === 0 && spans.length === 0) return projectedMessages;
2237
+ return projectedMessages.filter((m) => !hidden.has(m.seq) && !spans.some((span) => m.seq >= span.start && m.seq <= span.end));
2238
+ }
2239
+ return collectMessages(snapshot);
2240
+ }
1819
2241
  /**
1820
2242
  * Outer rail height for a message count: rows + breathing room, clamped so
1821
2243
  * both states always share the exact same height (never jumps on expand).
@@ -1839,7 +2261,7 @@ window.__ModuleLoader__.load({
1839
2261
  if (typeof document !== "undefined") {
1840
2262
  const ctx = document.createElement("canvas").getContext("2d");
1841
2263
  if (ctx) {
1842
- ctx.font = "13px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
2264
+ ctx.font = `13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
1843
2265
  for (const t of texts) {
1844
2266
  const w = ctx.measureText(t === "" ? "…" : t).width;
1845
2267
  if (w > widest) widest = w;
@@ -1894,7 +2316,7 @@ window.__ModuleLoader__.load({
1894
2316
  * conversation input dock purely to bind per-session lifecycle.
1895
2317
  */
1896
2318
  function TimelineRail(props) {
1897
- const { sessionId, sessionsService, t } = props;
2319
+ const { sessionId, sessionsService, useProjection, t } = props;
1898
2320
  const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
1899
2321
  const timelineUnknown = settings.status === "loading";
1900
2322
  const timelineDisabled = settings.value?.timeline === false;
@@ -1902,8 +2324,8 @@ window.__ModuleLoader__.load({
1902
2324
  const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
1903
2325
  const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
1904
2326
  const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
1905
- const messages = react.default.useMemo(() => collectMessages(nodeSnapshot), [nodeSnapshot]);
1906
- const [loadedAll, setLoadedAll] = react.default.useState(false);
2327
+ const projected = useProjection !== void 0 ? useProjection(TIMELINE_PROJECTION_KEY) : void 0;
2328
+ const messages = react.default.useMemo(() => railMessages(nodeSnapshot, projected), [nodeSnapshot, projected]);
1907
2329
  const [activeIndex, setActiveIndex] = react.default.useState(-1);
1908
2330
  const [show, setShow] = react.default.useState(false);
1909
2331
  const [rightOffset, setRightOffset] = react.default.useState(16);
@@ -1944,40 +2366,8 @@ window.__ModuleLoader__.load({
1944
2366
  return () => mql.removeEventListener("change", onChange);
1945
2367
  }, []);
1946
2368
  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
2369
  setRailWidth(railWidthFor(messages.map((m) => m.text)));
1980
- }, [loadedAll, messages]);
2370
+ }, [messages]);
1981
2371
  react.default.useEffect(() => {
1982
2372
  if (messages.length === 0) return;
1983
2373
  const indexByKey = /* @__PURE__ */ new Map();
@@ -1985,17 +2375,21 @@ window.__ModuleLoader__.load({
1985
2375
  const key = resolveAnchorKey(m);
1986
2376
  if (key !== void 0) indexByKey.set(key, i);
1987
2377
  });
2378
+ const sp = document.querySelector("[data-conversation-scroll]");
2379
+ if (sp === null) return;
2380
+ const rows = sp.querySelectorAll("[data-chat-anchor-key]");
2381
+ let settleTimer;
2382
+ let frame = 0;
2383
+ const nearRows = /* @__PURE__ */ new Set();
1988
2384
  const updateActive = () => {
1989
2385
  if (jumpPendingRef.current) return;
1990
- const sp = document.querySelector("[data-conversation-scroll]");
1991
- if (sp === null) return;
1992
2386
  const rect = sp.getBoundingClientRect();
1993
2387
  if (rect.height === 0) return;
2388
+ const candidates = nearRows.size > 0 ? nearRows : rows;
1994
2389
  const line = rect.top + rect.height * .4;
1995
- const rows = sp.querySelectorAll("[data-chat-anchor-key]");
1996
2390
  let best = -1;
1997
2391
  let bestDist = Infinity;
1998
- for (const row of rows) {
2392
+ for (const row of candidates) {
1999
2393
  const key = row.getAttribute("data-chat-anchor-key");
2000
2394
  if (key === null) continue;
2001
2395
  const idx = indexByKey.get(key) ?? -1;
@@ -2009,10 +2403,22 @@ window.__ModuleLoader__.load({
2009
2403
  }
2010
2404
  setActiveIndex(best);
2011
2405
  };
2012
- updateActive();
2013
- const el = document.querySelector("[data-conversation-scroll]");
2014
- let timer;
2015
- let settleTimer;
2406
+ const scheduleMeasure = () => {
2407
+ if (frame !== 0) return;
2408
+ frame = window.requestAnimationFrame(() => {
2409
+ frame = 0;
2410
+ updateActive();
2411
+ });
2412
+ };
2413
+ let io;
2414
+ if (typeof IntersectionObserver === "function") {
2415
+ io = new IntersectionObserver((entries) => {
2416
+ for (const entry of entries) if (entry.isIntersecting) nearRows.add(entry.target);
2417
+ else nearRows.delete(entry.target);
2418
+ scheduleMeasure();
2419
+ }, { root: sp });
2420
+ rows.forEach((row) => io?.observe(row));
2421
+ }
2016
2422
  const onScroll = () => {
2017
2423
  if (settleTimer !== void 0) clearTimeout(settleTimer);
2018
2424
  settleTimer = window.setTimeout(() => {
@@ -2026,22 +2432,20 @@ window.__ModuleLoader__.load({
2026
2432
  updateActive();
2027
2433
  }
2028
2434
  }, 150);
2029
- if (timer !== void 0) return;
2030
- timer = window.setTimeout(() => {
2031
- timer = void 0;
2032
- updateActive();
2033
- }, 60);
2435
+ scheduleMeasure();
2034
2436
  };
2035
- el?.addEventListener("scroll", onScroll, { passive: true });
2036
- const interval = window.setInterval(updateActive, 2e3);
2437
+ sp.addEventListener("scroll", onScroll, { passive: true });
2438
+ const interval = window.setInterval(scheduleMeasure, 2e3);
2439
+ scheduleMeasure();
2037
2440
  return () => {
2038
- if (timer !== void 0) clearTimeout(timer);
2441
+ io?.disconnect();
2442
+ if (frame !== 0) cancelAnimationFrame(frame);
2039
2443
  if (settleTimer !== void 0) clearTimeout(settleTimer);
2040
2444
  if (jumpFallbackRef.current !== void 0) {
2041
2445
  clearTimeout(jumpFallbackRef.current);
2042
2446
  jumpFallbackRef.current = void 0;
2043
2447
  }
2044
- el?.removeEventListener("scroll", onScroll);
2448
+ sp.removeEventListener("scroll", onScroll);
2045
2449
  clearInterval(interval);
2046
2450
  };
2047
2451
  }, [messages]);
@@ -2065,7 +2469,6 @@ window.__ModuleLoader__.load({
2065
2469
  syncFades,
2066
2470
  show,
2067
2471
  messages.length,
2068
- loadedAll,
2069
2472
  marksOnly
2070
2473
  ]);
2071
2474
  react.default.useLayoutEffect(() => {
@@ -2082,6 +2485,18 @@ window.__ModuleLoader__.load({
2082
2485
  show,
2083
2486
  syncFades
2084
2487
  ]);
2488
+ const height = railHeightFor(messages.length);
2489
+ react.default.useLayoutEffect(() => {
2490
+ if (show) return;
2491
+ const page = pageRef.current;
2492
+ if (page === null) return;
2493
+ page.scrollTop = page.scrollHeight;
2494
+ }, [
2495
+ show,
2496
+ messages.length,
2497
+ marksOnly,
2498
+ height
2499
+ ]);
2085
2500
  react.default.useEffect(() => {
2086
2501
  let raf = 0;
2087
2502
  const measure = () => {
@@ -2109,8 +2524,7 @@ window.__ModuleLoader__.load({
2109
2524
  clearInterval(guard);
2110
2525
  };
2111
2526
  }, []);
2112
- if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length < 2) return null;
2113
- const height = railHeightFor(messages.length);
2527
+ if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length === 0) return null;
2114
2528
  const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
2115
2529
  const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
2116
2530
  return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2157,51 +2571,37 @@ window.__ModuleLoader__.load({
2157
2571
  const mk = markKeyOf(m);
2158
2572
  const marked = markedSet.has(mk);
2159
2573
  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
- },
2574
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2575
+ className: `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`,
2179
2576
  children: [
2180
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
- className: "dsbt-title",
2182
- children: m.text === "" ? t("timeline.noText") : m.text
2577
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2578
+ type: "button",
2579
+ className: "dsbt-jump",
2580
+ ref: isActive ? activeItemRef : void 0,
2581
+ title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2582
+ "aria-label": `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
2583
+ "aria-current": isActive ? "location" : void 0,
2584
+ onClick: () => {
2585
+ if (key === void 0) return;
2586
+ jumpPendingRef.current = true;
2587
+ if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
2588
+ jumpFallbackRef.current = window.setTimeout(() => {
2589
+ jumpFallbackRef.current = void 0;
2590
+ jumpPendingRef.current = false;
2591
+ }, 800);
2592
+ jumpToMessage(sessionsService, sessionId, key).catch(() => {});
2593
+ },
2594
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2595
+ className: "dsbt-title",
2596
+ children: m.text === "" ? t("timeline.noText") : m.text
2597
+ })
2183
2598
  }),
2184
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2600
+ type: "button",
2185
2601
  className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
2186
- role: "button",
2187
- tabIndex: 0,
2188
2602
  "aria-pressed": marked,
2189
2603
  "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
- },
2604
+ onClick: () => toggleMark(m),
2205
2605
  children: "★"
2206
2606
  }),
2207
2607
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2236,6 +2636,7 @@ window.__ModuleLoader__.load({
2236
2636
  * @param ctx - client cordis context (slots/locale/sessions injected).
2237
2637
  */
2238
2638
  function apply(ctx) {
2639
+ const glassBridge = installGlassBridge();
2239
2640
  const paint = () => {
2240
2641
  const snapshot = settingsClient.getSnapshot();
2241
2642
  const value = snapshot.status === "ready" ? snapshot.value : void 0;
@@ -2266,6 +2667,7 @@ window.__ModuleLoader__.load({
2266
2667
  }, TimelineRail));
2267
2668
  ctx.effect(() => () => {
2268
2669
  unsubscribe();
2670
+ glassBridge.dispose();
2269
2671
  backgroundPainter.dispose();
2270
2672
  }, "deepseek-harness-background: background surface");
2271
2673
  }