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:
1 parent
b5e5645702
commit
c9f1b62809
7 files changed
+40
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user