chore: commit all local changes

This commit is contained in:
Mohamed Boudra
2026-02-25 13:53:23 +07:00
parent a060ae1256
commit c4523c70b5
18 changed files with 1818 additions and 67 deletions
@@ -61,6 +61,8 @@ interface AgentInputAreaProps {
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void
/** Optional draft context for listing commands before an agent exists. */
commandDraftConfig?: DraftCommandConfig
/** Called when a message is about to be sent (any path: keyboard, dictation, queued). */
onMessageSent?: () => void
}
const EMPTY_ARRAY: readonly QueuedMessage[] = []
@@ -77,6 +79,7 @@ export function AgentInputArea({
autoFocus = false,
onAddImages,
commandDraftConfig,
onMessageSent,
}: AgentInputAreaProps) {
markScrollInvestigationRender(`AgentInputArea:${serverId}:${agentId}`)
const { theme } = useUnistyles()
@@ -169,6 +172,7 @@ export function AgentInputArea({
}, [addImages, onAddImages])
const submitMessage = useCallback(async (text: string, images?: ImageAttachment[]) => {
onMessageSent?.()
if (onSubmitMessageRef.current) {
await onSubmitMessageRef.current({ text, images })
return
@@ -177,7 +181,7 @@ export function AgentInputArea({
throw new Error('Host is not connected')
}
await sendAgentMessageRef.current(agentIdRef.current, text, images)
}, [])
}, [onMessageSent])
useEffect(() => {
agentIdRef.current = agentId
@@ -137,6 +137,82 @@ function scrollAnchorIntoView(params: {
return true;
}
function forceScrollContainerToBottom(
refs: StreamRenderRefs,
fallbackOffset: number
): void {
const resolveNode = (
input: unknown
): HTMLElement | null => {
if (!(input instanceof HTMLElement)) {
return null;
}
if (input.scrollHeight - input.clientHeight > 1) {
return input;
}
let node: HTMLElement | null = input.parentElement;
while (node) {
if (node.scrollHeight - node.clientHeight > 1) {
return node;
}
node = node.parentElement;
}
return null;
};
const scrollViewHandle = refs.scrollViewRef.current as
| {
getNativeScrollRef?: () => unknown;
getScrollableNode?: () => unknown;
getInnerViewNode?: () => unknown;
getNativeRef?: () => unknown;
}
| null;
const anchorHandle = refs.bottomAnchorRef.current as
| ({ getNativeRef?: () => unknown } & object)
| null;
const candidates: unknown[] = [
scrollViewHandle?.getNativeScrollRef?.(),
scrollViewHandle?.getScrollableNode?.(),
scrollViewHandle?.getInnerViewNode?.(),
scrollViewHandle?.getNativeRef?.(),
scrollViewHandle,
typeof anchorHandle?.getNativeRef === "function"
? anchorHandle.getNativeRef()
: anchorHandle,
];
let scrollNode: HTMLElement | null = null;
for (const candidate of candidates) {
scrollNode = resolveNode(candidate);
if (scrollNode) {
break;
}
}
if (!scrollNode && typeof document !== "undefined") {
scrollNode = resolveNode(
document.querySelector("[data-testid='agent-chat-scroll']")
);
}
if (!scrollNode) {
return;
}
const snap = () => {
scrollNode.scrollTop = Math.max(
fallbackOffset,
scrollNode.scrollHeight - scrollNode.clientHeight
);
};
snap();
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(snap);
}
}
function createStreamRenderStrategy(
config: StreamRenderStrategyConfig
): StreamRenderStrategy {
@@ -255,14 +331,20 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
getBottomOffset: (metrics) =>
Math.max(0, metrics.contentHeight - metrics.viewportHeight),
scrollToBottom: ({ refs, metrics, animated }) => {
if (scrollAnchorIntoView({ refs, animated })) {
return;
const bottomOffset = Math.max(
0,
metrics.contentHeight - metrics.viewportHeight
);
const usedAnchor = scrollAnchorIntoView({ refs, animated });
if (!usedAnchor) {
refs.scrollViewRef.current?.scrollToEnd?.({ animated });
}
refs.scrollViewRef.current?.scrollToEnd?.({ animated });
// Always apply deterministic bottom offset to avoid partial anchors.
refs.scrollViewRef.current?.scrollTo?.({
y: Math.max(0, metrics.contentHeight - metrics.viewportHeight),
y: bottomOffset,
animated,
});
forceScrollContainerToBottom(refs, bottomOffset);
},
scrollToOffset: ({ refs, offset, animated }) => {
refs.scrollViewRef.current?.scrollTo({ y: offset, animated });
@@ -1,12 +1,14 @@
import {
Fragment,
createElement,
forwardRef,
isValidElement,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
useCallback,
} from "react";
import type { ComponentType, ReactElement, ReactNode } from "react";
import {
@@ -104,6 +106,10 @@ function renderStreamEdgeComponent(
return createElement(component);
}
export interface AgentStreamViewHandle {
scrollToBottom(): void;
}
export interface AgentStreamViewProps {
agentId: string;
serverId?: string;
@@ -112,13 +118,13 @@ export interface AgentStreamViewProps {
pendingPermissions: Map<string, PendingPermission>;
}
export function AgentStreamView({
export const AgentStreamView = forwardRef<AgentStreamViewHandle, AgentStreamViewProps>(function AgentStreamView({
agentId,
serverId,
agent,
streamItems,
pendingPermissions,
}: AgentStreamViewProps) {
}, ref) {
const flatListRef = useRef<FlatList<StreamItem>>(null);
const scrollViewRef = useRef<ScrollView>(null);
const bottomAnchorRef = useRef<View>(null);
@@ -139,6 +145,7 @@ export function AgentStreamView({
const hasScrolledInitially = useRef(false);
const hasAutoScrolledOnce = useRef(false);
const isNearBottomRef = useRef(true);
const pendingAnchorRequestRef = useRef(false);
const pendingAutoScrollFrameRef = useRef<number | null>(null);
const pendingAutoScrollAnimatedRef = useRef(false);
const scrollOffsetYRef = useRef(0);
@@ -182,6 +189,7 @@ export function AgentStreamView({
hasScrolledInitially.current = false;
hasAutoScrolledOnce.current = false;
isNearBottomRef.current = true;
pendingAnchorRequestRef.current = false;
setExpandedInlineToolCallIds(new Set());
}, [agentId]);
@@ -224,14 +232,29 @@ export function AgentStreamView({
]
);
const updateNearBottom = useCallback((value: boolean) => {
if (isNearBottomRef.current === value) return;
isNearBottomRef.current = value;
setIsNearBottom(value);
}, []);
const requestAnchorToBottom = useCallback(() => {
pendingAnchorRequestRef.current = true;
}, []);
const handleScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
const previousOffsetY = scrollOffsetYRef.current;
const previousContentHeight = streamViewportMetricsRef.current.contentHeight;
scrollOffsetYRef.current = contentOffset.y;
streamViewportMetricsRef.current = {
contentHeight: Math.max(0, contentSize.height),
viewportHeight: Math.max(0, layoutMeasurement.height),
};
const offsetDelta = contentOffset.y - previousOffsetY;
const contentHeightDelta =
streamViewportMetricsRef.current.contentHeight - previousContentHeight;
const threshold = Math.max(insets.bottom, 32);
const nearBottom = isNearBottomForStreamRenderStrategy({
strategy: streamRenderStrategy,
@@ -241,9 +264,22 @@ export function AgentStreamView({
viewportHeight: streamViewportMetricsRef.current.viewportHeight,
});
if (isNearBottomRef.current !== nearBottom) {
isNearBottomRef.current = nearBottom;
setIsNearBottom(nearBottom);
const pendingAnchorBefore = pendingAnchorRequestRef.current;
const shouldSuppressFalseNearBottom =
pendingAnchorBefore &&
!nearBottom &&
Math.abs(offsetDelta) <= 1 &&
contentHeightDelta > 0;
if (shouldSuppressFalseNearBottom) {
updateNearBottom(true);
} else {
updateNearBottom(nearBottom);
}
const shouldClearPendingAnchor =
pendingAnchorBefore && !nearBottom && Math.abs(offsetDelta) > 1;
if (shouldClearPendingAnchor) {
pendingAnchorRequestRef.current = false;
}
if (showDesktopWebScrollbar) {
@@ -255,6 +291,7 @@ export function AgentStreamView({
showDesktopWebScrollbar,
streamRenderStrategy,
streamScrollbarMetrics,
updateNearBottom,
]
);
@@ -282,12 +319,17 @@ export function AgentStreamView({
animated,
});
scrollOffsetYRef.current = targetOffset;
isNearBottomRef.current = true;
setIsNearBottom(true);
updateNearBottom(true);
},
[streamRenderRefs, streamRenderStrategy]
[updateNearBottom, streamRenderRefs, streamRenderStrategy]
);
useImperativeHandle(ref, () => ({
scrollToBottom() {
requestAnchorToBottom();
},
}), [requestAnchorToBottom]);
const handleContentSizeChange = useCallback(
(width: number, height: number) => {
const previousMetrics = streamViewportMetricsRef.current;
@@ -310,7 +352,11 @@ export function AgentStreamView({
scrollToBottomInternal({ animated: false });
hasAutoScrolledOnce.current = true;
hasScrolledInitially.current = true;
} else if (wasNearBottom || isNearBottomRef.current) {
} else if (
wasNearBottom ||
isNearBottomRef.current ||
pendingAnchorRequestRef.current
) {
scrollToBottomInternal({ animated: false });
}
}
@@ -376,7 +422,7 @@ export function AgentStreamView({
return () => handle.cancel();
}
if (!isNearBottomRef.current) {
if (!isNearBottomRef.current && !pendingAnchorRequestRef.current) {
return;
}
@@ -393,8 +439,6 @@ export function AgentStreamView({
function scrollToBottom() {
const animated = streamRenderStrategy.shouldAnimateManualScrollToBottom();
scrollToBottomInternal({ animated });
isNearBottomRef.current = true;
setIsNearBottom(true);
}
const flatListData = useMemo(() => {
@@ -982,7 +1026,7 @@ export function AgentStreamView({
</View>
</ToolCallSheetProvider>
);
}
});
function normalizeInlinePath(
rawPath: string,