deepseek-harness-background 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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));
@@ -772,6 +855,9 @@ window.__ModuleLoader__.load({
772
855
  };
773
856
  const promise = run();
774
857
  this.pending = promise;
858
+ promise.then(() => {
859
+ if (this.pending === promise) this.pending = void 0;
860
+ });
775
861
  return promise;
776
862
  }
777
863
  /**
@@ -799,7 +885,7 @@ window.__ModuleLoader__.load({
799
885
  const settingsClient = new SettingsClient();
800
886
  //#endregion
801
887
  //#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}}";
888
+ const css = ".-ozqdq_row{border-bottom:1px solid var(--dsw-alias-border-l2);flex-direction:column;gap:14px;padding:16px 0;display:flex}.-ozqdq_header{flex-direction:column;gap:4px;display:flex}.-ozqdq_titleRow{justify-content:space-between;align-items:center;gap:12px;min-height:22px;display:flex}.-ozqdq_clearBtn{border-radius:12px;flex:none;height:24px;padding:0 10px;font-size:12px;line-height:24px}.-ozqdq_title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:400;line-height:22px}.-ozqdq_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}.-ozqdq_preview{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;height:132px;position:relative;overflow:hidden}.-ozqdq_previewImg{object-fit:var(--bg-object-fit,cover);width:100%;height:100%;opacity:var(--bg-opacity,1);filter:blur(var(--bg-wallpaper-blur,0px));transform:scale(var(--bg-wallpaper-scale,1));display:block;position:absolute;inset:0}.-ozqdq_previewScrim{background:rgba(255, 255, 255, var(--bg-scrim,.25));position:absolute;inset:0}body[data-ds-dark-theme] .-ozqdq_previewScrim{background:rgba(0, 0, 0, var(--bg-scrim,.25))}.-ozqdq_previewGlass{background:var(--bg-preview-glass,#ffffff24);width:44%;height:46px;backdrop-filter:blur(var(--bg-glass-blur,16px)) saturate(var(--bg-glass-saturate,1.42)) brightness(var(--bg-glass-brightness,1));border:1px solid #ffffff38;border-radius:10px;justify-content:center;align-items:center;display:flex;position:absolute;bottom:16px;left:16px;box-shadow:inset 0 1px #ffffff4d}.-ozqdq_previewGlassText{color:#0f172ac7;text-shadow:0 1px 2px #fff6;white-space:nowrap;font-size:11px;line-height:1}body[data-ds-dark-theme] .-ozqdq_previewGlassText{color:#ffffffd9;text-shadow:0 1px 2px #00000059}.-ozqdq_previewEmpty{color:var(--dsw-alias-label-tertiary);justify-content:center;align-items:center;font-size:12px;line-height:1.5;display:flex;position:absolute;inset:0}.-ozqdq_section{flex-direction:column;gap:8px;display:flex}.-ozqdq_sectionLabel{color:var(--dsw-alias-label-tertiary);letter-spacing:.01em;font-size:12px;font-weight:500;line-height:18px}.-ozqdq_sourceRow{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.-ozqdq_urlRow{align-items:center;gap:8px;display:flex}.-ozqdq_urlInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);min-width:0;height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;flex:1;padding:0 10px;font-size:13px;line-height:1.5}.-ozqdq_urlInput::placeholder{color:var(--dsw-alias-label-dimmed)}.-ozqdq_urlInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}.-ozqdq_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5}.-ozqdq_enabledBadge{white-space:nowrap;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border-radius:999px;align-items:center;gap:4px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;display:inline-flex}.-ozqdq_btn{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border-radius:14px;justify-content:center;align-items:center;padding:0 12px;font-size:12px;line-height:18px;transition:background-color .15s,border-color .15s,color .15s;display:inline-flex}.-ozqdq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.-ozqdq_btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}.-ozqdq_btn:disabled{opacity:.45;cursor:default}.-ozqdq_btnPrimary{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:#0000}.-ozqdq_btnPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.-ozqdq_btnDanger{color:var(--dsw-alias-label-error)}.-ozqdq_btnDanger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.-ozqdq_sliderRow{align-items:center;gap:10px;display:flex}.-ozqdq_sliderLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_slider{-webkit-appearance:none;appearance:none;cursor:pointer;background:0 0;flex:1;min-width:0;height:18px;margin:0}.-ozqdq_slider::-webkit-slider-runnable-track{background:linear-gradient(90deg, var(--dsw-alias-brand-primary) var(--bg-fill,0%), var(--dsw-alias-border-l3) var(--bg-fill,0%));border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-track{background:var(--dsw-alias-border-l3);border-radius:2px;height:4px}.-ozqdq_slider::-moz-range-progress{background:var(--dsw-alias-brand-primary);border-radius:2px;height:4px}.-ozqdq_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%;margin-top:-5px}.-ozqdq_slider::-moz-range-thumb{border:2px solid var(--dsw-alias-brand-primary);width:14px;height:14px;box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), 0 1px 3px #00000047;background:#fff;border-radius:50%}.-ozqdq_slider:hover::-webkit-slider-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:hover::-moz-range-thumb{box-shadow:0 0 0 5px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent), 0 1px 4px #0000004d}.-ozqdq_slider:active::-webkit-slider-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:active::-moz-range-thumb{box-shadow:0 0 0 7px color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, transparent), 0 1px 5px #00000052;transform:scale(1.12)}.-ozqdq_slider:focus-visible{outline:none}.-ozqdq_slider:focus-visible::-webkit-slider-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_slider:focus-visible::-moz-range-thumb{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_sliderValue{text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);flex:0 0 52px;font-size:13px;line-height:1.5}.-ozqdq_segRow{align-items:center;gap:10px;display:flex}.-ozqdq_segLabel{color:var(--dsw-alias-label-primary);flex:0 0 96px;font-size:13px;line-height:1.5}.-ozqdq_segGroup{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex:1;gap:2px;min-width:0;padding:2px;display:inline-flex}.-ozqdq_segBtn{height:24px;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:7px;flex:1;padding:0 16px;font-size:12px;line-height:24px}.-ozqdq_segBtn:hover:not(.-ozqdq_segActive){color:var(--dsw-alias-label-primary)}.-ozqdq_segActive{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font-weight:500}.-ozqdq_error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5}.-ozqdq_clearRow{align-items:center;gap:10px;display:flex}.-ozqdq_switch{background:var(--dsw-alias-border-l3);cursor:pointer;border:none;border-radius:11px;flex:none;width:40px;height:22px;padding:0;transition:background-color .15s;position:relative}.-ozqdq_switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}.-ozqdq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.-ozqdq_switchKnob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:3px;left:3px;box-shadow:0 1px 3px #00000047}.-ozqdq_switch[aria-checked=true] .-ozqdq_switchKnob{transform:translate(18px)}@media (prefers-reduced-motion:reduce){.-ozqdq_btn,.-ozqdq_switch,.-ozqdq_switchKnob{transition:none}}";
803
889
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
804
890
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
805
891
  const tag = document.createElement("style");
@@ -809,41 +895,41 @@ window.__ModuleLoader__.load({
809
895
  document.head.appendChild(tag);
810
896
  }
811
897
  var SettingsRow_module_css_default = {
898
+ "error": "-ozqdq_error",
899
+ "urlRow": "-ozqdq_urlRow",
900
+ "switch": "-ozqdq_switch",
901
+ "hint": "-ozqdq_hint",
902
+ "titleRow": "-ozqdq_titleRow",
903
+ "clearRow": "-ozqdq_clearRow",
904
+ "previewGlass": "-ozqdq_previewGlass",
905
+ "previewEmpty": "-ozqdq_previewEmpty",
812
906
  "sourceRow": "-ozqdq_sourceRow",
907
+ "urlInput": "-ozqdq_urlInput",
908
+ "segGroup": "-ozqdq_segGroup",
813
909
  "previewScrim": "-ozqdq_previewScrim",
814
- "segBtn": "-ozqdq_segBtn",
815
- "header": "-ozqdq_header",
816
- "clearRow": "-ozqdq_clearRow",
910
+ "previewGlassText": "-ozqdq_previewGlassText",
911
+ "title": "-ozqdq_title",
912
+ "sliderValue": "-ozqdq_sliderValue",
913
+ "sliderLabel": "-ozqdq_sliderLabel",
817
914
  "switchKnob": "-ozqdq_switchKnob",
818
- "switch": "-ozqdq_switch",
819
- "segGroup": "-ozqdq_segGroup",
820
- "segActive": "-ozqdq_segActive",
821
- "segRow": "-ozqdq_segRow",
822
915
  "btn": "-ozqdq_btn",
823
- "previewEmpty": "-ozqdq_previewEmpty",
824
- "previewGlass": "-ozqdq_previewGlass",
825
- "row": "-ozqdq_row",
826
- "titleRow": "-ozqdq_titleRow",
827
- "btnDanger": "-ozqdq_btnDanger",
828
916
  "desc": "-ozqdq_desc",
829
917
  "sliderRow": "-ozqdq_sliderRow",
830
- "segLabel": "-ozqdq_segLabel",
831
- "title": "-ozqdq_title",
832
- "urlRow": "-ozqdq_urlRow",
918
+ "segRow": "-ozqdq_segRow",
919
+ "sectionLabel": "-ozqdq_sectionLabel",
833
920
  "enabledBadge": "-ozqdq_enabledBadge",
834
- "sliderLabel": "-ozqdq_sliderLabel",
835
- "hint": "-ozqdq_hint",
836
- "slider": "-ozqdq_slider",
837
- "urlInput": "-ozqdq_urlInput",
838
- "btnPrimary": "-ozqdq_btnPrimary",
839
- "clearBtn": "-ozqdq_clearBtn",
840
- "error": "-ozqdq_error",
921
+ "segActive": "-ozqdq_segActive",
922
+ "btnDanger": "-ozqdq_btnDanger",
841
923
  "previewImg": "-ozqdq_previewImg",
842
- "sectionLabel": "-ozqdq_sectionLabel",
843
- "section": "-ozqdq_section",
924
+ "segBtn": "-ozqdq_segBtn",
925
+ "clearBtn": "-ozqdq_clearBtn",
844
926
  "preview": "-ozqdq_preview",
845
- "sliderValue": "-ozqdq_sliderValue",
846
- "previewGlassText": "-ozqdq_previewGlassText"
927
+ "row": "-ozqdq_row",
928
+ "segLabel": "-ozqdq_segLabel",
929
+ "header": "-ozqdq_header",
930
+ "slider": "-ozqdq_slider",
931
+ "btnPrimary": "-ozqdq_btnPrimary",
932
+ "section": "-ozqdq_section"
847
933
  };
848
934
  //#endregion
849
935
  //#region src/client/SettingsRow.tsx
@@ -1389,7 +1475,16 @@ window.__ModuleLoader__.load({
1389
1475
  * nav 34px-wide rail, fixed right edge, vertically centered; collapsed
1390
1476
  * blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
1391
1477
  * whose scroll area holds 30px rows (tick indicator + fading-in title);
1392
- * 32px gradient fade veils top/bottom shown while that side clips.
1478
+ * 32px gradient fade veils top/bottom, expanded-only, shown while that
1479
+ * side actually clips.
1480
+ *
1481
+ * Deviations from the reference port, all fidelity fixes:
1482
+ * - The filter bar collapses to zero height while idle instead of staying
1483
+ * in layout invisibly (it pushed every tick ~33px down).
1484
+ * - Short stacks center vertically; overflowing stacks are bottom-pinned
1485
+ * by the component so the newest tick hugs the capsule's bottom edge
1486
+ * (the official idle strategy — the reference port flowed ticks from
1487
+ * the top and clipped the newest away).
1393
1488
  *
1394
1489
  * Glass integration: the official chat.deepseek.com paints (light
1395
1490
  * rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
@@ -1416,6 +1511,9 @@ window.__ModuleLoader__.load({
1416
1511
  const TIMELINE_CSS = `
1417
1512
  .dsbt-nav {
1418
1513
  user-select: none; -webkit-user-select: none;
1514
+ /* Filter-bar chrome height shared by the bar itself and the top veil's
1515
+ expanded offset. */
1516
+ --dsbt-bar-h: 33px;
1419
1517
  z-index: 90; align-items: center; width: 34px;
