diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx
index 260328c9..bcb57d2d 100644
--- a/packages/ui/src/components/chat/ChatContainer.tsx
+++ b/packages/ui/src/components/chat/ChatContainer.tsx
@@ -147,6 +147,7 @@ type HydratingToolSkeletonRow = {
};
type ChatViewportProps = {
+ onStatusOverlayNode: (node: HTMLDivElement | null) => void;
currentSessionId: string;
currentSessionKey: string;
isDesktopExpandedInput: boolean;
@@ -191,6 +192,7 @@ type ChatViewportProps = {
};
const ChatViewport = React.memo(({
+ onStatusOverlayNode,
currentSessionId,
currentSessionKey,
isDesktopExpandedInput,
@@ -414,9 +416,15 @@ const ChatViewport = React.memo(({
/>
{/* Static above the composer: inside the list it walked down
- with every streamed line while a turn was anchored. */}
-
-
+ with every streamed line while a turn was anchored. Solid
+ background — text streams beneath it; the measured height
+ feeds composerOverlayHeight so the live line never hides
+ under it. */}
+
@@ -435,7 +443,8 @@ const ChatViewport = React.memo(({
);
}, (prev, next) => {
- return prev.currentSessionId === next.currentSessionId
+ return prev.onStatusOverlayNode === next.onStatusOverlayNode
+ && prev.currentSessionId === next.currentSessionId
&& prev.currentSessionKey === next.currentSessionKey
&& prev.isDesktopExpandedInput === next.isDesktopExpandedInput
&& prev.isMobile === next.isMobile
@@ -917,10 +926,32 @@ export const ChatContainer: React.FC
= ({
activeTurnChangeRef.current(turnId);
}, []);
- // The composer sits below the timeline rather than over it, so no part of
- // the scroll container is occluded. Mobile surfaces that float the composer
- // pass their measured height here instead.
- const composerOverlayHeight = 0;
+ // The composer sits below the timeline, but the status/working row floats
+ // OVER the timeline's bottom edge; its measured height keeps the live
+ // streaming line above it and reserves matching end inset in the list.
+ const [statusOverlayHeight, setStatusOverlayHeight] = React.useState(0);
+ const composerOverlayHeight = statusOverlayHeight;
+ const statusOverlayObserverRef = React.useRef(null);
+ const onStatusOverlayNode = React.useCallback((node: HTMLDivElement | null) => {
+ statusOverlayObserverRef.current?.disconnect();
+ statusOverlayObserverRef.current = null;
+ if (!node || !globalThis.ResizeObserver) {
+ setStatusOverlayHeight(0);
+ return;
+ }
+ const update = () => {
+ const height = node.getBoundingClientRect().height;
+ setStatusOverlayHeight((prev) => (Math.abs(prev - height) < 1 ? prev : height));
+ };
+ const observer = new ResizeObserver(update);
+ observer.observe(node);
+ statusOverlayObserverRef.current = observer;
+ update();
+ }, []);
+ React.useEffect(() => () => {
+ statusOverlayObserverRef.current?.disconnect();
+ statusOverlayObserverRef.current = null;
+ }, []);
const lastUserMessageId = React.useMemo(() => {
for (let index = sessionMessages.length - 1; index >= 0; index -= 1) {
const message = sessionMessages[index];
@@ -1326,6 +1357,7 @@ export const ChatContainer: React.FC = ({
return (