diff --git a/packages/app/src/components/composer.test.tsx b/packages/app/src/components/composer.test.tsx index 423cdc160..71192c103 100644 --- a/packages/app/src/components/composer.test.tsx +++ b/packages/app/src/components/composer.test.tsx @@ -22,7 +22,6 @@ const { deleteAttachmentsMock, encodeImagesMock, openExternalUrlMock, - markScrollInvestigationRenderMock, mockSessionState, setAgentStreamTailMock, setAgentStreamHeadMock, @@ -161,7 +160,6 @@ const { ); hoistedMockSessionState.setAgentStreamTail = hoistedSetAgentStreamTailMock; hoistedMockSessionState.setAgentStreamHead = hoistedSetAgentStreamHeadMock; - const hoistedMarkScrollInvestigationRenderMock = vi.fn(); const hoistedAgentDirectoryStatusMock = vi.fn(() => "ready"); return { @@ -175,7 +173,6 @@ const { deleteAttachmentsMock: vi.fn(async () => {}), encodeImagesMock: vi.fn(async (images: AttachmentMetadata[]) => images), openExternalUrlMock: vi.fn(async () => {}), - markScrollInvestigationRenderMock: hoistedMarkScrollInvestigationRenderMock, mockSessionState: hoistedMockSessionState, setAgentStreamTailMock: hoistedSetAgentStreamTailMock, setAgentStreamHeadMock: hoistedSetAgentStreamHeadMock, @@ -339,11 +336,6 @@ vi.mock("@/contexts/toast-context", () => ({ useToast: () => ({ error: vi.fn() }), })); -vi.mock("@/utils/scroll-jank", () => ({ - markScrollInvestigationRender: markScrollInvestigationRenderMock, - markScrollInvestigationEvent: vi.fn(), -})); - vi.mock("@/components/agent-status-bar", () => ({ AgentStatusBar: () => null, DraftAgentStatusBar: () => null, @@ -592,7 +584,6 @@ beforeEach(() => { deleteAttachmentsMock.mockClear(); encodeImagesMock.mockClear(); openExternalUrlMock.mockClear(); - markScrollInvestigationRenderMock.mockClear(); setAgentStreamTailMock.mockClear(); setAgentStreamHeadMock.mockClear(); setQueuedMessagesMock.mockClear(); @@ -738,12 +729,6 @@ function dispatchAgentInterrupt() { }); } -function countMessageInputRenders(): number { - return markScrollInvestigationRenderMock.mock.calls.filter( - ([componentId]) => componentId === "MessageInput:server:agent", - ).length; -} - describe("Composer keyboard shortcuts", () => { it("interrupts a running agent without clearing a filled draft", async () => { mockSessionState.sessions.server.agents = new Map([ @@ -987,27 +972,6 @@ describe("Composer attachments", () => { expect(latestAttachments).toEqual([{ kind: "image", metadata: image }]); }); - it("does not re-render MessageInput when opening the attachment lightbox", () => { - const image = imageAttachment("img-lightbox-render"); - renderComposer({ initialAttachments: [{ kind: "image", metadata: image }] }); - const renderCountBeforeLightbox = countMessageInputRenders(); - - click(queryByTestId("composer-image-attachment-pill")!); - - expect(queryByTestId("attachment-lightbox-image")).not.toBeNull(); - expect(countMessageInputRenders()).toBe(renderCountBeforeLightbox); - }); - - it("still re-renders MessageInput when submit loading semantics change", () => { - renderComposer({ initialText: "pending submit", isSubmitLoading: false }); - const renderCountBeforeLoading = countMessageInputRenders(); - - renderComposer({ initialText: "pending submit", isSubmitLoading: true }); - - expect(countMessageInputRenders()).toBe(renderCountBeforeLoading + 1); - expect(document.querySelector('[aria-label="Send message"]')).toHaveProperty("disabled", true); - }); - it("enables dictation from server capabilities before the agent directory finishes loading", () => { agentDirectoryStatusMock.mockReturnValue("initial_loading"); diff --git a/packages/app/src/components/composer.tsx b/packages/app/src/components/composer.tsx index 99978fa0a..cd26fde4d 100644 --- a/packages/app/src/components/composer.tsx +++ b/packages/app/src/components/composer.tsx @@ -62,7 +62,6 @@ import { persistAttachmentFromFileUri, } from "@/attachments/service"; import { resolveStatusControlMode } from "@/components/composer.status-controls"; -import { markScrollInvestigationRender } from "@/utils/scroll-jank"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; @@ -972,7 +971,6 @@ export function Composer({ statusControls, inputWrapperStyle, }: ComposerProps) { - markScrollInvestigationRender(`Composer:${serverId}:${agentId}`); const buttonIconSize = resolveComposerButtonIconSize(); const client = useHostRuntimeClient(serverId); const isConnected = useHostRuntimeIsConnected(serverId); diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 5774019e1..618cf7424 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -51,7 +51,6 @@ import { formatShortcut } from "@/utils/format-shortcut"; import { getShortcutOs } from "@/utils/shortcut-platform"; import type { MessageInputKeyboardActionKind } from "@/keyboard/actions"; import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime"; -import { markScrollInvestigationEvent, markScrollInvestigationRender } from "@/utils/scroll-jank"; import { isWeb } from "@/constants/platform"; import { useComposerHeightMirror } from "./composer-height-mirror"; @@ -167,30 +166,6 @@ interface TextAreaHandle { } & Record; } -function logWebStickyBottom(_event: string, _details: Record): void { - // Intentionally disabled: this path is too noisy during voice debugging. -} - -function getDebugNow(): number | null { - if (typeof performance !== "undefined" && typeof performance.now === "function") { - return Number(performance.now().toFixed(3)); - } - return null; -} - -function getElementDescriptor(element: HTMLElement | null): string | null { - if (!element) return null; - const tag = element.tagName?.toLowerCase() ?? "unknown"; - const id = element.id ? `#${element.id}` : ""; - const testId = element.getAttribute?.("data-testid"); - const label = element.getAttribute?.("aria-label"); - let suffix: string; - if (testId) suffix = `[data-testid="${testId}"]`; - else if (label) suffix = `[aria-label="${label}"]`; - else suffix = ""; - return `${tag}${id}${suffix}`; -} - function AttachButtonIcon({ hovered, onAttachButtonRef, @@ -385,7 +360,6 @@ function resolveSendTooltipLabel(input: { } interface DesktopKeyPressContext { - investigationComponentId: string; onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined; isAgentRunning: boolean; onQueue: ((payload: MessagePayload) => void) | undefined; @@ -400,8 +374,6 @@ function handleDesktopKeyPressImpl( event: WebTextInputKeyPressEvent, ctx: DesktopKeyPressContext, ): void { - markScrollInvestigationEvent(ctx.investigationComponentId, "keyPress"); - if (isImeComposingKeyboardEvent(event.nativeEvent)) return; if (ctx.onKeyPressCallback) { @@ -568,107 +540,6 @@ function usePasteImagesEffect(args: PasteImagesEffectArgs): void { ]); } -interface ResizeObserverEffectArgs { - getWebTextArea: () => TextAreaHandle | null; - getWebElement: (target: "root" | "wrapper") => HTMLElement | null; - valueRef: React.MutableRefObject; -} - -function useComposerResizeObserverEffect(args: ResizeObserverEffectArgs): void { - const { getWebTextArea, getWebElement, valueRef } = args; - - useEffect(() => { - if (!isWeb || typeof ResizeObserver === "undefined") return; - - const textarea = getWebTextArea(); - const root = getWebElement("root"); - const wrapper = getWebElement("wrapper"); - const observed = [ - { name: "composer_root", element: root }, - { name: "composer_wrapper", element: wrapper }, - { name: "composer_textarea", element: textarea as unknown as HTMLElement | null }, - ].filter( - (entry): entry is { name: string; element: HTMLElement } => - entry.element instanceof HTMLElement, - ); - - if (observed.length === 0) return; - - const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - const target = entry.target as HTMLElement; - const match = observed.find((item) => item.element === target); - if (!match) continue; - const textareaNode = getWebTextArea(); - logWebStickyBottom("composer_element_resized", { - target: match.name, - width: target.clientWidth, - height: target.clientHeight, - offsetHeight: target.offsetHeight, - scrollHeight: target.scrollHeight, - textareaClientHeight: textareaNode?.clientHeight ?? null, - textareaOffsetHeight: textareaNode?.offsetHeight ?? null, - textareaScrollHeight: textareaNode?.scrollHeight ?? null, - textareaScrollTop: - (textareaNode as unknown as HTMLTextAreaElement | null)?.scrollTop ?? null, - valueLength: valueRef.current.length, - }); - } - }); - - for (const entry of observed) { - observer.observe(entry.element); - } - - return () => { - observer.disconnect(); - }; - }, [getWebElement, getWebTextArea, valueRef]); -} - -interface ScrollLogEffectArgs { - getWebTextArea: () => TextAreaHandle | null; - valueRef: React.MutableRefObject; -} - -function useComposerScrollLogEffect(args: ScrollLogEffectArgs): void { - const { getWebTextArea, valueRef } = args; - - useEffect(() => { - if (!isWeb) return; - const textarea = getWebTextArea() as (HTMLTextAreaElement & TextAreaHandle) | null; - if (!textarea || typeof textarea.addEventListener !== "function") return; - - const handleScroll = () => { - const textareaElement = textarea as unknown as HTMLElement; - const chatScroller = - typeof document !== "undefined" - ? (document.querySelector('[data-testid="agent-chat-scroll"]') as HTMLElement | null) - : null; - logWebStickyBottom("composer_textarea_scrolled", { - now: getDebugNow(), - scrollTop: textarea.scrollTop, - clientHeight: textarea.clientHeight ?? null, - scrollHeight: textarea.scrollHeight ?? null, - selectionStart: textarea.selectionStart ?? null, - selectionEnd: textarea.selectionEnd ?? null, - textareaDescriptor: getElementDescriptor(textareaElement), - chatScrollerDescriptor: getElementDescriptor(chatScroller), - chatScrollerContainsTextarea: Boolean( - chatScroller && textareaElement && chatScroller.contains(textareaElement), - ), - textareaScrollableAncestors: getScrollableAncestorChain(textareaElement), - valueLength: valueRef.current.length, - }); - }; - - textarea.addEventListener("scroll", handleScroll, { passive: true }); - return () => { - textarea.removeEventListener("scroll", handleScroll); - }; - }, [getWebTextArea, valueRef]); -} - function useAutoFocusOnWebEffect( textInputRef: React.MutableRefObject< TextInput | (TextInput & { getNativeRef?: () => unknown }) | null @@ -1056,13 +927,6 @@ function queueMessageImpl(ctx: QueueMessageContext): void { ctx.onMinimizeHeight(); } -function computeInvestigationComponentId( - voiceServerId: string | undefined, - voiceAgentId: string | undefined, -): string { - return `MessageInput:${voiceServerId ?? "unknown-server"}:${voiceAgentId ?? "unknown-agent"}`; -} - function computeIsRealtimeVoiceForAgent( voice: { isVoiceModeForAgent: (serverId: string, agentId: string) => boolean } | null | undefined, voiceServerId: string | undefined, @@ -1156,16 +1020,6 @@ function getWebTextAreaImpl( return null; } -function toHtmlElement(current: unknown): HTMLElement | null { - if (!current) return null; - if (current instanceof HTMLElement) return current; - const maybe = current as { getBoundingClientRect?: () => DOMRect }; - if (maybe.getBoundingClientRect) { - return current as HTMLElement; - } - return null; -} - interface SendButtonStateInput { disabled: boolean; isSubmitDisabled: boolean; @@ -1300,26 +1154,6 @@ function extractErrorMessage(error: unknown): string | null { return null; } -function getScrollableAncestorChain(element: HTMLElement | null): string[] { - if (!element || typeof window === "undefined") { - return []; - } - const results: string[] = []; - let current = element.parentElement; - while (current) { - const style = window.getComputedStyle(current); - const overflowY = style.overflowY; - const canScroll = - (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && - current.scrollHeight > current.clientHeight; - if (canScroll) { - results.push(getElementDescriptor(current) ?? current.tagName.toLowerCase()); - } - current = current.parentElement; - } - return results; -} - export const MessageInput = forwardRef( function MessageInput(props, ref) { const { @@ -1360,8 +1194,6 @@ export const MessageInput = forwardRef( inputWrapperStyle, } = resolveMessageInputProps(props); const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg; - const investigationComponentId = computeInvestigationComponentId(voiceServerId, voiceAgentId); - markScrollInvestigationRender(investigationComponentId); const toast = useToast(); const voice = useVoiceOptional(); const sendKeys = useShortcutKeys("message-input-send"); @@ -1697,11 +1529,6 @@ export const MessageInput = forwardRef( enabled: isWeb && inputHeight >= MAX_INPUT_HEIGHT, }); - const getWebElement = useCallback((target: "root" | "wrapper"): HTMLElement | null => { - const current = target === "root" ? rootRef.current : inputWrapperRef.current; - return toHtmlElement(current); - }, []); - usePasteImagesEffect({ getWebTextArea, isConnected, @@ -1711,10 +1538,6 @@ export const MessageInput = forwardRef( onAddImages, }); - useComposerResizeObserverEffect({ getWebTextArea, getWebElement, valueRef }); - - useComposerScrollLogEffect({ getWebTextArea, valueRef }); - const setBoundedInputHeight = useCallback( (nextHeight: number) => { const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight)); @@ -1746,20 +1569,9 @@ export const MessageInput = forwardRef( (event: NativeSyntheticEvent) => { const start = event.nativeEvent.selection?.start ?? 0; const end = event.nativeEvent.selection?.end ?? start; - if (isWeb) { - const textarea = getWebTextArea(); - logWebStickyBottom("composer_selection_changed", { - now: getDebugNow(), - start, - end, - textareaScrollTop: textarea?.scrollTop ?? null, - textareaClientHeight: textarea?.clientHeight ?? null, - textareaScrollHeight: textarea?.scrollHeight ?? null, - }); - } onSelectionChangeCallback?.({ start, end }); }, - [getWebTextArea, onSelectionChangeCallback], + [onSelectionChangeCallback], ); const shouldHandleDesktopSubmit = isWeb; @@ -1767,7 +1579,6 @@ export const MessageInput = forwardRef( function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) { if (!shouldHandleDesktopSubmit) return; handleDesktopKeyPressImpl(event, { - investigationComponentId, onKeyPressCallback, isAgentRunning, onQueue, @@ -1815,16 +1626,9 @@ export const MessageInput = forwardRef( const handleInputChange = useCallback( (nextValue: string) => { - markScrollInvestigationEvent(investigationComponentId, "inputChange"); onChangeText(nextValue); - if (isWeb) { - logWebStickyBottom("composer_text_changed", { - valueLength: nextValue.length, - lineCount: nextValue.split("\n").length, - }); - } }, - [investigationComponentId, onChangeText], + [onChangeText], ); const handleInputFocus = useCallback(() => { diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index cc5a18bf5..fe08422c7 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -79,7 +79,6 @@ import { } from "@/utils/inline-path"; import { getMarkdownListMarker } from "@/utils/markdown-list"; import { openExternalUrl } from "@/utils/open-external-url"; -import { markScrollInvestigationEvent } from "@/utils/scroll-jank"; import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; import { getAssistantImageMetadata, @@ -103,29 +102,6 @@ export type { InlinePathTarget } from "@/utils/inline-path"; type MarkdownStyles = Record; -function useMessageRenderTracker( - label: string, - identity: string, - watched: Record, -): void { - const renderCountRef = useRef(0); - const prevRef = useRef | null>(null); - renderCountRef.current += 1; - const changed: string[] = []; - if (prevRef.current !== null) { - for (const key of Object.keys(watched)) { - if (!Object.is(prevRef.current[key], watched[key])) { - changed.push(key); - } - } - } - prevRef.current = { ...watched }; - console.log(`[${label}]`, identity, `render #${renderCountRef.current}`, { - isFirst: renderCountRef.current === 1, - changed, - }); -} - interface UserMessageProps { message: string; images?: UserMessageImageAttachment[]; @@ -425,13 +401,6 @@ export const UserMessage = memo(function UserMessage({ isLastInGroup = true, disableOuterSpacing, }: UserMessageProps) { - useMessageRenderTracker("UserMessage", `${(message ?? "").slice(0, 16)}`, { - message, - images, - isFirstInGroup, - isLastInGroup, - disableOuterSpacing, - }); const isCompact = useIsCompactFormFactor(); const [messageHovered, setMessageHovered] = useState(false); const [copyButtonHovered, setCopyButtonHovered] = useState(false); @@ -1383,19 +1352,6 @@ export const AssistantMessage = memo(function AssistantMessage({ disableOuterSpacing, spacing = "default", }: AssistantMessageProps) { - useMessageRenderTracker( - "AssistantMessage", - `${serverId ?? "?"}/${(message ?? "").slice(0, 16)}`, - { - message, - onInlinePathPress, - workspaceRoot, - serverId, - client, - disableOuterSpacing, - spacing, - }, - ); const resolvedDisableOuterSpacing = useDisableOuterSpacing( disableOuterSpacing ?? spacing !== "default", ); @@ -2373,9 +2329,8 @@ function computeShimmerMetrics(input: { function useDetailWheelPropagationBlocker(input: { detailWrapperRef: React.RefObject; enabled: boolean; - wheelInvestigationComponentId: string; }): void { - const { detailWrapperRef, enabled, wheelInvestigationComponentId } = input; + const { detailWrapperRef, enabled } = input; useEffect(() => { if (!enabled) { return; @@ -2389,13 +2344,11 @@ function useDetailWheelPropagationBlocker(input: { event.stopPropagation(); } }; - markScrollInvestigationEvent(wheelInvestigationComponentId, "wheelAttach"); node.addEventListener("wheel", stopWheelPropagation, { passive: true }); return () => { - markScrollInvestigationEvent(wheelInvestigationComponentId, "wheelDetach"); node.removeEventListener("wheel", stopWheelPropagation); }; - }, [detailWrapperRef, enabled, wheelInvestigationComponentId]); + }, [detailWrapperRef, enabled]); } const SHIMMER_GRADIENT = @@ -2450,7 +2403,6 @@ const ExpandableBadge = memo(function ExpandableBadge({ const hasDetailContent = Boolean(renderDetails); const detailContent = hasDetailContent && isExpanded ? renderDetails?.() : null; const detailWrapperRef = useRef(null); - const wheelInvestigationComponentId = `ExpandableBadgeWheel:${testID ?? label}`; const handleHoverIn = useCallback(() => setIsHovered(true), []); const handleHoverOut = useCallback(() => { @@ -2554,7 +2506,6 @@ const ExpandableBadge = memo(function ExpandableBadge({ useDetailWheelPropagationBlocker({ detailWrapperRef, enabled: !isNative && isExpanded && hasDetailContent, - wheelInvestigationComponentId, }); const shimmerLabelStyle = useMemo( @@ -2797,21 +2748,6 @@ export const ToolCall = memo(function ToolCall({ onInlineDetailsExpandedChange, onOpenFilePath, }: ToolCallProps) { - useMessageRenderTracker("ToolCall", `${toolName}/${status}`, { - toolName, - args, - result, - error, - status, - detail, - cwd, - metadata, - isLastInSequence, - disableOuterSpacing, - onInlineDetailsHoverChange, - onInlineDetailsExpandedChange, - onOpenFilePath, - }); const { openToolCall } = useToolCallSheet(); const [isExpanded, setIsExpanded] = useState(false); diff --git a/packages/app/src/components/stream-strategy-web.tsx b/packages/app/src/components/stream-strategy-web.tsx index 36a91ddec..2b5fd0d91 100644 --- a/packages/app/src/components/stream-strategy-web.tsx +++ b/packages/app/src/components/stream-strategy-web.tsx @@ -25,17 +25,6 @@ const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64; const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1; import { useWebElementScrollbar } from "./use-web-scrollbar"; -function logWebStickyBottom(_event: string, _details: Record): void { - // Intentionally disabled: this path is too noisy during voice debugging. -} - -function getDebugNow(): number | null { - if (typeof performance !== "undefined" && typeof performance.now === "function") { - return Number(performance.now().toFixed(3)); - } - return null; -} - function isScrollContainerNearBottom( scrollContainer: Pick, thresholdPx = AUTO_SCROLL_BOTTOM_THRESHOLD_PX, @@ -92,36 +81,6 @@ function isScrollContainerOverscrolledPastBottom( return getScrollContainerDistanceFromBottom(scrollContainer) < 0; } -const VIEWPORT_LOG_PREFIX = "[WebStreamViewport]"; - -function vlog(agentId: string, event: string, details: Record = {}): void { - console.log(VIEWPORT_LOG_PREFIX, agentId, event, details); -} - -function useRenderTracker( - label: string, - agentId: string, - watched: Record, -): number { - const renderCountRef = useRef(0); - const prevRef = useRef | null>(null); - renderCountRef.current += 1; - const changed: string[] = []; - if (prevRef.current !== null) { - for (const key of Object.keys(watched)) { - if (!Object.is(prevRef.current[key], watched[key])) { - changed.push(key); - } - } - } - prevRef.current = { ...watched }; - console.log(VIEWPORT_LOG_PREFIX, agentId, `${label} render #${renderCountRef.current}`, { - isFirst: renderCountRef.current === 1, - changed, - }); - return renderCountRef.current; -} - function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: boolean }) { const { segments, @@ -135,26 +94,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool scrollEnabled, isMobileBreakpoint, } = props; - useRenderTracker("WebStreamViewport", props.agentId, { - agentId: props.agentId, - segments, - "segments.historyMounted": segments.historyMounted, - "segments.historyVirtualized": segments.historyVirtualized, - "segments.liveHead": segments.liveHead, - boundary, - renderers, - "renderers.renderHistoryMountedRow": renderers.renderHistoryMountedRow, - "renderers.renderHistoryVirtualizedRow": renderers.renderHistoryVirtualizedRow, - "renderers.renderLiveHeadRow": renderers.renderLiveHeadRow, - "renderers.renderLiveAuxiliary": renderers.renderLiveAuxiliary, - listEmptyComponent, - viewportRef, - routeBottomAnchorRequest, - isAuthoritativeHistoryReady, - onNearBottomChange, - scrollEnabled, - isMobileBreakpoint, - }); const scrollContainerRef = useRef(null); const contentRef = useRef(null); const handleScrollContainerRef = useCallback((node: HTMLElement | null) => { @@ -170,13 +109,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool }; const followOutputRef = useRef(followOutput); const lastKnownScrollTopRef = useRef(0); - const lastLoggedMetricsRef = useRef<{ - scrollTop: number; - clientWidth: number; - clientHeight: number; - scrollWidth: number; - scrollHeight: number; - } | null>(null); const pendingUserScrollUpIntentRef = useRef(false); const isPointerScrollActiveRef = useRef(false); const lastTouchClientYRef = useRef(null); @@ -214,28 +146,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool overscan: 8, }); useEffect(() => { - vlog(props.agentId, "effect:virtualizer-adjust setup fired", {}); rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => { const viewportHeight = instance.scrollRect?.height ?? 0; const scrollOffset = instance.scrollOffset ?? 0; const remainingDistance = instance.getTotalSize() - (scrollOffset + viewportHeight); - logWebStickyBottom("virtualizer_item_size_change", { - agentId: props.agentId, - delta: _delta, - itemIndex: _item.index, - itemStart: _item.start, - itemSize: _item.size, - viewportHeight, - scrollOffset, - totalSize: instance.getTotalSize(), - remainingDistance, - }); return remainingDistance > AUTO_SCROLL_BOTTOM_THRESHOLD_PX; }; return () => { rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined; }; - }, [rowVirtualizer, props.agentId]); + }, [rowVirtualizer]); const virtualRows = rowVirtualizer.getVirtualItems(); const virtualTotalSize = rowVirtualizer.getTotalSize(); @@ -293,21 +213,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool if (isScrollContainerOverscrolledPastBottom(scrollContainer)) { return; } - logWebStickyBottom("viewport_scroll_to_bottom", { - agentId: props.agentId, - behavior, - followOutput: followOutputRef.current, - scrollTop: scrollContainer.scrollTop, - clientWidth: scrollContainer.clientWidth, - clientHeight: scrollContainer.clientHeight, - scrollWidth: scrollContainer.scrollWidth, - scrollHeight: scrollContainer.scrollHeight, - }); scrollElementToBottom(scrollContainer, behavior); lastKnownScrollTopRef.current = scrollContainer.scrollTop; syncNearBottom(scrollContainer, onNearBottomChange); }, - [onNearBottomChange, props.agentId], + [onNearBottomChange], ); const scheduleStickToBottom = useCallback(() => { @@ -318,15 +228,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool if (pendingAutoScrollFrameRef.current !== null) { return; } - logWebStickyBottom("viewport_schedule_stick_to_bottom", { - agentId: props.agentId, - followOutput: followOutputRef.current, - scrollTop: scrollContainer?.scrollTop ?? null, - clientWidth: scrollContainer?.clientWidth ?? null, - clientHeight: scrollContainer?.clientHeight ?? null, - scrollWidth: scrollContainer?.scrollWidth ?? null, - scrollHeight: scrollContainer?.scrollHeight ?? null, - }); pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => { pendingAutoScrollFrameRef.current = null; if (!followOutputRef.current) { @@ -334,7 +235,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool } scrollMessagesToBottom("auto"); }); - }, [props.agentId, scrollMessagesToBottom]); + }, [scrollMessagesToBottom]); const forceStickToBottom = useCallback(() => { cancelPendingStickToBottom(); @@ -349,31 +250,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool return; } syncNearBottom(scrollContainer, onNearBottomChange); - const currentMetrics = { - scrollTop: scrollContainer.scrollTop, - clientWidth: scrollContainer.clientWidth, - clientHeight: scrollContainer.clientHeight, - scrollWidth: scrollContainer.scrollWidth, - scrollHeight: scrollContainer.scrollHeight, - }; - const previousMetrics = lastLoggedMetricsRef.current; - const shouldLog = - !previousMetrics || - previousMetrics.scrollTop !== currentMetrics.scrollTop || - previousMetrics.clientWidth !== currentMetrics.clientWidth || - previousMetrics.clientHeight !== currentMetrics.clientHeight || - previousMetrics.scrollWidth !== currentMetrics.scrollWidth || - previousMetrics.scrollHeight !== currentMetrics.scrollHeight; - if (shouldLog) { - lastLoggedMetricsRef.current = currentMetrics; - logWebStickyBottom("viewport_metrics_updated", { - agentId: props.agentId, - followOutput: followOutputRef.current, - distanceFromBottom: getScrollContainerDistanceFromBottom(scrollContainer), - ...currentMetrics, - }); - } - }, [onNearBottomChange, props.agentId]); + }, [onNearBottomChange]); const handleDomScroll = useCallback(() => { const scrollContainer = scrollContainerRef.current; @@ -402,26 +279,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool } lastKnownScrollTopRef.current = currentScrollTop; - logWebStickyBottom("viewport_dom_scroll", { - agentId: props.agentId, - now: getDebugNow(), - scrollTop: currentScrollTop, - clientHeight: scrollContainer.clientHeight, - scrollHeight: scrollContainer.scrollHeight, - activeElementTag: - typeof document !== "undefined" - ? (document.activeElement?.tagName?.toLowerCase() ?? null) - : null, - activeElementRole: - typeof document !== "undefined" - ? (document.activeElement?.getAttribute?.("aria-label") ?? null) - : null, - }); updateScrollMetrics(); - }, [cancelPendingStickToBottom, updateScrollMetrics, props.agentId]); + }, [cancelPendingStickToBottom, updateScrollMetrics]); useLayoutEffect(() => { - vlog(props.agentId, "effect:activation-reset fired", { activationKey, isActivationReady }); if (!isActivationReady) { return; } @@ -443,12 +304,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool return () => { window.clearTimeout(timeout); }; - }, [activationKey, forceStickToBottom, isActivationReady, scheduleStickToBottom, props.agentId]); + }, [activationKey, forceStickToBottom, isActivationReady, scheduleStickToBottom]); useEffect(() => { - vlog(props.agentId, "effect:stick-on-segments-change fired", { - followOutput: followOutputRef.current, - }); if (!followOutputRef.current) { return; } @@ -458,28 +316,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool segments.historyMounted, segments.historyVirtualized, segments.liveHead, - props.agentId, ]); useEffect(() => { - vlog(props.agentId, "effect:stick-on-virtual-total fired", { - followOutput: followOutputRef.current, - shouldUseVirtualizer, - virtualTotalSize, - }); if (!followOutputRef.current || !shouldUseVirtualizer) { return; } scheduleStickToBottom(); - }, [scheduleStickToBottom, shouldUseVirtualizer, virtualTotalSize, props.agentId]); + }, [scheduleStickToBottom, shouldUseVirtualizer, virtualTotalSize]); useEffect(() => { - vlog(props.agentId, "effect:metrics-on-segments-change fired", { - mounted: segments.historyMounted.length, - virtualized: segments.historyVirtualized.length, - liveHead: segments.liveHead.length, - virtualTotalSize, - }); updateScrollMetrics(); }, [ segments.historyMounted.length, @@ -487,11 +333,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool segments.liveHead.length, updateScrollMetrics, virtualTotalSize, - props.agentId, ]); useEffect(() => { - vlog(props.agentId, "effect:resize-observer setup fired", {}); const scrollContainer = scrollContainerRef.current; const contentNode = contentRef.current; if (!scrollContainer || typeof ResizeObserver === "undefined") { @@ -500,15 +344,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool updateScrollMetrics(); const observer = new ResizeObserver(() => { - logWebStickyBottom("viewport_resize_observed", { - agentId: props.agentId, - followOutput: followOutputRef.current, - scrollTop: scrollContainer.scrollTop, - clientWidth: scrollContainer.clientWidth, - clientHeight: scrollContainer.clientHeight, - scrollWidth: scrollContainer.scrollWidth, - scrollHeight: scrollContainer.scrollHeight, - }); updateScrollMetrics(); if (!followOutputRef.current) { return; @@ -522,68 +357,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool return () => { observer.disconnect(); }; - }, [props.agentId, scheduleStickToBottom, updateScrollMetrics]); + }, [scheduleStickToBottom, updateScrollMetrics]); useEffect(() => { - vlog(props.agentId, "effect:scroll-listeners setup fired", {}); const scrollContainer = scrollContainerRef.current; if (!scrollContainer) { return; } - const originalScrollTo = scrollContainer.scrollTo.bind(scrollContainer); - const scrollTopDescriptor = - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(scrollContainer), "scrollTop") ?? - Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop"); - scrollContainer.scrollTo = ((...args: Parameters) => { - const firstArg = args[0] as ScrollToOptions | number | undefined; - const target = - typeof firstArg === "object" && firstArg !== null - ? { - top: firstArg.top ?? null, - left: firstArg.left ?? null, - behavior: firstArg.behavior ?? null, - } - : { - top: typeof args[1] === "number" ? args[1] : null, - left: typeof firstArg === "number" ? firstArg : null, - behavior: null, - }; - logWebStickyBottom("viewport_scroll_to_called", { - agentId: props.agentId, - now: getDebugNow(), - currentScrollTop: scrollContainer.scrollTop, - target, - stack: - typeof Error !== "undefined" - ? (new Error().stack?.split("\n").slice(1, 6).join("\n") ?? null) - : null, - }); - return originalScrollTo(...args); - }) as typeof scrollContainer.scrollTo; - if (scrollTopDescriptor?.get && scrollTopDescriptor?.set) { - Object.defineProperty(scrollContainer, "scrollTop", { - configurable: true, - enumerable: scrollTopDescriptor.enumerable ?? false, - get() { - return scrollTopDescriptor.get?.call(scrollContainer); - }, - set(value: number) { - logWebStickyBottom("viewport_scroll_top_set", { - agentId: props.agentId, - now: getDebugNow(), - currentScrollTop: scrollTopDescriptor.get?.call(scrollContainer) ?? null, - nextScrollTop: value, - stack: - typeof Error !== "undefined" - ? (new Error().stack?.split("\n").slice(1, 6).join("\n") ?? null) - : null, - }); - return scrollTopDescriptor.set?.call(scrollContainer, value); - }, - }); - } - const handleWheel = (event: WheelEvent) => { if (event.deltaY < 0) { pendingUserScrollUpIntentRef.current = true; @@ -618,27 +399,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool const handleTouchEnd = () => { lastTouchClientYRef.current = null; }; - const handleSelectionChange = () => { - const activeElement = - typeof document !== "undefined" - ? (document.activeElement as HTMLTextAreaElement | null) - : null; - logWebStickyBottom("document_selection_changed", { - agentId: props.agentId, - now: getDebugNow(), - activeElementTag: activeElement?.tagName?.toLowerCase() ?? null, - activeElementRole: activeElement?.getAttribute?.("aria-label") ?? null, - selectionStart: - activeElement && typeof activeElement.selectionStart === "number" - ? activeElement.selectionStart - : null, - selectionEnd: - activeElement && typeof activeElement.selectionEnd === "number" - ? activeElement.selectionEnd - : null, - scrollTop: scrollContainer.scrollTop, - }); - }; scrollContainer.addEventListener("scroll", handleDomScroll, { passive: true }); scrollContainer.addEventListener("wheel", handleWheel, { passive: true }); @@ -649,9 +409,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool scrollContainer.addEventListener("touchmove", handleTouchMove, { passive: true }); scrollContainer.addEventListener("touchend", handleTouchEnd, { passive: true }); scrollContainer.addEventListener("touchcancel", handleTouchEnd, { passive: true }); - if (typeof document !== "undefined") { - document.addEventListener("selectionchange", handleSelectionChange, { passive: true }); - } return () => { scrollContainer.removeEventListener("scroll", handleDomScroll); @@ -663,18 +420,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool scrollContainer.removeEventListener("touchmove", handleTouchMove); scrollContainer.removeEventListener("touchend", handleTouchEnd); scrollContainer.removeEventListener("touchcancel", handleTouchEnd); - scrollContainer.scrollTo = originalScrollTo; - if (scrollTopDescriptor) { - Reflect.deleteProperty(scrollContainer, "scrollTop"); - } - if (typeof document !== "undefined") { - document.removeEventListener("selectionchange", handleSelectionChange); - } }; - }, [cancelPendingStickToBottom, handleDomScroll, props.agentId]); + }, [cancelPendingStickToBottom, handleDomScroll]); useEffect(() => { - vlog(props.agentId, "effect:viewportRef-handle setup fired", {}); const handle: StreamViewportHandle = { scrollToBottom: () => { setFollowOutput(true); @@ -685,16 +434,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool if (!followOutputRef.current) { return; } - const scrollContainer = scrollContainerRef.current; - logWebStickyBottom("viewport_prepare_for_change", { - agentId: props.agentId, - followOutput: followOutputRef.current, - scrollTop: scrollContainer?.scrollTop ?? null, - clientWidth: scrollContainer?.clientWidth ?? null, - clientHeight: scrollContainer?.clientHeight ?? null, - scrollWidth: scrollContainer?.scrollWidth ?? null, - scrollHeight: scrollContainer?.scrollHeight ?? null, - }); scheduleStickToBottom(); }, }; @@ -705,16 +444,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool } cancelPendingStickToBottom(); }; - }, [ - cancelPendingStickToBottom, - forceStickToBottom, - props.agentId, - scheduleStickToBottom, - viewportRef, - ]); + }, [cancelPendingStickToBottom, forceStickToBottom, scheduleStickToBottom, viewportRef]); const contentContainerStyle = useMemo((): CSSProperties => { - vlog(props.agentId, "memo:contentContainerStyle rebuilt", { isMobileBreakpoint }); return { display: "flex", flexDirection: "column", @@ -725,9 +457,8 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool paddingRight: isMobileBreakpoint ? 8 : 16, boxSizing: "border-box", }; - }, [isMobileBreakpoint, props.agentId]); + }, [isMobileBreakpoint]); const scrollContainerStyle = useMemo((): CSSProperties => { - vlog(props.agentId, "memo:scrollContainerStyle rebuilt", { scrollEnabled }); return { flex: 1, minHeight: 0, @@ -735,15 +466,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool overflowY: scrollEnabled ? "auto" : "hidden", overscrollBehaviorY: "contain", }; - }, [scrollEnabled, props.agentId]); + }, [scrollEnabled]); const virtualRowsContainerStyle = useMemo((): CSSProperties => { - vlog(props.agentId, "memo:virtualRowsContainerStyle rebuilt", { virtualTotalSize }); return { position: "relative", width: "100%", height: virtualTotalSize, }; - }, [virtualTotalSize, props.agentId]); + }, [virtualTotalSize]); const renderVirtualRowStyle = useCallback( (start: number): CSSProperties => ({ position: "absolute", @@ -757,40 +487,26 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool [], ); const mountedHistoryRows = useMemo(() => { - vlog(props.agentId, "memo:mountedHistoryRows rebuilt", { - count: segments.historyMounted.length, - }); return segments.historyMounted.map((item, index) => ( {renderHistoryMountedRow(item, index, segments.historyMounted)} )); - }, [renderHistoryMountedRow, segments.historyMounted, props.agentId]); + }, [renderHistoryMountedRow, segments.historyMounted]); const liveHeadRows = useMemo(() => { - vlog(props.agentId, "memo:liveHeadRows rebuilt", { count: segments.liveHead.length }); return segments.liveHead.map((item, index) => ( {renderLiveHeadRow(item, index, segments.liveHead)} )); - }, [renderLiveHeadRow, segments.liveHead, props.agentId]); + }, [renderLiveHeadRow, segments.liveHead]); const liveAuxiliary = useMemo(() => { - vlog(props.agentId, "memo:liveAuxiliary rebuilt", {}); return renderLiveAuxiliary(); - }, [renderLiveAuxiliary, props.agentId]); + }, [renderLiveAuxiliary]); const shouldRenderEmpty = !boundary.hasMountedHistory && !boundary.hasVirtualizedHistory && !boundary.hasLiveHead && !liveAuxiliary; - vlog(props.agentId, "render summary", { - virtualRowsRendered: virtualRows.length, - mountedHistoryRowsRendered: segments.historyMounted.length, - liveHeadRowsRendered: segments.liveHead.length, - shouldUseVirtualizer, - virtualTotalSize, - shouldRenderEmpty, - }); - return ( <>
["pendingByDraftId"]; type PendingCreateAttempt = PendingCreateByDraftId[string]; -function logWebStickyBottom(_event: string, _details: Record): void {} - function toErrorMessage(error: unknown): string { if (error instanceof Error) { return error.message; @@ -815,14 +813,10 @@ function ChatAgentContent({ ); const handleComposerHeightChange = useCallback( - (height: number) => { + (_height: number) => { if (!agentId) { return; } - logWebStickyBottom("screen_composer_height_change", { - agentId, - height, - }); streamViewRef.current?.prepareForViewportChange(); }, [agentId], @@ -832,9 +826,6 @@ function ChatAgentContent({ if (!agentId) { return; } - logWebStickyBottom("screen_message_sent_scroll_to_bottom", { - agentId, - }); streamViewRef.current?.scrollToBottom("message-sent"); }, [agentId]); diff --git a/packages/app/src/utils/scroll-jank.ts b/packages/app/src/utils/scroll-jank.ts deleted file mode 100644 index bb83e3074..000000000 --- a/packages/app/src/utils/scroll-jank.ts +++ /dev/null @@ -1,18 +0,0 @@ -type ScrollInvestigationEvent = - | "scrollEvent" - | "nearBottomTransition" - | "metricUpdate" - | "itemRenderCall" - | "wheelAttach" - | "wheelDetach" - | "inputChange" - | "keyPress"; - -export function installScrollJankInvestigation(): void {} - -export function markScrollInvestigationRender(_componentId: string): void {} - -export function markScrollInvestigationEvent( - _componentId: string, - _event: ScrollInvestigationEvent, -): void {}