From 7408ee9554e8a7d481eb1bfb9d3d7db21381906a Mon Sep 17 00:00:00 2001 From: DT_Lin <962018407@qq.com> Date: Sat, 1 Aug 2026 01:43:33 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0=E9=9A=8F?= =?UTF-8?q?=E5=B1=8F=E6=BB=9A=E5=8A=A8+=E4=BC=98=E5=8C=96=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E5=8D=A0=E4=BD=8D=E7=9B=92=E5=AD=90=E9=AB=98=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ChatWindow.tsx | 11 ++++++++--- hooks/useAgentSession.ts | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/components/ChatWindow.tsx b/components/ChatWindow.tsx index 794a34cf..b38890eb 100644 --- a/components/ChatWindow.tsx +++ b/components/ChatWindow.tsx @@ -224,6 +224,11 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate soundedExtensionDialogIdRef.current = extensionDialog.id; playDoneSoundRef.current(); }, [extensionDialog]); + + // Reserve enough bottom padding in the message list so the last message is + // not hidden behind the fixed ChatInput. The input area's minimum height is + // ~52px (textarea + padding + bottom controls), so we keep a static spacer. + const inputHeight = 52; // Register the abort handler for the global Esc shortcut useEffect(() => { @@ -719,9 +724,9 @@ export function ChatWindow({ session, newSessionCwd, onAgentEnd, onSessionCreate /> )} - {agentRunning && ( -
- )} + {/* Spacer sized to the bottom input area so the last message is + not hidden behind ChatInput, without wasting a full viewport. */} +
diff --git a/hooks/useAgentSession.ts b/hooks/useAgentSession.ts index a482f712..26c263ee 100644 --- a/hooks/useAgentSession.ts +++ b/hooks/useAgentSession.ts @@ -156,6 +156,10 @@ export type ThinkingLevelOption = "auto" | "off" | "minimal" | "low" | "medium" const PROGRAMMATIC_SCROLL_IGNORE_MS = 700; const USER_SCROLL_INTENT_MS = 1200; +// Distance from the bottom of the scroll container within which live-follow +// scrolling is active. Larger values make follow more lenient; smaller values +// require the user to stay closer to the bottom. +const SCROLL_BOTTOM_THRESHOLD = 150; const PROMPT_SETTLE_INITIAL_DELAY_MS = 800; const PROMPT_SETTLE_POLL_MS = 600; const PROMPT_SETTLE_MAX_MS = 20_000; @@ -396,6 +400,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) { const lastUserMsgRef = useRef(null); const pendingScrollToUserRef = useRef(false); const completionScrollAllowedRef = useRef(true); + const isNearBottomRef = useRef(true); const executeBashRef = useRef<(command: string, excludeFromContext: boolean) => Promise | undefined>(undefined); const userScrollIntentUntilRef = useRef(0); const ignoreProgrammaticScrollUntilRef = useRef(0); @@ -1129,6 +1134,13 @@ export function useAgentSession(opts: UseAgentSessionOptions) { dispatch({ type: "update", message: normalizeToolCalls(msg as AgentMessage) }); } setAgentPhase(null); + // Live-follow the streaming output only when the user is already near + // the bottom of the message list. If they scrolled up, leave them there. + if (isNearBottomRef.current) { + // Defer the scroll so React has time to update the DOM with the new + // streaming content; otherwise scrollIntoView may target stale layout. + requestAnimationFrame(() => scrollToBottom("auto")); + } break; } case "message_end": { @@ -1703,6 +1715,11 @@ export function useAgentSession(opts: UseAgentSessionOptions) { }, []); const handleScrollPositionChange = useCallback(() => { + const container = scrollContainerRef.current; + if (container) { + const { scrollTop, clientHeight, scrollHeight } = container; + isNearBottomRef.current = scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD; + } if (!agentRunningRef.current) return; if (Date.now() < ignoreProgrammaticScrollUntilRef.current) return; if (Date.now() > userScrollIntentUntilRef.current) return; @@ -1793,7 +1810,7 @@ export function useAgentSession(opts: UseAgentSessionOptions) { } else if (!initialScrollDoneRef.current) { initialScrollDoneRef.current = true; scrollToBottom("instant"); - } else if (!agentRunningRef.current && completionScrollAllowedRef.current) { + } else if (!agentRunningRef.current && (completionScrollAllowedRef.current || isNearBottomRef.current)) { scrollToBottom("smooth"); } }