From 0aa9dad3c77d4287486b294ee6f7ce58e0fc9df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 17:54:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9ADirectProject=20?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E8=87=AA=E5=8A=A8=E5=8A=A0=E8=BD=BD=E6=9B=B4?= =?UTF-8?q?=E6=97=A9=E5=8E=86=E5=8F=B2=E5=B9=B6=E6=94=AF=E6=8C=81=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=E5=BA=95=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删掉常驻的「显示更早的对话」按钮,改由触顶与填充视口两条触发自动加载,失败即挂起并留内联重试行 - 新增 useConversationScroll:列表唯一滚动所有权(跟随最新、ResizeObserver 补偿、前插锚点冻结、胶囊显隐) - 新增顶部加载行与底部居中 sticky 胶囊两个表现组件,文案与阈值取自 conversationScrollPolicy - 列表追加 overflow-anchor:none 关掉原生锚定;「有新回复」按最后一轮终态内容指纹判定 - DirectProjectChatView 交出 messagesRef / shouldFollowLatestRef / handleScroll,只传加载状态与回调 Co-authored-by: Junie --- .../chat/DirectProjectChatView.tsx | 30 +- .../DirectProjectConversation.tsx | 66 +++-- .../DirectProjectHistoryRow.tsx | 54 ++++ .../DirectProjectScrollToBottomCapsule.tsx | 29 ++ .../conversationScrollPolicy.ts | 15 + .../useConversationScroll.ts | 269 ++++++++++++++++++ 6 files changed, 422 insertions(+), 41 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectHistoryRow.tsx create mode 100644 apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectScrollToBottomCapsule.tsx create mode 100644 apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/useConversationScroll.ts diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx index e10829c5f..b914c8db9 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/DirectProjectChatView.tsx @@ -1,15 +1,12 @@ -import type { UIEventHandler } from 'react'; import type { Ref } from 'react'; import { useCallback, useEffect, useImperativeHandle, useMemo, - useRef, useState, } from 'react'; -import { AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD } from '../../../app/constants'; import { claimInitialTurnForPage } from '../../../app/initialTurnClaims'; import type { PendingUiConfirmation } from '../../../app/types'; import { projectNameFromPath } from '../../../features/agent-runtime'; @@ -106,8 +103,6 @@ export function DirectProjectChatView({ }: DirectProjectChatViewProps) { const { assets, projectId, refresh, versions } = useDirectProjectManifest(projectPath); - const messagesRef = useRef(null); - const shouldFollowLatestRef = useRef(true); const [runtimeNotice, setRuntimeNotice] = useState(''); const [settingsOpen, setSettingsOpen] = useState(false); const [approvalOpen, setApprovalOpen] = useState(false); @@ -137,8 +132,11 @@ export function DirectProjectChatView({ directEntries, directTurnRunning, directTurnStartedAt, + historyError, historyHasMore, + historyLoading, loadEarlierHistory, + retryEarlierHistory, localMessages, pendingTurns, startInitialTurn, @@ -197,7 +195,6 @@ export function DirectProjectChatView({ initialTurn.content, directCodexConversationMessageId(clientTurnId, 'user'), ); - shouldFollowLatestRef.current = true; startInitialTurn({ clientTurnId, ...(initialTurn.creationType @@ -209,28 +206,12 @@ export function DirectProjectChatView({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [turnStatus.displayBusy, initialTurn, projectId, projectPath]); - useEffect(() => { - if (!shouldFollowLatestRef.current) return; - const list = messagesRef.current; - if (list) list.scrollTop = list.scrollHeight; - }, [directEntries, localMessages]); - useImperativeHandle(ref, () => ({ announce: (text: string) => { appendLocalMessage({ role: 'assistant', text, updatedAt: Date.now() }); }, })); - const handleScroll: UIEventHandler = (event) => { - const list = event.currentTarget; - shouldFollowLatestRef.current = - list.scrollHeight - list.scrollTop - list.clientHeight <= - AGENT_CHAT_SCROLL_BOTTOM_THRESHOLD; - if (historyHasMore && list.scrollTop <= 24) { - void loadEarlierHistory(); - } - }; - return (
void loadEarlierHistory()} - onScroll={handleScroll} + onRetryEarlierHistory={() => void retryEarlierHistory()} /> {pendingConfirmation && onConfirmConfirmation && diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx index b9595d9ee..4aa29a5c2 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectConversation/DirectProjectConversation.tsx @@ -1,28 +1,40 @@ -import type { RefObject, UIEventHandler } from 'react'; - import { AgentMessageContent } from '../../../../../../../../packages/shared/src/components/AgentMessageContent'; import { useLiveNow } from '../../../../../features/project-workspace/useLiveNow'; import type { DirectChatTurn } from '../../conversation/directTurnPresentation'; import { formatTurnDuration } from '../ToolCallGroup/toolCallGroupPresentation'; +import { + DirectProjectHistoryErrorRow, + DirectProjectHistoryLoadingRow, +} from './DirectProjectHistoryRow'; +import { DirectProjectScrollToBottomCapsule } from './DirectProjectScrollToBottomCapsule'; import { DirectProjectTurn } from './DirectProjectTurn'; +import { useConversationScroll } from './useConversationScroll'; /** - * 会话区:回合列表、更早历史入口和运行中过程卡。 + * 会话区:回合列表、更早历史的加载 / 失败行与运行中过程卡。 + * + * **滚动归这一层所有**:贴底跟随、更早历史的自动加载、前插锚点与回到底部胶囊都在 + * `useConversationScroll` 里,视图不再自己持 `messagesRef` / `shouldFollowLatestRef` / + * `handleScroll`——三套补偿写同一份 `scrollTop`,分开持有必然互相覆盖。 * * 回合来自 DirectProject 自己的投影;这里不读历史、不发回合,只把容器给的状态渲染出来。 */ export function DirectProjectConversation({ turns, - messagesRef, historyHasMore, + historyLoading, + historyError, turnInFlight, activeTurnStartedAt, onLoadEarlierHistory, - onScroll, + onRetryEarlierHistory, }: { turns: DirectChatTurn[]; - messagesRef: RefObject; historyHasMore: boolean; + /** 更早历史正在读:驱动顶部加载行(延迟 150ms 才挂载)。 */ + historyLoading: boolean; + /** 更早历史读取失败:非空即挂起自动加载,只留内联错误行的重试。 */ + historyError: string | null; /** * 这一轮在飞吗:`DirectProjectTurnStatus.displayBusy`(本地命令在飞 ∪ 原生已确认在跑)。 * @@ -33,28 +45,48 @@ export function DirectProjectConversation({ turnInFlight: boolean; activeTurnStartedAt: number; onLoadEarlierHistory: () => void; - onScroll: UIEventHandler; + onRetryEarlierHistory: () => void; }) { + const { + listRef, + onScroll, + onToggleCapture, + scrollToBottom, + showScrollToBottom, + scrollToBottomLabel, + showHistoryLoading, + } = useConversationScroll({ + turns, + historyHasMore, + historyLoading, + historyError, + turnInFlight, + onLoadEarlierHistory, + }); return ( <> + {/* `overflow-anchor: none`:关掉浏览器原生 scroll anchoring,前插与展开的补偿只走 + `useConversationScroll` 这一份,避免两套补偿在同一帧里互相抵消。 */}
- {historyHasMore ? ( - + {showHistoryLoading ? : null} + {historyError ? ( + ) : null} {turns.map((turn) => ( ))} + {showScrollToBottom ? ( + + ) : null}
{turnInFlight ? ( +