fix: 模型切换串台 + 发送消息后滚动不到底部

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 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-02 12:01:10 +08:00
1 parent b5e5645702
commit c9f1b62809
7 files changed
+40 -11

No files matched your search

+4 -2
View File
@@ -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
+12 -3
View File
@@ -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 <Conversation> (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 (
<Conversation>
<ScrollOnStream isStreaming={isStreaming} />
<ScrollOnStream isStreaming={isStreaming} messageCount={messages.length} />
<ConversationContent className="mx-auto max-w-3xl px-4 py-6 gap-6">
{hasMore && (
<div className="flex justify-center">
+14 -4
View File
@@ -131,12 +131,20 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
const [hoveredFolder, setHoveredFolder] = useState<string | null>(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();