1420
1518
  height: var(--dsbt-h, 300px);
1421
1519
  transition: right .2s ease, opacity .2s ease;
@@ -1481,7 +1579,10 @@ window.__ModuleLoader__.load({
1481
1579
  /* Content fade veils — the official ::before/::after pair rebuilt as
1482
1580
  elements: a bar of the panel's OWN fill color masked to nothing over
1483
1581
  32px (solid -> mask == gradient-to-transparent, but works with the
1484
- translucent fill). Shown only while that side actually clips. */
1582
+ translucent fill). They belong to the EXPANDED panel only — the idle
1583
+ capsule hard-clips its ticks at the rounded ends like the official
1584
+ strip — and while expanded the top veil starts BELOW the filter bar,
1585
+ not over it. Shown only while that side actually clips. */
1485
1586
  .dsbt-fade {
1486
1587
  z-index: 2; pointer-events: none; opacity: 0;
1487
1588
  background-color: rgba(255, 255, 255, .94);
@@ -1492,15 +1593,25 @@ window.__ModuleLoader__.load({
1492
1593
  }
1493
1594
  body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
1494
1595
  .dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
1495
- .dsbt-fade.dsbt-fade-on { opacity: 1; }
1596
+ .dsbt-wrap.dsbt-show .dsbt-fade.dsbt-fade-on { opacity: 1; }
1597
+ .dsbt-wrap.dsbt-show .dsbt-fade:not(.dsbt-fade-bot) { top: var(--dsbt-bar-h, 33px); }
1496
1598
 
1497
- /* Filter bar — "marked only" toggle pinned above the scroll area. */
1599
+ /* Filter bar — "marked only" toggle pinned above the scroll area while
1600
+ expanded. Collapsed, it collapses to ZERO height (not just opacity): a
1601
+ transparent-but-in-layout bar pushed every tick ~33px down, one of the
1602
+ reasons the idle capsule read as off-alignment. */
1498
1603
  .dsbt-filterbar {
1604
+ height: var(--dsbt-bar-h, 33px);
1499
1605
  padding: 8px 12px 4px; display: flex; justify-content: flex-end;
1500
1606
  align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
1501
- box-sizing: border-box; flex: none; transition: opacity .2s ease;
1607
+ box-sizing: border-box; flex: none; overflow: hidden;
1608
+ transition: opacity .2s ease, height .2s ease, padding .2s ease,
1609
+ border-color .2s ease;
1610
+ }
1611
+ .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar {
1612
+ height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
1613
+ border-bottom-color: transparent;
1502
1614
  }
1503
- .dsbt-wrap:not(.dsbt-show) .dsbt-filterbar { opacity: 0; }
1504
1615
  body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
1505
1616
  .dsbt-filterbtn {
1506
1617
  font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
@@ -1522,13 +1633,19 @@ window.__ModuleLoader__.load({
1522
1633
  background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
1523
1634
  }
1524
1635
 
1525
- /* Scroll area — rows hug the right edge; scrolls only when overfull. */
1636
+ /* Scroll area — rows hug the right edge; scrolls only when overfull. The
1637
+ auto-margin pseudo pair vertically centers the stack when it fits the
1638
+ capsule (short conversations), and resolves to zero free space when it
1639
+ overflows, so bottom-pinned scrolling behaves like a plain column. */
1526
1640
  .dsbt-page {
1527
1641
  padding: 15px 0 15px 24px; box-sizing: border-box;
1528
1642
  overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
1529
1643
  flex-direction: column; align-items: flex-end; display: flex;
1530
1644
  position: relative; width: 100%; overflow: hidden;
1531
1645
  }
1646
+ .dsbt-page::before, .dsbt-page::after {
1647
+ content: ''; flex: none; margin-block: auto;
1648
+ }
1532
1649
  .dsbt-wrap.dsbt-show .dsbt-page {
1533
1650
  overflow-y: auto; overflow-x: hidden;
1534
1651
  scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
@@ -1548,15 +1665,26 @@ window.__ModuleLoader__.load({
1548
1665
 
1549
1666
  /* One message row: 30px, star bookmark + tick indicator at the right,
1550
1667
  title fading in. */
1668
+ /* Row shell: layout + shared ink only — the interactive hit areas are the
1669
+ sibling .dsbt-jump / .dsbt-star buttons below. */
1551
1670
  .dsbt-item {
1552
- cursor: pointer; height: 30px; min-height: 30px; flex-shrink: 0;
1671
+ height: 30px; min-height: 30px; flex-shrink: 0;
1553
1672
  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;
1673
+ width: calc(100% - 4px); margin-right: 4px;
1674
+ display: flex; box-sizing: border-box;
1557
1675
  color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
1558
1676
  transition: color .15s ease;
1559
1677
  }
1678
+
1679
+ /* Jump target: a real button so Enter/Space work natively without nesting
1680
+ an interactive element inside another. */
1681
+ .dsbt-jump {
1682
+ cursor: pointer; background: none; border: none; margin: 0; padding: 0;
1683
+ font: inherit; line-height: 20px; text-align: right; color: inherit;
1684
+ flex: 1 1 auto; min-width: 0; height: 100%;
1685
+ display: flex; align-items: center; justify-content: flex-end;
1686
+ }
1687
+ .dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
1560
1688
  .dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
1561
1689
  body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
1562
1690
  body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
@@ -1569,14 +1697,17 @@ window.__ModuleLoader__.load({
1569
1697
  .dsbt-show .dsbt-title { opacity: 1; }
1570
1698
 
1571
1699
  /* Star bookmark — appears on row hover while the panel is expanded; gold
1572
- when marked. Keyboard-operable (span[role=button]). */
1700
+ when marked. A real sibling button next to the jump target, so marking
1701
+ stays keyboard-operable without nested interactive elements. */
1573
1702
  .dsbt-star {
1574
1703
  font-size: 13px; line-height: 16px; width: 18px; height: 18px;
1575
1704
  display: inline-flex; align-items: center; justify-content: center;
1576
1705
  opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
1577
1706
  flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
1578
1707
  -webkit-user-select: none;
1708
+ background: none; border: none; padding: 0; font-family: inherit;
1579
1709
  }
1710
+ .dsbt-star:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; opacity: .9; }
1580
1711
  .dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
1581
1712
  .dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
1582
1713
  .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
@@ -1629,7 +1760,7 @@ window.__ModuleLoader__.load({
1629
1760
  }
1630
1761
 
1631
1762
  @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; }
1763
+ .dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-jump, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
1633
1764
  }
1634
1765
  `;
1635
1766
  //#endregion
@@ -1656,10 +1787,17 @@ window.__ModuleLoader__.load({
1656
1787
  * - Rewind integration: messages withdrawn by a rewind command node are
1657
1788
  * filtered out of the rail.
1658
1789
  *
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.
1790
+ * Data source, fastest first: the host-side `bgTimeline` session projection
1791
+ * (src/projection.ts — a complete enumeration of the session's user messages
1792
+ * delivered through the projection baseline/push channel) -> the loaded chat
1793
+ * node window. The conversation's own lazy loading is never driven from here;
1794
+ * clicking a row pages older history on demand, scrolls the conversation to
1795
+ * the target row, and keeps the rail's own active row visible.
1796
+ *
1797
+ * Collapsed idle strategy (official ScrollNav behavior): the tick stack is
1798
+ * pinned to the BOTTOM inner edge — the newest question's tick always sits at
1799
+ * the bottom of the capsule, older ticks clip away at the top. Hovering
1800
+ * expands the SAME box and scrolls the active row into view instead.
1663
1801
  *
1664
1802
  * The rail reads the shared settings transport: it renders unless
1665
1803
  * `timeline: false`. Its glass paints are self-contained official values
@@ -1746,13 +1884,30 @@ window.__ModuleLoader__.load({
1746
1884
  * @param chat - a chat snapshot (loosely typed on purpose).
1747
1885
  */
1748
1886
  function hiddenSeqsOfChat(chat) {
1887
+ const hidden = rewindHiddenSeqsOfChat(chat);
1888
+ const spans = rewindSpansOfChat(chat);
1889
+ if (spans.length === 0) return hidden;
1890
+ const nodes = chat.nodes;
1891
+ if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1892
+ for (const node of nodes.values()) {
1893
+ if (node === null || typeof node !== "object") continue;
1894
+ const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
1895
+ if (anchor === void 0) continue;
1896
+ if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
1897
+ }
1898
+ return hidden;
1899
+ }
1900
+ /**
1901
+ * Seq numbers a rewind hid directly: successful commands hide their own row;
1902
+ * preview commands hide their target.
1903
+ * @param chat - a chat snapshot (loosely typed on purpose).
1904
+ */
1905
+ function rewindHiddenSeqsOfChat(chat) {
1749
1906
  const hidden = /* @__PURE__ */ new Set();
1750
1907
  if (chat === null || typeof chat !== "object") return hidden;
1751
1908
  const nodes = chat.nodes;
1752
1909
  if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
1753
- const all = [...nodes.values()];
1754
- const spans = [];
1755
- for (const node of all) {
1910
+ for (const node of nodes.values()) {
1756
1911
  if (node === null || typeof node !== "object") continue;
1757
1912
  if (node.kind !== "command") continue;
1758
1913
  const command = node.data;
@@ -1764,23 +1919,39 @@ window.__ModuleLoader__.load({
1764
1919
  }
1765
1920
  const outcome = command.outcome;
1766
1921
  if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1922
+ if (typeof command.seq === "number") hidden.add(command.seq);
1923
+ }
1924
+ return hidden;
1925
+ }
1926
+ /**
1927
+ * The [target, sourceEventSeq] spans successful rewind commands cut — the
1928
+ * range form of hiding, needed to filter sources that are NOT chat nodes
1929
+ * (the host projection) by plain seq membership.
1930
+ * @param chat - a chat snapshot (loosely typed on purpose).
1931
+ */
1932
+ function rewindSpansOfChat(chat) {
1933
+ const spans = [];
1934
+ if (chat === null || typeof chat !== "object") return spans;
1935
+ const nodes = chat.nodes;
1936
+ if (nodes === void 0 || typeof nodes.values !== "function") return spans;
1937
+ for (const node of nodes.values()) {
1938
+ if (node === null || typeof node !== "object") continue;
1939
+ if (node.kind !== "command") continue;
1940
+ const command = node.data;
1941
+ if (command === null || typeof command !== "object") continue;
1942
+ if (command.name !== "rewind") continue;
1943
+ if (isRewindPreviewCommand(command)) continue;
1944
+ const outcome = command.outcome;
1945
+ if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
1767
1946
  const marker = outcome.sourceEventSeq;
1768
1947
  if (typeof marker !== "number") continue;
1769
- if (typeof command.seq === "number") hidden.add(command.seq);
1770
1948
  const target = rewindTargetOfOutcome(outcome.text);
1771
1949
  if (target !== void 0) spans.push({
1772
1950
  start: target,
1773
1951
  end: marker
1774
1952
  });
1775
1953
  }
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;
1954
+ return spans;
1784
1955
  }
1785
1956
  /**
1786
1957
  * Enumerate user messages from the loaded chat-node snapshot (the client-side
@@ -1816,6 +1987,65 @@ window.__ModuleLoader__.load({
1816
1987
  function resolveAnchorKey(m) {
1817
1988
  return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
1818
1989
  }
1990
+ /** The user-message node definition kind in ui-conversation. */
1991
+ const INPUT_MESSAGE_KIND = "input-message";
1992
+ /**
1993
+ * Rebuild a chat row's DOM anchor key from the durable message id — the same
1994
+ * engine formula as conversationContextKey(kind, id) for the input-message
1995
+ * definition (`13:input-message<id>`), so projected entries can jump even
1996
+ * when their chat node is not loaded yet.
1997
+ * @param id - the durable message id carried by the projection entry.
1998
+ */
1999
+ function inputAnchorKeyOf(id) {
2000
+ return `13:${INPUT_MESSAGE_KIND}${id}`;
2001
+ }
2002
+ /**
2003
+ * Validate one projected wire value into rail entries (defensive: the value
2004
+ * crosses the wire and may be absent, partial, or stale).
2005
+ * @param value - the raw useProjection(TIMELINE_PROJECTION_KEY) snapshot.
2006
+ */
2007
+ function normalizeProjectedTimeline(value) {
2008
+ if (value === null || typeof value !== "object") return [];
2009
+ const raw = value.messages;
2010
+ if (!Array.isArray(raw)) return [];
2011
+ const seen = /* @__PURE__ */ new Set();
2012
+ const out = [];
2013
+ for (const item of raw) {
2014
+ if (item === null || typeof item !== "object") continue;
2015
+ const rec = item;
2016
+ if (typeof rec.seq !== "number" || !Number.isFinite(rec.seq)) continue;
2017
+ if (seen.has(rec.seq)) continue;
2018
+ seen.add(rec.seq);
2019
+ out.push({
2020
+ seq: rec.seq,
2021
+ time: typeof rec.time === "number" ? rec.time : 0,
2022
+ text: typeof rec.text === "string" ? rec.text.slice(0, 80) : "",
2023
+ ...typeof rec.id === "string" && rec.id !== "" ? { key: inputAnchorKeyOf(rec.id) } : {}
2024
+ });
2025
+ }
2026
+ out.sort((a, b) => a.seq - b.seq);
2027
+ return out;
2028
+ }
2029
+ /**
2030
+ * Rail messages with the fastest source first: the host projection covers the
2031
+ * WHOLE session (and already excludes rewind cuts), so it wins whenever it
2032
+ * has anything; otherwise fall back to the loaded chat-node window. The
2033
+ * projection path still applies locally-known rewind hiding so a cut lands
2034
+ * instantly even before the next projection push arrives.
2035
+ * @param snapshot - the session conversation snapshot (node fallback).
2036
+ * @param projected - the raw bgTimeline projection value.
2037
+ */
2038
+ function railMessages(snapshot, projected) {
2039
+ const projectedMessages = normalizeProjectedTimeline(projected);
2040
+ if (projectedMessages.length > 0) {
2041
+ const chat = snapshot?.chat;
2042
+ const hidden = rewindHiddenSeqsOfChat(chat);
2043
+ const spans = rewindSpansOfChat(chat);
2044
+ if (hidden.size === 0 && spans.length === 0) return projectedMessages;
2045
+ return projectedMessages.filter((m) => !hidden.has(m.seq) && !spans.some((span) => m.seq >= span.start && m.seq <= span.end));
2046
+ }
2047
+ return collectMessages(snapshot);
2048
+ }
1819
2049
  /**
1820
2050
  * Outer rail height for a message count: rows + breathing room, clamped so
1821
2051
  * both states always share the exact same height (never jumps on expand).
@@ -1839,7 +2069,7 @@ window.__ModuleLoader__.load({
1839
2069
  if (typeof document !== "undefined") {
1840
2070
  const ctx = document.createElement("canvas").getContext("2d");
1841
2071
  if (ctx) {
1842
- ctx.font = "13px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif";
2072
+ ctx.font = `13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
1843
2073
  for (const t of texts) {
1844
2074
  const w = ctx.measureText(t === "" ? "…" : t).width;
1845
2075
  if (w > widest) widest = w;
@@ -1894,7 +2124,7 @@ window.__ModuleLoader__.load({
1894
2124
  * conversation input dock purely to bind per-session lifecycle.
1895
2125
  */
1896
2126
  function TimelineRail(props) {
1897
- const { sessionId, sessionsService, t } = props;
2127
+ const { sessionId, sessionsService, useProjection, t } = props;
1898
2128
  const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
1899
2129
  const timelineUnknown = settings.status === "loading";
1900
2130
  const timelineDisabled = settings.value?.timeline === false;
@@ -1902,8 +2132,8 @@ window.__ModuleLoader__.load({
1902
2132
  const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
1903
2133
  const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
1904
2134
  const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
1905
- const messages = react.default.useMemo(() => collectMessages(nodeSnapshot), [nodeSnapshot]);
1906
- const [loadedAll, setLoadedAll] = react.default.useState(false);
2135
+ const projected = useProjection !== void 0 ? useProjection(TIMELINE_PROJECTION_KEY) : void 0;
2136
+ const messages = react.default.useMemo(() => railMessages(nodeSnapshot, projected), [nodeSnapshot, projected]);
1907
2137
  const [activeIndex, setActiveIndex] = react.default.useState(-1);
1908
2138
  const [show, setShow] = react.default.useState(false);
1909
2139
  const [rightOffset, setRightOffset] = react.default.useState(16);
@@ -1944,40 +2174,8 @@ window.__ModuleLoader__.load({
1944
2174
  return () => mql.removeEventListener("change", onChange);
1945
2175
  }, []);
1946
2176
  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
2177
  setRailWidth(railWidthFor(messages.map((m) => m.text)));
1980
- }, [loadedAll, messages]);
2178
+ }, [messages]);
1981
2179
  react.default.useEffect(() => {
1982
2180
  if (messages.length === 0) return;
1983
2181
  const indexByKey = /* @__PURE__ */ new Map();
@@ -1985,17 +2183,21 @@ window.__ModuleLoader__.load({
1985
2183
  const key = resolveAnchorKey(m);
1986
2184
  if (key !== void 0) indexByKey.set(key, i);
1987
2185
  });
2186
+ const sp = document.querySelector("[data-conversation-scroll]");
2187
+ if (sp === null) return;
2188
+ const rows = sp.querySelectorAll("[data-chat-anchor-key]");
2189
+ let settleTimer;
2190
+ let frame = 0;
2191
+ const nearRows = /* @__PURE__ */ new Set();
1988
2192
  const updateActive = () => {
1989
2193
  if (jumpPendingRef.current) return;
1990
- const sp = document.querySelector("[data-conversation-scroll]");
1991
- if (sp === null) return;
1992
2194
  const rect = sp.getBoundingClientRect();
1993
2195
  if (rect.height === 0) return;
2196
+ const candidates = nearRows.size > 0 ? nearRows : rows;
1994
2197
  const line = rect.top + rect.height * .4;
1995
- const rows = sp.querySelectorAll("[data-chat-anchor-key]");
1996
2198
  let best = -1;
1997
2199
  let bestDist = Infinity;
1998
- for (const row of rows) {
2200
+ for (const row of candidates) {
1999
2201
  const key = row.getAttribute("data-chat-anchor-key");
2000
2202
  if (key === null) continue;
2001
2203
  const idx = indexByKey.get(key) ?? -1;
@@ -2009,10 +2211,22 @@ window.__ModuleLoader__.load({
2009
2211
  }
2010
2212
  setActiveIndex(best);
2011
2213
  };
2012
- updateActive();
2013
- const el = document.querySelector("[data-conversation-scroll]");
2014
- let timer;
2015
- let settleTimer;
2214
+ const scheduleMeasure = () => {
2215
+ if (frame !== 0) return;
2216
+ frame = window.requestAnimationFrame(() => {
2217
+ frame = 0;
2218
+ updateActive();
2219
+ });
2220
+ };
2221
+ let io;
2222
+ if (typeof IntersectionObserver === "function") {
2223
+ io = new IntersectionObserver((entries) => {
2224
+ for (const entry of entries) if (entry.isIntersecting) nearRows.add(entry.target);
2225
+ else nearRows.delete(entry.target);
2226
+ scheduleMeasure();
2227
+ }, { root: sp });
2228
+ rows.forEach((row) => io?.observe(row));
2229
+ }
2016
2230
  const onScroll = () => {
2017
2231
  if (settleTimer !== void 0) clearTimeout(settleTimer);
2018
2232
  settleTimer = window.setTimeout(() => {
@@ -2026,22 +2240,20 @@ window.__ModuleLoader__.load({
2026
2240
  updateActive();
2027
2241
  }
2028
2242
  }, 150);
2029
- if (timer !== void 0) return;
2030
- timer = window.setTimeout(() => {
2031
- timer = void 0;
2032
- updateActive();
2033
- }, 60);
2243
+ scheduleMeasure();
2034
2244
  };
2035
- el?.addEventListener("scroll", onScroll, { passive: true });
2036
- const interval = window.setInterval(updateActive, 2e3);
2245
+ sp.addEventListener("scroll", onScroll, { passive: true });
2246
+ const interval = window.setInterval(scheduleMeasure, 2e3);
2247
+ scheduleMeasure();
2037
2248
  return () => {
2038
- if (timer !== void 0) clearTimeout(timer);
2249
+ io?.disconnect();
2250
+ if (frame !== 0) cancelAnimationFrame(frame);
2039
2251
  if (settleTimer !== void 0) clearTimeout(settleTimer);
2040
2252
  if (jumpFallbackRef.current !== void 0) {
2041
2253
  clearTimeout(jumpFallbackRef.current);
2042
2254
  jumpFallbackRef.current = void 0;
2043
2255
  }
2044
- el?.removeEventListener("scroll", onScroll);
2256
+ sp.removeEventListener("scroll", onScroll);
2045
2257
  clearInterval(interval);
2046
2258
  };
2047
2259
  }, [messages]);
@@ -2065,7 +2277,6 @@ window.__ModuleLoader__.load({
2065
2277
  syncFades,
2066
2278
  show,
2067
2279
  messages.length,
2068
- loadedAll,
2069
2280
  marksOnly
2070
2281
  ]);
2071
2282
  react.default.useLayoutEffect(() => {
@@ -2082,6 +2293,18 @@ window.__ModuleLoader__.load({
2082
2293
  show,
2083
2294
  syncFades
2084
2295
  ]);
2296
+ const height = railHeightFor(messages.length);
2297
+ react.default.useLayoutEffect(() => {
2298
+ if (show) return;
2299
+ const page = pageRef.current;
2300
+ if (page === null) return;
2301
+ page.scrollTop = page.scrollHeight;
2302
+ }, [
2303
+ show,
2304
+ messages.length,
2305
+ marksOnly,
2306
+ height
2307
+ ]);
2085
2308
  react.default.useEffect(() => {
2086
2309
  let raf = 0;
2087
2310
  const measure = () => {
@@ -2109,8 +2332,7 @@ window.__ModuleLoader__.load({
2109
2332
  clearInterval(guard);
2110
2333
  };
2111
2334
  }, []);
2112
- if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length < 2) return null;
2113
- const height = railHeightFor(messages.length);
2335
+ if (timelineUnknown || timelineDisabled || sessionId === void 0 || sessionsService === void 0 || isNarrow || hidden || messages.length === 0) return null;
2114
2336
  const activeKey = activeIndex >= 0 && activeIndex < messages.length ? markKeyOf(messages[activeIndex]) : "";
2115
2337
  const displayMessages = marksOnly ? messages.filter((m) => markedSet.has(markKeyOf(m))) : messages;
2116
2338
  return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2157,51 +2379,37 @@ window.__ModuleLoader__.load({
2157
2379
  const mk = markKeyOf(m);
2158
2380
  const marked = markedSet.has(mk);
2159
2381
  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
- },
2382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2383
+ className: `dsbt-item${isActive ? " dsbt-active" : ""}${marked ? " dsbt-marked" : ""}`,
2179
2384
  children: [
2180
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2181
- className: "dsbt-title",
2182
- children: m.text === "" ? t("timeline.noText") : m.text
2385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2386
+ type: "button",
2387
+ className: "dsbt-jump",
2388
+ ref: isActive ? activeItemRef : void 0,
2389
+ title: `${marked ? "★ " : ""}${m.text === "" ? t("timeline.noText") : m.text}`,
2390
+ "aria-label": `${marked ? "★ " : ""}${t("timeline.roleUser")}: ${m.text.slice(0, 60) || t("timeline.noText")}`,
2391
+ "aria-current": isActive ? "location" : void 0,
2392
+ onClick: () => {
2393
+ if (key === void 0) return;
2394
+ jumpPendingRef.current = true;
2395
+ if (jumpFallbackRef.current !== void 0) clearTimeout(jumpFallbackRef.current);
2396
+ jumpFallbackRef.current = window.setTimeout(() => {
2397
+ jumpFallbackRef.current = void 0;
2398
+ jumpPendingRef.current = false;
2399
+ }, 800);
2400
+ jumpToMessage(sessionsService, sessionId, key).catch(() => {});
2401
+ },
2402
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2403
+ className: "dsbt-title",
2404
+ children: m.text === "" ? t("timeline.noText") : m.text
2405
+ })
2183
2406
  }),
2184
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2407
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2408
+ type: "button",
2185
2409
  className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
2186
- role: "button",
2187
- tabIndex: 0,
2188
2410
  "aria-pressed": marked,
2189
2411
  "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
- },
2412
+ onClick: () => toggleMark(m),
2205
2413
  children: "★"
2206
2414
  }),
2207
2415
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {