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
@@ -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 });
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+3
-2
@@ -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)!;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface CreateSessionRequest {
|
||||
system_prompt?: string;
|
||||
working_directory?: string;
|
||||
mode?: string;
|
||||
provider_id?: string;
|
||||
}
|
||||
|
||||
export interface SendMessageRequest {
|
||||
|
||||
Reference in new issue
Block a user