diff --git a/apps/web/src/domains/chat/components/chat-route-content.tsx b/apps/web/src/domains/chat/components/chat-route-content.tsx index c37e56b3a1b..9ff2b87159b 100644 --- a/apps/web/src/domains/chat/components/chat-route-content.tsx +++ b/apps/web/src/domains/chat/components/chat-route-content.tsx @@ -36,7 +36,7 @@ import { usePullRefresh } from "@/domains/chat/hooks/use-pull-refresh"; import { useRefreshLatestMessages as _useRefreshLatestMessages } from "@/domains/chat/hooks/use-refresh-latest-messages"; import { useConversationStarters } from "@/domains/chat/hooks/use-conversation-starters"; import type { TranscriptHandle, TranscriptProps } from "@/domains/chat/transcript/transcript"; -import { useDeprecatedTranscriptScroll } from "@/domains/chat/transcript/use-deprecated-transcript-scroll"; +import { useTranscriptScroll } from "@/domains/chat/transcript/use-transcript-scroll"; import { hasPendingAssistantResponse } from "@/domains/chat/utils/chat-utils"; import type { ChatError } from "@/domains/chat/types"; import type { AssistantState } from "@/domains/chat/hooks/use-assistant-lifecycle"; @@ -857,7 +857,7 @@ export function ChatRouteContent({ // Scroll coordination // ------------------------------------------------------------------------- - const scrollCoordinator = useDeprecatedTranscriptScroll({ + const scrollCoordinator = useTranscriptScroll({ transcriptRef: refs.transcriptRef, items: transcriptItems, conversationId: activeConversationId, diff --git a/apps/web/src/domains/chat/transcript/transcript-scroll-flag.ts b/apps/web/src/domains/chat/transcript/transcript-scroll-flag.ts deleted file mode 100644 index 9384ce68848..00000000000 --- a/apps/web/src/domains/chat/transcript/transcript-scroll-flag.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Runtime flag that turns OFF the deprecated transcript scroll hook. -// -// This file is intentionally isolated from the rest of the scroll -// utilities so it can be deleted in one move when the migration is -// complete — no other file in `transcript/` depends on it except -// `use-deprecated-transcript-scroll.ts` and `transcript-scroll.ts`, -// both of which will be deleted/refactored at that point. -// -// In the current shape of this work-in-progress: -// -// • Flag OFF (default) — the orchestrator runs the deprecated hook -// `useDeprecatedTranscriptScroll`, which is the existing -// production scroll-coordination logic. Production users land -// here. We keep this in the tree so we don't regress shipping -// behavior while we redesign the replacement. -// • Flag ON — the deprecated hook returns its no-op result. The -// replacement utilities in `transcript-scroll.ts` take over -// piecewise as features migrate. -// -// Why module-load read + reload-on-toggle: -// -// • React forbids conditionally calling different hooks across -// renders. By making the dispatch decision once at module-import -// time (before any component mounts), the dispatcher resolves to a -// single function identity for the entire page lifetime. -// • Toggling without a reload would leave the DOM in an inconsistent -// intermediate state (scroll listeners attached but no longer -// handled, in-flight auto-pin timers orphaned). A page reload is -// cheap and dev-only. -// -// Surface (exposed under `window._vellumDebug.flags`): -// -// toggleTranscriptScrollController() — flip current value -// toggleTranscriptScrollController(true) — force on -// toggleTranscriptScrollController(false) — force off - -const STORAGE_KEY = "vellumDebug.flags.transcriptScrollController"; - -/** Read the flag synchronously. Safe to call at module-load time. */ -export function getTranscriptScrollControllerEnabled(): boolean { - if (typeof window === "undefined") return false; - try { - return window.localStorage.getItem(STORAGE_KEY) === "true"; - } catch { - // Private-browsing modes or sandboxed contexts can throw on - // localStorage access. Treat any throw as "flag off". - return false; - } -} - -/** Persist the flag, log the new value, and reload the page so the - * dispatcher re-resolves. `value === undefined` flips the current - * value (the most common interactive case). */ -export function setTranscriptScrollControllerEnabled(value?: boolean): boolean { - if (typeof window === "undefined") return false; - const next = - value === undefined ? !getTranscriptScrollControllerEnabled() : !!value; - try { - window.localStorage.setItem(STORAGE_KEY, String(next)); - } catch { - // Persistence failed — log and bail so the user knows their - // toggle didn't stick. - console.warn( - "[vellumDebug] failed to persist transcriptScrollController flag", - ); - return getTranscriptScrollControllerEnabled(); - } - console.info( - `[vellumDebug] transcriptScrollController = ${next} — reloading…`, - ); - window.location.reload(); - return next; -} - -/** The flag value resolved exactly once at module load. The - * dispatcher reads this constant so hook-rule order stays stable - * across the page lifetime. */ -export const TRANSCRIPT_SCROLL_CONTROLLER_ENABLED = - getTranscriptScrollControllerEnabled(); diff --git a/apps/web/src/domains/chat/transcript/transcript-scroll.test.ts b/apps/web/src/domains/chat/transcript/transcript-scroll.test.ts deleted file mode 100644 index c9a844a1af1..00000000000 --- a/apps/web/src/domains/chat/transcript/transcript-scroll.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Tests for the imperative transcript scroll utilities. - * - * `bun:test` runs without a real DOM, so we exercise `attachSnapToLatest` - * — the pure imperative function the hook delegates to — against - * a minimal fake element shape. The hook itself is thin glue around - * this function; testing the function covers the load-bearing - * behavior (initial snap, re-snap on content resize, gesture disengage). - */ - -import { describe, expect, test } from "bun:test"; - -import { attachSnapToLatest } from "@/domains/chat/transcript/transcript-scroll"; - -type Listener = (...args: unknown[]) => void; - -type FakeElement = { - scrollTop: number; - scrollHeight: number; - addEventListener(event: string, listener: Listener): void; - removeEventListener(event: string, listener: Listener): void; - fire(event: string): void; - listenerCount(event: string): number; -}; - -function createFakeElement(scrollHeight: number): FakeElement { - const listeners = new Map>(); - return { - scrollTop: 0, - scrollHeight, - addEventListener(event, listener) { - let set = listeners.get(event); - if (!set) { - set = new Set(); - listeners.set(event, set); - } - set.add(listener); - }, - removeEventListener(event, listener) { - listeners.get(event)?.delete(listener); - }, - fire(event) { - listeners.get(event)?.forEach((l) => l()); - }, - listenerCount(event) { - return listeners.get(event)?.size ?? 0; - }, - }; -} - -class FakeResizeObserver { - static instances: FakeResizeObserver[] = []; - callback: ResizeObserverCallback; - observed: Element[] = []; - disconnected = false; - constructor(callback: ResizeObserverCallback) { - this.callback = callback; - FakeResizeObserver.instances.push(this); - } - observe(el: Element): void { - this.observed.push(el); - } - unobserve(): void {} - disconnect(): void { - this.disconnected = true; - } - fire(): void { - this.callback([], this as unknown as ResizeObserver); - } -} - -function installFakeResizeObserver(): void { - FakeResizeObserver.instances = []; - (globalThis as { ResizeObserver?: unknown }).ResizeObserver = - FakeResizeObserver; -} - -function uninstallFakeResizeObserver(): void { - delete (globalThis as { ResizeObserver?: unknown }).ResizeObserver; -} - -describe("attachSnapToLatest", () => { - test("snaps to bottom on initial attach", () => { - installFakeResizeObserver(); - const container = createFakeElement(1000); - const content = createFakeElement(1000); - container.scrollTop = 0; - - const stop = attachSnapToLatest({ - container: container as unknown as HTMLElement, - content: content as unknown as HTMLElement, - }); - - expect(container.scrollTop).toBe(1000); - - stop(); - uninstallFakeResizeObserver(); - }); - - test("re-snaps on content ResizeObserver fire (covers seed-then-grow race)", () => { - installFakeResizeObserver(); - const container = createFakeElement(500); - const content = createFakeElement(500); - - attachSnapToLatest({ - container: container as unknown as HTMLElement, - content: content as unknown as HTMLElement, - }); - - expect(container.scrollTop).toBe(500); - - // Simulate `useViewportMinHeight` seeding LatestTurnRow's minHeight: - // content grows, scrollHeight grows. - container.scrollHeight = 1500; - FakeResizeObserver.instances[0].fire(); - - expect(container.scrollTop).toBe(1500); - - uninstallFakeResizeObserver(); - }); - - test("stops re-snapping after user wheel gesture", () => { - installFakeResizeObserver(); - const container = createFakeElement(500); - const content = createFakeElement(500); - - attachSnapToLatest({ - container: container as unknown as HTMLElement, - content: content as unknown as HTMLElement, - }); - - // User scrolls up. - container.scrollTop = 100; - container.fire("wheel"); - - // ResizeObserver fires after disengage — should NOT re-snap. - container.scrollHeight = 1500; - FakeResizeObserver.instances[0].fire(); - - expect(container.scrollTop).toBe(100); - expect(FakeResizeObserver.instances[0].disconnected).toBe(true); - - uninstallFakeResizeObserver(); - }); - - test("stops re-snapping after touchmove and keydown gestures too", () => { - installFakeResizeObserver(); - const container1 = createFakeElement(500); - const content1 = createFakeElement(500); - attachSnapToLatest({ - container: container1 as unknown as HTMLElement, - content: content1 as unknown as HTMLElement, - }); - container1.scrollTop = 100; - container1.fire("touchmove"); - container1.scrollHeight = 1500; - FakeResizeObserver.instances[0].fire(); - expect(container1.scrollTop).toBe(100); - - const container2 = createFakeElement(500); - const content2 = createFakeElement(500); - attachSnapToLatest({ - container: container2 as unknown as HTMLElement, - content: content2 as unknown as HTMLElement, - }); - container2.scrollTop = 100; - container2.fire("keydown"); - container2.scrollHeight = 1500; - FakeResizeObserver.instances[1].fire(); - expect(container2.scrollTop).toBe(100); - - uninstallFakeResizeObserver(); - }); - - test("teardown removes listeners and disconnects observer", () => { - installFakeResizeObserver(); - const container = createFakeElement(500); - const content = createFakeElement(500); - - const stop = attachSnapToLatest({ - container: container as unknown as HTMLElement, - content: content as unknown as HTMLElement, - }); - - expect(container.listenerCount("wheel")).toBe(1); - expect(container.listenerCount("touchmove")).toBe(1); - expect(container.listenerCount("keydown")).toBe(1); - - stop(); - - expect(container.listenerCount("wheel")).toBe(0); - expect(container.listenerCount("touchmove")).toBe(0); - expect(container.listenerCount("keydown")).toBe(0); - expect(FakeResizeObserver.instances[0].disconnected).toBe(true); - - uninstallFakeResizeObserver(); - }); - - test("no ResizeObserver available — snaps once and returns inert teardown", () => { - // SSR / older test environments. - const container = createFakeElement(800); - const content = createFakeElement(800); - - const stop = attachSnapToLatest({ - container: container as unknown as HTMLElement, - content: content as unknown as HTMLElement, - }); - - expect(container.scrollTop).toBe(800); - expect(container.listenerCount("wheel")).toBe(0); - - // Should not throw. - stop(); - }); -}); diff --git a/apps/web/src/domains/chat/transcript/transcript-scroll.ts b/apps/web/src/domains/chat/transcript/transcript-scroll.ts deleted file mode 100644 index 6ad6eb24fee..00000000000 --- a/apps/web/src/domains/chat/transcript/transcript-scroll.ts +++ /dev/null @@ -1,136 +0,0 @@ -// Transcript scroll utilities — the imperative replacement for -// `useDeprecatedTranscriptScroll`. Listens to DOM lifecycle events -// (element attached, content resized, user gesture) rather than -// reacting to React state changes. Full spec lives at -// `/workspace/scratch/scroll-imperative-spec.md`. -// -// Gating against `TRANSCRIPT_SCROLL_CONTROLLER_ENABLED` lives inside -// each utility's body so component files import them without -// branching on the flag themselves. - -import { useCallback, useRef, type MutableRefObject } from "react"; - -import { TRANSCRIPT_SCROLL_CONTROLLER_ENABLED } from "@/domains/chat/transcript/transcript-scroll-flag"; - -/** - * Wire the transcript scroll container + content callback refs. - * - * Behavior when the controller flag is ON: - * 1. On scroll container DOM attach (which fires on conversation - * switch via `key={conversationId}` in `transcript.tsx`, and on - * fresh detail-page loads), the container is snapped to bottom. - * 2. A `ResizeObserver` watches the content wrapper. Until the user - * interacts, every content height change re-snaps to bottom. - * This covers the seed-then-grow race where `useViewportMinHeight` - * seeds `LatestTurnRow`'s `minHeight` in a post-paint effect, - * growing `scrollHeight` after the initial attach snap. - * 3. The first `wheel`/`touchmove`/`keydown` on the container - * disengages the observer — the user is now in control. - * - * When the controller flag is OFF, both callbacks forward to the - * passed-in refs without observing or scrolling — the deprecated hook - * still owns scroll coordination in that path. - * - * Both callbacks are returned as memoized identities so React doesn't - * tear them down between renders. The internal state (observer + - * gesture listeners) is keyed off element identity, not callback - * identity, so the wiring survives parent re-renders and tears down - * exactly when the underlying element changes. - */ -export function useTranscriptScrollOnAttach(args: { - scrollContainerRef: MutableRefObject; - contentRef: MutableRefObject; -}): { - scrollContainerCallbackRef: (el: HTMLDivElement | null) => void; - contentCallbackRef: (el: HTMLDivElement | null) => void; -} { - // Refs for tearing down the previous attach when the element - // changes (e.g. conversation switch with `key={conversationId}`). - const teardownRef = useRef<(() => void) | null>(null); - - const teardown = useCallback(() => { - teardownRef.current?.(); - teardownRef.current = null; - }, []); - - const scrollContainerCallbackRef = useCallback( - (el: HTMLDivElement | null) => { - args.scrollContainerRef.current = el; - // No work here — all setup waits for the content ref so we - // can attach the ResizeObserver to the inner wrapper. The - // content ref is guaranteed to fire on the same commit since - // it's a child of this element. - }, - [args.scrollContainerRef], - ); - - const contentCallbackRef = useCallback( - (el: HTMLDivElement | null) => { - args.contentRef.current = el; - if (!TRANSCRIPT_SCROLL_CONTROLLER_ENABLED) return; - - // Tear down whatever was wired to the previous content/container. - teardown(); - - if (!el) return; - const container = args.scrollContainerRef.current; - if (!container) return; - - teardownRef.current = attachSnapToLatest({ container, content: el }); - }, - [args.scrollContainerRef, args.contentRef, teardown], - ); - - return { scrollContainerCallbackRef, contentCallbackRef }; -} - -/** - * Snap the scroll container to bottom and keep it there through async - * layout settling. Returns a teardown function that disconnects the - * `ResizeObserver` and removes the gesture listeners. - * - * Pure imperative function — takes plain DOM elements, no React. The - * React wiring lives in `useTranscriptScrollOnAttach`; this function - * is independently testable with a fake `HTMLElement`. - */ -export function attachSnapToLatest(args: { - container: HTMLElement; - content: HTMLElement; -}): () => void { - const { container, content } = args; - - // Initial attach snap — synchronous, during commit, before paint. - container.scrollTop = container.scrollHeight; - - // Browsers without `ResizeObserver` get the initial snap only. - // Modern browsers (and the iOS WKWebView) all have it; this guard - // is for SSR / test environments. - if (typeof ResizeObserver === "undefined") { - return () => {}; - } - - let active = true; - - const stop = (): void => { - if (!active) return; - active = false; - observer.disconnect(); - container.removeEventListener("wheel", stop); - container.removeEventListener("touchmove", stop); - container.removeEventListener("keydown", stop); - }; - - const observer = new ResizeObserver(() => { - if (!active) return; - container.scrollTop = container.scrollHeight; - }); - observer.observe(content); - - // User-gesture disengage. The user touching the scroll container in - // any way means they're driving — stop fighting them. - container.addEventListener("wheel", stop, { passive: true }); - container.addEventListener("touchmove", stop, { passive: true }); - container.addEventListener("keydown", stop); - - return stop; -} diff --git a/apps/web/src/domains/chat/transcript/transcript.tsx b/apps/web/src/domains/chat/transcript/transcript.tsx index 1950ec48db7..1055ac2e21a 100644 --- a/apps/web/src/domains/chat/transcript/transcript.tsx +++ b/apps/web/src/domains/chat/transcript/transcript.tsx @@ -16,7 +16,6 @@ import type { TranscriptItem } from "@/domains/chat/transcript/types"; import { LatestTurnRow } from "@/domains/chat/transcript/latest-turn-row"; import { PullRefreshSpinner } from "@/domains/chat/transcript/pull-refresh-spinner"; import { TranscriptRow } from "@/domains/chat/transcript/transcript-row"; -import { useTranscriptScrollOnAttach } from "@/domains/chat/transcript/transcript-scroll"; import { PULL_THRESHOLD_PX, usePullToRefresh, @@ -27,7 +26,7 @@ import type { ConfirmationDecision } from "@/domains/chat/api/event-types"; /** Distance from the bottom (in px) at or below which the transcript is * considered pinned to the latest message. Surfaced through * `TranscriptHandle.getScrollState()` for the debug API. Kept in sync - * with the same threshold inside `useDeprecatedTranscriptScroll`. */ + * with the same threshold inside `useTranscriptScroll`. */ const PINNED_THRESHOLD_PX = 64; /** Outcome of a pull-to-refresh, returned by the consumer's @@ -156,11 +155,6 @@ export const Transcript = forwardRef( props; const scrollRef = useRef(null); const contentRef = useRef(null); - const { scrollContainerCallbackRef, contentCallbackRef } = - useTranscriptScrollOnAttach({ - scrollContainerRef: scrollRef, - contentRef, - }); const viewportMinHeight = useViewportMinHeight(scrollRef); const pullEnabled = !!pullRefreshEnabled && !!onPullRefresh; @@ -258,7 +252,7 @@ export const Transcript = forwardRef( return (
@@ -267,7 +261,7 @@ export const Transcript = forwardRef( * height changes (async min-height settle, late image loads, * streaming growth). Wrapping all rows in a single observed * element is cheaper than observing each row individually. */} -
+
{/* History items in chronological order — oldest at top. */} {partition.historyItems.map((item) => ( diff --git a/apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.test.ts b/apps/web/src/domains/chat/transcript/use-transcript-scroll.test.ts similarity index 99% rename from apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.test.ts rename to apps/web/src/domains/chat/transcript/use-transcript-scroll.test.ts index 24cca828b83..1f8195d2b02 100644 --- a/apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.test.ts +++ b/apps/web/src/domains/chat/transcript/use-transcript-scroll.test.ts @@ -27,7 +27,7 @@ import { PINNED_THRESHOLD_PX, SHOW_SCROLL_BUTTON_THRESHOLD_PX, type TranscriptHandle, -} from "@/domains/chat/transcript/use-deprecated-transcript-scroll"; +} from "@/domains/chat/transcript/use-transcript-scroll"; // --------------------------------------------------------------------------- // Fixtures diff --git a/apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.ts b/apps/web/src/domains/chat/transcript/use-transcript-scroll.ts similarity index 95% rename from apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.ts rename to apps/web/src/domains/chat/transcript/use-transcript-scroll.ts index c0df817879e..134b041060a 100644 --- a/apps/web/src/domains/chat/transcript/use-deprecated-transcript-scroll.ts +++ b/apps/web/src/domains/chat/transcript/use-transcript-scroll.ts @@ -34,7 +34,6 @@ import { import type { TranscriptItem } from "@/domains/chat/transcript/types"; import type { TranscriptHandle } from "@/domains/chat/transcript/transcript"; -import { TRANSCRIPT_SCROLL_CONTROLLER_ENABLED } from "@/domains/chat/transcript/transcript-scroll-flag"; export type { TranscriptHandle }; @@ -60,7 +59,7 @@ export const LOAD_OLDER_THRESHOLD_PX = 200; // Public hook API // --------------------------------------------------------------------------- -export interface UseDeprecatedTranscriptScrollArgs { +export interface UseTranscriptScrollArgs { transcriptRef: RefObject; items: TranscriptItem[]; conversationId: string | null; @@ -69,7 +68,7 @@ export interface UseDeprecatedTranscriptScrollArgs { onLoadOlder: () => void; } -export interface UseDeprecatedTranscriptScrollReturn { +export interface UseTranscriptScrollReturn { showScrollToLatest: boolean; scrollToLatest: (opts?: { behavior?: "auto" | "smooth" }) => void; } @@ -225,29 +224,9 @@ export function decideItemsChangeAction( // Hook implementation // --------------------------------------------------------------------------- -/** Returned when the dev flag has turned this hook off. The transcript - * then runs with no JavaScript scroll coordination at all — the - * defaults below match "nothing is happening". */ -const DISABLED_RESULT: UseDeprecatedTranscriptScrollReturn = { - showScrollToLatest: false, - scrollToLatest: () => {}, -}; - -export function useDeprecatedTranscriptScroll( - args: UseDeprecatedTranscriptScrollArgs, -): UseDeprecatedTranscriptScrollReturn { - // `TRANSCRIPT_SCROLL_CONTROLLER_ENABLED` is a module-load constant - // resolved once from localStorage at page load. It does NOT change - // across renders within a page lifetime (toggling the flag reloads - // the page). That means this early return is taken consistently for - // every render of every instance of this hook on a given page — - // either the no-op path runs forever or the full hook runs forever - // — which keeps React's hook-order rules satisfied even though no - // hooks are called on the no-op path. - if (TRANSCRIPT_SCROLL_CONTROLLER_ENABLED) { - return DISABLED_RESULT; - } - +export function useTranscriptScroll( + args: UseTranscriptScrollArgs, +): UseTranscriptScrollReturn { const { transcriptRef, items, diff --git a/apps/web/src/domains/chat/utils/debug-api.test.ts b/apps/web/src/domains/chat/utils/debug-api.test.ts index cc8ba51f059..73a5e5ea127 100644 --- a/apps/web/src/domains/chat/utils/debug-api.test.ts +++ b/apps/web/src/domains/chat/utils/debug-api.test.ts @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test"; import type { MutableRefObject } from "react"; import type { ChatEventStream } from "@/domains/chat/api/stream"; -import type { TranscriptHandle } from "@/domains/chat/transcript/use-deprecated-transcript-scroll"; +import type { TranscriptHandle } from "@/domains/chat/transcript/use-transcript-scroll"; import type { TranscriptItem } from "@/domains/chat/transcript/types"; import type { DisplayMessage } from "@/domains/chat/utils/reconcile"; import type { RuntimeMessage } from "@/domains/chat/api/messages"; diff --git a/apps/web/src/domains/chat/utils/debug-api.ts b/apps/web/src/domains/chat/utils/debug-api.ts index 22cb6d6e3ea..46b98b45c26 100644 --- a/apps/web/src/domains/chat/utils/debug-api.ts +++ b/apps/web/src/domains/chat/utils/debug-api.ts @@ -43,12 +43,11 @@ import type { import { recordChatDiagnostic } from "@/domains/chat/utils/diagnostics"; import type { DisplayMessage } from "@/domains/chat/utils/reconcile"; import type { ReconcileActiveConversationResult } from "@/domains/chat/hooks/use-message-reconciliation"; -import { setTranscriptScrollControllerEnabled } from "@/domains/chat/transcript/transcript-scroll-flag"; import { setImpersonatedAssistantVersion } from "@/lib/backwards-compat/impersonate-version-flag"; import { classifyScrollPosition, type TranscriptHandle, -} from "@/domains/chat/transcript/use-deprecated-transcript-scroll"; +} from "@/domains/chat/transcript/use-transcript-scroll"; import type { TranscriptItem } from "@/domains/chat/transcript/types"; import { type TerminalReason, @@ -696,15 +695,10 @@ const API_NS = "api"; /** * Dev-only toggle surface. Each function is a single-purpose imperative * flip — call from the console to flip a localStorage-persisted flag. - * Toggles that change React hook ordering (e.g. swapping which scroll - * coordinator runs) reload the page so the new value takes effect - * cleanly. See `transcript-scroll-flag.ts` for the storage layer. + * Toggles that change React hook ordering or module-load constants + * reload the page so the new value takes effect cleanly. */ export interface VellumDebugFlagsApi { - /** Flip the parallel `useTranscriptScrollController` path on or off. - * Persists to localStorage and reloads the page. Pass `true`/`false` - * to force a specific value; omit to flip the current value. */ - toggleTranscriptScrollController(value?: boolean): boolean; /** Override the assistant's reported version for every version-gated * code path in the web client (the wire-field cutover, the * server-mint gate, `useAssistantSupports`, …). Persists to @@ -832,7 +826,6 @@ export function useChatDebugApi(refs: ChatDebugRefs): void { }; const api = createChatDebugApi(stableRefs); const flagsApi: VellumDebugFlagsApi = { - toggleTranscriptScrollController: setTranscriptScrollControllerEnabled, impersonateVersion: setImpersonatedAssistantVersion, }; const uninstall = installVellumDebugApi(api, flagsApi); diff --git a/apps/web/src/lib/backwards-compat/impersonate-version-flag.ts b/apps/web/src/lib/backwards-compat/impersonate-version-flag.ts index b6f3e0b759f..69a6722f059 100644 --- a/apps/web/src/lib/backwards-compat/impersonate-version-flag.ts +++ b/apps/web/src/lib/backwards-compat/impersonate-version-flag.ts @@ -14,7 +14,7 @@ // through `setIdentity`, so the override is uniformly applied without // any consumer needing to know about it. // -// Reload-on-change matches the DX of `transcript-scroll-flag.ts`: +// Reload-on-change rationale: // • some consumers cache version-derived constants at module load // (e.g. anything that wants a stable identity across re-renders); // • SSE handlers re-read from the store on every event but a stale