fix: we load extensions when agent starts so don't do it up front (#6350)
This commit is contained in:
@@ -477,6 +477,11 @@ impl ExtensionManager {
|
||||
pub async fn add_extension(&self, config: ExtensionConfig) -> ExtensionResult<()> {
|
||||
let config_name = config.key().to_string();
|
||||
let sanitized_name = normalize(config_name.clone());
|
||||
|
||||
if self.extensions.lock().await.contains_key(&sanitized_name) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut temp_dir = None;
|
||||
|
||||
let client: Box<dyn McpClientTrait> = match &config {
|
||||
|
||||
+6
-42
@@ -39,7 +39,7 @@ import PermissionSettingsView from './components/settings/permission/PermissionS
|
||||
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
|
||||
import RecipesView from './components/recipes/RecipesView';
|
||||
import { View, ViewOptions } from './utils/navigationUtils';
|
||||
import { NoProviderOrModelError, useAgent } from './hooks/useAgent';
|
||||
|
||||
import { useNavigation } from './hooks/useNavigation';
|
||||
import { errorMessage } from './utils/conversionUtils';
|
||||
import { usePageViewTracking } from './hooks/useAnalytics';
|
||||
@@ -51,10 +51,10 @@ function PageViewTracker() {
|
||||
}
|
||||
|
||||
// Route Components
|
||||
const HubRouteWrapper = ({ isExtensionsLoading }: { isExtensionsLoading: boolean }) => {
|
||||
const HubRouteWrapper = () => {
|
||||
const setView = useNavigation();
|
||||
|
||||
return <Hub setView={setView} isExtensionsLoading={isExtensionsLoading} />;
|
||||
return <Hub setView={setView} />;
|
||||
};
|
||||
|
||||
const PairRouteWrapper = ({
|
||||
@@ -363,15 +363,12 @@ const ExtensionsRoute = () => {
|
||||
|
||||
export function AppInner() {
|
||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||
const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null);
|
||||
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
|
||||
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
|
||||
const [isExtensionsLoading, setIsExtensionsLoading] = useState(false);
|
||||
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const setView = useNavigation();
|
||||
const location = useLocation();
|
||||
|
||||
const [chat, setChat] = useState<ChatType>({
|
||||
sessionId: '',
|
||||
@@ -384,7 +381,6 @@ export function AppInner() {
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
|
||||
const { addExtension } = useConfig();
|
||||
const { loadCurrentChat } = useAgent();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Sending reactReady signal to Electron');
|
||||
@@ -398,28 +394,6 @@ export function AppInner() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle URL parameters and deeplinks on app startup
|
||||
const loadingHub = location.pathname === '/';
|
||||
useEffect(() => {
|
||||
if (loadingHub) {
|
||||
(async () => {
|
||||
try {
|
||||
const loadedChat = await loadCurrentChat({
|
||||
setAgentWaitingMessage,
|
||||
setIsExtensionsLoading,
|
||||
});
|
||||
setChat(loadedChat);
|
||||
} catch (e) {
|
||||
if (e instanceof NoProviderOrModelError) {
|
||||
// the onboarding flow will trigger
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [loadCurrentChat, setAgentWaitingMessage, navigate, loadingHub, setChat]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const link = args[0] as string;
|
||||
@@ -616,18 +590,13 @@ export function AppInner() {
|
||||
path="/"
|
||||
element={
|
||||
<ProviderGuard didSelectProvider={didSelectProvider}>
|
||||
<ChatProvider
|
||||
chat={chat}
|
||||
setChat={setChat}
|
||||
contextKey="hub"
|
||||
agentWaitingMessage={agentWaitingMessage}
|
||||
>
|
||||
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
|
||||
<AppLayout />
|
||||
</ChatProvider>
|
||||
</ProviderGuard>
|
||||
}
|
||||
>
|
||||
<Route index element={<HubRouteWrapper isExtensionsLoading={isExtensionsLoading} />} />
|
||||
<Route index element={<HubRouteWrapper />} />
|
||||
<Route
|
||||
path="pair"
|
||||
element={
|
||||
@@ -643,12 +612,7 @@ export function AppInner() {
|
||||
<Route
|
||||
path="extensions"
|
||||
element={
|
||||
<ChatProvider
|
||||
chat={chat}
|
||||
setChat={setChat}
|
||||
contextKey="extensions"
|
||||
agentWaitingMessage={agentWaitingMessage}
|
||||
>
|
||||
<ChatProvider chat={chat} setChat={setChat} contextKey="extensions">
|
||||
<ExtensionsRoute />
|
||||
</ChatProvider>
|
||||
}
|
||||
|
||||
@@ -96,7 +96,6 @@ interface ChatInputProps {
|
||||
initialPrompt?: string;
|
||||
toolCount: number;
|
||||
append?: (message: Message) => void;
|
||||
isExtensionsLoading?: boolean;
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
@@ -121,7 +120,6 @@ export default function ChatInput({
|
||||
initialPrompt,
|
||||
toolCount,
|
||||
append: _append,
|
||||
isExtensionsLoading = false,
|
||||
}: ChatInputProps) {
|
||||
const [_value, setValue] = useState(initialValue);
|
||||
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
|
||||
@@ -1109,8 +1107,7 @@ export default function ChatInput({
|
||||
isAnyImageLoading ||
|
||||
isAnyDroppedFileLoading ||
|
||||
isRecording ||
|
||||
isTranscribing ||
|
||||
isExtensionsLoading;
|
||||
isTranscribing;
|
||||
|
||||
// Queue management functions - no storage persistence, only in-memory
|
||||
const handleRemoveQueuedMessage = (messageId: string) => {
|
||||
@@ -1353,17 +1350,15 @@ export default function ChatInput({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isExtensionsLoading
|
||||
? 'Loading extensions...'
|
||||
: isAnyImageLoading
|
||||
? 'Waiting for images to save...'
|
||||
: isAnyDroppedFileLoading
|
||||
? 'Processing dropped files...'
|
||||
: isRecording
|
||||
? 'Recording...'
|
||||
: isTranscribing
|
||||
? 'Transcribing...'
|
||||
: 'Send'}
|
||||
{isAnyImageLoading
|
||||
? 'Waiting for images to save...'
|
||||
: isAnyDroppedFileLoading
|
||||
? 'Processing dropped files...'
|
||||
: isRecording
|
||||
? 'Recording...'
|
||||
: isTranscribing
|
||||
? 'Transcribing...'
|
||||
: 'Send'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -23,10 +23,8 @@ import { startNewSession } from '../sessions';
|
||||
|
||||
export default function Hub({
|
||||
setView,
|
||||
isExtensionsLoading,
|
||||
}: {
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
isExtensionsLoading: boolean;
|
||||
}) {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
const customEvent = e as unknown as CustomEvent;
|
||||
@@ -59,7 +57,6 @@ export default function Hub({
|
||||
messages={[]}
|
||||
disableAnimation={false}
|
||||
sessionCosts={undefined}
|
||||
isExtensionsLoading={isExtensionsLoading}
|
||||
toolCount={0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ interface ChatContextType {
|
||||
clearRecipe: () => void;
|
||||
// Context identification
|
||||
contextKey: string; // 'hub' or 'pair-{sessionId}'
|
||||
agentWaitingMessage: string | null;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||
@@ -24,14 +23,12 @@ interface ChatProviderProps {
|
||||
chat: ChatType;
|
||||
setChat: (chat: ChatType) => void;
|
||||
contextKey?: string; // Optional context key, defaults to 'hub'
|
||||
agentWaitingMessage: string | null;
|
||||
}
|
||||
|
||||
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
||||
children,
|
||||
chat,
|
||||
setChat,
|
||||
agentWaitingMessage,
|
||||
contextKey = 'hub',
|
||||
}) => {
|
||||
const resetChat = () => {
|
||||
@@ -69,7 +66,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
||||
setRecipe,
|
||||
clearRecipe,
|
||||
contextKey,
|
||||
agentWaitingMessage,
|
||||
};
|
||||
|
||||
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useConfig } from '../components/ConfigContext';
|
||||
import { ChatType } from '../types/chat';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
import {
|
||||
backupConfig,
|
||||
initConfig,
|
||||
readAllConfig,
|
||||
Recipe,
|
||||
recoverConfig,
|
||||
resumeAgent,
|
||||
startAgent,
|
||||
validateConfig,
|
||||
} from '../api';
|
||||
|
||||
export enum AgentState {
|
||||
UNINITIALIZED = 'uninitialized',
|
||||
INITIALIZING = 'initializing',
|
||||
NO_PROVIDER = 'no_provider',
|
||||
INITIALIZED = 'initialized',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export interface InitializationContext {
|
||||
recipe?: Recipe;
|
||||
resumeSessionId?: string;
|
||||
setAgentWaitingMessage: (msg: string | null) => void;
|
||||
setIsExtensionsLoading?: (isLoading: boolean) => void;
|
||||
}
|
||||
|
||||
interface UseAgentReturn {
|
||||
agentState: AgentState;
|
||||
resetChat: () => void;
|
||||
loadCurrentChat: (context: InitializationContext) => Promise<ChatType>;
|
||||
}
|
||||
|
||||
export class NoProviderOrModelError extends Error {
|
||||
constructor() {
|
||||
super('No provider or model configured');
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export function useAgent(): UseAgentReturn {
|
||||
const [agentState, setAgentState] = useState<AgentState>(AgentState.UNINITIALIZED);
|
||||
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||
const initPromiseRef = useRef<Promise<ChatType> | null>(null);
|
||||
const deletedSessionsRef = useRef<Set<string>>(new Set());
|
||||
const recipeIdFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('recipeId') as string | null | undefined) ?? null
|
||||
);
|
||||
const recipeDeeplinkFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('recipeDeeplink') as string | null | undefined) ?? null
|
||||
);
|
||||
const scheduledJobIdFromConfig = useRef<string | null>(
|
||||
(window.appConfig.get('scheduledJobId') as string | null | undefined) ?? null
|
||||
);
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
|
||||
const resetChat = useCallback(() => {
|
||||
setSessionId(null);
|
||||
setAgentState(AgentState.UNINITIALIZED);
|
||||
recipeIdFromConfig.current = null;
|
||||
recipeDeeplinkFromConfig.current = null;
|
||||
scheduledJobIdFromConfig.current = null;
|
||||
deletedSessionsRef.current.clear();
|
||||
}, []);
|
||||
|
||||
const agentIsInitialized = agentState === AgentState.INITIALIZED;
|
||||
const currentChat = useCallback(
|
||||
async (initContext: InitializationContext): Promise<ChatType> => {
|
||||
// Skip deleted sessions
|
||||
if (
|
||||
initContext.resumeSessionId &&
|
||||
deletedSessionsRef.current.has(initContext.resumeSessionId)
|
||||
) {
|
||||
initContext.resumeSessionId = undefined;
|
||||
|
||||
// Clear from URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('resumeSessionId');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
if (sessionId && deletedSessionsRef.current.has(sessionId)) {
|
||||
setSessionId(null);
|
||||
}
|
||||
|
||||
if (agentIsInitialized && sessionId && !deletedSessionsRef.current.has(sessionId)) {
|
||||
let agentResponse;
|
||||
try {
|
||||
agentResponse = await resumeAgent({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
load_model_and_extensions: false,
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
} catch {
|
||||
// Mark session as deleted and clear state
|
||||
deletedSessionsRef.current.add(sessionId);
|
||||
setSessionId(null);
|
||||
|
||||
// Clear from URL
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.get('resumeSessionId')) {
|
||||
url.searchParams.delete('resumeSessionId');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through to create new session
|
||||
if (agentResponse?.data) {
|
||||
const agentSession = agentResponse.data;
|
||||
const messages = agentSession.conversation || [];
|
||||
return {
|
||||
sessionId: agentSession.id,
|
||||
name: agentSession.recipe?.title || agentSession.name,
|
||||
messages,
|
||||
recipe: agentSession.recipe,
|
||||
recipeParameterValues: agentSession.user_recipe_values || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (initPromiseRef.current) {
|
||||
return initPromiseRef.current;
|
||||
}
|
||||
|
||||
const initPromise = (async () => {
|
||||
setAgentState(AgentState.INITIALIZING);
|
||||
const agentWaitingMessage = initContext.setAgentWaitingMessage;
|
||||
agentWaitingMessage('Agent is initializing');
|
||||
|
||||
try {
|
||||
const config = window.electron.getConfig();
|
||||
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
if (!provider || !model) {
|
||||
setAgentState(AgentState.NO_PROVIDER);
|
||||
throw new NoProviderOrModelError();
|
||||
}
|
||||
|
||||
let agentResponse;
|
||||
try {
|
||||
agentResponse = initContext.resumeSessionId
|
||||
? await resumeAgent({
|
||||
body: {
|
||||
session_id: initContext.resumeSessionId,
|
||||
load_model_and_extensions: false,
|
||||
},
|
||||
throwOnError: true,
|
||||
})
|
||||
: await startAgent({
|
||||
body: {
|
||||
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
|
||||
...buildRecipeInput(
|
||||
initContext.recipe,
|
||||
recipeIdFromConfig.current,
|
||||
recipeDeeplinkFromConfig.current
|
||||
),
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
} catch (error) {
|
||||
// If resuming fails, mark session as deleted and create new agent
|
||||
if (initContext.resumeSessionId) {
|
||||
deletedSessionsRef.current.add(initContext.resumeSessionId);
|
||||
|
||||
// Clear from URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('resumeSessionId');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
|
||||
agentResponse = await startAgent({
|
||||
body: {
|
||||
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
|
||||
...buildRecipeInput(
|
||||
initContext.recipe,
|
||||
recipeIdFromConfig.current,
|
||||
recipeDeeplinkFromConfig.current
|
||||
),
|
||||
},
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
// Clear resume flag
|
||||
initContext.resumeSessionId = undefined;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const agentSession = agentResponse.data;
|
||||
if (!agentSession) {
|
||||
throw Error('Failed to get session info');
|
||||
}
|
||||
setSessionId(agentSession.id);
|
||||
|
||||
if (!initContext.recipe && agentSession.recipe && scheduledJobIdFromConfig.current) {
|
||||
agentSession.recipe = {
|
||||
...agentSession.recipe,
|
||||
scheduledJobId: scheduledJobIdFromConfig.current,
|
||||
isScheduledExecution: true,
|
||||
} as Recipe;
|
||||
scheduledJobIdFromConfig.current = null;
|
||||
}
|
||||
|
||||
recipeIdFromConfig.current = null;
|
||||
recipeDeeplinkFromConfig.current = null;
|
||||
|
||||
agentWaitingMessage('Agent is loading config');
|
||||
|
||||
await initConfig();
|
||||
|
||||
try {
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch (error) {
|
||||
console.warn('Initial config read failed, attempting recovery:', error);
|
||||
await handleConfigRecovery();
|
||||
}
|
||||
|
||||
agentWaitingMessage('Extensions are loading');
|
||||
|
||||
const recipeForInit = initContext.recipe || agentSession.recipe || undefined;
|
||||
await initializeSystem(agentSession.id, provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setIsExtensionsLoading: initContext.setIsExtensionsLoading,
|
||||
recipeParameters: agentSession.user_recipe_values,
|
||||
recipe: recipeForInit,
|
||||
});
|
||||
|
||||
const recipe = initContext.recipe || agentSession.recipe;
|
||||
const conversation = agentSession.conversation || [];
|
||||
// If we're loading a recipe from initContext (new recipe load), start with empty messages
|
||||
// Otherwise, use the messages from the session
|
||||
const messages = initContext.recipe && !initContext.resumeSessionId ? [] : conversation;
|
||||
let initChat: ChatType = {
|
||||
sessionId: agentSession.id,
|
||||
name: agentSession.recipe?.title || agentSession.name,
|
||||
messages: messages,
|
||||
recipe: recipe,
|
||||
recipeParameterValues: agentSession.user_recipe_values || null,
|
||||
};
|
||||
|
||||
setAgentState(AgentState.INITIALIZED);
|
||||
|
||||
return initChat;
|
||||
} catch (error) {
|
||||
if (
|
||||
(error + '').includes('Failed to create provider') ||
|
||||
error instanceof NoProviderOrModelError
|
||||
) {
|
||||
setAgentState(AgentState.NO_PROVIDER);
|
||||
throw error;
|
||||
}
|
||||
setAgentState(AgentState.ERROR);
|
||||
if (typeof error === 'object' && error !== null && 'message' in error) {
|
||||
let error_message = error.message as string;
|
||||
throw new Error(error_message);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
agentWaitingMessage(null);
|
||||
initPromiseRef.current = null;
|
||||
}
|
||||
})();
|
||||
|
||||
initPromiseRef.current = initPromise;
|
||||
return initPromise;
|
||||
},
|
||||
[agentIsInitialized, sessionId, read, getExtensions, addExtension]
|
||||
);
|
||||
|
||||
return {
|
||||
agentState,
|
||||
resetChat,
|
||||
loadCurrentChat: currentChat,
|
||||
};
|
||||
}
|
||||
|
||||
const handleConfigRecovery = async () => {
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
|
||||
if (shouldMigrateExtensions) {
|
||||
try {
|
||||
await backupConfig({ throwOnError: true });
|
||||
await initConfig();
|
||||
} catch (migrationError) {
|
||||
console.error('Migration failed:', migrationError);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await validateConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
try {
|
||||
await recoverConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.warn('Config recovery failed, reinitializing...');
|
||||
await initConfig();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildRecipeInput = (
|
||||
recipeOverride?: Recipe,
|
||||
recipeId?: string | null,
|
||||
recipeDeeplink?: string | null
|
||||
) => {
|
||||
if (recipeId) {
|
||||
return { recipe_id: recipeId };
|
||||
}
|
||||
|
||||
if (recipeDeeplink) {
|
||||
return { recipe_deeplink: recipeDeeplink };
|
||||
}
|
||||
|
||||
if (recipeOverride) {
|
||||
return { recipe: recipeOverride };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
Reference in New Issue
Block a user