chore: remove scroll jank investigation logging

This commit is contained in:
Mohamed Boudra
2026-04-27 17:13:52 +07:00
parent 40ce317ac2
commit e250024b03
7 changed files with 21 additions and 630 deletions
@@ -22,7 +22,6 @@ const {
deleteAttachmentsMock, deleteAttachmentsMock,
encodeImagesMock, encodeImagesMock,
openExternalUrlMock, openExternalUrlMock,
markScrollInvestigationRenderMock,
mockSessionState, mockSessionState,
setAgentStreamTailMock, setAgentStreamTailMock,
setAgentStreamHeadMock, setAgentStreamHeadMock,
@@ -161,7 +160,6 @@ const {
); );
hoistedMockSessionState.setAgentStreamTail = hoistedSetAgentStreamTailMock; hoistedMockSessionState.setAgentStreamTail = hoistedSetAgentStreamTailMock;
hoistedMockSessionState.setAgentStreamHead = hoistedSetAgentStreamHeadMock; hoistedMockSessionState.setAgentStreamHead = hoistedSetAgentStreamHeadMock;
const hoistedMarkScrollInvestigationRenderMock = vi.fn();
const hoistedAgentDirectoryStatusMock = vi.fn(() => "ready"); const hoistedAgentDirectoryStatusMock = vi.fn(() => "ready");
return { return {
@@ -175,7 +173,6 @@ const {
deleteAttachmentsMock: vi.fn(async () => {}), deleteAttachmentsMock: vi.fn(async () => {}),
encodeImagesMock: vi.fn(async (images: AttachmentMetadata[]) => images), encodeImagesMock: vi.fn(async (images: AttachmentMetadata[]) => images),
openExternalUrlMock: vi.fn(async () => {}), openExternalUrlMock: vi.fn(async () => {}),
markScrollInvestigationRenderMock: hoistedMarkScrollInvestigationRenderMock,
mockSessionState: hoistedMockSessionState, mockSessionState: hoistedMockSessionState,
setAgentStreamTailMock: hoistedSetAgentStreamTailMock, setAgentStreamTailMock: hoistedSetAgentStreamTailMock,
setAgentStreamHeadMock: hoistedSetAgentStreamHeadMock, setAgentStreamHeadMock: hoistedSetAgentStreamHeadMock,
@@ -339,11 +336,6 @@ vi.mock("@/contexts/toast-context", () => ({
useToast: () => ({ error: vi.fn() }), useToast: () => ({ error: vi.fn() }),
})); }));
vi.mock("@/utils/scroll-jank", () => ({
markScrollInvestigationRender: markScrollInvestigationRenderMock,
markScrollInvestigationEvent: vi.fn(),
}));
vi.mock("@/components/agent-status-bar", () => ({ vi.mock("@/components/agent-status-bar", () => ({
AgentStatusBar: () => null, AgentStatusBar: () => null,
DraftAgentStatusBar: () => null, DraftAgentStatusBar: () => null,
@@ -592,7 +584,6 @@ beforeEach(() => {
deleteAttachmentsMock.mockClear(); deleteAttachmentsMock.mockClear();
encodeImagesMock.mockClear(); encodeImagesMock.mockClear();
openExternalUrlMock.mockClear(); openExternalUrlMock.mockClear();
markScrollInvestigationRenderMock.mockClear();
setAgentStreamTailMock.mockClear(); setAgentStreamTailMock.mockClear();
setAgentStreamHeadMock.mockClear(); setAgentStreamHeadMock.mockClear();
setQueuedMessagesMock.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", () => { describe("Composer keyboard shortcuts", () => {
it("interrupts a running agent without clearing a filled draft", async () => { it("interrupts a running agent without clearing a filled draft", async () => {
mockSessionState.sessions.server.agents = new Map([ mockSessionState.sessions.server.agents = new Map([
@@ -987,27 +972,6 @@ describe("Composer attachments", () => {
expect(latestAttachments).toEqual([{ kind: "image", metadata: image }]); 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", () => { it("enables dictation from server capabilities before the agent directory finishes loading", () => {
agentDirectoryStatusMock.mockReturnValue("initial_loading"); agentDirectoryStatusMock.mockReturnValue("initial_loading");
-2
View File
@@ -62,7 +62,6 @@ import {
persistAttachmentFromFileUri, persistAttachmentFromFileUri,
} from "@/attachments/service"; } from "@/attachments/service";
import { resolveStatusControlMode } from "@/components/composer.status-controls"; import { resolveStatusControlMode } from "@/components/composer.status-controls";
import { markScrollInvestigationRender } from "@/utils/scroll-jank";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style"; import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
@@ -972,7 +971,6 @@ export function Composer({
statusControls, statusControls,
inputWrapperStyle, inputWrapperStyle,
}: ComposerProps) { }: ComposerProps) {
markScrollInvestigationRender(`Composer:${serverId}:${agentId}`);
const buttonIconSize = resolveComposerButtonIconSize(); const buttonIconSize = resolveComposerButtonIconSize();
const client = useHostRuntimeClient(serverId); const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId); const isConnected = useHostRuntimeIsConnected(serverId);
+2 -198
View File
@@ -51,7 +51,6 @@ import { formatShortcut } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform"; import { getShortcutOs } from "@/utils/shortcut-platform";
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions"; import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime"; import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
import { markScrollInvestigationEvent, markScrollInvestigationRender } from "@/utils/scroll-jank";
import { isWeb } from "@/constants/platform"; import { isWeb } from "@/constants/platform";
import { useComposerHeightMirror } from "./composer-height-mirror"; import { useComposerHeightMirror } from "./composer-height-mirror";
@@ -167,30 +166,6 @@ interface TextAreaHandle {
} & Record<string, unknown>; } & Record<string, unknown>;
} }
function logWebStickyBottom(_event: string, _details: Record<string, unknown>): 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({ function AttachButtonIcon({
hovered, hovered,
onAttachButtonRef, onAttachButtonRef,
@@ -385,7 +360,6 @@ function resolveSendTooltipLabel(input: {
} }
interface DesktopKeyPressContext { interface DesktopKeyPressContext {
investigationComponentId: string;
onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined; onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined;
isAgentRunning: boolean; isAgentRunning: boolean;
onQueue: ((payload: MessagePayload) => void) | undefined; onQueue: ((payload: MessagePayload) => void) | undefined;
@@ -400,8 +374,6 @@ function handleDesktopKeyPressImpl(
event: WebTextInputKeyPressEvent, event: WebTextInputKeyPressEvent,
ctx: DesktopKeyPressContext, ctx: DesktopKeyPressContext,
): void { ): void {
markScrollInvestigationEvent(ctx.investigationComponentId, "keyPress");
if (isImeComposingKeyboardEvent(event.nativeEvent)) return; if (isImeComposingKeyboardEvent(event.nativeEvent)) return;
if (ctx.onKeyPressCallback) { 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<string>;
}
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<string>;
}
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( function useAutoFocusOnWebEffect(
textInputRef: React.MutableRefObject< textInputRef: React.MutableRefObject<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
@@ -1056,13 +927,6 @@ function queueMessageImpl(ctx: QueueMessageContext): void {
ctx.onMinimizeHeight(); ctx.onMinimizeHeight();
} }
function computeInvestigationComponentId(
voiceServerId: string | undefined,
voiceAgentId: string | undefined,
): string {
return `MessageInput:${voiceServerId ?? "unknown-server"}:${voiceAgentId ?? "unknown-agent"}`;
}
function computeIsRealtimeVoiceForAgent( function computeIsRealtimeVoiceForAgent(
voice: { isVoiceModeForAgent: (serverId: string, agentId: string) => boolean } | null | undefined, voice: { isVoiceModeForAgent: (serverId: string, agentId: string) => boolean } | null | undefined,
voiceServerId: string | undefined, voiceServerId: string | undefined,
@@ -1156,16 +1020,6 @@ function getWebTextAreaImpl(
return null; 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 { interface SendButtonStateInput {
disabled: boolean; disabled: boolean;
isSubmitDisabled: boolean; isSubmitDisabled: boolean;
@@ -1300,26 +1154,6 @@ function extractErrorMessage(error: unknown): string | null {
return 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<MessageInputRef, MessageInputProps>( export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
function MessageInput(props, ref) { function MessageInput(props, ref) {
const { const {
@@ -1360,8 +1194,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
inputWrapperStyle, inputWrapperStyle,
} = resolveMessageInputProps(props); } = resolveMessageInputProps(props);
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg; const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
const investigationComponentId = computeInvestigationComponentId(voiceServerId, voiceAgentId);
markScrollInvestigationRender(investigationComponentId);
const toast = useToast(); const toast = useToast();
const voice = useVoiceOptional(); const voice = useVoiceOptional();
const sendKeys = useShortcutKeys("message-input-send"); const sendKeys = useShortcutKeys("message-input-send");
@@ -1697,11 +1529,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
enabled: isWeb && inputHeight >= MAX_INPUT_HEIGHT, 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({ usePasteImagesEffect({
getWebTextArea, getWebTextArea,
isConnected, isConnected,
@@ -1711,10 +1538,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
onAddImages, onAddImages,
}); });
useComposerResizeObserverEffect({ getWebTextArea, getWebElement, valueRef });
useComposerScrollLogEffect({ getWebTextArea, valueRef });
const setBoundedInputHeight = useCallback( const setBoundedInputHeight = useCallback(
(nextHeight: number) => { (nextHeight: number) => {
const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight)); const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight));
@@ -1746,20 +1569,9 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
(event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
const start = event.nativeEvent.selection?.start ?? 0; const start = event.nativeEvent.selection?.start ?? 0;
const end = event.nativeEvent.selection?.end ?? start; 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 }); onSelectionChangeCallback?.({ start, end });
}, },
[getWebTextArea, onSelectionChangeCallback], [onSelectionChangeCallback],
); );
const shouldHandleDesktopSubmit = isWeb; const shouldHandleDesktopSubmit = isWeb;
@@ -1767,7 +1579,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) { function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) {
if (!shouldHandleDesktopSubmit) return; if (!shouldHandleDesktopSubmit) return;
handleDesktopKeyPressImpl(event, { handleDesktopKeyPressImpl(event, {
investigationComponentId,
onKeyPressCallback, onKeyPressCallback,
isAgentRunning, isAgentRunning,
onQueue, onQueue,
@@ -1815,16 +1626,9 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
const handleInputChange = useCallback( const handleInputChange = useCallback(
(nextValue: string) => { (nextValue: string) => {
markScrollInvestigationEvent(investigationComponentId, "inputChange");
onChangeText(nextValue); onChangeText(nextValue);
if (isWeb) {
logWebStickyBottom("composer_text_changed", {
valueLength: nextValue.length,
lineCount: nextValue.split("\n").length,
});
}
}, },
[investigationComponentId, onChangeText], [onChangeText],
); );
const handleInputFocus = useCallback(() => { const handleInputFocus = useCallback(() => {
+2 -66
View File
@@ -79,7 +79,6 @@ import {
} from "@/utils/inline-path"; } from "@/utils/inline-path";
import { getMarkdownListMarker } from "@/utils/markdown-list"; import { getMarkdownListMarker } from "@/utils/markdown-list";
import { openExternalUrl } from "@/utils/open-external-url"; import { openExternalUrl } from "@/utils/open-external-url";
import { markScrollInvestigationEvent } from "@/utils/scroll-jank";
import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks";
import { import {
getAssistantImageMetadata, getAssistantImageMetadata,
@@ -103,29 +102,6 @@ export type { InlinePathTarget } from "@/utils/inline-path";
type MarkdownStyles = Record<string, TextStyle & ViewStyle & { [key: string]: unknown }>; type MarkdownStyles = Record<string, TextStyle & ViewStyle & { [key: string]: unknown }>;
function useMessageRenderTracker(
label: string,
identity: string,
watched: Record<string, unknown>,
): void {
const renderCountRef = useRef(0);
const prevRef = useRef<Record<string, unknown> | 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 { interface UserMessageProps {
message: string; message: string;
images?: UserMessageImageAttachment[]; images?: UserMessageImageAttachment[];
@@ -425,13 +401,6 @@ export const UserMessage = memo(function UserMessage({
isLastInGroup = true, isLastInGroup = true,
disableOuterSpacing, disableOuterSpacing,
}: UserMessageProps) { }: UserMessageProps) {
useMessageRenderTracker("UserMessage", `${(message ?? "").slice(0, 16)}`, {
message,
images,
isFirstInGroup,
isLastInGroup,
disableOuterSpacing,
});
const isCompact = useIsCompactFormFactor(); const isCompact = useIsCompactFormFactor();
const [messageHovered, setMessageHovered] = useState(false); const [messageHovered, setMessageHovered] = useState(false);
const [copyButtonHovered, setCopyButtonHovered] = useState(false); const [copyButtonHovered, setCopyButtonHovered] = useState(false);
@@ -1383,19 +1352,6 @@ export const AssistantMessage = memo(function AssistantMessage({
disableOuterSpacing, disableOuterSpacing,
spacing = "default", spacing = "default",
}: AssistantMessageProps) { }: AssistantMessageProps) {
useMessageRenderTracker(
"AssistantMessage",
`${serverId ?? "?"}/${(message ?? "").slice(0, 16)}`,
{
message,
onInlinePathPress,
workspaceRoot,
serverId,
client,
disableOuterSpacing,
spacing,
},
);
const resolvedDisableOuterSpacing = useDisableOuterSpacing( const resolvedDisableOuterSpacing = useDisableOuterSpacing(
disableOuterSpacing ?? spacing !== "default", disableOuterSpacing ?? spacing !== "default",
); );
@@ -2373,9 +2329,8 @@ function computeShimmerMetrics(input: {
function useDetailWheelPropagationBlocker(input: { function useDetailWheelPropagationBlocker(input: {
detailWrapperRef: React.RefObject<View | null>; detailWrapperRef: React.RefObject<View | null>;
enabled: boolean; enabled: boolean;
wheelInvestigationComponentId: string;
}): void { }): void {
const { detailWrapperRef, enabled, wheelInvestigationComponentId } = input; const { detailWrapperRef, enabled } = input;
useEffect(() => { useEffect(() => {
if (!enabled) { if (!enabled) {
return; return;
@@ -2389,13 +2344,11 @@ function useDetailWheelPropagationBlocker(input: {
event.stopPropagation(); event.stopPropagation();
} }
}; };
markScrollInvestigationEvent(wheelInvestigationComponentId, "wheelAttach");
node.addEventListener("wheel", stopWheelPropagation, { passive: true }); node.addEventListener("wheel", stopWheelPropagation, { passive: true });
return () => { return () => {
markScrollInvestigationEvent(wheelInvestigationComponentId, "wheelDetach");
node.removeEventListener("wheel", stopWheelPropagation); node.removeEventListener("wheel", stopWheelPropagation);
}; };
}, [detailWrapperRef, enabled, wheelInvestigationComponentId]); }, [detailWrapperRef, enabled]);
} }
const SHIMMER_GRADIENT = const SHIMMER_GRADIENT =
@@ -2450,7 +2403,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
const hasDetailContent = Boolean(renderDetails); const hasDetailContent = Boolean(renderDetails);
const detailContent = hasDetailContent && isExpanded ? renderDetails?.() : null; const detailContent = hasDetailContent && isExpanded ? renderDetails?.() : null;
const detailWrapperRef = useRef<View | null>(null); const detailWrapperRef = useRef<View | null>(null);
const wheelInvestigationComponentId = `ExpandableBadgeWheel:${testID ?? label}`;
const handleHoverIn = useCallback(() => setIsHovered(true), []); const handleHoverIn = useCallback(() => setIsHovered(true), []);
const handleHoverOut = useCallback(() => { const handleHoverOut = useCallback(() => {
@@ -2554,7 +2506,6 @@ const ExpandableBadge = memo(function ExpandableBadge({
useDetailWheelPropagationBlocker({ useDetailWheelPropagationBlocker({
detailWrapperRef, detailWrapperRef,
enabled: !isNative && isExpanded && hasDetailContent, enabled: !isNative && isExpanded && hasDetailContent,
wheelInvestigationComponentId,
}); });
const shimmerLabelStyle = useMemo( const shimmerLabelStyle = useMemo(
@@ -2797,21 +2748,6 @@ export const ToolCall = memo(function ToolCall({
onInlineDetailsExpandedChange, onInlineDetailsExpandedChange,
onOpenFilePath, onOpenFilePath,
}: ToolCallProps) { }: ToolCallProps) {
useMessageRenderTracker("ToolCall", `${toolName}/${status}`, {
toolName,
args,
result,
error,
status,
detail,
cwd,
metadata,
isLastInSequence,
disableOuterSpacing,
onInlineDetailsHoverChange,
onInlineDetailsExpandedChange,
onOpenFilePath,
});
const { openToolCall } = useToolCallSheet(); const { openToolCall } = useToolCallSheet();
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
@@ -25,17 +25,6 @@ const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64;
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1; const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1;
import { useWebElementScrollbar } from "./use-web-scrollbar"; import { useWebElementScrollbar } from "./use-web-scrollbar";
function logWebStickyBottom(_event: string, _details: Record<string, unknown>): 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( function isScrollContainerNearBottom(
scrollContainer: Pick<HTMLElement, "scrollTop" | "clientHeight" | "scrollHeight">, scrollContainer: Pick<HTMLElement, "scrollTop" | "clientHeight" | "scrollHeight">,
thresholdPx = AUTO_SCROLL_BOTTOM_THRESHOLD_PX, thresholdPx = AUTO_SCROLL_BOTTOM_THRESHOLD_PX,
@@ -92,36 +81,6 @@ function isScrollContainerOverscrolledPastBottom(
return getScrollContainerDistanceFromBottom(scrollContainer) < 0; return getScrollContainerDistanceFromBottom(scrollContainer) < 0;
} }
const VIEWPORT_LOG_PREFIX = "[WebStreamViewport]";
function vlog(agentId: string, event: string, details: Record<string, unknown> = {}): void {
console.log(VIEWPORT_LOG_PREFIX, agentId, event, details);
}
function useRenderTracker(
label: string,
agentId: string,
watched: Record<string, unknown>,
): number {
const renderCountRef = useRef(0);
const prevRef = useRef<Record<string, unknown> | 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 }) { function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: boolean }) {
const { const {
segments, segments,
@@ -135,26 +94,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
scrollEnabled, scrollEnabled,
isMobileBreakpoint, isMobileBreakpoint,
} = props; } = 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<HTMLElement | null>(null); const scrollContainerRef = useRef<HTMLElement | null>(null);
const contentRef = useRef<HTMLElement | null>(null); const contentRef = useRef<HTMLElement | null>(null);
const handleScrollContainerRef = useCallback((node: HTMLElement | null) => { const handleScrollContainerRef = useCallback((node: HTMLElement | null) => {
@@ -170,13 +109,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
}; };
const followOutputRef = useRef(followOutput); const followOutputRef = useRef(followOutput);
const lastKnownScrollTopRef = useRef(0); const lastKnownScrollTopRef = useRef(0);
const lastLoggedMetricsRef = useRef<{
scrollTop: number;
clientWidth: number;
clientHeight: number;
scrollWidth: number;
scrollHeight: number;
} | null>(null);
const pendingUserScrollUpIntentRef = useRef(false); const pendingUserScrollUpIntentRef = useRef(false);
const isPointerScrollActiveRef = useRef(false); const isPointerScrollActiveRef = useRef(false);
const lastTouchClientYRef = useRef<number | null>(null); const lastTouchClientYRef = useRef<number | null>(null);
@@ -214,28 +146,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
overscan: 8, overscan: 8,
}); });
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:virtualizer-adjust setup fired", {});
rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => { rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = (_item, _delta, instance) => {
const viewportHeight = instance.scrollRect?.height ?? 0; const viewportHeight = instance.scrollRect?.height ?? 0;
const scrollOffset = instance.scrollOffset ?? 0; const scrollOffset = instance.scrollOffset ?? 0;
const remainingDistance = instance.getTotalSize() - (scrollOffset + viewportHeight); 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 remainingDistance > AUTO_SCROLL_BOTTOM_THRESHOLD_PX;
}; };
return () => { return () => {
rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined; rowVirtualizer.shouldAdjustScrollPositionOnItemSizeChange = undefined;
}; };
}, [rowVirtualizer, props.agentId]); }, [rowVirtualizer]);
const virtualRows = rowVirtualizer.getVirtualItems(); const virtualRows = rowVirtualizer.getVirtualItems();
const virtualTotalSize = rowVirtualizer.getTotalSize(); const virtualTotalSize = rowVirtualizer.getTotalSize();
@@ -293,21 +213,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
if (isScrollContainerOverscrolledPastBottom(scrollContainer)) { if (isScrollContainerOverscrolledPastBottom(scrollContainer)) {
return; 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); scrollElementToBottom(scrollContainer, behavior);
lastKnownScrollTopRef.current = scrollContainer.scrollTop; lastKnownScrollTopRef.current = scrollContainer.scrollTop;
syncNearBottom(scrollContainer, onNearBottomChange); syncNearBottom(scrollContainer, onNearBottomChange);
}, },
[onNearBottomChange, props.agentId], [onNearBottomChange],
); );
const scheduleStickToBottom = useCallback(() => { const scheduleStickToBottom = useCallback(() => {
@@ -318,15 +228,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
if (pendingAutoScrollFrameRef.current !== null) { if (pendingAutoScrollFrameRef.current !== null) {
return; 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 = window.requestAnimationFrame(() => {
pendingAutoScrollFrameRef.current = null; pendingAutoScrollFrameRef.current = null;
if (!followOutputRef.current) { if (!followOutputRef.current) {
@@ -334,7 +235,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
} }
scrollMessagesToBottom("auto"); scrollMessagesToBottom("auto");
}); });
}, [props.agentId, scrollMessagesToBottom]); }, [scrollMessagesToBottom]);
const forceStickToBottom = useCallback(() => { const forceStickToBottom = useCallback(() => {
cancelPendingStickToBottom(); cancelPendingStickToBottom();
@@ -349,31 +250,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
return; return;
} }
syncNearBottom(scrollContainer, onNearBottomChange); syncNearBottom(scrollContainer, onNearBottomChange);
const currentMetrics = { }, [onNearBottomChange]);
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]);
const handleDomScroll = useCallback(() => { const handleDomScroll = useCallback(() => {
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
@@ -402,26 +279,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
} }
lastKnownScrollTopRef.current = currentScrollTop; 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(); updateScrollMetrics();
}, [cancelPendingStickToBottom, updateScrollMetrics, props.agentId]); }, [cancelPendingStickToBottom, updateScrollMetrics]);
useLayoutEffect(() => { useLayoutEffect(() => {
vlog(props.agentId, "effect:activation-reset fired", { activationKey, isActivationReady });
if (!isActivationReady) { if (!isActivationReady) {
return; return;
} }
@@ -443,12 +304,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
return () => { return () => {
window.clearTimeout(timeout); window.clearTimeout(timeout);
}; };
}, [activationKey, forceStickToBottom, isActivationReady, scheduleStickToBottom, props.agentId]); }, [activationKey, forceStickToBottom, isActivationReady, scheduleStickToBottom]);
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:stick-on-segments-change fired", {
followOutput: followOutputRef.current,
});
if (!followOutputRef.current) { if (!followOutputRef.current) {
return; return;
} }
@@ -458,28 +316,16 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
segments.historyMounted, segments.historyMounted,
segments.historyVirtualized, segments.historyVirtualized,
segments.liveHead, segments.liveHead,
props.agentId,
]); ]);
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:stick-on-virtual-total fired", {
followOutput: followOutputRef.current,
shouldUseVirtualizer,
virtualTotalSize,
});
if (!followOutputRef.current || !shouldUseVirtualizer) { if (!followOutputRef.current || !shouldUseVirtualizer) {
return; return;
} }
scheduleStickToBottom(); scheduleStickToBottom();
}, [scheduleStickToBottom, shouldUseVirtualizer, virtualTotalSize, props.agentId]); }, [scheduleStickToBottom, shouldUseVirtualizer, virtualTotalSize]);
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:metrics-on-segments-change fired", {
mounted: segments.historyMounted.length,
virtualized: segments.historyVirtualized.length,
liveHead: segments.liveHead.length,
virtualTotalSize,
});
updateScrollMetrics(); updateScrollMetrics();
}, [ }, [
segments.historyMounted.length, segments.historyMounted.length,
@@ -487,11 +333,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
segments.liveHead.length, segments.liveHead.length,
updateScrollMetrics, updateScrollMetrics,
virtualTotalSize, virtualTotalSize,
props.agentId,
]); ]);
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:resize-observer setup fired", {});
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
const contentNode = contentRef.current; const contentNode = contentRef.current;
if (!scrollContainer || typeof ResizeObserver === "undefined") { if (!scrollContainer || typeof ResizeObserver === "undefined") {
@@ -500,15 +344,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
updateScrollMetrics(); updateScrollMetrics();
const observer = new ResizeObserver(() => { 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(); updateScrollMetrics();
if (!followOutputRef.current) { if (!followOutputRef.current) {
return; return;
@@ -522,68 +357,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [props.agentId, scheduleStickToBottom, updateScrollMetrics]); }, [scheduleStickToBottom, updateScrollMetrics]);
useEffect(() => { useEffect(() => {
vlog(props.agentId, "effect:scroll-listeners setup fired", {});
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) { if (!scrollContainer) {
return; return;
} }
const originalScrollTo = scrollContainer.scrollTo.bind(scrollContainer);
const scrollTopDescriptor =
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(scrollContainer), "scrollTop") ??
Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop");
scrollContainer.scrollTo = ((...args: Parameters<HTMLElement["scrollTo"]>) => {
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) => { const handleWheel = (event: WheelEvent) => {
if (event.deltaY < 0) { if (event.deltaY < 0) {
pendingUserScrollUpIntentRef.current = true; pendingUserScrollUpIntentRef.current = true;
@@ -618,27 +399,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
const handleTouchEnd = () => { const handleTouchEnd = () => {
lastTouchClientYRef.current = null; 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("scroll", handleDomScroll, { passive: true });
scrollContainer.addEventListener("wheel", handleWheel, { 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("touchmove", handleTouchMove, { passive: true });
scrollContainer.addEventListener("touchend", handleTouchEnd, { passive: true }); scrollContainer.addEventListener("touchend", handleTouchEnd, { passive: true });
scrollContainer.addEventListener("touchcancel", handleTouchEnd, { passive: true }); scrollContainer.addEventListener("touchcancel", handleTouchEnd, { passive: true });
if (typeof document !== "undefined") {
document.addEventListener("selectionchange", handleSelectionChange, { passive: true });
}
return () => { return () => {
scrollContainer.removeEventListener("scroll", handleDomScroll); scrollContainer.removeEventListener("scroll", handleDomScroll);
@@ -663,18 +420,10 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
scrollContainer.removeEventListener("touchmove", handleTouchMove); scrollContainer.removeEventListener("touchmove", handleTouchMove);
scrollContainer.removeEventListener("touchend", handleTouchEnd); scrollContainer.removeEventListener("touchend", handleTouchEnd);
scrollContainer.removeEventListener("touchcancel", 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(() => { useEffect(() => {
vlog(props.agentId, "effect:viewportRef-handle setup fired", {});
const handle: StreamViewportHandle = { const handle: StreamViewportHandle = {
scrollToBottom: () => { scrollToBottom: () => {
setFollowOutput(true); setFollowOutput(true);
@@ -685,16 +434,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
if (!followOutputRef.current) { if (!followOutputRef.current) {
return; 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(); scheduleStickToBottom();
}, },
}; };
@@ -705,16 +444,9 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
} }
cancelPendingStickToBottom(); cancelPendingStickToBottom();
}; };
}, [ }, [cancelPendingStickToBottom, forceStickToBottom, scheduleStickToBottom, viewportRef]);
cancelPendingStickToBottom,
forceStickToBottom,
props.agentId,
scheduleStickToBottom,
viewportRef,
]);
const contentContainerStyle = useMemo((): CSSProperties => { const contentContainerStyle = useMemo((): CSSProperties => {
vlog(props.agentId, "memo:contentContainerStyle rebuilt", { isMobileBreakpoint });
return { return {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@@ -725,9 +457,8 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
paddingRight: isMobileBreakpoint ? 8 : 16, paddingRight: isMobileBreakpoint ? 8 : 16,
boxSizing: "border-box", boxSizing: "border-box",
}; };
}, [isMobileBreakpoint, props.agentId]); }, [isMobileBreakpoint]);
const scrollContainerStyle = useMemo((): CSSProperties => { const scrollContainerStyle = useMemo((): CSSProperties => {
vlog(props.agentId, "memo:scrollContainerStyle rebuilt", { scrollEnabled });
return { return {
flex: 1, flex: 1,
minHeight: 0, minHeight: 0,
@@ -735,15 +466,14 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
overflowY: scrollEnabled ? "auto" : "hidden", overflowY: scrollEnabled ? "auto" : "hidden",
overscrollBehaviorY: "contain", overscrollBehaviorY: "contain",
}; };
}, [scrollEnabled, props.agentId]); }, [scrollEnabled]);
const virtualRowsContainerStyle = useMemo((): CSSProperties => { const virtualRowsContainerStyle = useMemo((): CSSProperties => {
vlog(props.agentId, "memo:virtualRowsContainerStyle rebuilt", { virtualTotalSize });
return { return {
position: "relative", position: "relative",
width: "100%", width: "100%",
height: virtualTotalSize, height: virtualTotalSize,
}; };
}, [virtualTotalSize, props.agentId]); }, [virtualTotalSize]);
const renderVirtualRowStyle = useCallback( const renderVirtualRowStyle = useCallback(
(start: number): CSSProperties => ({ (start: number): CSSProperties => ({
position: "absolute", position: "absolute",
@@ -757,40 +487,26 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
[], [],
); );
const mountedHistoryRows = useMemo(() => { const mountedHistoryRows = useMemo(() => {
vlog(props.agentId, "memo:mountedHistoryRows rebuilt", {
count: segments.historyMounted.length,
});
return segments.historyMounted.map((item, index) => ( return segments.historyMounted.map((item, index) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{renderHistoryMountedRow(item, index, segments.historyMounted)} {renderHistoryMountedRow(item, index, segments.historyMounted)}
</Fragment> </Fragment>
)); ));
}, [renderHistoryMountedRow, segments.historyMounted, props.agentId]); }, [renderHistoryMountedRow, segments.historyMounted]);
const liveHeadRows = useMemo(() => { const liveHeadRows = useMemo(() => {
vlog(props.agentId, "memo:liveHeadRows rebuilt", { count: segments.liveHead.length });
return segments.liveHead.map((item, index) => ( return segments.liveHead.map((item, index) => (
<Fragment key={item.id}>{renderLiveHeadRow(item, index, segments.liveHead)}</Fragment> <Fragment key={item.id}>{renderLiveHeadRow(item, index, segments.liveHead)}</Fragment>
)); ));
}, [renderLiveHeadRow, segments.liveHead, props.agentId]); }, [renderLiveHeadRow, segments.liveHead]);
const liveAuxiliary = useMemo(() => { const liveAuxiliary = useMemo(() => {
vlog(props.agentId, "memo:liveAuxiliary rebuilt", {});
return renderLiveAuxiliary(); return renderLiveAuxiliary();
}, [renderLiveAuxiliary, props.agentId]); }, [renderLiveAuxiliary]);
const shouldRenderEmpty = const shouldRenderEmpty =
!boundary.hasMountedHistory && !boundary.hasMountedHistory &&
!boundary.hasVirtualizedHistory && !boundary.hasVirtualizedHistory &&
!boundary.hasLiveHead && !boundary.hasLiveHead &&
!liveAuxiliary; !liveAuxiliary;
vlog(props.agentId, "render summary", {
virtualRowsRendered: virtualRows.length,
mountedHistoryRowsRendered: segments.historyMounted.length,
liveHeadRowsRendered: segments.liveHead.length,
shouldUseVirtualizer,
virtualTotalSize,
shouldRenderEmpty,
});
return ( return (
<> <>
<div <div
+1 -10
View File
@@ -306,8 +306,6 @@ type RouteBottomAnchorRequest = ReturnType<typeof deriveRouteBottomAnchorRequest
type PendingCreateByDraftId = ReturnType<typeof useCreateFlowStore.getState>["pendingByDraftId"]; type PendingCreateByDraftId = ReturnType<typeof useCreateFlowStore.getState>["pendingByDraftId"];
type PendingCreateAttempt = PendingCreateByDraftId[string]; type PendingCreateAttempt = PendingCreateByDraftId[string];
function logWebStickyBottom(_event: string, _details: Record<string, unknown>): void {}
function toErrorMessage(error: unknown): string { function toErrorMessage(error: unknown): string {
if (error instanceof Error) { if (error instanceof Error) {
return error.message; return error.message;
@@ -815,14 +813,10 @@ function ChatAgentContent({
); );
const handleComposerHeightChange = useCallback( const handleComposerHeightChange = useCallback(
(height: number) => { (_height: number) => {
if (!agentId) { if (!agentId) {
return; return;
} }
logWebStickyBottom("screen_composer_height_change", {
agentId,
height,
});
streamViewRef.current?.prepareForViewportChange(); streamViewRef.current?.prepareForViewportChange();
}, },
[agentId], [agentId],
@@ -832,9 +826,6 @@ function ChatAgentContent({
if (!agentId) { if (!agentId) {
return; return;
} }
logWebStickyBottom("screen_message_sent_scroll_to_bottom", {
agentId,
});
streamViewRef.current?.scrollToBottom("message-sent"); streamViewRef.current?.scrollToBottom("message-sent");
}, [agentId]); }, [agentId]);
-18
View File
@@ -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 {}