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 {