deepseek-harness-background 0.5.3 → 0.5.5

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
@@ -161,19 +161,13 @@ window.__ModuleLoader__.load({
161
161
 
162
162
  /* The sidebar build badge paints with the label-ink token — a TEXT token we
163
163
  must not override globally — so it gets a theme-aware translucent ink. */
164
- body[data-dsh-bg] [class*="_buildRevision"] {
164
+ body[data-dsh-bg] [class*="_buildVersion"] {
165
165
  background-color: rgba(15, 17, 21, 0.62);
166
166
  }
167
- body[data-ds-dark-theme][data-dsh-bg] [class*="_buildRevision"] {
167
+ body[data-ds-dark-theme][data-dsh-bg] [class*="_buildVersion"] {
168
168
  background-color: rgba(249, 250, 251, 0.62);
169
169
  }
170
170
 
171
- /* The empty-state hero glow asset would wash out the wallpaper behind the
172
- welcome message — dim it so the art stays visible. */
173
- body[data-dsh-bg] [class*="_heroGlow"] {
174
- opacity: 0.2;
175
- }
176
-
177
171
  /* ---- Whitelisted chrome buttons --------------------------------------
178
172
  These three buttons' official fills ride button-* tokens shared with
179
173
  unrelated surfaces, so instead of overriding the tokens they get explicit
@@ -279,7 +273,7 @@ window.__ModuleLoader__.load({
279
273
  /* Sidebar build badge: translucent ink already, but it sat directly on
280
274
  the wallpaper without frost. Join the shared blur chain under the same
281
275
  gate (its theme-aware ink fills from the active block above). */
282
- body[data-dsh-bg-glass] [class*="_buildRevision"] {
276
+ body[data-dsh-bg-glass] [class*="_buildVersion"] {
283
277
  -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);
284
278
  backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
285
279
  }
@@ -961,7 +955,9 @@ window.__ModuleLoader__.load({
961
955
  "background.uploadFailed": "上传失败,请重试",
962
956
  "background.saveFailed": "保存失败,请重试",
963
957
  "background.timelineEnhance": "会话时间线增强",
964
- "background.timelineEnhanceNarrowHint": "为官方会话时间线的已加载轮次补上平滑跳转"
958
+ "background.timelineEnhanceNarrowHint": "为官方会话时间线的已加载轮次补上平滑跳转",
959
+ "background.migratedNotice": "已从旧版 dsh 的设置中恢复你的背景设置",
960
+ "background.migratedDismiss": "知道了"
965
961
  };
966
962
  /** English copy. */
967
963
  const en = {
@@ -990,7 +986,9 @@ window.__ModuleLoader__.load({
990
986
  "background.uploadFailed": "Upload failed, try again",
991
987
  "background.saveFailed": "Save failed, try again",
992
988
  "background.timelineEnhance": "Conversation timeline enhancement",
993
- "background.timelineEnhanceNarrowHint": "Adds smooth jumps for the rail's loaded turns"
989
+ "background.timelineEnhanceNarrowHint": "Adds smooth jumps for the rail's loaded turns",
990
+ "background.migratedNotice": "Restored your background settings from the previous dsh version",
991
+ "background.migratedDismiss": "Got it"
994
992
  };
995
993
  //#endregion
996
994
  //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
@@ -1046,7 +1044,8 @@ window.__ModuleLoader__.load({
1046
1044
  if (!body.ok || body.value === void 0) throw new Error("unexpected payload");
1047
1045
  this.snapshot = {
1048
1046
  status: "ready",
1049
- value: body.value
1047
+ value: body.value,
1048
+ ...body.migrated === void 0 ? {} : { migrated: body.migrated }
1050
1049
  };
1051
1050
  } catch {
1052
1051
  this.snapshot = { status: "error" };
@@ -1075,7 +1074,8 @@ window.__ModuleLoader__.load({
1075
1074
  if (seq !== this.saveSeq) return "superseded";
1076
1075
  this.snapshot = {
1077
1076
  status: "ready",
1078
- value: body.value
1077
+ value: body.value,
1078
+ ...this.snapshot.migrated === void 0 ? {} : { migrated: this.snapshot.migrated }
1079
1079
  };
1080
1080
  this.notify();
1081
1081
  return "ok";
@@ -1115,7 +1115,7 @@ window.__ModuleLoader__.load({
1115
1115
  const settingsClient = new SettingsClient();
1116
1116
  //#endregion
1117
1117
  //#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
1118
- 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}}";
1118
+ 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_notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;justify-content:space-between;align-items:center;gap:12px;padding:8px 12px;display:flex}.-ozqdq_noticeText{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.-ozqdq_noticeBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-size:12px;line-height:18px;text-decoration:underline}.-ozqdq_noticeBtn:hover{color:var(--dsw-alias-label-primary)}.-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}}";
1119
1119
  const tagId = "deepseek-harness-background/SettingsRow.module.css";
1120
1120
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1121
1121
  const tag = document.createElement("style");
@@ -1125,41 +1125,44 @@ window.__ModuleLoader__.load({
1125
1125
  document.head.appendChild(tag);
1126
1126
  }
1127
1127
  var SettingsRow_module_css_default = {
1128
- "clearBtn": "-ozqdq_clearBtn",
1128
+ "previewGlassText": "-ozqdq_previewGlassText",
1129
+ "title": "-ozqdq_title",
1130
+ "switchKnob": "-ozqdq_switchKnob",
1131
+ "sliderValue": "-ozqdq_sliderValue",
1132
+ "error": "-ozqdq_error",
1133
+ "urlRow": "-ozqdq_urlRow",
1134
+ "btn": "-ozqdq_btn",
1135
+ "sliderLabel": "-ozqdq_sliderLabel",
1129
1136
  "slider": "-ozqdq_slider",
1130
- "sourceRow": "-ozqdq_sourceRow",
1137
+ "clearBtn": "-ozqdq_clearBtn",
1131
1138
  "preview": "-ozqdq_preview",
1132
- "switch": "-ozqdq_switch",
1133
- "segGroup": "-ozqdq_segGroup",
1134
- "btnDanger": "-ozqdq_btnDanger",
1135
- "previewEmpty": "-ozqdq_previewEmpty",
1139
+ "urlInput": "-ozqdq_urlInput",
1140
+ "segRow": "-ozqdq_segRow",
1141
+ "enabledBadge": "-ozqdq_enabledBadge",
1136
1142
  "titleRow": "-ozqdq_titleRow",
1137
- "sliderRow": "-ozqdq_sliderRow",
1138
- "sliderValue": "-ozqdq_sliderValue",
1143
+ "sectionLabel": "-ozqdq_sectionLabel",
1144
+ "sourceRow": "-ozqdq_sourceRow",
1139
1145
  "segBtn": "-ozqdq_segBtn",
1140
- "previewGlass": "-ozqdq_previewGlass",
1141
1146
  "row": "-ozqdq_row",
1142
- "error": "-ozqdq_error",
1143
- "previewScrim": "-ozqdq_previewScrim",
1144
- "switchKnob": "-ozqdq_switchKnob",
1145
- "urlInput": "-ozqdq_urlInput",
1146
- "btnPrimary": "-ozqdq_btnPrimary",
1147
- "segActive": "-ozqdq_segActive",
1148
- "segLabel": "-ozqdq_segLabel",
1149
- "urlRow": "-ozqdq_urlRow",
1150
- "previewGlassText": "-ozqdq_previewGlassText",
1151
- "sectionLabel": "-ozqdq_sectionLabel",
1152
- "enabledBadge": "-ozqdq_enabledBadge",
1153
- "segRow": "-ozqdq_segRow",
1147
+ "noticeBtn": "-ozqdq_noticeBtn",
1154
1148
  "previewImg": "-ozqdq_previewImg",
1155
- "btn": "-ozqdq_btn",
1156
- "sliderLabel": "-ozqdq_sliderLabel",
1149
+ "switch": "-ozqdq_switch",
1150
+ "btnDanger": "-ozqdq_btnDanger",
1151
+ "segLabel": "-ozqdq_segLabel",
1152
+ "previewGlass": "-ozqdq_previewGlass",
1153
+ "previewEmpty": "-ozqdq_previewEmpty",
1154
+ "noticeText": "-ozqdq_noticeText",
1157
1155
  "header": "-ozqdq_header",
1158
- "clearRow": "-ozqdq_clearRow",
1159
- "desc": "-ozqdq_desc",
1160
1156
  "section": "-ozqdq_section",
1157
+ "previewScrim": "-ozqdq_previewScrim",
1158
+ "desc": "-ozqdq_desc",
1159
+ "notice": "-ozqdq_notice",
1160
+ "sliderRow": "-ozqdq_sliderRow",
1161
+ "segGroup": "-ozqdq_segGroup",
1161
1162
  "hint": "-ozqdq_hint",
1162
- "title": "-ozqdq_title"
1163
+ "btnPrimary": "-ozqdq_btnPrimary",
1164
+ "segActive": "-ozqdq_segActive",
1165
+ "clearRow": "-ozqdq_clearRow"
1163
1166
  };
1164
1167
  //#endregion
1165
1168
  //#region src/client/SettingsRow.tsx
@@ -1331,6 +1334,7 @@ window.__ModuleLoader__.load({
1331
1334
  const [uploading, setUploading] = (0, react.useState)(false);
1332
1335
  const [urlText, setUrlText] = (0, react.useState)("");
1333
1336
  const [error, setError] = (0, react.useState)("");
1337
+ const [noticeDismissed, setNoticeDismissed] = (0, react.useState)(false);
1334
1338
  const fileInput = (0, react.useRef)(null);
1335
1339
  const previewRef = (0, react.useRef)(null);
1336
1340
  const draftRef = (0, react.useRef)(DEFAULTS);
@@ -1513,6 +1517,19 @@ window.__ModuleLoader__.load({
1513
1517
  children: t("background.description")
1514
1518
  })]
1515
1519
  }),
1520
+ snapshot.migrated !== void 0 && !noticeDismissed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1521
+ className: SettingsRow_module_css_default.notice,
1522
+ role: "status",
1523
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1524
+ className: SettingsRow_module_css_default.noticeText,
1525
+ children: t("background.migratedNotice")
1526
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1527
+ type: "button",
1528
+ className: SettingsRow_module_css_default.noticeBtn,
1529
+ onClick: () => setNoticeDismissed(true),
1530
+ children: t("background.migratedDismiss")
1531
+ })]
1532
+ }),
1516
1533
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1517
1534
  className: SettingsRow_module_css_default.preview,
1518
1535
  ref: previewRef,
@@ -1695,123 +1712,445 @@ window.__ModuleLoader__.load({
1695
1712
  ]
1696
1713
  });
1697
1714
  }
1698
- //#endregion
1699
- //#region src/client/timeline/jump.ts
1700
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1701
- /** How long one jump may keep paging older history before giving up. */
1702
- const JUMP_PAGE_DEADLINE_MS = 3e4;
1703
- /** How long to wait for the chat view to commit the target row. */
1704
- const JUMP_ROW_WAIT_MS = 3e3;
1705
- /** Settle between consecutive pages so a resolver without loadingOlder cannot hot-spin the loop. */
1706
- const PAGE_COOLDOWN_MS = 50;
1707
- /** Distance from the scrollport top the official ChatView lands a jumped row at. */
1708
- const OFFICIAL_LANDING_INSET_PX = 24;
1709
- /** Distances under this snap instead of animating. */
1710
- const MIN_GLIDE_DISTANCE_PX = 8;
1711
- /** Glide duration floor / cap — a far-boundary jump stays a tangible slide. */
1712
- const MIN_GLIDE_MS = 280;
1713
- const MAX_GLIDE_MS = 900;
1714
- /** Every mounted conversation scrollport (multi-column layouts have several). */
1715
- function conversationScrollports() {
1716
- return Array.from(document.querySelectorAll("[data-conversation-scroll]"));
1717
- }
1718
1715
  /**
1719
- * Whether the target chat row is committed in ANY mounted conversation
1720
- * scrollport. Kernel generations disagree on where the node table lives
1721
- * (0.1.1: `SessionSnapshot.chat.nodes`; 0.1.2+: the `useChat` ChatSnapshot
1722
- * node store, unreachable from `session.getSnapshot()` — its SessionSnapshot
1723
- * faces no node table at all), but the rendered DOM is the ground truth both
1724
- * share, so a jump probes it FIRST before deciding whether paging is needed.
1725
- * @param anchorKey - chat anchor key of the target row.
1716
+ * The rail's own inset, read off the custom property the official component
1717
+ * publishes (`--turn-rail-inset`). Falls back to the official constant.
1718
+ * @param rail - the rail element (or null).
1726
1719
  */
1727
- function rowRenderedInDom(anchorKey) {
1728
- const selector = `[data-chat-anchor-key="${CSS.escape(anchorKey)}"]`;
1729
- for (const scrollport of conversationScrollports()) if (scrollport.querySelector(selector) !== null) return true;
1730
- return false;
1720
+ function railInsetOf(rail) {
1721
+ if (rail === null) return 6;
1722
+ const px = Number.parseFloat(rail.style.getPropertyValue("--turn-rail-inset"));
1723
+ return Number.isFinite(px) && px >= 0 ? px : 6;
1731
1724
  }
1732
1725
  /**
1733
- * Poll the conversation DOM until the target row is committed. The session
1734
- * store updates BEFORE React renders the prepended page, so the old one-shot
1735
- * query right after loadOlder() resolved could miss the row and silently
1736
- * give up — the reported "click does nothing until the conversation is
1737
- * nudged (scroll/click)" behavior.
1738
- * @param key - chat anchor key to look for.
1739
- * @param timeoutMs - polling budget.
1726
+ * Whether `rail` is the frame-style rail (dsh >= 0.1.2-alpha.3): marks sit
1727
+ * at a fixed pitch and scroll INSIDE the frame, whose scroller offset the
1728
+ * component publishes inline as `--turn-scroll-top` — every render, resting
1729
+ * at `0px` included. The compressing alpha.1/2 rail never publishes that
1730
+ * property, so its presence is the generation test. The legacy port paints
1731
+ * its own `--dsbt-*` properties and can never trip this.
1732
+ * @param rail - the rail element (or null).
1740
1733
  */
1741
- async function waitForChatRow(key, timeoutMs) {
1742
- const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
1743
- const deadline = Date.now() + timeoutMs;
1744
- for (;;) {
1745
- for (const scrollport of conversationScrollports()) {
1746
- const row = scrollport.querySelector(selector);
1747
- if (row !== null) return row;
1748
- }
1749
- if (Date.now() >= deadline) return null;
1750
- await delay(40);
1751
- }
1752
- }
1753
- /** Maximum scrollable offset of a conversation scrollport. */
1754
- function scrollFloorOf(el) {
1755
- return Math.max(0, el.scrollHeight - el.clientHeight);
1734
+ function isFrameRail(rail) {
1735
+ return rail !== null && rail.style.getPropertyValue("--turn-scroll-top") !== "";
1756
1736
  }
1757
1737
  /**
1758
- * Destination scrollTop that centers `row` inside `el` — the same geometry
1759
- * as scrollIntoView({ block: 'center' }), computed directly so the glide owns
1760
- * the scroll position instead of the browser.
1761
- * @param el - the conversation scrollport.
1762
- * @param row - the target chat row.
1738
+ * Resolve the entry under a pointer Y on the frame-style rail — the official
1739
+ * alpha.3 geometry verbatim (`itemAtPointer` in that generation's
1740
+ * TurnNavigator.tsx): marks live in the scrolled ladder at a fixed 10px
1741
+ * pitch, so the click offset adds the scroller offset. Read off the published
1742
+ * `--turn-scroll-top` var rather than the scroller element itself — the same
1743
+ * metric the official preview positioning uses, so any one-frame render lag
1744
+ * affects both identically.
1745
+ * @param count - number of entries the rail shows.
1746
+ * @param rail - the rail element.
1747
+ * @param clientY - pointer viewport Y.
1763
1748
  */
1764
- function centeredScrollTopFor(el, row) {
1765
- const floor = scrollFloorOf(el);
1766
- const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
1767
- if (!Number.isFinite(target)) return el.scrollTop;
1768
- return Math.max(0, Math.min(floor, target));
1749
+ function frameIndexAtPointer(count, rail, clientY) {
1750
+ const rect = rail.getBoundingClientRect();
1751
+ const inset = railInsetOf(rail);
1752
+ const raw = Number.parseFloat(rail.style.getPropertyValue("--turn-scroll-top"));
1753
+ const scrollTop = Number.isFinite(raw) ? raw : 0;
1754
+ const offset = clientY - rect.top + scrollTop - inset;
1755
+ return Math.max(0, Math.min(count - 1, Math.round(offset / 10)));
1769
1756
  }
1770
1757
  /**
1771
- * Destination scrollTop the official ChatView uses for a navigation jump:
1772
- * the row's top minus a 24px inset. Kept as a named resolver so the enhancer
1773
- * lands exactly where the official would — only animated instead of assigned.
1774
- * @param el - the conversation scrollport.
1775
- * @param row - the target chat row.
1758
+ * Resolve the gesture that produced a rail click to an entry index.
1759
+ *
1760
+ * Two rail generations answer here, told apart by capability:
1761
+ *
1762
+ * - The virtualized rail (dsh 0.1.7+) tags every RENDERED mark with its own
1763
+ * ladder index (`data-index`), so the gesture resolves by data. This is the
1764
+ * robust path: the virtualizer mounts only a window of marks, and their DOM
1765
+ * order is not the ladder order.
1766
+ * - The frame-style rail tags nothing and paints marks with
1767
+ * `pointer-events: none`, so a MOUSE gesture arrives at the `<nav>` with
1768
+ * coordinates and resolves through the rail geometry over the published
1769
+ * `--turn-scroll-top`; a KEYBOARD gesture activates the focused mark button,
1770
+ * recovered by its position among its siblings.
1771
+ *
1772
+ * Returns -1 when neither applies (a keyboard target outside the rail, or a
1773
+ * rail of an unsupported generation, which the kernel's own handler then owns).
1774
+ *
1775
+ * Assumption: the mark ticks are the ONLY buttons under the rail — the
1776
+ * official rail renders marks exclusively and its preview tooltip carries
1777
+ * none. A future non-mark control added inside the rail would shift the
1778
+ * button index and must revisit this.
1779
+ *
1780
+ * @param rail - the rail element.
1781
+ * @param event - the click event.
1776
1782
  */
1777
- function officialTargetTopFor(el, row) {
1778
- const floor = scrollFloorOf(el);
1779
- const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - OFFICIAL_LANDING_INSET_PX;
1780
- if (!Number.isFinite(target)) return el.scrollTop;
1781
- return Math.max(0, Math.min(floor, target));
1783
+ function indexForEvent(rail, event) {
1784
+ const target = event.target;
1785
+ if (target instanceof Element) {
1786
+ const mark = target.closest("[data-index]");
1787
+ if (mark !== null && rail.contains(mark)) {
1788
+ const tagged = Number(mark.getAttribute("data-index"));
1789
+ if (Number.isSafeInteger(tagged) && tagged >= 0) return tagged;
1790
+ }
1791
+ const button = target.closest("button");
1792
+ if (button !== null && rail.contains(button)) {
1793
+ const index = Array.from(rail.querySelectorAll("button")).indexOf(button);
1794
+ if (index >= 0) return index;
1795
+ }
1796
+ }
1797
+ const count = rail.querySelectorAll("button").length;
1798
+ if (count === 0) return -1;
1799
+ return isFrameRail(rail) ? frameIndexAtPointer(count, rail, event.clientY) : -1;
1782
1800
  }
1783
1801
  /**
1784
- * Glide duration for a distance: longer slides take longer, bounded to
1785
- * [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history jump reads as a deliberate
1786
- * glide, not a sluggish race).
1787
- * @param distance - px to travel.
1802
+ * Validate the official navigation array crossing the slot boundary. The
1803
+ * value is produced by the kernel and may be absent, partial, or from an
1804
+ * older build, so every field is checked before use.
1805
+ * @param value - the raw `useChat(s => s.navigation.items())` result.
1788
1806
  */
1789
- function glideDurationFor(distance) {
1790
- if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
1791
- return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
1807
+ function normalizeNavigationItems(value) {
1808
+ if (!Array.isArray(value)) return [];
1809
+ const out = [];
1810
+ const seen = /* @__PURE__ */ new Set();
1811
+ for (const item of value) {
1812
+ if (item === null || typeof item !== "object") continue;
1813
+ const rec = item;
1814
+ if (typeof rec.turn !== "number" || !Number.isFinite(rec.turn)) continue;
1815
+ if (typeof rec.anchorKey !== "string" || rec.anchorKey === "") continue;
1816
+ if (seen.has(rec.turn)) continue;
1817
+ seen.add(rec.turn);
1818
+ out.push({
1819
+ turn: rec.turn,
1820
+ anchorKey: rec.anchorKey,
1821
+ prompt: typeof rec.prompt === "string" ? rec.prompt : "",
1822
+ response: typeof rec.response === "string" ? rec.response : ""
1823
+ });
1824
+ }
1825
+ return out;
1792
1826
  }
1793
1827
  /**
1794
- * Detach the host's bottom-follow state when the reader currently sits inside
1795
- * its follow zone: assign the scroll position just outside the boundary. That
1796
- * one synchronous scroll event settles at-bottom to false (moved-by-reader +
1797
- * outside the zone) so a tip-moved re-render cannot yank the glide back to
1798
- * the floor. No-op when already above the zone or when the assignment cannot
1799
- * move (content shorter than the viewport).
1800
- * @param el - the conversation scrollport.
1801
- * @param followZonePx - the host's own follow threshold.
1828
+ * Validate one `turnOutline` wire value into its entries. The value is the
1829
+ * host projection's wire view (an array of `{ turn, seq, prompt, response }`
1830
+ * — see `dsh-session-turn-outline`) read through `useProjection`, so it may
1831
+ * be absent (pre-baseline, or an older kernel that never registers the key —
1832
+ * which reads `undefined`), partial, or damaged. `turn` and `seq` are the
1833
+ * load-bearing fields — a mark cannot exist or page without them — and a
1834
+ * damaged one drops the entry; the previews are decorative and degrade to ''.
1835
+ * @param value - the raw `useProjection('turnOutline')` value.
1802
1836
  */
1803
- function detachBottomFollow(el, followZonePx = 24) {
1804
- const floor = scrollFloorOf(el);
1805
- if (el.scrollTop < floor - followZonePx) return false;
1806
- const detached = Math.max(0, floor - followZonePx - 1);
1807
- if (Math.abs(el.scrollTop - detached) < 1) return false;
1808
- el.scrollTop = detached;
1809
- return true;
1837
+ function normalizeOutlineItems(value) {
1838
+ if (!Array.isArray(value)) return [];
1839
+ const out = [];
1840
+ for (const item of value) {
1841
+ if (item === null || typeof item !== "object") continue;
1842
+ const rec = item;
1843
+ if (typeof rec.turn !== "number" || !Number.isSafeInteger(rec.turn) || rec.turn < 0) continue;
1844
+ if (typeof rec.seq !== "number" || !Number.isSafeInteger(rec.seq) || rec.seq < 0) continue;
1845
+ out.push({
1846
+ turn: rec.turn,
1847
+ seq: rec.seq,
1848
+ prompt: typeof rec.prompt === "string" ? rec.prompt : "",
1849
+ response: typeof rec.response === "string" ? rec.response : ""
1850
+ });
1851
+ }
1852
+ return out;
1810
1853
  }
1811
1854
  /**
1812
- * Wait until a detach assignment's scroll event has been dispatched AND
1813
- * consumed by the host's scroll handler: scroll events fire on a later frame
1814
- * than the assignment, and a paging commit's layout effect runs before that
1855
+ * Rebuild the rail's full mark ladder exactly as the official ChatView does
1856
+ * (a mirror of `mergeTurnRailItems` in dsh-client-ui-chat): outline entries
1857
+ * seed every known turn as unloaded, loaded items then override with their
1858
+ * anchor key and richer previews, ascending by turn. The enhancer maps a
1859
+ * click to a mark by INDEX, so its ladder must match the RENDERED marks —
1860
+ * which are exactly this merge on an alpha.3 kernel; without the outline
1861
+ * value the merge degrades to the loaded items (the alpha.1/2 ladder).
1862
+ * @param loaded - the validated loaded-window items.
1863
+ * @param outline - the raw turnOutline wire value.
1864
+ */
1865
+ function mergeRailItems(loaded, outline) {
1866
+ const byTurn = /* @__PURE__ */ new Map();
1867
+ for (const entry of normalizeOutlineItems(outline)) byTurn.set(entry.turn, {
1868
+ turn: entry.turn,
1869
+ prompt: entry.prompt,
1870
+ response: entry.response
1871
+ });
1872
+ for (const item of loaded) {
1873
+ const preview = byTurn.get(item.turn);
1874
+ byTurn.set(item.turn, {
1875
+ turn: item.turn,
1876
+ prompt: item.prompt !== "" ? item.prompt : preview?.prompt ?? "",
1877
+ response: item.response !== "" ? item.response : preview?.response ?? "",
1878
+ anchorKey: item.anchorKey
1879
+ });
1880
+ }
1881
+ if (byTurn.size === 0) return [];
1882
+ return [...byTurn.values()].sort((left, right) => left.turn - right.turn);
1883
+ }
1884
+ //#endregion
1885
+ //#region src/client/timeline/official-enhance.tsx
1886
+ /**
1887
+ * Official-rail enhancer — behaviour-only improvements to the kernel's own
1888
+ * turn navigator (dsh >= 0.1.2-rc.1, `TurnNavigator` in dsh-client-ui-chat).
1889
+ *
1890
+ * This component renders NOTHING. It borrows the official rail exactly as the
1891
+ * kernel painted it and fixes its behaviour, scoped to what the running rail
1892
+ * generation still lacks — told apart by capability, never by version compare:
1893
+ *
1894
+ * Smooth jump. The official `navigateToTurn` is one assignment
1895
+ * (`el.scrollTop += flowTop(row, el) - 24`); a click across a long transcript
1896
+ * teleports. Clicks on LOADED marks are intercepted in the capture phase and
1897
+ * re-run through the shared glide instead. Both generations also render marks
1898
+ * for turns OUTSIDE the loaded window (fed by the whole-log `turnOutline`
1899
+ * projection); those jumps page history through the kernel's own loadThrough
1900
+ * machinery — marks without an anchor key are left to it, and the click index
1901
+ * is resolved against the same merged ladder the kernel renders. While one of
1902
+ * those kernel jumps is still paging (a mark pulses `aria-busy`), the plugin
1903
+ * stands down entirely: the kernel's loaded branch cancels its own pending
1904
+ * jump before landing, and an interception would bypass that cancellation.
1905
+ *
1906
+ * Two rail generations are supported, discovered by {@link findOfficialRail}:
1907
+ * the frame-style rail of dsh 0.1.2-rc.1 … 0.1.6 (inline frame metrics, marks
1908
+ * mapped by geometry) and the virtualized rail of dsh 0.1.7+ (no inline
1909
+ * metrics, marks tagged `data-index`, only a window of them mounted at once).
1910
+ *
1911
+ * State-ownership note (why intercepting the official click loses nothing):
1912
+ * the kernel's scrollport (`data-conversation-scroll`, ChatView's
1913
+ * `onScrollRef` handler) treats every unaccounted scrollTop assignment as a
1914
+ * reader move and on each frame (a) flips its own at-bottom machinery,
1915
+ * (b) `chatScroll.save(position)` for view-tab restore, and (c) re-derives
1916
+ * the rail's active mark. The glide animates the real scrollport, so all three
1917
+ * keep converging to the target row while it runs — the only thing the
1918
+ * interception skips is the official handler's synchronous active-mark
1919
+ * assignment, which its own scroll-driven derivation re-settles by the time
1920
+ * the glide lands.
1921
+ *
1922
+ * The jump goes through the shared backend (jump.ts).
1923
+ *
1924
+ * Interception notes: React 18 dispatches `onClick` from a listener on the
1925
+ * root container during the BUBBLE phase. A capture listener on the rail
1926
+ * itself therefore runs first, and `stopImmediatePropagation()` there prevents
1927
+ * the event from ever reaching React's root — the official handler never
1928
+ * fires. The frame-style rail owns pointer input for its whole column (marks
1929
+ * carry `pointer-events: none`); the virtualized rail's marks are real
1930
+ * buttons. Both arrive at the same capture listener on the rail.
1931
+ *
1932
+ * Nothing here touches the official DOM: no nodes are injected into a
1933
+ * React-owned subtree and no official class or paint is overridden.
1934
+ */
1935
+ /**
1936
+ * Find the official turn rail, in either supported generation.
1937
+ *
1938
+ * Identity comes from the SEAT, not from a rendered mark: the rail is the only
1939
+ * `<nav>` inside the conversation scrollport, and dsh 0.1.7's virtualizer
1940
+ * mounts its marks lazily — measured on the real rail, the `<nav>` exists with
1941
+ * an EMPTY marks container until the virtualizer has layout. Requiring a
1942
+ * rendered `button[data-index]` would therefore miss the rail exactly when it
1943
+ * first appears.
1944
+ *
1945
+ * The generation is then told apart by capability where it matters: the click
1946
+ * resolver reads a mark's own `data-index`, and only falls back to the legacy
1947
+ * frame geometry for a rail that publishes `--turn-scroll-top`.
1948
+ * @param root - the tree to search (defaults to the document).
1949
+ * @returns the rail element, or null when the conversation shows none.
1950
+ */
1951
+ function findOfficialRail(root = document) {
1952
+ return root.querySelector("[data-conversation-scroll] nav");
1953
+ }
1954
+ /**
1955
+ * Rails claimed by an enhancer instance, keyed by the owning instance's
1956
+ * claim token. One session-scoped enhancer mounts per session; without a
1957
+ * claim two instances would both attach capture listeners to the FIRST rail
1958
+ * in the document (multi-column layouts) and one of them — not necessarily
1959
+ * the one owning the reader's column — would win the click. The token
1960
+ * distinguishes "already mine" (the poll keeps it) from "someone else's"
1961
+ * (this instance must stand down). Weak: a replaced rail element disappears
1962
+ * as soon as the browser collects it, so remounts need no bookkeeping.
1963
+ */
1964
+ const claimedRails = /* @__PURE__ */ new WeakMap();
1965
+ /** How often the official rail is looked for (it mounts with the chat view). */
1966
+ const RAIL_POLL_MS = 400;
1967
+ /**
1968
+ * The enhancer: no visual output, only the behaviour fixes the running rail
1969
+ * generation lacks.
1970
+ * @param props - dock slot props plus the enabled gate.
1971
+ * @returns null (always).
1972
+ */
1973
+ function OfficialTimelineEnhancer(props) {
1974
+ const { jumpToAnchor, useChat, useProjection, enabled } = props;
1975
+ const ladder = mergeRailItems(normalizeNavigationItems(useChat === void 0 ? void 0 : useChat((snapshot) => {
1976
+ const nav = snapshot?.navigation;
1977
+ return typeof nav?.items === "function" ? nav.items() : void 0;
1978
+ })), useProjection === void 0 ? void 0 : useProjection("turnOutline"));
1979
+ const [rail, setRail] = react.default.useState(null);
1980
+ const ladderRef = react.default.useRef(ladder);
1981
+ ladderRef.current = ladder;
1982
+ const railRef = react.default.useRef(rail);
1983
+ railRef.current = rail;
1984
+ const jumpToAnchorRef = react.default.useRef(jumpToAnchor);
1985
+ jumpToAnchorRef.current = jumpToAnchor;
1986
+ const claimToken = react.default.useMemo(() => Symbol("dsh-bg-timeline-claim"), []);
1987
+ react.default.useEffect(() => {
1988
+ const check = () => {
1989
+ const found = findOfficialRail();
1990
+ if (found === null) {
1991
+ setRail(null);
1992
+ return;
1993
+ }
1994
+ const owner = claimedRails.get(found);
1995
+ if (owner !== void 0 && owner !== claimToken) {
1996
+ setRail(null);
1997
+ return;
1998
+ }
1999
+ claimedRails.set(found, claimToken);
2000
+ setRail(found);
2001
+ };
2002
+ check();
2003
+ const timer = window.setInterval(check, RAIL_POLL_MS);
2004
+ return () => {
2005
+ window.clearInterval(timer);
2006
+ if (railRef.current !== null) claimedRails.delete(railRef.current);
2007
+ };
2008
+ }, [claimToken]);
2009
+ const navigate = react.default.useCallback((item) => {
2010
+ if (item.anchorKey === void 0) return;
2011
+ const jump = jumpToAnchorRef.current;
2012
+ if (jump === void 0) return;
2013
+ jump(item.anchorKey).catch(() => {});
2014
+ }, []);
2015
+ react.default.useEffect(() => {
2016
+ if (!enabled || rail === null) return;
2017
+ const onClick = (event) => {
2018
+ if (rail.querySelector("[aria-busy=\"true\"]") !== null) return;
2019
+ const index = indexForEvent(rail, event);
2020
+ const item = ladderRef.current[index];
2021
+ if (item === void 0 || item.anchorKey === void 0) return;
2022
+ event.preventDefault();
2023
+ event.stopImmediatePropagation();
2024
+ navigate(item);
2025
+ };
2026
+ rail.addEventListener("click", onClick, true);
2027
+ return () => {
2028
+ rail.removeEventListener("click", onClick, true);
2029
+ };
2030
+ }, [
2031
+ enabled,
2032
+ rail,
2033
+ navigate
2034
+ ]);
2035
+ return null;
2036
+ }
2037
+ //#endregion
2038
+ //#region src/client/timeline/jump.ts
2039
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2040
+ /** How long one jump may keep paging older history before giving up. */
2041
+ const JUMP_PAGE_DEADLINE_MS = 3e4;
2042
+ /** How long to wait for the chat view to commit the target row. */
2043
+ const JUMP_ROW_WAIT_MS = 3e3;
2044
+ /** Settle between consecutive pages so a resolver without loadingOlder cannot hot-spin the loop. */
2045
+ const PAGE_COOLDOWN_MS = 50;
2046
+ /** Distance from the scrollport top the official ChatView lands a jumped row at. */
2047
+ const OFFICIAL_LANDING_INSET_PX = 24;
2048
+ /** Distances under this snap instead of animating. */
2049
+ const MIN_GLIDE_DISTANCE_PX = 8;
2050
+ /** Glide duration floor / cap — a far-boundary jump stays a tangible slide. */
2051
+ const MIN_GLIDE_MS = 280;
2052
+ const MAX_GLIDE_MS = 900;
2053
+ /** Every mounted conversation scrollport (multi-column layouts have several). */
2054
+ function conversationScrollports() {
2055
+ return Array.from(document.querySelectorAll("[data-conversation-scroll]"));
2056
+ }
2057
+ /**
2058
+ * Whether the target chat row is committed in ANY mounted conversation
2059
+ * scrollport. Kernel generations disagree on where the node table lives
2060
+ * (0.1.1: `SessionSnapshot.chat.nodes`; 0.1.2+: the `useChat` ChatSnapshot
2061
+ * node store, unreachable from `session.getSnapshot()` — its SessionSnapshot
2062
+ * faces no node table at all), but the rendered DOM is the ground truth both
2063
+ * share, so a jump probes it FIRST before deciding whether paging is needed.
2064
+ * @param anchorKey - chat anchor key of the target row.
2065
+ */
2066
+ function rowRenderedInDom(anchorKey) {
2067
+ const selector = `[data-chat-anchor-key="${CSS.escape(anchorKey)}"]`;
2068
+ for (const scrollport of conversationScrollports()) if (scrollport.querySelector(selector) !== null) return true;
2069
+ return false;
2070
+ }
2071
+ /**
2072
+ * Poll the conversation DOM until the target row is committed. The session
2073
+ * store updates BEFORE React renders the prepended page, so the old one-shot
2074
+ * query right after loadOlder() resolved could miss the row and silently
2075
+ * give up — the reported "click does nothing until the conversation is
2076
+ * nudged (scroll/click)" behavior.
2077
+ * @param key - chat anchor key to look for.
2078
+ * @param timeoutMs - polling budget.
2079
+ */
2080
+ async function waitForChatRow(key, timeoutMs) {
2081
+ const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
2082
+ const deadline = Date.now() + timeoutMs;
2083
+ for (;;) {
2084
+ for (const scrollport of conversationScrollports()) {
2085
+ const row = scrollport.querySelector(selector);
2086
+ if (row !== null) return row;
2087
+ }
2088
+ if (Date.now() >= deadline) return null;
2089
+ await delay(40);
2090
+ }
2091
+ }
2092
+ /** Maximum scrollable offset of a conversation scrollport. */
2093
+ function scrollFloorOf(el) {
2094
+ return Math.max(0, el.scrollHeight - el.clientHeight);
2095
+ }
2096
+ /**
2097
+ * Destination scrollTop that centers `row` inside `el` — the same geometry
2098
+ * as scrollIntoView({ block: 'center' }), computed directly so the glide owns
2099
+ * the scroll position instead of the browser.
2100
+ * @param el - the conversation scrollport.
2101
+ * @param row - the target chat row.
2102
+ */
2103
+ function centeredScrollTopFor(el, row) {
2104
+ const floor = scrollFloorOf(el);
2105
+ const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
2106
+ if (!Number.isFinite(target)) return el.scrollTop;
2107
+ return Math.max(0, Math.min(floor, target));
2108
+ }
2109
+ /**
2110
+ * Destination scrollTop the official ChatView uses for a navigation jump:
2111
+ * the row's top minus a 24px inset. Kept as a named resolver so the enhancer
2112
+ * lands exactly where the official would — only animated instead of assigned.
2113
+ * @param el - the conversation scrollport.
2114
+ * @param row - the target chat row.
2115
+ */
2116
+ function officialTargetTopFor(el, row) {
2117
+ const floor = scrollFloorOf(el);
2118
+ const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - OFFICIAL_LANDING_INSET_PX;
2119
+ if (!Number.isFinite(target)) return el.scrollTop;
2120
+ return Math.max(0, Math.min(floor, target));
2121
+ }
2122
+ /**
2123
+ * Glide duration for a distance: longer slides take longer, bounded to
2124
+ * [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history jump reads as a deliberate
2125
+ * glide, not a sluggish race).
2126
+ * @param distance - px to travel.
2127
+ */
2128
+ function glideDurationFor(distance) {
2129
+ if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
2130
+ return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
2131
+ }
2132
+ /**
2133
+ * Detach the host's bottom-follow state when the reader currently sits inside
2134
+ * its follow zone: assign the scroll position just outside the boundary. That
2135
+ * one synchronous scroll event settles at-bottom to false (moved-by-reader +
2136
+ * outside the zone) so a tip-moved re-render cannot yank the glide back to
2137
+ * the floor. No-op when already above the zone or when the assignment cannot
2138
+ * move (content shorter than the viewport).
2139
+ * @param el - the conversation scrollport.
2140
+ * @param followZonePx - the host's own follow threshold.
2141
+ */
2142
+ function detachBottomFollow(el, followZonePx = 24) {
2143
+ const floor = scrollFloorOf(el);
2144
+ if (el.scrollTop < floor - followZonePx) return false;
2145
+ const detached = Math.max(0, floor - followZonePx - 1);
2146
+ if (Math.abs(el.scrollTop - detached) < 1) return false;
2147
+ el.scrollTop = detached;
2148
+ return true;
2149
+ }
2150
+ /**
2151
+ * Wait until a detach assignment's scroll event has been dispatched AND
2152
+ * consumed by the host's scroll handler: scroll events fire on a later frame
2153
+ * than the assignment, and a paging commit's layout effect runs before that
1815
2154
  * dispatch — it would re-run the follow logic while the at-bottom flag is
1816
2155
  * still true and drag the viewport to the growing floor, undoing the detach
1817
2156
  * (two rAFs + a macrotask bracket the dispatch window).
@@ -1957,306 +2296,6 @@ window.__ModuleLoader__.load({
1957
2296
  }
1958
2297
  return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
1959
2298
  }
1960
- /**
1961
- * The rail's own inset, read off the custom property the official component
1962
- * publishes (`--turn-rail-inset`). Falls back to the official constant.
1963
- * @param rail - the rail element (or null).
1964
- */
1965
- function railInsetOf(rail) {
1966
- if (rail === null) return 6;
1967
- const px = Number.parseFloat(rail.style.getPropertyValue("--turn-rail-inset"));
1968
- return Number.isFinite(px) && px >= 0 ? px : 6;
1969
- }
1970
- /**
1971
- * Whether `rail` is the frame-style rail (dsh >= 0.1.2-alpha.3): marks sit
1972
- * at a fixed pitch and scroll INSIDE the frame, whose scroller offset the
1973
- * component publishes inline as `--turn-scroll-top` — every render, resting
1974
- * at `0px` included. The compressing alpha.1/2 rail never publishes that
1975
- * property, so its presence is the generation test. The legacy port paints
1976
- * its own `--dsbt-*` properties and can never trip this.
1977
- * @param rail - the rail element (or null).
1978
- */
1979
- function isFrameRail(rail) {
1980
- return rail !== null && rail.style.getPropertyValue("--turn-scroll-top") !== "";
1981
- }
1982
- /**
1983
- * Resolve the entry under a pointer Y on the frame-style rail — the official
1984
- * alpha.3 geometry verbatim (`itemAtPointer` in that generation's
1985
- * TurnNavigator.tsx): marks live in the scrolled ladder at a fixed 10px
1986
- * pitch, so the click offset adds the scroller offset. Read off the published
1987
- * `--turn-scroll-top` var rather than the scroller element itself — the same
1988
- * metric the official preview positioning uses, so any one-frame render lag
1989
- * affects both identically.
1990
- * @param count - number of entries the rail shows.
1991
- * @param rail - the rail element.
1992
- * @param clientY - pointer viewport Y.
1993
- */
1994
- function frameIndexAtPointer(count, rail, clientY) {
1995
- const rect = rail.getBoundingClientRect();
1996
- const inset = railInsetOf(rail);
1997
- const raw = Number.parseFloat(rail.style.getPropertyValue("--turn-scroll-top"));
1998
- const scrollTop = Number.isFinite(raw) ? raw : 0;
1999
- const offset = clientY - rect.top + scrollTop - inset;
2000
- return Math.max(0, Math.min(count - 1, Math.round(offset / 10)));
2001
- }
2002
- /**
2003
- * Resolve the gesture that produced a rail click to an entry index.
2004
- *
2005
- * Keyboard activation targets the focused mark button (recovered by its
2006
- * position among the rail's buttons); a mouse click carries coordinates and
2007
- * resolves through the rail geometry — the frame-style mapping over the
2008
- * published `--turn-scroll-top`. Returns -1 when neither applies (a
2009
- * keyboard target outside the rail, or a rail of an unsupported generation,
2010
- * which the kernel's own handler then owns).
2011
- *
2012
- * Assumption: the mark ticks are the ONLY buttons under the rail — the
2013
- * official rail renders marks exclusively and its preview tooltip carries
2014
- * none. A future non-mark control added inside the rail would shift the
2015
- * button index and must revisit this.
2016
- *
2017
- * @param rail - the rail element.
2018
- * @param event - the click event.
2019
- */
2020
- function indexForEvent(rail, event) {
2021
- const target = event.target;
2022
- if (target instanceof Element) {
2023
- const button = target.closest("button");
2024
- if (button !== null && rail.contains(button)) {
2025
- const index = Array.from(rail.querySelectorAll("button")).indexOf(button);
2026
- if (index >= 0) return index;
2027
- }
2028
- }
2029
- const count = rail.querySelectorAll("button").length;
2030
- if (count === 0) return -1;
2031
- return isFrameRail(rail) ? frameIndexAtPointer(count, rail, event.clientY) : -1;
2032
- }
2033
- /**
2034
- * Validate the official navigation array crossing the slot boundary. The
2035
- * value is produced by the kernel and may be absent, partial, or from an
2036
- * older build, so every field is checked before use.
2037
- * @param value - the raw `useChat(s => s.navigation.items())` result.
2038
- */
2039
- function normalizeNavigationItems(value) {
2040
- if (!Array.isArray(value)) return [];
2041
- const out = [];
2042
- const seen = /* @__PURE__ */ new Set();
2043
- for (const item of value) {
2044
- if (item === null || typeof item !== "object") continue;
2045
- const rec = item;
2046
- if (typeof rec.turn !== "number" || !Number.isFinite(rec.turn)) continue;
2047
- if (typeof rec.anchorKey !== "string" || rec.anchorKey === "") continue;
2048
- if (seen.has(rec.turn)) continue;
2049
- seen.add(rec.turn);
2050
- out.push({
2051
- turn: rec.turn,
2052
- anchorKey: rec.anchorKey,
2053
- prompt: typeof rec.prompt === "string" ? rec.prompt : "",
2054
- response: typeof rec.response === "string" ? rec.response : ""
2055
- });
2056
- }
2057
- return out;
2058
- }
2059
- /**
2060
- * Validate one `turnOutline` wire value into its entries. The value is the
2061
- * host projection's wire view (an array of `{ turn, seq, prompt, response }`
2062
- * — see `dsh-session-turn-outline`) read through `useProjection`, so it may
2063
- * be absent (pre-baseline, or an older kernel that never registers the key —
2064
- * which reads `undefined`), partial, or damaged. `turn` and `seq` are the
2065
- * load-bearing fields — a mark cannot exist or page without them — and a
2066
- * damaged one drops the entry; the previews are decorative and degrade to ''.
2067
- * @param value - the raw `useProjection('turnOutline')` value.
2068
- */
2069
- function normalizeOutlineItems(value) {
2070
- if (!Array.isArray(value)) return [];
2071
- const out = [];
2072
- for (const item of value) {
2073
- if (item === null || typeof item !== "object") continue;
2074
- const rec = item;
2075
- if (typeof rec.turn !== "number" || !Number.isSafeInteger(rec.turn) || rec.turn < 0) continue;
2076
- if (typeof rec.seq !== "number" || !Number.isSafeInteger(rec.seq) || rec.seq < 0) continue;
2077
- out.push({
2078
- turn: rec.turn,
2079
- seq: rec.seq,
2080
- prompt: typeof rec.prompt === "string" ? rec.prompt : "",
2081
- response: typeof rec.response === "string" ? rec.response : ""
2082
- });
2083
- }
2084
- return out;
2085
- }
2086
- /**
2087
- * Rebuild the rail's full mark ladder exactly as the official ChatView does
2088
- * (a mirror of `mergeTurnRailItems` in dsh-client-ui-chat): outline entries
2089
- * seed every known turn as unloaded, loaded items then override with their
2090
- * anchor key and richer previews, ascending by turn. The enhancer maps a
2091
- * click to a mark by INDEX, so its ladder must match the RENDERED marks —
2092
- * which are exactly this merge on an alpha.3 kernel; without the outline
2093
- * value the merge degrades to the loaded items (the alpha.1/2 ladder).
2094
- * @param loaded - the validated loaded-window items.
2095
- * @param outline - the raw turnOutline wire value.
2096
- */
2097
- function mergeRailItems(loaded, outline) {
2098
- const byTurn = /* @__PURE__ */ new Map();
2099
- for (const entry of normalizeOutlineItems(outline)) byTurn.set(entry.turn, {
2100
- turn: entry.turn,
2101
- prompt: entry.prompt,
2102
- response: entry.response
2103
- });
2104
- for (const item of loaded) {
2105
- const preview = byTurn.get(item.turn);
2106
- byTurn.set(item.turn, {
2107
- turn: item.turn,
2108
- prompt: item.prompt !== "" ? item.prompt : preview?.prompt ?? "",
2109
- response: item.response !== "" ? item.response : preview?.response ?? "",
2110
- anchorKey: item.anchorKey
2111
- });
2112
- }
2113
- if (byTurn.size === 0) return [];
2114
- return [...byTurn.values()].sort((left, right) => left.turn - right.turn);
2115
- }
2116
- //#endregion
2117
- //#region src/client/timeline/official-enhance.tsx
2118
- /**
2119
- * Official-rail enhancer — behaviour-only improvements to the kernel's own
2120
- * turn navigator (dsh >= 0.1.2-rc.1, `TurnNavigator` in dsh-client-ui-chat).
2121
- *
2122
- * This component renders NOTHING. It borrows the official rail exactly as the
2123
- * kernel painted it and fixes its behaviour, scoped to what the frame-style
2124
- * rail (dsh >= 0.1.2-alpha.3, unchanged on 0.1.2-rc.1) still lacks — told
2125
- * apart by capability, never by version compare:
2126
- *
2127
- * Smooth jump. The official `navigateToTurn` is one assignment
2128
- * (`el.scrollTop += flowTop(row, el) - 24`); a click across a long transcript
2129
- * teleports. Clicks on LOADED marks are intercepted in the capture phase and
2130
- * re-run through the shared glide instead. The frame-style rail also renders
2131
- * marks for turns OUTSIDE the loaded window (fed by the whole-log
2132
- * `turnOutline` projection); those jumps page history through the kernel's
2133
- * own loadThrough machinery — marks without an anchor key are left to it, and
2134
- * the click index is resolved against the same merged ladder the kernel
2135
- * renders. While one of those kernel jumps is still paging (a mark pulses
2136
- * `aria-busy`), the plugin stands down entirely: the kernel's loaded branch
2137
- * cancels its own pending jump before landing, and an interception would
2138
- * bypass that cancellation.
2139
- *
2140
- * State-ownership note (why intercepting the official click loses nothing):
2141
- * the kernel's scrollport (`data-conversation-scroll`, ChatView's
2142
- * `onScrollRef` handler) treats every unaccounted scrollTop assignment as a
2143
- * reader move and on each frame (a) flips its own at-bottom machinery,
2144
- * (b) `chatScroll.save(position)` for view-tab restore, and (c)
2145
- * `scheduleActiveTurn()` to re-derive the rail's active mark. The glide
2146
- * animates the real scrollport, so all three keep converging to the target
2147
- * row while it runs — the only thing the interception skips is the official
2148
- * handler's synchronous `setActiveTurn(item.turn)`, which its own
2149
- * scroll-driven active derivation re-settles by the time the glide lands.
2150
- *
2151
- * The jump goes through the shared backend (jump.ts).
2152
- *
2153
- * Interception notes: React 18 dispatches `onClick` from a listener on the
2154
- * root container during the BUBBLE phase. A capture listener on the rail
2155
- * itself therefore runs first, and `stopImmediatePropagation()` there prevents
2156
- * the event from ever reaching React's root — the official handler never
2157
- * fires. The rail owns pointer input for its whole column (marks carry
2158
- * `pointer-events: none`), so one listener catches mouse and keyboard alike.
2159
- *
2160
- * Nothing here touches the official DOM: no nodes are injected into a
2161
- * React-owned subtree and no official class or paint is overridden.
2162
- */
2163
- /**
2164
- * Structural anchor of the official rail — its inline style carries the
2165
- * frame's own metric (`--turn-natural-height`), published on every supported
2166
- * kernel (dsh >= 0.1.2-rc.1). Whether the found rail is the supported
2167
- * frame-style generation is then told apart by isFrameRail, never by a
2168
- * version compare.
2169
- */
2170
- const OFFICIAL_RAIL_SELECTOR = "[data-conversation-scroll] nav[style*=\"--turn-natural-height\"]";
2171
- /**
2172
- * Rails claimed by an enhancer instance, keyed by the owning instance's
2173
- * claim token. One session-scoped enhancer mounts per session; without a
2174
- * claim two instances would both attach capture listeners to the FIRST rail
2175
- * in the document (multi-column layouts) and one of them — not necessarily
2176
- * the one owning the reader's column — would win the click. The token
2177
- * distinguishes "already mine" (the poll keeps it) from "someone else's"
2178
- * (this instance must stand down). Weak: a replaced rail element disappears
2179
- * as soon as the browser collects it, so remounts need no bookkeeping.
2180
- */
2181
- const claimedRails = /* @__PURE__ */ new WeakMap();
2182
- /** How often the official rail is looked for (it mounts with the chat view). */
2183
- const RAIL_POLL_MS = 400;
2184
- /**
2185
- * The enhancer: no visual output, only the behaviour fixes the running rail
2186
- * generation lacks.
2187
- * @param props - dock slot props plus the enabled gate.
2188
- * @returns null (always).
2189
- */
2190
- function OfficialTimelineEnhancer(props) {
2191
- const { sessionId, sessionsService, useChat, useProjection, enabled } = props;
2192
- const ladder = mergeRailItems(normalizeNavigationItems(useChat === void 0 ? void 0 : useChat((snapshot) => {
2193
- const nav = snapshot?.navigation;
2194
- return typeof nav?.items === "function" ? nav.items() : void 0;
2195
- })), useProjection === void 0 ? void 0 : useProjection("turnOutline"));
2196
- const [rail, setRail] = react.default.useState(null);
2197
- const ladderRef = react.default.useRef(ladder);
2198
- ladderRef.current = ladder;
2199
- const railRef = react.default.useRef(rail);
2200
- railRef.current = rail;
2201
- const claimToken = react.default.useMemo(() => Symbol("dsh-bg-timeline-claim"), []);
2202
- react.default.useEffect(() => {
2203
- const check = () => {
2204
- const found = document.querySelector(OFFICIAL_RAIL_SELECTOR);
2205
- if (found === null) {
2206
- setRail(null);
2207
- return;
2208
- }
2209
- const owner = claimedRails.get(found);
2210
- if (owner !== void 0 && owner !== claimToken) {
2211
- setRail(null);
2212
- return;
2213
- }
2214
- claimedRails.set(found, claimToken);
2215
- setRail(found);
2216
- };
2217
- check();
2218
- const timer = window.setInterval(check, RAIL_POLL_MS);
2219
- return () => {
2220
- window.clearInterval(timer);
2221
- if (railRef.current !== null) claimedRails.delete(railRef.current);
2222
- };
2223
- }, [claimToken]);
2224
- const navigate = react.default.useCallback((item) => {
2225
- if (item.anchorKey === void 0) return;
2226
- if (sessionsService === void 0 || sessionId === void 0) return;
2227
- const scrollport = rail === null ? null : rail.closest("[data-conversation-scroll]");
2228
- jumpToMessage(sessionsService, sessionId, item.anchorKey, {
2229
- followZonePx: 24,
2230
- targetTop: officialTargetTopFor,
2231
- scrollport
2232
- }).catch(() => {});
2233
- }, [
2234
- sessionsService,
2235
- sessionId,
2236
- rail
2237
- ]);
2238
- react.default.useEffect(() => {
2239
- if (!enabled || rail === null) return;
2240
- const onClick = (event) => {
2241
- if (rail.querySelector("[aria-busy=\"true\"]") !== null) return;
2242
- const index = indexForEvent(rail, event);
2243
- const item = ladderRef.current[index];
2244
- if (item === void 0 || item.anchorKey === void 0) return;
2245
- event.preventDefault();
2246
- event.stopImmediatePropagation();
2247
- navigate(item);
2248
- };
2249
- rail.addEventListener("click", onClick, true);
2250
- return () => {
2251
- rail.removeEventListener("click", onClick, true);
2252
- };
2253
- }, [
2254
- enabled,
2255
- rail,
2256
- navigate
2257
- ]);
2258
- return null;
2259
- }
2260
2299
  //#endregion
2261
2300
  //#region src/client/timeline/index.tsx
2262
2301
  /**
@@ -2361,7 +2400,14 @@ window.__ModuleLoader__.load({
2361
2400
  id: "deepseek-harness-background.timeline",
2362
2401
  order: 45,
2363
2402
  locale: "ui-background",
2364
- inject: () => ({ sessionsService: ctx.sessions })
2403
+ inject: (sessionId) => ({ jumpToAnchor: (anchorKey) => {
2404
+ const scrollport = findOfficialRail()?.closest("[data-conversation-scroll]") ?? null;
2405
+ return jumpToMessage(ctx.sessions, sessionId, anchorKey, {
2406
+ followZonePx: 24,
2407
+ targetTop: officialTargetTopFor,
2408
+ scrollport
2409
+ });
2410
+ } })
2365
2411
  }, TimelineBridge));
2366
2412
  ctx.effect(() => () => {
2367
2413
  unsubscribe();