chore: remove scroll jank investigation logging
This commit is contained in:
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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 {}
|
|
||||||
Reference in New Issue
Block a user