From 755e9f893dc59264f0d1fcb44017a88cd3893fd6 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Wed, 22 Oct 2025 14:25:49 -0400 Subject: [PATCH] Diagnostics (#5323) Co-authored-by: Douwe Osinga --- Cargo.lock | 97 ++++++++++++++++++- crates/goose-cli/src/cli.rs | 16 ++- crates/goose-cli/src/commands/session.rs | 40 +++++++- crates/goose-server/Cargo.toml | 1 - crates/goose-server/src/openapi.rs | 3 +- crates/goose-server/src/routes/health.rs | 14 --- crates/goose-server/src/routes/mod.rs | 4 +- crates/goose-server/src/routes/status.rs | 46 +++++++++ crates/goose/Cargo.toml | 3 + crates/goose/src/session/diagnostics.rs | 62 ++++++++++++ crates/goose/src/session/mod.rs | 2 + ui/desktop/openapi.json | 36 ++++++- ui/desktop/src/api/sdk.gen.ts | 9 +- ui/desktop/src/api/types.gen.ts | 25 +++++ ui/desktop/src/components/ChatInput.tsx | 33 ++++++- .../src/components/ui/DownloadDiagnostics.tsx | 93 ++++++++++++++++++ 16 files changed, 450 insertions(+), 34 deletions(-) delete mode 100644 crates/goose-server/src/routes/health.rs create mode 100644 crates/goose-server/src/routes/status.rs create mode 100644 crates/goose/src/session/diagnostics.rs create mode 100644 ui/desktop/src/components/ui/DownloadDiagnostics.tsx diff --git a/Cargo.lock b/Cargo.lock index 2626ac1b..18578f0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -253,8 +253,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "zstd", - "zstd-safe", + "zstd 0.13.3", + "zstd-safe 7.2.3", ] [[package]] @@ -1075,7 +1075,7 @@ dependencies = [ "arrayvec", "cc", "cfg-if", - "constant_time_eq", + "constant_time_eq 0.3.1", ] [[package]] @@ -1203,6 +1203,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cast" version = "0.3.0" @@ -1524,6 +1544,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -2634,6 +2660,7 @@ dependencies = [ "serial_test", "sha2", "sqlx", + "sys-info", "temp-env", "tempfile", "test-case", @@ -2655,6 +2682,7 @@ dependencies = [ "webbrowser 0.8.15", "winapi", "wiremock", + "zip 0.6.6", ] [[package]] @@ -2810,7 +2838,6 @@ dependencies = [ "chrono", "clap", "config", - "etcetera", "futures", "goose", "goose-mcp", @@ -4650,6 +4677,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -4671,6 +4709,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pem" version = "3.0.5" @@ -6539,6 +6589,16 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sys-info" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "sysinfo" version = "0.32.1" @@ -8580,10 +8640,18 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ + "aes", "byteorder", + "bzip2", + "constant_time_eq 0.1.5", "crc32fast", "crossbeam-utils", "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -8615,13 +8683,32 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + [[package]] name = "zstd" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe", + "zstd-safe 7.2.3", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 5688645e..df7236dd 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -135,7 +135,7 @@ enum SessionCommand { #[arg(short, long, help = "Regex for removing matched sessions (optional)")] regex: Option, }, - #[command(about = "Export a session to Markdown format")] + #[command(about = "Export a session")] Export { #[command(flatten)] identifier: Option, @@ -156,6 +156,16 @@ enum SessionCommand { )] format: String, }, + #[command(name = "diagnostics")] + Diagnostics { + /// Session ID to generate diagnostics for + #[arg(short, long)] + session_id: String, + + /// Output path for the diagnostics zip file (optional, defaults to current directory) + #[arg(short, long)] + output: Option, + }, } #[derive(Subcommand, Debug)] @@ -847,6 +857,10 @@ pub async fn cli() -> Result<()> { .await?; Ok(()) } + Some(SessionCommand::Diagnostics { session_id, output }) => { + crate::commands::session::handle_diagnostics(&session_id, output).await?; + Ok(()) + } None => { let session_start = std::time::Instant::now(); let session_type = if resume { "resumed" } else { "new" }; diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 21d412ae..32875eab 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -2,10 +2,11 @@ use crate::session::message_to_markdown; use anyhow::{Context, Result}; use cliclack::{confirm, multiselect, select}; -use goose::session::{Session, SessionManager}; +use goose::session::{generate_diagnostics, Session, SessionManager}; use goose::utils::safe_truncate; use regex::Regex; use std::fs; +use std::io::Write; use std::path::PathBuf; const TRUNCATED_DESC_LENGTH: usize = 60; @@ -204,10 +205,39 @@ pub async fn handle_session_export( Ok(()) } -/// Convert a list of messages to markdown format for session export -/// -/// This function handles the formatting of a complete session including headers, -/// message organization, and proper tool request/response pairing. + +pub async fn handle_diagnostics(session_id: &str, output_path: Option) -> Result<()> { + println!( + "Generating diagnostics bundle for session '{}'...", + session_id + ); + + let diagnostics_data = generate_diagnostics(session_id).await.with_context(|| { + format!( + "Failed to write to generate diagnostics bundle for session '{}'", + session_id + ) + })?; + + let output_file = if let Some(path) = output_path { + path.clone() + } else { + PathBuf::from(format!("diagnostics_{}.zip", session_id)) + }; + + let mut file = fs::File::create(&output_file).context(format!( + "Failed to create output file: {}", + output_file.display() + ))?; + + file.write_all(&diagnostics_data) + .context("Failed to write diagnostics data")?; + + println!("Diagnostics bundle saved to: {}", output_file.display()); + + Ok(()) +} + fn export_session_to_markdown( messages: Vec, session_name: &String, diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index eefd68f9..59d4f204 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -33,7 +33,6 @@ base64 = "0.21" config = { version = "0.14.1", features = ["toml"] } thiserror = "1.0" clap = { version = "4.4", features = ["derive"] } -etcetera = "0.8.0" serde_yaml = "0.9.34" utoipa = { version = "4.1", features = ["axum_extras", "chrono"] } reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "multipart"], default-features = false } diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index dade7c5a..0cb509f8 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -322,7 +322,8 @@ derive_utoipa!(Icon as IconSchema); #[derive(OpenApi)] #[openapi( paths( - super::routes::health::status, + super::routes::status::status, + super::routes::status::diagnostics, super::routes::config_management::backup_config, super::routes::config_management::recover_config, super::routes::config_management::validate_config, diff --git a/crates/goose-server/src/routes/health.rs b/crates/goose-server/src/routes/health.rs deleted file mode 100644 index 13723024..00000000 --- a/crates/goose-server/src/routes/health.rs +++ /dev/null @@ -1,14 +0,0 @@ -use axum::{routing::get, Router}; - -#[utoipa::path(get, path = "/status", - responses( - (status = 200, description = "ok", body = String), - ) -)] -async fn status() -> String { - "ok".to_string() -} - -pub fn routes() -> Router { - Router::new().route("/status", get(status)) -} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index 1d34dc29..85afda5c 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -4,13 +4,13 @@ pub mod config_management; pub mod context; pub mod errors; pub mod extension; -pub mod health; pub mod recipe; pub mod recipe_utils; pub mod reply; pub mod schedule; pub mod session; pub mod setup; +pub mod status; pub mod utils; use std::sync::Arc; @@ -19,7 +19,7 @@ use axum::Router; // Function to configure all routes pub fn configure(state: Arc) -> Router { Router::new() - .merge(health::routes()) + .merge(status::routes()) .merge(reply::routes(state.clone())) .merge(agent::routes(state.clone())) .merge(audio::routes(state.clone())) diff --git a/crates/goose-server/src/routes/status.rs b/crates/goose-server/src/routes/status.rs new file mode 100644 index 00000000..110d4c7a --- /dev/null +++ b/crates/goose-server/src/routes/status.rs @@ -0,0 +1,46 @@ +use axum::body::Body; +use axum::http::HeaderValue; +use axum::response::IntoResponse; +use axum::{extract::Path, http::StatusCode, routing::get, Router}; +use goose::session::generate_diagnostics; + +#[utoipa::path(get, path = "/status", + responses( + (status = 200, description = "ok", body = String), + ) +)] +async fn status() -> String { + "ok".to_string() +} + +#[utoipa::path(get, path = "/diagnostics/{session_id}", + responses( + (status = 200, description = "Diagnostics zip file", content_type = "application/zip", body = Vec), + (status = 500, description = "Failed to generate diagnostics"), + ) +)] +async fn diagnostics(Path(session_id): Path) -> impl IntoResponse { + match generate_diagnostics(&session_id).await { + Ok(zip_data) => { + let filename = format!("attachment; filename=\"diagnostics_{}.zip\"", session_id); + let headers = [ + ( + http::header::CONTENT_TYPE, + HeaderValue::from_static("application/zip"), + ), + ( + http::header::CONTENT_DISPOSITION, + HeaderValue::from_str(&filename).map_err(|_e| StatusCode::BAD_REQUEST)?, + ), + ]; + + Ok((headers, Body::from(zip_data))) + } + Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), + } +} +pub fn routes() -> Router { + Router::new() + .route("/status", get(status)) + .route("/diagnostics/{session_id}", get(diagnostics)) +} diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 7e492b68..e70471c8 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -99,11 +99,14 @@ dashmap = "6.1" ahash = "0.8" tokio-util = "0.7.15" unicode-normalization = "0.1" +zip = "0.6" +sys-info = "0.9" oauth2 = "5.0.0" schemars = { version = "1.0.4", default-features = false, features = ["derive"] } insta = "1.43.2" + [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } diff --git a/crates/goose/src/session/diagnostics.rs b/crates/goose/src/session/diagnostics.rs new file mode 100644 index 00000000..1e807e01 --- /dev/null +++ b/crates/goose/src/session/diagnostics.rs @@ -0,0 +1,62 @@ +use crate::config::paths::Paths; +use crate::session::SessionManager; +use std::fs::{self}; +use std::io::Cursor; +use std::io::Write; +use zip::write::FileOptions; +use zip::ZipWriter; + +pub async fn generate_diagnostics(session_id: &str) -> anyhow::Result> { + let logs_dir = Paths::in_state_dir("logs"); + let config_dir = Paths::config_dir(); + let config_path = config_dir.join("config.yaml"); + + let system_info = format!( + "App Version: {}\n\ + OS: {}\n\ + OS Version: {}\n\ + Architecture: {}\n\ + Timestamp: {}\n", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + sys_info::os_release().unwrap_or_else(|_| "unknown".to_string()), + std::env::consts::ARCH, + chrono::Utc::now().to_rfc3339() + ); + + let mut buffer = Vec::new(); + { + let mut zip = ZipWriter::new(Cursor::new(&mut buffer)); + let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + let mut log_files: Vec<_> = fs::read_dir(&logs_dir)? + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "jsonl")) + .collect(); + + log_files.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok())); + + for entry in log_files.iter().rev().take(3) { + let path = entry.path(); + let name = path.file_name().unwrap().to_str().unwrap(); + zip.start_file(format!("logs/{}", name), options)?; + zip.write_all(&fs::read(&path)?)?; + } + + let session_data = SessionManager::export_session(session_id).await?; + zip.start_file("session.json", options)?; + zip.write_all(session_data.as_bytes())?; + + if config_path.exists() { + zip.start_file("config.yaml", options)?; + zip.write_all(&fs::read(&config_path)?)?; + } + + zip.start_file("system.txt", options)?; + zip.write_all(system_info.as_bytes())?; + + zip.finish()?; + } + + Ok(buffer) +} diff --git a/crates/goose/src/session/mod.rs b/crates/goose/src/session/mod.rs index 221f89d7..502f754d 100644 --- a/crates/goose/src/session/mod.rs +++ b/crates/goose/src/session/mod.rs @@ -1,6 +1,8 @@ +mod diagnostics; pub mod extension_data; mod legacy; pub mod session_manager; +pub use diagnostics::generate_diagnostics; pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState}; pub use session_manager::{Session, SessionInsights, SessionManager}; diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 22c72d2c..05a4c000 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -896,6 +896,40 @@ ] } }, + "/diagnostics/{session_id}": { + "get": { + "tags": [ + "super::routes::status" + ], + "operationId": "diagnostics", + "parameters": [ + { + "name": "session_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Diagnostics zip file", + "content": { + "application/zip": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "500": { + "description": "Failed to generate diagnostics" + } + } + } + }, "/handle_openrouter": { "post": { "tags": [ @@ -1972,7 +2006,7 @@ "/status": { "get": { "tags": [ - "super::routes::health" + "super::routes::status" ], "operationId": "status", "responses": { diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index c965aaba..23b33eac 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ManageContextData, ManageContextErrors, ManageContextResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, ManageContextData, ManageContextErrors, ManageContextResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionDescriptionData, UpdateSessionDescriptionErrors, UpdateSessionDescriptionResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -256,6 +256,13 @@ export const manageContext = (options: Opt }); }; +export const diagnostics = (options: Options) => { + return (options.client ?? client).get({ + url: '/diagnostics/{session_id}', + ...options + }); +}; + export const startOpenrouterSetup = (options?: Options) => { return (options?.client ?? client).post({ url: '/handle_openrouter', diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 542e03cb..04299bb4 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -1594,6 +1594,31 @@ export type ManageContextResponses = { export type ManageContextResponse = ManageContextResponses[keyof ManageContextResponses]; +export type DiagnosticsData = { + body?: never; + path: { + session_id: string; + }; + query?: never; + url: '/diagnostics/{session_id}'; +}; + +export type DiagnosticsErrors = { + /** + * Failed to generate diagnostics + */ + 500: unknown; +}; + +export type DiagnosticsResponses = { + /** + * Diagnostics zip file + */ + 200: Blob | File; +}; + +export type DiagnosticsResponse = DiagnosticsResponses[keyof DiagnosticsResponses]; + export type StartOpenrouterSetupData = { body?: never; path?: never; diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 7419f099..f4d8ade5 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1,5 +1,5 @@ import React, { useRef, useState, useEffect, useMemo, useCallback } from 'react'; -import { FolderKey, ScrollText } from 'lucide-react'; +import { Bug, FolderKey, ScrollText } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip'; import { Button } from './ui/button'; import type { View } from '../utils/navigationUtils'; @@ -8,7 +8,6 @@ import { Attach, Send, Close, Microphone } from './icons'; import { ChatState } from '../types/chatState'; import debounce from 'lodash/debounce'; import { LocalMessageStorage } from '../utils/localMessageStorage'; -import { Message } from '../api'; import { DirSwitcher } from './bottom_menu/DirSwitcher'; import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar'; import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection'; @@ -28,6 +27,8 @@ import { DroppedFile, useFileDrop } from '../hooks/useFileDrop'; import { Recipe } from '../recipe'; import MessageQueue from './MessageQueue'; import { detectInterruption } from '../utils/interruptionDetector'; +import { DiagnosticsModal } from './ui/DownloadDiagnostics'; +import { Message } from '../api'; interface QueuedMessage { id: string; @@ -149,6 +150,8 @@ export default function ChatInput({ const agentIsReady = chatContext === null || chatContext.agentWaitingMessage === null; const draftLoadedRef = useRef(false); + const [diagnosticsOpen, setDiagnosticsOpen] = useState(false); + // Debug logging for draft context useEffect(() => { // Debug logging removed - draft functionality is working correctly @@ -1548,7 +1551,6 @@ export default function ChatInput({
- {/* Attach button */} + + Generate diagnostics bundle + + )}
+ {sessionId && diagnosticsOpen && ( + setDiagnosticsOpen(false)} + sessionId={sessionId} + /> + )} + void; + sessionId: string; +} + +export const DiagnosticsModal: React.FC = ({ + isOpen, + onClose, + sessionId, +}) => { + const [isDownloading, setIsDownloading] = useState(false); + + const handleDownload = async () => { + setIsDownloading(true); + + try { + const response = await diagnostics({ + path: { session_id: sessionId }, + throwOnError: true, + }); + + const blob = new Blob([response.data], { type: 'application/zip' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `diagnostics_${sessionId}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + + onClose(); + } catch { + toastError({ + title: 'Diagnostics Error', + msg: 'Failed to download diagnostics', + }); + } finally { + setIsDownloading(false); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+ +
+

Download Diagnostics

+

+ Hit the download button to get a zip file containing all the important information to + diagnose a problem in goose. You can share this file with the team or if you are a + developer look at it yourself. +

+
    +
  • Basic system info
  • +
  • Your current session messages
  • +
  • Recent log files
  • +
  • Configuration settings
  • +
+

+ Warning: If your session contains sensitive information, do not share + this file publicly. +

+
+
+
+ + +
+
+
+ ); +};