Summary
The Cortex chat panel sometimes fails to scroll all the way to the bottom when a new message arrives, leaving the latest content partially or fully hidden below the fold. Manual scroll is required to see it.
Root cause
interface/src/components/CortexChatPanel.tsx:402-404:
useEffect(() => {
messagesEndRef.current?.scrollIntoView({behavior: "smooth"});
}, [messages.length, isStreaming, toolActivity.length]);
The dependency array misses several height-changing events:
-
Tool result expansion is invisible to the effect. In interface/src/hooks/useCortexChat.ts:122-133, the tool_completed SSE event updates an existing toolActivity[i].status from "running" → "done" and attaches a result_preview. The array length is unchanged, so the effect doesn't re-fire — but the rendered tool card grows in height. If the result preview is tall, it pushes the assistant message below the fold.
-
ThinkingIndicator toggle doesn't fire it either. At CortexChatPanel.tsx:505, the indicator is shown based on !toolActivity.some(t => t.status === "running"). That's derived state, not a length change, so its appear/disappear doesn't re-scroll.
-
Async Markdown layout shifts. <Markdown> (CortexChatPanel.tsx:493) renders synchronously, but code-block syntax highlighting, font loading, and embedded image loads reflow heights after the scroll has already finished. Nothing re-scrolls.
-
behavior: "smooth" races layout. The animation locks onto the target's pixel position when it starts. Between the message arriving (messages.length++), isStreaming → false removing the streaming UI, and toolActivity clearing in the finally block (useCortexChat.ts:167-168), several layout shifts can happen during the in-flight smooth scroll, so it lands short.
Note: messages are not streamed token-by-token — the assistant content arrives whole on the done SSE event (useCortexChat.ts:142-151). So the bug is layout-shift-driven, not streaming-text-driven.
Steps to reproduce (intermittent)
Most reliable when:
- An assistant turn includes one or more tool calls whose
result_preview is tall.
- Or the assistant response contains code blocks / images that reflow after first paint.
Expected
After any new message or tool result, the chat scrolls fully so the bottom of the latest content is visible.
Actual
Scroll stops short of the bottom; user must manually scroll down.
Suggested fix
Either of:
- Replace the effect with a
ResizeObserver on the messages container that keeps it pinned to bottom whenever scrollHeight grows and the user was already near the bottom (preserve scroll-up intent).
- Or include tool-status in deps (e.g.
toolActivity.map(t => t.status).join(",")), switch to behavior: "auto", and follow up with a requestAnimationFrame (or short setTimeout) re-scroll after layout settles — to catch async markdown reflow.
Summary
The Cortex chat panel sometimes fails to scroll all the way to the bottom when a new message arrives, leaving the latest content partially or fully hidden below the fold. Manual scroll is required to see it.
Root cause
interface/src/components/CortexChatPanel.tsx:402-404:The dependency array misses several height-changing events:
Tool result expansion is invisible to the effect. In
interface/src/hooks/useCortexChat.ts:122-133, thetool_completedSSE event updates an existingtoolActivity[i].statusfrom"running"→"done"and attaches aresult_preview. The array length is unchanged, so the effect doesn't re-fire — but the rendered tool card grows in height. If the result preview is tall, it pushes the assistant message below the fold.ThinkingIndicatortoggle doesn't fire it either. AtCortexChatPanel.tsx:505, the indicator is shown based on!toolActivity.some(t => t.status === "running"). That's derived state, not a length change, so its appear/disappear doesn't re-scroll.Async Markdown layout shifts.
<Markdown>(CortexChatPanel.tsx:493) renders synchronously, but code-block syntax highlighting, font loading, and embedded image loads reflow heights after the scroll has already finished. Nothing re-scrolls.behavior: "smooth"races layout. The animation locks onto the target's pixel position when it starts. Between the message arriving (messages.length++),isStreaming → falseremoving the streaming UI, andtoolActivityclearing in thefinallyblock (useCortexChat.ts:167-168), several layout shifts can happen during the in-flight smooth scroll, so it lands short.Note: messages are not streamed token-by-token — the assistant content arrives whole on the
doneSSE event (useCortexChat.ts:142-151). So the bug is layout-shift-driven, not streaming-text-driven.Steps to reproduce (intermittent)
Most reliable when:
result_previewis tall.Expected
After any new message or tool result, the chat scrolls fully so the bottom of the latest content is visible.
Actual
Scroll stops short of the bottom; user must manually scroll down.
Suggested fix
Either of:
ResizeObserveron the messages container that keeps it pinned to bottom wheneverscrollHeightgrows and the user was already near the bottom (preserve scroll-up intent).toolActivity.map(t => t.status).join(",")), switch tobehavior: "auto", and follow up with arequestAnimationFrame(or shortsetTimeout) re-scroll after layout settles — to catch async markdown reflow.