From 4e786db03ebeea701683d31210ae30f458f81da6 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Fri, 26 Sep 2025 10:54:53 -0400 Subject: [PATCH] fix: keep one goosed client per BrowswerWindow (#4805) --- ui/desktop/src/App.test.tsx | 4 +- .../extensions/modal/ExtensionModal.tsx | 2 +- ui/desktop/src/goosed.ts | 34 ++------------ ui/desktop/src/main.ts | 46 +++++++++++++------ 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 2da20f9b44..ecc30beed8 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -37,7 +37,7 @@ vi.mock('./utils/costDatabase', () => ({ initializeCostDatabase: vi.fn().mockResolvedValue(undefined), })); -vi.mock('./api/sdk.gen', () => { +vi.mock('./api', () => { const test_chat = { data: { session_id: 'test', @@ -303,7 +303,7 @@ describe('App Component - Brand New State', () => { it('should handle config recovery gracefully', async () => { // Mock config error that triggers recovery - const { readAllConfig, recoverConfig } = await import('./api/sdk.gen'); + const { readAllConfig, recoverConfig } = await import('./api'); console.log(recoverConfig); vi.mocked(readAllConfig).mockRejectedValueOnce(new Error('Config read error')); diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 5a390cfb3b..3a7c84f658 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -15,7 +15,7 @@ import ExtensionConfigFields from './ExtensionConfigFields'; import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react'; import ExtensionInfoFields from './ExtensionInfoFields'; import ExtensionTimeoutField from './ExtensionTimeoutField'; -import { upsertConfig } from '../../../../api/sdk.gen'; +import { upsertConfig } from '../../../../api'; import { ConfirmationModal } from '../../../ui/ConfirmationModal'; interface ExtensionModalProps { diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 6cff75f4f3..a74904b9d2 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -9,7 +9,7 @@ import { App } from 'electron'; import { Buffer } from 'node:buffer'; import { status } from './api'; -import { client } from './api/client.gen'; +import { Client } from './api/client'; // Find an available port to start goosed on export const findAvailablePort = (): Promise => { @@ -26,14 +26,13 @@ export const findAvailablePort = (): Promise => { }); }; -// Goose process manager. Take in the app, port, and directory to start goosed in. // Check if goosed server is ready by polling the status endpoint -export const checkServerStatus = async (): Promise => { +export const checkServerStatus = async (client: Client): Promise => { const interval = 100; const maxAttempts = 200; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await status({ throwOnError: true }); + await status({ client, throwOnError: true }); return true; } catch { if (attempt === maxAttempts) { @@ -47,25 +46,10 @@ export const checkServerStatus = async (): Promise => { const connectToExternalBackend = async ( workingDir: string, - port: number = 3000, - serverSecret: string + port: number = 3000 ): Promise<[number, string, ChildProcess]> => { log.info(`Using external goosed backend on port ${port}`); - // Configure the client BEFORE checking server status - client.setConfig({ - baseUrl: `http://127.0.0.1:${port}`, - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': serverSecret, - }, - }); - - const isReady = await checkServerStatus(); - if (!isReady) { - throw new Error(`External goosed server not accessible on port ${port}`); - } - const mockProcess = { pid: undefined, kill: () => { @@ -104,7 +88,7 @@ export const startGoosed = async ( dir = path.resolve(path.normalize(dir)); if (process.env.GOOSE_EXTERNAL_BACKEND) { - return connectToExternalBackend(dir, 3000, serverSecret); + return connectToExternalBackend(dir, 3000); } // Validate that the directory actually exists and is a directory @@ -262,14 +246,6 @@ export const startGoosed = async ( throw err; // Propagate the error }); - client.setConfig({ - baseUrl: `http://127.0.0.1:${port}`, - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': serverSecret, - }, - }); - const try_kill_goose = () => { try { if (isWindows) { diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 9ebba638f6..f16e297eb8 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -50,13 +50,14 @@ import { import { UPDATES_ENABLED } from './updates'; import { Recipe } from './recipe'; import './utils/recipeHash'; -import { decodeRecipe } from './api/sdk.gen'; -import { client } from './api/client.gen'; +import { decodeRecipe } from './api'; +import { Client, createClient, createConfig } from './api/client'; -async function decodeRecipeMain(deeplink: string): Promise { +async function decodeRecipeMain(client: Client, deeplink: string): Promise { try { return ( await decodeRecipe({ + client, throwOnError: true, body: { deeplink }, }) @@ -487,10 +488,10 @@ let appConfig = { GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true', }; -// Track windows by ID -let windowCounter = 0; const windowMap = new Map(); +const goosedClients = new Map(); + // Track power save blockers per window const windowPowerSaveBlockers = new Map(); // windowId -> blockerId @@ -507,7 +508,7 @@ const createChat = async ( ) => { // Initialize variables for process and configuration let port = 0; - let working_dir = ''; + let workingDir = ''; let goosedProcess: import('child_process').ChildProcess | null = null; if (viewType === 'recipeEditor') { @@ -521,7 +522,7 @@ const createChat = async ( ); if (config) { port = config.GOOSE_PORT; - working_dir = config.GOOSE_WORKING_DIR; + workingDir = config.GOOSE_WORKING_DIR; } } catch (e) { console.error('Failed to get config from localStorage:', e); @@ -551,7 +552,7 @@ const createChat = async ( envVars ); port = newPort; - working_dir = newWorkingDir; + workingDir = newWorkingDir; goosedProcess = newGoosedProcess; } @@ -592,7 +593,7 @@ const createChat = async ( JSON.stringify({ ...appConfig, GOOSE_PORT: port, - GOOSE_WORKING_DIR: working_dir, + GOOSE_WORKING_DIR: workingDir, REQUEST_DIR: dir, GOOSE_BASE_URL_SHARE: baseUrlShare, GOOSE_VERSION: version, @@ -603,6 +604,17 @@ const createChat = async ( }, }); + const goosedClient = createClient( + createConfig({ + baseUrl: `http://127.0.0.1:${port}`, + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': SERVER_SECRET, + }, + }) + ); + goosedClients.set(mainWindow.id, goosedClient); + // Let windowStateKeeper manage the window mainWindowState.manage(mainWindow); @@ -660,7 +672,7 @@ const createChat = async ( shell.openExternal(url); }); - const windowId = ++windowCounter; + const windowId = mainWindow.id; 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`)); @@ -738,7 +750,7 @@ const createChat = async ( console.log('[Main] Starting background recipe decoding for:', recipeDeeplink); // Decode recipe asynchronously after window is created - decodeRecipeMain(recipeDeeplink) + decodeRecipeMain(goosedClient, recipeDeeplink) .then((decodedRecipe) => { if (decodedRecipe) { console.log('[Main] Recipe decoded successfully, updating window config'); @@ -1061,8 +1073,16 @@ ipcMain.handle('get-secret-key', () => { return SERVER_SECRET; }); -ipcMain.handle('get-goosed-host-port', async () => { - await checkServerStatus(); +ipcMain.handle('get-goosed-host-port', async (event) => { + const windowId = BrowserWindow.fromWebContents(event.sender)?.id; + if (!windowId) { + return null; + } + const client = goosedClients.get(windowId); + if (!client) { + return null; + } + await checkServerStatus(client); return client.getConfig().baseUrl || null; });