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 -1
View File
@@ -9,7 +9,7 @@ use crate::commands::configure::handle_configure;
use crate::commands::info::handle_info; use crate::commands::info::handle_info;
use crate::commands::project::{handle_project_default, handle_projects_interactive}; use crate::commands::project::{handle_project_default, handle_projects_interactive};
use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate}; use crate::commands::recipe::{handle_deeplink, handle_list, handle_open, handle_validate};
// Import the new handlers from commands::schedule
use crate::commands::schedule::{ use crate::commands::schedule::{
handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove, handle_schedule_add, handle_schedule_cron_help, handle_schedule_list, handle_schedule_remove,
handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop, handle_schedule_run_now, handle_schedule_services_status, handle_schedule_services_stop,
+47 -17
View File
@@ -2,43 +2,73 @@ use anyhow::Result;
use console::style; use console::style;
use goose::config::paths::Paths; use goose::config::paths::Paths;
use goose::config::Config; use goose::config::Config;
use goose::session::session_manager::{DB_NAME, SESSIONS_FOLDER};
use serde_yaml; use serde_yaml;
fn print_aligned(label: &str, value: &str, width: usize) { fn print_aligned(label: &str, value: &str, width: usize) {
println!(" {:<width$} {}", label, value, width = width); println!(" {:<width$} {}", label, value, width = width);
} }
use goose::config::base::CONFIG_YAML_NAME;
use std::fs;
use std::path::Path;
fn check_path_status(path: &Path) -> String {
if path.exists() {
"".to_string()
} else {
let mut current = path.parent();
while let Some(parent) = current {
if parent.exists() {
return match fs::metadata(parent).map(|m| !m.permissions().readonly()) {
Ok(true) => style("missing (can create)").dim().to_string(),
Ok(false) => style("missing (read-only parent)").red().to_string(),
Err(_) => style("missing (cannot check)").red().to_string(),
};
}
current = parent.parent();
}
style("missing (no writable parent)").red().to_string()
}
}
pub fn handle_info(verbose: bool) -> Result<()> { pub fn handle_info(verbose: bool) -> Result<()> {
let logs_dir = Paths::in_state_dir("logs"); let logs_dir = Paths::in_state_dir("logs");
let sessions_dir = Paths::in_data_dir("sessions"); let sessions_dir = Paths::in_data_dir(SESSIONS_FOLDER);
let sessions_db = sessions_dir.join("sessions.db"); let sessions_db = sessions_dir.join(DB_NAME);
// Get paths using a stored reference to the global config
let config = Config::global(); let config = Config::global();
let config_dir = Paths::config_dir().display().to_string(); let config_dir = Paths::config_dir();
let config_yaml_file = config_dir.join(CONFIG_YAML_NAME);
// Define the labels and their corresponding path values once.
let paths = [ let paths = [
("Config dir:", config_dir), ("Config dir:", &config_dir),
("Sessions DB (sqlite):", sessions_db.display().to_string()), ("Config yaml:", &config_yaml_file),
("Logs dir:", logs_dir.display().to_string()), ("Sessions DB (sqlite):", &sessions_db),
("Logs dir:", &logs_dir),
]; ];
// Calculate padding: use the max length of the label plus extra space. let label_padding = paths.iter().map(|(l, _)| l.len()).max().unwrap_or(0) + 4;
let basic_padding = paths.iter().map(|(l, _)| l.len()).max().unwrap_or(0) + 4; let path_padding = paths
.iter()
.map(|(_, p)| p.display().to_string().len())
.max()
.unwrap_or(0)
+ 4;
// Print version information
println!("{}", style("goose Version:").cyan().bold()); println!("{}", style("goose Version:").cyan().bold());
print_aligned("Version:", env!("CARGO_PKG_VERSION"), basic_padding); print_aligned("Version:", env!("CARGO_PKG_VERSION"), label_padding);
println!(); println!();
// Print location information println!("{}", style("Paths:").cyan().bold());
println!("{}", style("goose Locations:").cyan().bold());
for (label, path) in &paths { for (label, path) in &paths {
print_aligned(label, path, basic_padding); println!(
"{:<label_padding$}{:<path_padding$}{}",
label,
path.display(),
check_path_status(path)
);
} }
// Print verbose info if requested
if verbose { if verbose {
println!("\n{}", style("goose Configuration:").cyan().bold()); println!("\n{}", style("goose Configuration:").cyan().bold());
let values = config.all_values()?; let values = config.all_values()?;
+1 -1
View File
@@ -4,7 +4,7 @@ use goose_cli::cli::cli;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
if let Err(e) = goose_cli::logging::setup_logging(None, None) { if let Err(e) = goose_cli::logging::setup_logging(None, None) {
eprintln!("Warning: Failed to initialize telemetry: {}", e); eprintln!("Warning: Failed to initialize logging: {}", e);
} }
let result = cli().await; let result = cli().await;
+1 -4
View File
@@ -28,13 +28,10 @@ async fn shutdown_signal() {
} }
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
// Initialize logging and telemetry
crate::logging::setup_logging(Some("goosed"))?; crate::logging::setup_logging(Some("goosed"))?;
let settings = configuration::Settings::new()?; let settings = configuration::Settings::new()?;
// Initialize pricing cache on startup
tracing::info!("Initializing pricing cache...");
if let Err(e) = initialize_pricing_cache().await { if let Err(e) = initialize_pricing_cache().await {
tracing::warn!( tracing::warn!(
"Failed to initialize pricing cache: {}. Pricing data may not be available.", "Failed to initialize pricing cache: {}. Pricing data may not be available.",
@@ -61,7 +58,7 @@ pub async fn run() -> Result<()> {
let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?; let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?;
info!("listening on {}", listener.local_addr()?); info!("listening on {}", listener.local_addr()?);
// Ensure the listener/socket is properly closed on cancellation by using graceful shutdown
axum::serve(listener, app) axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal()) .with_graceful_shutdown(shutdown_signal())
.await?; .await?;
+16 -29
View File
@@ -1,4 +1,4 @@
use anyhow::{Context, Result}; use anyhow::Result;
use tracing_appender::rolling::Rotation; use tracing_appender::rolling::Rotation;
use tracing_subscriber::{ use tracing_subscriber::{
filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
@@ -20,11 +20,8 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
} else { } else {
format!("{}.log", timestamp) format!("{}.log", timestamp)
}; };
let file_appender = tracing_appender::rolling::RollingFileAppender::new( let file_appender =
Rotation::NEVER, // we do manual rotation via file naming and cleanup_old_logs tracing_appender::rolling::RollingFileAppender::new(Rotation::NEVER, log_dir, log_filename);
log_dir,
log_filename,
);
// Create JSON file logging layer // Create JSON file logging layer
let file_layer = fmt::layer() let file_layer = fmt::layer()
@@ -34,35 +31,27 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
.with_ansi(false) .with_ansi(false)
.with_file(true); .with_file(true);
// Create console logging layer for development - INFO and above only let base_env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new("")
.add_directive("mcp_client=info".parse().unwrap())
.add_directive("goose=debug".parse().unwrap())
.add_directive("goose_server=info".parse().unwrap())
.add_directive("tower_http=info".parse().unwrap())
.add_directive(LevelFilter::WARN.into())
});
let console_layer = fmt::layer() let console_layer = fmt::layer()
.with_writer(std::io::stderr) .with_writer(std::io::stderr)
.with_target(true) .with_target(true)
.with_level(true) .with_level(true)
.with_ansi(true)
.with_file(true) .with_file(true)
.with_ansi(false)
.with_line_number(true) .with_line_number(true)
.pretty(); .pretty();
// Base filter for all logging
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
// Set default levels for different modules
EnvFilter::new("")
// Set mcp-client to DEBUG
.add_directive("mcp_client=debug".parse().unwrap())
// Set goose module to DEBUG
.add_directive("goose=debug".parse().unwrap())
// Set goose-server to INFO
.add_directive("goose_server=info".parse().unwrap())
// Set tower-http to INFO for request logging
.add_directive("tower_http=info".parse().unwrap())
// Set everything else to WARN
.add_directive(LevelFilter::WARN.into())
});
let mut layers = vec![ let mut layers = vec![
file_layer.with_filter(env_filter).boxed(), file_layer.with_filter(base_env_filter.clone()).boxed(),
console_layer.with_filter(LevelFilter::INFO).boxed(), console_layer.with_filter(base_env_filter).boxed(),
]; ];
if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() { if let Ok((otlp_tracing_layer, otlp_metrics_layer, otlp_logs_layer)) = otlp_layer::init_otlp() {
@@ -89,9 +78,7 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
let subscriber = Registry::default().with(layers); let subscriber = Registry::default().with(layers);
subscriber subscriber.try_init()?;
.try_init()
.context("Failed to set global subscriber")?;
Ok(()) Ok(())
} }
+2 -4
View File
@@ -17,6 +17,7 @@ use thiserror::Error;
const KEYRING_SERVICE: &str = "goose"; const KEYRING_SERVICE: &str = "goose";
const KEYRING_USERNAME: &str = "secrets"; const KEYRING_USERNAME: &str = "secrets";
pub const CONFIG_YAML_NAME: &str = "config.yaml";
#[cfg(test)] #[cfg(test)]
const TEST_KEYRING_SERVICE: &str = "goose-test"; const TEST_KEYRING_SERVICE: &str = "goose-test";
@@ -119,9 +120,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
let config_dir = Paths::config_dir(); let config_dir = Paths::config_dir();
std::fs::create_dir_all(&config_dir).expect("Failed to create config directory"); let config_path = config_dir.join(CONFIG_YAML_NAME);
let config_path = config_dir.join("config.yaml");
let secrets = match env::var("GOOSE_DISABLE_KEYRING") { let secrets = match env::var("GOOSE_DISABLE_KEYRING") {
Ok(_) => SecretStorage::File { Ok(_) => SecretStorage::File {
@@ -433,7 +432,6 @@ impl Config {
// Convert to YAML for storage // Convert to YAML for storage
let yaml_value = serde_yaml::to_string(&values)?; let yaml_value = serde_yaml::to_string(&values)?;
// Ensure the directory exists
if let Some(parent) = self.config_path.parent() { if let Some(parent) = self.config_path.parent() {
std::fs::create_dir_all(parent) std::fs::create_dir_all(parent)
.map_err(|e| ConfigError::DirectoryError(e.to_string()))?; .map_err(|e| ConfigError::DirectoryError(e.to_string()))?;
+2 -1
View File
@@ -24,7 +24,8 @@ pub fn prepare_log_directory(component: &str, use_date_subdir: bool) -> Result<P
component_dir component_dir
}; };
fs::create_dir_all(&log_dir).context("Failed to create log directory")?; fs::create_dir_all(&log_dir)
.with_context(|| format!("Failed to create log directory: {:?}", log_dir))?;
Ok(log_dir) Ok(log_dir)
} }
+4 -17
View File
@@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -166,7 +166,6 @@ impl PricingCache {
self.save_to_disk(&cached_data).await?; self.save_to_disk(&cached_data).await?;
// Update memory cache
{ {
let mut cache = self.memory_cache.write().await; let mut cache = self.memory_cache.write().await;
*cache = Some(cached_data); *cache = Some(cached_data);
@@ -179,15 +178,6 @@ impl PricingCache {
pub async fn initialize(&self) -> Result<()> { pub async fn initialize(&self) -> Result<()> {
// Try loading from disk first // Try loading from disk first
if let Ok(Some(cached)) = self.load_from_disk().await { if let Ok(Some(cached)) = self.load_from_disk().await {
// Log how many models we have cached
let total_models: usize = cached.pricing.values().map(|models| models.len()).sum();
tracing::debug!(
"Loaded {} providers with {} total models from disk cache",
cached.pricing.len(),
total_models
);
// Update memory cache
{ {
let mut cache = self.memory_cache.write().await; let mut cache = self.memory_cache.write().await;
*cache = Some(cached); *cache = Some(cached);
@@ -196,8 +186,6 @@ impl PricingCache {
return Ok(()); return Ok(());
} }
// If no disk cache, fetch from OpenRouter
tracing::info!("Fetching pricing data from OpenRouter API");
self.refresh().await self.refresh().await
} }
} }
@@ -213,14 +201,13 @@ lazy_static::lazy_static! {
static ref PRICING_CACHE: PricingCache = PricingCache::new(); static ref PRICING_CACHE: PricingCache = PricingCache::new();
} }
/// Create a properly configured HTTP client for the current runtime fn create_http_client() -> Result<Client> {
fn create_http_client() -> Client {
Client::builder() Client::builder()
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
.pool_idle_timeout(Duration::from_secs(90)) .pool_idle_timeout(Duration::from_secs(90))
.pool_max_idle_per_host(10) .pool_max_idle_per_host(10)
.build() .build()
.expect("Failed to create HTTP client") .map_err(|e| anyhow!(e))
} }
/// OpenRouter model pricing information /// OpenRouter model pricing information
@@ -254,7 +241,7 @@ pub struct OpenRouterModelsResponse {
/// Internal function to fetch pricing data /// Internal function to fetch pricing data
async fn fetch_openrouter_pricing_internal() -> Result<HashMap<String, OpenRouterModel>> { async fn fetch_openrouter_pricing_internal() -> Result<HashMap<String, OpenRouterModel>> {
let client = create_http_client(); let client = create_http_client()?;
let response = client let response = client
.get("https://openrouter.ai/api/v1/models") .get("https://openrouter.ai/api/v1/models")
.send() .send()
+2 -14
View File
@@ -190,13 +190,6 @@ impl Scheduler {
let local_tz = Local::now().timezone(); let local_tz = Local::now().timezone();
tracing::info!(
"Creating cron task for job '{}' cron: '{}' in timezone: {:?}",
job.id,
cron,
local_tz
);
Job::new_async_tz(&cron, local_tz, move |_uuid, _l| { Job::new_async_tz(&cron, local_tz, move |_uuid, _l| {
tracing::info!("Cron task triggered for job '{}'", job_for_task.id); tracing::info!("Cron task triggered for job '{}'", job_for_task.id);
let task_job_id = job_for_task.id.clone(); let task_job_id = job_for_task.id.clone();
@@ -215,7 +208,6 @@ impl Scheduler {
}; };
if !should_execute { if !should_execute {
tracing::info!("Skipping paused job '{}'", task_job_id);
return; return;
} }
@@ -731,15 +723,11 @@ async fn execute_job(
} }
} }
if let Err(e) = SessionManager::update_session(&session.id) SessionManager::update_session(&session.id)
.schedule_id(Some(job.id.clone())) .schedule_id(Some(job.id.clone()))
.recipe(Some(recipe)) .recipe(Some(recipe))
.apply() .apply()
.await .await?;
{
tracing::error!("Failed to update session: {}", e);
}
Ok(session.id) Ok(session.id)
} }
+4 -2
View File
@@ -19,6 +19,8 @@ use tracing::{info, warn};
use utoipa::ToSchema; use utoipa::ToSchema;
const CURRENT_SCHEMA_VERSION: i32 = 5; const CURRENT_SCHEMA_VERSION: i32 = 5;
pub const SESSIONS_FOLDER: &str = "sessions";
pub const DB_NAME: &str = "sessions.db";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
@@ -335,7 +337,7 @@ pub struct SessionStorage {
} }
pub fn ensure_session_dir() -> Result<PathBuf> { pub fn ensure_session_dir() -> Result<PathBuf> {
let session_dir = Paths::data_dir().join("sessions"); let session_dir = Paths::data_dir().join(SESSIONS_FOLDER);
if !session_dir.exists() { if !session_dir.exists() {
fs::create_dir_all(&session_dir)?; fs::create_dir_all(&session_dir)?;
@@ -439,7 +441,7 @@ impl sqlx::FromRow<'_, sqlx::sqlite::SqliteRow> for Session {
impl SessionStorage { impl SessionStorage {
async fn new() -> Result<Self> { async fn new() -> Result<Self> {
let session_dir = ensure_session_dir()?; let session_dir = ensure_session_dir()?;
let db_path = session_dir.join("sessions.db"); let db_path = session_dir.join(DB_NAME);
let storage = if db_path.exists() { let storage = if db_path.exists() {
Self::open(&db_path).await? Self::open(&db_path).await?
-1
View File
@@ -34,7 +34,6 @@ impl Default for OtlpConfig {
impl OtlpConfig { impl OtlpConfig {
pub fn from_config() -> Option<Self> { pub fn from_config() -> Option<Self> {
// Try to get from goose config system (which checks env vars first, then config file)
let config = crate::config::Config::global(); let config = crate::config::Config::global();
// Try to get the endpoint from config (checks OTEL_EXPORTER_OTLP_ENDPOINT env var first) // Try to get the endpoint from config (checks OTEL_EXPORTER_OTLP_ENDPOINT env var first)
@@ -1,6 +1,5 @@
import { ModeSection } from '../mode/ModeSection'; import { ModeSection } from '../mode/ModeSection';
import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection'; import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection';
import SchedulerSection from '../scheduler/SchedulerSection';
import DictationSection from '../dictation/DictationSection'; import DictationSection from '../dictation/DictationSection';
import { SecurityToggle } from '../security/SecurityToggle'; import { SecurityToggle } from '../security/SecurityToggle';
import { ResponseStylesSection } from '../response_styles/ResponseStylesSection'; 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 Choose which scheduling backend to use for scheduled recipes and tasks
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="px-2">
<SchedulerSection />
</CardContent>
</Card> </Card>
<Card className="pb-2 rounded-lg"> <Card className="pb-2 rounded-lg">
<CardHeader className="pb-0"> <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 { createServer } from 'net';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs';
import { getBinaryPath } from './utils/pathUtils'; import { getBinaryPath } from './utils/pathUtils';
import log from './utils/logger'; import log from './utils/logger';
import { App } from 'electron'; import { App } from 'electron';
@@ -11,7 +10,6 @@ import { Buffer } from 'node:buffer';
import { status } from './api'; import { status } from './api';
import { Client } from './api/client'; import { Client } from './api/client';
// Find an available port to start goosed on
export const findAvailablePort = (): Promise<number> => { export const findAvailablePort = (): Promise<number> => {
return new Promise((resolve, _reject) => { return new Promise((resolve, _reject) => {
const server = createServer(); const server = createServer();
@@ -27,11 +25,20 @@ export const findAvailablePort = (): Promise<number> => {
}; };
// 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 (client: Client): Promise<boolean> => { export const checkServerStatus = async (client: Client, errorLog: string[]): Promise<boolean> => {
const interval = 100; // ms 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++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
if (errorLog.some(fatal)) {
log.error('Detected fatal error in server logs');
return false;
}
try { try {
await status({ client, throwOnError: true }); await status({ client, throwOnError: true });
return true; return true;
@@ -48,7 +55,7 @@ export const checkServerStatus = async (client: Client): Promise<boolean> => {
const connectToExternalBackend = async ( const connectToExternalBackend = async (
workingDir: string, workingDir: string,
port: number = 3000 port: number = 3000
): Promise<[number, string, ChildProcess]> => { ): Promise<[number, string, ChildProcess, string[]]> => {
log.info(`Using external goosed backend on port ${port}`); log.info(`Using external goosed backend on port ${port}`);
const mockProcess = { const mockProcess = {
@@ -58,7 +65,7 @@ const connectToExternalBackend = async (
}, },
} as ChildProcess; } as ChildProcess;
return [port, workingDir, mockProcess]; return [port, workingDir, mockProcess, []];
}; };
interface GooseProcessEnv { interface GooseProcessEnv {
@@ -76,39 +83,23 @@ interface GooseProcessEnv {
export const startGoosed = async ( export const startGoosed = async (
app: App, app: App,
serverSecret: string, serverSecret: string,
dir: string | null = null, dir: string,
env: Partial<GooseProcessEnv> = {} env: Partial<GooseProcessEnv> = {}
): Promise<[number, string, ChildProcess]> => { ): Promise<[number, string, ChildProcess, string[]]> => {
const homeDir = os.homedir();
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
const homeDir = os.homedir();
if (!dir) {
dir = homeDir;
}
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); 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'); let goosedPath = getBinaryPath(app, 'goosed');
const resolvedGoosedPath = path.resolve(goosedPath); const resolvedGoosedPath = path.resolve(goosedPath);
const port = await findAvailablePort(); const port = await findAvailablePort();
const stderrLines: string[] = [];
log.info(`Starting goosed from: ${resolvedGoosedPath} on port ${port} in dir ${dir}`); 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) => { 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) => { goosedProcess.on('close', (code: number | null) => {
@@ -215,5 +213,5 @@ export const startGoosed = async (
}); });
log.info(`Goosed server successfully started on port ${port}`); 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, EnvToggles,
loadSettings, loadSettings,
saveSettings, saveSettings,
SchedulingEngine,
updateEnvironmentVariables, updateEnvironmentVariables,
updateSchedulingEngineEnvironment,
} from './utils/settings'; } from './utils/settings';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
// import electron from "electron"; // import electron from "electron";
@@ -502,38 +500,20 @@ const createChat = async (
scheduledJobId?: string, // Scheduled job ID if applicable scheduledJobId?: string, // Scheduled job ID if applicable
recipeId?: string recipeId?: string
) => { ) => {
// Initialize variables for process and configuration updateEnvironmentVariables(envToggles);
let port = 0;
let workingDir = '';
let goosedProcess: import('child_process').ChildProcess | null = null;
{ const envVars = {
// Apply current environment settings before creating chat GOOSE_PATH_ROOT: process.env.GOOSE_PATH_ROOT,
updateEnvironmentVariables(envToggles); };
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({ const mainWindowState = windowStateKeeper({
defaultWidth: 940, // large enough to show the sidebar on launch defaultWidth: 940,
defaultHeight: 800, defaultHeight: 800,
}); });
@@ -594,23 +574,21 @@ const createChat = async (
); );
goosedClients.set(mainWindow.id, goosedClient); goosedClients.set(mainWindow.id, goosedClient);
console.log('[Main] Waiting for backend server to be ready...'); const serverReady = await checkServerStatus(goosedClient, errorLog);
const serverReady = await checkServerStatus(goosedClient);
if (!serverReady) { 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 // Let windowStateKeeper manage the window
mainWindowState.manage(mainWindow); 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.session.setSpellCheckerLanguages(['en-US', 'en-GB']);
mainWindow.webContents.on('context-menu', (_event, params) => { mainWindow.webContents.on('context-menu', (_event, params) => {
const menu = new Menu(); const menu = new Menu();
@@ -1167,22 +1145,6 @@ ipcMain.handle('get-goosed-host-port', async (event) => {
return client.getConfig().baseUrl || null; 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 // Handle menu bar icon visibility
ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => { ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => {
try { try {
-2
View File
@@ -80,7 +80,6 @@ type ElectronAPI = {
getSettings: () => Promise<unknown | null>; getSettings: () => Promise<unknown | null>;
getSecretKey: () => Promise<string>; getSecretKey: () => Promise<string>;
getGoosedHostPort: () => Promise<string | null>; getGoosedHostPort: () => Promise<string | null>;
setSchedulingEngine: (engine: string) => Promise<boolean>;
setWakelock: (enable: boolean) => Promise<boolean>; setWakelock: (enable: boolean) => Promise<boolean>;
getWakelockState: () => Promise<boolean>; getWakelockState: () => Promise<boolean>;
openNotificationsSettings: () => Promise<boolean>; openNotificationsSettings: () => Promise<boolean>;
@@ -186,7 +185,6 @@ const electronAPI: ElectronAPI = {
getSettings: () => ipcRenderer.invoke('get-settings'), getSettings: () => ipcRenderer.invoke('get-settings'),
getSecretKey: () => ipcRenderer.invoke('get-secret-key'), getSecretKey: () => ipcRenderer.invoke('get-secret-key'),
getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'), getGoosedHostPort: () => ipcRenderer.invoke('get-goosed-host-port'),
setSchedulingEngine: (engine: string) => ipcRenderer.invoke('set-scheduling-engine', engine),
setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable), setWakelock: (enable: boolean) => ipcRenderer.invoke('set-wakelock', enable),
getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'), getWakelockState: () => ipcRenderer.invoke('get-wakelock-state'),
openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'), openNotificationsSettings: () => ipcRenderer.invoke('open-notifications-settings'),
-16
View File
@@ -2,23 +2,18 @@ import { app } from 'electron';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
// Types
export interface EnvToggles { export interface EnvToggles {
GOOSE_SERVER__MEMORY: boolean; GOOSE_SERVER__MEMORY: boolean;
GOOSE_SERVER__COMPUTER_CONTROLLER: boolean; GOOSE_SERVER__COMPUTER_CONTROLLER: boolean;
} }
export type SchedulingEngine = 'builtin-cron' | 'temporal';
export interface Settings { export interface Settings {
envToggles: EnvToggles; envToggles: EnvToggles;
showMenuBarIcon: boolean; showMenuBarIcon: boolean;
showDockIcon: boolean; showDockIcon: boolean;
schedulingEngine: SchedulingEngine;
enableWakelock: boolean; enableWakelock: boolean;
} }
// Constants
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
const defaultSettings: Settings = { const defaultSettings: Settings = {
@@ -28,7 +23,6 @@ const defaultSettings: Settings = {
}, },
showMenuBarIcon: true, showMenuBarIcon: true,
showDockIcon: true, showDockIcon: true,
schedulingEngine: 'builtin-cron',
enableWakelock: false, enableWakelock: false,
}; };
@@ -53,7 +47,6 @@ export function saveSettings(settings: Settings): void {
} }
} }
// Environment management
export function updateEnvironmentVariables(envToggles: EnvToggles): void { export function updateEnvironmentVariables(envToggles: EnvToggles): void {
if (envToggles.GOOSE_SERVER__MEMORY) { if (envToggles.GOOSE_SERVER__MEMORY) {
process.env.GOOSE_SERVER__MEMORY = 'true'; process.env.GOOSE_SERVER__MEMORY = 'true';
@@ -67,12 +60,3 @@ export function updateEnvironmentVariables(envToggles: EnvToggles): void {
delete process.env.GOOSE_SERVER__COMPUTER_CONTROLLER; 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';
}
}