deepseek-harness-background 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -6
- package/README.zh.md +30 -6
- package/lib/client.js +1274 -1077
- package/lib/client.js.map +1 -1
- package/lib/index.js +2 -4
- package/package.json +8 -9
package/lib/client.js
CHANGED
|
@@ -65,7 +65,8 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
* dock family (agent task strips: TodoPanel / GoalBar / QueueDock) and its
|
|
66
66
|
* takeover panels (approval, question, plan review), the subagent lineage
|
|
67
67
|
* popover, the home hero "preview" badge, the sidebar build badge, and the
|
|
68
|
-
* timeline rail (timeline-css.ts
|
|
68
|
+
* timeline rail (timeline/legacy-rail-css.ts + the official-rail rules below).
|
|
69
|
+
* Every glassed surface shares ONE recipe:
|
|
69
70
|
* fill from the painter's --dsw-specific-input-major token (same alpha
|
|
70
71
|
* curve + theme dimming), the full shared blur/saturate/brightness chain
|
|
71
72
|
* driven by the glass-blur slider, and the shared sheen vars — so no
|
|
@@ -281,6 +282,25 @@ window.__ModuleLoader__.load({
|
|
|
281
282
|
-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);
|
|
282
283
|
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
283
284
|
}
|
|
285
|
+
|
|
286
|
+
/* ---- Official turn rail (dsh >= 0.1.2, TurnNavigator) -----------------
|
|
287
|
+
The enhance mode borrows the kernel's rail untouched, so its hover
|
|
288
|
+
preview card keeps the OFFICIAL opaque fill (bg-layer-1, not mediated
|
|
289
|
+
by any overridden token) — the "悬浮反馈形态不透明" complaint. Glass it
|
|
290
|
+
explicitly with the same explicit-fill recipe as the chrome buttons
|
|
291
|
+
(fill = the painter's composer token + shared blur/sheen). Anchors are
|
|
292
|
+
structural, never class names: the kernel's module-CSS class hashes are
|
|
293
|
+
not a stable contract, but nav[style*="--turn-natural-height"] (its
|
|
294
|
+
inline rail metrics) and the preview's role="tooltip" are. No edge
|
|
295
|
+
dissolve on the marks column: the first and last tick dashes straddle
|
|
296
|
+
the marks box edges by 1px, so a fade zone sized off the box height
|
|
297
|
+
swallows them instead of the band. */
|
|
298
|
+
body[data-dsh-bg-glass] nav[style*="--turn-natural-height"] [role="tooltip"] {
|
|
299
|
+
background-color: var(--dsw-specific-input-major);
|
|
300
|
+
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));
|
|
301
|
+
-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);
|
|
302
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
303
|
+
}
|
|
284
304
|
`;
|
|
285
305
|
/**
|
|
286
306
|
* Inject the backdrop stylesheet once. Safe to call repeatedly; a second call
|
|
@@ -305,7 +325,7 @@ window.__ModuleLoader__.load({
|
|
|
305
325
|
/**
|
|
306
326
|
* Session-projection key this plugin owns in the client-visible value map —
|
|
307
327
|
* the host fold registers it (src/projection.ts) and the rail reads it
|
|
308
|
-
* (src/client/timeline.
|
|
328
|
+
* (src/client/timeline/source.ts); one shared constant so the two can never drift.
|
|
309
329
|
*/
|
|
310
330
|
const TIMELINE_PROJECTION_KEY = "bgTimeline";
|
|
311
331
|
/** Default background rendering mode. */
|
|
@@ -946,15 +966,12 @@ window.__ModuleLoader__.load({
|
|
|
946
966
|
"background.uploadFailed": "上传失败,请重试",
|
|
947
967
|
"background.saveFailed": "保存失败,请重试",
|
|
948
968
|
"background.timeline": "会话时间线",
|
|
949
|
-
"background.timelineHint": "
|
|
950
|
-
"
|
|
951
|
-
"
|
|
969
|
+
"background.timelineHint": "右侧轮次导航轨,悬停预览该轮问题与直达更早历史",
|
|
970
|
+
"background.timelineEnhance": "会话时间线增强",
|
|
971
|
+
"background.timelineEnhanceHint": "为官方会话时间线补上平滑跳转,并可直达需要“加载更多”的更早轮次",
|
|
972
|
+
"timeline.railLabel": "轮次导航",
|
|
952
973
|
"timeline.noText": "(无文本内容)",
|
|
953
|
-
"timeline.
|
|
954
|
-
"timeline.mark": "标记为重点",
|
|
955
|
-
"timeline.unmark": "取消重点标记",
|
|
956
|
-
"timeline.noMarks": "还没有标记的重点",
|
|
957
|
-
"timeline.cannotJump": "无法跳转:这条历史消息缺少定位信息"
|
|
974
|
+
"timeline.jump": "跳转到第 {turn} 轮"
|
|
958
975
|
};
|
|
959
976
|
/** English copy. */
|
|
960
977
|
const en = {
|
|
@@ -983,15 +1000,12 @@ window.__ModuleLoader__.load({
|
|
|
983
1000
|
"background.uploadFailed": "Upload failed, try again",
|
|
984
1001
|
"background.saveFailed": "Save failed, try again",
|
|
985
1002
|
"background.timeline": "Conversation timeline",
|
|
986
|
-
"background.timelineHint": "
|
|
987
|
-
"
|
|
988
|
-
"
|
|
1003
|
+
"background.timelineHint": "Turn rail at the right edge; hover to preview a turn and reach earlier history",
|
|
1004
|
+
"background.timelineEnhance": "Conversation timeline enhancement",
|
|
1005
|
+
"background.timelineEnhanceHint": "Adds smooth jumps and direct reach into turns that still need “load earlier”",
|
|
1006
|
+
"timeline.railLabel": "Turn navigation",
|
|
989
1007
|
"timeline.noText": "(no text)",
|
|
990
|
-
"timeline.
|
|
991
|
-
"timeline.mark": "Mark as key point",
|
|
992
|
-
"timeline.unmark": "Unmark key point",
|
|
993
|
-
"timeline.noMarks": "No marked key points",
|
|
994
|
-
"timeline.cannotJump": "Cannot jump: this history message lacks position info"
|
|
1008
|
+
"timeline.jump": "Jump to turn {turn}"
|
|
995
1009
|
};
|
|
996
1010
|
//#endregion
|
|
997
1011
|
//#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
@@ -1115,6 +1129,31 @@ window.__ModuleLoader__.load({
|
|
|
1115
1129
|
/** Shared client for the painter and the settings row. */
|
|
1116
1130
|
const settingsClient = new SettingsClient();
|
|
1117
1131
|
//#endregion
|
|
1132
|
+
//#region src/client/timeline/mode-store.ts
|
|
1133
|
+
let mode = "legacy";
|
|
1134
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1135
|
+
/** @returns the currently detected mode. */
|
|
1136
|
+
function timelineMode() {
|
|
1137
|
+
return mode;
|
|
1138
|
+
}
|
|
1139
|
+
/** Observe mode changes. @returns the disposer. */
|
|
1140
|
+
function subscribeTimelineMode(listener) {
|
|
1141
|
+
listeners.add(listener);
|
|
1142
|
+
return () => {
|
|
1143
|
+
listeners.delete(listener);
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Publish the detected mode (called by the session-scoped dock entry).
|
|
1148
|
+
* No-ops when the mode is unchanged, so a re-render cannot notify.
|
|
1149
|
+
* @param next - the mode to publish.
|
|
1150
|
+
*/
|
|
1151
|
+
function reportTimelineMode(next) {
|
|
1152
|
+
if (mode === next) return;
|
|
1153
|
+
mode = next;
|
|
1154
|
+
for (const listener of listeners) listener();
|
|
1155
|
+
}
|
|
1156
|
+
//#endregion
|
|
1118
1157
|
//#region \0dsh-css:D:\Project\deepseek-harness-background\src\client\SettingsRow.module.css.mjs
|
|
1119
1158
|
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}}";
|
|
1120
1159
|
const tagId = "deepseek-harness-background/SettingsRow.module.css";
|
|
@@ -1126,41 +1165,41 @@ window.__ModuleLoader__.load({
|
|
|
1126
1165
|
document.head.appendChild(tag);
|
|
1127
1166
|
}
|
|
1128
1167
|
var SettingsRow_module_css_default = {
|
|
1129
|
-
"
|
|
1130
|
-
"
|
|
1131
|
-
"
|
|
1132
|
-
"
|
|
1133
|
-
"
|
|
1134
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
1135
|
-
"urlRow": "-ozqdq_urlRow",
|
|
1136
|
-
"clearRow": "-ozqdq_clearRow",
|
|
1137
|
-
"error": "-ozqdq_error",
|
|
1168
|
+
"sliderValue": "-ozqdq_sliderValue",
|
|
1169
|
+
"btnPrimary": "-ozqdq_btnPrimary",
|
|
1170
|
+
"desc": "-ozqdq_desc",
|
|
1171
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
1172
|
+
"section": "-ozqdq_section",
|
|
1138
1173
|
"sliderLabel": "-ozqdq_sliderLabel",
|
|
1139
1174
|
"title": "-ozqdq_title",
|
|
1140
|
-
"
|
|
1141
|
-
"sliderValue": "-ozqdq_sliderValue",
|
|
1142
|
-
"titleRow": "-ozqdq_titleRow",
|
|
1143
|
-
"clearBtn": "-ozqdq_clearBtn",
|
|
1144
|
-
"segGroup": "-ozqdq_segGroup",
|
|
1145
|
-
"segBtn": "-ozqdq_segBtn",
|
|
1146
|
-
"hint": "-ozqdq_hint",
|
|
1175
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
1147
1176
|
"urlInput": "-ozqdq_urlInput",
|
|
1148
|
-
"
|
|
1149
|
-
"
|
|
1177
|
+
"segGroup": "-ozqdq_segGroup",
|
|
1178
|
+
"segRow": "-ozqdq_segRow",
|
|
1179
|
+
"btn": "-ozqdq_btn",
|
|
1180
|
+
"clearRow": "-ozqdq_clearRow",
|
|
1150
1181
|
"previewImg": "-ozqdq_previewImg",
|
|
1151
1182
|
"slider": "-ozqdq_slider",
|
|
1152
|
-
"
|
|
1153
|
-
"
|
|
1154
|
-
"
|
|
1155
|
-
"
|
|
1156
|
-
"
|
|
1157
|
-
"
|
|
1183
|
+
"btnDanger": "-ozqdq_btnDanger",
|
|
1184
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
1185
|
+
"previewEmpty": "-ozqdq_previewEmpty",
|
|
1186
|
+
"error": "-ozqdq_error",
|
|
1187
|
+
"switch": "-ozqdq_switch",
|
|
1188
|
+
"sliderRow": "-ozqdq_sliderRow",
|
|
1189
|
+
"segLabel": "-ozqdq_segLabel",
|
|
1158
1190
|
"sectionLabel": "-ozqdq_sectionLabel",
|
|
1159
1191
|
"header": "-ozqdq_header",
|
|
1160
|
-
"
|
|
1161
|
-
"
|
|
1162
|
-
"
|
|
1163
|
-
"
|
|
1192
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
1193
|
+
"preview": "-ozqdq_preview",
|
|
1194
|
+
"previewGlassText": "-ozqdq_previewGlassText",
|
|
1195
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
1196
|
+
"segBtn": "-ozqdq_segBtn",
|
|
1197
|
+
"segActive": "-ozqdq_segActive",
|
|
1198
|
+
"titleRow": "-ozqdq_titleRow",
|
|
1199
|
+
"switchKnob": "-ozqdq_switchKnob",
|
|
1200
|
+
"row": "-ozqdq_row",
|
|
1201
|
+
"urlRow": "-ozqdq_urlRow",
|
|
1202
|
+
"hint": "-ozqdq_hint"
|
|
1164
1203
|
};
|
|
1165
1204
|
//#endregion
|
|
1166
1205
|
//#region src/client/SettingsRow.tsx
|
|
@@ -1326,6 +1365,9 @@ window.__ModuleLoader__.load({
|
|
|
1326
1365
|
*/
|
|
1327
1366
|
function BackgroundSettingsRow({ t }) {
|
|
1328
1367
|
const snapshot = (0, react.useSyncExternalStore)(settingsClient.subscribe, readSnapshot);
|
|
1368
|
+
const navMode = (0, react.useSyncExternalStore)(subscribeTimelineMode, timelineMode);
|
|
1369
|
+
const timelineLabelKey = navMode === "enhance" ? "background.timelineEnhance" : "background.timeline";
|
|
1370
|
+
const timelineHintKey = navMode === "enhance" ? "background.timelineEnhanceHint" : "background.timelineHint";
|
|
1329
1371
|
const [draft, setDraft] = (0, react.useState)(DEFAULTS);
|
|
1330
1372
|
const [uploading, setUploading] = (0, react.useState)(false);
|
|
1331
1373
|
const [urlText, setUrlText] = (0, react.useState)("");
|
|
@@ -1657,21 +1699,21 @@ window.__ModuleLoader__.load({
|
|
|
1657
1699
|
children: [
|
|
1658
1700
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1659
1701
|
className: SettingsRow_module_css_default.segLabel,
|
|
1660
|
-
children: t(
|
|
1702
|
+
children: t(timelineLabelKey)
|
|
1661
1703
|
}),
|
|
1662
1704
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1663
1705
|
type: "button",
|
|
1664
1706
|
role: "switch",
|
|
1665
1707
|
"aria-checked": draft.timeline,
|
|
1666
|
-
"aria-label": t(
|
|
1667
|
-
title: t(
|
|
1708
|
+
"aria-label": t(timelineLabelKey),
|
|
1709
|
+
title: t(timelineHintKey),
|
|
1668
1710
|
className: SettingsRow_module_css_default.switch,
|
|
1669
1711
|
onClick: handleTimelineToggle,
|
|
1670
1712
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: SettingsRow_module_css_default.switchKnob })
|
|
1671
1713
|
}),
|
|
1672
1714
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1673
1715
|
className: SettingsRow_module_css_default.hint,
|
|
1674
|
-
children: t(
|
|
1716
|
+
children: t(timelineHintKey)
|
|
1675
1717
|
})
|
|
1676
1718
|
]
|
|
1677
1719
|
})
|
|
@@ -1695,458 +1737,571 @@ window.__ModuleLoader__.load({
|
|
|
1695
1737
|
});
|
|
1696
1738
|
}
|
|
1697
1739
|
//#endregion
|
|
1698
|
-
//#region src/client/timeline
|
|
1740
|
+
//#region src/client/timeline/jump.ts
|
|
1741
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1742
|
+
/** How long one jump may keep paging older history before giving up. */
|
|
1743
|
+
const JUMP_PAGE_DEADLINE_MS = 3e4;
|
|
1744
|
+
/** How long to wait for the chat view to commit the target row. */
|
|
1745
|
+
const JUMP_ROW_WAIT_MS = 3e3;
|
|
1746
|
+
/** Distance from the scrollport top the official ChatView lands a jumped row at. */
|
|
1747
|
+
const OFFICIAL_LANDING_INSET_PX = 24;
|
|
1748
|
+
/** Distances under this snap instead of animating. */
|
|
1749
|
+
const MIN_GLIDE_DISTANCE_PX = 8;
|
|
1750
|
+
/** Glide duration floor / cap — a far-boundary jump stays a tangible slide. */
|
|
1751
|
+
const MIN_GLIDE_MS = 280;
|
|
1752
|
+
const MAX_GLIDE_MS = 900;
|
|
1753
|
+
/** How many rounds one warm-up page waits for its prepend to commit. */
|
|
1754
|
+
const WARM_PAGE_SETTLE_ROUNDS = 8;
|
|
1755
|
+
/** Pause between those rounds. */
|
|
1756
|
+
const WARM_PAGE_SETTLE_MS = 25;
|
|
1757
|
+
/** Wall-clock budget for one warm-up run. */
|
|
1758
|
+
const WARM_DEADLINE_MS = 15e3;
|
|
1759
|
+
/** Page budget for one warm-up run. */
|
|
1760
|
+
const WARM_MAX_PAGES = 40;
|
|
1761
|
+
/** Every mounted conversation scrollport (multi-column layouts have several). */
|
|
1762
|
+
function conversationScrollports() {
|
|
1763
|
+
return Array.from(document.querySelectorAll("[data-conversation-scroll]"));
|
|
1764
|
+
}
|
|
1699
1765
|
/**
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1707
|
-
* blurred capsule (height − 8px); expanding panel (max 260px, radius 16)
|
|
1708
|
-
* whose scroll area holds 30px rows (tick indicator + fading-in title);
|
|
1709
|
-
* 32px gradient fade veils top/bottom, expanded-only, shown while that
|
|
1710
|
-
* side actually clips.
|
|
1711
|
-
*
|
|
1712
|
-
* Deviations from the reference port, all fidelity fixes:
|
|
1713
|
-
* - The filter bar collapses to zero height while idle instead of staying
|
|
1714
|
-
* in layout invisibly (it pushed every tick ~33px down).
|
|
1715
|
-
* - Short stacks center vertically; overflowing stacks are bottom-pinned
|
|
1716
|
-
* by the component so the newest tick hugs the capsule's bottom edge
|
|
1717
|
-
* (the official idle strategy — the reference port flowed ticks from
|
|
1718
|
-
* the top and clipped the newest away).
|
|
1719
|
-
*
|
|
1720
|
-
* Glass integration: the official chat.deepseek.com paints (light
|
|
1721
|
-
* rgba(255,255,255,.8)/.94, dark rgba(21,21,23,.6)/rgba(28,28,32,.95),
|
|
1722
|
-
* 5px collapsed / 16px expanded blur) remain as the NO-GLASS fallback.
|
|
1723
|
-
* While the plugin's glass system is on (`body[data-dsh-bg-glass]`), the
|
|
1724
|
-
* rail joins the SAME unified recipe as the composer card and bubbles:
|
|
1725
|
-
* fill = the painter's `--dsw-specific-input-major` token, blur/saturate/
|
|
1726
|
-
* brightness = the shared `--bg-glass-*` variables, i.e. the glass-blur
|
|
1727
|
-
* slider. Class prefix `dsbt-` deliberately avoids every
|
|
1728
|
-
* `[class*="_xxx"]` anchor of the plugin's glass sheet (underscores only).
|
|
1766
|
+
* Whether the target chat row is committed in ANY mounted conversation
|
|
1767
|
+
* scrollport. Kernel generations disagree on where the node table lives
|
|
1768
|
+
* (0.1.1: `SessionSnapshot.chat.nodes`; 0.1.2+: the `useChat` ChatSnapshot
|
|
1769
|
+
* node store, unreachable from `session.getSnapshot()` — its SessionSnapshot
|
|
1770
|
+
* faces no node table at all), but the rendered DOM is the ground truth both
|
|
1771
|
+
* share, so a jump probes it FIRST before deciding whether paging is needed.
|
|
1772
|
+
* @param anchorKey - chat anchor key of the target row.
|
|
1729
1773
|
*/
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (typeof document === "undefined") return;
|
|
1735
|
-
if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
|
|
1736
|
-
const tag = document.createElement("style");
|
|
1737
|
-
tag.dataset.plugin = "deepseek-harness-background";
|
|
1738
|
-
tag.dataset.pluginCss = TIMELINE_CSS_TAG;
|
|
1739
|
-
tag.textContent = TIMELINE_CSS;
|
|
1740
|
-
document.head.appendChild(tag);
|
|
1774
|
+
function rowRenderedInDom(anchorKey) {
|
|
1775
|
+
const selector = `[data-chat-anchor-key="${CSS.escape(anchorKey)}"]`;
|
|
1776
|
+
for (const scrollport of conversationScrollports()) if (scrollport.querySelector(selector) !== null) return true;
|
|
1777
|
+
return false;
|
|
1741
1778
|
}
|
|
1742
|
-
const TIMELINE_CSS = `
|
|
1743
|
-
.dsbt-nav {
|
|
1744
|
-
user-select: none; -webkit-user-select: none;
|
|
1745
|
-
/* Filter-bar chrome height shared by the bar itself and the top veil's
|
|
1746
|
-
expanded offset. */
|
|
1747
|
-
--dsbt-bar-h: 33px;
|
|
1748
|
-
z-index: 90; align-items: center; width: 34px;
|
|
1749
|
-
height: var(--dsbt-h, 300px);
|
|
1750
|
-
transition: right .2s ease, opacity .2s ease;
|
|
1751
|
-
display: flex; position: fixed; top: 50%; right: 16px;
|
|
1752
|
-
transform: translateY(-50%); pointer-events: auto;
|
|
1753
|
-
}
|
|
1754
|
-
@media (max-width: 767px) { .dsbt-nav { display: none; } }
|
|
1755
|
-
|
|
1756
|
-
/* Collapsed capsule — frosted strip behind the tick marks. Official paint:
|
|
1757
|
-
light rgba(255,255,255,.8), dark rgba(21,21,23,.6), 5px blur. */
|
|
1758
|
-
.dsbt-bg {
|
|
1759
|
-
background-color: rgba(255, 255, 255, .8);
|
|
1760
|
-
-webkit-backdrop-filter: blur(5px);
|
|
1761
|
-
backdrop-filter: blur(5px);
|
|
1762
|
-
box-shadow:
|
|
1763
|
-
inset 0 1px 0 rgba(255, 255, 255, .28),
|
|
1764
|
-
inset 0 0 0 .5px rgba(255, 255, 255, .1),
|
|
1765
|
-
0 6px 18px rgba(0, 0, 0, .07);
|
|
1766
|
-
border-radius: 16px; width: 34px;
|
|
1767
|
-
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1768
|
-
position: absolute; top: 50%; right: 0; transform: translateY(-50%);
|
|
1769
|
-
transition: opacity .2s ease; pointer-events: none;
|
|
1770
|
-
}
|
|
1771
|
-
body[data-ds-dark-theme] .dsbt-bg {
|
|
1772
|
-
background-color: rgba(21, 21, 23, .6);
|
|
1773
|
-
box-shadow:
|
|
1774
|
-
inset 0 1px 0 rgba(255, 255, 255, .09),
|
|
1775
|
-
inset 0 0 0 .5px rgba(255, 255, 255, .05),
|
|
1776
|
-
0 6px 18px rgba(0, 0, 0, .3);
|
|
1777
|
-
}
|
|
1778
|
-
.dsbt-bg.dsbt-bg-hide { opacity: 0; }
|
|
1779
|
-
|
|
1780
|
-
/* Expanding panel — exactly the capsule's box (identical height in both
|
|
1781
|
-
states), width animates 34px -> measured fit (max 260px). Official
|
|
1782
|
-
hover-state paint: white/black frosted glass at ~94% alpha, 16px blur. */
|
|
1783
|
-
.dsbt-wrap {
|
|
1784
|
-
width: 34px;
|
|
1785
|
-
height: calc(var(--dsbt-h, 300px) - 8px); max-height: calc(var(--dsbt-h, 300px) - 8px);
|
|
1786
|
-
border: 1px solid transparent; border-radius: 16px;
|
|
1787
|
-
flex-direction: column; align-items: stretch;
|
|
1788
|
-
transition: width .22s cubic-bezier(.4, 0, .2, 1), background-color .2s ease,
|
|
1789
|
-
box-shadow .2s ease, border-color .2s ease;
|
|
1790
|
-
display: flex; position: absolute; top: 50%; right: 0;
|
|
1791
|
-
transform: translateY(-50%);
|
|
1792
|
-
overflow: hidden; box-sizing: border-box; background: transparent;
|
|
1793
|
-
pointer-events: none;
|
|
1794
|
-
}
|
|
1795
|
-
.dsbt-wrap.dsbt-show {
|
|
1796
|
-
pointer-events: auto;
|
|
1797
|
-
width: min(var(--dsbt-w, 260px), 260px);
|
|
1798
|
-
background-color: rgba(255, 255, 255, .94);
|
|
1799
|
-
-webkit-backdrop-filter: blur(16px);
|
|
1800
|
-
backdrop-filter: blur(16px);
|
|
1801
|
-
border-color: rgba(0, 0, 0, .06);
|
|
1802
|
-
box-shadow: 0 10px 30px rgba(0, 0, 0, .08), 0 2px 8px rgba(0, 0, 0, .04);
|
|
1803
|
-
}
|
|
1804
|
-
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show {
|
|
1805
|
-
background-color: rgba(28, 28, 32, .95);
|
|
1806
|
-
border-color: rgba(255, 255, 255, .08);
|
|
1807
|
-
box-shadow: 0 10px 30px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .25);
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
/* Content fade veils — the official ::before/::after pair rebuilt as
|
|
1811
|
-
elements: a bar of the panel's OWN fill color masked to nothing over
|
|
1812
|
-
32px (solid -> mask == gradient-to-transparent, but works with the
|
|
1813
|
-
translucent fill). They belong to the EXPANDED panel only — the idle
|
|
1814
|
-
capsule hard-clips its ticks at the rounded ends like the official
|
|
1815
|
-
strip — and while expanded the top veil starts BELOW the filter bar,
|
|
1816
|
-
not over it. Shown only while that side actually clips. */
|
|
1817
|
-
.dsbt-fade {
|
|
1818
|
-
z-index: 2; pointer-events: none; opacity: 0;
|
|
1819
|
-
background-color: rgba(255, 255, 255, .94);
|
|
1820
|
-
-webkit-mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1821
|
-
mask-image: linear-gradient(#000 20.19%, transparent 100%);
|
|
1822
|
-
width: 100%; height: 32px; transition: opacity .2s ease;
|
|
1823
|
-
position: absolute; left: 0; top: 0;
|
|
1824
|
-
}
|
|
1825
|
-
body[data-ds-dark-theme] .dsbt-fade { background-color: rgba(28, 28, 32, .95); }
|
|
1826
|
-
.dsbt-fade.dsbt-fade-bot { top: auto; bottom: 0; transform: rotate(180deg); }
|
|
1827
|
-
.dsbt-wrap.dsbt-show .dsbt-fade.dsbt-fade-on { opacity: 1; }
|
|
1828
|
-
.dsbt-wrap.dsbt-show .dsbt-fade:not(.dsbt-fade-bot) { top: var(--dsbt-bar-h, 33px); }
|
|
1829
|
-
|
|
1830
|
-
/* Filter bar — "marked only" toggle pinned above the scroll area while
|
|
1831
|
-
expanded. Collapsed, it collapses to ZERO height (not just opacity): a
|
|
1832
|
-
transparent-but-in-layout bar pushed every tick ~33px down, one of the
|
|
1833
|
-
reasons the idle capsule read as off-alignment. */
|
|
1834
|
-
.dsbt-filterbar {
|
|
1835
|
-
height: var(--dsbt-bar-h, 33px);
|
|
1836
|
-
padding: 8px 12px 4px; display: flex; justify-content: flex-end;
|
|
1837
|
-
align-items: center; border-bottom: 1px solid rgba(0, 0, 0, .05);
|
|
1838
|
-
box-sizing: border-box; flex: none; overflow: hidden;
|
|
1839
|
-
transition: opacity .2s ease, height .2s ease, padding .2s ease,
|
|
1840
|
-
border-color .2s ease;
|
|
1841
|
-
}
|
|
1842
|
-
.dsbt-wrap:not(.dsbt-show) .dsbt-filterbar {
|
|
1843
|
-
height: 0; padding-top: 0; padding-bottom: 0; opacity: 0;
|
|
1844
|
-
border-bottom-color: transparent;
|
|
1845
|
-
}
|
|
1846
|
-
body[data-ds-dark-theme] .dsbt-filterbar { border-bottom-color: rgba(255, 255, 255, .06); }
|
|
1847
|
-
.dsbt-filterbtn {
|
|
1848
|
-
font-size: 11px; line-height: 16px; padding: 2px 8px; border-radius: 10px;
|
|
1849
|
-
border: 1px solid rgba(0, 0, 0, .1); background: rgba(0, 0, 0, .03);
|
|
1850
|
-
color: rgba(0, 0, 0, .65); cursor: pointer; transition: all .15s ease;
|
|
1851
|
-
display: inline-flex; align-items: center; gap: 3px; font-family: inherit;
|
|
1852
|
-
}
|
|
1853
|
-
.dsbt-filterbtn:hover { background: rgba(0, 0, 0, .07); color: rgba(0, 0, 0, .9); }
|
|
1854
|
-
.dsbt-filterbtn.dsbt-filteron {
|
|
1855
|
-
background: rgba(245, 158, 11, .12); border-color: #f59e0b;
|
|
1856
|
-
color: #b45309; font-weight: 600;
|
|
1857
|
-
}
|
|
1858
|
-
body[data-ds-dark-theme] .dsbt-filterbtn {
|
|
1859
|
-
border-color: rgba(255, 255, 255, .12); background: rgba(255, 255, 255, .05);
|
|
1860
|
-
color: rgba(255, 255, 255, .7);
|
|
1861
|
-
}
|
|
1862
|
-
body[data-ds-dark-theme] .dsbt-filterbtn:hover { background: rgba(255, 255, 255, .1); color: rgba(255, 255, 255, .95); }
|
|
1863
|
-
body[data-ds-dark-theme] .dsbt-filterbtn.dsbt-filteron {
|
|
1864
|
-
background: rgba(251, 191, 36, .18); border-color: #fbbf24; color: #fbbf24;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
/* Scroll area — rows hug the right edge; scrolls only when overfull. The
|
|
1868
|
-
auto-margin pseudo pair vertically centers the stack when it fits the
|
|
1869
|
-
capsule (short conversations), and resolves to zero free space when it
|
|
1870
|
-
overflows, so bottom-pinned scrolling behaves like a plain column. */
|
|
1871
|
-
.dsbt-page {
|
|
1872
|
-
padding: 15px 0 15px 24px; box-sizing: border-box;
|
|
1873
|
-
overscroll-behavior: contain; flex: 1 1 auto; min-height: 0;
|
|
1874
|
-
flex-direction: column; align-items: flex-end; display: flex;
|
|
1875
|
-
position: relative; width: 100%; overflow: hidden;
|
|
1876
|
-
}
|
|
1877
|
-
.dsbt-page::before, .dsbt-page::after {
|
|
1878
|
-
content: ''; flex: none; margin-block: auto;
|
|
1879
|
-
}
|
|
1880
|
-
.dsbt-wrap.dsbt-show .dsbt-page {
|
|
1881
|
-
overflow-y: auto; overflow-x: hidden;
|
|
1882
|
-
scrollbar-width: thin; scrollbar-color: rgba(0, 0, 0, .15) transparent;
|
|
1883
|
-
}
|
|
1884
|
-
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-page { scrollbar-color: rgba(255, 255, 255, .25) transparent; }
|
|
1885
|
-
.dsbt-page::-webkit-scrollbar { width: 4px; }
|
|
1886
|
-
.dsbt-page::-webkit-scrollbar-track { background: transparent; }
|
|
1887
|
-
.dsbt-page::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, .15); border-radius: 4px; }
|
|
1888
|
-
body[data-ds-dark-theme] .dsbt-page::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, .25); }
|
|
1889
|
-
|
|
1890
|
-
/* Empty state (filtered list with no marks). */
|
|
1891
|
-
.dsbt-empty {
|
|
1892
|
-
font-size: 12px; line-height: 18px; padding: 16px 12px;
|
|
1893
|
-
color: rgba(0, 0, 0, .45); text-align: center; width: 100%; box-sizing: border-box;
|
|
1894
|
-
}
|
|
1895
|
-
body[data-ds-dark-theme] .dsbt-empty { color: rgba(255, 255, 255, .45); }
|
|
1896
|
-
|
|
1897
|
-
/* One message row: 30px, star bookmark + tick indicator at the right,
|
|
1898
|
-
title fading in. */
|
|
1899
|
-
/* Row shell: layout + shared ink only — the interactive hit areas are the
|
|
1900
|
-
sibling .dsbt-jump / .dsbt-star buttons below. */
|
|
1901
|
-
.dsbt-item {
|
|
1902
|
-
height: 30px; min-height: 30px; flex-shrink: 0;
|
|
1903
|
-
justify-content: flex-end; align-items: center;
|
|
1904
|
-
width: calc(100% - 4px); margin-right: 4px;
|
|
1905
|
-
display: flex; box-sizing: border-box;
|
|
1906
|
-
color: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .65));
|
|
1907
|
-
transition: color .15s ease;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
/* Jump target: a real button so Enter/Space work natively without nesting
|
|
1911
|
-
an interactive element inside another. */
|
|
1912
|
-
.dsbt-jump {
|
|
1913
|
-
cursor: pointer; background: none; border: none; margin: 0; padding: 0;
|
|
1914
|
-
font: inherit; line-height: 20px; text-align: right; color: inherit;
|
|
1915
|
-
flex: 1 1 auto; min-width: 0; height: 100%;
|
|
1916
|
-
display: flex; align-items: center; justify-content: flex-end;
|
|
1917
|
-
}
|
|
1918
|
-
.dsbt-jump:focus-visible { outline: 1px solid currentColor; outline-offset: -1px; border-radius: 6px; }
|
|
1919
|
-
.dsbt-jump:disabled { cursor: default; opacity: .55; }
|
|
1920
|
-
.dsbt-jump:disabled .dsbt-title { text-decoration: line-through; text-decoration-color: currentColor; }
|
|
1921
|
-
.dsbt-item:hover { color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .95)); }
|
|
1922
|
-
body[data-ds-dark-theme] .dsbt-item { color: rgba(255, 255, 255, .65); }
|
|
1923
|
-
body[data-ds-dark-theme] .dsbt-item:hover { color: rgba(255, 255, 255, .95); }
|
|
1924
|
-
.dsbt-title {
|
|
1925
|
-
font-size: 13px; line-height: 20px; flex: 1 1 auto; min-width: 0;
|
|
1926
|
-
text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
|
|
1927
|
-
margin-right: 6px; text-align: right; color: inherit;
|
|
1928
|
-
opacity: 0; transition: opacity .12s ease, color .15s ease;
|
|
1929
|
-
}
|
|
1930
|
-
.dsbt-show .dsbt-title { opacity: 1; }
|
|
1931
|
-
|
|
1932
|
-
/* Star bookmark — appears on row hover while the panel is expanded; gold
|
|
1933
|
-
when marked. A real sibling button next to the jump target, so marking
|
|
1934
|
-
stays keyboard-operable without nested interactive elements. */
|
|
1935
|
-
.dsbt-star {
|
|
1936
|
-
font-size: 13px; line-height: 16px; width: 18px; height: 18px;
|
|
1937
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
1938
|
-
opacity: 0; cursor: pointer; transition: all .15s ease; margin-right: 6px;
|
|
1939
|
-
flex-shrink: 0; color: rgba(0, 0, 0, .25); border-radius: 4px; user-select: none;
|
|
1940
|
-
-webkit-user-select: none;
|
|
1941
|
-
background: none; border: none; padding: 0; font-family: inherit;
|
|
1942
|
-
}
|
|
1943
|
-
.dsbt-star:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; opacity: .9; }
|
|
1944
|
-
.dsbt-wrap.dsbt-show .dsbt-item:hover .dsbt-star { opacity: .75; }
|
|
1945
|
-
.dsbt-wrap.dsbt-show .dsbt-star:hover { opacity: 1; transform: scale(1.2); color: #d97706; }
|
|
1946
|
-
.dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #f59e0b; }
|
|
1947
|
-
body[data-ds-dark-theme] .dsbt-star { color: rgba(255, 255, 255, .25); }
|
|
1948
|
-
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star:hover { color: #fde047; }
|
|
1949
|
-
body[data-ds-dark-theme] .dsbt-wrap.dsbt-show .dsbt-star.dsbt-staron { opacity: 1; color: #fbbf24; }
|
|
1950
|
-
|
|
1951
|
-
.dsbt-ind { flex-shrink: 0; justify-content: center; align-items: center; width: 16px; height: 20px; display: flex; }
|
|
1952
|
-
.dsbt-line {
|
|
1953
|
-
background-color: var(--dsw-alias-border-l4, rgba(0, 0, 0, .16));
|
|
1954
|
-
border-radius: 4px; flex-shrink: 0; width: 8px; height: 2px;
|
|
1955
|
-
transition: background-color .2s ease, transform .2s ease, width .2s ease, height .2s ease;
|
|
1956
|
-
}
|
|
1957
|
-
.dsbt-item:hover .dsbt-line { background-color: var(--dsw-alias-label-primary, rgba(0, 0, 0, .9)); }
|
|
1958
|
-
.dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1959
|
-
.dsbt-item.dsbt-active .dsbt-title { color: var(--dsw-alias-state-business-primary, #4d6bfe); font-weight: 500; }
|
|
1960
|
-
.dsbt-item.dsbt-active .dsbt-line {
|
|
1961
|
-
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1962
|
-
transform-origin: 50%; transform: scale(1.5);
|
|
1963
|
-
}
|
|
1964
|
-
body[data-ds-dark-theme] .dsbt-item.dsbt-active { color: var(--dsw-alias-state-business-primary, #4d6bfe); }
|
|
1965
|
-
body[data-ds-dark-theme] .dsbt-line { background-color: rgba(255, 255, 255, .2); }
|
|
1966
|
-
body[data-ds-dark-theme] .dsbt-item:hover .dsbt-line { background-color: rgba(255, 255, 255, .9); }
|
|
1967
|
-
|
|
1968
|
-
/* Marked (key-point) rows: a wider golden tick in both themes; the active
|
|
1969
|
-
state keeps its brand-blue scale so reading position always wins. */
|
|
1970
|
-
.dsbt-item.dsbt-marked .dsbt-line { background-color: #d97706; width: 10px; height: 2.5px; }
|
|
1971
|
-
.dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #b45309; }
|
|
1972
|
-
.dsbt-item.dsbt-marked.dsbt-active .dsbt-line {
|
|
1973
|
-
background-color: var(--dsw-alias-state-business-primary, #4d6bfe);
|
|
1974
|
-
transform-origin: 50%; transform: scale(1.5);
|
|
1975
|
-
}
|
|
1976
|
-
body[data-ds-dark-theme] .dsbt-item.dsbt-marked .dsbt-line { background-color: #fbbf24; width: 10px; height: 2.5px; }
|
|
1977
|
-
body[data-ds-dark-theme] .dsbt-item.dsbt-marked:hover .dsbt-line { background-color: #fde047; }
|
|
1978
|
-
|
|
1979
|
-
/* ---- Unified glass integration -----------------------------------------
|
|
1980
|
-
Glass system on (wallpaper active + panelOpacity < 1): the rail joins
|
|
1981
|
-
the composer/bubble recipe — the same translucent fill token and the
|
|
1982
|
-
same blur/saturate/brightness chain (the glass-blur slider drives it,
|
|
1983
|
-
no fixed 5/16px). Placed after the dark-theme rules so equal-specificity
|
|
1984
|
-
overrides win in both schemes. */
|
|
1985
|
-
body[data-dsh-bg-glass] .dsbt-bg,
|
|
1986
|
-
body[data-dsh-bg-glass] .dsbt-wrap.dsbt-show {
|
|
1987
|
-
background-color: var(--dsw-specific-input-major);
|
|
1988
|
-
-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);
|
|
1989
|
-
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
1990
|
-
}
|
|
1991
|
-
body[data-dsh-bg-glass] .dsbt-fade {
|
|
1992
|
-
background-color: var(--dsw-specific-input-major);
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1996
|
-
.dsbt-nav, .dsbt-bg, .dsbt-wrap, .dsbt-fade, .dsbt-title, .dsbt-line, .dsbt-jump, .dsbt-star, .dsbt-filterbtn, .dsbt-filterbar { transition: none; }
|
|
1997
|
-
}
|
|
1998
|
-
`;
|
|
1999
|
-
//#endregion
|
|
2000
|
-
//#region src/client/timeline.tsx
|
|
2001
1779
|
/**
|
|
2002
|
-
*
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
* key-
|
|
2008
|
-
*
|
|
2009
|
-
* 260px, otherwise the width fits the longest title). Both states share one
|
|
2010
|
-
* computed outer height, so expanding never jumps, and 32px fade veils mask
|
|
2011
|
-
* the clipped edge on each side while that side actually scrolls.
|
|
2012
|
-
*
|
|
2013
|
-
* Enhancements ported from dsh-chat-timeline v0.1.4 (MIT):
|
|
2014
|
-
* - Key-point bookmarks: star a question in the expanded panel, persisted per
|
|
2015
|
-
* session in localStorage; marked rows show a golden tick in the capsule
|
|
2016
|
-
* and a "marked only" filter narrows the list.
|
|
2017
|
-
* - Jump stabilization: the reading-position tracker freezes while a jump
|
|
2018
|
-
* glide animates (cleared 150ms after the last scroll event, plus a 2s
|
|
2019
|
-
* fallback), so the panel cannot jitter mid-jump. The glide itself is a
|
|
2020
|
-
* rAF ease-in-out on the conversation scrollport: a ~26px bottom-follow
|
|
2021
|
-
* detach first settles the official at-bottom flag outside its 25px zone
|
|
2022
|
-
* (so a tip-moved re-render cannot yank the slide back to the floor — the
|
|
2023
|
-
* earlier instant+smooth double pass kept the jump working but made the
|
|
2024
|
-
* slide invisible), and wheel/touch/keyboard input cancels it so the
|
|
2025
|
-
* reader takes over instantly.
|
|
2026
|
-
* - Rewind integration: messages withdrawn by a rewind command node are
|
|
2027
|
-
* filtered out of the rail.
|
|
2028
|
-
*
|
|
2029
|
-
* Data source, fastest first: the host-side `bgTimeline` session projection
|
|
2030
|
-
* (src/projection.ts — a complete enumeration of the session's user messages
|
|
2031
|
-
* delivered through the projection baseline/push channel) -> the loaded chat
|
|
2032
|
-
* node window. The conversation's own lazy loading is never driven from here;
|
|
2033
|
-
* clicking a row pages older history on demand, scrolls the conversation to
|
|
2034
|
-
* the target row, and keeps the rail's own active row visible.
|
|
2035
|
-
*
|
|
2036
|
-
* Collapsed idle strategy (official ScrollNav behavior): the tick stack is
|
|
2037
|
-
* pinned to the BOTTOM inner edge — the newest question's tick always sits at
|
|
2038
|
-
* the bottom of the capsule, older ticks clip away at the top. Hovering
|
|
2039
|
-
* expands the SAME box and scrolls the active row into view instead.
|
|
2040
|
-
*
|
|
2041
|
-
* The rail reads the shared settings transport: it renders unless
|
|
2042
|
-
* `timeline: false`. Its glass paints are self-contained official values
|
|
2043
|
-
* (see timeline-css.ts), independent of the plugin's token overrides.
|
|
1780
|
+
* Poll the conversation DOM until the target row is committed. The session
|
|
1781
|
+
* store updates BEFORE React renders the prepended page, so the old one-shot
|
|
1782
|
+
* query right after loadOlder() resolved could miss the row and silently
|
|
1783
|
+
* give up — the reported "click does nothing until the conversation is
|
|
1784
|
+
* nudged (scroll/click)" behavior.
|
|
1785
|
+
* @param key - chat anchor key to look for.
|
|
1786
|
+
* @param timeoutMs - polling budget.
|
|
2044
1787
|
*/
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
if (
|
|
1788
|
+
async function waitForChatRow(key, timeoutMs) {
|
|
1789
|
+
const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
|
|
1790
|
+
const deadline = Date.now() + timeoutMs;
|
|
1791
|
+
for (;;) {
|
|
1792
|
+
for (const scrollport of conversationScrollports()) {
|
|
1793
|
+
const row = scrollport.querySelector(selector);
|
|
1794
|
+
if (row !== null) return row;
|
|
1795
|
+
}
|
|
1796
|
+
if (Date.now() >= deadline) return null;
|
|
1797
|
+
await delay(40);
|
|
2054
1798
|
}
|
|
2055
|
-
|
|
1799
|
+
}
|
|
1800
|
+
/** Maximum scrollable offset of a conversation scrollport. */
|
|
1801
|
+
function scrollFloorOf(el) {
|
|
1802
|
+
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
2056
1803
|
}
|
|
2057
1804
|
/**
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2060
|
-
*
|
|
2061
|
-
* @param
|
|
1805
|
+
* Destination scrollTop that centers `row` inside `el` — the same geometry
|
|
1806
|
+
* as scrollIntoView({ block: 'center' }), computed directly so the glide owns
|
|
1807
|
+
* the scroll position instead of the browser.
|
|
1808
|
+
* @param el - the conversation scrollport.
|
|
1809
|
+
* @param row - the target chat row.
|
|
2062
1810
|
*/
|
|
2063
|
-
function
|
|
2064
|
-
|
|
2065
|
-
const
|
|
2066
|
-
if (
|
|
2067
|
-
return
|
|
1811
|
+
function centeredScrollTopFor(el, row) {
|
|
1812
|
+
const floor = scrollFloorOf(el);
|
|
1813
|
+
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
|
|
1814
|
+
if (!Number.isFinite(target)) return el.scrollTop;
|
|
1815
|
+
return Math.max(0, Math.min(floor, target));
|
|
2068
1816
|
}
|
|
2069
|
-
/** localStorage key prefix for the per-session marked-key lists. */
|
|
2070
|
-
const MARKS_STORAGE_PREFIX = "dsbt_marks_";
|
|
2071
1817
|
/**
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
1818
|
+
* Destination scrollTop the official ChatView uses for a navigation jump:
|
|
1819
|
+
* the row's top minus a 24px inset. Kept as a named resolver so the enhancer
|
|
1820
|
+
* lands exactly where the official would — only animated instead of assigned.
|
|
1821
|
+
* @param el - the conversation scrollport.
|
|
1822
|
+
* @param row - the target chat row.
|
|
2074
1823
|
*/
|
|
2075
|
-
function
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const parsed = JSON.parse(raw);
|
|
2081
|
-
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
2082
|
-
} catch {
|
|
2083
|
-
return [];
|
|
2084
|
-
}
|
|
1824
|
+
function officialTargetTopFor(el, row) {
|
|
1825
|
+
const floor = scrollFloorOf(el);
|
|
1826
|
+
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - OFFICIAL_LANDING_INSET_PX;
|
|
1827
|
+
if (!Number.isFinite(target)) return el.scrollTop;
|
|
1828
|
+
return Math.max(0, Math.min(floor, target));
|
|
2085
1829
|
}
|
|
2086
1830
|
/**
|
|
2087
|
-
*
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
1831
|
+
* Glide duration for a distance: longer slides take longer, bounded to
|
|
1832
|
+
* [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history jump reads as a deliberate
|
|
1833
|
+
* glide, not a sluggish race).
|
|
1834
|
+
* @param distance - px to travel.
|
|
2090
1835
|
*/
|
|
2091
|
-
function
|
|
2092
|
-
if (
|
|
2093
|
-
|
|
2094
|
-
if (!Array.isArray(marks) || marks.length === 0) window.localStorage.removeItem(MARKS_STORAGE_PREFIX + sessionId);
|
|
2095
|
-
else window.localStorage.setItem(MARKS_STORAGE_PREFIX + sessionId, JSON.stringify(marks));
|
|
2096
|
-
} catch {}
|
|
2097
|
-
}
|
|
2098
|
-
/** Parse digits into a finite number (undefined otherwise). */
|
|
2099
|
-
function parseSeqDigits(digits) {
|
|
2100
|
-
if (digits === void 0) return void 0;
|
|
2101
|
-
const n = Number.parseInt(digits, 10);
|
|
2102
|
-
return Number.isFinite(n) ? n : void 0;
|
|
1836
|
+
function glideDurationFor(distance) {
|
|
1837
|
+
if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
|
|
1838
|
+
return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
|
|
2103
1839
|
}
|
|
2104
|
-
/**
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
1840
|
+
/**
|
|
1841
|
+
* Detach the host's bottom-follow state when the reader currently sits inside
|
|
1842
|
+
* its follow zone: assign the scroll position just outside the boundary. That
|
|
1843
|
+
* one synchronous scroll event settles at-bottom to false (moved-by-reader +
|
|
1844
|
+
* outside the zone) so a tip-moved re-render cannot yank the glide back to
|
|
1845
|
+
* the floor. No-op when already above the zone or when the assignment cannot
|
|
1846
|
+
* move (content shorter than the viewport).
|
|
1847
|
+
* @param el - the conversation scrollport.
|
|
1848
|
+
* @param followZonePx - the host's own follow threshold.
|
|
1849
|
+
*/
|
|
1850
|
+
function detachBottomFollow(el, followZonePx = 25) {
|
|
1851
|
+
const floor = scrollFloorOf(el);
|
|
1852
|
+
if (el.scrollTop < floor - followZonePx) return false;
|
|
1853
|
+
const detached = Math.max(0, floor - followZonePx - 1);
|
|
1854
|
+
if (Math.abs(el.scrollTop - detached) < 1) return false;
|
|
1855
|
+
el.scrollTop = detached;
|
|
1856
|
+
return true;
|
|
2110
1857
|
}
|
|
2111
1858
|
/**
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2116
|
-
*
|
|
2117
|
-
*
|
|
1859
|
+
* Wait until a detach assignment's scroll event has been dispatched AND
|
|
1860
|
+
* consumed by the host's scroll handler: scroll events fire on a later frame
|
|
1861
|
+
* than the assignment, and a paging commit's layout effect runs before that
|
|
1862
|
+
* dispatch — it would re-run the follow logic while the at-bottom flag is
|
|
1863
|
+
* still true and drag the viewport to the growing floor, undoing the detach
|
|
1864
|
+
* (two rAFs + a macrotask bracket the dispatch window).
|
|
2118
1865
|
*/
|
|
2119
|
-
function
|
|
2120
|
-
if (typeof
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
if (hashMatch !== null) return parseSeqDigits(hashMatch[1]);
|
|
2125
|
-
const broadMatch = /(?:rewound|withdrawn|已撤回).*?(?:target\s+)?(\d+)/i.exec(text);
|
|
2126
|
-
if (broadMatch !== null) return parseSeqDigits(broadMatch[1]);
|
|
1866
|
+
function settleScrollEvents() {
|
|
1867
|
+
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
|
1868
|
+
return new Promise((resolve) => {
|
|
1869
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.setTimeout(resolve, 0)));
|
|
1870
|
+
});
|
|
2127
1871
|
}
|
|
2128
1872
|
/**
|
|
2129
|
-
*
|
|
2130
|
-
*
|
|
2131
|
-
*
|
|
2132
|
-
*
|
|
2133
|
-
*
|
|
2134
|
-
* @param
|
|
1873
|
+
* Animate el.scrollTop toward target with an ease-in-out cubic profile.
|
|
1874
|
+
* Wheel / touch / keyboard-scroll input cancels the glide so the reader
|
|
1875
|
+
* takes over immediately. Resolves true when the glide completed, false
|
|
1876
|
+
* when the reader cancelled it.
|
|
1877
|
+
* @param el - the conversation scrollport.
|
|
1878
|
+
* @param target - destination scrollTop.
|
|
1879
|
+
* @param durationMs - animation budget.
|
|
2135
1880
|
*/
|
|
2136
|
-
function
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
1881
|
+
function animateScrollTop(el, target, durationMs) {
|
|
1882
|
+
return new Promise((resolve) => {
|
|
1883
|
+
const start = el.scrollTop;
|
|
1884
|
+
const delta = target - start;
|
|
1885
|
+
let done = false;
|
|
1886
|
+
let frame = 0;
|
|
1887
|
+
const finish = (completed) => {
|
|
1888
|
+
if (done) return;
|
|
1889
|
+
done = true;
|
|
1890
|
+
el.removeEventListener("wheel", onWheel);
|
|
1891
|
+
el.removeEventListener("touchstart", onTouch);
|
|
1892
|
+
window.removeEventListener("keydown", onKey);
|
|
1893
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
1894
|
+
resolve(completed);
|
|
1895
|
+
};
|
|
1896
|
+
const onWheel = () => finish(false);
|
|
1897
|
+
const onTouch = () => finish(false);
|
|
1898
|
+
const onKey = (e) => {
|
|
1899
|
+
if ([
|
|
1900
|
+
"ArrowUp",
|
|
1901
|
+
"ArrowDown",
|
|
1902
|
+
"PageUp",
|
|
1903
|
+
"PageDown",
|
|
1904
|
+
"Home",
|
|
1905
|
+
"End",
|
|
1906
|
+
" "
|
|
1907
|
+
].includes(e.key)) finish(false);
|
|
1908
|
+
};
|
|
1909
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
1910
|
+
el.addEventListener("touchstart", onTouch, { passive: true });
|
|
1911
|
+
window.addEventListener("keydown", onKey);
|
|
1912
|
+
if (Math.abs(delta) < .5 || durationMs <= 0) {
|
|
1913
|
+
el.scrollTop = target;
|
|
1914
|
+
finish(true);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
const t0 = performance.now();
|
|
1918
|
+
const ease = (x) => x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
1919
|
+
const step = (now) => {
|
|
1920
|
+
if (done) return;
|
|
1921
|
+
const t = Math.min(1, (now - t0) / durationMs);
|
|
1922
|
+
el.scrollTop = start + delta * ease(t);
|
|
1923
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
1924
|
+
else finish(true);
|
|
1925
|
+
};
|
|
1926
|
+
frame = requestAnimationFrame(step);
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Page older history in WITHOUT moving what the reader is looking at.
|
|
1931
|
+
*
|
|
1932
|
+
* The host's ChatView compensates a prepend through an internal anchor ref
|
|
1933
|
+
* that only its own "load earlier" button and its own navigation arm. A
|
|
1934
|
+
* plugin calling `session.loadOlder()` from the outside gets no compensation,
|
|
1935
|
+
* so the prepended rows push the reader's content down — a visible jump.
|
|
1936
|
+
* This restores the visual position by the growth the prepend added.
|
|
1937
|
+
*
|
|
1938
|
+
* A reader pinned to the floor needs none of it: the host's follow logic
|
|
1939
|
+
* re-pins to the (growing) floor by itself, and compensating there would
|
|
1940
|
+
* fight it and overshoot.
|
|
1941
|
+
*
|
|
1942
|
+
* @param session - the session handle to page.
|
|
1943
|
+
* @param el - the conversation scrollport.
|
|
1944
|
+
* @param followZonePx - the host's bottom-follow threshold.
|
|
1945
|
+
* @returns whether the page actually grew the transcript (false = exhausted
|
|
1946
|
+
* or failed, so the caller should stop paging).
|
|
1947
|
+
*/
|
|
1948
|
+
async function compensatedLoadOlder(session, el, followZonePx = 25) {
|
|
1949
|
+
const pinned = scrollFloorOf(el) - el.scrollTop <= followZonePx;
|
|
1950
|
+
const heightBefore = el.scrollHeight;
|
|
1951
|
+
try {
|
|
1952
|
+
await session.loadOlder();
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
console.warn("[deepseek-harness-background] timeline history page failed:", error);
|
|
1955
|
+
return false;
|
|
1956
|
+
}
|
|
1957
|
+
for (let round = 0; round < WARM_PAGE_SETTLE_ROUNDS; round += 1) {
|
|
1958
|
+
await settleScrollEvents();
|
|
1959
|
+
const growth = el.scrollHeight - heightBefore;
|
|
1960
|
+
if (growth > 0) {
|
|
1961
|
+
if (!pinned) el.scrollTop = el.scrollTop + growth;
|
|
1962
|
+
return true;
|
|
1963
|
+
}
|
|
1964
|
+
if (round < 7) await delay(WARM_PAGE_SETTLE_MS);
|
|
1965
|
+
}
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Page older history in until the rail is full or the log is exhausted — the
|
|
1970
|
+
* "turns behind the load-earlier button become reachable" half of the
|
|
1971
|
+
* enhancement, shared by both frontends.
|
|
1972
|
+
*
|
|
1973
|
+
* Stops at `capacity` on purpose: past the rail's uncompressed tick count the
|
|
1974
|
+
* official stylesheet squeezes every mark into the same 420px column and the
|
|
1975
|
+
* rail degrades into a solid bar the reader cannot aim at. Filling it exactly
|
|
1976
|
+
* to capacity is free reach; going past it is not.
|
|
1977
|
+
*
|
|
1978
|
+
* @param session - the session handle to page.
|
|
1979
|
+
* @param scrollport - the conversation scrollport (compensation anchor).
|
|
1980
|
+
* @param options - budgets, the live entry count and the rail's capacity.
|
|
1981
|
+
*/
|
|
1982
|
+
async function warmHistory(session, scrollport, options) {
|
|
1983
|
+
const followZonePx = options.followZonePx ?? 25;
|
|
1984
|
+
const deadline = Date.now() + (options.deadlineMs ?? WARM_DEADLINE_MS);
|
|
1985
|
+
const maxPages = options.maxPages ?? WARM_MAX_PAGES;
|
|
1986
|
+
const alive = options.alive ?? (() => true);
|
|
1987
|
+
for (let page = 0; page < maxPages; page += 1) {
|
|
1988
|
+
if (!alive()) return;
|
|
1989
|
+
if (options.countOf() >= options.capacity) return;
|
|
1990
|
+
const snapshot = session.getSnapshot();
|
|
1991
|
+
if (snapshot?.hasMore !== true) return;
|
|
1992
|
+
if (Date.now() >= deadline) return;
|
|
1993
|
+
if (snapshot.loadingOlder === true) {
|
|
1994
|
+
await delay(50);
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
1998
|
+
await delay(100);
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
if (!await compensatedLoadOlder(session, scrollport, followZonePx)) return;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Ensure the target node is loaded, wait for the chat view to commit its row
|
|
2006
|
+
* into the DOM, then glide it into view. Paging uses a time budget instead
|
|
2007
|
+
* of a fixed iteration guard (each in-flight page burned part of that guard,
|
|
2008
|
+
* so slow history could exhaust it and silently fail).
|
|
2009
|
+
*
|
|
2010
|
+
* @param sessionsService - the runtime sessions service.
|
|
2011
|
+
* @param sessionId - the session owning the target row.
|
|
2012
|
+
* @param anchorKey - chat anchor key of the target row.
|
|
2013
|
+
* @param options - follow zone, landing resolver, paging scopes and budgets.
|
|
2014
|
+
* @returns whether the row was reached (false when it never rendered or the
|
|
2015
|
+
* reader took the scrollport back mid-glide).
|
|
2016
|
+
*/
|
|
2017
|
+
async function jumpToMessage(sessionsService, sessionId, anchorKey, options = {}) {
|
|
2018
|
+
const session = sessionsService.binding(sessionId)?.session;
|
|
2019
|
+
if (session === void 0) return false;
|
|
2020
|
+
const followZonePx = options.followZonePx ?? 25;
|
|
2021
|
+
const resolveTarget = options.targetTop ?? centeredScrollTopFor;
|
|
2022
|
+
const pageDeadlineMs = options.pageDeadlineMs ?? JUMP_PAGE_DEADLINE_MS;
|
|
2023
|
+
const rowWaitMs = options.rowWaitMs ?? JUMP_ROW_WAIT_MS;
|
|
2024
|
+
const ports = options.scrollport != null ? [options.scrollport] : conversationScrollports();
|
|
2025
|
+
let detachedAny = false;
|
|
2026
|
+
for (const el of ports) if (detachBottomFollow(el, followZonePx)) {
|
|
2027
|
+
el.dispatchEvent(new Event("scroll"));
|
|
2028
|
+
detachedAny = true;
|
|
2029
|
+
}
|
|
2030
|
+
if (detachedAny) await settleScrollEvents();
|
|
2031
|
+
const deadline = Date.now() + pageDeadlineMs;
|
|
2032
|
+
for (;;) {
|
|
2033
|
+
if (rowRenderedInDom(anchorKey)) break;
|
|
2034
|
+
const snapshot = session.getSnapshot();
|
|
2035
|
+
if ((snapshot?.chat?.nodes?.get(anchorKey) ?? snapshot?.nodes?.get(anchorKey)) !== void 0) break;
|
|
2036
|
+
if (snapshot?.hasMore !== true) break;
|
|
2037
|
+
if (Date.now() >= deadline) break;
|
|
2038
|
+
if (snapshot.loadingOlder === true) {
|
|
2039
|
+
await delay(50);
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
2043
|
+
await delay(100);
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
try {
|
|
2047
|
+
await session.loadOlder();
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
console.warn("[deepseek-harness-background] timeline jump paging failed:", error);
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
const row = await waitForChatRow(anchorKey, rowWaitMs);
|
|
2054
|
+
if (row === null) {
|
|
2055
|
+
console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${anchorKey}`);
|
|
2056
|
+
return false;
|
|
2057
|
+
}
|
|
2058
|
+
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2059
|
+
const scroller = row.closest("[data-conversation-scroll]");
|
|
2060
|
+
if (scroller === null) {
|
|
2061
|
+
row.scrollIntoView({
|
|
2062
|
+
behavior: "auto",
|
|
2063
|
+
block: "center"
|
|
2064
|
+
});
|
|
2065
|
+
if (!reducedMotion) row.scrollIntoView({
|
|
2066
|
+
behavior: "smooth",
|
|
2067
|
+
block: "center"
|
|
2068
|
+
});
|
|
2069
|
+
return true;
|
|
2070
|
+
}
|
|
2071
|
+
const target = resolveTarget(scroller, row);
|
|
2072
|
+
if (reducedMotion || Math.abs(target - scroller.scrollTop) < MIN_GLIDE_DISTANCE_PX) {
|
|
2073
|
+
scroller.scrollTop = target;
|
|
2074
|
+
return true;
|
|
2075
|
+
}
|
|
2076
|
+
if (detachBottomFollow(scroller, followZonePx)) {
|
|
2077
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
2078
|
+
await settleScrollEvents();
|
|
2079
|
+
}
|
|
2080
|
+
return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
|
|
2081
|
+
}
|
|
2082
|
+
/** The official stylesheet's rail height cap (TurnNavigator.module.css). */
|
|
2083
|
+
const RAIL_HEIGHT_CAP_PX = 420;
|
|
2084
|
+
/** Band clearance the official stylesheet reserves around the rail. */
|
|
2085
|
+
const RAIL_BAND_CLEARANCE_PX = 64;
|
|
2086
|
+
/**
|
|
2087
|
+
* The rail's own inset, read off the custom property the official component
|
|
2088
|
+
* publishes (`--turn-rail-inset`). Falls back to the official constant.
|
|
2089
|
+
* @param rail - the rail element (or null).
|
|
2090
|
+
*/
|
|
2091
|
+
function railInsetOf(rail) {
|
|
2092
|
+
if (rail === null) return 6;
|
|
2093
|
+
const px = Number.parseFloat(rail.style.getPropertyValue("--turn-rail-inset"));
|
|
2094
|
+
return Number.isFinite(px) && px >= 0 ? px : 6;
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* The tallest the rail can render. The official stylesheet clamps the rail's
|
|
2098
|
+
* height with `min(natural, band - 64px, 420px)` and the band measurements
|
|
2099
|
+
* are published inline on the scroll host (ConversationRoot's
|
|
2100
|
+
* `--dsh-conversation-viewport-height` / `--dsh-composer-height`). Outside
|
|
2101
|
+
* the official layout — the ported rail portals itself to body, and its own
|
|
2102
|
+
* stylesheet carries the same 420px cap — only the cap applies.
|
|
2103
|
+
*
|
|
2104
|
+
* The rendered `clientHeight` must NOT be used here: with few loaded turns
|
|
2105
|
+
* the height IS the natural (count-driven) height, so a measured capacity
|
|
2106
|
+
* would equal the count and a "rail full" gate could never open — the
|
|
2107
|
+
* warm-up fixed point that left early turns unreachable.
|
|
2108
|
+
*/
|
|
2109
|
+
function railMaxHeightOf(rail) {
|
|
2110
|
+
const scrollport = rail.closest("[data-conversation-scroll]");
|
|
2111
|
+
if (scrollport !== null) {
|
|
2112
|
+
const viewport = Number.parseFloat(scrollport.style.getPropertyValue("--dsh-conversation-viewport-height"));
|
|
2113
|
+
const composer = Number.parseFloat(scrollport.style.getPropertyValue("--dsh-composer-height"));
|
|
2114
|
+
if (Number.isFinite(viewport) && Number.isFinite(composer)) return Math.min(RAIL_HEIGHT_CAP_PX, Math.max(0, viewport - composer - RAIL_BAND_CLEARANCE_PX));
|
|
2115
|
+
}
|
|
2116
|
+
return RAIL_HEIGHT_CAP_PX;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* How many marks the rail can show before it starts compressing them.
|
|
2120
|
+
*
|
|
2121
|
+
* Past this count the official stylesheet clamps the rail's height and every
|
|
2122
|
+
* mark switches from its fixed 10px spacing to a percentage position, so the
|
|
2123
|
+
* column degrades into a solid bar the reader cannot aim at. Filling the rail
|
|
2124
|
+
* exactly to this capacity is therefore free extra reach; going past it is
|
|
2125
|
+
* not.
|
|
2126
|
+
*
|
|
2127
|
+
* @param rail - the rail element (or null).
|
|
2128
|
+
*/
|
|
2129
|
+
function railCapacityOf(rail) {
|
|
2130
|
+
if (rail === null) return 41;
|
|
2131
|
+
const usable = railMaxHeightOf(rail) - 2 * railInsetOf(rail);
|
|
2132
|
+
if (!Number.isFinite(usable)) return 41;
|
|
2133
|
+
return Math.max(2, Math.floor(usable / 10) + 1);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Resolve the entry under a pointer Y — the official geometry verbatim
|
|
2137
|
+
* (`itemAtPointer` in TurnNavigator.tsx), so this plugin lands on exactly the
|
|
2138
|
+
* mark the official rail would have picked.
|
|
2139
|
+
* @param count - number of entries the rail shows.
|
|
2140
|
+
* @param rail - the rail element.
|
|
2141
|
+
* @param clientY - pointer viewport Y.
|
|
2142
|
+
*/
|
|
2143
|
+
function indexAtPointer(count, rail, clientY) {
|
|
2144
|
+
const rect = rail.getBoundingClientRect();
|
|
2145
|
+
const inset = railInsetOf(rail);
|
|
2146
|
+
const usableHeight = Math.max(1, rect.height - 2 * inset);
|
|
2147
|
+
const ratio = Math.max(0, Math.min(1, (clientY - rect.top - inset) / usableHeight));
|
|
2148
|
+
return Math.round(ratio * (count - 1));
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Resolve the gesture that produced a rail click to an entry index.
|
|
2152
|
+
*
|
|
2153
|
+
* Keyboard activation targets the focused mark button (recovered by its
|
|
2154
|
+
* position among the rail's buttons); a mouse click carries coordinates and
|
|
2155
|
+
* resolves through the rail geometry. Returns -1 when neither applies.
|
|
2156
|
+
*
|
|
2157
|
+
* Assumption (both frontends): the mark ticks are the ONLY buttons under the
|
|
2158
|
+
* rail — the official rail renders marks exclusively and its preview tooltip
|
|
2159
|
+
* carries none, and the ported rail does the same. A future non-mark control
|
|
2160
|
+
* added inside the rail would shift the button index and must revisit this.
|
|
2161
|
+
*
|
|
2162
|
+
* @param rail - the rail element.
|
|
2163
|
+
* @param event - the click event.
|
|
2164
|
+
*/
|
|
2165
|
+
function indexForEvent(rail, event) {
|
|
2166
|
+
const target = event.target;
|
|
2167
|
+
if (target instanceof Element) {
|
|
2168
|
+
const button = target.closest("button");
|
|
2169
|
+
if (button !== null && rail.contains(button)) {
|
|
2170
|
+
const index = Array.from(rail.querySelectorAll("button")).indexOf(button);
|
|
2171
|
+
if (index >= 0) return index;
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const count = rail.querySelectorAll("button").length;
|
|
2175
|
+
return count === 0 ? -1 : indexAtPointer(count, rail, event.clientY);
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Validate the official navigation array crossing the slot boundary. The
|
|
2179
|
+
* value is produced by the kernel and may be absent, partial, or from an
|
|
2180
|
+
* older build, so every field is checked before use.
|
|
2181
|
+
* @param value - the raw `useChat(s => s.navigation.items())` result.
|
|
2182
|
+
*/
|
|
2183
|
+
function normalizeNavigationItems(value) {
|
|
2184
|
+
if (!Array.isArray(value)) return [];
|
|
2185
|
+
const out = [];
|
|
2186
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2187
|
+
for (const item of value) {
|
|
2188
|
+
if (item === null || typeof item !== "object") continue;
|
|
2189
|
+
const rec = item;
|
|
2190
|
+
if (typeof rec.turn !== "number" || !Number.isFinite(rec.turn)) continue;
|
|
2191
|
+
if (typeof rec.anchorKey !== "string" || rec.anchorKey === "") continue;
|
|
2192
|
+
if (seen.has(rec.turn)) continue;
|
|
2193
|
+
seen.add(rec.turn);
|
|
2194
|
+
out.push({
|
|
2195
|
+
turn: rec.turn,
|
|
2196
|
+
anchorKey: rec.anchorKey,
|
|
2197
|
+
prompt: typeof rec.prompt === "string" ? rec.prompt : "",
|
|
2198
|
+
response: typeof rec.response === "string" ? rec.response : ""
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
return out;
|
|
2202
|
+
}
|
|
2203
|
+
//#endregion
|
|
2204
|
+
//#region src/client/timeline/source.ts
|
|
2205
|
+
/**
|
|
2206
|
+
* Enumerate the chat nodes of one conversation snapshot, duck-typed across
|
|
2207
|
+
* both kernel generations:
|
|
2208
|
+
* - dsh <= 0.1.1: `snapshot.chat.nodes` is a `Map` keyed by node key.
|
|
2209
|
+
* - dsh >= 0.1.2: the Chat snapshot is `{ order, nodes: { values() } }` and
|
|
2210
|
+
* the session snapshot has no `chat` field at all.
|
|
2211
|
+
*
|
|
2212
|
+
* Both stores expose `values()`, so one structural read covers both; the
|
|
2213
|
+
* receiver must survive the call because these are prototype methods (the
|
|
2214
|
+
* real Session's own methods read `this`, and so do Map's).
|
|
2215
|
+
*
|
|
2216
|
+
* @param snapshot - a session snapshot or a Chat snapshot (loosely typed).
|
|
2217
|
+
* @returns the nodes, in the store's own order; empty when neither shape fits.
|
|
2218
|
+
*/
|
|
2219
|
+
function chatNodesOf(snapshot) {
|
|
2220
|
+
const valuesOf = (store) => {
|
|
2221
|
+
if (store === null || typeof store !== "object") return void 0;
|
|
2222
|
+
const values = store.values;
|
|
2223
|
+
if (typeof values !== "function") return void 0;
|
|
2224
|
+
let iterable;
|
|
2225
|
+
try {
|
|
2226
|
+
iterable = values.call(store);
|
|
2227
|
+
} catch {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
if (iterable === null || typeof iterable !== "object") return void 0;
|
|
2231
|
+
const out = [];
|
|
2232
|
+
for (const node of iterable) if (node !== null && typeof node === "object") out.push(node);
|
|
2233
|
+
return out;
|
|
2234
|
+
};
|
|
2235
|
+
const rec = snapshot ?? {};
|
|
2236
|
+
return valuesOf(rec.chat?.nodes) ?? valuesOf(rec.nodes) ?? [];
|
|
2237
|
+
}
|
|
2238
|
+
/** Is this node part of the visible surface? Nodes without the flag are legacy. */
|
|
2239
|
+
function isVisible(node) {
|
|
2240
|
+
return node.visibility === void 0 || node.visibility === "visible";
|
|
2241
|
+
}
|
|
2242
|
+
/** Preview cap so entries stay small (one short line per row). */
|
|
2243
|
+
const MAX_TEXT_CHARS = 80;
|
|
2244
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
2245
|
+
function userTextOf(content) {
|
|
2246
|
+
if (!Array.isArray(content)) return "";
|
|
2247
|
+
let out = "";
|
|
2248
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
|
|
2249
|
+
const text = block.text;
|
|
2250
|
+
if (typeof text === "string") out += text;
|
|
2251
|
+
}
|
|
2252
|
+
return out.trim().slice(0, MAX_TEXT_CHARS);
|
|
2253
|
+
}
|
|
2254
|
+
/** Parse digits into a finite number (undefined otherwise). */
|
|
2255
|
+
function parseSeqDigits(digits) {
|
|
2256
|
+
if (digits === void 0) return void 0;
|
|
2257
|
+
const n = Number.parseInt(digits, 10);
|
|
2258
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2259
|
+
}
|
|
2260
|
+
/** Extract a seq from a string-ish field ('@42', 'seq 42', '#42', bare 42). */
|
|
2261
|
+
function seqFromStringish(value) {
|
|
2262
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2263
|
+
if (typeof value !== "string") return void 0;
|
|
2264
|
+
const m = /(?:@|seq\s*|#)?(\d+)/i.exec(value);
|
|
2265
|
+
return m === null ? void 0 : parseSeqDigits(m[1]);
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Extract the rewind target seq from a rewind outcome text. Multi-pattern:
|
|
2269
|
+
* an explicit `seq 42` declaration wins, then `#42`, then the broad
|
|
2270
|
+
* english/chinese verb forms — a single "rewound" regex silently missed real
|
|
2271
|
+
* success texts like "已撤回 seq 42" / "Withdrawn seq 42".
|
|
2272
|
+
* @param text - the command outcome text.
|
|
2273
|
+
*/
|
|
2274
|
+
function rewindTargetOfOutcome(text) {
|
|
2275
|
+
if (typeof text !== "string") return void 0;
|
|
2276
|
+
const seqMatch = /seq\s*(\d+)/i.exec(text);
|
|
2277
|
+
if (seqMatch !== null) return parseSeqDigits(seqMatch[1]);
|
|
2278
|
+
const hashMatch = /#(\d+)/.exec(text);
|
|
2279
|
+
if (hashMatch !== null) return parseSeqDigits(hashMatch[1]);
|
|
2280
|
+
const broadMatch = /(?:rewound|withdrawn|已撤回).*?(?:target\s+)?(\d+)/i.exec(text);
|
|
2281
|
+
if (broadMatch !== null) return parseSeqDigits(broadMatch[1]);
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Resolve the target of a rewind command: structured fields first
|
|
2285
|
+
* (outcome.targetSeq / outcome.target / args.targetSeq / args.target /
|
|
2286
|
+
* args.seq / args.raw / args[0]), then the outcome text — so
|
|
2287
|
+
* locale-independent producers keep parsing without text heuristics (the
|
|
2288
|
+
* args.seq guard skips the command's own seq).
|
|
2289
|
+
* @param command - the command payload.
|
|
2290
|
+
*/
|
|
2291
|
+
function rewindTargetOfCommand(command) {
|
|
2292
|
+
if (command === null || typeof command !== "object") return void 0;
|
|
2293
|
+
const rec = command;
|
|
2294
|
+
const outcome = rec.outcome;
|
|
2295
|
+
if (outcome !== null && typeof outcome === "object") {
|
|
2296
|
+
const o = outcome;
|
|
2297
|
+
if (typeof o.targetSeq === "number" && Number.isFinite(o.targetSeq)) return o.targetSeq;
|
|
2298
|
+
if (typeof o.target === "number" && Number.isFinite(o.target)) return o.target;
|
|
2299
|
+
}
|
|
2300
|
+
const args = rec.args;
|
|
2301
|
+
if (args !== null && typeof args === "object") {
|
|
2302
|
+
const record = args;
|
|
2303
|
+
if (typeof record.targetSeq === "number" && Number.isFinite(record.targetSeq)) return record.targetSeq;
|
|
2304
|
+
if (typeof record.target === "number" && Number.isFinite(record.target)) return record.target;
|
|
2150
2305
|
if (typeof record.seq === "number" && Number.isFinite(record.seq) && record.seq !== rec.seq) return record.seq;
|
|
2151
2306
|
const rawish = record.raw ?? record["0"] ?? (Array.isArray(args) ? args[0] : void 0);
|
|
2152
2307
|
if (rawish !== void 0) {
|
|
@@ -2174,38 +2329,14 @@ window.__ModuleLoader__.load({
|
|
|
2174
2329
|
return args.preview === true || args.dryRun === true;
|
|
2175
2330
|
}
|
|
2176
2331
|
/**
|
|
2177
|
-
*
|
|
2178
|
-
*
|
|
2179
|
-
*
|
|
2180
|
-
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
2181
|
-
*/
|
|
2182
|
-
function hiddenSeqsOfChat(chat) {
|
|
2183
|
-
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
2184
|
-
for (const seq of rewindMarkedSeqsOfChat(chat)) hidden.add(seq);
|
|
2185
|
-
const spans = rewindSpansOfChat(chat);
|
|
2186
|
-
if (spans.length === 0) return hidden;
|
|
2187
|
-
const nodes = chat.nodes;
|
|
2188
|
-
if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
|
|
2189
|
-
for (const node of nodes.values()) {
|
|
2190
|
-
if (node === null || typeof node !== "object") continue;
|
|
2191
|
-
const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
|
|
2192
|
-
if (anchor === void 0) continue;
|
|
2193
|
-
if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
|
|
2194
|
-
}
|
|
2195
|
-
return hidden;
|
|
2196
|
-
}
|
|
2197
|
-
/**
|
|
2198
|
-
* Seq numbers of nodes carrying a rewind-hidden marker (upstream v0.1.5):
|
|
2199
|
-
* host/plugin producers can tag the node record itself (rewindHidden), its
|
|
2200
|
-
* data payload, or the payload's attributes map.
|
|
2332
|
+
* Seq numbers of nodes carrying a rewind-hidden marker: host/plugin producers
|
|
2333
|
+
* can tag the node record itself (rewindHidden), its data payload, or the
|
|
2334
|
+
* payload's attributes map.
|
|
2201
2335
|
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
2202
2336
|
*/
|
|
2203
2337
|
function rewindMarkedSeqsOfChat(chat) {
|
|
2204
2338
|
const marked = /* @__PURE__ */ new Set();
|
|
2205
|
-
const
|
|
2206
|
-
if (nodes === void 0 || typeof nodes.values !== "function") return marked;
|
|
2207
|
-
for (const node of nodes.values()) {
|
|
2208
|
-
if (node === null || typeof node !== "object") continue;
|
|
2339
|
+
for (const node of chatNodesOf(chat)) {
|
|
2209
2340
|
const data = node.data;
|
|
2210
2341
|
if (data === null || typeof data !== "object") continue;
|
|
2211
2342
|
const record = data;
|
|
@@ -2228,10 +2359,7 @@ window.__ModuleLoader__.load({
|
|
|
2228
2359
|
function rewindHiddenSeqsOfChat(chat) {
|
|
2229
2360
|
const hidden = /* @__PURE__ */ new Set();
|
|
2230
2361
|
if (chat === null || typeof chat !== "object") return hidden;
|
|
2231
|
-
const
|
|
2232
|
-
if (nodes === void 0 || typeof nodes.values !== "function") return hidden;
|
|
2233
|
-
for (const node of nodes.values()) {
|
|
2234
|
-
if (node === null || typeof node !== "object") continue;
|
|
2362
|
+
for (const node of chatNodesOf(chat)) {
|
|
2235
2363
|
if (node.kind !== "command") continue;
|
|
2236
2364
|
const command = node.data;
|
|
2237
2365
|
if (command === null || typeof command !== "object") continue;
|
|
@@ -2242,7 +2370,8 @@ window.__ModuleLoader__.load({
|
|
|
2242
2370
|
}
|
|
2243
2371
|
const outcome = command.outcome;
|
|
2244
2372
|
if (outcome === null || typeof outcome !== "object" || outcome.kind !== "success") continue;
|
|
2245
|
-
|
|
2373
|
+
const own = command.seq;
|
|
2374
|
+
if (typeof own === "number") hidden.add(own);
|
|
2246
2375
|
}
|
|
2247
2376
|
return hidden;
|
|
2248
2377
|
}
|
|
@@ -2255,18 +2384,17 @@ window.__ModuleLoader__.load({
|
|
|
2255
2384
|
function rewindSpansOfChat(chat) {
|
|
2256
2385
|
const spans = [];
|
|
2257
2386
|
if (chat === null || typeof chat !== "object") return spans;
|
|
2258
|
-
const
|
|
2259
|
-
if (nodes === void 0 || typeof nodes.values !== "function") return spans;
|
|
2260
|
-
for (const node of nodes.values()) {
|
|
2261
|
-
if (node === null || typeof node !== "object") continue;
|
|
2387
|
+
for (const node of chatNodesOf(chat)) {
|
|
2262
2388
|
if (node.kind !== "command") continue;
|
|
2263
2389
|
const command = node.data;
|
|
2264
2390
|
if (command === null || typeof command !== "object") continue;
|
|
2265
2391
|
if (command.name !== "rewind") continue;
|
|
2266
2392
|
if (isRewindPreviewCommand(command)) continue;
|
|
2267
2393
|
const outcome = command.outcome;
|
|
2268
|
-
if (outcome === null || typeof outcome !== "object"
|
|
2269
|
-
const
|
|
2394
|
+
if (outcome === null || typeof outcome !== "object") continue;
|
|
2395
|
+
const record = outcome;
|
|
2396
|
+
if (record.kind !== "success") continue;
|
|
2397
|
+
const marker = record.sourceEventSeq;
|
|
2270
2398
|
if (typeof marker !== "number") continue;
|
|
2271
2399
|
const target = rewindTargetOfCommand(command);
|
|
2272
2400
|
if (target !== void 0) spans.push({
|
|
@@ -2277,51 +2405,22 @@ window.__ModuleLoader__.load({
|
|
|
2277
2405
|
return spans;
|
|
2278
2406
|
}
|
|
2279
2407
|
/**
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2408
|
+
* Collect the seq numbers a rewind hid: preview commands hide their target
|
|
2409
|
+
* directly; successful commands hide their own row plus every node anchored
|
|
2410
|
+
* inside the rewound span [target, sourceEventSeq].
|
|
2411
|
+
* @param chat - a chat snapshot (loosely typed on purpose).
|
|
2283
2412
|
*/
|
|
2284
|
-
function
|
|
2285
|
-
const
|
|
2286
|
-
const
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
if (
|
|
2292
|
-
if (
|
|
2293
|
-
const data = node.data;
|
|
2294
|
-
if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
2295
|
-
const key = typeof node.key === "string" ? node.key : void 0;
|
|
2296
|
-
if (key === void 0) continue;
|
|
2297
|
-
const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
|
|
2298
|
-
if (hidden.has(seq)) continue;
|
|
2299
|
-
out.push({
|
|
2300
|
-
seq,
|
|
2301
|
-
time: data.time,
|
|
2302
|
-
text: userTextOf(data.content),
|
|
2303
|
-
key
|
|
2304
|
-
});
|
|
2413
|
+
function hiddenSeqsOfChat(chat) {
|
|
2414
|
+
const hidden = rewindHiddenSeqsOfChat(chat);
|
|
2415
|
+
for (const seq of rewindMarkedSeqsOfChat(chat)) hidden.add(seq);
|
|
2416
|
+
const spans = rewindSpansOfChat(chat);
|
|
2417
|
+
if (spans.length === 0) return hidden;
|
|
2418
|
+
for (const node of chatNodesOf(chat)) {
|
|
2419
|
+
const anchor = typeof node.anchorSeq === "number" ? node.anchorSeq : void 0;
|
|
2420
|
+
if (anchor === void 0) continue;
|
|
2421
|
+
if (spans.some((span) => anchor >= span.start && anchor <= span.end)) hidden.add(anchor);
|
|
2305
2422
|
}
|
|
2306
|
-
|
|
2307
|
-
return out;
|
|
2308
|
-
}
|
|
2309
|
-
/**
|
|
2310
|
-
* Count the user messages the loaded window holds WITHOUT collecting or
|
|
2311
|
-
* sorting them — the fast gate railMessages uses to skip the merge path on
|
|
2312
|
-
* the streaming hot path (see there).
|
|
2313
|
-
* @param chat - the loaded chat snapshot.
|
|
2314
|
-
*/
|
|
2315
|
-
function countWindowUserMessages(chat) {
|
|
2316
|
-
const nodes = chat?.nodes;
|
|
2317
|
-
if (nodes === void 0 || typeof nodes.values !== "function") return 0;
|
|
2318
|
-
let count = 0;
|
|
2319
|
-
for (const node of nodes.values()) if (node !== null && typeof node === "object" && node.kind === "user") count += 1;
|
|
2320
|
-
return count;
|
|
2321
|
-
}
|
|
2322
|
-
/** Resolve the DOM anchor key of one entry (direct key form only). */
|
|
2323
|
-
function resolveAnchorKey(m) {
|
|
2324
|
-
return typeof m.key === "string" && m.key !== "" ? m.key : void 0;
|
|
2423
|
+
return hidden;
|
|
2325
2424
|
}
|
|
2326
2425
|
/** The user-message node definition kind in ui-conversation. */
|
|
2327
2426
|
const INPUT_MESSAGE_KIND = "input-message";
|
|
@@ -2355,15 +2454,53 @@ window.__ModuleLoader__.load({
|
|
|
2355
2454
|
out.push({
|
|
2356
2455
|
seq: rec.seq,
|
|
2357
2456
|
time: typeof rec.time === "number" ? rec.time : 0,
|
|
2358
|
-
text: typeof rec.text === "string" ? rec.text.slice(0,
|
|
2359
|
-
...typeof rec.id === "string" && rec.id !== "" ? {
|
|
2457
|
+
text: typeof rec.text === "string" ? rec.text.slice(0, MAX_TEXT_CHARS) : "",
|
|
2458
|
+
...typeof rec.id === "string" && rec.id !== "" ? { anchorKey: inputAnchorKeyOf(rec.id) } : {}
|
|
2360
2459
|
});
|
|
2361
2460
|
}
|
|
2362
2461
|
out.sort((a, b) => a.seq - b.seq);
|
|
2363
2462
|
return out;
|
|
2364
2463
|
}
|
|
2365
2464
|
/**
|
|
2366
|
-
*
|
|
2465
|
+
* Enumerate user messages from the loaded chat-node snapshot (the client-side
|
|
2466
|
+
* collector; sorted by anchor sequence, rewind-hidden entries dropped).
|
|
2467
|
+
* @param snapshot - a session snapshot or Chat snapshot (loosely typed).
|
|
2468
|
+
*/
|
|
2469
|
+
function collectMessages(snapshot) {
|
|
2470
|
+
const hidden = hiddenSeqsOfChat(snapshot);
|
|
2471
|
+
const out = [];
|
|
2472
|
+
for (const node of chatNodesOf(snapshot)) {
|
|
2473
|
+
if (node.kind !== "user") continue;
|
|
2474
|
+
if (!isVisible(node)) continue;
|
|
2475
|
+
const data = node.data;
|
|
2476
|
+
if (data === null || typeof data !== "object" || typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
2477
|
+
const anchorKey = typeof node.key === "string" ? node.key : void 0;
|
|
2478
|
+
if (anchorKey === void 0) continue;
|
|
2479
|
+
const seq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
|
|
2480
|
+
if (hidden.has(seq)) continue;
|
|
2481
|
+
out.push({
|
|
2482
|
+
seq,
|
|
2483
|
+
time: data.time,
|
|
2484
|
+
text: userTextOf(data.content),
|
|
2485
|
+
anchorKey
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
2489
|
+
return out;
|
|
2490
|
+
}
|
|
2491
|
+
/**
|
|
2492
|
+
* Count the user messages the loaded window holds WITHOUT collecting or
|
|
2493
|
+
* sorting them — the fast gate railMessages uses to skip the merge path on
|
|
2494
|
+
* the streaming hot path (see there).
|
|
2495
|
+
* @param snapshot - the loaded chat snapshot.
|
|
2496
|
+
*/
|
|
2497
|
+
function countWindowUserMessages(snapshot) {
|
|
2498
|
+
let count = 0;
|
|
2499
|
+
for (const node of chatNodesOf(snapshot)) if (node.kind === "user" && isVisible(node)) count += 1;
|
|
2500
|
+
return count;
|
|
2501
|
+
}
|
|
2502
|
+
/**
|
|
2503
|
+
* Rail entries: the host projection (whole-session index) MERGED with the
|
|
2367
2504
|
* loaded chat-node window. The projection normally wins outright, but a
|
|
2368
2505
|
* projection that lost its baseline (e.g. the plugin was hot-reloaded after
|
|
2369
2506
|
* the session's tail page seeded the client store) can miss early entries —
|
|
@@ -2376,14 +2513,13 @@ window.__ModuleLoader__.load({
|
|
|
2376
2513
|
*/
|
|
2377
2514
|
function railMessages(snapshot, projected) {
|
|
2378
2515
|
const projectedMessages = normalizeProjectedTimeline(projected);
|
|
2379
|
-
const
|
|
2380
|
-
const
|
|
2381
|
-
const
|
|
2382
|
-
const spans = rewindSpansOfChat(chat);
|
|
2516
|
+
const hidden = rewindHiddenSeqsOfChat(snapshot);
|
|
2517
|
+
const marked = rewindMarkedSeqsOfChat(snapshot);
|
|
2518
|
+
const spans = rewindSpansOfChat(snapshot);
|
|
2383
2519
|
const isHidden = (seq) => hidden.has(seq) || marked.has(seq) || spans.some((span) => seq >= span.start && seq <= span.end);
|
|
2384
2520
|
if (projectedMessages.length === 0) return collectMessages(snapshot);
|
|
2385
|
-
const windowUserCount = countWindowUserMessages(
|
|
2386
|
-
const projectionLacksKeys = projectedMessages.some((m) => m.
|
|
2521
|
+
const windowUserCount = countWindowUserMessages(snapshot);
|
|
2522
|
+
const projectionLacksKeys = projectedMessages.some((m) => m.anchorKey === void 0);
|
|
2387
2523
|
if (windowUserCount <= projectedMessages.length && !projectionLacksKeys) return marked.size === 0 && hidden.size === 0 && spans.length === 0 ? projectedMessages : projectedMessages.filter((m) => !isHidden(m.seq));
|
|
2388
2524
|
const windowMessages = collectMessages(snapshot);
|
|
2389
2525
|
const bySeq = /* @__PURE__ */ new Map();
|
|
@@ -2395,465 +2531,290 @@ window.__ModuleLoader__.load({
|
|
|
2395
2531
|
bySeq.set(m.seq, m);
|
|
2396
2532
|
continue;
|
|
2397
2533
|
}
|
|
2398
|
-
if (existing.
|
|
2534
|
+
if (existing.anchorKey === void 0) bySeq.set(m.seq, {
|
|
2399
2535
|
...existing,
|
|
2400
|
-
|
|
2536
|
+
anchorKey: m.anchorKey,
|
|
2401
2537
|
...existing.text === "" && m.text !== "" ? { text: m.text } : {}
|
|
2402
2538
|
});
|
|
2403
2539
|
}
|
|
2404
2540
|
return [...bySeq.values()].sort((a, b) => a.seq - b.seq);
|
|
2405
2541
|
}
|
|
2542
|
+
//#endregion
|
|
2543
|
+
//#region src/client/timeline/legacy-rail-css.ts
|
|
2406
2544
|
/**
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
*
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2422
|
-
|
|
2423
|
-
function railWidthFor(texts) {
|
|
2424
|
-
if (texts.length > 8) return RAIL_MAX_WIDTH;
|
|
2425
|
-
let widest = 0;
|
|
2426
|
-
if (typeof document !== "undefined") {
|
|
2427
|
-
const ctx = document.createElement("canvas").getContext("2d");
|
|
2428
|
-
if (ctx) {
|
|
2429
|
-
ctx.font = `13px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
|
|
2430
|
-
for (const t of texts) {
|
|
2431
|
-
const w = ctx.measureText(t === "" ? "…" : t).width;
|
|
2432
|
-
if (w > widest) widest = w;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
return Math.round(Math.max(RAIL_MIN_WIDTH, Math.min(RAIL_MAX_WIDTH, widest + 6 + 24 + 16 + 12)));
|
|
2437
|
-
}
|
|
2438
|
-
/** How long one jump may keep paging older history before giving up. */
|
|
2439
|
-
const JUMP_PAGE_DEADLINE_MS = 3e4;
|
|
2440
|
-
/** How long to wait for the chat view to commit the target row. */
|
|
2441
|
-
const JUMP_ROW_WAIT_MS = 3e3;
|
|
2442
|
-
/** How long the jump-stabilization lock may stay engaged without settling. */
|
|
2443
|
-
const JUMP_LOCK_FALLBACK_MS = 2e3;
|
|
2444
|
-
/**
|
|
2445
|
-
* Poll the conversation DOM until the target row is committed. The session
|
|
2446
|
-
* store updates BEFORE React renders the prepended page, so the old one-shot
|
|
2447
|
-
* query right after loadOlder() resolved could miss the row and silently
|
|
2448
|
-
* give up — the reported "click does nothing until the conversation is
|
|
2449
|
-
* nudged (scroll/click)" behavior.
|
|
2450
|
-
* @param key - chat anchor key to look for.
|
|
2451
|
-
* @param timeoutMs - polling budget.
|
|
2452
|
-
*/
|
|
2453
|
-
async function waitForChatRow(key, timeoutMs) {
|
|
2454
|
-
const scrollports = Array.from(document.querySelectorAll("[data-conversation-scroll]"));
|
|
2455
|
-
if (scrollports.length === 0) return null;
|
|
2456
|
-
const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
|
|
2457
|
-
const deadline = Date.now() + timeoutMs;
|
|
2458
|
-
for (;;) {
|
|
2459
|
-
for (const scrollport of scrollports) {
|
|
2460
|
-
const row = scrollport.querySelector(selector);
|
|
2461
|
-
if (row !== null) return row;
|
|
2462
|
-
}
|
|
2463
|
-
if (Date.now() >= deadline) return null;
|
|
2464
|
-
await delay(40);
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
/** Official conversation follow threshold: the view keeps bottom-following while within this distance of the floor (dsh-client-ui-conversation, 25px). */
|
|
2468
|
-
const CONVERSATION_FOLLOW_ZONE_PX = 25;
|
|
2469
|
-
/** Distances under this snap instead of animating. */
|
|
2470
|
-
const MIN_GLIDE_DISTANCE_PX = 8;
|
|
2471
|
-
/** Glide duration floor / cap — a far-boundary rail click stays a tangible slide. */
|
|
2472
|
-
const MIN_GLIDE_MS = 280;
|
|
2473
|
-
const MAX_GLIDE_MS = 900;
|
|
2474
|
-
/** Maximum scrollable offset of a conversation scrollport. */
|
|
2475
|
-
function scrollFloorOf(el) {
|
|
2476
|
-
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* Destination scrollTop that centers `row` inside `el` — the same geometry
|
|
2480
|
-
* as scrollIntoView({ block: 'center' }), computed directly so the glide owns
|
|
2481
|
-
* the scroll position instead of the browser.
|
|
2482
|
-
* @param el - the conversation scrollport.
|
|
2483
|
-
* @param row - the target chat row.
|
|
2484
|
-
*/
|
|
2485
|
-
function centeredScrollTopFor(el, row) {
|
|
2486
|
-
const floor = scrollFloorOf(el);
|
|
2487
|
-
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
|
|
2488
|
-
if (!Number.isFinite(target)) return el.scrollTop;
|
|
2489
|
-
return Math.max(0, Math.min(floor, target));
|
|
2490
|
-
}
|
|
2491
|
-
/**
|
|
2492
|
-
* Glide duration for a distance: longer slides take longer, bounded to
|
|
2493
|
-
* [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history rail click reads as a
|
|
2494
|
-
* deliberate glide, not a sluggish race).
|
|
2495
|
-
* @param distance - px to travel.
|
|
2496
|
-
*/
|
|
2497
|
-
function glideDurationFor(distance) {
|
|
2498
|
-
if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
|
|
2499
|
-
return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
|
|
2500
|
-
}
|
|
2501
|
-
/**
|
|
2502
|
-
* Detach the official bottom-follow state when the reader currently sits
|
|
2503
|
-
* inside its follow zone: assign the scroll position just outside the 25px
|
|
2504
|
-
* boundary. That one synchronous scroll event settles at-bottom to false
|
|
2505
|
-
* (moved-by-reader + outside the zone) so a tip-moved re-render cannot yank
|
|
2506
|
-
* the glide back to the floor. No-op when already above the zone or when
|
|
2507
|
-
* the assignment cannot move (content shorter than the viewport).
|
|
2508
|
-
* @param el - the conversation scrollport.
|
|
2509
|
-
*/
|
|
2510
|
-
function detachBottomFollow(el) {
|
|
2511
|
-
const floor = scrollFloorOf(el);
|
|
2512
|
-
if (el.scrollTop < floor - CONVERSATION_FOLLOW_ZONE_PX) return false;
|
|
2513
|
-
const detached = Math.max(0, floor - CONVERSATION_FOLLOW_ZONE_PX - 1);
|
|
2514
|
-
if (Math.abs(el.scrollTop - detached) < 1) return false;
|
|
2515
|
-
el.scrollTop = detached;
|
|
2516
|
-
return true;
|
|
2517
|
-
}
|
|
2518
|
-
/**
|
|
2519
|
-
* Wait until a detach assignment's scroll event has been dispatched AND
|
|
2520
|
-
* consumed by the official scroll handler: scroll events fire on a later
|
|
2521
|
-
* frame than the assignment, and a paging commit's layout effect runs
|
|
2522
|
-
* before that dispatch — it would re-run the follow logic while the
|
|
2523
|
-
* at-bottom flag is still true and drag the viewport to the growing floor,
|
|
2524
|
-
* undoing the detach (two rAFs + a macrotask bracket the dispatch window).
|
|
2545
|
+
* Styles for the legacy port of the official turn rail (legacy-rail.tsx).
|
|
2546
|
+
*
|
|
2547
|
+
* A faithful port of `TurnNavigator.module.css` from dsh-client-ui-chat, kept
|
|
2548
|
+
* on the plugin's own `dsbt-` prefix because this rail isportaled to body
|
|
2549
|
+
* instead of living inside the chat scrollport. Metrics are the official ones
|
|
2550
|
+
* verbatim (28px column, 10px tick spacing, 6px end inset, 12/18/20px tick
|
|
2551
|
+
* widths, 420px height ceiling, 100px preview, 300px preview width).
|
|
2552
|
+
*
|
|
2553
|
+
* Glass + edge fade (unlike stock chrome): the rail is background plugin
|
|
2554
|
+
* chrome over the user's art, so the hover preview joins the shared glass
|
|
2555
|
+
* recipe under body[data-dsh-bg-glass] (fill = the painter's composer token
|
|
2556
|
+
* + the shared blur/sheen — the same strength as the composer card), and
|
|
2557
|
+
* the marks column gets the DeepSeek-web edge dissolve (top/bottom ticks
|
|
2558
|
+
* fade instead of hard-clipping). The fade is paint-only, always on; the
|
|
2559
|
+
* glass follows the wallpaper/panel-opacity sliders exactly like every
|
|
2560
|
+
* other glassed surface. Both kernel generations render identically.
|
|
2525
2561
|
*/
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2562
|
+
/** Unique id stamped on the injected style element (dedup key). */
|
|
2563
|
+
const TIMELINE_CSS_TAG = "deepseek-harness-background/timeline";
|
|
2564
|
+
/** Inject the rail stylesheet once. Idempotent (dedup key on the tag). */
|
|
2565
|
+
function injectTimelineCss() {
|
|
2566
|
+
if (typeof document === "undefined") return;
|
|
2567
|
+
if (document.querySelector(`style[data-plugin-css="deepseek-harness-background/timeline"]`)) return;
|
|
2568
|
+
const tag = document.createElement("style");
|
|
2569
|
+
tag.dataset.plugin = "deepseek-harness-background";
|
|
2570
|
+
tag.dataset.pluginCss = TIMELINE_CSS_TAG;
|
|
2571
|
+
tag.textContent = TIMELINE_CSS;
|
|
2572
|
+
document.head.appendChild(tag);
|
|
2531
2573
|
}
|
|
2574
|
+
const TIMELINE_CSS = `
|
|
2575
|
+
/* Portaled host: the official slot is a zero-height sticky box inside the
|
|
2576
|
+
chat scrollport; a fixed box centred on the transcript band is the
|
|
2577
|
+
equivalent outside it. */
|
|
2578
|
+
.dsbt-slot {
|
|
2579
|
+
position: fixed; top: 50%; right: 16px; transform: translateY(-50%);
|
|
2580
|
+
z-index: 6; display: flex; align-items: center; pointer-events: none;
|
|
2581
|
+
}
|
|
2582
|
+
@media (max-width: 767px) { .dsbt-slot { display: none; } }
|
|
2583
|
+
|
|
2584
|
+
.dsbt-rail {
|
|
2585
|
+
position: relative; width: 28px; cursor: pointer; pointer-events: auto;
|
|
2586
|
+
transition: height 220ms cubic-bezier(.2, .8, .2, 1);
|
|
2587
|
+
height: min(var(--dsbt-natural-h, 12px), var(--dsbt-band, 420px), 420px);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
.dsbt-marks {
|
|
2591
|
+
position: absolute; inset: var(--dsbt-inset, 6px) 0;
|
|
2592
|
+
/* No edge dissolve here: the first and last tick dashes straddle the
|
|
2593
|
+
marks box edges by 1px, so any fade zone sized off the box height
|
|
2594
|
+
swallows them (at a 10-turn rail the 8% recipe rendered the ends at
|
|
2595
|
+
~14% opacity). The end ticks must stay fully painted. */
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
.dsbt-markPosition {
|
|
2599
|
+
position: absolute; top: min(var(--dsbt-natural, 0px), var(--dsbt-ratio, 0%));
|
|
2600
|
+
right: 0; left: 0; height: 10px; transform: translateY(-50%);
|
|
2601
|
+
transition: top 220ms cubic-bezier(.2, .8, .2, 1);
|
|
2602
|
+
animation: dsbt-mark-enter 150ms ease-out;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/* The rail owns pointer input for the whole column, so a mark is a keyboard
|
|
2606
|
+
destination that paints one tick — never a mouse target of its own. */
|
|
2607
|
+
.dsbt-mark {
|
|
2608
|
+
position: absolute; inset: 0 0 0 auto; width: 20px; padding: 0; border: 0;
|
|
2609
|
+
border-radius: 8px; background: transparent; cursor: pointer;
|
|
2610
|
+
pointer-events: none;
|
|
2611
|
+
}
|
|
2612
|
+
.dsbt-mark::before {
|
|
2613
|
+
position: absolute; top: 50%; right: 0; width: 12px; height: 2px;
|
|
2614
|
+
border-radius: 2px; background: var(--dsw-alias-border-l4);
|
|
2615
|
+
content: ''; transform: translateY(-50%);
|
|
2616
|
+
transition: width 140ms ease, background-color 140ms ease;
|
|
2617
|
+
}
|
|
2618
|
+
.dsbt-markPreview::before { width: 18px; background: var(--dsw-alias-label-tertiary); }
|
|
2619
|
+
.dsbt-markActive::before { width: 20px; background: var(--dsw-alias-label-primary); }
|
|
2620
|
+
.dsbt-mark:focus-visible::before {
|
|
2621
|
+
width: 20px; background: var(--dsw-alias-state-business-primary);
|
|
2622
|
+
}
|
|
2623
|
+
.dsbt-mark:focus-visible {
|
|
2624
|
+
outline: 1px solid var(--dsw-alias-state-business-primary);
|
|
2625
|
+
outline-offset: 2px;
|
|
2626
|
+
}
|
|
2627
|
+
/* Entries without an anchor key (early events lacking a durable id) cannot
|
|
2628
|
+
jump; dim them instead of silently eating the click. */
|
|
2629
|
+
.dsbt-mark:disabled::before { width: 8px; opacity: .5; }
|
|
2630
|
+
|
|
2631
|
+
.dsbt-preview {
|
|
2632
|
+
--dsbt-preview-h: 100px;
|
|
2633
|
+
position: absolute;
|
|
2634
|
+
top: clamp(
|
|
2635
|
+
0px,
|
|
2636
|
+
calc(min(var(--dsbt-natural, 0px), var(--dsbt-ratio, 0%))
|
|
2637
|
+
+ var(--dsbt-inset, 6px) - var(--dsbt-preview-h) / 2),
|
|
2638
|
+
calc(100% - var(--dsbt-preview-h))
|
|
2639
|
+
);
|
|
2640
|
+
right: calc(100% + 10px); box-sizing: border-box;
|
|
2641
|
+
width: min(300px, calc(100vw - 120px)); max-height: var(--dsbt-preview-h);
|
|
2642
|
+
overflow: hidden; padding: 10px 12px;
|
|
2643
|
+
border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px;
|
|
2644
|
+
color: var(--dsw-alias-label-primary);
|
|
2645
|
+
background: var(--dsw-alias-bg-layer-1); box-shadow: var(--dsw-shadow-lv2);
|
|
2646
|
+
font: var(--dsw-font-xs-strong-13); pointer-events: none;
|
|
2647
|
+
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4;
|
|
2648
|
+
animation: dsbt-preview-enter 120ms ease-out;
|
|
2649
|
+
transition: top 140ms cubic-bezier(.2, .8, .2, 1);
|
|
2650
|
+
/* Content dissolve: a clamped preview's last lines soften out instead of
|
|
2651
|
+
hard-cutting. No effect on short copy (the mask's fade zone rides the
|
|
2652
|
+
empty tail) and no effect on ticks — it lives on the preview only. */
|
|
2653
|
+
mask-image: linear-gradient(180deg, #000 86%, transparent 100%);
|
|
2654
|
+
-webkit-mask-image: linear-gradient(180deg, #000 86%, transparent 100%);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/* Hover preview is glass chrome just like the composer card: its official
|
|
2658
|
+
fill is the opaque bg-layer-1 token, so it needs the explicit-fill
|
|
2659
|
+
recipe (painter's composer token + shared blur/sheen chain). Gated on
|
|
2660
|
+
data-dsh-bg-glass like every other glassed surface. */
|
|
2661
|
+
body[data-dsh-bg-glass] .dsbt-preview {
|
|
2662
|
+
background-color: var(--dsw-specific-input-major);
|
|
2663
|
+
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));
|
|
2664
|
+
-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);
|
|
2665
|
+
backdrop-filter: blur(var(--bg-glass-blur, 16px)) saturate(var(--bg-glass-saturate, 1.42)) brightness(var(--bg-glass-brightness, 1)) contrast(1.01);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
@keyframes dsbt-mark-enter { from { opacity: 0; } to { opacity: 1; } }
|
|
2669
|
+
@keyframes dsbt-preview-enter {
|
|
2670
|
+
from { opacity: 0; transform: translateX(4px); }
|
|
2671
|
+
to { opacity: 1; transform: translateX(0); }
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2675
|
+
.dsbt-rail, .dsbt-markPosition, .dsbt-mark::before, .dsbt-preview {
|
|
2676
|
+
transition: none; animation: none;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
`;
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/client/timeline/legacy-rail.tsx
|
|
2532
2682
|
/**
|
|
2533
|
-
*
|
|
2534
|
-
*
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2683
|
+
* Legacy turn rail — the official `TurnNavigator` UI ported into this plugin
|
|
2684
|
+
* for kernels that do not publish an official turn-navigation index
|
|
2685
|
+
* (dsh <= 0.1.1).
|
|
2686
|
+
*
|
|
2687
|
+
* Structure, metrics and paints mirror `TurnNavigator.tsx` /
|
|
2688
|
+
* `TurnNavigator.module.css` from dsh-client-ui-chat; styles live in
|
|
2689
|
+
* ./legacy-rail-css.ts. The only differences are the ones a portaled box
|
|
2690
|
+
* forces:
|
|
2691
|
+
* - The official slot is a zero-height sticky box inside the chat scrollport;
|
|
2692
|
+
* this one is a fixed box centred on the same band (the transcript minus
|
|
2693
|
+
* the composer), positioned from the scrollport's live rect.
|
|
2694
|
+
* - The official rail reads the turn index the kernel assembles; this one
|
|
2695
|
+
* reads the shared backend (./source.ts), i.e. the host projection when it
|
|
2696
|
+
* exists and the loaded node window otherwise. That is why it can offer
|
|
2697
|
+
* turns the chat view has not loaded yet.
|
|
2698
|
+
*
|
|
2699
|
+
* Behaviour is identical to the enhanced official rail because both run the
|
|
2700
|
+
* same jump engine (./jump.ts): a click glides instead of teleporting, and
|
|
2701
|
+
* reaching for the rail pages older history in up to its uncompressed
|
|
2702
|
+
* capacity with the reader's view held still.
|
|
2540
2703
|
*/
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
};
|
|
2556
|
-
const onWheel = () => finish(false);
|
|
2557
|
-
const onTouch = () => finish(false);
|
|
2558
|
-
const onKey = (e) => {
|
|
2559
|
-
if ([
|
|
2560
|
-
"ArrowUp",
|
|
2561
|
-
"ArrowDown",
|
|
2562
|
-
"PageUp",
|
|
2563
|
-
"PageDown",
|
|
2564
|
-
"Home",
|
|
2565
|
-
"End",
|
|
2566
|
-
" "
|
|
2567
|
-
].includes(e.key)) finish(false);
|
|
2568
|
-
};
|
|
2569
|
-
el.addEventListener("wheel", onWheel, { passive: true });
|
|
2570
|
-
el.addEventListener("touchstart", onTouch, { passive: true });
|
|
2571
|
-
window.addEventListener("keydown", onKey);
|
|
2572
|
-
if (Math.abs(delta) < .5 || durationMs <= 0) {
|
|
2573
|
-
el.scrollTop = target;
|
|
2574
|
-
finish(true);
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
const t0 = performance.now();
|
|
2578
|
-
const ease = (x) => x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
2579
|
-
const step = (now) => {
|
|
2580
|
-
if (done) return;
|
|
2581
|
-
const t = Math.min(1, (now - t0) / durationMs);
|
|
2582
|
-
el.scrollTop = start + delta * ease(t);
|
|
2583
|
-
if (t < 1) frame = requestAnimationFrame(step);
|
|
2584
|
-
else finish(true);
|
|
2585
|
-
};
|
|
2586
|
-
frame = requestAnimationFrame(step);
|
|
2587
|
-
});
|
|
2704
|
+
if (typeof document !== "undefined") injectTimelineCss();
|
|
2705
|
+
/** Composer height assumed when neither the seat nor the kernel's own var is available. */
|
|
2706
|
+
const FALLBACK_COMPOSER_PX = 152;
|
|
2707
|
+
/** The official stylesheet's own ceiling and band deduction. */
|
|
2708
|
+
const RAIL_MAX_HEIGHT_PX = 420;
|
|
2709
|
+
const RAIL_BAND_DEDUCTION_PX = 64;
|
|
2710
|
+
/** How often the rail's host band and offset are re-measured. */
|
|
2711
|
+
const MEASURE_INTERVAL_MS = 400;
|
|
2712
|
+
/** Read one px-valued custom property off an element (undefined when absent). */
|
|
2713
|
+
function cssPx(el, name) {
|
|
2714
|
+
const raw = el.style.getPropertyValue(name);
|
|
2715
|
+
if (raw === "") return void 0;
|
|
2716
|
+
const px = Number.parseFloat(raw);
|
|
2717
|
+
return Number.isFinite(px) ? px : void 0;
|
|
2588
2718
|
}
|
|
2589
2719
|
/**
|
|
2590
|
-
*
|
|
2591
|
-
*
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
2720
|
+
* The band the rail may occupy: the transcript the reader actually sees, i.e.
|
|
2721
|
+
* the scrollport minus the sticky composer stack — the official measurement,
|
|
2722
|
+
* taken from the kernel's own vars when it publishes them (0.1.2+) and from
|
|
2723
|
+
* the composer seat otherwise.
|
|
2724
|
+
* @param sp - the conversation scrollport.
|
|
2594
2725
|
*/
|
|
2595
|
-
|
|
2596
|
-
const
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
el.dispatchEvent(new Event("scroll"));
|
|
2601
|
-
detachedAny = true;
|
|
2602
|
-
}
|
|
2603
|
-
if (detachedAny) await settleScrollEvents();
|
|
2604
|
-
const deadline = Date.now() + JUMP_PAGE_DEADLINE_MS;
|
|
2605
|
-
for (;;) {
|
|
2606
|
-
const snapshot = session.getSnapshot();
|
|
2607
|
-
if (snapshot?.chat?.nodes?.get(key) !== void 0) break;
|
|
2608
|
-
if (snapshot?.hasMore !== true) break;
|
|
2609
|
-
if (Date.now() >= deadline) break;
|
|
2610
|
-
if (snapshot.loadingOlder === true) {
|
|
2611
|
-
await delay(50);
|
|
2612
|
-
continue;
|
|
2613
|
-
}
|
|
2614
|
-
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
2615
|
-
await delay(100);
|
|
2616
|
-
continue;
|
|
2617
|
-
}
|
|
2618
|
-
try {
|
|
2619
|
-
await session.loadOlder();
|
|
2620
|
-
} catch (error) {
|
|
2621
|
-
console.warn("[deepseek-harness-background] timeline jump paging failed:", error);
|
|
2622
|
-
break;
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
const row = await waitForChatRow(key, rowWaitMs);
|
|
2626
|
-
if (row === null) {
|
|
2627
|
-
console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${key}`);
|
|
2628
|
-
return false;
|
|
2629
|
-
}
|
|
2630
|
-
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2631
|
-
const scroller = row.closest("[data-conversation-scroll]");
|
|
2632
|
-
if (scroller === null) {
|
|
2633
|
-
row.scrollIntoView({
|
|
2634
|
-
behavior: "auto",
|
|
2635
|
-
block: "center"
|
|
2636
|
-
});
|
|
2637
|
-
if (!reducedMotion) row.scrollIntoView({
|
|
2638
|
-
behavior: "smooth",
|
|
2639
|
-
block: "center"
|
|
2640
|
-
});
|
|
2641
|
-
return true;
|
|
2642
|
-
}
|
|
2643
|
-
const target = centeredScrollTopFor(scroller, row);
|
|
2644
|
-
if (reducedMotion || Math.abs(target - scroller.scrollTop) < MIN_GLIDE_DISTANCE_PX) {
|
|
2645
|
-
scroller.scrollTop = target;
|
|
2646
|
-
return true;
|
|
2647
|
-
}
|
|
2648
|
-
if (detachBottomFollow(scroller)) {
|
|
2649
|
-
scroller.dispatchEvent(new Event("scroll"));
|
|
2650
|
-
await settleScrollEvents();
|
|
2651
|
-
}
|
|
2652
|
-
return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
|
|
2726
|
+
function bandOf(sp) {
|
|
2727
|
+
const viewport = cssPx(sp, "--dsh-conversation-viewport-height") ?? sp.clientHeight;
|
|
2728
|
+
const seat = sp.querySelector("[data-composer-seat]");
|
|
2729
|
+
const composer = cssPx(sp, "--dsh-composer-height") ?? seat?.offsetHeight ?? FALLBACK_COMPOSER_PX;
|
|
2730
|
+
return Math.max(0, Math.min(RAIL_MAX_HEIGHT_PX, viewport - composer - RAIL_BAND_DEDUCTION_PX));
|
|
2653
2731
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
const pageTop = page.scrollTop;
|
|
2661
|
-
const pageBottom = pageTop + page.clientHeight;
|
|
2662
|
-
const itemTop = item.offsetTop;
|
|
2663
|
-
const itemBottom = itemTop + item.offsetHeight;
|
|
2664
|
-
if (itemTop < pageTop) page.scrollTop = Math.max(0, itemTop - 10);
|
|
2665
|
-
else if (itemBottom > pageBottom) page.scrollTop = itemBottom - page.clientHeight + 10;
|
|
2732
|
+
/** Tick metrics for one count, mirroring the official `railSize`. */
|
|
2733
|
+
function railSize(count) {
|
|
2734
|
+
return {
|
|
2735
|
+
["--dsbt-natural-h"]: `${(count - 1) * 10 + 12}px`,
|
|
2736
|
+
["--dsbt-inset"]: `6px`
|
|
2737
|
+
};
|
|
2666
2738
|
}
|
|
2667
|
-
/**
|
|
2668
|
-
function
|
|
2669
|
-
|
|
2739
|
+
/** Position of one tick, mirroring the official `itemPosition`. */
|
|
2740
|
+
function tickPosition(index, count) {
|
|
2741
|
+
const ratio = count <= 1 ? 0 : index / (count - 1);
|
|
2742
|
+
return {
|
|
2743
|
+
["--dsbt-natural"]: `${index * 10}px`,
|
|
2744
|
+
["--dsbt-ratio"]: `${ratio * 100}%`
|
|
2745
|
+
};
|
|
2670
2746
|
}
|
|
2671
2747
|
/**
|
|
2672
|
-
* The
|
|
2673
|
-
*
|
|
2748
|
+
* The legacy turn rail.
|
|
2749
|
+
* @param props - dock slot props plus the enabled gate.
|
|
2750
|
+
* @returns the portaled rail, or null when it must not render.
|
|
2674
2751
|
*/
|
|
2675
|
-
function
|
|
2676
|
-
const { sessionId, sessionsService, useProjection, t } = props;
|
|
2677
|
-
const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
|
|
2678
|
-
const timelineUnknown = settings.status === "loading";
|
|
2679
|
-
const timelineDisabled = settings.value?.timeline === false;
|
|
2752
|
+
function LegacyTimelineRail(props) {
|
|
2753
|
+
const { sessionId, sessionsService, useProjection, enabled, t } = props;
|
|
2680
2754
|
const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
|
|
2681
2755
|
const subscribeSession = react.default.useCallback((listener) => session === void 0 ? () => {} : session.subscribe(listener), [session]);
|
|
2682
2756
|
const snapshotSession = react.default.useCallback(() => session === void 0 ? void 0 : session.getSnapshot(), [session]);
|
|
2683
2757
|
const nodeSnapshot = react.default.useSyncExternalStore(subscribeSession, snapshotSession);
|
|
2684
|
-
const projected = useProjection
|
|
2685
|
-
const
|
|
2758
|
+
const projected = useProjection === void 0 ? void 0 : useProjection(TIMELINE_PROJECTION_KEY);
|
|
2759
|
+
const entries = react.default.useMemo(() => railMessages(nodeSnapshot, projected), [nodeSnapshot, projected]);
|
|
2686
2760
|
const [activeIndex, setActiveIndex] = react.default.useState(-1);
|
|
2687
|
-
const [
|
|
2761
|
+
const [previewIndex, setPreviewIndex] = react.default.useState(-1);
|
|
2688
2762
|
const [rightOffset, setRightOffset] = react.default.useState(16);
|
|
2689
|
-
const [
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
});
|
|
2694
|
-
const [hidden, setHidden] = react.default.useState(() => otherRailPresent());
|
|
2695
|
-
const [marks, setMarks] = react.default.useState(() => readMarks(sessionId));
|
|
2696
|
-
const [marksOnly, setMarksOnly] = react.default.useState(false);
|
|
2697
|
-
const [isNarrow, setIsNarrow] = react.default.useState(() => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(max-width: 767px)").matches);
|
|
2698
|
-
const pageRef = react.default.useRef(null);
|
|
2699
|
-
const activeItemRef = react.default.useRef(null);
|
|
2700
|
-
const jumpPendingRef = react.default.useRef(false);
|
|
2701
|
-
const jumpFallbackRef = react.default.useRef(void 0);
|
|
2702
|
-
react.default.useEffect(() => {
|
|
2703
|
-
setMarks(readMarks(sessionId));
|
|
2704
|
-
setMarksOnly(false);
|
|
2705
|
-
}, [sessionId]);
|
|
2706
|
-
const markedSet = react.default.useMemo(() => new Set(marks), [marks]);
|
|
2707
|
-
const marksRef = react.default.useRef(marks);
|
|
2708
|
-
marksRef.current = marks;
|
|
2709
|
-
const toggleMark = react.default.useCallback((m) => {
|
|
2710
|
-
const k = markKeyOf(m);
|
|
2711
|
-
if (k === "") return;
|
|
2712
|
-
const prev = marksRef.current;
|
|
2713
|
-
const next = prev.includes(k) ? prev.filter((x) => x !== k) : [...prev, k];
|
|
2714
|
-
marksRef.current = next;
|
|
2715
|
-
setMarks(next);
|
|
2716
|
-
writeMarks(sessionId, next);
|
|
2717
|
-
}, [sessionId]);
|
|
2763
|
+
const [band, setBand] = react.default.useState(RAIL_MAX_HEIGHT_PX);
|
|
2764
|
+
const aliveRef = react.default.useRef(true);
|
|
2765
|
+
const warmingRef = react.default.useRef(false);
|
|
2766
|
+
const railRef = react.default.useRef(null);
|
|
2718
2767
|
react.default.useEffect(() => {
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
return () => mql.removeEventListener("change", onChange);
|
|
2768
|
+
aliveRef.current = true;
|
|
2769
|
+
return () => {
|
|
2770
|
+
aliveRef.current = false;
|
|
2771
|
+
};
|
|
2724
2772
|
}, []);
|
|
2773
|
+
const entriesRef = react.default.useRef(entries);
|
|
2774
|
+
entriesRef.current = entries;
|
|
2725
2775
|
react.default.useEffect(() => {
|
|
2726
|
-
|
|
2727
|
-
}, [messages]);
|
|
2728
|
-
react.default.useEffect(() => {
|
|
2729
|
-
if (messages.length === 0) return;
|
|
2776
|
+
if (entries.length === 0) return;
|
|
2730
2777
|
const indexByKey = /* @__PURE__ */ new Map();
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
if (key !== void 0) indexByKey.set(key, i);
|
|
2778
|
+
entries.forEach((entry, index) => {
|
|
2779
|
+
if (entry.anchorKey !== void 0) indexByKey.set(entry.anchorKey, index);
|
|
2734
2780
|
});
|
|
2735
2781
|
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2736
2782
|
if (sp === null) return;
|
|
2737
2783
|
const rows = sp.querySelectorAll("[data-chat-anchor-key]");
|
|
2738
|
-
let settleTimer;
|
|
2739
2784
|
let frame = 0;
|
|
2740
|
-
const
|
|
2741
|
-
|
|
2742
|
-
if (jumpPendingRef.current) return;
|
|
2785
|
+
const measure = () => {
|
|
2786
|
+
frame = 0;
|
|
2743
2787
|
const rect = sp.getBoundingClientRect();
|
|
2744
2788
|
if (rect.height === 0) return;
|
|
2745
|
-
const candidates = nearRows.size > 0 ? nearRows : rows;
|
|
2746
2789
|
const line = rect.top + rect.height * .4;
|
|
2747
2790
|
let best = -1;
|
|
2748
2791
|
let bestDist = Infinity;
|
|
2749
|
-
for (const row of
|
|
2792
|
+
for (const row of rows) {
|
|
2750
2793
|
const key = row.getAttribute("data-chat-anchor-key");
|
|
2751
2794
|
if (key === null) continue;
|
|
2752
|
-
const
|
|
2753
|
-
if (
|
|
2795
|
+
const index = indexByKey.get(key);
|
|
2796
|
+
if (index === void 0) continue;
|
|
2754
2797
|
const r = row.getBoundingClientRect();
|
|
2755
2798
|
const dist = Math.abs(r.top + r.height / 2 - line);
|
|
2756
2799
|
if (dist < bestDist) {
|
|
2757
2800
|
bestDist = dist;
|
|
2758
|
-
best =
|
|
2801
|
+
best = index;
|
|
2759
2802
|
}
|
|
2760
2803
|
}
|
|
2761
2804
|
setActiveIndex(best);
|
|
2762
2805
|
};
|
|
2763
|
-
const
|
|
2764
|
-
if (frame
|
|
2765
|
-
frame = window.requestAnimationFrame(() => {
|
|
2766
|
-
frame = 0;
|
|
2767
|
-
updateActive();
|
|
2768
|
-
});
|
|
2769
|
-
};
|
|
2770
|
-
let io;
|
|
2771
|
-
if (typeof IntersectionObserver === "function") {
|
|
2772
|
-
io = new IntersectionObserver((entries) => {
|
|
2773
|
-
for (const entry of entries) if (entry.isIntersecting) nearRows.add(entry.target);
|
|
2774
|
-
else nearRows.delete(entry.target);
|
|
2775
|
-
scheduleMeasure();
|
|
2776
|
-
}, { root: sp });
|
|
2777
|
-
rows.forEach((row) => io?.observe(row));
|
|
2778
|
-
}
|
|
2779
|
-
const onScroll = () => {
|
|
2780
|
-
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2781
|
-
settleTimer = window.setTimeout(() => {
|
|
2782
|
-
settleTimer = void 0;
|
|
2783
|
-
if (jumpPendingRef.current) {
|
|
2784
|
-
jumpPendingRef.current = false;
|
|
2785
|
-
if (jumpFallbackRef.current !== void 0) {
|
|
2786
|
-
clearTimeout(jumpFallbackRef.current);
|
|
2787
|
-
jumpFallbackRef.current = void 0;
|
|
2788
|
-
}
|
|
2789
|
-
updateActive();
|
|
2790
|
-
}
|
|
2791
|
-
}, 150);
|
|
2792
|
-
scheduleMeasure();
|
|
2806
|
+
const schedule = () => {
|
|
2807
|
+
if (frame === 0) frame = window.requestAnimationFrame(measure);
|
|
2793
2808
|
};
|
|
2794
|
-
sp.addEventListener("scroll",
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2809
|
+
sp.addEventListener("scroll", schedule, { passive: true });
|
|
2810
|
+
const timer = window.setInterval(schedule, 2e3);
|
|
2811
|
+
schedule();
|
|
2797
2812
|
return () => {
|
|
2798
|
-
|
|
2813
|
+
sp.removeEventListener("scroll", schedule);
|
|
2814
|
+
window.clearInterval(timer);
|
|
2799
2815
|
if (frame !== 0) cancelAnimationFrame(frame);
|
|
2800
|
-
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
2801
|
-
if (jumpFallbackRef.current !== void 0) {
|
|
2802
|
-
clearTimeout(jumpFallbackRef.current);
|
|
2803
|
-
jumpFallbackRef.current = void 0;
|
|
2804
|
-
}
|
|
2805
|
-
sp.removeEventListener("scroll", onScroll);
|
|
2806
|
-
clearInterval(interval);
|
|
2807
2816
|
};
|
|
2808
|
-
}, [
|
|
2809
|
-
const syncFades = react.default.useCallback(() => {
|
|
2810
|
-
const page = pageRef.current;
|
|
2811
|
-
if (page === null) {
|
|
2812
|
-
setFades({
|
|
2813
|
-
top: false,
|
|
2814
|
-
bottom: false
|
|
2815
|
-
});
|
|
2816
|
-
return;
|
|
2817
|
-
}
|
|
2818
|
-
const canTop = page.scrollTop > 2;
|
|
2819
|
-
const canBot = page.scrollTop + page.clientHeight < page.scrollHeight - 2;
|
|
2820
|
-
setFades((prev) => prev.top === canTop && prev.bottom === canBot ? prev : {
|
|
2821
|
-
top: canTop,
|
|
2822
|
-
bottom: canBot
|
|
2823
|
-
});
|
|
2824
|
-
}, []);
|
|
2825
|
-
react.default.useEffect(syncFades, [
|
|
2826
|
-
syncFades,
|
|
2827
|
-
show,
|
|
2828
|
-
messages.length,
|
|
2829
|
-
marksOnly
|
|
2830
|
-
]);
|
|
2831
|
-
react.default.useLayoutEffect(() => {
|
|
2832
|
-
if (!show) return;
|
|
2833
|
-
const page = pageRef.current;
|
|
2834
|
-
const item = activeItemRef.current;
|
|
2835
|
-
if (page === null || item === null) return;
|
|
2836
|
-
scrollTimelineItemIntoView(page, item);
|
|
2837
|
-
syncFades();
|
|
2838
|
-
}, [
|
|
2839
|
-
activeIndex,
|
|
2840
|
-
messages.length,
|
|
2841
|
-
marksOnly,
|
|
2842
|
-
show,
|
|
2843
|
-
syncFades
|
|
2844
|
-
]);
|
|
2845
|
-
const height = railHeightFor(messages.length);
|
|
2846
|
-
react.default.useLayoutEffect(() => {
|
|
2847
|
-
if (show) return;
|
|
2848
|
-
const page = pageRef.current;
|
|
2849
|
-
if (page === null) return;
|
|
2850
|
-
page.scrollTop = page.scrollHeight;
|
|
2851
|
-
}, [
|
|
2852
|
-
show,
|
|
2853
|
-
messages.length,
|
|
2854
|
-
marksOnly,
|
|
2855
|
-
height
|
|
2856
|
-
]);
|
|
2817
|
+
}, [entries]);
|
|
2857
2818
|
react.default.useEffect(() => {
|
|
2858
2819
|
let raf = 0;
|
|
2859
2820
|
const measure = () => {
|
|
@@ -2862,6 +2823,7 @@ window.__ModuleLoader__.load({
|
|
|
2862
2823
|
if (sp === null) return;
|
|
2863
2824
|
const rect = sp.getBoundingClientRect();
|
|
2864
2825
|
if (rect.width === 0 && rect.height === 0) return;
|
|
2826
|
+
setBand(bandOf(sp));
|
|
2865
2827
|
const next = Math.max(8, Math.round(window.innerWidth - rect.right + 12));
|
|
2866
2828
|
setRightOffset((prev) => Math.abs(prev - next) > .5 ? next : prev);
|
|
2867
2829
|
};
|
|
@@ -2872,116 +2834,350 @@ window.__ModuleLoader__.load({
|
|
|
2872
2834
|
const ro = typeof ResizeObserver === "function" && sp !== null ? new ResizeObserver(schedule) : null;
|
|
2873
2835
|
if (ro !== null && sp !== null) ro.observe(sp);
|
|
2874
2836
|
window.addEventListener("resize", schedule);
|
|
2875
|
-
const
|
|
2837
|
+
const timer = window.setInterval(schedule, MEASURE_INTERVAL_MS);
|
|
2876
2838
|
measure();
|
|
2877
2839
|
return () => {
|
|
2878
2840
|
if (raf !== 0) cancelAnimationFrame(raf);
|
|
2879
2841
|
ro?.disconnect();
|
|
2880
2842
|
window.removeEventListener("resize", schedule);
|
|
2881
|
-
clearInterval(
|
|
2843
|
+
window.clearInterval(timer);
|
|
2882
2844
|
};
|
|
2883
2845
|
}, []);
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
className: "dsbt-title",
|
|
2960
|
-
children: m.text === "" ? t("timeline.noText") : m.text
|
|
2961
|
-
})
|
|
2962
|
-
}),
|
|
2963
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2964
|
-
type: "button",
|
|
2965
|
-
className: marked ? "dsbt-star dsbt-staron" : "dsbt-star",
|
|
2966
|
-
"aria-pressed": marked,
|
|
2967
|
-
"aria-label": marked ? t("timeline.unmark") : t("timeline.mark"),
|
|
2968
|
-
onClick: () => toggleMark(m),
|
|
2969
|
-
children: "★"
|
|
2970
|
-
}),
|
|
2971
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2972
|
-
className: "dsbt-ind",
|
|
2973
|
-
"aria-hidden": true,
|
|
2974
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsbt-line" })
|
|
2975
|
-
})
|
|
2976
|
-
]
|
|
2977
|
-
}, m.seq);
|
|
2978
|
-
})
|
|
2846
|
+
const navigate = react.default.useCallback((entry) => {
|
|
2847
|
+
if (entry.anchorKey === void 0) return;
|
|
2848
|
+
if (sessionsService === void 0 || sessionId === void 0) return;
|
|
2849
|
+
jumpToMessage(sessionsService, sessionId, entry.anchorKey, { followZonePx: 25 }).catch(() => {});
|
|
2850
|
+
}, [sessionsService, sessionId]);
|
|
2851
|
+
const warm = react.default.useCallback(async () => {
|
|
2852
|
+
if (session === void 0) return;
|
|
2853
|
+
if (warmingRef.current) return;
|
|
2854
|
+
const sp = document.querySelector("[data-conversation-scroll]");
|
|
2855
|
+
if (sp === null) return;
|
|
2856
|
+
warmingRef.current = true;
|
|
2857
|
+
try {
|
|
2858
|
+
await warmHistory(session, sp, {
|
|
2859
|
+
followZonePx: 25,
|
|
2860
|
+
alive: () => aliveRef.current,
|
|
2861
|
+
countOf: () => entriesRef.current.length,
|
|
2862
|
+
capacity: railCapacityOf(railRef.current)
|
|
2863
|
+
});
|
|
2864
|
+
} finally {
|
|
2865
|
+
warmingRef.current = false;
|
|
2866
|
+
}
|
|
2867
|
+
}, [session]);
|
|
2868
|
+
if (!enabled || sessionId === void 0 || sessionsService === void 0 || entries.length < 2) return null;
|
|
2869
|
+
const count = entries.length;
|
|
2870
|
+
const preview = previewIndex >= 0 && previewIndex < count ? entries[previewIndex] : void 0;
|
|
2871
|
+
return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2872
|
+
className: "dsbt-slot",
|
|
2873
|
+
style: { right: rightOffset },
|
|
2874
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
2875
|
+
className: "dsbt-rail",
|
|
2876
|
+
ref: railRef,
|
|
2877
|
+
style: {
|
|
2878
|
+
...railSize(count),
|
|
2879
|
+
["--dsbt-band"]: `${band}px`
|
|
2880
|
+
},
|
|
2881
|
+
"aria-label": t("timeline.railLabel"),
|
|
2882
|
+
onPointerMove: (event) => {
|
|
2883
|
+
setPreviewIndex(indexAtPointer(count, event.currentTarget, event.clientY));
|
|
2884
|
+
},
|
|
2885
|
+
onPointerLeave: () => {
|
|
2886
|
+
setPreviewIndex(-1);
|
|
2887
|
+
},
|
|
2888
|
+
onPointerEnter: () => {
|
|
2889
|
+
warm();
|
|
2890
|
+
},
|
|
2891
|
+
onFocus: () => {
|
|
2892
|
+
warm();
|
|
2893
|
+
},
|
|
2894
|
+
onClick: (event) => {
|
|
2895
|
+
const index = indexForEvent(event.currentTarget, event.nativeEvent);
|
|
2896
|
+
const entry = entries[index];
|
|
2897
|
+
if (entry !== void 0) navigate(entry);
|
|
2898
|
+
},
|
|
2899
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2900
|
+
className: "dsbt-marks",
|
|
2901
|
+
children: entries.map((entry, index) => {
|
|
2902
|
+
const active = index === activeIndex;
|
|
2903
|
+
const cls = active ? "dsbt-mark dsbt-markActive" : index === previewIndex ? "dsbt-mark dsbt-markPreview" : "dsbt-mark";
|
|
2904
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2905
|
+
className: "dsbt-markPosition",
|
|
2906
|
+
style: tickPosition(index, count),
|
|
2907
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2908
|
+
type: "button",
|
|
2909
|
+
className: cls,
|
|
2910
|
+
disabled: entry.anchorKey === void 0,
|
|
2911
|
+
"aria-label": t("timeline.jump", { turn: index + 1 }),
|
|
2912
|
+
"aria-current": active ? "true" : void 0,
|
|
2913
|
+
onFocus: () => {
|
|
2914
|
+
setPreviewIndex(index);
|
|
2915
|
+
},
|
|
2916
|
+
onBlur: () => {
|
|
2917
|
+
setPreviewIndex(-1);
|
|
2918
|
+
}
|
|
2919
|
+
})
|
|
2920
|
+
}, entry.seq);
|
|
2979
2921
|
})
|
|
2980
|
-
|
|
2981
|
-
|
|
2922
|
+
}), preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2923
|
+
role: "tooltip",
|
|
2924
|
+
className: "dsbt-preview",
|
|
2925
|
+
style: tickPosition(previewIndex, count),
|
|
2926
|
+
children: preview.text === "" ? t("timeline.noText") : preview.text
|
|
2927
|
+
})]
|
|
2928
|
+
})
|
|
2982
2929
|
}), document.body);
|
|
2983
2930
|
}
|
|
2984
2931
|
//#endregion
|
|
2932
|
+
//#region src/client/timeline/official-enhance.tsx
|
|
2933
|
+
/**
|
|
2934
|
+
* Official-rail enhancer — behaviour-only improvements to the kernel's own
|
|
2935
|
+
* turn navigator (dsh >= 0.1.2, `TurnNavigator` in dsh-client-ui-chat).
|
|
2936
|
+
*
|
|
2937
|
+
* This component renders NOTHING. It borrows the official rail exactly as the
|
|
2938
|
+
* kernel painted it and fixes two behaviours:
|
|
2939
|
+
*
|
|
2940
|
+
* 1. Smooth jump. The official `navigateToTurn` is one assignment
|
|
2941
|
+
* (`el.scrollTop += flowTop(row, el) - 24`); a click across a long
|
|
2942
|
+
* transcript teleports. Clicks are intercepted in the CAPTURE phase and
|
|
2943
|
+
* re-run through the shared glide instead.
|
|
2944
|
+
* 2. Reachable history. The official rail only lists turns the client has
|
|
2945
|
+
* already loaded, so anything behind the "load earlier" button has no mark
|
|
2946
|
+
* at all. Hovering or focusing the rail pages older history in — only up
|
|
2947
|
+
* to the rail's own uncompressed capacity, so the marks never degrade into
|
|
2948
|
+
* a solid bar — with the scroll position compensated so the reader's view
|
|
2949
|
+
* never moves.
|
|
2950
|
+
*
|
|
2951
|
+
* State-ownership note (why intercepting the official click loses nothing):
|
|
2952
|
+
* the kernel's scrollport (`data-conversation-scroll`, ChatView's
|
|
2953
|
+
* `onScrollRef` handler) treats every unaccounted scrollTop assignment as a
|
|
2954
|
+
* reader move and on each frame (a) flips its own at-bottom machinery,
|
|
2955
|
+
* (b) `chatScroll.save(position)` for view-tab restore, and (c)
|
|
2956
|
+
* `scheduleActiveTurn()` to re-derive the rail's active mark. The glide
|
|
2957
|
+
* animates the real scrollport, so all three keep converging to the target
|
|
2958
|
+
* row while it runs — the only thing the interception skips is the official
|
|
2959
|
+
* handler's synchronous `setActiveTurn(item.turn)`, which its own
|
|
2960
|
+
* scroll-driven active derivation re-settles by the time the glide lands.
|
|
2961
|
+
*
|
|
2962
|
+
* Both go through the shared backend (jump.ts), which is why the legacy port
|
|
2963
|
+
* behaves identically.
|
|
2964
|
+
*
|
|
2965
|
+
* Interception notes: React 18 dispatches `onClick` from a listener on the
|
|
2966
|
+
* root container during the BUBBLE phase. A capture listener on the rail
|
|
2967
|
+
* itself therefore runs first, and `stopImmediatePropagation()` there prevents
|
|
2968
|
+
* the event from ever reaching React's root — the official handler never
|
|
2969
|
+
* fires. The rail owns pointer input for its whole column (marks carry
|
|
2970
|
+
* `pointer-events: none`), so one listener catches mouse and keyboard alike.
|
|
2971
|
+
*
|
|
2972
|
+
* Nothing here touches the official DOM: no nodes are injected into a
|
|
2973
|
+
* React-owned subtree and no official class or paint is overridden.
|
|
2974
|
+
*/
|
|
2975
|
+
/** Structural anchor of the official rail (its inline style carries the rail's own metrics). */
|
|
2976
|
+
const OFFICIAL_RAIL_SELECTOR = "[data-conversation-scroll] nav[style*=\"--turn-natural-height\"]";
|
|
2977
|
+
/**
|
|
2978
|
+
* Rails claimed by an enhancer instance, keyed by the owning instance's
|
|
2979
|
+
* claim token. One session-scoped enhancer mounts per session; without a
|
|
2980
|
+
* claim two instances would both attach capture listeners to the FIRST rail
|
|
2981
|
+
* in the document (multi-column layouts) and one of them — not necessarily
|
|
2982
|
+
* the one owning the reader's column — would win the click. The token
|
|
2983
|
+
* distinguishes "already mine" (the poll keeps it) from "someone else's"
|
|
2984
|
+
* (this instance must stand down). Weak: a replaced rail element disappears
|
|
2985
|
+
* as soon as the browser collects it, so remounts need no bookkeeping.
|
|
2986
|
+
*/
|
|
2987
|
+
const claimedRails = /* @__PURE__ */ new WeakMap();
|
|
2988
|
+
/** How often the official rail is looked for (it mounts with the chat view). */
|
|
2989
|
+
const RAIL_POLL_MS = 400;
|
|
2990
|
+
/**
|
|
2991
|
+
* The enhancer: no visual output, only the two behaviour fixes.
|
|
2992
|
+
* @param props - dock slot props plus the enabled gate.
|
|
2993
|
+
* @returns null (always).
|
|
2994
|
+
*/
|
|
2995
|
+
function OfficialTimelineEnhancer(props) {
|
|
2996
|
+
const { sessionId, sessionsService, useChat, enabled } = props;
|
|
2997
|
+
const items = normalizeNavigationItems(useChat === void 0 ? void 0 : useChat((snapshot) => {
|
|
2998
|
+
const nav = snapshot?.navigation;
|
|
2999
|
+
return typeof nav?.items === "function" ? nav.items() : void 0;
|
|
3000
|
+
}));
|
|
3001
|
+
const session = sessionId !== void 0 && sessionsService !== void 0 ? sessionsService.binding(sessionId)?.session : void 0;
|
|
3002
|
+
const [rail, setRail] = react.default.useState(null);
|
|
3003
|
+
const itemsRef = react.default.useRef(items);
|
|
3004
|
+
itemsRef.current = items;
|
|
3005
|
+
const railRef = react.default.useRef(rail);
|
|
3006
|
+
railRef.current = rail;
|
|
3007
|
+
const aliveRef = react.default.useRef(true);
|
|
3008
|
+
const enabledRef = react.default.useRef(enabled);
|
|
3009
|
+
enabledRef.current = enabled;
|
|
3010
|
+
const warmingRef = react.default.useRef(false);
|
|
3011
|
+
react.default.useEffect(() => {
|
|
3012
|
+
aliveRef.current = true;
|
|
3013
|
+
return () => {
|
|
3014
|
+
aliveRef.current = false;
|
|
3015
|
+
};
|
|
3016
|
+
}, []);
|
|
3017
|
+
const claimToken = react.default.useMemo(() => Symbol("dsh-bg-timeline-claim"), []);
|
|
3018
|
+
react.default.useEffect(() => {
|
|
3019
|
+
const check = () => {
|
|
3020
|
+
const found = document.querySelector(OFFICIAL_RAIL_SELECTOR);
|
|
3021
|
+
if (found === null) {
|
|
3022
|
+
setRail(null);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
const owner = claimedRails.get(found);
|
|
3026
|
+
if (owner !== void 0 && owner !== claimToken) {
|
|
3027
|
+
setRail(null);
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
claimedRails.set(found, claimToken);
|
|
3031
|
+
setRail(found);
|
|
3032
|
+
};
|
|
3033
|
+
check();
|
|
3034
|
+
const timer = window.setInterval(check, RAIL_POLL_MS);
|
|
3035
|
+
return () => {
|
|
3036
|
+
window.clearInterval(timer);
|
|
3037
|
+
if (railRef.current !== null) claimedRails.delete(railRef.current);
|
|
3038
|
+
};
|
|
3039
|
+
}, [claimToken]);
|
|
3040
|
+
const navigate = react.default.useCallback((item) => {
|
|
3041
|
+
if (sessionsService === void 0 || sessionId === void 0) return;
|
|
3042
|
+
const scrollport = rail === null ? null : rail.closest("[data-conversation-scroll]");
|
|
3043
|
+
jumpToMessage(sessionsService, sessionId, item.anchorKey, {
|
|
3044
|
+
followZonePx: 24,
|
|
3045
|
+
targetTop: officialTargetTopFor,
|
|
3046
|
+
scrollport
|
|
3047
|
+
}).catch(() => {});
|
|
3048
|
+
}, [
|
|
3049
|
+
sessionsService,
|
|
3050
|
+
sessionId,
|
|
3051
|
+
rail
|
|
3052
|
+
]);
|
|
3053
|
+
/**
|
|
3054
|
+
* Page older history in until the rail is full or the log is exhausted.
|
|
3055
|
+
* Each page is compensated so the reader's view stays put (see
|
|
3056
|
+
* compensatedLoadOlder for why the kernel cannot do this for us).
|
|
3057
|
+
*/
|
|
3058
|
+
const warm = react.default.useCallback(async () => {
|
|
3059
|
+
if (session === void 0 || rail === null) return;
|
|
3060
|
+
if (warmingRef.current) return;
|
|
3061
|
+
const scrollport = rail.closest("[data-conversation-scroll]");
|
|
3062
|
+
if (scrollport === null) return;
|
|
3063
|
+
warmingRef.current = true;
|
|
3064
|
+
try {
|
|
3065
|
+
await warmHistory(session, scrollport, {
|
|
3066
|
+
followZonePx: 24,
|
|
3067
|
+
alive: () => aliveRef.current && enabledRef.current,
|
|
3068
|
+
countOf: () => itemsRef.current.length,
|
|
3069
|
+
capacity: railCapacityOf(rail)
|
|
3070
|
+
});
|
|
3071
|
+
} finally {
|
|
3072
|
+
warmingRef.current = false;
|
|
3073
|
+
}
|
|
3074
|
+
}, [session, rail]);
|
|
3075
|
+
react.default.useEffect(() => {
|
|
3076
|
+
if (!enabled || rail === null) return;
|
|
3077
|
+
const onClick = (event) => {
|
|
3078
|
+
const index = indexForEvent(rail, event);
|
|
3079
|
+
const item = itemsRef.current[index];
|
|
3080
|
+
if (item === void 0) return;
|
|
3081
|
+
event.preventDefault();
|
|
3082
|
+
event.stopImmediatePropagation();
|
|
3083
|
+
navigate(item);
|
|
3084
|
+
};
|
|
3085
|
+
rail.addEventListener("click", onClick, true);
|
|
3086
|
+
return () => {
|
|
3087
|
+
rail.removeEventListener("click", onClick, true);
|
|
3088
|
+
};
|
|
3089
|
+
}, [
|
|
3090
|
+
enabled,
|
|
3091
|
+
rail,
|
|
3092
|
+
navigate
|
|
3093
|
+
]);
|
|
3094
|
+
react.default.useEffect(() => {
|
|
3095
|
+
if (!enabled || rail === null) return;
|
|
3096
|
+
const start = () => {
|
|
3097
|
+
warm();
|
|
3098
|
+
};
|
|
3099
|
+
rail.addEventListener("pointerenter", start);
|
|
3100
|
+
rail.addEventListener("focusin", start);
|
|
3101
|
+
return () => {
|
|
3102
|
+
rail.removeEventListener("pointerenter", start);
|
|
3103
|
+
rail.removeEventListener("focusin", start);
|
|
3104
|
+
};
|
|
3105
|
+
}, [
|
|
3106
|
+
enabled,
|
|
3107
|
+
rail,
|
|
3108
|
+
warm
|
|
3109
|
+
]);
|
|
3110
|
+
return null;
|
|
3111
|
+
}
|
|
3112
|
+
//#endregion
|
|
3113
|
+
//#region src/client/timeline/index.tsx
|
|
3114
|
+
/**
|
|
3115
|
+
* Dock-slot entry for the conversation timeline: detects which frontend the
|
|
3116
|
+
* running kernel supports and mounts exactly one of them.
|
|
3117
|
+
*
|
|
3118
|
+
* Detection is a capability check on the slot props, not a version compare:
|
|
3119
|
+
* the official turn rail is rendered from the very index `ui-chat` publishes
|
|
3120
|
+
* as a session hook (`useChat(s => s.navigation.items())`, dsh >= 0.1.2), so
|
|
3121
|
+
* the presence of that hook IS the presence of the official rail. Comparing
|
|
3122
|
+
* version strings would break on pre-releases, forks, and deployments that
|
|
3123
|
+
* mount a different conversation target.
|
|
3124
|
+
*
|
|
3125
|
+
* - `useChat` present -> 'enhance': the official rail stays on screen and
|
|
3126
|
+
* OfficialTimelineEnhancer improves only its behaviour.
|
|
3127
|
+
* - `useChat` absent -> 'legacy': no official rail exists, so this plugin
|
|
3128
|
+
* renders its own port of it from the shared backend.
|
|
3129
|
+
*
|
|
3130
|
+
* The detected mode is published to ./mode-store.ts because the settings row
|
|
3131
|
+
* (root-scoped, no session) cannot probe the kernel itself but must switch its
|
|
3132
|
+
* copy between "conversation timeline" and "conversation timeline enhancement".
|
|
3133
|
+
*/
|
|
3134
|
+
/**
|
|
3135
|
+
* Mount the timeline frontend the running kernel supports.
|
|
3136
|
+
* @param props - dock slot props.
|
|
3137
|
+
* @returns the enhancer or the legacy rail.
|
|
3138
|
+
*/
|
|
3139
|
+
function TimelineBridge(props) {
|
|
3140
|
+
const { useChat, t, ...rest } = props;
|
|
3141
|
+
const settings = react.default.useSyncExternalStore(settingsClient.subscribe, settingsClient.getSnapshot);
|
|
3142
|
+
const enabled = settings.value?.timeline !== false && settings.status !== "loading";
|
|
3143
|
+
const mode = useChat === void 0 ? "legacy" : "enhance";
|
|
3144
|
+
react.default.useEffect(() => {
|
|
3145
|
+
reportTimelineMode(mode);
|
|
3146
|
+
}, [mode]);
|
|
3147
|
+
if (mode === "enhance") return react.default.createElement(OfficialTimelineEnhancer, {
|
|
3148
|
+
...rest,
|
|
3149
|
+
useChat,
|
|
3150
|
+
enabled,
|
|
3151
|
+
t
|
|
3152
|
+
});
|
|
3153
|
+
return react.default.createElement(LegacyTimelineRail, {
|
|
3154
|
+
...rest,
|
|
3155
|
+
enabled,
|
|
3156
|
+
t
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
/** localStorage prefix the removed key-point bookmark feature used (< 0.5). */
|
|
3160
|
+
const MARKS_STORAGE_PREFIX = "dsbt_marks_";
|
|
3161
|
+
/**
|
|
3162
|
+
* Drop the bookmark rows pre-0.5 versions left in localStorage.
|
|
3163
|
+
*
|
|
3164
|
+
* The bookmarks are gone — the rail now reuses the official UI, which has no
|
|
3165
|
+
* affordance for them — so the rows are dead weight every session carried
|
|
3166
|
+
* forever. One-way and irreversible by design; the key prefix is owned by
|
|
3167
|
+
* this plugin, so nothing else can be caught in the sweep.
|
|
3168
|
+
*/
|
|
3169
|
+
function clearLegacyMarks() {
|
|
3170
|
+
if (typeof window === "undefined") return;
|
|
3171
|
+
try {
|
|
3172
|
+
const stale = [];
|
|
3173
|
+
for (let i = 0; i < window.localStorage.length; i += 1) {
|
|
3174
|
+
const key = window.localStorage.key(i);
|
|
3175
|
+
if (key !== null && key.startsWith(MARKS_STORAGE_PREFIX)) stale.push(key);
|
|
3176
|
+
}
|
|
3177
|
+
for (const key of stale) window.localStorage.removeItem(key);
|
|
3178
|
+
} catch {}
|
|
3179
|
+
}
|
|
3180
|
+
//#endregion
|
|
2985
3181
|
//#region src/client/index.ts
|
|
2986
3182
|
/**
|
|
2987
3183
|
* Service injections this entry declares: `slots` + `locale` for the settings
|
|
@@ -3001,6 +3197,7 @@ window.__ModuleLoader__.load({
|
|
|
3001
3197
|
*/
|
|
3002
3198
|
function apply(ctx) {
|
|
3003
3199
|
const glassBridge = installGlassBridge();
|
|
3200
|
+
clearLegacyMarks();
|
|
3004
3201
|
const paint = () => {
|
|
3005
3202
|
const snapshot = settingsClient.getSnapshot();
|
|
3006
3203
|
const value = snapshot.status === "ready" ? snapshot.value : void 0;
|
|
@@ -3028,7 +3225,7 @@ window.__ModuleLoader__.load({
|
|
|
3028
3225
|
order: 45,
|
|
3029
3226
|
locale: "ui-background",
|
|
3030
3227
|
inject: () => ({ sessionsService: ctx.sessions })
|
|
3031
|
-
},
|
|
3228
|
+
}, TimelineBridge));
|
|
3032
3229
|
ctx.effect(() => () => {
|
|
3033
3230
|
unsubscribe();
|
|
3034
3231
|
glassBridge.dispose();
|