feat: simplify navigation, make reload work (#4498)

This commit is contained in:
Jack Amadeo
2025-09-05 20:36:38 -04:00
committed by GitHub
parent 5410df3f1e
commit 7fb746bbcb
7 changed files with 235 additions and 306 deletions
-1
View File
@@ -39,7 +39,6 @@
"start-alpha-gui": "ALPHA=true npm run start-gui"
},
"dependencies": {
"@tanstack/react-form": "^0.13.0",
"@ai-sdk/openai": "^2.0.14",
"@ai-sdk/ui-utils": "^1.2.11",
"@mcp-ui/client": "^5.9.0",
+31 -31
View File
@@ -4,9 +4,9 @@
* @vitest-environment jsdom
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { screen, render, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import App from './App';
import { AppInner } from './App';
// Set up globals for jsdom
Object.defineProperty(window, 'location', {
@@ -161,13 +161,19 @@ vi.mock('./components/AnnouncementModal', () => ({
default: () => null,
}));
// Create mocks that we can track and configure per test
const mockNavigate = vi.fn();
const mockSearchParams = new URLSearchParams();
const mockSetSearchParams = vi.fn();
// Mock react-router-dom to avoid HashRouter issues in tests
vi.mock('react-router-dom', () => ({
HashRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Routes: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Route: ({ element }: { element: React.ReactNode }) => element,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
useLocation: () => ({ state: null, pathname: '/' }),
useSearchParams: () => [mockSearchParams, mockSetSearchParams],
Outlet: () => null,
}));
@@ -216,6 +222,14 @@ Object.defineProperty(window, 'matchMedia', {
describe('App Component - Brand New State', () => {
beforeEach(() => {
vi.clearAllMocks();
mockNavigate.mockClear();
mockSetSearchParams.mockClear();
// Reset search params
mockSearchParams.forEach((_, key) => {
mockSearchParams.delete(key);
});
window.location.hash = '';
window.location.search = '';
window.location.pathname = '/';
@@ -235,21 +249,16 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});
render(<App />);
render(<AppInner />);
// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});
// Check that we navigated to "/" not "/welcome"
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});
// History should have been updated to "/"
expect(window.history.replaceState).toHaveBeenCalledWith({}, '', '#/');
// The app should initialize without any navigation calls since we're already at "/"
// No navigate calls should be made when no provider is configured
expect(mockNavigate).not.toHaveBeenCalled();
});
it('should handle deep links correctly when app is brand new', async () => {
@@ -260,20 +269,17 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});
// Simulate a deep link
window.location.search = '?view=settings';
// Set up search params to simulate view=settings deep link
mockSearchParams.set('view', 'settings');
render(<App />);
render(<AppInner />);
// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});
// Should redirect to settings route via hash
await waitFor(() => {
expect(window.location.hash).toBe('#/settings');
});
expect(screen.getByText(/^Select an AI model provider/)).toBeInTheDocument();
});
it('should not redirect to /welcome when provider is configured', async () => {
@@ -284,18 +290,15 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});
render(<App />);
render(<AppInner />);
// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});
// Should stay at "/" since provider is configured
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});
// Should not navigate anywhere since provider is configured and we're already at "/"
expect(mockNavigate).not.toHaveBeenCalled();
});
it('should handle config recovery gracefully', async () => {
@@ -310,17 +313,14 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});
render(<App />);
render(<AppInner />);
// Wait for initialization and recovery
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});
// App should still initialize and navigate to "/"
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});
// App should still initialize without any navigation calls
expect(mockNavigate).not.toHaveBeenCalled();
});
});
+150 -235
View File
@@ -1,6 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IpcRendererEvent } from 'electron';
import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
import {
HashRouter,
Routes,
Route,
useNavigate,
useLocation,
useSearchParams,
} from 'react-router-dom';
import { openSharedSessionFromDeepLink } from './sessionLinks';
import { type SharedSessionDetails } from './sharedSessions';
import { ErrorUI } from './components/ErrorBoundary';
@@ -29,7 +36,6 @@ import { ModelAndProviderProvider } from './components/ModelAndProviderContext';
import PermissionSettingsView from './components/settings/permission/PermissionSetting';
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
import { Recipe } from './recipe';
import RecipesView from './components/recipes/RecipesView';
import RecipeEditor from './components/recipes/RecipeEditor';
import { createNavigationHandler, View, ViewOptions } from './utils/navigationUtils';
@@ -85,9 +91,11 @@ const PairRouteWrapper = ({
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
const routeState =
(location.state as PairRouteState) || (window.history.state as PairRouteState) || {};
const [resumeSessionId] = useState(routeState.resumeSessionId);
const [searchParams] = useSearchParams();
const [initialMessage] = useState(routeState.initialMessage);
const resumeSessionId = searchParams.get('resumeSessionId') ?? undefined;
return (
<Pair
chat={chat}
@@ -132,24 +140,18 @@ const RecipesRoute = () => {
};
const RecipeEditorRoute = () => {
const location = useLocation();
// Check for config from multiple sources:
// 1. Location state (from navigation)
// 2. localStorage (from "View Recipe" button)
// 3. Window electron config (from deeplinks)
let config = location.state?.config;
if (!config) {
const storedConfig = localStorage.getItem('viewRecipeConfig');
if (storedConfig) {
try {
config = JSON.parse(storedConfig);
// Clear the stored config after using it
localStorage.removeItem('viewRecipeConfig');
} catch (error) {
console.error('Failed to parse stored recipe config:', error);
}
// 1. localStorage (from "View Recipe" button)
// 2. Window electron config (from deeplinks)
let config;
const storedConfig = localStorage.getItem('viewRecipeConfig');
if (storedConfig) {
try {
config = JSON.parse(storedConfig);
// Clear the stored config after using it
localStorage.removeItem('viewRecipeConfig');
} catch (error) {
console.error('Failed to parse stored recipe config:', error);
}
}
@@ -304,37 +306,21 @@ const ExtensionsRoute = () => {
);
};
export default function App() {
export function AppInner() {
const [fatalError, setFatalError] = useState<string | null>(null);
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
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 [didSyncUrlParams, setDidSyncUrlParams] = useState<boolean>(false);
const [viewType, setViewType] = useState<string | null>(null);
const [resumeSessionId, setResumeSessionId] = useState<string | null>(null);
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
const [recipeFromAppConfig, setRecipeFromAppConfig] = useState<Recipe | null>(
(window.appConfig?.get('recipe') as Recipe) || null
);
const navigate = useNavigate();
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const location = useLocation();
const [_searchParams, setSearchParams] = useSearchParams();
const viewType = urlParams.get('view') || null;
const resumeSessionId = urlParams.get('resumeSessionId') || null;
setViewType(viewType);
setResumeSessionId(resumeSessionId);
setDidSyncUrlParams(true);
}, []);
const [chat, _setChat] = useState<ChatType>({
const [chat, setChat] = useState<ChatType>({
sessionId: generateSessionId(),
title: 'Pair Chat',
messages: [],
@@ -342,22 +328,17 @@ export default function App() {
recipeConfig: null,
});
const setChat = useCallback<typeof _setChat>(
(update) => {
_setChat(update);
},
[_setChat]
);
const { addExtension } = useConfig();
const { agentState, loadCurrentChat, resetChat } = useAgent();
const resetChatIfNecessary = useCallback(() => {
if (chat.messages.length > 0) {
setResumeSessionId(null);
setRecipeFromAppConfig(null);
setSearchParams((prev) => {
prev.delete('resumeSessionId');
return prev;
});
resetChat();
}
}, [resetChat, chat.messages.length]);
}, [chat.messages.length, setSearchParams, resetChat]);
useEffect(() => {
console.log('Sending reactReady signal to Electron');
@@ -372,77 +353,25 @@ export default function App() {
}, []);
// Handle URL parameters and deeplinks on app startup
const loadingHub = location.pathname === '/';
useEffect(() => {
if (!didSyncUrlParams) {
return;
}
const stateData: PairRouteState = {
resumeSessionId: resumeSessionId || undefined,
};
(async () => {
try {
await loadCurrentChat({
setAgentWaitingMessage,
setIsExtensionsLoading,
recipeConfig: recipeFromAppConfig || undefined,
...stateData,
});
} catch (e) {
if (e instanceof NoProviderOrModelError) {
// the onboarding flow will trigger
} else {
throw e;
if (loadingHub) {
(async () => {
try {
await loadCurrentChat({
setAgentWaitingMessage,
setIsExtensionsLoading,
});
} catch (e) {
if (e instanceof NoProviderOrModelError) {
// the onboarding flow will trigger
} else {
throw e;
}
}
}
})();
if (resumeSessionId || recipeFromAppConfig) {
window.location.hash = '#/pair';
window.history.replaceState(stateData, '', '#/pair');
return;
})();
}
if (!viewType) {
if (window.location.hash === '' || window.location.hash === '#') {
window.location.hash = '#/';
window.history.replaceState({}, '', '#/');
}
} else {
if (viewType === 'recipeEditor' && recipeFromAppConfig) {
window.location.hash = '#/recipe-editor';
window.history.replaceState({ config: recipeFromAppConfig }, '', '#/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);
}
}
}
}, [
recipeFromAppConfig,
resetChat,
loadCurrentChat,
setAgentWaitingMessage,
didSyncUrlParams,
resumeSessionId,
viewType,
]);
}, [resetChat, loadCurrentChat, setAgentWaitingMessage, navigate, loadingHub]);
useEffect(() => {
const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => {
@@ -451,24 +380,19 @@ export default function App() {
setIsLoadingSharedSession(true);
setSharedSessionError(null);
try {
await openSharedSessionFromDeepLink(link, (_view: View, _options?: ViewOptions) => {
// Navigate to shared session view with the session data
window.location.hash = '#/shared-session';
if (_options) {
window.history.replaceState(_options, '', '#/shared-session');
}
await openSharedSessionFromDeepLink(link, (_view: View, options?: ViewOptions) => {
navigate('/shared-session', { state: options });
});
} catch (error) {
console.error('Unexpected error opening shared session:', error);
// Navigate to shared session view with error
window.location.hash = '#/shared-session';
const shareToken = link.replace('goose://sessions/', '');
const options = {
sessionDetails: null,
error: error instanceof Error ? error.message : 'Unknown error',
shareToken,
};
window.history.replaceState(options, '', '#/shared-session');
navigate('/shared-session', { state: options });
} finally {
setIsLoadingSharedSession(false);
}
@@ -477,7 +401,7 @@ export default function App() {
return () => {
window.electron.off('open-shared-session', handleOpenSharedSession);
};
}, []);
}, [navigate]);
useEffect(() => {
console.log('Setting up keyboard shortcuts');
@@ -566,32 +490,15 @@ export default function App() {
);
if (section && newView === 'settings') {
window.location.hash = `#/settings?section=${section}`;
navigate(`/settings?section=${section}`);
} else {
window.location.hash = `#/${newView}`;
navigate(`/${newView}`);
}
};
const urlParams = new URLSearchParams(window.location.search);
const viewFromUrl = urlParams.get('view');
if (viewFromUrl) {
const windowConfig = window.electron.getConfig();
if (viewFromUrl === 'recipeEditor') {
const initialViewOptions = {
recipeConfig: JSON.stringify(windowConfig?.recipeConfig),
view: viewFromUrl,
};
window.history.replaceState(
{},
'',
`/recipe-editor?${new URLSearchParams(initialViewOptions).toString()}`
);
} else {
window.history.replaceState({}, '', `/${viewFromUrl}`);
}
}
window.electron.on('set-view', handleSetView);
return () => window.electron.off('set-view', handleSetView);
}, []);
}, [navigate]);
useEffect(() => {
const handleFocusInput = (_event: IpcRendererEvent, ..._args: unknown[]) => {
@@ -611,98 +518,106 @@ export default function App() {
}
return (
<DraftProvider>
<ModelAndProviderProvider>
<HashRouter>
<ToastContainer
aria-label="Toast notifications"
toastClassName={() =>
`relative min-h-16 mb-4 p-2 rounded-lg
<>
<ToastContainer
aria-label="Toast notifications"
toastClassName={() =>
`relative min-h-16 mb-4 p-2 rounded-lg
flex justify-between overflow-hidden cursor-pointer
text-text-on-accent bg-background-inverse
`
}
style={{ width: '380px' }}
className="mt-6"
position="top-right"
autoClose={3000}
closeOnClick
pauseOnHover
}
style={{ width: '380px' }}
className="mt-6"
position="top-right"
autoClose={3000}
closeOnClick
pauseOnHover
/>
<ExtensionInstallModal addExtension={addExtension} />
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
<div className="titlebar-drag-region" />
<Routes>
<Route
path="welcome"
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
/>
<ExtensionInstallModal addExtension={addExtension} />
<div className="relative w-screen h-screen overflow-hidden bg-background-muted flex flex-col">
<div className="titlebar-drag-region" />
<Routes>
<Route
path="welcome"
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
/>
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
<Route
path="/"
element={
<ProviderGuard didSelectProvider={didSelectProvider}>
<ChatProvider
chat={chat}
setChat={setChat}
contextKey="hub"
agentWaitingMessage={agentWaitingMessage}
>
<AppLayout setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} />
</ChatProvider>
</ProviderGuard>
}
>
<Route
index
element={
<HubRouteWrapper
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
isExtensionsLoading={isExtensionsLoading}
resetChat={resetChatIfNecessary}
/>
}
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
<Route
path="/"
element={
<ProviderGuard didSelectProvider={didSelectProvider}>
<ChatProvider
chat={chat}
setChat={setChat}
contextKey="hub"
agentWaitingMessage={agentWaitingMessage}
>
<AppLayout setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} />
</ChatProvider>
</ProviderGuard>
}
>
<Route
index
element={
<HubRouteWrapper
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
isExtensionsLoading={isExtensionsLoading}
resetChat={resetChatIfNecessary}
/>
<Route
path="pair"
element={
<PairRouteWrapper
chat={chat}
setChat={setChat}
agentState={agentState}
loadCurrentChat={loadCurrentChat}
setFatalError={setFatalError}
setAgentWaitingMessage={setAgentWaitingMessage}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
}
/>
<Route path="settings" element={<SettingsRoute />} />
<Route path="extensions" element={<ExtensionsRoute />} />
<Route path="sessions" element={<SessionsRoute />} />
<Route path="schedules" element={<SchedulesRoute />} />
<Route path="recipes" element={<RecipesRoute />} />
<Route path="recipe-editor" element={<RecipeEditorRoute />} />
<Route
path="shared-session"
element={
<SharedSessionRouteWrapper
isLoadingSharedSession={isLoadingSharedSession}
setIsLoadingSharedSession={setIsLoadingSharedSession}
sharedSessionError={sharedSessionError}
/>
}
/>
<Route path="permission" element={<PermissionRoute />} />
</Route>
</Routes>
</div>
{isGoosehintsModalOpen && (
<GoosehintsModal
directory={window.appConfig?.get('GOOSE_WORKING_DIR') as string}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
}
/>
)}
<Route
path="pair"
element={
<PairRouteWrapper
chat={chat}
setChat={setChat}
agentState={agentState}
loadCurrentChat={loadCurrentChat}
setFatalError={setFatalError}
setAgentWaitingMessage={setAgentWaitingMessage}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
}
/>
<Route path="settings" element={<SettingsRoute />} />
<Route path="extensions" element={<ExtensionsRoute />} />
<Route path="sessions" element={<SessionsRoute />} />
<Route path="schedules" element={<SchedulesRoute />} />
<Route path="recipes" element={<RecipesRoute />} />
<Route path="recipe-editor" element={<RecipeEditorRoute />} />
<Route
path="shared-session"
element={
<SharedSessionRouteWrapper
isLoadingSharedSession={isLoadingSharedSession}
setIsLoadingSharedSession={setIsLoadingSharedSession}
sharedSessionError={sharedSessionError}
/>
}
/>
<Route path="permission" element={<PermissionRoute />} />
</Route>
</Routes>
</div>
{isGoosehintsModalOpen && (
<GoosehintsModal
directory={window.appConfig?.get('GOOSE_WORKING_DIR') as string}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
/>
)}
</>
);
}
export default function App() {
return (
<DraftProvider>
<ModelAndProviderProvider>
<HashRouter>
<AppInner />
</HashRouter>
<AnnouncementModal />
</ModelAndProviderProvider>
+8 -1
View File
@@ -9,6 +9,7 @@ import 'react-toastify/dist/ReactToastify.css';
import { cn } from '../utils';
import { ChatType } from '../types/chat';
import { useSearchParams } from 'react-router-dom';
export interface PairRouteState {
resumeSessionId?: string;
@@ -45,16 +46,21 @@ export default function Pair({
const [messageToSubmit, setMessageToSubmit] = useState<string | null>(null);
const [isTransitioningFromHub, setIsTransitioningFromHub] = useState(false);
const [loadingChat, setLoadingChat] = useState(false);
const [_searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
const initializeFromState = async () => {
setLoadingChat(true);
try {
const chat = await loadCurrentChat({
resumeSessionId: resumeSessionId,
resumeSessionId,
setAgentWaitingMessage,
});
setChat(chat);
setSearchParams((prev) => {
prev.set('resumeSessionId', chat.sessionId);
return prev;
});
} catch (error) {
console.log(error);
setFatalError(`Agent init failure: ${error instanceof Error ? error.message : '' + error}`);
@@ -70,6 +76,7 @@ export default function Pair({
setAgentWaitingMessage,
loadCurrentChat,
resumeSessionId,
setSearchParams,
]);
// Followed by sending the initialMessage if we have one. This will happen
+6 -2
View File
@@ -49,12 +49,16 @@ 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 [recipeFromAppConfig, setRecipeFromAppConfig] = useState<Recipe | null>(
(window.appConfig.get('recipe') as Recipe) || null
);
const { getExtensions, addExtension, read } = useConfig();
const resetChat = useCallback(() => {
setSessionId(null);
setAgentState(AgentState.UNINITIALIZED);
setRecipeFromAppConfig(null);
}, []);
const agentIsInitialized = agentState === AgentState.INITIALIZED;
@@ -112,7 +116,7 @@ export function useAgent(): UseAgentReturn {
: await startAgent({
body: {
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
recipe: initContext.recipeConfig,
recipe: recipeFromAppConfig ?? initContext.recipeConfig,
},
throwOnError: true,
});
@@ -179,7 +183,7 @@ export function useAgent(): UseAgentReturn {
initPromiseRef.current = initPromise;
return initPromise;
},
[getExtensions, addExtension, read, agentIsInitialized, sessionId]
[agentIsInitialized, sessionId, read, recipeFromAppConfig, getExtensions, addExtension]
);
return {
+1 -1
View File
@@ -29,7 +29,7 @@ export const useRecipeManager = (chat: ChatType, recipeConfig?: Recipe | null) =
const finalRecipeConfig = chat.recipeConfig;
useEffect(() => {
if (!chatContext?.setRecipeConfig) return;
if (!chatContext) return;
// If we have a recipe from navigation state, persist it
if (recipeConfig && !chatContext.chat.recipeConfig) {
+39 -35
View File
@@ -14,6 +14,7 @@ import {
shell,
Tray,
} from 'electron';
import { pathToFileURL, format as formatUrl, URLSearchParams } from 'node:url';
import { Buffer } from 'node:buffer';
import fs from 'node:fs/promises';
import fsSync from 'node:fs';
@@ -515,7 +516,7 @@ const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blocke
const createChat = async (
app: App,
query?: string,
_query?: string,
dir?: string,
_version?: string,
resumeSessionId?: string,
@@ -679,43 +680,46 @@ const createChat = async (
shell.openExternal(url);
});
// Load the index.html of the app.
let queryParams = '';
if (query) {
queryParams = `?initialQuery=${encodeURIComponent(query)}`;
}
// Add resumeSessionId to query params if provided
if (resumeSessionId) {
queryParams = queryParams
? `${queryParams}&resumeSessionId=${encodeURIComponent(resumeSessionId)}`
: `?resumeSessionId=${encodeURIComponent(resumeSessionId)}`;
}
// Add view type to query params if provided
if (viewType) {
queryParams = queryParams
? `${queryParams}&view=${encodeURIComponent(viewType)}`
: `?view=${encodeURIComponent(viewType)}`;
}
// For recipe deeplinks, navigate directly to pair view
if (recipe || recipeDeeplink) {
queryParams = queryParams ? `${queryParams}&view=pair` : `?view=pair`;
}
// Increment window counter to track number of windows
const windowId = ++windowCounter;
const url = MAIN_WINDOW_VITE_DEV_SERVER_URL
? new URL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
: pathToFileURL(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParams}`);
} else {
// In production, we need to use a proper file protocol URL with correct base path
const indexPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`);
mainWindow.loadFile(indexPath, {
search: queryParams ? queryParams.slice(1) : undefined,
});
let appPath = '/';
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',
};
if (viewType) {
appPath = routeMap[viewType] || '/';
}
if (appPath === '/' && (recipe !== undefined || recipeDeeplink !== undefined)) {
appPath = '/pair';
}
let searchParams = new URLSearchParams();
if (resumeSessionId) {
searchParams.set('resumeSessionId', resumeSessionId);
if (appPath === '/') {
appPath = '/pair';
}
}
// Goose's react app uses HashRouter, so the path + search params follow a #/
url.hash = `${appPath}?${searchParams.toString()}`;
let formattedUrl = formatUrl(url);
console.log('Opening URL: ', formattedUrl);
mainWindow.loadURL(formattedUrl);
// Set up local keyboard shortcuts that only work when the window is focused
mainWindow.webContents.on('before-input-event', (event, input) => {