Retain session through view changes (#1580)

This commit is contained in:
Matthew Diamant
2025-03-10 11:04:54 -07:00
committed by GitHub
parent f49efbbb86
commit c95a15a38c
4 changed files with 76 additions and 80 deletions

View File

@@ -21,6 +21,7 @@ import MoreModelsView from './components/settings/models/MoreModelsView';
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView'; import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
import SessionsView from './components/sessions/SessionsView'; import SessionsView from './components/sessions/SessionsView';
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage'; import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
import { useChat } from './hooks/useChat';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
@@ -151,38 +152,7 @@ export default function App() {
setupStoredProvider(); setupStoredProvider();
}, []); }, []);
// Check for resumeSessionId in URL parameters const { chat, setChat } = useChat({ setView, setIsLoadingSession });
useEffect(() => {
const checkForResumeSession = async () => {
const urlParams = new URLSearchParams(window.location.search);
const resumeSessionId = urlParams.get('resumeSessionId');
if (!resumeSessionId) {
return;
}
setIsLoadingSession(true);
try {
const sessionDetails = await fetchSessionDetails(resumeSessionId);
// Only set view if we have valid session details
if (sessionDetails && sessionDetails.session_id) {
setView('chat', {
resumedSession: sessionDetails,
});
} else {
console.error('Invalid session details received');
}
} catch (error) {
console.error('Failed to fetch session details:', error);
} finally {
// Always clear the loading state
setIsLoadingSession(false);
}
};
checkForResumeSession();
}, []);
useEffect(() => { useEffect(() => {
const handleFatalError = (_: any, errorMessage: string) => { const handleFatalError = (_: any, errorMessage: string) => {
@@ -233,6 +203,13 @@ export default function App() {
return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />; return <ErrorScreen error={fatalError} onReload={() => window.electron.reloadApp()} />;
} }
if (isLoadingSession)
return (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
</div>
);
return ( return (
<> <>
<ToastContainer <ToastContainer
@@ -300,16 +277,12 @@ export default function App() {
)} )}
{view === 'chat' && !isLoadingSession && ( {view === 'chat' && !isLoadingSession && (
<ChatView <ChatView
chat={chat}
setChat={setChat}
setView={setView} setView={setView}
viewOptions={viewOptions}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/> />
)} )}
{view === 'chat' && isLoadingSession && (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
</div>
)}
{view === 'sessions' && <SessionsView setView={setView} />} {view === 'sessions' && <SessionsView setView={setView} />}
</div> </div>
</div> </div>

View File

@@ -27,7 +27,7 @@ import {
} from '../types/message'; } from '../types/message';
export interface ChatType { export interface ChatType {
id: number; id: string;
title: string; title: string;
// messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests // messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests
// anything before this index should not render any buttons, but anything after should // anything before this index should not render any buttons, but anything after should
@@ -36,40 +36,16 @@ export interface ChatType {
} }
export default function ChatView({ export default function ChatView({
chat,
setChat,
setView, setView,
viewOptions,
setIsGoosehintsModalOpen, setIsGoosehintsModalOpen,
}: { }: {
chat: ChatType;
setChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: Record<any, any>) => void; setView: (view: View, viewOptions?: Record<any, any>) => void;
viewOptions?: Record<any, any>;
setIsGoosehintsModalOpen: (isOpen: boolean) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) { }) {
// Check if we're resuming a session
const resumedSession = viewOptions?.resumedSession;
// Generate or retrieve session ID
// The session ID should not change for the duration of the chat
const sessionId = resumedSession?.session_id || generateSessionId();
const [chat, setChat] = useState<ChatType>(() => {
// If resuming a session, convert the session messages to our format
if (resumedSession) {
return {
id: resumedSession.session_id,
title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`,
messages: resumedSession.messages,
messageHistoryIndex: resumedSession.messages.length,
};
}
return {
id: sessionId,
title: 'New Chat',
messages: [],
messageHistoryIndex: 0,
};
});
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({}); const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false); const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now()); const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
@@ -89,8 +65,8 @@ export default function ChatView({
handleSubmit: _submitMessage, handleSubmit: _submitMessage,
} = useMessageStream({ } = useMessageStream({
api: getApiUrl('/reply'), api: getApiUrl('/reply'),
initialMessages: resumedSession ? resumedSession.messages : chat?.messages || [], initialMessages: chat.messages,
body: { session_id: sessionId, session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR') }, body: { session_id: chat.id, session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR') },
onFinish: async (message, _reason) => { onFinish: async (message, _reason) => {
window.electron.stopPowerSaveBlocker(); window.electron.stopPowerSaveBlocker();
@@ -122,7 +98,7 @@ export default function ChatView({
const updatedChat = { ...prevChat, messages }; const updatedChat = { ...prevChat, messages };
return updatedChat; return updatedChat;
}); });
}, [messages, sessionId, resumedSession]); }, [messages]);
useEffect(() => { useEffect(() => {
if (messages.length > 0) { if (messages.length > 0) {

View File

@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
import { ChatType } from '../components/ChatView';
import { fetchSessionDetails, generateSessionId } from '../sessions';
type UseChatArgs = {
setIsLoadingSession: (isLoading: boolean) => void;
setView: (view: string) => void;
};
export const useChat = ({ setIsLoadingSession, setView }: UseChatArgs) => {
const [chat, setChat] = useState<ChatType>({
id: generateSessionId(),
title: 'New Chat',
messages: [],
messageHistoryIndex: 0,
});
// Check for resumeSessionId in URL parameters
useEffect(() => {
const checkForResumeSession = async () => {
const urlParams = new URLSearchParams(window.location.search);
const resumeSessionId = urlParams.get('resumeSessionId');
if (!resumeSessionId) {
return;
}
setIsLoadingSession(true);
try {
const sessionDetails = await fetchSessionDetails(resumeSessionId);
// Only set view if we have valid session details
if (sessionDetails && sessionDetails.session_id) {
setChat({
id: sessionDetails.session_id,
title: sessionDetails.metadata?.description || `ID: ${sessionDetails.session_id}`,
messages: sessionDetails.messages,
messageHistoryIndex: sessionDetails.messages.length,
});
setView('chat');
} else {
console.error('Invalid session details received');
}
} catch (error) {
console.error('Failed to fetch session details:', error);
} finally {
// Always clear the loading state
setIsLoadingSession(false);
}
};
checkForResumeSession();
}, []);
return { chat, setChat };
};

View File

@@ -1,4 +1,5 @@
import { getApiUrl, getSecretKey } from './config'; import { getApiUrl, getSecretKey } from './config';
import { Message } from './types/message';
export interface SessionMetadata { export interface SessionMetadata {
description: string; description: string;
@@ -28,19 +29,10 @@ export interface SessionsResponse {
sessions: Session[]; sessions: Session[];
} }
export interface SessionMessage {
role: 'user' | 'assistant';
created: number;
content: {
type: string;
text: string;
}[];
}
export interface SessionDetails { export interface SessionDetails {
session_id: string; session_id: string;
metadata: SessionMetadata; metadata: SessionMetadata;
messages: SessionMessage[]; messages: Message[];
} }
/** /**