Show errors on failure (#5643)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2025-11-10 13:39:23 -05:00
committed by GitHub
parent a971a64007
commit 8cd379ca33
17 changed files with 126 additions and 312 deletions
@@ -1,6 +1,5 @@
import { ModeSection } from '../mode/ModeSection';
import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection';
import SchedulerSection from '../scheduler/SchedulerSection';
import DictationSection from '../dictation/DictationSection';
import { SecurityToggle } from '../security/SecurityToggle';
import { ResponseStylesSection } from '../response_styles/ResponseStylesSection';
@@ -48,9 +47,6 @@ export default function ChatSettingsSection() {
Choose which scheduling backend to use for scheduled recipes and tasks
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<SchedulerSection />
</CardContent>
</Card>
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
@@ -1,113 +0,0 @@
import { useState, useEffect } from 'react';
import { SchedulingEngine, Settings } from '../../../utils/settings';
interface SchedulingEngineOption {
key: SchedulingEngine;
label: string;
description: string;
}
const schedulingEngineOptions: SchedulingEngineOption[] = [
{
key: 'builtin-cron',
label: 'Built-in Cron (Default)',
description:
"Uses Goose's built-in cron scheduler. Simple and reliable for basic scheduling needs.",
},
{
key: 'temporal',
label: 'Temporal',
description:
'Uses Temporal workflow engine for advanced scheduling features. Requires Temporal CLI to be installed.',
},
];
interface SchedulerSectionProps {
onSchedulingEngineChange?: (engine: SchedulingEngine) => void;
}
export default function SchedulerSection({ onSchedulingEngineChange }: SchedulerSectionProps) {
const [schedulingEngine, setSchedulingEngine] = useState<SchedulingEngine>('builtin-cron');
useEffect(() => {
const loadSchedulingEngine = async () => {
try {
const settings = (await window.electron.getSettings()) as Settings | null;
if (settings?.schedulingEngine) {
setSchedulingEngine(settings.schedulingEngine);
}
} catch (error) {
console.error('Failed to load scheduling engine setting:', error);
}
};
loadSchedulingEngine();
}, []);
const handleEngineChange = async (engine: SchedulingEngine) => {
try {
setSchedulingEngine(engine);
await window.electron.setSchedulingEngine(engine);
if (onSchedulingEngineChange) {
onSchedulingEngineChange(engine);
}
} catch (error) {
console.error('Failed to save scheduling engine setting:', error);
}
};
return (
<div className="space-y-1">
{schedulingEngineOptions.map((option) => {
const isChecked = schedulingEngine === option.key;
return (
<div key={option.key} className="group hover:cursor-pointer text-sm">
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${
isChecked
? 'bg-background-muted'
: 'bg-background-default hover:bg-background-muted'
} rounded-lg transition-all`}
onClick={() => handleEngineChange(option.key)}
>
<div className="flex">
<div>
<h3 className="text-text-default">{option.label}</h3>
<p className="text-xs text-text-muted mt-[2px]">{option.description}</p>
</div>
</div>
<div className="relative flex items-center gap-2">
<input
type="radio"
name="schedulingEngine"
value={option.key}
checked={isChecked}
onChange={() => handleEngineChange(option.key)}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-border-default
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out group-hover:border-border-default"
></div>
</div>
</div>
</div>
);
})}
<div className="mt-4 p-3 bg-background-subtle rounded-md">
<p className="text-xs text-text-muted">
<strong>Note:</strong> Changing the scheduling engine will apply to new Goose sessions.
You will need to restart Goose for the change to take full effect. <br />
The scheduling engines do not share the list of schedules.
</p>
</div>
</div>
);
}
+26 -28
View File
@@ -2,7 +2,6 @@ import { spawn, ChildProcess } from 'child_process';
import { createServer } from 'net';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { getBinaryPath } from './utils/pathUtils';
import log from './utils/logger';
import { App } from 'electron';
@@ -11,7 +10,6 @@ import { Buffer } from 'node:buffer';
import { status } from './api';
import { Client } from './api/client';
// Find an available port to start goosed on
export const findAvailablePort = (): Promise<number> => {
return new Promise((resolve, _reject) => {
const server = createServer();
@@ -27,11 +25,20 @@ export const findAvailablePort = (): Promise<number> => {
};
// Check if goosed server is ready by polling the status endpoint
export const checkServerStatus = async (client: Client): Promise<boolean> => {
export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
const interval = 100; // ms
const maxAttempts = 1200; // 120s
const maxAttempts = 30; // 3s
const fatal = (line: string) => {
const trimmed = line.trim().toLowerCase();
return trimmed.startsWith("thread 'main' panicked at") || trimmed.startsWith('error:');
};
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (errorLog.some(fatal)) {
log.error('Detected fatal error in server logs');
return false;
}
try {
await status({ client, throwOnError: true });
return true;
@@ -48,7 +55,7 @@ export const checkServerStatus = async (client: Client): Promise<boolean> => {
const connectToExternalBackend = async (
workingDir: string,
port: number = 3000
): Promise<[number, string, ChildProcess]> => {
): Promise<[number, string, ChildProcess, string[]]> => {
log.info(`Using external goosed backend on port ${port}`);
const mockProcess = {
@@ -58,7 +65,7 @@ const connectToExternalBackend = async (
},
} as ChildProcess;
return [port, workingDir, mockProcess];
return [port, workingDir, mockProcess, []];
};
interface GooseProcessEnv {
@@ -76,39 +83,23 @@ interface GooseProcessEnv {
export const startGoosed = async (
app: App,
serverSecret: string,
dir: string | null = null,
dir: string,
env: Partial<GooseProcessEnv> = {}
): Promise<[number, string, ChildProcess]> => {
const homeDir = os.homedir();
): Promise<[number, string, ChildProcess, string[]]> => {
const isWindows = process.platform === 'win32';
if (!dir) {
dir = homeDir;
}
const homeDir = os.homedir();
dir = path.resolve(path.normalize(dir));
if (process.env.GOOSE_EXTERNAL_BACKEND) {
return connectToExternalBackend(dir, 3000);
}
try {
const stats = fs.lstatSync(dir);
if (!stats.isDirectory()) {
log.warn(`Provided path is not a directory, falling back to home directory`);
dir = homeDir;
}
} catch {
log.warn(`Directory does not exist, falling back to home directory`);
dir = homeDir;
}
let goosedPath = getBinaryPath(app, 'goosed');
const resolvedGoosedPath = path.resolve(goosedPath);
const port = await findAvailablePort();
const stderrLines: string[] = [];
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`);
@@ -183,7 +174,14 @@ export const startGoosed = async (
});
goosedProcess.stderr?.on('data', (data: Buffer) => {
log.error(`goosed stderr for port ${port} and dir ${dir}: ${data.toString()}`);
const lines = data
.toString()
.split('\n')
.filter((l) => l.trim());
lines.forEach((line) => {
log.error(`goosed stderr for port ${port} and dir ${dir}: ${line}`);
stderrLines.push(line);
});
});
goosedProcess.on('close', (code: number | null) => {
@@ -215,5 +213,5 @@ export const startGoosed = async (
});
log.info(`Goosed server successfully started on port ${port}`);
return [port, dir, goosedProcess];
return [port, dir, goosedProcess, stderrLines];
};
+20 -58
View File
@@ -33,9 +33,7 @@ import {
EnvToggles,
loadSettings,
saveSettings,
SchedulingEngine,
updateEnvironmentVariables,
updateSchedulingEngineEnvironment,
} from './utils/settings';
import * as crypto from 'crypto';
// import electron from "electron";
@@ -502,38 +500,20 @@ const createChat = async (
scheduledJobId?: string, // Scheduled job ID if applicable
recipeId?: string
) => {
// Initialize variables for process and configuration
let port = 0;
let workingDir = '';
let goosedProcess: import('child_process').ChildProcess | null = null;
updateEnvironmentVariables(envToggles);
{
// Apply current environment settings before creating chat
updateEnvironmentVariables(envToggles);
const envVars = {
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
};
const [port, workingDir, goosedProcess, errorLog] = await startGoosed(
app,
SERVER_SECRET,
dir || os.homedir(),
envVars
);
// Apply scheduling engine setting
const settings = loadSettings();
updateSchedulingEngineEnvironment(settings.schedulingEngine);
const envVars = {
GOOSE_SCHEDULER_TYPE: process.env.GOOSE_SCHEDULER_TYPE,
GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
};
const [newPort, newWorkingDir, newGoosedProcess] = await startGoosed(
app,
SERVER_SECRET,
dir,
envVars
);
port = newPort;
workingDir = newWorkingDir;
goosedProcess = newGoosedProcess;
}
// Create window config with loading state for recipe deeplinks
// Load and manage window state
const mainWindowState = windowStateKeeper({
defaultWidth: 940, // large enough to show the sidebar on launch
defaultWidth: 940,
defaultHeight: 800,
});
@@ -594,23 +574,21 @@ const createChat = async (
);
goosedClients.set(mainWindow.id, goosedClient);
console.log('[Main] Waiting for backend server to be ready...');
const serverReady = await checkServerStatus(goosedClient);
const serverReady = await checkServerStatus(goosedClient, errorLog);
if (!serverReady) {
throw new Error('Backend server failed to start in time');
dialog.showMessageBoxSync({
type: 'error',
title: 'Goose Failed to Start',
message: 'The backend server failed to start.',
detail: errorLog.join('\n'),
buttons: ['OK'],
});
app.quit();
}
// Let windowStateKeeper manage the window
mainWindowState.manage(mainWindow);
// Enable spellcheck / right and ctrl + click on mispelled word
//
// NOTE: We could use webContents.session.availableSpellCheckerLanguages to include
// all languages in the list of spell checked words, but it diminishes the times you
// get red squigglies back for mispelled english words. Given the rest of Goose only
// renders in english right now, this feels like the correct set of language codes
// for the moment.
//
mainWindow.webContents.session.setSpellCheckerLanguages(['en-US', 'en-GB']);
mainWindow.webContents.on('context-menu', (_event, params) => {
const menu = new Menu();
@@ -1167,22 +1145,6 @@ ipcMain.handle('get-goosed-host-port', async (event) => {
return client.getConfig().baseUrl || null;
});
ipcMain.handle('set-scheduling-engine', async (_event, engine: string) => {
try {
const settings = loadSettings();
settings.schedulingEngine = engine as SchedulingEngine;
saveSettings(settings);
// Update the environment variable immediately
updateSchedulingEngineEnvironment(settings.schedulingEngine);
return true;
} catch (error) {
console.error('Error setting scheduling engine:', error);
return false;
}
});
// Handle menu bar icon visibility
ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => {
try {
-2
View File
@@ -80,7 +80,6 @@ type ElectronAPI = {
getSettings: () => Promise<unknown | null>;
getSecretKey: () => Promise<string>;
getGoosedHostPort: () => Promise<string | null>;
setSchedulingEngine: (engine: string) => Promise<boolean>;
setWakelock: (enable: boolean) => Promise<boolean>;
getWakelockState: () => Promise<boolean>;
openNotificationsSettings: () => Promise<boolean>;
@@ -186,7 +185,6 @@ const electronAPI: ElectronAPI = {
getSettings: () => ipcRenderer.invoke('get-settings'),
getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
setSchedulingEngine: (engine: string) => ipcRenderer.invoke('set-scheduling-engine', engine),
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'),
openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'),
-16
View File
@@ -2,23 +2,18 @@ import { app } from 'electron';
import fs from 'fs';
import path from 'path';
// Types
export interface EnvToggles {
GOOSE_SERVER__MEMORY: boolean;
GOOSE_SERVER__COMPUTER_CONTROLLER: boolean;
}
export type SchedulingEngine = 'builtin-cron' | 'temporal';
export interface Settings {
envToggles: EnvToggles;
showMenuBarIcon: boolean;
showDockIcon: boolean;
schedulingEngine: SchedulingEngine;
enableWakelock: boolean;
}
// Constants
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
const defaultSettings: Settings = {
@@ -28,7 +23,6 @@ const defaultSettings: Settings = {
},
showMenuBarIcon: true,
showDockIcon: true,
schedulingEngine: 'builtin-cron',
enableWakelock: false,
};
@@ -53,7 +47,6 @@ export function saveSettings(settings: Settings): void {
}
}
// Environment management
export function updateEnvironmentVariables(envToggles: EnvToggles): void {
if (envToggles.GOOSE_SERVER__MEMORY) {
process.env.GOOSE_SERVER__MEMORY = 'true';
@@ -67,12 +60,3 @@ export function updateEnvironmentVariables(envToggles: EnvToggles): void {
delete process.env.GOOSE_SERVER__COMPUTER_CONTROLLER;
}
}
export function updateSchedulingEngineEnvironment(schedulingEngine: SchedulingEngine): void {
// Set GOOSE_SCHEDULER_TYPE based on the scheduling engine setting
if (schedulingEngine === 'temporal') {
process.env.GOOSE_SCHEDULER_TYPE = 'temporal';
} else {
process.env.GOOSE_SCHEDULER_TYPE = 'legacy';
}
}