fix(chat): 打开主 Agent 会话默认贴底并完整显示最后一条

最后一条 final answer 的完整内容是懒加载(AnswerBlock useInView+fetchDetail),
在 activity 数组稳定后才撑开高度,而滚动 layout effect 只依赖 activity 引用、
不会因高度变化重触发,导致打开后最后一条被顶到屏幕外。新增 ResizeObserver 观测
transcript 容器:用户仍贴底时,任何高度变化(transcript 挂载/详情展开/markdown 撑高)
都重新贴底;手动上滚则不再拽回。planner/worker 会话同样受益。

Claude-Session: https://claude.ai/code/session_01RVrPtpngE9T9DRJHosf6MB
This commit is contained in:
Autumn-27 committed 2026-08-20 13:17:38 -04:00
1 parent a98081eea9
commit 39c540edfc
1 file changed
+20
@@ -1004,6 +1004,26 @@ export function SessionsTab({ taskId }: { taskId: string }) {
const vp = viewport();
if (vp) vp.scrollTop = vp.scrollHeight;
}, [activity, viewport]);
// Lazy detail loads (AnswerBlock / ToolBlock / Markdown) grow the content AFTER the
// activity array settles, WITHOUT changing its reference — so the layout effects
// above never re-fire and a freshly opened session would leave its last message
// scrolled partly off-screen (the final answer expands from a one-line summary to
// full markdown below the fold). A ResizeObserver re-pins to the bottom on any
// height change while the user is still at the bottom, so opening the main agent
// lands on the last message fully shown. contentRef's div is always mounted, so the
// observer catches the transcript mounting + each detail expanding.
// biome-ignore lint/correctness/useExhaustiveDependencies: activeId intentionally rebinds the observer to the new session's content.
React.useEffect(() => {
const el = contentRef.current;
if (!el || typeof ResizeObserver === "undefined") return;
const ro = new ResizeObserver(() => {
if (!atBottomRef.current) return;
const vp = viewport();
if (vp) vp.scrollTop = vp.scrollHeight;
});
ro.observe(el);
return () => ro.disconnect();
}, [activeId, viewport]);
function stop() {
if (stopping) return;