From c9f1b62809fec9a00135c683befe01248dc1352c Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Mon, 2 Mar 2026 12:01:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9E=8B=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E4=B8=B2=E5=8F=B0=20+=20=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=90=8E=E6=BB=9A=E5=8A=A8=E4=B8=8D=E5=88=B0=E5=BA=95=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. page.tsx — loadSession 加 cancelled 竞态守卫,快速切换会话时 丢弃过期的 fetch 结果,防止旧会话的 model/provider 覆盖新会话 2. ChatView.tsx — 去掉 if(modelName)/if(providerId) 守卫,改为 无条件同步 props,空值走 localStorage → default fallback 链 3. ChatListPanel.tsx + types + route.ts + db.ts — 三个创建会话入口 (handleFolderSelect/handleNewChat/handleCreateSessionInProject) 统一从 localStorage 读取并传递 model + provider_id,DB 层写入 4. MessageList.tsx — ScrollOnStream 增加 messageCount prop, 消息数增加时立即 scrollToBottom,覆盖乐观插入和 assistant 落地 Co-Authored-By: Claude Opus 4.6 --- src/app/api/chat/sessions/route.ts | 1 + src/app/chat/[id]/page.tsx | 5 +++++ src/components/chat/ChatView.tsx | 6 ++++-- src/components/chat/MessageList.tsx | 15 ++++++++++++--- src/components/layout/ChatListPanel.tsx | 18 ++++++++++++++---- src/lib/db.ts | 5 +++-- src/types/index.ts | 1 + 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/app/api/chat/sessions/route.ts b/src/app/api/chat/sessions/route.ts index 6787393..94e5bf1 100644 --- a/src/app/api/chat/sessions/route.ts +++ b/src/app/api/chat/sessions/route.ts @@ -43,6 +43,7 @@ export async function POST(request: NextRequest) { body.system_prompt, body.working_directory, body.mode, + body.provider_id, ); const response: SessionResponse = { session }; return Response.json(response, { status: 201 }); diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 5ff9c3b..ea808b4 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -78,11 +78,15 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { // Load session info and set working directory useEffect(() => { + let cancelled = false; + async function loadSession() { try { const res = await fetch(`/api/chat/sessions/${id}`); + if (cancelled) return; if (res.ok) { const data: { session: ChatSession } = await res.json(); + if (cancelled) return; if (data.session.working_directory) { setWorkingDirectory(data.session.working_directory); setSessionWorkingDir(data.session.working_directory); @@ -105,6 +109,7 @@ export default function ChatSessionPage({ params }: ChatSessionPageProps) { } loadSession(); + return () => { cancelled = true; }; }, [id, setWorkingDirectory, setSessionId, setPanelSessionTitle, setPanelOpen]); useEffect(() => { diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index bfa197f..e12c78b 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -36,11 +36,13 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal const [currentProviderId, setCurrentProviderId] = useState(providerId || (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') : null) || ''); // Sync model/provider when session data loads (props update after async fetch) + // Unconditional: when modelName is empty (old session with no saved model), + // fall back to localStorage or default to avoid stale values from previous session. useEffect(() => { - if (modelName) setCurrentModel(modelName); + setCurrentModel(modelName || (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') : null) || 'sonnet'); }, [modelName]); useEffect(() => { - if (providerId) setCurrentProviderId(providerId); + setCurrentProviderId(providerId || (typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') : null) || ''); }, [providerId]); // Stream snapshot from the manager — drives all streaming UI diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx index 3aa2717..202496e 100644 --- a/src/components/chat/MessageList.tsx +++ b/src/components/chat/MessageList.tsx @@ -15,12 +15,21 @@ import { StreamingMessage } from './StreamingMessage'; import { CodePilotLogo } from './CodePilotLogo'; /** - * Scrolls to bottom when streaming starts. + * Scrolls to bottom when streaming starts or new messages are appended. * Must be rendered inside (StickToBottom provider). */ -function ScrollOnStream({ isStreaming }: { isStreaming: boolean }) { +function ScrollOnStream({ isStreaming, messageCount }: { isStreaming: boolean; messageCount: number }) { const { scrollToBottom } = useStickToBottomContext(); const wasStreaming = useRef(false); + const prevCount = useRef(messageCount); + + // Scroll when new messages are appended (covers optimistic user message + assistant completion) + useEffect(() => { + if (messageCount > prevCount.current) { + scrollToBottom(); + } + prevCount.current = messageCount; + }, [messageCount, scrollToBottom]); useEffect(() => { if (isStreaming && !wasStreaming.current) { @@ -116,7 +125,7 @@ export function MessageList({ return ( - + {hasMore && (
diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index 56eb186..aaab10f 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -131,12 +131,20 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { const [hoveredFolder, setHoveredFolder] = useState(null); const [creatingChat, setCreatingChat] = useState(false); + /** Read current model + provider_id from localStorage for new session creation */ + const getCurrentModelAndProvider = useCallback(() => { + const model = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-model') || '' : ''; + const provider_id = typeof window !== 'undefined' ? localStorage.getItem('codepilot:last-provider-id') || '' : ''; + return { model, provider_id }; + }, []); + const handleFolderSelect = useCallback(async (path: string) => { try { + const { model, provider_id } = getCurrentModelAndProvider(); const res = await fetch("/api/chat/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ working_directory: path }), + body: JSON.stringify({ working_directory: path, model, provider_id }), }); if (res.ok) { const data = await res.json(); @@ -146,7 +154,7 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { } catch { // Silently fail } - }, [router]); + }, [router, getCurrentModelAndProvider]); const openFolderPicker = useCallback(async (defaultPath?: string) => { if (isElectron) { @@ -180,10 +188,11 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { return; } + const { model, provider_id } = getCurrentModelAndProvider(); const res = await fetch("/api/chat/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ working_directory: lastDir, model: localStorage.getItem('codepilot:last-model') || '' }), + body: JSON.stringify({ working_directory: lastDir, model, provider_id }), }); if (!res.ok) { // Backend rejected it (e.g. INVALID_DIRECTORY) — prompt user @@ -302,10 +311,11 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { ) => { e.stopPropagation(); try { + const { model, provider_id } = getCurrentModelAndProvider(); const res = await fetch("/api/chat/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ working_directory: workingDirectory }), + body: JSON.stringify({ working_directory: workingDirectory, model, provider_id }), }); if (res.ok) { const data = await res.json(); diff --git a/src/lib/db.ts b/src/lib/db.ts index dc89482..e98fa1e 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -686,6 +686,7 @@ export function createSession( systemPrompt?: string, workingDirectory?: string, mode?: string, + providerId?: string, ): ChatSession { const db = getDb(); const id = crypto.randomBytes(16).toString('hex'); @@ -694,8 +695,8 @@ export function createSession( const projectName = path.basename(wd); db.prepare( - 'INSERT INTO chat_sessions (id, title, created_at, updated_at, model, system_prompt, working_directory, sdk_session_id, project_name, status, mode, sdk_cwd) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd); + 'INSERT INTO chat_sessions (id, title, created_at, updated_at, model, system_prompt, working_directory, sdk_session_id, project_name, status, mode, sdk_cwd, provider_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd, providerId || ''); return getSession(id)!; } diff --git a/src/types/index.ts b/src/types/index.ts index 4bf3da4..260d91e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -174,6 +174,7 @@ export interface CreateSessionRequest { system_prompt?: string; working_directory?: string; mode?: string; + provider_id?: string; } export interface SendMessageRequest {