fix: keep one goosed client per BrowswerWindow (#4805)

This commit is contained in:
Jack Amadeo
2025-09-26 10:54:53 -04:00
committed by GitHub
parent d0b875e630
commit 4e786db03e
4 changed files with 41 additions and 45 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ vi.mock('./utils/costDatabase', () => ({
initializeCostDatabase: vi.fn().mockResolvedValue(undefined), initializeCostDatabase: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock('./api/sdk.gen', () => { vi.mock('./api', () => {
const test_chat = { const test_chat = {
data: { data: {
session_id: 'test', session_id: 'test',
@@ -303,7 +303,7 @@ describe('App Component - Brand New State', () => {
it('should handle config recovery gracefully', async () => { it('should handle config recovery gracefully', async () => {
// Mock config error that triggers recovery // Mock config error that triggers recovery
const { readAllConfig, recoverConfig } = await import('./api/sdk.gen'); const { readAllConfig, recoverConfig } = await import('./api');
console.log(recoverConfig); console.log(recoverConfig);
vi.mocked(readAllConfig).mockRejectedValueOnce(new Error('Config read error')); vi.mocked(readAllConfig).mockRejectedValueOnce(new Error('Config read error'));
@@ -15,7 +15,7 @@ import ExtensionConfigFields from './ExtensionConfigFields';
import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react'; import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react';
import ExtensionInfoFields from './ExtensionInfoFields'; import ExtensionInfoFields from './ExtensionInfoFields';
import ExtensionTimeoutField from './ExtensionTimeoutField'; import ExtensionTimeoutField from './ExtensionTimeoutField';
import { upsertConfig } from '../../../../api/sdk.gen'; import { upsertConfig } from '../../../../api';
import { ConfirmationModal } from '../../../ui/ConfirmationModal'; import { ConfirmationModal } from '../../../ui/ConfirmationModal';
interface ExtensionModalProps { interface ExtensionModalProps {
+5 -29
View File
@@ -9,7 +9,7 @@ import { App } from 'electron';
import { Buffer } from 'node:buffer'; import { Buffer } from 'node:buffer';
import { status } from './api'; import { status } from './api';
import { client } from './api/client.gen'; import { Client } from './api/client';
// Find an available port to start goosed on // Find an available port to start goosed on
export const findAvailablePort = (): Promise<number> => { export const findAvailablePort = (): Promise<number> => {
@@ -26,14 +26,13 @@ export const findAvailablePort = (): Promise<number> => {
}); });
}; };
// 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 // Check if goosed server is ready by polling the status endpoint
export const checkServerStatus = async (): Promise<boolean> => { export const checkServerStatus = async (client: Client): Promise<boolean> => {
const interval = 100; const interval = 100;
const maxAttempts = 200; const maxAttempts = 200;
for (let attempt = 1; attempt <= maxAttempts; attempt++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try { try {
await status({ throwOnError: true }); await status({ client, throwOnError: true });
return true; return true;
} catch { } catch {
if (attempt === maxAttempts) { if (attempt === maxAttempts) {
@@ -47,25 +46,10 @@ export const checkServerStatus = async (): Promise<boolean> => {
const connectToExternalBackend = async ( const connectToExternalBackend = async (
workingDir: string, workingDir: string,
port: number = 3000, port: number = 3000
serverSecret: string
): Promise<[number, string, ChildProcess]> => { ): Promise<[number, string, ChildProcess]> => {
log.info(`Using external goosed backend on port ${port}`); 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 = { const mockProcess = {
pid: undefined, pid: undefined,
kill: () => { kill: () => {
@@ -104,7 +88,7 @@ export const startGoosed = async (
dir = path.resolve(path.normalize(dir)); dir = path.resolve(path.normalize(dir));
if (process.env.GOOSE_EXTERNAL_BACKEND) { 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 // Validate that the directory actually exists and is a directory
@@ -262,14 +246,6 @@ export const startGoosed = async (
throw err; // Propagate the error 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 = () => { const try_kill_goose = () => {
try { try {
if (isWindows) { if (isWindows) {
+33 -13
View File
@@ -50,13 +50,14 @@ import {
import { UPDATES_ENABLED } from './updates'; import { UPDATES_ENABLED } from './updates';
import { Recipe } from './recipe'; import { Recipe } from './recipe';
import './utils/recipeHash'; import './utils/recipeHash';
import { decodeRecipe } from './api/sdk.gen'; import { decodeRecipe } from './api';
import { client } from './api/client.gen'; import { Client, createClient, createConfig } from './api/client';
async function decodeRecipeMain(deeplink: string): Promise<Recipe | null> { async function decodeRecipeMain(client: Client, deeplink: string): Promise<Recipe | null> {
try { try {
return ( return (
await decodeRecipe({ await decodeRecipe({
client,
throwOnError: true, throwOnError: true,
body: { deeplink }, body: { deeplink },
}) })
@@ -487,10 +488,10 @@ let appConfig = {
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true', GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
}; };
// Track windows by ID
let windowCounter = 0;
const windowMap = new Map<number, BrowserWindow>(); const windowMap = new Map<number, BrowserWindow>();
const goosedClients = new Map<number, Client>();
// Track power save blockers per window // Track power save blockers per window
const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blockerId const windowPowerSaveBlockers = new Map<number, number>(); // windowId -> blockerId
@@ -507,7 +508,7 @@ const createChat = async (
) => { ) => {
// Initialize variables for process and configuration // Initialize variables for process and configuration
let port = 0; let port = 0;
let working_dir = ''; let workingDir = '';
let goosedProcess: import('child_process').ChildProcess | null = null; let goosedProcess: import('child_process').ChildProcess | null = null;
if (viewType === 'recipeEditor') { if (viewType === 'recipeEditor') {
@@ -521,7 +522,7 @@ const createChat = async (
); );
if (config) { if (config) {
port = config.GOOSE_PORT; port = config.GOOSE_PORT;
working_dir = config.GOOSE_WORKING_DIR; workingDir = config.GOOSE_WORKING_DIR;
} }
} catch (e) { } catch (e) {
console.error('Failed to get config from localStorage:', e); console.error('Failed to get config from localStorage:', e);
@@ -551,7 +552,7 @@ const createChat = async (
envVars envVars
); );
port = newPort; port = newPort;
working_dir = newWorkingDir; workingDir = newWorkingDir;
goosedProcess = newGoosedProcess; goosedProcess = newGoosedProcess;
} }
@@ -592,7 +593,7 @@ const createChat = async (
JSON.stringify({ JSON.stringify({
...appConfig, ...appConfig,
GOOSE_PORT: port, GOOSE_PORT: port,
GOOSE_WORKING_DIR: working_dir, GOOSE_WORKING_DIR: workingDir,
REQUEST_DIR: dir, REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: baseUrlShare, GOOSE_BASE_URL_SHARE: baseUrlShare,
GOOSE_VERSION: version, 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 // Let windowStateKeeper manage the window
mainWindowState.manage(mainWindow); mainWindowState.manage(mainWindow);
@@ -660,7 +672,7 @@ const createChat = async (
shell.openExternal(url); shell.openExternal(url);
}); });
const windowId = ++windowCounter; const windowId = mainWindow.id;
const url = MAIN_WINDOW_VITE_DEV_SERVER_URL const url = MAIN_WINDOW_VITE_DEV_SERVER_URL
? new 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`)); : 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); console.log('[Main] Starting background recipe decoding for:', recipeDeeplink);
// Decode recipe asynchronously after window is created // Decode recipe asynchronously after window is created
decodeRecipeMain(recipeDeeplink) decodeRecipeMain(goosedClient, recipeDeeplink)
.then((decodedRecipe) => { .then((decodedRecipe) => {
if (decodedRecipe) { if (decodedRecipe) {
console.log('[Main] Recipe decoded successfully, updating window config'); console.log('[Main] Recipe decoded successfully, updating window config');
@@ -1061,8 +1073,16 @@ ipcMain.handle('get-secret-key', () => {
return SERVER_SECRET; return SERVER_SECRET;
}); });
ipcMain.handle('get-goosed-host-port', async () => { ipcMain.handle('get-goosed-host-port', async (event) => {
await checkServerStatus(); 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; return client.getConfig().baseUrl || null;
}); });