mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(web): kick the compositor on a chat scroll freeze, then take the wheel and keyboard over by default When the freeze probe calls the chat log frozen, apply one frame of `will-change: transform` so Blink rebuilds the scroller's compositing state from the current layout, let the next downward wheel through natively and measure it against what it asked for (the detector's own normalisation and 8px yardstick), and only if it stuck, fell short or snapped back answer the wheel and the scrolling keys from JavaScript. The takeover is on by default now; `open-design:chat-scroll-takeover = '0'` is the escape hatch. `ChatPane` releases the takeover — and asks the probe to start a fresh surface — when `activeConversationId` changes, because the log node is reused across conversations. A healed surface is released the same way so a recurrence can be judged again. One new event, `client_chat_scroll_heal`, carries the outcome, the kick geometry and the notch arithmetic, joined to the freeze by `probe_id`; `client_chat_scroll_frozen` is untouched. * fix(web): keep Home/End finite and let the takeover go when the chat log is removed Home and End were ±Infinity folded into the same pixel accumulator as the arrows, so Home-then-End in one frame (or key repeat) summed to NaN and the browser read the NaN scrollTop as 0 — End could not reach the bottom. Edge jumps now live in their own `pendingEdge` slot: the last edge pressed wins, pressing one discards the steps queued before it, and steps pressed after it apply on top; applyPending refuses to write a non-finite value. A tab or route unmount removes the chat log without a conversation change, and the probe only notices on a scroll the missing node never sends, so the takeover stayed engaged with its document-level keydown listener cancelling arrows and paging with nothing to scroll. Every input path (wheel, keydown, frame) now checks `isConnected` and disengages, and ChatPane's release effect also releases in its cleanup.
61 lines
3.0 KiB
TypeScript
61 lines
3.0 KiB
TypeScript
'use client';
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
import { installErrorHandlers } from '../../src/analytics/error-tracking';
|
|
import { MatrixLoader } from '../../src/components/MatrixLoader';
|
|
import { installWebObservability } from '../../src/observability/install';
|
|
import { installChatScrollExperiments } from '../../src/runtime/chat-scroll-experiments';
|
|
import { installChatScrollTakeover } from '../../src/runtime/chat-scroll-takeover';
|
|
|
|
// Install browser exception handlers at module-load time, before any other
|
|
// client code can throw. The hooks buffer events until AnalyticsProvider
|
|
// finishes `bootstrapExceptionTracking()` with the PostHog key, so even
|
|
// errors thrown during the dynamic import of `src/App` are captured.
|
|
installErrorHandlers();
|
|
|
|
// Install the rest of the observability surface (long tasks, white-screen
|
|
// detector, resource-error capture, boot timing, visibility tracking).
|
|
// Same buffer + consent-bypass transport as the exception handler above
|
|
// so events fired before AnalyticsProvider initialises still flush.
|
|
installWebObservability();
|
|
|
|
// The consumer of the scroll-freeze probe's verdict that acts on it: when the
|
|
// chat log's compositor-side scroll extent goes stale, kick the compositor for
|
|
// one frame, watch the next wheel, and if that did not help answer the wheel
|
|
// and the keyboard from JavaScript. Deliberately NOT part of
|
|
// `installWebObservability()` — that entry point is for observers, and this
|
|
// changes behaviour. On by default (product decision, 2026-09-17); setting
|
|
// `open-design:chat-scroll-takeover` to `'0'` makes this call read one storage
|
|
// key and return without registering anything.
|
|
installChatScrollTakeover();
|
|
|
|
// 滚动冻结的两个未证伪假设(H2 自观察 / H3 msg-enter 的 fill:both)各有一个
|
|
// localStorage 开关,好让同一个包能做 A/B。两个都不设时这一调用读两个键就返回,
|
|
// 根节点上不留任何痕迹 —— 行为和没有这行时一模一样。
|
|
// 必须在 React 挂载**之前**跑:H3 是入场动画,类名晚一帧到就会先闪一次动画。
|
|
// 操作说明(怎么开、怎么关、开了会失去什么)写在模块顶部的 docblock 里。
|
|
installChatScrollExperiments();
|
|
|
|
// The product is a fully client-driven SPA — every component reads
|
|
// localStorage, window.location, etc. — so we opt out of static-time
|
|
// rendering for the entire tree. This keeps `next build --output export`
|
|
// from trying to evaluate browser-only code while still emitting a real
|
|
// shell HTML the daemon can serve as the SPA fallback.
|
|
const App = dynamic(() => import('../../src/App').then((m) => m.App), {
|
|
ssr: false,
|
|
// Keeps the `od-loading-shell` class on the outer node: the white-screen
|
|
// detector filters this whole subtree out by that class when deciding
|
|
// whether the app really mounted (`src/observability/white-screen.ts`).
|
|
loading: () => (
|
|
<div className="od-loading-shell">
|
|
<MatrixLoader />
|
|
<span>Loading OpenDesign…</span>
|
|
</div>
|
|
),
|
|
});
|
|
|
|
export function ClientApp() {
|
|
return <App />;
|
|
}
|