refactor(chat): rename useTranscriptScroll → useDeprecatedTranscriptScroll, add dev flag to disable it#32030
Conversation
| // Imperative scroll controller — owns ResizeObservers, user-input | ||
| // listeners, the auto-pin window, and the saved anchor snapshot. The | ||
| // hook keeps a stable reference for the controller's lifetime. | ||
| const [controller] = useState(() => new TranscriptScrollController()); |
There was a problem hiding this comment.
This all feels too risky to do at once. Let's do this:
- create this as a parallel implementation path instead of changing
useTranscriptScroll - add a
window._vellumDebug.flags.toggleTranscriptScrollController(flag?: boolean) - we can develop old vs new so we can just nuke
use-transcript-scroll.tswhen we're done
There was a problem hiding this comment.
Reshaped per your direction — pushed force-with-lease on the same branch.
use-transcript-scroll.tsis now bit-for-bit identical toorigin/main. Zero diff against legacy behavior.- New parallel hook lives at
use-transcript-scroll-controller.ts. window._vellumDebug.flags.toggleTranscriptScrollController(flag?: boolean)is wired up — persists to localStorage, reloads page, default off.useTranscriptScrollDispatch(new file) resolves to one of the two hooks at module-load time so React sees a stable hook identity across renders.
PR 2 will evolve the new file to drive the controller via imperative methods and delete its kitchen-sink items effect. PR 3 splits chain-load into a pagination-owned hook. When the new path reaches parity, we flip the flag default and nuke use-transcript-scroll.ts in a final cleanup PR.
Updated PR description + title above.
5088da4 to
415842c
Compare
| // PR 1 (this PR) ships the parallel implementation at near-feature | ||
| // parity with the legacy hook so the toggle is a fair comparison: the | ||
| // imperative DOM bookkeeping lives in the controller, but the | ||
| // kitchen-sink items `useLayoutEffect` is preserved (now talking to | ||
| // controller methods instead of local refs) so the path behaves | ||
| // identically to `use-transcript-scroll.ts` for every existing | ||
| // scenario. |
There was a problem hiding this comment.
Delete all comments referencing PR numbers
There was a problem hiding this comment.
Done. Stripped PR-number references from all five new files (transcript-scroll-controller.ts, use-transcript-scroll-controller.ts, use-transcript-scroll-dispatch.ts, transcript-scroll-flag.ts, and the test file). Pre-existing ATL-* references in debug-api.ts left alone — those are not from this PR.
| // PR plan (3 PRs total — see scratch/scroll-imperative-spec.md): | ||
| // • PR 1 (this PR): extract the controller, keep the existing hook | ||
| // surface and behavior. Net: zero behavior change. | ||
| // • PR 2: flip orchestrator call sites to imperative method calls | ||
| // around the data events (`onConversationSwitched`, | ||
| // `onMessageSubmitted`, `beforeOlderPageFetch`, | ||
| // `afterOlderPageApplied`). Delete the items `useLayoutEffect`. | ||
| // • PR 3: split the underfilled-viewport chain-load into a hook | ||
| // owned by the pagination layer. |
There was a problem hiding this comment.
Same as this remove
There was a problem hiding this comment.
Done — see reply above for the file list.
There was a problem hiding this comment.
I would expect all three of these to be disabled if we toggled our scroll controller. And the new scroll controller should not need to necessarily align with the api given by our hook
There was a problem hiding this comment.
Reshaped the new path to be event-driven instead of API-mirrored. The new hook no longer matches the legacy { handleScroll, scrollToLatest({behavior}) } surface — it returns { mode: "controller", isPinnedToLatest, showScrollToLatest, onMessageSubmitted, onScrollToLatestClicked } instead.
All three orchestrator wirings in this file now narrow on scrollCoordinator.mode and short-circuit in controller mode:
- Scroll listener
useEffect(above) — early-returns whenmode !== "legacy". The controller attaches its ownscrolllistener insideattachScrollElementso we don't double-attach. handleScrollToLatestcallback — routes toscrollCoordinator.onScrollToLatestClicked()on controller mode, falls back to the legacyscrollToLatest({behavior:"smooth"})on legacy mode.handleSubmitscroll call (line ~932) — routes toscrollCoordinator.onMessageSubmitted()on controller mode, falls back to the legacyscrollToLatest({behavior:"auto"})on legacy mode.
The controller now owns the scroll-event listener, the classified state (isPinnedToLatest / showScrollToLatest exposed via subscribe + snapshot for useSyncExternalStore), and the imperative scroll-to-latest. The orchestrator in controller mode just instantiates the hook and informs it of user events; the controller decides what to do.
Dispatcher return is now a discriminated union (UseTranscriptScrollLegacyReturn | UseTranscriptScrollControllerReturn) so the narrowing is type-safe.
e5052db to
2c75640
Compare
|
Reshaped per the latest direction from FE. This PR no longer introduces the controller. It only:
Pushed force-with-lease at The three earlier review comments are now stale (the files they pointed to no longer exist), so I won't try to resolve them in-thread. Happy to walk through the new shape on review. |
| // ------------------------------------------------------------------------- | ||
|
|
||
| const scrollCoordinator = useTranscriptScroll({ | ||
| const scrollCoordinator = useTranscriptScrollDispatch({ |
There was a problem hiding this comment.
Don't make this useTranscriptScrollDispatch, just import useDeprecatedTranscriptScroll directly
There was a problem hiding this comment.
Done — orchestrator now imports useDeprecatedTranscriptScroll directly. d1a752f
| @@ -846,6 +846,10 @@ export function ChatRouteContent({ | |||
| }); | |||
|
|
|||
| useEffect(() => { | |||
There was a problem hiding this comment.
move this useEffect into useDeprecatedTranscriptScroll
There was a problem hiding this comment.
Done — useEffect moved into useDeprecatedTranscriptScroll. The orchestrator no longer wires the scroll listener. d1a752f
| function useTranscriptScrollDisabled( | ||
| _args: TranscriptScrollDispatchArgs, | ||
| ): TranscriptScrollDisabledReturn { | ||
| return DISABLED_RESULT; | ||
| } |
There was a problem hiding this comment.
delete this - we don't need any logic at all for TRANSCRIPT_SCROLL_CONTROLLER_ENABLED
There was a problem hiding this comment.
Done — deleted use-transcript-scroll-dispatch.ts entirely. d1a752f
| | TranscriptScrollDisabledReturn; | ||
|
|
||
| const DISABLED_RESULT: TranscriptScrollDisabledReturn = { | ||
| mode: "disabled", |
There was a problem hiding this comment.
the other mode is going to be "controller", not "deprecated", but we should not introduce that other mode in this PR at all. for now our toggle will just on and off the deprecated scroll
There was a problem hiding this comment.
Done — no dispatcher / no controller mode in this PR. The flag (when ON) just early-returns no-op defaults from useDeprecatedTranscriptScroll itself. Module-load constant so the early return is consistent across renders → React hook rules satisfied. d1a752f
2c75640 to
d1a752f
Compare
d1a752f to
69305a8
Compare
…croll, add dev flag to disable it
Signals intent to delete the existing transcript scroll-coordination
logic without introducing a replacement in this PR. Sets up a runtime
flag (`window._vellumDebug.flags.toggleTranscriptScrollController()`)
that turns the deprecated hook OFF — flipping it on leaves the
transcript with no JavaScript scroll coordination at all. This is the
baseline against which the eventual controller will be built and
landed (behind this same flag) in a follow-up PR.
Renames:
- `use-transcript-scroll.ts` → `use-deprecated-transcript-scroll.ts`
- `useTranscriptScroll` → `useDeprecatedTranscriptScroll`
- `UseTranscriptScrollArgs` → `UseDeprecatedTranscriptScrollArgs`
- `UseTranscriptScrollReturn` → `UseDeprecatedTranscriptScrollReturn`
- Corresponding `.test.ts` file renamed.
New files:
- `transcript-scroll-flag.ts` — localStorage flag (default off,
persists across reloads, toggling reloads the page so React hook
ordering stays stable).
- `use-transcript-scroll-dispatch.ts` — module-load picker that
returns a discriminated union: `{ mode: "deprecated", ...full
deprecated-hook surface }` when the flag is off; `{ mode: "disabled",
isPinnedToLatest: true, showScrollToLatest: false }` when on.
Changes to existing files:
- `chat-route-content.tsx` — switched from `useTranscriptScroll` to
`useTranscriptScrollDispatch`. Three legacy wirings (scroll listener
`useEffect`, scroll-to-latest button callback, submit-time scroll
call) now narrow on `scrollCoordinator.mode === "deprecated"` and
short-circuit in disabled mode.
- `debug-api.ts` + tests — adds `toggleTranscriptScrollController()`
to `_vellumDebug.flags`. Import path updated for the rename.
Invariants:
- Deprecated-hook file is byte-for-byte identical to origin/main
modulo the four rename substitutions above (verified via `git diff
--stat` showing 99% similarity).
- Flag default off → production behavior unchanged.
Test coverage:
- All 77 existing `use-deprecated-transcript-scroll.test.ts` cases
still pass.
- All 4 existing `debug-api.test.ts` cases still pass (extended for
the new flag toggler).
69305a8 to
dbb6084
Compare
Why
Signals intent to delete the existing transcript scroll-coordination logic without introducing a replacement in this PR. Sets up a runtime flag (
window._vellumDebug.flags.toggleTranscriptScrollController()) that turns the deprecated hook off. Flipping it on leaves the transcript with zero JavaScript scroll coordination — that's the baseline against which the eventualTranscriptScrollControllerwill be built and landed (behind this same flag) in a follow-up PR.The flag name reflects the eventual destination, not the current intermediate state.
What's in this PR
Renames (the deprecation signal)
use-transcript-scroll.ts→use-deprecated-transcript-scroll.tsuseTranscriptScroll→useDeprecatedTranscriptScrollUseTranscriptScrollArgs→UseDeprecatedTranscriptScrollArgsUseTranscriptScrollReturn→UseDeprecatedTranscriptScrollReturn.test.tsfile renamed.Behavior changes inside the renamed hook
useEffect. The orchestrator no longer wires thescrolllistener externally.handleScrollis no longer part of the return surface — it's an internal-only callback.TRANSCRIPT_SCROLL_CONTROLLER_ENABLED(read once from localStorage) gates the entire hook body. When the flag is on, the hook early-returns{ isPinnedToLatest: true, showScrollToLatest: false, scrollToLatest: () => {} }and runs no effects, no state, no listeners. Because the constant is stable across the page lifetime (toggling reloads), the early return is taken consistently every render → React hook-order rules are satisfied.New files
transcript-scroll-flag.ts— localStorage flag, default off, persists across reloads, toggling reloads the page.Changes to the orchestrator
chat-route-content.tsximportsuseDeprecatedTranscriptScrolldirectly (no dispatcher abstraction).useEffectmoved into the hook.modenarrowing — when the flag is on the hook simply returns defaults that produce a no-op pill and a no-opscrollToLatest.Debug API
debug-api.ts+ tests — addstoggleTranscriptScrollController(flag?: boolean)towindow._vellumDebug.flags.How to test
Test coverage
use-deprecated-transcript-scroll.test.tscases still pass.debug-api.test.tscases still pass (extended for the new flag toggler).Follow-ups (not in this PR)
TranscriptScrollControllerand a new hook that drives it. Land behind the same flag — flipping it on routes to the new path. The deprecated hook stays as-is on the other side of the toggle until parity.use-deprecated-transcript-scroll.tsoutright.