deepseek-harness-background 0.5.3 → 0.5.4
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 +172 -172
- package/README.zh.md +170 -170
- package/lib/client.js +486 -485
- package/lib/client.js.map +1 -1
- package/package.json +85 -85
package/lib/client.js
CHANGED
|
@@ -1125,41 +1125,41 @@ window.__ModuleLoader__.load({
|
|
|
1125
1125
|
document.head.appendChild(tag);
|
|
1126
1126
|
}
|
|
1127
1127
|
var SettingsRow_module_css_default = {
|
|
1128
|
-
"
|
|
1129
|
-
"
|
|
1130
|
-
"
|
|
1128
|
+
"enabledBadge": "-ozqdq_enabledBadge",
|
|
1129
|
+
"previewImg": "-ozqdq_previewImg",
|
|
1130
|
+
"btnPrimary": "-ozqdq_btnPrimary",
|
|
1131
|
+
"hint": "-ozqdq_hint",
|
|
1132
|
+
"segActive": "-ozqdq_segActive",
|
|
1133
|
+
"clearRow": "-ozqdq_clearRow",
|
|
1134
|
+
"segBtn": "-ozqdq_segBtn",
|
|
1135
|
+
"previewGlassText": "-ozqdq_previewGlassText",
|
|
1136
|
+
"btn": "-ozqdq_btn",
|
|
1131
1137
|
"preview": "-ozqdq_preview",
|
|
1132
|
-
"
|
|
1133
|
-
"
|
|
1138
|
+
"clearBtn": "-ozqdq_clearBtn",
|
|
1139
|
+
"urlInput": "-ozqdq_urlInput",
|
|
1134
1140
|
"btnDanger": "-ozqdq_btnDanger",
|
|
1135
|
-
"
|
|
1136
|
-
"titleRow": "-ozqdq_titleRow",
|
|
1137
|
-
"sliderRow": "-ozqdq_sliderRow",
|
|
1138
|
-
"sliderValue": "-ozqdq_sliderValue",
|
|
1139
|
-
"segBtn": "-ozqdq_segBtn",
|
|
1140
|
-
"previewGlass": "-ozqdq_previewGlass",
|
|
1141
|
-
"row": "-ozqdq_row",
|
|
1142
|
-
"error": "-ozqdq_error",
|
|
1143
|
-
"previewScrim": "-ozqdq_previewScrim",
|
|
1141
|
+
"urlRow": "-ozqdq_urlRow",
|
|
1144
1142
|
"switchKnob": "-ozqdq_switchKnob",
|
|
1145
|
-
"
|
|
1146
|
-
"btnPrimary": "-ozqdq_btnPrimary",
|
|
1147
|
-
"segActive": "-ozqdq_segActive",
|
|
1143
|
+
"segRow": "-ozqdq_segRow",
|
|
1148
1144
|
"segLabel": "-ozqdq_segLabel",
|
|
1149
|
-
"
|
|
1150
|
-
"
|
|
1145
|
+
"previewGlass": "-ozqdq_previewGlass",
|
|
1146
|
+
"header": "-ozqdq_header",
|
|
1151
1147
|
"sectionLabel": "-ozqdq_sectionLabel",
|
|
1152
|
-
"enabledBadge": "-ozqdq_enabledBadge",
|
|
1153
|
-
"segRow": "-ozqdq_segRow",
|
|
1154
|
-
"previewImg": "-ozqdq_previewImg",
|
|
1155
|
-
"btn": "-ozqdq_btn",
|
|
1156
1148
|
"sliderLabel": "-ozqdq_sliderLabel",
|
|
1157
|
-
"header": "-ozqdq_header",
|
|
1158
|
-
"clearRow": "-ozqdq_clearRow",
|
|
1159
1149
|
"desc": "-ozqdq_desc",
|
|
1150
|
+
"sliderValue": "-ozqdq_sliderValue",
|
|
1160
1151
|
"section": "-ozqdq_section",
|
|
1161
|
-
"
|
|
1162
|
-
"
|
|
1152
|
+
"titleRow": "-ozqdq_titleRow",
|
|
1153
|
+
"previewEmpty": "-ozqdq_previewEmpty",
|
|
1154
|
+
"sliderRow": "-ozqdq_sliderRow",
|
|
1155
|
+
"previewScrim": "-ozqdq_previewScrim",
|
|
1156
|
+
"slider": "-ozqdq_slider",
|
|
1157
|
+
"segGroup": "-ozqdq_segGroup",
|
|
1158
|
+
"error": "-ozqdq_error",
|
|
1159
|
+
"title": "-ozqdq_title",
|
|
1160
|
+
"row": "-ozqdq_row",
|
|
1161
|
+
"sourceRow": "-ozqdq_sourceRow",
|
|
1162
|
+
"switch": "-ozqdq_switch"
|
|
1163
1163
|
};
|
|
1164
1164
|
//#endregion
|
|
1165
1165
|
//#region src/client/SettingsRow.tsx
|
|
@@ -1695,393 +1695,131 @@ window.__ModuleLoader__.load({
|
|
|
1695
1695
|
]
|
|
1696
1696
|
});
|
|
1697
1697
|
}
|
|
1698
|
-
//#endregion
|
|
1699
|
-
//#region src/client/timeline/jump.ts
|
|
1700
|
-
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1701
|
-
/** How long one jump may keep paging older history before giving up. */
|
|
1702
|
-
const JUMP_PAGE_DEADLINE_MS = 3e4;
|
|
1703
|
-
/** How long to wait for the chat view to commit the target row. */
|
|
1704
|
-
const JUMP_ROW_WAIT_MS = 3e3;
|
|
1705
|
-
/** Settle between consecutive pages so a resolver without loadingOlder cannot hot-spin the loop. */
|
|
1706
|
-
const PAGE_COOLDOWN_MS = 50;
|
|
1707
|
-
/** Distance from the scrollport top the official ChatView lands a jumped row at. */
|
|
1708
|
-
const OFFICIAL_LANDING_INSET_PX = 24;
|
|
1709
|
-
/** Distances under this snap instead of animating. */
|
|
1710
|
-
const MIN_GLIDE_DISTANCE_PX = 8;
|
|
1711
|
-
/** Glide duration floor / cap — a far-boundary jump stays a tangible slide. */
|
|
1712
|
-
const MIN_GLIDE_MS = 280;
|
|
1713
|
-
const MAX_GLIDE_MS = 900;
|
|
1714
|
-
/** Every mounted conversation scrollport (multi-column layouts have several). */
|
|
1715
|
-
function conversationScrollports() {
|
|
1716
|
-
return Array.from(document.querySelectorAll("[data-conversation-scroll]"));
|
|
1717
|
-
}
|
|
1718
1698
|
/**
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1722
|
-
* node store, unreachable from `session.getSnapshot()` — its SessionSnapshot
|
|
1723
|
-
* faces no node table at all), but the rendered DOM is the ground truth both
|
|
1724
|
-
* share, so a jump probes it FIRST before deciding whether paging is needed.
|
|
1725
|
-
* @param anchorKey - chat anchor key of the target row.
|
|
1699
|
+
* The rail's own inset, read off the custom property the official component
|
|
1700
|
+
* publishes (`--turn-rail-inset`). Falls back to the official constant.
|
|
1701
|
+
* @param rail - the rail element (or null).
|
|
1726
1702
|
*/
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
return
|
|
1703
|
+
function railInsetOf(rail) {
|
|
1704
|
+
if (rail === null) return 6;
|
|
1705
|
+
const px = Number.parseFloat(rail.style.getPropertyValue("--turn-rail-inset"));
|
|
1706
|
+
return Number.isFinite(px) && px >= 0 ? px : 6;
|
|
1731
1707
|
}
|
|
1732
1708
|
/**
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
* @param
|
|
1709
|
+
* Whether `rail` is the frame-style rail (dsh >= 0.1.2-alpha.3): marks sit
|
|
1710
|
+
* at a fixed pitch and scroll INSIDE the frame, whose scroller offset the
|
|
1711
|
+
* component publishes inline as `--turn-scroll-top` — every render, resting
|
|
1712
|
+
* at `0px` included. The compressing alpha.1/2 rail never publishes that
|
|
1713
|
+
* property, so its presence is the generation test. The legacy port paints
|
|
1714
|
+
* its own `--dsbt-*` properties and can never trip this.
|
|
1715
|
+
* @param rail - the rail element (or null).
|
|
1740
1716
|
*/
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
const deadline = Date.now() + timeoutMs;
|
|
1744
|
-
for (;;) {
|
|
1745
|
-
for (const scrollport of conversationScrollports()) {
|
|
1746
|
-
const row = scrollport.querySelector(selector);
|
|
1747
|
-
if (row !== null) return row;
|
|
1748
|
-
}
|
|
1749
|
-
if (Date.now() >= deadline) return null;
|
|
1750
|
-
await delay(40);
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
/** Maximum scrollable offset of a conversation scrollport. */
|
|
1754
|
-
function scrollFloorOf(el) {
|
|
1755
|
-
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
1717
|
+
function isFrameRail(rail) {
|
|
1718
|
+
return rail !== null && rail.style.getPropertyValue("--turn-scroll-top") !== "";
|
|
1756
1719
|
}
|
|
1757
1720
|
/**
|
|
1758
|
-
*
|
|
1759
|
-
*
|
|
1760
|
-
* the
|
|
1761
|
-
*
|
|
1762
|
-
*
|
|
1721
|
+
* Resolve the entry under a pointer Y on the frame-style rail — the official
|
|
1722
|
+
* alpha.3 geometry verbatim (`itemAtPointer` in that generation's
|
|
1723
|
+
* TurnNavigator.tsx): marks live in the scrolled ladder at a fixed 10px
|
|
1724
|
+
* pitch, so the click offset adds the scroller offset. Read off the published
|
|
1725
|
+
* `--turn-scroll-top` var rather than the scroller element itself — the same
|
|
1726
|
+
* metric the official preview positioning uses, so any one-frame render lag
|
|
1727
|
+
* affects both identically.
|
|
1728
|
+
* @param count - number of entries the rail shows.
|
|
1729
|
+
* @param rail - the rail element.
|
|
1730
|
+
* @param clientY - pointer viewport Y.
|
|
1763
1731
|
*/
|
|
1764
|
-
function
|
|
1765
|
-
const
|
|
1766
|
-
const
|
|
1767
|
-
|
|
1768
|
-
|
|
1732
|
+
function frameIndexAtPointer(count, rail, clientY) {
|
|
1733
|
+
const rect = rail.getBoundingClientRect();
|
|
1734
|
+
const inset = railInsetOf(rail);
|
|
1735
|
+
const raw = Number.parseFloat(rail.style.getPropertyValue("--turn-scroll-top"));
|
|
1736
|
+
const scrollTop = Number.isFinite(raw) ? raw : 0;
|
|
1737
|
+
const offset = clientY - rect.top + scrollTop - inset;
|
|
1738
|
+
return Math.max(0, Math.min(count - 1, Math.round(offset / 10)));
|
|
1769
1739
|
}
|
|
1770
1740
|
/**
|
|
1771
|
-
*
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
1741
|
+
* Resolve the gesture that produced a rail click to an entry index.
|
|
1742
|
+
*
|
|
1743
|
+
* Keyboard activation targets the focused mark button (recovered by its
|
|
1744
|
+
* position among the rail's buttons); a mouse click carries coordinates and
|
|
1745
|
+
* resolves through the rail geometry — the frame-style mapping over the
|
|
1746
|
+
* published `--turn-scroll-top`. Returns -1 when neither applies (a
|
|
1747
|
+
* keyboard target outside the rail, or a rail of an unsupported generation,
|
|
1748
|
+
* which the kernel's own handler then owns).
|
|
1749
|
+
*
|
|
1750
|
+
* Assumption: the mark ticks are the ONLY buttons under the rail — the
|
|
1751
|
+
* official rail renders marks exclusively and its preview tooltip carries
|
|
1752
|
+
* none. A future non-mark control added inside the rail would shift the
|
|
1753
|
+
* button index and must revisit this.
|
|
1754
|
+
*
|
|
1755
|
+
* @param rail - the rail element.
|
|
1756
|
+
* @param event - the click event.
|
|
1776
1757
|
*/
|
|
1777
|
-
function
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1758
|
+
function indexForEvent(rail, event) {
|
|
1759
|
+
const target = event.target;
|
|
1760
|
+
if (target instanceof Element) {
|
|
1761
|
+
const button = target.closest("button");
|
|
1762
|
+
if (button !== null && rail.contains(button)) {
|
|
1763
|
+
const index = Array.from(rail.querySelectorAll("button")).indexOf(button);
|
|
1764
|
+
if (index >= 0) return index;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
const count = rail.querySelectorAll("button").length;
|
|
1768
|
+
if (count === 0) return -1;
|
|
1769
|
+
return isFrameRail(rail) ? frameIndexAtPointer(count, rail, event.clientY) : -1;
|
|
1782
1770
|
}
|
|
1783
1771
|
/**
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
* @param
|
|
1772
|
+
* Validate the official navigation array crossing the slot boundary. The
|
|
1773
|
+
* value is produced by the kernel and may be absent, partial, or from an
|
|
1774
|
+
* older build, so every field is checked before use.
|
|
1775
|
+
* @param value - the raw `useChat(s => s.navigation.items())` result.
|
|
1788
1776
|
*/
|
|
1789
|
-
function
|
|
1790
|
-
if (!
|
|
1791
|
-
|
|
1777
|
+
function normalizeNavigationItems(value) {
|
|
1778
|
+
if (!Array.isArray(value)) return [];
|
|
1779
|
+
const out = [];
|
|
1780
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1781
|
+
for (const item of value) {
|
|
1782
|
+
if (item === null || typeof item !== "object") continue;
|
|
1783
|
+
const rec = item;
|
|
1784
|
+
if (typeof rec.turn !== "number" || !Number.isFinite(rec.turn)) continue;
|
|
1785
|
+
if (typeof rec.anchorKey !== "string" || rec.anchorKey === "") continue;
|
|
1786
|
+
if (seen.has(rec.turn)) continue;
|
|
1787
|
+
seen.add(rec.turn);
|
|
1788
|
+
out.push({
|
|
1789
|
+
turn: rec.turn,
|
|
1790
|
+
anchorKey: rec.anchorKey,
|
|
1791
|
+
prompt: typeof rec.prompt === "string" ? rec.prompt : "",
|
|
1792
|
+
response: typeof rec.response === "string" ? rec.response : ""
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
return out;
|
|
1792
1796
|
}
|
|
1793
1797
|
/**
|
|
1794
|
-
*
|
|
1795
|
-
*
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1800
|
-
*
|
|
1801
|
-
* @param
|
|
1798
|
+
* Validate one `turnOutline` wire value into its entries. The value is the
|
|
1799
|
+
* host projection's wire view (an array of `{ turn, seq, prompt, response }`
|
|
1800
|
+
* — see `dsh-session-turn-outline`) read through `useProjection`, so it may
|
|
1801
|
+
* be absent (pre-baseline, or an older kernel that never registers the key —
|
|
1802
|
+
* which reads `undefined`), partial, or damaged. `turn` and `seq` are the
|
|
1803
|
+
* load-bearing fields — a mark cannot exist or page without them — and a
|
|
1804
|
+
* damaged one drops the entry; the previews are decorative and degrade to ''.
|
|
1805
|
+
* @param value - the raw `useProjection('turnOutline')` value.
|
|
1802
1806
|
*/
|
|
1803
|
-
function
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
function settleScrollEvents() {
|
|
1820
|
-
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
|
1821
|
-
return new Promise((resolve) => {
|
|
1822
|
-
requestAnimationFrame(() => requestAnimationFrame(() => window.setTimeout(resolve, 0)));
|
|
1823
|
-
});
|
|
1824
|
-
}
|
|
1825
|
-
/**
|
|
1826
|
-
* Animate el.scrollTop toward target with an ease-in-out cubic profile.
|
|
1827
|
-
* Wheel / touch / keyboard-scroll input cancels the glide so the reader
|
|
1828
|
-
* takes over immediately. Resolves true when the glide completed, false
|
|
1829
|
-
* when the reader cancelled it.
|
|
1830
|
-
* @param el - the conversation scrollport.
|
|
1831
|
-
* @param target - destination scrollTop.
|
|
1832
|
-
* @param durationMs - animation budget.
|
|
1833
|
-
*/
|
|
1834
|
-
function animateScrollTop(el, target, durationMs) {
|
|
1835
|
-
return new Promise((resolve) => {
|
|
1836
|
-
const start = el.scrollTop;
|
|
1837
|
-
const delta = target - start;
|
|
1838
|
-
let done = false;
|
|
1839
|
-
let frame = 0;
|
|
1840
|
-
const finish = (completed) => {
|
|
1841
|
-
if (done) return;
|
|
1842
|
-
done = true;
|
|
1843
|
-
el.removeEventListener("wheel", onWheel);
|
|
1844
|
-
el.removeEventListener("touchstart", onTouch);
|
|
1845
|
-
window.removeEventListener("keydown", onKey);
|
|
1846
|
-
if (frame !== 0) cancelAnimationFrame(frame);
|
|
1847
|
-
resolve(completed);
|
|
1848
|
-
};
|
|
1849
|
-
const onWheel = () => finish(false);
|
|
1850
|
-
const onTouch = () => finish(false);
|
|
1851
|
-
const onKey = (e) => {
|
|
1852
|
-
if ([
|
|
1853
|
-
"ArrowUp",
|
|
1854
|
-
"ArrowDown",
|
|
1855
|
-
"PageUp",
|
|
1856
|
-
"PageDown",
|
|
1857
|
-
"Home",
|
|
1858
|
-
"End",
|
|
1859
|
-
" "
|
|
1860
|
-
].includes(e.key)) finish(false);
|
|
1861
|
-
};
|
|
1862
|
-
el.addEventListener("wheel", onWheel, { passive: true });
|
|
1863
|
-
el.addEventListener("touchstart", onTouch, { passive: true });
|
|
1864
|
-
window.addEventListener("keydown", onKey);
|
|
1865
|
-
if (Math.abs(delta) < .5 || durationMs <= 0) {
|
|
1866
|
-
el.scrollTop = target;
|
|
1867
|
-
finish(true);
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
const t0 = performance.now();
|
|
1871
|
-
const ease = (x) => x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
1872
|
-
const step = (now) => {
|
|
1873
|
-
if (done) return;
|
|
1874
|
-
const t = Math.min(1, (now - t0) / durationMs);
|
|
1875
|
-
el.scrollTop = start + delta * ease(t);
|
|
1876
|
-
if (t < 1) frame = requestAnimationFrame(step);
|
|
1877
|
-
else finish(true);
|
|
1878
|
-
};
|
|
1879
|
-
frame = requestAnimationFrame(step);
|
|
1880
|
-
});
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Ensure the target node is loaded, wait for the chat view to commit its row
|
|
1884
|
-
* into the DOM, then glide it into view. Paging uses a time budget instead
|
|
1885
|
-
* of a fixed iteration guard (each in-flight page burned part of that guard,
|
|
1886
|
-
* so slow history could exhaust it and silently fail).
|
|
1887
|
-
*
|
|
1888
|
-
* @param sessionsService - the runtime sessions service.
|
|
1889
|
-
* @param sessionId - the session owning the target row.
|
|
1890
|
-
* @param anchorKey - chat anchor key of the target row.
|
|
1891
|
-
* @param options - follow zone, landing resolver, paging scopes and budgets.
|
|
1892
|
-
* @returns whether the row was reached (false when it never rendered or the
|
|
1893
|
-
* reader took the scrollport back mid-glide).
|
|
1894
|
-
*/
|
|
1895
|
-
async function jumpToMessage(sessionsService, sessionId, anchorKey, options = {}) {
|
|
1896
|
-
const session = sessionsService.binding(sessionId)?.session;
|
|
1897
|
-
if (session === void 0) return false;
|
|
1898
|
-
const followZonePx = options.followZonePx ?? 24;
|
|
1899
|
-
const resolveTarget = options.targetTop ?? centeredScrollTopFor;
|
|
1900
|
-
const pageDeadlineMs = options.pageDeadlineMs ?? JUMP_PAGE_DEADLINE_MS;
|
|
1901
|
-
const rowWaitMs = options.rowWaitMs ?? JUMP_ROW_WAIT_MS;
|
|
1902
|
-
const ports = options.scrollport != null ? [options.scrollport] : conversationScrollports();
|
|
1903
|
-
let detachedAny = false;
|
|
1904
|
-
for (const el of ports) if (detachBottomFollow(el, followZonePx)) {
|
|
1905
|
-
el.dispatchEvent(new Event("scroll"));
|
|
1906
|
-
detachedAny = true;
|
|
1907
|
-
}
|
|
1908
|
-
if (detachedAny) await settleScrollEvents();
|
|
1909
|
-
const deadline = Date.now() + pageDeadlineMs;
|
|
1910
|
-
for (;;) {
|
|
1911
|
-
if (rowRenderedInDom(anchorKey)) break;
|
|
1912
|
-
const snapshot = session.getSnapshot();
|
|
1913
|
-
if (snapshot?.hasMore !== true) break;
|
|
1914
|
-
if (Date.now() >= deadline) break;
|
|
1915
|
-
if (snapshot.loadingOlder === true) {
|
|
1916
|
-
await delay(50);
|
|
1917
|
-
continue;
|
|
1918
|
-
}
|
|
1919
|
-
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
1920
|
-
await delay(100);
|
|
1921
|
-
continue;
|
|
1922
|
-
}
|
|
1923
|
-
try {
|
|
1924
|
-
await session.loadOlder();
|
|
1925
|
-
} catch (error) {
|
|
1926
|
-
console.warn("[deepseek-harness-background] timeline jump paging failed:", error);
|
|
1927
|
-
break;
|
|
1928
|
-
}
|
|
1929
|
-
await delay(PAGE_COOLDOWN_MS);
|
|
1930
|
-
}
|
|
1931
|
-
const row = await waitForChatRow(anchorKey, rowWaitMs);
|
|
1932
|
-
if (row === null) {
|
|
1933
|
-
console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${anchorKey}`);
|
|
1934
|
-
return false;
|
|
1935
|
-
}
|
|
1936
|
-
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1937
|
-
const scroller = row.closest("[data-conversation-scroll]");
|
|
1938
|
-
if (scroller === null) {
|
|
1939
|
-
row.scrollIntoView({
|
|
1940
|
-
behavior: "auto",
|
|
1941
|
-
block: "center"
|
|
1942
|
-
});
|
|
1943
|
-
if (!reducedMotion) row.scrollIntoView({
|
|
1944
|
-
behavior: "smooth",
|
|
1945
|
-
block: "center"
|
|
1946
|
-
});
|
|
1947
|
-
return true;
|
|
1948
|
-
}
|
|
1949
|
-
const target = resolveTarget(scroller, row);
|
|
1950
|
-
if (reducedMotion || Math.abs(target - scroller.scrollTop) < MIN_GLIDE_DISTANCE_PX) {
|
|
1951
|
-
scroller.scrollTop = target;
|
|
1952
|
-
return true;
|
|
1953
|
-
}
|
|
1954
|
-
if (detachBottomFollow(scroller, followZonePx)) {
|
|
1955
|
-
scroller.dispatchEvent(new Event("scroll"));
|
|
1956
|
-
await settleScrollEvents();
|
|
1957
|
-
}
|
|
1958
|
-
return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
|
|
1959
|
-
}
|
|
1960
|
-
/**
|
|
1961
|
-
* The rail's own inset, read off the custom property the official component
|
|
1962
|
-
* publishes (`--turn-rail-inset`). Falls back to the official constant.
|
|
1963
|
-
* @param rail - the rail element (or null).
|
|
1964
|
-
*/
|
|
1965
|
-
function railInsetOf(rail) {
|
|
1966
|
-
if (rail === null) return 6;
|
|
1967
|
-
const px = Number.parseFloat(rail.style.getPropertyValue("--turn-rail-inset"));
|
|
1968
|
-
return Number.isFinite(px) && px >= 0 ? px : 6;
|
|
1969
|
-
}
|
|
1970
|
-
/**
|
|
1971
|
-
* Whether `rail` is the frame-style rail (dsh >= 0.1.2-alpha.3): marks sit
|
|
1972
|
-
* at a fixed pitch and scroll INSIDE the frame, whose scroller offset the
|
|
1973
|
-
* component publishes inline as `--turn-scroll-top` — every render, resting
|
|
1974
|
-
* at `0px` included. The compressing alpha.1/2 rail never publishes that
|
|
1975
|
-
* property, so its presence is the generation test. The legacy port paints
|
|
1976
|
-
* its own `--dsbt-*` properties and can never trip this.
|
|
1977
|
-
* @param rail - the rail element (or null).
|
|
1978
|
-
*/
|
|
1979
|
-
function isFrameRail(rail) {
|
|
1980
|
-
return rail !== null && rail.style.getPropertyValue("--turn-scroll-top") !== "";
|
|
1981
|
-
}
|
|
1982
|
-
/**
|
|
1983
|
-
* Resolve the entry under a pointer Y on the frame-style rail — the official
|
|
1984
|
-
* alpha.3 geometry verbatim (`itemAtPointer` in that generation's
|
|
1985
|
-
* TurnNavigator.tsx): marks live in the scrolled ladder at a fixed 10px
|
|
1986
|
-
* pitch, so the click offset adds the scroller offset. Read off the published
|
|
1987
|
-
* `--turn-scroll-top` var rather than the scroller element itself — the same
|
|
1988
|
-
* metric the official preview positioning uses, so any one-frame render lag
|
|
1989
|
-
* affects both identically.
|
|
1990
|
-
* @param count - number of entries the rail shows.
|
|
1991
|
-
* @param rail - the rail element.
|
|
1992
|
-
* @param clientY - pointer viewport Y.
|
|
1993
|
-
*/
|
|
1994
|
-
function frameIndexAtPointer(count, rail, clientY) {
|
|
1995
|
-
const rect = rail.getBoundingClientRect();
|
|
1996
|
-
const inset = railInsetOf(rail);
|
|
1997
|
-
const raw = Number.parseFloat(rail.style.getPropertyValue("--turn-scroll-top"));
|
|
1998
|
-
const scrollTop = Number.isFinite(raw) ? raw : 0;
|
|
1999
|
-
const offset = clientY - rect.top + scrollTop - inset;
|
|
2000
|
-
return Math.max(0, Math.min(count - 1, Math.round(offset / 10)));
|
|
2001
|
-
}
|
|
2002
|
-
/**
|
|
2003
|
-
* Resolve the gesture that produced a rail click to an entry index.
|
|
2004
|
-
*
|
|
2005
|
-
* Keyboard activation targets the focused mark button (recovered by its
|
|
2006
|
-
* position among the rail's buttons); a mouse click carries coordinates and
|
|
2007
|
-
* resolves through the rail geometry — the frame-style mapping over the
|
|
2008
|
-
* published `--turn-scroll-top`. Returns -1 when neither applies (a
|
|
2009
|
-
* keyboard target outside the rail, or a rail of an unsupported generation,
|
|
2010
|
-
* which the kernel's own handler then owns).
|
|
2011
|
-
*
|
|
2012
|
-
* Assumption: the mark ticks are the ONLY buttons under the rail — the
|
|
2013
|
-
* official rail renders marks exclusively and its preview tooltip carries
|
|
2014
|
-
* none. A future non-mark control added inside the rail would shift the
|
|
2015
|
-
* button index and must revisit this.
|
|
2016
|
-
*
|
|
2017
|
-
* @param rail - the rail element.
|
|
2018
|
-
* @param event - the click event.
|
|
2019
|
-
*/
|
|
2020
|
-
function indexForEvent(rail, event) {
|
|
2021
|
-
const target = event.target;
|
|
2022
|
-
if (target instanceof Element) {
|
|
2023
|
-
const button = target.closest("button");
|
|
2024
|
-
if (button !== null && rail.contains(button)) {
|
|
2025
|
-
const index = Array.from(rail.querySelectorAll("button")).indexOf(button);
|
|
2026
|
-
if (index >= 0) return index;
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
const count = rail.querySelectorAll("button").length;
|
|
2030
|
-
if (count === 0) return -1;
|
|
2031
|
-
return isFrameRail(rail) ? frameIndexAtPointer(count, rail, event.clientY) : -1;
|
|
2032
|
-
}
|
|
2033
|
-
/**
|
|
2034
|
-
* Validate the official navigation array crossing the slot boundary. The
|
|
2035
|
-
* value is produced by the kernel and may be absent, partial, or from an
|
|
2036
|
-
* older build, so every field is checked before use.
|
|
2037
|
-
* @param value - the raw `useChat(s => s.navigation.items())` result.
|
|
2038
|
-
*/
|
|
2039
|
-
function normalizeNavigationItems(value) {
|
|
2040
|
-
if (!Array.isArray(value)) return [];
|
|
2041
|
-
const out = [];
|
|
2042
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2043
|
-
for (const item of value) {
|
|
2044
|
-
if (item === null || typeof item !== "object") continue;
|
|
2045
|
-
const rec = item;
|
|
2046
|
-
if (typeof rec.turn !== "number" || !Number.isFinite(rec.turn)) continue;
|
|
2047
|
-
if (typeof rec.anchorKey !== "string" || rec.anchorKey === "") continue;
|
|
2048
|
-
if (seen.has(rec.turn)) continue;
|
|
2049
|
-
seen.add(rec.turn);
|
|
2050
|
-
out.push({
|
|
2051
|
-
turn: rec.turn,
|
|
2052
|
-
anchorKey: rec.anchorKey,
|
|
2053
|
-
prompt: typeof rec.prompt === "string" ? rec.prompt : "",
|
|
2054
|
-
response: typeof rec.response === "string" ? rec.response : ""
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
return out;
|
|
2058
|
-
}
|
|
2059
|
-
/**
|
|
2060
|
-
* Validate one `turnOutline` wire value into its entries. The value is the
|
|
2061
|
-
* host projection's wire view (an array of `{ turn, seq, prompt, response }`
|
|
2062
|
-
* — see `dsh-session-turn-outline`) read through `useProjection`, so it may
|
|
2063
|
-
* be absent (pre-baseline, or an older kernel that never registers the key —
|
|
2064
|
-
* which reads `undefined`), partial, or damaged. `turn` and `seq` are the
|
|
2065
|
-
* load-bearing fields — a mark cannot exist or page without them — and a
|
|
2066
|
-
* damaged one drops the entry; the previews are decorative and degrade to ''.
|
|
2067
|
-
* @param value - the raw `useProjection('turnOutline')` value.
|
|
2068
|
-
*/
|
|
2069
|
-
function normalizeOutlineItems(value) {
|
|
2070
|
-
if (!Array.isArray(value)) return [];
|
|
2071
|
-
const out = [];
|
|
2072
|
-
for (const item of value) {
|
|
2073
|
-
if (item === null || typeof item !== "object") continue;
|
|
2074
|
-
const rec = item;
|
|
2075
|
-
if (typeof rec.turn !== "number" || !Number.isSafeInteger(rec.turn) || rec.turn < 0) continue;
|
|
2076
|
-
if (typeof rec.seq !== "number" || !Number.isSafeInteger(rec.seq) || rec.seq < 0) continue;
|
|
2077
|
-
out.push({
|
|
2078
|
-
turn: rec.turn,
|
|
2079
|
-
seq: rec.seq,
|
|
2080
|
-
prompt: typeof rec.prompt === "string" ? rec.prompt : "",
|
|
2081
|
-
response: typeof rec.response === "string" ? rec.response : ""
|
|
2082
|
-
});
|
|
2083
|
-
}
|
|
2084
|
-
return out;
|
|
1807
|
+
function normalizeOutlineItems(value) {
|
|
1808
|
+
if (!Array.isArray(value)) return [];
|
|
1809
|
+
const out = [];
|
|
1810
|
+
for (const item of value) {
|
|
1811
|
+
if (item === null || typeof item !== "object") continue;
|
|
1812
|
+
const rec = item;
|
|
1813
|
+
if (typeof rec.turn !== "number" || !Number.isSafeInteger(rec.turn) || rec.turn < 0) continue;
|
|
1814
|
+
if (typeof rec.seq !== "number" || !Number.isSafeInteger(rec.seq) || rec.seq < 0) continue;
|
|
1815
|
+
out.push({
|
|
1816
|
+
turn: rec.turn,
|
|
1817
|
+
seq: rec.seq,
|
|
1818
|
+
prompt: typeof rec.prompt === "string" ? rec.prompt : "",
|
|
1819
|
+
response: typeof rec.response === "string" ? rec.response : ""
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
return out;
|
|
2085
1823
|
}
|
|
2086
1824
|
/**
|
|
2087
1825
|
* Rebuild the rail's full mark ladder exactly as the official ChatView does
|
|
@@ -2161,101 +1899,357 @@ window.__ModuleLoader__.load({
|
|
|
2161
1899
|
* React-owned subtree and no official class or paint is overridden.
|
|
2162
1900
|
*/
|
|
2163
1901
|
/**
|
|
2164
|
-
* Structural anchor of the official rail — its inline style carries the
|
|
2165
|
-
* frame's own metric (`--turn-natural-height`), published on every supported
|
|
2166
|
-
* kernel (dsh >= 0.1.2-rc.1). Whether the found rail is the supported
|
|
2167
|
-
* frame-style generation is then told apart by isFrameRail, never by a
|
|
2168
|
-
* version compare.
|
|
1902
|
+
* Structural anchor of the official rail — its inline style carries the
|
|
1903
|
+
* frame's own metric (`--turn-natural-height`), published on every supported
|
|
1904
|
+
* kernel (dsh >= 0.1.2-rc.1). Whether the found rail is the supported
|
|
1905
|
+
* frame-style generation is then told apart by isFrameRail, never by a
|
|
1906
|
+
* version compare.
|
|
1907
|
+
*/
|
|
1908
|
+
const OFFICIAL_RAIL_SELECTOR = "[data-conversation-scroll] nav[style*=\"--turn-natural-height\"]";
|
|
1909
|
+
/**
|
|
1910
|
+
* Rails claimed by an enhancer instance, keyed by the owning instance's
|
|
1911
|
+
* claim token. One session-scoped enhancer mounts per session; without a
|
|
1912
|
+
* claim two instances would both attach capture listeners to the FIRST rail
|
|
1913
|
+
* in the document (multi-column layouts) and one of them — not necessarily
|
|
1914
|
+
* the one owning the reader's column — would win the click. The token
|
|
1915
|
+
* distinguishes "already mine" (the poll keeps it) from "someone else's"
|
|
1916
|
+
* (this instance must stand down). Weak: a replaced rail element disappears
|
|
1917
|
+
* as soon as the browser collects it, so remounts need no bookkeeping.
|
|
1918
|
+
*/
|
|
1919
|
+
const claimedRails = /* @__PURE__ */ new WeakMap();
|
|
1920
|
+
/** How often the official rail is looked for (it mounts with the chat view). */
|
|
1921
|
+
const RAIL_POLL_MS = 400;
|
|
1922
|
+
/**
|
|
1923
|
+
* The enhancer: no visual output, only the behaviour fixes the running rail
|
|
1924
|
+
* generation lacks.
|
|
1925
|
+
* @param props - dock slot props plus the enabled gate.
|
|
1926
|
+
* @returns null (always).
|
|
1927
|
+
*/
|
|
1928
|
+
function OfficialTimelineEnhancer(props) {
|
|
1929
|
+
const { jumpToAnchor, useChat, useProjection, enabled } = props;
|
|
1930
|
+
const ladder = mergeRailItems(normalizeNavigationItems(useChat === void 0 ? void 0 : useChat((snapshot) => {
|
|
1931
|
+
const nav = snapshot?.navigation;
|
|
1932
|
+
return typeof nav?.items === "function" ? nav.items() : void 0;
|
|
1933
|
+
})), useProjection === void 0 ? void 0 : useProjection("turnOutline"));
|
|
1934
|
+
const [rail, setRail] = react.default.useState(null);
|
|
1935
|
+
const ladderRef = react.default.useRef(ladder);
|
|
1936
|
+
ladderRef.current = ladder;
|
|
1937
|
+
const railRef = react.default.useRef(rail);
|
|
1938
|
+
railRef.current = rail;
|
|
1939
|
+
const jumpToAnchorRef = react.default.useRef(jumpToAnchor);
|
|
1940
|
+
jumpToAnchorRef.current = jumpToAnchor;
|
|
1941
|
+
const claimToken = react.default.useMemo(() => Symbol("dsh-bg-timeline-claim"), []);
|
|
1942
|
+
react.default.useEffect(() => {
|
|
1943
|
+
const check = () => {
|
|
1944
|
+
const found = document.querySelector(OFFICIAL_RAIL_SELECTOR);
|
|
1945
|
+
if (found === null) {
|
|
1946
|
+
setRail(null);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const owner = claimedRails.get(found);
|
|
1950
|
+
if (owner !== void 0 && owner !== claimToken) {
|
|
1951
|
+
setRail(null);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
claimedRails.set(found, claimToken);
|
|
1955
|
+
setRail(found);
|
|
1956
|
+
};
|
|
1957
|
+
check();
|
|
1958
|
+
const timer = window.setInterval(check, RAIL_POLL_MS);
|
|
1959
|
+
return () => {
|
|
1960
|
+
window.clearInterval(timer);
|
|
1961
|
+
if (railRef.current !== null) claimedRails.delete(railRef.current);
|
|
1962
|
+
};
|
|
1963
|
+
}, [claimToken]);
|
|
1964
|
+
const navigate = react.default.useCallback((item) => {
|
|
1965
|
+
if (item.anchorKey === void 0) return;
|
|
1966
|
+
const jump = jumpToAnchorRef.current;
|
|
1967
|
+
if (jump === void 0) return;
|
|
1968
|
+
jump(item.anchorKey).catch(() => {});
|
|
1969
|
+
}, []);
|
|
1970
|
+
react.default.useEffect(() => {
|
|
1971
|
+
if (!enabled || rail === null) return;
|
|
1972
|
+
const onClick = (event) => {
|
|
1973
|
+
if (rail.querySelector("[aria-busy=\"true\"]") !== null) return;
|
|
1974
|
+
const index = indexForEvent(rail, event);
|
|
1975
|
+
const item = ladderRef.current[index];
|
|
1976
|
+
if (item === void 0 || item.anchorKey === void 0) return;
|
|
1977
|
+
event.preventDefault();
|
|
1978
|
+
event.stopImmediatePropagation();
|
|
1979
|
+
navigate(item);
|
|
1980
|
+
};
|
|
1981
|
+
rail.addEventListener("click", onClick, true);
|
|
1982
|
+
return () => {
|
|
1983
|
+
rail.removeEventListener("click", onClick, true);
|
|
1984
|
+
};
|
|
1985
|
+
}, [
|
|
1986
|
+
enabled,
|
|
1987
|
+
rail,
|
|
1988
|
+
navigate
|
|
1989
|
+
]);
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
//#endregion
|
|
1993
|
+
//#region src/client/timeline/jump.ts
|
|
1994
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1995
|
+
/** How long one jump may keep paging older history before giving up. */
|
|
1996
|
+
const JUMP_PAGE_DEADLINE_MS = 3e4;
|
|
1997
|
+
/** How long to wait for the chat view to commit the target row. */
|
|
1998
|
+
const JUMP_ROW_WAIT_MS = 3e3;
|
|
1999
|
+
/** Settle between consecutive pages so a resolver without loadingOlder cannot hot-spin the loop. */
|
|
2000
|
+
const PAGE_COOLDOWN_MS = 50;
|
|
2001
|
+
/** Distance from the scrollport top the official ChatView lands a jumped row at. */
|
|
2002
|
+
const OFFICIAL_LANDING_INSET_PX = 24;
|
|
2003
|
+
/** Distances under this snap instead of animating. */
|
|
2004
|
+
const MIN_GLIDE_DISTANCE_PX = 8;
|
|
2005
|
+
/** Glide duration floor / cap — a far-boundary jump stays a tangible slide. */
|
|
2006
|
+
const MIN_GLIDE_MS = 280;
|
|
2007
|
+
const MAX_GLIDE_MS = 900;
|
|
2008
|
+
/** Every mounted conversation scrollport (multi-column layouts have several). */
|
|
2009
|
+
function conversationScrollports() {
|
|
2010
|
+
return Array.from(document.querySelectorAll("[data-conversation-scroll]"));
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Whether the target chat row is committed in ANY mounted conversation
|
|
2014
|
+
* scrollport. Kernel generations disagree on where the node table lives
|
|
2015
|
+
* (0.1.1: `SessionSnapshot.chat.nodes`; 0.1.2+: the `useChat` ChatSnapshot
|
|
2016
|
+
* node store, unreachable from `session.getSnapshot()` — its SessionSnapshot
|
|
2017
|
+
* faces no node table at all), but the rendered DOM is the ground truth both
|
|
2018
|
+
* share, so a jump probes it FIRST before deciding whether paging is needed.
|
|
2019
|
+
* @param anchorKey - chat anchor key of the target row.
|
|
2020
|
+
*/
|
|
2021
|
+
function rowRenderedInDom(anchorKey) {
|
|
2022
|
+
const selector = `[data-chat-anchor-key="${CSS.escape(anchorKey)}"]`;
|
|
2023
|
+
for (const scrollport of conversationScrollports()) if (scrollport.querySelector(selector) !== null) return true;
|
|
2024
|
+
return false;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Poll the conversation DOM until the target row is committed. The session
|
|
2028
|
+
* store updates BEFORE React renders the prepended page, so the old one-shot
|
|
2029
|
+
* query right after loadOlder() resolved could miss the row and silently
|
|
2030
|
+
* give up — the reported "click does nothing until the conversation is
|
|
2031
|
+
* nudged (scroll/click)" behavior.
|
|
2032
|
+
* @param key - chat anchor key to look for.
|
|
2033
|
+
* @param timeoutMs - polling budget.
|
|
2034
|
+
*/
|
|
2035
|
+
async function waitForChatRow(key, timeoutMs) {
|
|
2036
|
+
const selector = `[data-chat-anchor-key="${CSS.escape(key)}"]`;
|
|
2037
|
+
const deadline = Date.now() + timeoutMs;
|
|
2038
|
+
for (;;) {
|
|
2039
|
+
for (const scrollport of conversationScrollports()) {
|
|
2040
|
+
const row = scrollport.querySelector(selector);
|
|
2041
|
+
if (row !== null) return row;
|
|
2042
|
+
}
|
|
2043
|
+
if (Date.now() >= deadline) return null;
|
|
2044
|
+
await delay(40);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
/** Maximum scrollable offset of a conversation scrollport. */
|
|
2048
|
+
function scrollFloorOf(el) {
|
|
2049
|
+
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Destination scrollTop that centers `row` inside `el` — the same geometry
|
|
2053
|
+
* as scrollIntoView({ block: 'center' }), computed directly so the glide owns
|
|
2054
|
+
* the scroll position instead of the browser.
|
|
2055
|
+
* @param el - the conversation scrollport.
|
|
2056
|
+
* @param row - the target chat row.
|
|
2057
|
+
*/
|
|
2058
|
+
function centeredScrollTopFor(el, row) {
|
|
2059
|
+
const floor = scrollFloorOf(el);
|
|
2060
|
+
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - (el.clientHeight - row.offsetHeight) / 2;
|
|
2061
|
+
if (!Number.isFinite(target)) return el.scrollTop;
|
|
2062
|
+
return Math.max(0, Math.min(floor, target));
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Destination scrollTop the official ChatView uses for a navigation jump:
|
|
2066
|
+
* the row's top minus a 24px inset. Kept as a named resolver so the enhancer
|
|
2067
|
+
* lands exactly where the official would — only animated instead of assigned.
|
|
2068
|
+
* @param el - the conversation scrollport.
|
|
2069
|
+
* @param row - the target chat row.
|
|
2070
|
+
*/
|
|
2071
|
+
function officialTargetTopFor(el, row) {
|
|
2072
|
+
const floor = scrollFloorOf(el);
|
|
2073
|
+
const target = el.scrollTop + (row.getBoundingClientRect().top - el.getBoundingClientRect().top) - OFFICIAL_LANDING_INSET_PX;
|
|
2074
|
+
if (!Number.isFinite(target)) return el.scrollTop;
|
|
2075
|
+
return Math.max(0, Math.min(floor, target));
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Glide duration for a distance: longer slides take longer, bounded to
|
|
2079
|
+
* [MIN_GLIDE_MS, MAX_GLIDE_MS] (a deep-history jump reads as a deliberate
|
|
2080
|
+
* glide, not a sluggish race).
|
|
2081
|
+
* @param distance - px to travel.
|
|
2169
2082
|
*/
|
|
2170
|
-
|
|
2083
|
+
function glideDurationFor(distance) {
|
|
2084
|
+
if (!Number.isFinite(distance)) return MIN_GLIDE_MS;
|
|
2085
|
+
return Math.round(Math.max(MIN_GLIDE_MS, Math.min(MAX_GLIDE_MS, 240 + Math.abs(distance) * .14)));
|
|
2086
|
+
}
|
|
2171
2087
|
/**
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2176
|
-
* the
|
|
2177
|
-
*
|
|
2178
|
-
*
|
|
2179
|
-
*
|
|
2088
|
+
* Detach the host's bottom-follow state when the reader currently sits inside
|
|
2089
|
+
* its follow zone: assign the scroll position just outside the boundary. That
|
|
2090
|
+
* one synchronous scroll event settles at-bottom to false (moved-by-reader +
|
|
2091
|
+
* outside the zone) so a tip-moved re-render cannot yank the glide back to
|
|
2092
|
+
* the floor. No-op when already above the zone or when the assignment cannot
|
|
2093
|
+
* move (content shorter than the viewport).
|
|
2094
|
+
* @param el - the conversation scrollport.
|
|
2095
|
+
* @param followZonePx - the host's own follow threshold.
|
|
2180
2096
|
*/
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2097
|
+
function detachBottomFollow(el, followZonePx = 24) {
|
|
2098
|
+
const floor = scrollFloorOf(el);
|
|
2099
|
+
if (el.scrollTop < floor - followZonePx) return false;
|
|
2100
|
+
const detached = Math.max(0, floor - followZonePx - 1);
|
|
2101
|
+
if (Math.abs(el.scrollTop - detached) < 1) return false;
|
|
2102
|
+
el.scrollTop = detached;
|
|
2103
|
+
return true;
|
|
2104
|
+
}
|
|
2184
2105
|
/**
|
|
2185
|
-
*
|
|
2186
|
-
*
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2106
|
+
* Wait until a detach assignment's scroll event has been dispatched AND
|
|
2107
|
+
* consumed by the host's scroll handler: scroll events fire on a later frame
|
|
2108
|
+
* than the assignment, and a paging commit's layout effect runs before that
|
|
2109
|
+
* dispatch — it would re-run the follow logic while the at-bottom flag is
|
|
2110
|
+
* still true and drag the viewport to the growing floor, undoing the detach
|
|
2111
|
+
* (two rAFs + a macrotask bracket the dispatch window).
|
|
2189
2112
|
*/
|
|
2190
|
-
function
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
return () => {
|
|
2220
|
-
window.clearInterval(timer);
|
|
2221
|
-
if (railRef.current !== null) claimedRails.delete(railRef.current);
|
|
2113
|
+
function settleScrollEvents() {
|
|
2114
|
+
if (typeof requestAnimationFrame !== "function") return Promise.resolve();
|
|
2115
|
+
return new Promise((resolve) => {
|
|
2116
|
+
requestAnimationFrame(() => requestAnimationFrame(() => window.setTimeout(resolve, 0)));
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* Animate el.scrollTop toward target with an ease-in-out cubic profile.
|
|
2121
|
+
* Wheel / touch / keyboard-scroll input cancels the glide so the reader
|
|
2122
|
+
* takes over immediately. Resolves true when the glide completed, false
|
|
2123
|
+
* when the reader cancelled it.
|
|
2124
|
+
* @param el - the conversation scrollport.
|
|
2125
|
+
* @param target - destination scrollTop.
|
|
2126
|
+
* @param durationMs - animation budget.
|
|
2127
|
+
*/
|
|
2128
|
+
function animateScrollTop(el, target, durationMs) {
|
|
2129
|
+
return new Promise((resolve) => {
|
|
2130
|
+
const start = el.scrollTop;
|
|
2131
|
+
const delta = target - start;
|
|
2132
|
+
let done = false;
|
|
2133
|
+
let frame = 0;
|
|
2134
|
+
const finish = (completed) => {
|
|
2135
|
+
if (done) return;
|
|
2136
|
+
done = true;
|
|
2137
|
+
el.removeEventListener("wheel", onWheel);
|
|
2138
|
+
el.removeEventListener("touchstart", onTouch);
|
|
2139
|
+
window.removeEventListener("keydown", onKey);
|
|
2140
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
2141
|
+
resolve(completed);
|
|
2222
2142
|
};
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
sessionId,
|
|
2236
|
-
rail
|
|
2237
|
-
]);
|
|
2238
|
-
react.default.useEffect(() => {
|
|
2239
|
-
if (!enabled || rail === null) return;
|
|
2240
|
-
const onClick = (event) => {
|
|
2241
|
-
if (rail.querySelector("[aria-busy=\"true\"]") !== null) return;
|
|
2242
|
-
const index = indexForEvent(rail, event);
|
|
2243
|
-
const item = ladderRef.current[index];
|
|
2244
|
-
if (item === void 0 || item.anchorKey === void 0) return;
|
|
2245
|
-
event.preventDefault();
|
|
2246
|
-
event.stopImmediatePropagation();
|
|
2247
|
-
navigate(item);
|
|
2143
|
+
const onWheel = () => finish(false);
|
|
2144
|
+
const onTouch = () => finish(false);
|
|
2145
|
+
const onKey = (e) => {
|
|
2146
|
+
if ([
|
|
2147
|
+
"ArrowUp",
|
|
2148
|
+
"ArrowDown",
|
|
2149
|
+
"PageUp",
|
|
2150
|
+
"PageDown",
|
|
2151
|
+
"Home",
|
|
2152
|
+
"End",
|
|
2153
|
+
" "
|
|
2154
|
+
].includes(e.key)) finish(false);
|
|
2248
2155
|
};
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2156
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
2157
|
+
el.addEventListener("touchstart", onTouch, { passive: true });
|
|
2158
|
+
window.addEventListener("keydown", onKey);
|
|
2159
|
+
if (Math.abs(delta) < .5 || durationMs <= 0) {
|
|
2160
|
+
el.scrollTop = target;
|
|
2161
|
+
finish(true);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const t0 = performance.now();
|
|
2165
|
+
const ease = (x) => x < .5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;
|
|
2166
|
+
const step = (now) => {
|
|
2167
|
+
if (done) return;
|
|
2168
|
+
const t = Math.min(1, (now - t0) / durationMs);
|
|
2169
|
+
el.scrollTop = start + delta * ease(t);
|
|
2170
|
+
if (t < 1) frame = requestAnimationFrame(step);
|
|
2171
|
+
else finish(true);
|
|
2252
2172
|
};
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2173
|
+
frame = requestAnimationFrame(step);
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
/**
|
|
2177
|
+
* Ensure the target node is loaded, wait for the chat view to commit its row
|
|
2178
|
+
* into the DOM, then glide it into view. Paging uses a time budget instead
|
|
2179
|
+
* of a fixed iteration guard (each in-flight page burned part of that guard,
|
|
2180
|
+
* so slow history could exhaust it and silently fail).
|
|
2181
|
+
*
|
|
2182
|
+
* @param sessionsService - the runtime sessions service.
|
|
2183
|
+
* @param sessionId - the session owning the target row.
|
|
2184
|
+
* @param anchorKey - chat anchor key of the target row.
|
|
2185
|
+
* @param options - follow zone, landing resolver, paging scopes and budgets.
|
|
2186
|
+
* @returns whether the row was reached (false when it never rendered or the
|
|
2187
|
+
* reader took the scrollport back mid-glide).
|
|
2188
|
+
*/
|
|
2189
|
+
async function jumpToMessage(sessionsService, sessionId, anchorKey, options = {}) {
|
|
2190
|
+
const session = sessionsService.binding(sessionId)?.session;
|
|
2191
|
+
if (session === void 0) return false;
|
|
2192
|
+
const followZonePx = options.followZonePx ?? 24;
|
|
2193
|
+
const resolveTarget = options.targetTop ?? centeredScrollTopFor;
|
|
2194
|
+
const pageDeadlineMs = options.pageDeadlineMs ?? JUMP_PAGE_DEADLINE_MS;
|
|
2195
|
+
const rowWaitMs = options.rowWaitMs ?? JUMP_ROW_WAIT_MS;
|
|
2196
|
+
const ports = options.scrollport != null ? [options.scrollport] : conversationScrollports();
|
|
2197
|
+
let detachedAny = false;
|
|
2198
|
+
for (const el of ports) if (detachBottomFollow(el, followZonePx)) {
|
|
2199
|
+
el.dispatchEvent(new Event("scroll"));
|
|
2200
|
+
detachedAny = true;
|
|
2201
|
+
}
|
|
2202
|
+
if (detachedAny) await settleScrollEvents();
|
|
2203
|
+
const deadline = Date.now() + pageDeadlineMs;
|
|
2204
|
+
for (;;) {
|
|
2205
|
+
if (rowRenderedInDom(anchorKey)) break;
|
|
2206
|
+
const snapshot = session.getSnapshot();
|
|
2207
|
+
if (snapshot?.hasMore !== true) break;
|
|
2208
|
+
if (Date.now() >= deadline) break;
|
|
2209
|
+
if (snapshot.loadingOlder === true) {
|
|
2210
|
+
await delay(50);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
if (snapshot.openState !== void 0 && snapshot.openState !== "open") {
|
|
2214
|
+
await delay(100);
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
await session.loadOlder();
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
console.warn("[deepseek-harness-background] timeline jump paging failed:", error);
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
await delay(PAGE_COOLDOWN_MS);
|
|
2224
|
+
}
|
|
2225
|
+
const row = await waitForChatRow(anchorKey, rowWaitMs);
|
|
2226
|
+
if (row === null) {
|
|
2227
|
+
console.warn(`[deepseek-harness-background] timeline jump target never rendered: ${anchorKey}`);
|
|
2228
|
+
return false;
|
|
2229
|
+
}
|
|
2230
|
+
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2231
|
+
const scroller = row.closest("[data-conversation-scroll]");
|
|
2232
|
+
if (scroller === null) {
|
|
2233
|
+
row.scrollIntoView({
|
|
2234
|
+
behavior: "auto",
|
|
2235
|
+
block: "center"
|
|
2236
|
+
});
|
|
2237
|
+
if (!reducedMotion) row.scrollIntoView({
|
|
2238
|
+
behavior: "smooth",
|
|
2239
|
+
block: "center"
|
|
2240
|
+
});
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
const target = resolveTarget(scroller, row);
|
|
2244
|
+
if (reducedMotion || Math.abs(target - scroller.scrollTop) < MIN_GLIDE_DISTANCE_PX) {
|
|
2245
|
+
scroller.scrollTop = target;
|
|
2246
|
+
return true;
|
|
2247
|
+
}
|
|
2248
|
+
if (detachBottomFollow(scroller, followZonePx)) {
|
|
2249
|
+
scroller.dispatchEvent(new Event("scroll"));
|
|
2250
|
+
await settleScrollEvents();
|
|
2251
|
+
}
|
|
2252
|
+
return await animateScrollTop(scroller, target, glideDurationFor(Math.abs(target - scroller.scrollTop)));
|
|
2259
2253
|
}
|
|
2260
2254
|
//#endregion
|
|
2261
2255
|
//#region src/client/timeline/index.tsx
|
|
@@ -2361,7 +2355,14 @@ window.__ModuleLoader__.load({
|
|
|
2361
2355
|
id: "deepseek-harness-background.timeline",
|
|
2362
2356
|
order: 45,
|
|
2363
2357
|
locale: "ui-background",
|
|
2364
|
-
inject: () => ({
|
|
2358
|
+
inject: (sessionId) => ({ jumpToAnchor: (anchorKey) => {
|
|
2359
|
+
const scrollport = document.querySelector("[data-conversation-scroll] nav[style*=\"--turn-natural-height\"]")?.closest("[data-conversation-scroll]") ?? null;
|
|
2360
|
+
return jumpToMessage(ctx.sessions, sessionId, anchorKey, {
|
|
2361
|
+
followZonePx: 24,
|
|
2362
|
+
targetTop: officialTargetTopFor,
|
|
2363
|
+
scrollport
|
|
2364
|
+
});
|
|
2365
|
+
} })
|
|
2365
2366
|
}, TimelineBridge));
|
|
2366
2367
|
ctx.effect(() => () => {
|
|
2367
2368
|
unsubscribe();
|