mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Move out app init (#4185)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -104,6 +104,7 @@ vi.mock('./contexts/ChatContext', () => ({
|
||||
recipeConfig: null,
|
||||
},
|
||||
setChat: vi.fn(),
|
||||
setPairChat: vi.fn(), // Keep this from HEAD
|
||||
resetChat: vi.fn(),
|
||||
hasActiveSession: false,
|
||||
setRecipeConfig: vi.fn(),
|
||||
@@ -115,6 +116,7 @@ vi.mock('./contexts/ChatContext', () => ({
|
||||
clearDraft: vi.fn(),
|
||||
contextKey: 'hub',
|
||||
}),
|
||||
DEFAULT_CHAT_TITLE: 'New Chat', // Keep this from HEAD
|
||||
}));
|
||||
|
||||
vi.mock('./contexts/DraftContext', () => ({
|
||||
@@ -137,19 +139,6 @@ vi.mock('./components/AnnouncementModal', () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('./hooks/useChat', () => ({
|
||||
useChat: () => ({
|
||||
chat: {
|
||||
id: 'test-id',
|
||||
title: 'Test Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipeConfig: null,
|
||||
},
|
||||
setChat: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock react-router-dom to avoid HashRouter issues in tests
|
||||
vi.mock('react-router-dom', () => ({
|
||||
HashRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
|
||||
+42
-497
@@ -3,14 +3,11 @@ import { IpcRendererEvent } from 'electron';
|
||||
import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { openSharedSessionFromDeepLink, type SessionLinksViewOptions } from './sessionLinks';
|
||||
import { type SharedSessionDetails } from './sharedSessions';
|
||||
import { initializeSystem } from './utils/providerUtils';
|
||||
import { initializeCostDatabase } from './utils/costDatabase';
|
||||
import { ErrorUI } from './components/ErrorBoundary';
|
||||
import { ConfirmationModal } from './components/ui/ConfirmationModal';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { extractExtensionName } from './components/settings/extensions/utils';
|
||||
import { GoosehintsModal } from './components/GoosehintsModal';
|
||||
import { type ExtensionConfig } from './extensions';
|
||||
import AnnouncementModal from './components/AnnouncementModal';
|
||||
import { generateSessionId } from './sessions';
|
||||
import ProviderGuard from './components/ProviderGuard';
|
||||
@@ -29,18 +26,10 @@ import { ChatProvider } from './contexts/ChatContext';
|
||||
import { DraftProvider } from './contexts/DraftContext';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig, MalformedConfigError } from './components/ConfigContext';
|
||||
import { useConfig } from './components/ConfigContext';
|
||||
import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
|
||||
import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings/extensions';
|
||||
import {
|
||||
backupConfig,
|
||||
initConfig,
|
||||
readAllConfig,
|
||||
recoverConfig,
|
||||
validateConfig,
|
||||
} from './api/sdk.gen';
|
||||
import PermissionSettingsView from './components/settings/permission/PermissionSetting';
|
||||
import { COST_TRACKING_ENABLED } from './updates';
|
||||
|
||||
import { type SessionDetails } from './sessions';
|
||||
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
|
||||
@@ -48,52 +37,9 @@ import { Recipe } from './recipe';
|
||||
import RecipesView from './components/RecipesView';
|
||||
import RecipeEditor from './components/RecipeEditor';
|
||||
|
||||
export type View =
|
||||
| 'welcome'
|
||||
| 'chat'
|
||||
| 'pair'
|
||||
| 'settings'
|
||||
| 'extensions'
|
||||
| 'moreModels'
|
||||
| 'configureProviders'
|
||||
| 'configPage'
|
||||
| 'ConfigureProviders'
|
||||
| 'settingsV2'
|
||||
| 'sessions'
|
||||
| 'schedules'
|
||||
| 'sharedSession'
|
||||
| 'loading'
|
||||
| 'recipeEditor'
|
||||
| 'recipes'
|
||||
| 'permission';
|
||||
|
||||
export type ViewOptions = {
|
||||
// Settings view options
|
||||
extensionId?: string;
|
||||
showEnvVars?: boolean;
|
||||
deepLinkConfig?: ExtensionConfig;
|
||||
|
||||
// Session view options
|
||||
resumedSession?: SessionDetails;
|
||||
sessionDetails?: SessionDetails;
|
||||
error?: string;
|
||||
shareToken?: string;
|
||||
baseUrl?: string;
|
||||
|
||||
// Recipe editor options
|
||||
config?: unknown;
|
||||
|
||||
// Permission view options
|
||||
parentView?: View;
|
||||
|
||||
// Generic options
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type ViewConfig = {
|
||||
view: View;
|
||||
viewOptions?: ViewOptions;
|
||||
};
|
||||
// Import the new modules
|
||||
import { createNavigationHandler, View, ViewOptions } from './utils/navigationUtils';
|
||||
import { initializeApp } from './utils/appInitialization';
|
||||
|
||||
// Route Components
|
||||
const HubRouteWrapper = ({
|
||||
@@ -108,6 +54,7 @@ const HubRouteWrapper = ({
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const setView = createNavigationHandler(navigate);
|
||||
|
||||
return (
|
||||
<Hub
|
||||
@@ -115,46 +62,7 @@ const HubRouteWrapper = ({
|
||||
chat={chat}
|
||||
setChat={setChat}
|
||||
setPairChat={setPairChat}
|
||||
setView={(view: View, options?: ViewOptions) => {
|
||||
// Convert view to route navigation
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/');
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions');
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules');
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes');
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers');
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome');
|
||||
break;
|
||||
default:
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
setView={setView}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
/>
|
||||
);
|
||||
@@ -174,6 +82,7 @@ const PairRouteWrapper = ({
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const chatRef = useRef(chat);
|
||||
const setView = createNavigationHandler(navigate);
|
||||
|
||||
// Keep the ref updated with the current chat state
|
||||
useEffect(() => {
|
||||
@@ -273,46 +182,7 @@ const PairRouteWrapper = ({
|
||||
<Pair
|
||||
chat={chat}
|
||||
setChat={setChat}
|
||||
setView={(view: View, options?: ViewOptions) => {
|
||||
// Convert view to route navigation
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/');
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions');
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules');
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes');
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers');
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome');
|
||||
break;
|
||||
default:
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
setView={setView}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
/>
|
||||
);
|
||||
@@ -321,105 +191,19 @@ const PairRouteWrapper = ({
|
||||
const SettingsRoute = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const setView = createNavigationHandler(navigate);
|
||||
|
||||
// Get viewOptions from location.state or history.state
|
||||
const viewOptions =
|
||||
(location.state as SettingsViewOptions) || (window.history.state as SettingsViewOptions) || {};
|
||||
return (
|
||||
<SettingsView
|
||||
onClose={() => navigate('/')}
|
||||
setView={(view: View, options?: ViewOptions) => {
|
||||
// Convert view to route navigation
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/');
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair');
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions');
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules');
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes');
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers');
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome');
|
||||
break;
|
||||
default:
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
viewOptions={viewOptions}
|
||||
/>
|
||||
);
|
||||
return <SettingsView onClose={() => navigate('/')} setView={setView} viewOptions={viewOptions} />;
|
||||
};
|
||||
|
||||
const SessionsRoute = () => {
|
||||
const navigate = useNavigate();
|
||||
const setView = createNavigationHandler(navigate);
|
||||
|
||||
return (
|
||||
<SessionsView
|
||||
setView={(view: View, options?: ViewOptions) => {
|
||||
// Convert view to route navigation
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/', { state: options });
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions');
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules');
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes');
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers');
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome');
|
||||
break;
|
||||
default:
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <SessionsView setView={setView} />;
|
||||
};
|
||||
|
||||
const SchedulesRoute = () => {
|
||||
@@ -548,6 +332,7 @@ const SharedSessionRouteWrapper = ({
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const setView = createNavigationHandler(navigate);
|
||||
|
||||
const historyState = window.history.state;
|
||||
const sessionDetails = (location.state?.sessionDetails ||
|
||||
@@ -565,47 +350,7 @@ const SharedSessionRouteWrapper = ({
|
||||
if (shareToken && baseUrl) {
|
||||
setIsLoadingSharedSession(true);
|
||||
try {
|
||||
await openSharedSessionFromDeepLink(
|
||||
`goose://sessions/${shareToken}`,
|
||||
(view: View, _options?: SessionLinksViewOptions) => {
|
||||
// Convert view to route navigation
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/', { state: _options });
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: _options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: _options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions');
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules');
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes');
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: _options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers');
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: _options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: _options });
|
||||
break;
|
||||
default:
|
||||
navigate('/');
|
||||
}
|
||||
},
|
||||
baseUrl
|
||||
);
|
||||
await openSharedSessionFromDeepLink(`goose://sessions/${shareToken}`, setView, baseUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to retry loading shared session:', error);
|
||||
} finally {
|
||||
@@ -668,7 +413,7 @@ export default function App() {
|
||||
title: 'Pair Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
recipeConfig: null, // Initialize with no recipe
|
||||
recipeConfig: null,
|
||||
});
|
||||
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
@@ -745,220 +490,24 @@ export default function App() {
|
||||
}
|
||||
initAttemptedRef.current = true;
|
||||
|
||||
console.log(`Initializing app`);
|
||||
const initialize = async () => {
|
||||
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;
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const resumeSessionId = urlParams.get('resumeSessionId');
|
||||
const recipeConfig = window.appConfig.get('recipe');
|
||||
|
||||
// Check for session resume first - this takes priority over other navigation
|
||||
if (resumeSessionId) {
|
||||
console.log('Session resume detected, letting useChat hook handle navigation');
|
||||
|
||||
// Even when resuming a session, we need to initialize the system
|
||||
const initializeForSessionResume = async () => {
|
||||
try {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
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) {
|
||||
await initializeSystem(provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
} else {
|
||||
throw new Error('No provider/model configured for session resume');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize system for session resume:', error);
|
||||
setFatalError(
|
||||
`Failed to initialize system for session resume: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
initializeForSessionResume();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for recipe config - this also needs provider initialization
|
||||
if (recipeConfig && typeof recipeConfig === 'object') {
|
||||
console.log('Recipe deeplink detected, initializing system for recipe');
|
||||
|
||||
const initializeForRecipe = async () => {
|
||||
try {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
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) {
|
||||
await initializeSystem(provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
|
||||
// Set up the recipe in pair chat after system is initialized
|
||||
setPairChat((prevChat) => ({
|
||||
...prevChat,
|
||||
recipeConfig: recipeConfig as Recipe,
|
||||
title: (recipeConfig as Recipe)?.title || 'Recipe Chat',
|
||||
messages: [], // Start fresh for recipe
|
||||
messageHistoryIndex: 0,
|
||||
}));
|
||||
|
||||
// Navigate to pair view
|
||||
window.location.hash = '#/pair';
|
||||
window.history.replaceState(
|
||||
{
|
||||
recipeConfig: recipeConfig,
|
||||
resetChat: true,
|
||||
},
|
||||
'',
|
||||
'#/pair'
|
||||
);
|
||||
} else {
|
||||
throw new Error('No provider/model configured for recipe');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize system for recipe:', error);
|
||||
setFatalError(
|
||||
`Failed to initialize system for recipe: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
initializeForRecipe();
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewType) {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
// Handle recipe editor deep link - use hash routing
|
||||
window.location.hash = '#/recipe-editor';
|
||||
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
|
||||
} else {
|
||||
// Handle other deep links by redirecting to appropriate route
|
||||
const routeMap: Record<string, string> = {
|
||||
chat: '#/',
|
||||
pair: '#/pair',
|
||||
settings: '#/settings',
|
||||
sessions: '#/sessions',
|
||||
schedules: '#/schedules',
|
||||
recipes: '#/recipes',
|
||||
permission: '#/permission',
|
||||
ConfigureProviders: '#/configure-providers',
|
||||
sharedSession: '#/shared-session',
|
||||
recipeEditor: '#/recipe-editor',
|
||||
welcome: '#/welcome',
|
||||
};
|
||||
|
||||
const route = routeMap[viewType];
|
||||
if (route) {
|
||||
window.location.hash = route;
|
||||
window.history.replaceState({}, '', route);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// Start cost database initialization early (non-blocking) - only if cost tracking is enabled
|
||||
const costDbPromise = COST_TRACKING_ENABLED
|
||||
? initializeCostDatabase().catch((error) => {
|
||||
console.error('Failed to initialize cost database:', error);
|
||||
})
|
||||
: (() => {
|
||||
console.log('Cost tracking disabled, skipping cost database initialization');
|
||||
return Promise.resolve();
|
||||
})();
|
||||
|
||||
await initConfig();
|
||||
|
||||
try {
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch (error) {
|
||||
console.warn('Initial config read failed, attempting recovery:', error);
|
||||
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
|
||||
if (shouldMigrateExtensions) {
|
||||
console.log('Performing extension migration...');
|
||||
try {
|
||||
await backupConfig({ throwOnError: true });
|
||||
await initConfig();
|
||||
} catch (migrationError) {
|
||||
console.error('Migration failed:', migrationError);
|
||||
// Continue with recovery attempts
|
||||
}
|
||||
}
|
||||
|
||||
// Try recovery if migration didn't work or wasn't needed
|
||||
console.log('Attempting config recovery...');
|
||||
try {
|
||||
// Try to validate first (faster than recovery)
|
||||
await validateConfig({ throwOnError: true });
|
||||
// If validation passes, try reading again
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.log('Config validation failed, attempting recovery...');
|
||||
try {
|
||||
await recoverConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.warn('Config recovery failed, reinitializing...');
|
||||
await initConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
// Initialize system in parallel with cost database (if enabled)
|
||||
const initPromises = [
|
||||
initializeSystem(provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
}),
|
||||
];
|
||||
|
||||
if (COST_TRACKING_ENABLED) {
|
||||
initPromises.push(costDbPromise);
|
||||
}
|
||||
|
||||
await Promise.all(initPromises);
|
||||
} catch (error) {
|
||||
console.error('Error in system initialization:', error);
|
||||
if (error instanceof MalformedConfigError) {
|
||||
throw error;
|
||||
}
|
||||
window.location.hash = '#/';
|
||||
window.history.replaceState({}, '', '#/');
|
||||
}
|
||||
} else {
|
||||
window.location.hash = '#/';
|
||||
window.history.replaceState({}, '', '#/');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fatal error during initialization:', error);
|
||||
setFatalError(error instanceof Error ? error.message : 'Unknown error occurred');
|
||||
}
|
||||
await initializeApp({
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider: provider as string,
|
||||
model: model as string,
|
||||
});
|
||||
};
|
||||
|
||||
initializeApp();
|
||||
initialize().catch((error) => {
|
||||
console.error('Fatal error during initialization:', error);
|
||||
setFatalError(error instanceof Error ? error.message : 'Unknown error occurred');
|
||||
});
|
||||
}, [getExtensions, addExtension, read, setPairChat]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -975,7 +524,7 @@ export default function App() {
|
||||
|
||||
// Handle navigation to pair view for recipe deeplinks after router is ready
|
||||
useEffect(() => {
|
||||
const recipeConfig = window.appConfig.get('recipe');
|
||||
const recipeConfig = window.appConfig?.get('recipe');
|
||||
if (
|
||||
recipeConfig &&
|
||||
typeof recipeConfig === 'object' &&
|
||||
@@ -1028,7 +577,7 @@ export default function App() {
|
||||
return () => {
|
||||
window.electron.off('open-shared-session', handleOpenSharedSession);
|
||||
};
|
||||
}, [setSharedSessionError]);
|
||||
}, []);
|
||||
|
||||
// Handle recipe decode events from main process
|
||||
useEffect(() => {
|
||||
@@ -1094,7 +643,7 @@ export default function App() {
|
||||
if ((isMac ? event.metaKey : event.ctrlKey) && event.key === 'n') {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const workingDir = window.appConfig.get('GOOSE_WORKING_DIR');
|
||||
const workingDir = window.appConfig?.get('GOOSE_WORKING_DIR');
|
||||
console.log(`Creating new chat window with working dir: ${workingDir}`);
|
||||
window.electron.createChatWindow(undefined, workingDir as string);
|
||||
} catch (error) {
|
||||
@@ -1111,8 +660,7 @@ export default function App() {
|
||||
// Prevent default drag and drop behavior globally to avoid opening files in new windows
|
||||
// but allow our React components to handle drops in designated areas
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const preventDefaults = (e: any) => {
|
||||
const preventDefaults = (e: globalThis.DragEvent) => {
|
||||
// Only prevent default if we're not over a designated drop zone
|
||||
const target = e.target as HTMLElement;
|
||||
const isOverDropZone = target.closest('[data-drop-zone="true"]') !== null;
|
||||
@@ -1123,15 +671,13 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleDragOver = (e: any) => {
|
||||
const handleDragOver = (e: globalThis.DragEvent) => {
|
||||
// Always prevent default for dragover to allow dropping
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleDrop = (e: any) => {
|
||||
const handleDrop = (e: globalThis.DragEvent) => {
|
||||
// Only prevent default if we're not over a designated drop zone
|
||||
const target = e.target as HTMLElement;
|
||||
const isOverDropZone = target.closest('[data-drop-zone="true"]') !== null;
|
||||
@@ -1157,10 +703,9 @@ export default function App() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Setting up fatal error handler');
|
||||
const handleFatalError = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const errorMessage = args[0] as string;
|
||||
console.error('Encountered a fatal error: ', errorMessage);
|
||||
console.error('Encountered a fatal error:', errorMessage);
|
||||
console.error('Is loading session:', isLoadingSession);
|
||||
setFatalError(errorMessage);
|
||||
};
|
||||
@@ -1171,7 +716,6 @@ export default function App() {
|
||||
}, [isLoadingSession]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Setting up view change handler');
|
||||
const handleSetView = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||
const newView = args[0] as View;
|
||||
const section = args[1] as string | undefined;
|
||||
@@ -1232,7 +776,7 @@ export default function App() {
|
||||
try {
|
||||
const allowedCommands = await window.electron.getAllowedExtensions();
|
||||
if (allowedCommands && allowedCommands.length > 0) {
|
||||
const isCommandAllowed = allowedCommands.some((allowedCmd) =>
|
||||
const isCommandAllowed = allowedCommands.some((allowedCmd: string) =>
|
||||
command.startsWith(allowedCmd)
|
||||
);
|
||||
if (!isCommandAllowed) {
|
||||
@@ -1337,12 +881,13 @@ export default function App() {
|
||||
return <ErrorUI error={new Error(fatalError)} />;
|
||||
}
|
||||
|
||||
if (isLoadingSession)
|
||||
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 (
|
||||
<DraftProvider>
|
||||
@@ -1407,7 +952,7 @@ export default function App() {
|
||||
chat={pairChat}
|
||||
setChat={setPairChat}
|
||||
contextKey={`pair-${pairChat.id}`}
|
||||
key={pairChat.id} // Add key prop to force re-render when chat ID changes
|
||||
key={pairChat.id}
|
||||
>
|
||||
<PairRouteWrapper
|
||||
chat={pairChat}
|
||||
@@ -1492,7 +1037,7 @@ export default function App() {
|
||||
</div>
|
||||
{isGoosehintsModalOpen && (
|
||||
<GoosehintsModal
|
||||
directory={window.appConfig.get('GOOSE_WORKING_DIR') as string}
|
||||
directory={window.appConfig?.get('GOOSE_WORKING_DIR') as string}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
ChatContextManagerProvider,
|
||||
useChatContextManager,
|
||||
} from './context_management/ChatContextManager';
|
||||
import { type View, ViewOptions } from '../App';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
import { MainPanelLayout } from './Layout/MainPanelLayout';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect, useMemo } from 'react';
|
||||
import { FolderKey, ScrollText } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
|
||||
import { Button } from './ui/button';
|
||||
import type { View } from '../App';
|
||||
import type { View } from '../utils/navigationUtils';
|
||||
import Stop from './ui/Stop';
|
||||
import { Attach, Send, Close, Microphone } from './icons';
|
||||
import { ChatState } from '../types/chatState';
|
||||
@@ -107,7 +107,9 @@ export default function ChatInput({
|
||||
// Derived state - chatState != Idle means we're in some form of loading state
|
||||
const isLoading = chatState !== ChatState.Idle;
|
||||
const { alerts, addAlert, clearAlerts } = useAlerts();
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
null
|
||||
) as React.RefObject<HTMLDivElement>;
|
||||
const toolCount = useToolCount();
|
||||
const { isLoadingCompaction, handleManualCompaction } = useChatContextManager();
|
||||
const { getProviders, read } = useConfig();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SidebarSeparator,
|
||||
} from '../ui/sidebar';
|
||||
import { ChatSmart, Gear } from '../icons';
|
||||
import { ViewOptions, View } from '../../App';
|
||||
import { ViewOptions, View } from '../../utils/navigationUtils';
|
||||
import { useChatContext } from '../../contexts/ChatContext';
|
||||
import { DEFAULT_CHAT_TITLE } from '../../contexts/ChatContext';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import AppSidebar from '../GooseSidebar/AppSidebar';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import { AppWindowMac, AppWindow } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Sidebar, SidebarInset, SidebarProvider, SidebarTrigger, useSidebar } from '../ui/sidebar';
|
||||
|
||||
@@ -20,7 +20,7 @@ interface SearchBarProps {
|
||||
currentIndex: number;
|
||||
};
|
||||
/** Optional ref for the search input element */
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
/** Initial search term */
|
||||
initialSearchTerm?: string;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,9 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
|
||||
count: number;
|
||||
} | null>(null);
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const searchInputRef: React.RefObject<HTMLInputElement> = React.useRef<HTMLInputElement>(
|
||||
null
|
||||
) as React.RefObject<HTMLInputElement>;
|
||||
const highlighterRef = React.useRef<SearchHighlighter | null>(null);
|
||||
const containerRef = React.useRef<SearchContainerElement | null>(null);
|
||||
const lastSearchRef = React.useRef<{ term: string; caseSensitive: boolean }>({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import ExtensionsSection from '../settings/extensions/ExtensionsSection';
|
||||
import { ExtensionConfig } from '../../api';
|
||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import FlappyGoose from './FlappyGoose';
|
||||
import { type View, ViewOptions } from '../App';
|
||||
|
||||
import { SessionInsights } from './sessions/SessionsInsights';
|
||||
import ChatInput from './ChatInput';
|
||||
import { generateSessionId } from '../sessions';
|
||||
@@ -31,6 +31,7 @@ import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import { ChatType } from '../types/chat';
|
||||
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
|
||||
export default function Hub({
|
||||
chat: _chat,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { type View, ViewOptions } from '../App';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
import BaseChat from './BaseChat';
|
||||
import { useRecipeManager } from '../hooks/useRecipeManager';
|
||||
import { useIsMobile } from '../hooks/use-mobile';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { fetchSessions, updateSessionMetadata, type Session } from '../../sessio
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import { formatMessageTimestamp } from '../../utils/timeUtils';
|
||||
import { SearchView } from '../conversation/SearchView';
|
||||
import { SearchHighlighter } from '../../utils/searchHighlighter';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, ViewOptions } from '../../App';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import { fetchSessionDetails, type SessionDetails } from '../../sessions';
|
||||
import SessionListView from './SessionListView';
|
||||
import SessionHistoryView from './SessionHistoryView';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import type { View, ViewOptions } from '../../App';
|
||||
import { View, ViewOptions } from '../../utils/navigationUtils';
|
||||
import ModelsSection from './models/ModelsSection';
|
||||
import SessionSharingSection from './sessions/SessionSharingSection';
|
||||
import AppSettingsSection from './app/AppSettingsSection';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import type { View } from '../../../App';
|
||||
import { View } from '../../../utils/navigationUtils';
|
||||
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { AddModelModal } from '../subcomponents/AddModelModal';
|
||||
import { LeadWorkerSettings } from '../subcomponents/LeadWorkerSettings';
|
||||
import { View } from '../../../../App';
|
||||
import { View } from '../../../../utils/navigationUtils';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -22,12 +22,13 @@ import { toastSuccess, toastError } from '../../../../toasts';
|
||||
import ViewRecipeModal from '../../../ViewRecipeModal';
|
||||
|
||||
interface ModelsBottomBarProps {
|
||||
dropdownRef: React.RefObject<HTMLDivElement | null>;
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
setView: (view: View) => void;
|
||||
alerts: Alert[];
|
||||
recipeConfig?: Recipe | null;
|
||||
hasMessages?: boolean; // Add prop to know if there are messages to create a recipe from
|
||||
}
|
||||
|
||||
export default function ModelsBottomBar({
|
||||
dropdownRef,
|
||||
setView,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Input } from '../../../ui/input';
|
||||
import { Select } from '../../../ui/Select';
|
||||
import { useConfig } from '../../../ConfigContext';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import type { View } from '../../../../App';
|
||||
import type { View } from '../../../../utils/navigationUtils';
|
||||
import Model, { getProviderMetadata } from '../modelInterface';
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../../../ui/button';
|
||||
import { AddModelModal } from './AddModelModal';
|
||||
import type { View } from '../../../../App';
|
||||
import type { View } from '../../../../utils/navigationUtils';
|
||||
import { shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
|
||||
interface ConfigureModelButtonsProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '../../ui/button';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { View, ViewOptions } from '../../../App';
|
||||
import { View, ViewOptions } from '../../../utils/navigationUtils';
|
||||
|
||||
interface ResetProviderSectionProps {
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ChatType } from '../types/chat';
|
||||
import { fetchSessionDetails, generateSessionId } from '../sessions';
|
||||
import { View, ViewOptions } from '../App';
|
||||
|
||||
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
|
||||
type UseChatArgs = {
|
||||
setIsLoadingSession: (isLoading: boolean) => void;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useChatEngine } from './useChatEngine';
|
||||
@@ -19,30 +22,24 @@ describe('useChatEngine', () => {
|
||||
let mockUseMessageStream: Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock the global window object more completely for the React testing environment
|
||||
const mockWindow = {
|
||||
appConfig: {
|
||||
// Mock the appConfig and electron APIs on the existing window object
|
||||
Object.defineProperty(window, 'appConfig', {
|
||||
value: {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === 'GOOSE_API_HOST') return 'http://localhost';
|
||||
if (key === 'GOOSE_PORT') return '8000';
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
electron: {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {
|
||||
logInfo: vi.fn(),
|
||||
},
|
||||
setTimeout: vi.fn((fn: () => void) => {
|
||||
fn(); // Execute immediately for tests
|
||||
return 123;
|
||||
}),
|
||||
clearTimeout: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
CustomEvent: vi.fn(),
|
||||
// Add basic browser objects required by React Testing Library
|
||||
HTMLElement: class MockHTMLElement {},
|
||||
Event: class MockEvent {},
|
||||
};
|
||||
vi.stubGlobal('window', mockWindow);
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Dynamically import the hook so we can get a reference to the mock
|
||||
const { useMessageStream } = await import('./useMessageStream');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions';
|
||||
import { type View } from './App';
|
||||
import { View } from './utils/navigationUtils';
|
||||
|
||||
export interface SessionLinksViewOptions {
|
||||
sessionDetails?: SharedSessionDetails | null;
|
||||
error?: string;
|
||||
shareToken?: string;
|
||||
baseUrl?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { ChatType } from '../types/chat';
|
||||
import { Recipe } from '../recipe';
|
||||
import { initializeSystem } from './providerUtils';
|
||||
import { initializeCostDatabase } from './costDatabase';
|
||||
import {
|
||||
type ExtensionConfig,
|
||||
type FixedExtensionEntry,
|
||||
MalformedConfigError,
|
||||
} from '../components/ConfigContext';
|
||||
import { backupConfig, initConfig, readAllConfig, recoverConfig, validateConfig } from '../api';
|
||||
import { COST_TRACKING_ENABLED } from '../updates';
|
||||
|
||||
interface InitializationDependencies {
|
||||
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
|
||||
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void;
|
||||
provider: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export const initializeApp = async ({
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
}: InitializationDependencies) => {
|
||||
console.log(`Initializing app`);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const resumeSessionId = urlParams.get('resumeSessionId');
|
||||
const recipeConfig = window.appConfig.get('recipe');
|
||||
|
||||
if (resumeSessionId) {
|
||||
console.log('Session resume detected, letting useChat hook handle navigation');
|
||||
await initializeForSessionResume({ getExtensions, addExtension, provider, model });
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipeConfig && typeof recipeConfig === 'object') {
|
||||
console.log('Recipe deeplink detected, initializing system for recipe');
|
||||
await initializeForRecipe({
|
||||
recipeConfig: recipeConfig as Recipe,
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewType) {
|
||||
handleViewTypeDeepLink(viewType, recipeConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
const costDbPromise = COST_TRACKING_ENABLED
|
||||
? initializeCostDatabase().catch((error) => {
|
||||
console.error('Failed to initialize cost database:', error);
|
||||
})
|
||||
: (() => {
|
||||
console.log('Cost tracking disabled, skipping cost database initialization');
|
||||
return Promise.resolve();
|
||||
})();
|
||||
|
||||
await initConfig();
|
||||
|
||||
try {
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch (error) {
|
||||
console.warn('Initial config read failed, attempting recovery:', error);
|
||||
await handleConfigRecovery();
|
||||
}
|
||||
|
||||
if (provider && model) {
|
||||
try {
|
||||
const initPromises = [
|
||||
initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
}),
|
||||
];
|
||||
|
||||
if (COST_TRACKING_ENABLED) {
|
||||
initPromises.push(costDbPromise);
|
||||
}
|
||||
|
||||
await Promise.all(initPromises);
|
||||
} catch (error) {
|
||||
console.error('Error in system initialization:', error);
|
||||
if (error instanceof MalformedConfigError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.location.hash = '#/';
|
||||
window.history.replaceState({}, '', '#/');
|
||||
};
|
||||
|
||||
const initializeForSessionResume = async ({
|
||||
getExtensions,
|
||||
addExtension,
|
||||
provider,
|
||||
model,
|
||||
}: Pick<InitializationDependencies, 'getExtensions' | 'addExtension' | 'provider' | 'model'>) => {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
};
|
||||
|
||||
const initializeForRecipe = async ({
|
||||
recipeConfig,
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setPairChat,
|
||||
provider,
|
||||
model,
|
||||
}: Pick<
|
||||
InitializationDependencies,
|
||||
'getExtensions' | 'addExtension' | 'setPairChat' | 'provider' | 'model'
|
||||
> & {
|
||||
recipeConfig: Recipe;
|
||||
}) => {
|
||||
await initConfig();
|
||||
await readAllConfig({ throwOnError: true });
|
||||
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
});
|
||||
|
||||
setPairChat((prevChat) => ({
|
||||
...prevChat,
|
||||
recipeConfig: recipeConfig,
|
||||
title: recipeConfig?.title || 'Recipe Chat',
|
||||
messages: [],
|
||||
messageHistoryIndex: 0,
|
||||
}));
|
||||
|
||||
window.location.hash = '#/pair';
|
||||
window.history.replaceState(
|
||||
{
|
||||
recipeConfig: recipeConfig,
|
||||
resetChat: true,
|
||||
},
|
||||
'',
|
||||
'#/pair'
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
window.location.hash = '#/recipe-editor';
|
||||
window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor');
|
||||
} else {
|
||||
const routeMap: Record<string, string> = {
|
||||
chat: '#/',
|
||||
pair: '#/pair',
|
||||
settings: '#/settings',
|
||||
sessions: '#/sessions',
|
||||
schedules: '#/schedules',
|
||||
recipes: '#/recipes',
|
||||
permission: '#/permission',
|
||||
ConfigureProviders: '#/configure-providers',
|
||||
sharedSession: '#/shared-session',
|
||||
recipeEditor: '#/recipe-editor',
|
||||
welcome: '#/welcome',
|
||||
};
|
||||
|
||||
const route = routeMap[viewType];
|
||||
if (route) {
|
||||
window.location.hash = route;
|
||||
window.history.replaceState({}, '', route);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfigRecovery = async () => {
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
|
||||
if (shouldMigrateExtensions) {
|
||||
console.log('Performing extension migration...');
|
||||
try {
|
||||
await backupConfig({ throwOnError: true });
|
||||
await initConfig();
|
||||
} catch (migrationError) {
|
||||
console.error('Migration failed:', migrationError);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Attempting config recovery...');
|
||||
try {
|
||||
await validateConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.log('Config validation failed, attempting recovery...');
|
||||
try {
|
||||
await recoverConfig({ throwOnError: true });
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch {
|
||||
console.warn('Config recovery failed, reinitializing...');
|
||||
await initConfig();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
export type View =
|
||||
| 'welcome'
|
||||
| 'chat'
|
||||
| 'pair'
|
||||
| 'settings'
|
||||
| 'extensions'
|
||||
| 'moreModels'
|
||||
| 'configureProviders'
|
||||
| 'configPage'
|
||||
| 'ConfigureProviders'
|
||||
| 'settingsV2'
|
||||
| 'sessions'
|
||||
| 'schedules'
|
||||
| 'sharedSession'
|
||||
| 'loading'
|
||||
| 'recipeEditor'
|
||||
| 'recipes'
|
||||
| 'permission';
|
||||
|
||||
export type ViewOptions = {
|
||||
extensionId?: string;
|
||||
showEnvVars?: boolean;
|
||||
deepLinkConfig?: unknown;
|
||||
resumedSession?: unknown;
|
||||
sessionDetails?: unknown;
|
||||
error?: string;
|
||||
shareToken?: string;
|
||||
baseUrl?: string;
|
||||
config?: unknown;
|
||||
parentView?: View;
|
||||
parentViewOptions?: ViewOptions;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const createNavigationHandler = (navigate: NavigateFunction) => {
|
||||
return (view: View, options?: ViewOptions) => {
|
||||
switch (view) {
|
||||
case 'chat':
|
||||
navigate('/', { state: options });
|
||||
break;
|
||||
case 'pair':
|
||||
navigate('/pair', { state: options });
|
||||
break;
|
||||
case 'settings':
|
||||
navigate('/settings', { state: options });
|
||||
break;
|
||||
case 'sessions':
|
||||
navigate('/sessions', { state: options });
|
||||
break;
|
||||
case 'schedules':
|
||||
navigate('/schedules', { state: options });
|
||||
break;
|
||||
case 'recipes':
|
||||
navigate('/recipes', { state: options });
|
||||
break;
|
||||
case 'permission':
|
||||
navigate('/permission', { state: options });
|
||||
break;
|
||||
case 'ConfigureProviders':
|
||||
navigate('/configure-providers', { state: options });
|
||||
break;
|
||||
case 'sharedSession':
|
||||
navigate('/shared-session', { state: options });
|
||||
break;
|
||||
case 'recipeEditor':
|
||||
navigate('/recipe-editor', { state: options });
|
||||
break;
|
||||
case 'welcome':
|
||||
navigate('/welcome', { state: options });
|
||||
break;
|
||||
case 'extensions':
|
||||
navigate('/extensions', { state: options });
|
||||
break;
|
||||
default:
|
||||
navigate('/', { state: options });
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user