mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 15:14:21 +01:00
Retain session through view changes (#1580)
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
55
ui/desktop/src/hooks/useChat.ts
Normal file
55
ui/desktop/src/hooks/useChat.ts
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user