Show errors on failure (#5643)
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user