diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index bcee3d2309e..52212dee5a3 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -750,8 +750,8 @@ pub fn configure_extensions_dialog() -> Result<(), Box> { let extension = cliclack::select("Which built-in extension would you like to enable?") .item( "autovisualiser", - "Auto Visualizer", - "Data visualization and UI generation tools", + "Auto Visualiser", + "Data visualisation and UI generation tools", ) .item( "computercontroller", diff --git a/crates/goose-server/src/routes/audio.rs b/crates/goose-server/src/routes/audio.rs index 8b2046f2af5..e0071249b79 100644 --- a/crates/goose-server/src/routes/audio.rs +++ b/crates/goose-server/src/routes/audio.rs @@ -42,13 +42,35 @@ struct WhisperResponse { text: String, } -/// Validate audio input and return decoded bytes and file extension -fn validate_audio_input( - audio: &str, - mime_type: &str, -) -> Result<(Vec, &'static str), StatusCode> { +/// Transcribe audio using OpenAI's Whisper API +/// +/// # Request +/// - `audio`: Base64 encoded audio data +/// - `mime_type`: MIME type of the audio (e.g., "audio/webm", "audio/wav") +/// +/// # Response +/// - `text`: Transcribed text from the audio +/// +/// # Errors +/// - 401: Unauthorized (missing or invalid X-Secret-Key header) +/// - 412: Precondition Failed (OpenAI API key not configured) +/// - 400: Bad Request (invalid base64 audio data) +/// - 413: Payload Too Large (audio file exceeds 25MB limit) +/// - 415: Unsupported Media Type (unsupported audio format) +/// - 502: Bad Gateway (OpenAI API error) +/// - 503: Service Unavailable (network error) +async fn transcribe_handler( + State(state): State>, + headers: HeaderMap, + Json(request): Json, +) -> Result, StatusCode> { + verify_secret_key(&headers, &state)?; + + // Validate input first before checking API key configuration // Decode the base64 audio data - let audio_bytes = BASE64.decode(audio).map_err(|_| StatusCode::BAD_REQUEST)?; + let audio_bytes = BASE64 + .decode(&request.audio) + .map_err(|_| StatusCode::BAD_REQUEST)?; // Check file size if audio_bytes.len() > MAX_AUDIO_SIZE_BYTES { @@ -61,9 +83,8 @@ fn validate_audio_input( } // Determine file extension based on MIME type - let file_extension = match mime_type { + let file_extension = match request.mime_type.as_str() { "audio/webm" => "webm", - "audio/webm;codecs=opus" => "webm", "audio/mp4" => "mp4", "audio/mpeg" => "mp3", "audio/mpga" => "mpga", @@ -73,18 +94,13 @@ fn validate_audio_input( _ => return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE), }; - Ok((audio_bytes, file_extension)) -} - -/// Get OpenAI configuration (API key and host) -fn get_openai_config() -> Result<(String, String), StatusCode> { + // Get the OpenAI API key from config (after input validation) let config = goose::config::Config::global(); + let api_key: String = config + .get_secret("OPENAI_API_KEY") + .map_err(|_| StatusCode::PRECONDITION_FAILED)?; - let api_key: String = config.get_secret("OPENAI_API_KEY").map_err(|e| { - tracing::error!("Failed to get OpenAI API key: {:?}", e); - StatusCode::PRECONDITION_FAILED - })?; - + // Get the OpenAI host from config (with default) let openai_host = match config.get("OPENAI_HOST", false) { Ok(value) => value .as_str() @@ -93,41 +109,19 @@ fn get_openai_config() -> Result<(String, String), StatusCode> { Err(_) => "https://api.openai.com".to_string(), }; - Ok((api_key, openai_host)) -} - -/// Send transcription request to OpenAI Whisper API -async fn send_openai_request( - audio_bytes: Vec, - file_extension: &str, - mime_type: &str, - api_key: &str, - openai_host: &str, -) -> Result { - tracing::info!("Using OpenAI host: {}", openai_host); - tracing::info!( - "Audio file size: {} bytes, extension: {}, mime_type: {}", - audio_bytes.len(), - file_extension, - mime_type - ); + tracing::debug!("Using OpenAI host: {}", openai_host); // Create a multipart form with the audio file let part = reqwest::multipart::Part::bytes(audio_bytes) .file_name(format!("audio.{}", file_extension)) - .mime_str(mime_type) - .map_err(|e| { - tracing::error!("Failed to create multipart part: {:?}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; + .mime_str(&request.mime_type) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let form = reqwest::multipart::Form::new() .part("file", part) .text("model", "whisper-1") .text("response_format", "json"); - tracing::info!("Created multipart form for OpenAI Whisper API"); - // Make request to OpenAI Whisper API let client = Client::builder() .timeout(Duration::from_secs(OPENAI_TIMEOUT_SECONDS)) @@ -137,11 +131,6 @@ async fn send_openai_request( StatusCode::INTERNAL_SERVER_ERROR })?; - tracing::info!( - "Sending request to OpenAI: {}/v1/audio/transcriptions", - openai_host - ); - let response = client .post(format!("{}/v1/audio/transcriptions", openai_host)) .header("Authorization", format!("Bearer {}", api_key)) @@ -161,25 +150,9 @@ async fn send_openai_request( } })?; - tracing::info!( - "Received response from OpenAI with status: {}", - response.status() - ); - if !response.status().is_success() { - let status = response.status(); let error_text = response.text().await.unwrap_or_default(); - tracing::error!("OpenAI API error (status: {}): {}", status, error_text); - - // Check for specific error codes - if status == 401 { - tracing::error!("OpenAI API key appears to be invalid or unauthorized"); - return Err(StatusCode::UNAUTHORIZED); - } else if status == 429 { - tracing::error!("OpenAI API quota or rate limit exceeded"); - return Err(StatusCode::TOO_MANY_REQUESTS); - } - + tracing::error!("OpenAI API error: {}", error_text); return Err(StatusCode::BAD_GATEWAY); } @@ -188,45 +161,6 @@ async fn send_openai_request( StatusCode::INTERNAL_SERVER_ERROR })?; - Ok(whisper_response) -} - -/// Transcribe audio using OpenAI's Whisper API -/// -/// # Request -/// - `audio`: Base64 encoded audio data -/// - `mime_type`: MIME type of the audio (e.g., "audio/webm", "audio/wav") -/// -/// # Response -/// - `text`: Transcribed text from the audio -/// -/// # Errors -/// - 401: Unauthorized (missing or invalid X-Secret-Key header) -/// - 412: Precondition Failed (OpenAI API key not configured) -/// - 400: Bad Request (invalid base64 audio data) -/// - 413: Payload Too Large (audio file exceeds 25MB limit) -/// - 415: Unsupported Media Type (unsupported audio format) -/// - 502: Bad Gateway (OpenAI API error) -/// - 503: Service Unavailable (network error) -async fn transcribe_handler( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - - let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?; - let (api_key, openai_host) = get_openai_config()?; - - let whisper_response = send_openai_request( - audio_bytes, - file_extension, - &request.mime_type, - &api_key, - &openai_host, - ) - .await?; - Ok(Json(TranscribeResponse { text: whisper_response.text, })) @@ -243,13 +177,39 @@ async fn transcribe_elevenlabs_handler( ) -> Result, StatusCode> { verify_secret_key(&headers, &state)?; - let (audio_bytes, file_extension) = validate_audio_input(&request.audio, &request.mime_type)?; + // Validate input first before checking API key configuration + // Decode the base64 audio data + let audio_bytes = BASE64 + .decode(&request.audio) + .map_err(|_| StatusCode::BAD_REQUEST)?; + + // Check file size + if audio_bytes.len() > MAX_AUDIO_SIZE_BYTES { + tracing::warn!( + "Audio file too large: {} bytes (max: {} bytes)", + audio_bytes.len(), + MAX_AUDIO_SIZE_BYTES + ); + return Err(StatusCode::PAYLOAD_TOO_LARGE); + } + + // Determine file extension and content type based on MIME type + let (file_extension, content_type) = match request.mime_type.as_str() { + "audio/webm" => ("webm", "audio/webm"), + "audio/mp4" => ("mp4", "audio/mp4"), + "audio/mpeg" => ("mp3", "audio/mpeg"), + "audio/mpga" => ("mp3", "audio/mpeg"), + "audio/m4a" => ("m4a", "audio/m4a"), + "audio/wav" => ("wav", "audio/wav"), + "audio/x-wav" => ("wav", "audio/wav"), + _ => return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE), + }; // Get the ElevenLabs API key from config (after input validation) let config = goose::config::Config::global(); // First try to get it as a secret - let api_key: String = match config.get_secret::("ELEVENLABS_API_KEY") { + let api_key: String = match config.get_secret("ELEVENLABS_API_KEY") { Ok(key) => key, Err(_) => { // Try to get it as non-secret (for backward compatibility) @@ -257,6 +217,7 @@ async fn transcribe_elevenlabs_handler( Ok(value) => { match value.as_str() { Some(key_str) => { + tracing::info!("Migrating ElevenLabs API key to secret storage"); let key = key_str.to_string(); // Migrate to secret storage if let Err(e) = config.set( @@ -267,25 +228,17 @@ async fn transcribe_elevenlabs_handler( tracing::error!("Failed to migrate ElevenLabs API key: {:?}", e); } // Delete the non-secret version - if let Err(e) = config.delete("ELEVENLABS_API_KEY") { - tracing::warn!( - "Failed to delete non-secret ElevenLabs API key: {:?}", - e - ); - } + let _ = config.delete("ELEVENLABS_API_KEY"); key } None => { - tracing::error!( - "ElevenLabs API key is not a string, found: {:?}", - value - ); + tracing::error!("ElevenLabs API key is not a string"); return Err(StatusCode::PRECONDITION_FAILED); } } } - Err(_) => { - tracing::error!("No ElevenLabs API key found in configuration"); + Err(e) => { + tracing::error!("Failed to get ElevenLabs API key from config: {:?}", e); return Err(StatusCode::PRECONDITION_FAILED); } } @@ -295,7 +248,7 @@ async fn transcribe_elevenlabs_handler( // Create multipart form for ElevenLabs API let part = reqwest::multipart::Part::bytes(audio_bytes) .file_name(format!("audio.{}", file_extension)) - .mime_str(&request.mime_type) + .mime_str(content_type) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let form = reqwest::multipart::Form::new() @@ -333,9 +286,8 @@ async fn transcribe_elevenlabs_handler( })?; if !response.status().is_success() { - let status = response.status(); let error_text = response.text().await.unwrap_or_default(); - tracing::error!("ElevenLabs API error (status: {}): {}", status, error_text); + tracing::error!("ElevenLabs API error: {}", error_text); // Check for specific error codes if error_text.contains("Unauthorized") || error_text.contains("Invalid API key") { @@ -378,13 +330,16 @@ async fn check_dictation_config( let config = goose::config::Config::global(); // Check if ElevenLabs API key is configured - let has_elevenlabs = match config.get_secret::("ELEVENLABS_API_KEY") { - Ok(_) => true, - Err(_) => { + let has_elevenlabs = config + .get_secret::("ELEVENLABS_API_KEY") + .map(|_| true) + .unwrap_or_else(|_| { // Check non-secret for backward compatibility - config.get("ELEVENLABS_API_KEY", false).is_ok() - } - }; + config + .get("ELEVENLABS_API_KEY", false) + .map(|_| true) + .unwrap_or(false) + }); Ok(Json(serde_json::json!({ "elevenlabs": has_elevenlabs diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 570ae5bf349..0224cc79565 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -167,7 +167,6 @@ pub async fn read_config( } let config = Config::global(); - let response_value = match config.get(&query.key, query.is_secret) { Ok(value) => { if query.is_secret { @@ -183,9 +182,7 @@ pub async fn read_config( Value::Null } } - Err(_) => { - return Err(StatusCode::INTERNAL_SERVER_ERROR); - } + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), }; Ok(Json(response_value)) } diff --git a/crates/goose/src/agents/extension_malware_check.rs b/crates/goose/src/agents/extension_malware_check.rs deleted file mode 100644 index 29b8d566c51..00000000000 --- a/crates/goose/src/agents/extension_malware_check.rs +++ /dev/null @@ -1,511 +0,0 @@ -use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use tracing::{debug, error, trace}; - -use crate::agents::extension::ExtensionError; - -#[derive(Clone)] -pub struct OsvChecker { - client: reqwest::Client, - endpoint: Url, -} - -impl OsvChecker { - /// Constructs a checker. Honors OSV_ENDPOINT env var if present. - pub fn new() -> Result> { - let client = http_client().map_err(Box::new)?; - let endpoint = std::env::var("OSV_ENDPOINT") - .ok() - .and_then(|s| Url::parse(&s).ok()) - .unwrap_or_else(|| Url::parse(DEFAULT_OSV_ENDPOINT).expect("valid default OSV url")); - Ok(Self { client, endpoint }) - } - - /// Constructs with a custom endpoint (handy for tests). - pub fn with_endpoint(endpoint: Url) -> Result> { - let client = http_client().map_err(Box::new)?; - Ok(Self { client, endpoint }) - } - - /// Query OSV and **fail** if any MAL-* advisories are found. - /// - `ecosystem`: e.g., "npm", "PyPI" - /// - `version`: if `None`, checks by name only. - pub async fn deny_if_malicious( - &self, - name: &str, - ecosystem: &str, - version: Option<&str>, - ) -> Result<(), ExtensionError> { - deny_if_malicious_impl(&self.client, &self.endpoint, name, ecosystem, version).await - } -} - -/// Convenience: infer ecosystem from command token + parse first package arg. -/// - ends_with("npx") → npm -/// - ends_with("uvx") → PyPI -/// unknown commands → skip (fail open) -pub async fn deny_if_malicious_cmd_args(cmd: &str, args: &[String]) -> Result<(), ExtensionError> { - let ecosystem = if cmd.ends_with("uvx") { - "PyPI" - } else if cmd.ends_with("npx") { - "npm" - } else { - debug!(%cmd, ?args, "Unknown ecosystem for command; skipping OSV check (fail open)."); - return Ok(()); - }; - - if let Some((name, version)) = parse_first_package_arg(ecosystem, args) { - OsvChecker::new() - .map_err(|e| *e)? - .deny_if_malicious(&name, ecosystem, version.as_deref()) - .await?; - } else { - debug!(%cmd, ?args, "No package token found; skipping OSV check."); - } - - Ok(()) -} - -/// Direct call without command inference. -pub async fn deny_if_malicious( - name: &str, - ecosystem: &str, - version: Option<&str>, -) -> Result<(), ExtensionError> { - OsvChecker::new() - .map_err(|e| *e)? - .deny_if_malicious(name, ecosystem, version) - .await -} - -fn parse_first_package_arg(ecosystem: &str, args: &[String]) -> Option<(String, Option)> { - let is_flag = |s: &str| s.starts_with('-'); - let token = args - .iter() - .find(|a| !is_flag(a.as_str()))? - .trim() - .to_string(); - if token.is_empty() { - return None; - } - match ecosystem { - "npm" => parse_npm_token(&token), - "PyPI" => parse_pypi_token(&token), - _ => None, - } -} - -fn parse_npm_token(token: &str) -> Option<(String, Option)> { - // Handles: - // react@18.3.1 - // @scope/pkg@1.2.3 (split at the LAST '@') - // eslint (no version) - if token.starts_with('@') { - if let Some(idx) = token.rfind('@') { - if idx > 0 { - let (name, ver) = token.split_at(idx); - let ver = ver.trim_start_matches('@'); - if !ver.is_empty() && ver != "latest" { - return Some((name.to_string(), Some(ver.to_string()))); - } else { - return Some((name.to_string(), None)); - } - } - } - Some((token.to_string(), None)) - } else if let Some(idx) = token.find('@') { - let (name, ver) = token.split_at(idx); - let ver = ver.trim_start_matches('@'); - if !name.is_empty() { - if !ver.is_empty() && ver != "latest" { - return Some((name.to_string(), Some(ver.to_string()))); - } else { - return Some((name.to_string(), None)); - } - } - None - } else { - Some((token.to_string(), None)) - } -} - -fn parse_pypi_token(token: &str) -> Option<(String, Option)> { - // Accept exact pins: - // package==1.2.3 - // package[extra]==1.2.3 - // Treat "latest" as None. Ignore other specifiers (>=, <=, ~=, !=) for pinning. - let lowered = token.to_ascii_lowercase(); - if let Some(idx) = lowered.find("==") { - let (name, ver) = token.split_at(idx); - let ver = ver.trim_start_matches('=').trim_start_matches('='); - let name = name.trim(); - if name.is_empty() { - return None; - } - if ver.is_empty() || ver.eq_ignore_ascii_case("latest") { - return Some((name.to_string(), None)); - } - return Some((name.to_string(), Some(ver.to_string()))); - } - Some((token.to_string(), None)) -} - -const DEFAULT_OSV_ENDPOINT: &str = "https://api.osv.dev/v1/query"; - -#[derive(Serialize)] -struct QueryReq<'a> { - #[serde(skip_serializing_if = "Option::is_none")] - version: Option<&'a str>, - package: Package<'a>, - #[serde(skip_serializing_if = "Option::is_none")] - page_token: Option, -} - -#[derive(Serialize)] -struct Package<'a> { - name: &'a str, - ecosystem: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - purl: Option<&'a str>, -} - -#[derive(Deserialize)] -struct QueryResp { - #[serde(default)] - vulns: Vec, - #[serde(default)] - next_page_token: Option, -} - -#[derive(Deserialize)] -struct Vuln { - id: String, - #[serde(default)] - summary: String, -} - -async fn deny_if_malicious_impl( - client: &reqwest::Client, - endpoint: &Url, - name: &str, - ecosystem: &str, - version: Option<&str>, -) -> Result<(), ExtensionError> { - debug!(name, ecosystem, ?version, "OSV query starting"); - let mut page_token: Option = None; - let mut mal: Vec = Vec::new(); - - loop { - let body = QueryReq { - version, - package: Package { - name, - ecosystem, - purl: None, - }, - page_token: page_token.clone(), - }; - trace!(?body.page_token, "OSV page"); - - let resp = match client.post(endpoint.clone()).json(&body).send().await { - Ok(r) => r, - Err(e) => { - error!(%e, name, ecosystem, ?version, "OSV request failed; failing open."); - return Ok(()); - } - }; - - let resp = match resp.error_for_status() { - Ok(r) => r, - Err(e) => { - error!(%e, name, ecosystem, ?version, "OSV HTTP error; failing open."); - return Ok(()); - } - }; - - let payload: QueryResp = match resp.json().await { - Ok(p) => p, - Err(e) => { - error!(%e, name, ecosystem, ?version, "OSV JSON parse error; failing open."); - return Ok(()); - } - }; - - mal.extend( - payload - .vulns - .into_iter() - .filter(|v| v.id.starts_with("MAL-")), - ); - - match payload.next_page_token { - Some(tok) if !tok.is_empty() => page_token = Some(tok), - _ => break, - } - } - - if !mal.is_empty() { - let ver = version.unwrap_or(""); - let details = mal - .into_iter() - .map(|v| { - if v.summary.is_empty() { - v.id - } else { - format!("{} — {}", v.id, v.summary) - } - }) - .collect::>() - .join("; "); - error!(name, ecosystem, version=%ver, %details, "Blocked malicious package via OSV MAL-*."); - return Err(ExtensionError::ConfigError(format!( - "Blocked malicious package: {name}@{ver} ({ecosystem}). OSV MAL advisories: {details}" - ))); - } - - debug!(name, ecosystem, ?version, "OSV: no MAL advisories."); - Ok(()) -} - -#[allow(clippy::result_large_err)] -fn http_client() -> Result { - let mut headers = HeaderMap::new(); - headers.insert( - USER_AGENT, - HeaderValue::from_static("goose-osv-check/1.1 (+https://osv.dev)"), - ); - reqwest::Client::builder() - .default_headers(headers) - .timeout(std::time::Duration::from_secs(10)) - .build() - .map_err(|e| ExtensionError::SetupError(format!("failed to build HTTP client: {e}"))) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use serial_test; - use tokio; - use wiremock::matchers::{method, path}; - use wiremock::{Mock, MockServer, ResponseTemplate}; - - fn checker_for(server: &MockServer) -> OsvChecker { - let url = Url::parse(&format!("{}/v1/query", server.uri())).unwrap(); - OsvChecker::with_endpoint(url).unwrap() - } - - // Helper to temporarily set an environment variable and restore it on drop - struct TempEnvVar { - key: String, - original: Option, - } - - impl TempEnvVar { - fn set(key: &str, value: &str) -> Self { - let original = std::env::var(key).ok(); - std::env::set_var(key, value); - Self { - key: key.to_string(), - original, - } - } - } - - impl Drop for TempEnvVar { - fn drop(&mut self) { - match &self.original { - Some(val) => std::env::set_var(&self.key, val), - None => std::env::remove_var(&self.key), - } - } - } - - #[tokio::test] - async fn allows_clean_package() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [], - "next_page_token": null - }))) - .mount(&server) - .await; - - let c = checker_for(&server); - let res = c - .deny_if_malicious("some_clean_package", "PyPI", None) - .await; - assert!(res.is_ok()); - } - - #[tokio::test] - async fn blocks_malicious_package() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [ { "id": "MAL-1234", "summary": "Malicious package" } ], - "next_page_token": null - }))) - .mount(&server) - .await; - - let c = checker_for(&server); - let res = c - .deny_if_malicious("bad_package", "PyPI", Some("1.0.0")) - .await; - assert!(res.is_err()); - let msg = format!("{:?}", res.unwrap_err()); - assert!(msg.contains("Blocked malicious package")); - assert!(msg.contains("MAL-1234")); - } - - #[tokio::test] - #[serial_test::serial] - async fn cmd_args_pypi_clean() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [], - "next_page_token": null - }))) - .mount(&server) - .await; - - // Use env var so OsvChecker::new() picks it up - let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri())); - let args = vec!["some_clean_package==1.2.3".to_string()]; - let res = deny_if_malicious_cmd_args("uvx", &args).await; - assert!(res.is_ok()); - } - - #[tokio::test] - #[serial_test::serial] - async fn cmd_args_npm_scoped_malicious() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [ { "id": "MAL-9999", "summary": "Malicious npm package" } ], - "next_page_token": null - }))) - .mount(&server) - .await; - - let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri())); - let args = vec!["@scope/pkg@2.0.0".to_string()]; - let res = deny_if_malicious_cmd_args("npx", &args).await; - assert!(res.is_err()); - let msg = format!("{:?}", res.unwrap_err()); - assert!(msg.contains("Blocked malicious package")); - assert!(msg.contains("MAL-9999")); - } - - #[tokio::test] - #[serial_test::serial] - async fn cmd_args_skip_flags_then_parse() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [], - "next_page_token": null - }))) - .mount(&server) - .await; - - let _env = TempEnvVar::set("OSV_ENDPOINT", &format!("{}/v1/query", server.uri())); - let args = vec![ - "--dry-run".into(), - "-y".into(), - "some_clean_package@1.2.3".into(), - ]; - let res = deny_if_malicious_cmd_args("npx", &args).await; - assert!(res.is_ok()); - } - - #[tokio::test] - async fn pagination_works() { - let server = MockServer::start().await; - // 1st page: no vulns, but has next - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [], - "next_page_token": "page-2" - }))) - .up_to_n_times(1) - .mount(&server) - .await; - - // 2nd page: MAL hit - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "vulns": [ { "id": "MAL-4242", "summary": "Second page hit" } ], - "next_page_token": null - }))) - .mount(&server) - .await; - - let c = checker_for(&server); - let res = c.deny_if_malicious("pkg", "npm", None).await; - assert!(res.is_err()); - let msg = format!("{:?}", res.unwrap_err()); - assert!(msg.contains("MAL-4242")); - } - - #[tokio::test] - async fn fail_open_on_http_error() { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/v1/query")) - .respond_with(ResponseTemplate::new(500)) - .mount(&server) - .await; - - let c = checker_for(&server); - let res = c.deny_if_malicious("pkg", "npm", None).await; - assert!(res.is_ok(), "should fail-open on HTTP errors"); - } - - #[tokio::test] - async fn unknown_command_is_skipped() { - let args = vec!["whatever@1.0.0".into()]; - // no mock server: we shouldn't call OSV at all - let res = deny_if_malicious_cmd_args("some-other-bin", &args).await; - assert!(res.is_ok()); - } - - #[test] - fn parse_npm_scoped_with_version() { - assert_eq!( - super::parse_npm_token("@scope/pkg@1.2.3"), - Some(("@scope/pkg".into(), Some("1.2.3".into()))) - ); - } - - #[test] - fn parse_npm_unscoped_latest_is_none() { - assert_eq!( - super::parse_npm_token("react@latest"), - Some(("react".into(), None)) - ); - } - - #[test] - fn parse_pypi_exact_pin_and_latest() { - assert_eq!( - super::parse_pypi_token("requests==2.32.3"), - Some(("requests".into(), Some("2.32.3".into()))) - ); - assert_eq!( - super::parse_pypi_token("requests==latest"), - Some(("requests".into(), None)) - ); - } -} diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index b6576d8eb7f..657c9f62b53 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -26,7 +26,6 @@ use tracing::{error, warn}; use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo}; use super::tool_execution::ToolCallResult; use crate::agents::extension::{Envs, ProcessExit}; -use crate::agents::extension_malware_check; use crate::config::{Config, ExtensionConfigManager}; use crate::oauth::oauth_flow; use crate::prompt_template; @@ -364,10 +363,6 @@ impl ExtensionManager { let command = Command::new(cmd).configure(|command| { command.args(args).envs(all_envs); }); - - // Check for malicious packages before launching the process - extension_malware_check::deny_if_malicious_cmd_args(cmd, args).await?; - let client = child_process_client(command, timeout).await?; Box::new(client) } diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index ae668b714ea..c18cfd91607 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -1,7 +1,6 @@ mod agent; mod context; pub mod extension; -pub mod extension_malware_check; pub mod extension_manager; pub mod final_output_tool; mod large_response_handler; diff --git a/documentation/README.md b/documentation/README.md index 9edbce8a676..0c6c2c27be9 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -39,4 +39,3 @@ $ GIT_USER= yarn deploy ``` If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. - diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/autovis-banner.png b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/autovis-banner.png new file mode 100644 index 00000000000..2fbe4093ca6 Binary files /dev/null and b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/autovis-banner.png differ diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md new file mode 100644 index 00000000000..a1e9b0110eb --- /dev/null +++ b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/index.md @@ -0,0 +1,66 @@ +--- +title: "Auto Visualiser with MCP-UI" +description: "Automatically show renders visual representations of data as you interact with it, powered by MCP-UI" +authors: + - mic +--- + +![blog banner](autovis-banner.png) + +Data visualization just got a major upgrade in Goose. With the new MCP-UI auto visualiser feature, you no longer need to manually request charts, graphs, or visual representations of your data. Goose now automatically detects when data would benefit from visualization and renders interactive visual components directly in your conversation. + + + + +## What is Auto Visualiser? + +[Auto Visualiser](/docs/mcp/autovisualiser-mcp) is a built-in extension that integrates with [Goose's MCP-UI system](/docs/guides/interactive-chat/) to automatically generate visual representations of data as you work. + +![switch](switch.png) + +Once turned on, a smorgasboard of visualisation tools are made available as tools which will be automatically invoked if and when it makes sense to, for example, show things as a radar diagram, or a "sankey" diagram: + +![sankey](sankey.png) + +You can also explicitly ask for a visualisation, even specifying what style you want, and Goose will try to shape your data and then render it inline. This is powered by the [MCP-UI](https://mcpui.dev/) emerging standard, which lets the MCP server formulate a visualisation (in this case using libraries like d3.js) and render it inline. + +The autovisualiser analyzes data patterns and automatically suggests the most appropriate visualization type. My favourite is the treemap which is a great way to see releative sizes of things in a way that pie charts can mislead. It is also interactive in that you can drill down. + +![tree map](treemap.png) + + +Of course, if you want you can resort to the "last refuge of the unimaginative" and graph the weather: + +![weather](weather.png) + +Note this is an early feature, and functionality may change and vary a lot (as with MCP-UI). This is an early example of what is emerging as generative UI, although in this case the templates are pre-generated and the data is dynamically matched to the session and then displayed (from local resources). + + +## Types of Visualizations + +There are currently several families of things it can show: + +* sankey diagrams +* radar charts +* chord diagrams +* donut/pie charts +* bar graphs and general charts +* treemap visualisations (tiles) +* maps (place things on geographic maps) + +--- + +*Ready to see your data? [Enable the Auto Visualiser extension](/docs/mcp/autovisualiser-mcp#configuration) in Goose.* + + + + + + + + + + + + + diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/sankey.png b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/sankey.png new file mode 100644 index 00000000000..88f20687077 Binary files /dev/null and b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/sankey.png differ diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/switch.png b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/switch.png new file mode 100644 index 00000000000..c42426645fe Binary files /dev/null and b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/switch.png differ diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/treemap.png b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/treemap.png new file mode 100644 index 00000000000..4f6ea776c60 Binary files /dev/null and b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/treemap.png differ diff --git a/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/weather.png b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/weather.png new file mode 100644 index 00000000000..fe0bf816bb9 Binary files /dev/null and b/documentation/blog/2025-08-27-autovisualiser-with-mcp-ui/weather.png differ diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/gpt5.png b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/gpt5.png deleted file mode 100644 index fe3cc2e4fcc..00000000000 Binary files a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/gpt5.png and /dev/null differ diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md deleted file mode 100644 index 3b069dfb8da..00000000000 --- a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/index.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: "Your First Goose Experience Is On Us" -description: New Goose users receive $10 in Tetrate Agent Router credits for instant access to multiple models including GPT-5 and Sonnet-4. -authors: - - mic - - rizel ---- - -![](tetrate-header.png) - - You shouldn’t need a credit card to vibe code with Goose. While Goose is completely free to use, the reality is that most performant LLMs aren't. You want to experience Goose in action without breaking the bank or jumping through hoops. We've been thinking about how to make that first step easier for newcomers to Goose. - -That's why we're thrilled about our newest provider integration: [Tetrate's Agent Router Service](https://router.tetrate.ai). From August 27th through October 2nd, new Goose users can get $10 in credits to use Goose with any model on the Tetrate platform. - - - -We've upgraded the onboarding flow. Tetrate Agent Router now appears as a [recommended setup option](/docs/getting-started/installation#set-llm-provider) for new users. Selecting Tetrate takes you through OAuth account creation, then drops you back into Goose with your $10 credits ready to go. - -![fresh install](welcome.png) - - -This integration gives Goose users: -* **Instant access** to models without manual setup -* **$10 in credits** to start building without a paywall -* **A unified model layer** powered by Tetrate -* **Stable routing** built on [Envoy](https://www.envoyproxy.io/), an open source proxy for high-scale systems - - -## Tetrate's Agent Router Service - -Tetrate's Agent Router Service provides unified access to a comprehensive collection of AI models including open source options to cutting-edge frontier models like GPT-5, Sonnet-4, and Grok-4. - -### From Cloud Infrastructure to AI Model Routing - -Tetrate brings years of expertise in routing and infrastructure to the AI space. As major contributors to open source projects like Istio and Envoy, they understand how to build reliable, scalable routing systems. Now they're applying this same expertise to LLM traffic management. - -LLM requests are inherently stateless, making them ideal for intelligent routing across multiple providers and models. This allows you to optimize for cost, speed, availability, or quality, or even use multiple models to cross-check results. Terminology in this space is still settling. Goose refers to Tetrate as a “provider” for consistency, though under the hood it is a router service that connects to other providers. That layer abstracts away model selection, auth, and host config, keeping your setup clean. - -## Why This Collaboration Matters - -Our goal is simple: make Goose accessible to everyone, immediately. That means removing barriers to getting started. Tetrate's generous credit offering and seamless integration help us achieve exactly that. - -It also reflects Tetrate's ongoing commitment to open source and making AI development more accessible to developers worldwide. - -## Explore the Full Model Catalog - -While Goose auto-configures with Sonnet-4 by default, you have access to Tetrate's entire model catalog through the interface: - -![providers](providers.png) -![gpt5](gpt5.png) - -Browse and select from a wide range of options, including: -- **Open-weight models** (like Kimi/K2) hosted and ready to use -- **Frontier models** from various providers -- **Specialized models** optimized for different use cases - -:::tip Protip - Want the best of both worlds? Use Goose’s [Lead/Worker configuration](/docs/tutorials/lead-worker) to combine a powerful frontier model with a faster open-weight model. Let your Lead handle the high-level thinking while Workers take care of the repetitive tasks—saving you both time and credits. -::: - ---- - -Thank you to Tetrate for supporting open source and making AI development more accessible! - -**What are you waiting for?** [Get started with Goose](/) - -*Got questions?* Explore our [docs](/docs/category/guides), browse the [blog](/blog), or join the conversation in our [Discord](https://discord.gg/block-opensource) and [GitHub Discussions](https://github.com/block/goose/discussions). We’d love to have you. - - - - - - - - - - - - - \ No newline at end of file diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/providers.png b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/providers.png deleted file mode 100644 index 9d6be1264bd..00000000000 Binary files a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/providers.png and /dev/null differ diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/startup.png b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/startup.png deleted file mode 100644 index a368a06751d..00000000000 Binary files a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/startup.png and /dev/null differ diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/tetrate-header.png b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/tetrate-header.png deleted file mode 100644 index 6751daf6269..00000000000 Binary files a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/tetrate-header.png and /dev/null differ diff --git a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/welcome.png b/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/welcome.png deleted file mode 100644 index 978699bf332..00000000000 Binary files a/documentation/blog/2025-08-27-get-started-for-free-with-tetrate/welcome.png and /dev/null differ diff --git a/documentation/blog/2025-08-28-ai-teammate/goose-janitor.png b/documentation/blog/2025-08-28-ai-teammate/goose-janitor.png deleted file mode 100644 index 78145a81953..00000000000 Binary files a/documentation/blog/2025-08-28-ai-teammate/goose-janitor.png and /dev/null differ diff --git a/documentation/blog/2025-08-28-ai-teammate/index.md b/documentation/blog/2025-08-28-ai-teammate/index.md deleted file mode 100644 index ba75780f41d..00000000000 --- a/documentation/blog/2025-08-28-ai-teammate/index.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -title: "When AI Becomes Your New Team Member: The Story of Goose Janitor" -description: "How an engineering team is trialling autonomous AI-powered technical debt cleanup" -authors: - - angie ---- - -![blog banner](goose-janitor.png) - -Over the years, Block's iOS engineers have felt the pain of technical debt accumulating. Feature flags are a specific example of this. Even when successfully rolled out, they tend to linger in the codebase, each one a small weight slowing down development. - -In the early stages of 2025, with a renewed focus on developer acceleration, the Foundation iOS team decided to organize 'Feature Flag Removal Month' - an opportunity for teams in the large iOS monorepo to come together and delete _possibly hundreds of thousands of lines of dead code_. - -With the serendipitous launch of Goose recipes around the same time, the team wondered could a dedicated recipe aid this effort? [Gemma Barlow](https://www.linkedin.com/in/gemmakbarlow/), a newer iOS engineer on the team, wanted to find out. - - - - - -## Phase 1: Making Knowledge AI-Accessible - -Gemma's first move was to utilize an existing system of `.mdc`, `.goosehints` and other symlinked context documents to capture how to safely remove feature flags from the repository. - -She added documentation that allows AI agents to glean enough context to perform their removal work accurately. - -## Phase 2: Validation Across Generations - -Given AI documentation is designed to work with a variety of tools in use at Block, further iteration and validation was needed. Several weeks of tests and trials later, it was confirmed the approach was accurate enough to be useful *and* it could handle the three different generations of feature flag implementations that had accumulated: - -- super legacy flags the team used way back yonder -- legacy flags that were newer but are now old -- current implementation of feature flags - -This documentation alone would help teams clean up faster. But now, AI could also understand and safely navigate the complexity of real world legacy systems in most scenarios, a win for developer velocity! 🎉 - -## Phase 3: Building an AI Team Member - -This was great progress. Gemma could have stopped here. - -But instead, she utilized Goose recipes to create **Goose Janitor**. - - -Goose Janitor acts as new AI team member whose responsibility is to tidy up the code after we're done experimenting. It was heavily inspired by existing [Goose recipes](/recipes/detail/?id=clean-up-feature-flag) and internal discussions and experimentation occurring elsewhere in Block. Here's how it is run: - -```bash -goose run \ ---recipe .goose/recipes/goose-janitor-flag-removal.yaml \ ---params feature_flag_key=log-observer-is-enabled \ ---params variant_to_remain=true \ ---params create_pr=false -``` - -The recipe: -- Runs completely autonomously (no human intervention needed) -- Handles different flag implementations, ranging in complexity -- Attempts refactoring for outdated code paths -- Can automatically create draft pull requests, via the Github CLI -- Integrates with [Xcode Index MCP](https://github.com/block/xcode-index-mcp) for deep iOS project understanding -- Plans, implements, builds and tests locally to improve the accuracy of the flag removal result - - -## The Bigger Picture: AI-First Development - -Recipes like Goose Janitor represent a fundamental shift in how we think about AI in software development. They can be deployed to: - -- Understand complex legacy codebases -- Make safe refactoring decisions -- Integrate seamlessly with existing development workflows -- Provide developer velocity improvements -- Scale across large codebases - -Teams at Block are confident that Goose Janitor will be of assistance in production scale cleanup work. - -This is exactly the kind of work AI should handle: tedious, repetitive, but requiring deep codebase knowledge. By automating portions of their work, developers can focus on what they do best, which is building new features and solving novel problems while AI keeps the codebase clean and maintainable. - - -## The AI-First Mindset - -This story illustrates what an AI-first approach to legacy codebases looks like in practice. - -Start by making tribal knowledge AI-accessible. Test and validate that AI can actually handle the complexity with enough accuracy to prove useful. Even if manual intervention is required for complex cases, a first pass performed by AI can be a useful boost to productivity. Build automation that scales across teams and focus human energy on high value creative work. - - -## What's Next? - -The success of Goose Janitor opens up fascinating possibilities. What other forms of technical debt could benefit from this approach? What other "AI team members" could we build to handle routine but knowledge intensive work? - -As we move toward an AI-first future, stories like Gemma's show us the path. Not just using AI tools, but thinking systematically about how to make our codebases and processes AI-ready. - -The future of software development is mixed teams where AI agents are autonomous contributors, handling the maintenance work that keeps our systems healthy while humans focus on building the future. - ---- - -Want the tweak the base recipe for your own needs? Check out [Clean up feature flag](/recipes/detail/?id=clean-up-feature-flag) in our Recipe Cookbook! - - - - - - - - - - - - - \ No newline at end of file diff --git a/documentation/docs/getting-started/using-extensions.md b/documentation/docs/getting-started/using-extensions.md index 01720ae877b..068efbf4234 100644 --- a/documentation/docs/getting-started/using-extensions.md +++ b/documentation/docs/getting-started/using-extensions.md @@ -105,10 +105,6 @@ You can also add any other [MCP Server](#mcp-servers) as a Goose extension, even Extensions can be installed directly via the [extensions directory][extensions-directory], CLI, or UI. -:::warning Airgapped Environments -If you're in a corporate or airgapped environment and extensions fail to activate, see [Airgapped/Offline Environments](/docs/troubleshooting#airgappedoffline-environment-issues) for workarounds. -::: - ### MCP Servers You can install any MCP server as a Goose extension. diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 0155f1dbec8..8af534cab52 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -256,6 +256,7 @@ When the keyring is disabled, secrets are stored here: * Windows: `%APPDATA%\Block\goose\config\secrets.yaml` ::: + ## Langfuse Integration These variables configure the [Langfuse integration for observability](/docs/tutorials/langfuse). diff --git a/documentation/docs/guides/file-management.md b/documentation/docs/guides/file-management.md index d210782ac5c..c660c84e10f 100644 --- a/documentation/docs/guides/file-management.md +++ b/documentation/docs/guides/file-management.md @@ -48,7 +48,7 @@ Implement validation and testing steps before and after Goose modifies any files ### Change Review -Manually review or use automated code reviews to ensure the quality of generated code or changes. Integrate tools such as diff tools to visualize changes made by Goose. Implement a review process with team members or CI/CD pipelines. +Manually review or use automated code reviews to ensure the quality of generated code or changes. Integrate tools such as diff tools to visualise changes made by Goose. Implement a review process with team members or CI/CD pipelines. ### Codebase Organization diff --git a/documentation/docs/mcp/alby-mcp.md b/documentation/docs/mcp/alby-mcp.md index 42cbfc5219c..6e8cea79916 100644 --- a/documentation/docs/mcp/alby-mcp.md +++ b/documentation/docs/mcp/alby-mcp.md @@ -1,5 +1,5 @@ --- -title: Alby Bitcoin Payments MCP +title: Alby Bitcoin Payments Extension description: Connect Goose to your Bitcoin Lightning Wallet --- diff --git a/documentation/docs/mcp/autovisualiser-mcp.md b/documentation/docs/mcp/autovisualiser-mcp.md new file mode 100644 index 00000000000..ba7efc1486d --- /dev/null +++ b/documentation/docs/mcp/autovisualiser-mcp.md @@ -0,0 +1,159 @@ +--- +title: Auto Visualiser Extension +description: Add automatic data visualization to Goose +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import GooseBuiltinInstaller from '@site/src/components/GooseBuiltinInstaller'; +import ImageCarousel from '@site/src/components/ImageCarousel'; + +The Auto Visualiser extension enables Goose to automatically generate interactive data visualizations directly in your conversation. This built-in extension leverages MCP-UI to create charts, graphs, maps, and other visual representations of your data without requiring manual requests. + +This guide will cover enabling and using the Auto Visualiser MCP Server, which is a built-in Goose extension. + +## Configuration + + + + + + + + + 1. Run the `configure` command: + ```sh + goose configure + ``` + + 2. Choose to `Toggle Extensions` + ```sh + ┌ goose-configure + │ + ◇ What would you like you configure? + │ Toggle Extensions + │ + ◆ Enable extensions: (use "space" to toggle and "enter" to submit) + // highlight-start + │ ● autovisualiser + // highlight-end + └ Extension settings updated successfully + ``` + + + + +## Visualization Types + +The Auto Visualiser is a powerful extension that integrates with Goose's MCP-UI system to automatically detect when data would benefit from visualization and render interactive visual components. It analyzes data patterns in your conversations and suggests the most appropriate visualization type. + +| Chart Type | Description | Detected When Prompt Implies | +|------------|-------------|------------------------------| +| **Sankey Diagrams** | Flow data visualization showing relationships and quantities | Flow or process data
(workflows, funnels, process datasets) | +| **Radar Charts** | Multi-dimensional data comparison (spider charts) | Multi-dimensional comparisons
(performance metrics, feature comparisons) | +| **Donut/Pie Charts** | Categorical data visualization with support for multiple charts | Categorical proportions
(percentage breakdowns, category distributions) | +| **Treemap Visualizations** | Hierarchical data with proportional area representation | Hierarchical data
(nested categories, organizational structures) | +| **Chord Diagrams** | Relationship and flow visualization between entities | Relationship matrices
(network connections, cross-references) | +| **Interactive Maps** | Geographic data visualization with location markers using Leaflet | Geographic information
(location data, coordinates, addresses) | +| **Line/Bar/Scatter Charts** | Traditional chart types for data analysis | Time series data
(historical data, trends over time) | + +### Example Visualizations + + + +### Features + +- **Automatic Detection**: Goose intelligently chooses the best visualization type for your data +- **Interactive Components**: All visualizations are interactive with hover effects, zooming, and drilling capabilities +- **MCP-UI Integration**: Leverages the emerging MCP-UI standard for seamless rendering +- **Multiple Chart Support**: Can render multiple visualizations in a single response +- **Customizable Styling**: Supports custom colors, labels, and formatting options +- **Export Capability**: Visualizations can be saved as HTML files for sharing + + +## Example Usage + +In this example, I'll have Goose analyze some sales data and automatically visualize it using the Auto Visualiser extension. + + +### Goose Prompt +``` +I have quarterly sales data for different product categories. Can you help me understand: +1. The hierarchical breakdown of revenue across our nested product categories +2. How our performance metrics compare across all four quarters +3. The customer flow through our sales funnel process + +Here's the data: +- Electronics: Q1: $150k, Q2: $180k, Q3: $220k, Q4: $195k +- Clothing: Q1: $120k, Q2: $140k, Q3: $160k, Q4: $175k +- Home & Garden: Q1: $80k, Q2: $95k, Q3: $110k, Q4: $125k +``` + +### Goose Output + +:::note Desktop + +I'll help you analyze your quarterly sales data with three different visualizations to address each of your questions. Let me create these visualizations for you. + +First, let's visualize the hierarchical breakdown of your revenue across product categories: + +![](/img/autovisualiser-treemap.png) + +![](/img/autovisualiser-radar.png) + +![](/img/autovisualiser-sankey.png) + + +I've created three comprehensive visualizations to help you understand your quarterly sales data from different perspectives: + +## 1. Hierarchical Revenue Breakdown (Treemap) +The **treemap visualization** shows your revenue hierarchy with proportional areas representing the relative size of each category and quarter. Key insights: +- **Electronics** is your largest revenue generator ($745k total) +- **Clothing** follows as second largest ($595k total) +- **Home & Garden** is smallest but growing ($410k total) +- You can see how each quarter contributes within each category + +## 2. Performance Metrics Comparison (Radar Chart) +The **radar chart** displays how all three product categories performed across the four quarters, making it easy to compare: +- **Electronics** shows strong Q3 performance ($220k) with slight Q4 decline +- **Clothing** demonstrates consistent growth throughout the year +- **Home & Garden** shows steady upward trajectory across all quarters +- All categories show their strongest performance in Q3-Q4 + +## 3. Sales Funnel Flow (Sankey Diagram) +The **Sankey diagram** represents customer flow through your sales process, with flow thickness proportional to revenue: +- Shows how market opportunities flow into prospects for each category +- Demonstrates the conversion from prospects to actual sales +- **Electronics** has the thickest flow (highest revenue conversion) +- **Home & Garden** has the thinnest but represents growth opportunity + +## Key Business Insights: +- **Total Revenue**: $1.75M across all categories +- **Growth Pattern**: All categories show positive growth trends +- **Category Performance**: Electronics (42.6%), Clothing (34%), Home & Garden (23.4%) +- **Seasonal Trends**: Q3 appears to be your strongest quarter overall + +Would you like me to create additional visualizations or dive deeper into any specific aspect of your sales data? +::: + + + +## Troubleshooting + +- **Visualization not appearing**: Ensure the Auto Visualiser extension is enabled and your data follows the expected format. + +- **Data format errors**: Check that your data structure matches the required schema for the visualization type. The extension provides detailed error messages to guide you. + +- **Performance with large datasets**: For very large datasets, consider aggregating or sampling your data before visualization. + diff --git a/documentation/docs/mcp/cloudflare-mcp.md b/documentation/docs/mcp/cloudflare-mcp.md index 5e05d9482d0..091b3273540 100644 --- a/documentation/docs/mcp/cloudflare-mcp.md +++ b/documentation/docs/mcp/cloudflare-mcp.md @@ -1,5 +1,5 @@ --- -title: Cloudflare MCP Server +title: Cloudflare Extension description: Add Cloudflare MCP Servers as Goose Extensions unlisted: true --- diff --git a/documentation/docs/mcp/container-use-mcp.md b/documentation/docs/mcp/container-use-mcp.md index f096a584c35..1b454d5ac3d 100644 --- a/documentation/docs/mcp/container-use-mcp.md +++ b/documentation/docs/mcp/container-use-mcp.md @@ -1,5 +1,5 @@ --- -title: Container Use +title: Container Use Extension description: Use Container-Use MCP as a Goose Extension --- diff --git a/documentation/docs/troubleshooting.md b/documentation/docs/troubleshooting.md index a18e98bc154..5df20974b3f 100644 --- a/documentation/docs/troubleshooting.md +++ b/documentation/docs/troubleshooting.md @@ -292,54 +292,6 @@ This likely means that the local host address is not accessible from WSL. If you still encounter a `failed to connect` error, you can try using WSL's [Mirrored Networking](https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking) setting if you using Windows 11 22H2 or higher ---- - -### Airgapped/Offline Environment Issues - -If you're working in an airgapped, offline, or corporate-restricted environment, you may encounter issues where MCP server extensions fail to activate or download their runtime dependencies. - -#### Symptoms: -- Extensions fail to activate with error messages about missing runtime environments -- Errors containing "hermit:fatal" or failed internet downloads -- Extensions work on personal machines but fail in corporate/restricted networks -- Error messages like: `Failed to start extension: Could not run extension command` - -#### Solution: -Goose Desktop uses **"shims"** (packaged versions of `npx` and `uvx`) that automatically download runtime environments via Hermit. In restricted networks, these downloads fail. - -**Workaround - Use Custom Command Names:** - -1. **Create alternatively named versions of package runners on your system:** - ```bash - # For uvx (Python packages) - ln -s /usr/local/bin/uvx /usr/local/bin/runuv - - # For npx (Node.js packages) - ln -s /usr/local/bin/npx /usr/local/bin/runnpx - ``` - -2. **Update your MCP server configurations to use the custom names:** - - Instead of: - ```yaml - extensions: - example: - cmd: uvx - args: [mcp-server-example] - ``` - - Use: - ```yaml - extensions: - example: - cmd: runuv # This bypasses Goose's shims - args: [mcp-server-example] - ``` - -3. **Why this works:** Goose only replaces known command names (`npx`, `uvx`, `jbang`, etc.) with its packaged shims. Custom names are passed through unchanged to your system's actual executables. - -4. **Require more changes**: In a corporate proxy environment or airgapped environment where the above doesn't work, it is recommended that you customize and package up Goose desktop with shims/config that will work given the network constraints you have (for example, TLS certificate limitations, proxies, inability to download required content etc). - --- ### Need Further Help? If you have questions, run into issues, or just need to brainstorm ideas join the [Discord Community][discord]! diff --git a/documentation/static/img/autovisualiser-radar.png b/documentation/static/img/autovisualiser-radar.png new file mode 100644 index 00000000000..0669e55c0df Binary files /dev/null and b/documentation/static/img/autovisualiser-radar.png differ diff --git a/documentation/static/img/autovisualiser-sankey.png b/documentation/static/img/autovisualiser-sankey.png new file mode 100644 index 00000000000..3eb72ad68b2 Binary files /dev/null and b/documentation/static/img/autovisualiser-sankey.png differ diff --git a/documentation/static/img/autovisualiser-treemap.png b/documentation/static/img/autovisualiser-treemap.png new file mode 100644 index 00000000000..dde097dd179 Binary files /dev/null and b/documentation/static/img/autovisualiser-treemap.png differ diff --git a/documentation/static/servers.json b/documentation/static/servers.json index 8e62f830236..e228493fc54 100644 --- a/documentation/static/servers.json +++ b/documentation/static/servers.json @@ -50,6 +50,17 @@ } ] }, + { + "id": "autovisualiser", + "name": "Auto Visualiser", + "description": "Automatic data visualisation via MCP-UI", + "command": "", + "link": "https://github.com/block/goose/tree/main/crates/goose-mcp/src/autovisualiser", + "installation_notes": "This is a built-in extension that comes with Goose and can be enabled on the Extensions page.", + "is_builtin": true, + "endorsed": true, + "environmentVariables": [] + }, { "id": "blender-mcp", "name": "Blender", diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 2b1e7381eb1..5b1c2f855ed 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,8 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { IpcRendererEvent } from 'electron'; import { HashRouter, Routes, Route, useNavigate, useLocation } from 'react-router-dom'; -import { openSharedSessionFromDeepLink, type SessionLinksViewOptions } from './sessionLinks'; -import { type SharedSessionDetails } from './sharedSessions'; import { ErrorUI } from './components/ErrorBoundary'; import { ExtensionInstallModal } from './components/ExtensionInstallModal'; import { ToastContainer } from 'react-toastify'; @@ -16,7 +14,6 @@ import Hub from './components/hub'; import Pair from './components/pair'; import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView'; import SessionsView from './components/sessions/SessionsView'; -import SharedSessionView from './components/sessions/SharedSessionView'; import SchedulesView from './components/schedule/SchedulesView'; import ProviderSettings from './components/settings/providers/ProviderSettingsPage'; import { useChat } from './hooks/useChat'; @@ -318,48 +315,6 @@ const WelcomeRoute = () => { ); }; -// Wrapper component for SharedSessionRoute to access parent state -const SharedSessionRouteWrapper = ({ - isLoadingSharedSession, - setIsLoadingSharedSession, - sharedSessionError, -}: { - isLoadingSharedSession: boolean; - setIsLoadingSharedSession: (loading: boolean) => void; - sharedSessionError: string | null; -}) => { - const location = useLocation(); - const navigate = useNavigate(); - const setView = createNavigationHandler(navigate); - - const historyState = window.history.state; - const sessionDetails = (location.state?.sessionDetails || - historyState?.sessionDetails) as SharedSessionDetails | null; - const error = location.state?.error || historyState?.error || sharedSessionError; - const shareToken = location.state?.shareToken || historyState?.shareToken; - const baseUrl = location.state?.baseUrl || historyState?.baseUrl; - - return ( - { - if (shareToken && baseUrl) { - setIsLoadingSharedSession(true); - try { - await openSharedSessionFromDeepLink(`goose://sessions/${shareToken}`, setView, baseUrl); - } catch (error) { - console.error('Failed to retry loading shared session:', error); - } finally { - setIsLoadingSharedSession(false); - } - } - }} - /> - ); -}; - const ExtensionsRoute = () => { const navigate = useNavigate(); const location = useLocation(); @@ -398,8 +353,6 @@ export default function App() { const [isLoadingSession, setIsLoadingSession] = useState(false); const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false); const [agentWaitingMessage, setAgentWaitingMessage] = useState(null); - const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false); - const [sharedSessionError, setSharedSessionError] = useState(null); // Add separate state for pair chat to maintain its own conversation const [pairChat, setPairChat] = useState({ @@ -446,9 +399,6 @@ export default function App() { case 'ConfigureProviders': window.location.hash = '#/configure-providers'; break; - case 'sharedSession': - window.location.hash = '#/shared-session'; - break; case 'recipeEditor': window.location.hash = '#/recipe-editor'; break; @@ -526,44 +476,6 @@ export default function App() { } }, []); - useEffect(() => { - const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => { - const link = args[0] as string; - window.electron.logInfo(`Opening shared session from deep link ${link}`); - setIsLoadingSharedSession(true); - setSharedSessionError(null); - try { - await openSharedSessionFromDeepLink( - link, - (_view: View, _options?: SessionLinksViewOptions) => { - // Navigate to shared session view with the session data - window.location.hash = '#/shared-session'; - if (_options) { - window.history.replaceState(_options, '', '#/shared-session'); - } - } - ); - } catch (error) { - console.error('Unexpected error opening shared session:', error); - // Navigate to shared session view with error - window.location.hash = '#/shared-session'; - const shareToken = link.replace('goose://sessions/', ''); - const options = { - sessionDetails: null, - error: error instanceof Error ? error.message : 'Unknown error', - shareToken, - }; - window.history.replaceState(options, '', '#/shared-session'); - } finally { - setIsLoadingSharedSession(false); - } - }; - window.electron.on('open-shared-session', handleOpenSharedSession); - return () => { - window.electron.off('open-shared-session', handleOpenSharedSession); - }; - }, []); - // Handle recipe decode events from main process useEffect(() => { const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => { @@ -881,18 +793,6 @@ export default function App() { } /> - - - - } - /> - {/* Microphone button - show only if dictation is enabled */} - {dictationSettings?.enabled && ( + {/* Microphone button - show if dictation is enabled, disable if not configured */} + {(dictationSettings?.enabled || dictationSettings?.provider === null) && ( <> {!canUseDictation ? ( diff --git a/ui/desktop/src/components/Layout/AppLayout.tsx b/ui/desktop/src/components/Layout/AppLayout.tsx index 47c23c5178f..e340037c623 100644 --- a/ui/desktop/src/components/Layout/AppLayout.tsx +++ b/ui/desktop/src/components/Layout/AppLayout.tsx @@ -54,9 +54,7 @@ const AppLayoutContent: React.FC = ({ setIsGoosehintsModalOpen } case 'ConfigureProviders': navigate('/configure-providers'); break; - case 'sharedSession': - navigate('/shared-session', { state: viewOptions }); - break; + case 'recipeEditor': navigate('/recipe-editor', { state: viewOptions }); break; diff --git a/ui/desktop/src/components/ParameterInputModal.tsx b/ui/desktop/src/components/ParameterInputModal.tsx index 023da48be74..c79c7fd5455 100644 --- a/ui/desktop/src/components/ParameterInputModal.tsx +++ b/ui/desktop/src/components/ParameterInputModal.tsx @@ -120,7 +120,7 @@ const ParameterInputModal: React.FC = ({

Recipe Parameters

-
+ {parameters.map((param) => (
+ + {/* Ollama Card - outline style */} +
+ {/* Detected badge - similar to recommended but green */} + {ollamaDetected && ( +
+ + Detected + +
+ )} + +
{ + setShowFirstTimeSetup(false); + setShowOllamaSetup(true); + }} + className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" + > +
+
+ +

+ Ollama +

+
+
+ + + +
+
+

+ Advanced: Run AI models locally on your computer. Completely free and private + with no internet required. (Does require significant hardware) +

+
+
diff --git a/ui/desktop/src/components/RecipesView.tsx b/ui/desktop/src/components/RecipesView.tsx index 674b350cde8..a13c0e00db2 100644 --- a/ui/desktop/src/components/RecipesView.tsx +++ b/ui/desktop/src/components/RecipesView.tsx @@ -372,90 +372,67 @@ Parameters you can use: } }; - // Render a recipe item with error handling - const RecipeItem = ({ savedRecipe }: { savedRecipe: SavedRecipe }) => { - try { - return ( - -
-
-
-

{savedRecipe.recipe.title}

- {savedRecipe.isGlobal ? ( - - ) : ( - - )} -
-

- {savedRecipe.recipe.description} -

-
- - {savedRecipe.lastModified.toLocaleDateString()} -
-
- -
- - - -
+ // Render a recipe item + const RecipeItem = ({ savedRecipe }: { savedRecipe: SavedRecipe }) => ( + +
+
+
+

{savedRecipe.recipe.title}

+ {savedRecipe.isGlobal ? ( + + ) : ( + + )}
- - ); - } catch (error) { - // Error row showing failed to read file with filename and error details - return ( - -
-
-
- -

- Failed to read file: {savedRecipe.filename} -

-
-

- {error instanceof Error ? error.message : 'Unknown error'} -

-
+

+ {savedRecipe.recipe.description} +

+
+ + {savedRecipe.lastModified.toLocaleDateString()}
- - ); - } - }; +
+ +
+ + + +
+
+ + ); // Render skeleton loader for recipe items const RecipeSkeleton = () => ( diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx index 713f97d0d81..2f3832a3283 100644 --- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx +++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx @@ -1,12 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Calendar, MessageSquareText, Folder, - Share2, Sparkles, - Copy, - Check, Target, LoaderCircle, AlertCircle, @@ -17,21 +14,11 @@ import { toast } from 'react-toastify'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { ScrollArea } from '../ui/scroll-area'; import { formatMessageTimestamp } from '../../utils/timeUtils'; -import { createSharedSession } from '../../sharedSessions'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '../ui/dialog'; import ProgressiveMessageList from '../ProgressiveMessageList'; import { SearchView } from '../conversation/SearchView'; import { ContextManagerProvider } from '../context_management/ContextManager'; import { Message } from '../../types/message'; import BackButton from '../ui/BackButton'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'; // Helper function to determine if a message is a user message (same as useChatEngine) const isUserMessage = (message: Message): boolean => { @@ -150,74 +137,6 @@ const SessionHistoryView: React.FC = ({ onRetry, showActionButtons = true, }) => { - const [isShareModalOpen, setIsShareModalOpen] = useState(false); - const [shareLink, setShareLink] = useState(''); - const [isSharing, setIsSharing] = useState(false); - const [isCopied, setIsCopied] = useState(false); - const [canShare, setCanShare] = useState(false); - - useEffect(() => { - const savedSessionConfig = localStorage.getItem('session_sharing_config'); - if (savedSessionConfig) { - try { - const config = JSON.parse(savedSessionConfig); - if (config.enabled && config.baseUrl) { - setCanShare(true); - } - } catch (error) { - console.error('Error parsing session sharing config:', error); - } - } - }, []); - - const handleShare = async () => { - setIsSharing(true); - - try { - const savedSessionConfig = localStorage.getItem('session_sharing_config'); - if (!savedSessionConfig) { - throw new Error('Session sharing is not configured. Please configure it in settings.'); - } - - const config = JSON.parse(savedSessionConfig); - if (!config.enabled || !config.baseUrl) { - throw new Error('Session sharing is not enabled or base URL is not configured.'); - } - - const shareToken = await createSharedSession( - config.baseUrl, - session.metadata.working_dir, - session.messages, - session.metadata.description || 'Shared Session', - session.metadata.total_tokens - ); - - const shareableLink = `goose://sessions/${shareToken}`; - setShareLink(shareableLink); - setIsShareModalOpen(true); - } catch (error) { - console.error('Error sharing session:', error); - toast.error( - `Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}` - ); - } finally { - setIsSharing(false); - } - }; - - const handleCopyLink = () => { - navigator.clipboard - .writeText(shareLink) - .then(() => { - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - }) - .catch((err) => { - console.error('Failed to copy link:', err); - toast.error('Failed to copy link to clipboard'); - }); - }; - const handleLaunchInNewWindow = () => { if (session) { console.log('Launching session in new window:', session.session_id); @@ -249,136 +168,63 @@ const SessionHistoryView: React.FC = ({ // Define action buttons const actionButtons = showActionButtons ? ( - <> - - - - - {!canShare ? ( - -

- To enable session sharing, go to Settings {'>'} Session {'>'}{' '} - Session Sharing. -

-
- ) : null} -
- - + ) : null; return ( - <> - -
- -
- {!isLoading && session.messages.length > 0 ? ( - <> -
- - - {formatMessageTimestamp(session.messages[0]?.created)} - - - - {session.metadata.message_count} - - {session.metadata.total_tokens !== null && ( - - - {session.metadata.total_tokens.toLocaleString()} - - )} -
-
+ +
+ +
+ {!isLoading && session.messages.length > 0 ? ( + <> +
+ + + {formatMessageTimestamp(session.messages[0]?.created)} + + + + {session.metadata.message_count} + + {session.metadata.total_tokens !== null && ( - - {session.metadata.working_dir} + + {session.metadata.total_tokens.toLocaleString()} -
- - ) : ( -
- - Loading session details... + )}
- )} -
-
- - -
-
- - - - - - - Share Session (beta) - - - Share this session link to give others a read only view of your goose chat. - - - -
-
- - {shareLink} - - -
+
+ + + {session.metadata.working_dir} + +
+ + ) : ( +
+ + Loading session details... +
+ )}
- - - - -
-
- + + + +
+ ); }; diff --git a/ui/desktop/src/components/sessions/SharedSessionView.tsx b/ui/desktop/src/components/sessions/SharedSessionView.tsx deleted file mode 100644 index 1f39d5cd16e..00000000000 --- a/ui/desktop/src/components/sessions/SharedSessionView.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { Calendar, MessageSquareText, Folder, Target, LoaderCircle, Share2 } from 'lucide-react'; -import { type SharedSessionDetails } from '../../sharedSessions'; -import { SessionMessages } from './SessionViewComponents'; -import { formatMessageTimestamp } from '../../utils/timeUtils'; -import { MainPanelLayout } from '../Layout/MainPanelLayout'; - -interface SharedSessionViewProps { - session: SharedSessionDetails | null; - isLoading: boolean; - error: string | null; - onRetry: () => void; -} - -// Custom SessionHeader component matching SessionHistoryView style -const SessionHeader: React.FC<{ - children: React.ReactNode; - title: string; -}> = ({ children, title }) => { - return ( -
-

{title}

-
{children}
-
- ); -}; - -const SharedSessionView: React.FC = ({ - session, - isLoading, - error, - onRetry, -}) => { - return ( - -
-
-
- - Shared Session -
-
- - -
- {!isLoading && session && session.messages.length > 0 ? ( - <> -
- - - {formatMessageTimestamp(session.messages[0]?.created)} - - - - {session.message_count} - - {session.total_tokens !== null && ( - - - {session.total_tokens.toLocaleString()} - - )} -
-
- - - {session.working_dir} - -
- - ) : ( -
- - Loading session details... -
- )} -
-
- - -
-
- ); -}; - -export default SharedSessionView; diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index e414134e9c1..dcbed3ab597 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -2,12 +2,11 @@ import { ScrollArea } from '../ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { View, ViewOptions } from '../../utils/navigationUtils'; import ModelsSection from './models/ModelsSection'; -import SessionSharingSection from './sessions/SessionSharingSection'; import AppSettingsSection from './app/AppSettingsSection'; import ConfigSettings from './config/ConfigSettings'; import { ExtensionConfig } from '../../api'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; -import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react'; +import { Bot, Monitor, MessageSquare } from 'lucide-react'; import { useState, useEffect } from 'react'; import ChatSettingsSection from './chat/ChatSettingsSection'; import { CONFIGURATION_ENABLED } from '../../updates'; @@ -37,7 +36,6 @@ export default function SettingsView({ update: 'app', models: 'models', modes: 'chat', - sharing: 'sharing', styles: 'chat', tools: 'chat', app: 'app', @@ -93,14 +91,6 @@ export default function SettingsView({ Chat - - - Session - App @@ -123,13 +113,6 @@ export default function SettingsView({ - - - - ({ @@ -29,19 +27,20 @@ export default function DictationSection() { useEffect(() => { const loadSettings = async () => { const savedSettings = localStorage.getItem(DICTATION_SETTINGS_KEY); - - let loadedSettings: DictationSettings; - if (savedSettings) { const parsed = JSON.parse(savedSettings); - loadedSettings = parsed; + setSettings(parsed); + setShowElevenLabsKey(parsed.provider === 'elevenlabs'); } else { - loadedSettings = await getDefaultDictationSettings(getProviders); + // Default settings + const defaultSettings: DictationSettings = { + enabled: true, + provider: 'openai', + }; + setSettings(defaultSettings); + localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(defaultSettings)); } - setSettings(loadedSettings); - setShowElevenLabsKey(loadedSettings.provider === 'elevenlabs'); - // Load ElevenLabs API key from storage setIsLoadingKey(true); try { @@ -59,7 +58,7 @@ export default function DictationSection() { }; loadSettings(); - }, [read, getProviders]); + }, [read]); // Save ElevenLabs key on unmount if it has changed useEffect(() => { @@ -110,7 +109,6 @@ export default function DictationSection() { }; const saveSettings = (newSettings: DictationSettings) => { - console.log('Saving dictation settings to localStorage:', newSettings); setSettings(newSettings); localStorage.setItem(DICTATION_SETTINGS_KEY, JSON.stringify(newSettings)); }; @@ -132,26 +130,18 @@ export default function DictationSection() { const handleElevenLabsKeyChange = (key: string) => { setElevenLabsApiKey(key); elevenLabsApiKeyRef.current = key; - // If user starts typing, they're updating the key - if (key.length > 0) { - setHasElevenLabsKey(false); // Hide "configured" while typing - } }; const saveElevenLabsKey = async () => { // Save to secure storage try { if (elevenLabsApiKey.trim()) { - console.log('Saving ElevenLabs API key to secure storage...'); await upsert(ELEVENLABS_API_KEY, elevenLabsApiKey, true); setHasElevenLabsKey(true); - console.log('ElevenLabs API key saved successfully'); } else { // If key is empty, remove it from storage - console.log('Removing ElevenLabs API key from secure storage...'); await upsert(ELEVENLABS_API_KEY, null, true); setHasElevenLabsKey(false); - console.log('ElevenLabs API key removed successfully'); } } catch (error) { console.error('Error saving ElevenLabs API key:', error); diff --git a/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx b/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx deleted file mode 100644 index a9df69ccb6f..00000000000 --- a/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Input } from '../../ui/input'; -import { Check, Lock, Loader2, AlertCircle } from 'lucide-react'; -import { Switch } from '../../ui/switch'; -import { Button } from '../../ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; - -export default function SessionSharingSection() { - const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE'); - console.log('envBaseUrlShare', envBaseUrlShare); - - // If env is set, force sharing enabled and set the baseUrl accordingly. - const [sessionSharingConfig, setSessionSharingConfig] = useState({ - enabled: envBaseUrlShare ? true : false, - baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '', - }); - const [urlError, setUrlError] = useState(''); - const [testResult, setTestResult] = useState<{ - status: 'success' | 'error' | 'testing' | null; - message: string; - }>({ status: null, message: '' }); - - // isUrlConfigured is true if the user has configured a baseUrl and it is valid. - const isUrlConfigured = - !envBaseUrlShare && - sessionSharingConfig.enabled && - isValidUrl(String(sessionSharingConfig.baseUrl)); - - // Only load saved config from localStorage if the env variable is not provided. - useEffect(() => { - if (envBaseUrlShare) { - // If env variable is set, save the forced configuration to localStorage - const forcedConfig = { - enabled: true, - baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '', - }; - localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig)); - } else { - const savedSessionConfig = localStorage.getItem('session_sharing_config'); - if (savedSessionConfig) { - try { - const config = JSON.parse(savedSessionConfig); - setSessionSharingConfig(config); - } catch (error) { - console.error('Error parsing session sharing config:', error); - } - } - } - }, [envBaseUrlShare]); - - // Helper to check if the user's input is a valid URL - function isValidUrl(value: string): boolean { - if (!value) return false; - try { - new URL(value); - return true; - } catch { - return false; - } - } - - // Toggle sharing (only allowed when env is not set). - const toggleSharing = () => { - if (envBaseUrlShare) { - return; // Do nothing if the environment variable forces sharing. - } - setSessionSharingConfig((prev) => { - const updated = { ...prev, enabled: !prev.enabled }; - localStorage.setItem('session_sharing_config', JSON.stringify(updated)); - return updated; - }); - }; - - // Handle changes to the base URL field - const handleBaseUrlChange = (e: React.ChangeEvent) => { - const newBaseUrl = e.target.value; - setSessionSharingConfig((prev) => ({ - ...prev, - baseUrl: newBaseUrl, - })); - - // Clear previous test results when URL changes - setTestResult({ status: null, message: '' }); - - if (isValidUrl(newBaseUrl)) { - setUrlError(''); - const updated = { ...sessionSharingConfig, baseUrl: newBaseUrl }; - localStorage.setItem('session_sharing_config', JSON.stringify(updated)); - } else { - setUrlError('Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).'); - } - }; - - // Test connection to the configured URL - const testConnection = async () => { - const baseUrl = sessionSharingConfig.baseUrl; - if (!baseUrl) return; - - setTestResult({ status: 'testing', message: 'Testing connection...' }); - - try { - // Create an AbortController for timeout - const controller = new AbortController(); - const timeoutId = window.setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(baseUrl, { - method: 'GET', - headers: { - Accept: 'application/json, text/plain, */*', - }, - signal: controller.signal, - }); - - window.clearTimeout(timeoutId); - - // Consider any response (even 404) as a successful connection - // since it means we can reach the server - if (response.status < 500) { - setTestResult({ - status: 'success', - message: 'Connection successful!', - }); - } else { - setTestResult({ - status: 'error', - message: `Server error: HTTP ${response.status}. The server may not be configured correctly.`, - }); - } - } catch (error) { - console.error('Connection test failed:', error); - let errorMessage = 'Connection failed. '; - - if (error instanceof TypeError && error.message.includes('fetch')) { - errorMessage += - 'Unable to reach the server. Please check the URL and your network connection.'; - } else if (error instanceof Error) { - if (error.name === 'AbortError') { - errorMessage += 'Connection timed out. The server may be slow or unreachable.'; - } else { - errorMessage += error.message; - } - } else { - errorMessage += 'Unknown error occurred.'; - } - - setTestResult({ - status: 'error', - message: errorMessage, - }); - } - }; - - return ( -
- - - Session Sharing - - {(envBaseUrlShare as string) - ? 'Session sharing is configured but fully opt-in — your sessions are only shared when you explicitly click the share button.' - : 'You can enable session sharing to share your sessions with others.'} - - - -
- {/* Toggle for enabling session sharing */} -
- - - {envBaseUrlShare ? ( - - ) : ( - - )} -
- - {/* Base URL field (only visible if enabled) */} - {sessionSharingConfig.enabled && ( -
-
- - {isUrlConfigured && } -
-
- -
- {urlError &&

{urlError}

} - - {(isUrlConfigured || (envBaseUrlShare as string)) && ( -
- - - {/* Test Results */} - {testResult.status && testResult.status !== 'testing' && ( -
- {testResult.status === 'success' ? ( - - ) : ( - - )} - {testResult.message} -
- )} -
- )} -
- )} -
-
-
-
- ); -} diff --git a/ui/desktop/src/hooks/dictationConstants.ts b/ui/desktop/src/hooks/dictationConstants.ts deleted file mode 100644 index 972ba08b6cf..00000000000 --- a/ui/desktop/src/hooks/dictationConstants.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DictationSettings, DictationProvider } from './useDictationSettings'; - -export const DICTATION_SETTINGS_KEY = 'dictation_settings'; -export const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; - -export const getDefaultDictationSettings = async ( - getProviders: (refresh: boolean) => Promise> -): Promise => { - const providers = await getProviders(false); - - // Check if we have an OpenAI API key as primary default - const openAIProvider = providers.find((p) => p.name === 'openai'); - - if (openAIProvider && openAIProvider.is_configured) { - return { - enabled: true, - provider: 'openai' as DictationProvider, - }; - } else { - return { - enabled: false, - provider: null as DictationProvider, - }; - } -}; diff --git a/ui/desktop/src/hooks/useDictationSettings.ts b/ui/desktop/src/hooks/useDictationSettings.ts index d6fe046b5a4..57ab0abb890 100644 --- a/ui/desktop/src/hooks/useDictationSettings.ts +++ b/ui/desktop/src/hooks/useDictationSettings.ts @@ -1,10 +1,5 @@ import { useState, useEffect } from 'react'; import { useConfig } from '../components/ConfigContext'; -import { - DICTATION_SETTINGS_KEY, - ELEVENLABS_API_KEY, - getDefaultDictationSettings, -} from './dictationConstants'; export type DictationProvider = 'openai' | 'elevenlabs' | null; @@ -13,6 +8,9 @@ export interface DictationSettings { provider: DictationProvider; } +const DICTATION_SETTINGS_KEY = 'dictation_settings'; +const ELEVENLABS_API_KEY = 'ELEVENLABS_API_KEY'; + export const useDictationSettings = () => { const [settings, setSettings] = useState(null); const [hasElevenLabsKey, setHasElevenLabsKey] = useState(false); @@ -22,13 +20,23 @@ export const useDictationSettings = () => { const loadSettings = async () => { // Load settings from localStorage const saved = localStorage.getItem(DICTATION_SETTINGS_KEY); - if (saved) { - const parsedSettings = JSON.parse(saved); - setSettings(parsedSettings); + setSettings(JSON.parse(saved)); } else { - const defaultSettings = await getDefaultDictationSettings(getProviders); - setSettings(defaultSettings); + const providers = await getProviders(false); + // Check if we have an OpenAI API key as primary default + const openAIProvider = providers.find((p) => p.name === 'openai'); + if (openAIProvider && openAIProvider.is_configured) { + setSettings({ + enabled: true, + provider: 'openai', + }); + } else { + setSettings({ + enabled: false, + provider: null, + }); + } } // Load ElevenLabs API key from storage (non-secret for frontend access) diff --git a/ui/desktop/src/hooks/useWhisper.ts b/ui/desktop/src/hooks/useWhisper.ts index fe0ddb62d8c..24c9847a0a4 100644 --- a/ui/desktop/src/hooks/useWhisper.ts +++ b/ui/desktop/src/hooks/useWhisper.ts @@ -87,7 +87,7 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp // Define stopRecording before startRecording to avoid circular dependency const stopRecording = useCallback(() => { - setIsRecording(false); + setIsRecording(false); // Always update the visual state if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { mediaRecorderRef.current.stop(); @@ -159,20 +159,14 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp reader.readAsDataURL(audioBlob); }); - const mimeType = audioBlob.type; - if (!mimeType) { - throw new Error('Unable to determine audio format. Please try again.'); - } - let endpoint = ''; let headers: Record = { 'Content-Type': 'application/json', 'X-Secret-Key': await window.electron.getSecretKey(), }; - let body: Record = { audio: base64Audio, - mime_type: mimeType, + mime_type: 'audio/webm', }; // Choose endpoint based on provider @@ -240,32 +234,23 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp try { // Request microphone permission - const stream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - sampleRate: 44100, - }, - }); + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); streamRef.current = stream; - // Verify we have valid audio tracks - const audioTracks = stream.getAudioTracks(); - if (audioTracks.length === 0) { - throw new Error('No audio tracks available in the microphone stream'); - } - - // AudioContext creation is disabled to prevent MediaRecorder conflicts - setAudioContext(null); - setAnalyser(null); - - // Determine best supported MIME type - const supportedTypes = ['audio/webm;codecs=opus', 'audio/webm', 'audio/mp4', 'audio/wav']; + // Create audio context and analyser for visualization + const context = new AudioContext(); + const source = context.createMediaStreamSource(stream); + const analyserNode = context.createAnalyser(); + analyserNode.fftSize = 2048; + source.connect(analyserNode); - const mimeType = supportedTypes.find((type) => MediaRecorder.isTypeSupported(type)) || ''; + setAudioContext(context); + setAnalyser(analyserNode); - const mediaRecorder = new MediaRecorder(stream, mimeType ? { mimeType } : {}); + // Create MediaRecorder + const mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm', + }); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; @@ -312,49 +297,12 @@ export const useWhisper = ({ onTranscription, onError, onSizeWarning }: UseWhisp }; mediaRecorder.onstop = async () => { - const audioBlob = new Blob(audioChunksRef.current, { type: mimeType || 'audio/webm' }); - - // Check if the blob is empty - if (audioBlob.size === 0) { - onError?.( - new Error( - 'No audio data was recorded. Please check your microphone permissions and try again.' - ) - ); - return; - } - + const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); await transcribeAudio(audioBlob); }; - // Add error handler for MediaRecorder - mediaRecorder.onerror = (event) => { - console.error('MediaRecorder error:', event); - onError?.(new Error('Recording failed: Unknown error')); - }; - - if (!stream.active) { - throw new Error('Audio stream became inactive before recording could start'); - } - - // Check audio tracks again before starting recording - if (audioTracks.length === 0) { - throw new Error('No audio tracks available in the stream'); - } - - const activeAudioTracks = audioTracks.filter((track) => track.readyState === 'live'); - if (activeAudioTracks.length === 0) { - throw new Error('No live audio tracks available'); - } - - try { - mediaRecorder.start(100); - setIsRecording(true); - } catch (startError) { - console.error('Error calling mediaRecorder.start():', startError); - const errorMessage = startError instanceof Error ? startError.message : String(startError); - throw new Error(`Failed to start recording: ${errorMessage}`); - } + mediaRecorder.start(1000); // Collect data every second for size monitoring + setIsRecording(true); } catch (error) { console.error('Error starting recording:', error); stopRecording(); diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 18658fbc723..8b8f870af7d 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -297,8 +297,6 @@ async function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) { if (parsedUrl.hostname === 'extension') { window.webContents.send('add-extension', pendingDeepLink); - } else if (parsedUrl.hostname === 'sessions') { - window.webContents.send('open-shared-session', pendingDeepLink); } else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') { const recipeDeeplink = parsedUrl.searchParams.get('config'); const scheduledJobId = parsedUrl.searchParams.get('scheduledJob'); @@ -361,8 +359,6 @@ app.on('open-url', async (_event, url) => { if (parsedUrl.hostname === 'extension') { firstOpenWindow.webContents.send('add-extension', pendingDeepLink); - } else if (parsedUrl.hostname === 'sessions') { - firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink); } } }); @@ -465,16 +461,7 @@ const getGooseProvider = () => { ]; }; -const getSharingUrl = () => { - // checks app env for sharing url - loadShellEnv(app.isPackaged); // will try to take it from the zshrc file - // if GOOSE_BASE_URL_SHARE is found, we will set process.env.GOOSE_BASE_URL_SHARE, otherwise we return what it is set - // to in the env at bundle time - return process.env.GOOSE_BASE_URL_SHARE; -}; - const getVersion = () => { - // checks app env for sharing url loadShellEnv(app.isPackaged); // will try to take it from the zshrc file // to in the env at bundle time return process.env.GOOSE_VERSION; @@ -482,8 +469,6 @@ const getVersion = () => { const [provider, model, predefinedModels] = getGooseProvider(); -const sharingUrl = getSharingUrl(); - const gooseVersion = getVersion(); const SERVER_SECRET = process.env.GOOSE_EXTERNAL_BACKEND @@ -608,7 +593,7 @@ const createChat = async ( GOOSE_PORT: port, // Ensure this specific window gets the correct port GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir, - GOOSE_BASE_URL_SHARE: sharingUrl, + GOOSE_VERSION: gooseVersion, recipe: recipe, }), @@ -663,7 +648,7 @@ const createChat = async ( GOOSE_PORT: port, // Ensure this specific window's config gets the correct port GOOSE_WORKING_DIR: working_dir, REQUEST_DIR: dir, - GOOSE_BASE_URL_SHARE: sharingUrl, + recipe: recipe, }; diff --git a/ui/desktop/src/recipe/recipeStorage.ts b/ui/desktop/src/recipe/recipeStorage.ts index bd40aa051ba..d74b630863b 100644 --- a/ui/desktop/src/recipe/recipeStorage.ts +++ b/ui/desktop/src/recipe/recipeStorage.ts @@ -12,7 +12,6 @@ export interface SavedRecipe { isGlobal: boolean; lastModified: Date; isArchived?: boolean; - filename: string; // The actual filename used } /** @@ -67,7 +66,6 @@ async function loadRecipeFromFile( return { ...recipeData, isGlobal: isGlobal, - filename: recipeName, }; } catch (error) { console.warn(`Failed to load recipe from ${filePath}:`, error); @@ -114,7 +112,6 @@ export async function saveRecipe(recipe: Recipe, options: SaveRecipeOptions): Pr // Create saved recipe object const savedRecipe: SavedRecipe = { name: sanitizedName, - filename: sanitizedName, recipe: recipe, isGlobal: global, lastModified: new Date(), diff --git a/ui/desktop/src/sessionLinks.ts b/ui/desktop/src/sessionLinks.ts deleted file mode 100644 index 565ee941748..00000000000 --- a/ui/desktop/src/sessionLinks.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions'; -import { View } from './utils/navigationUtils'; - -export interface SessionLinksViewOptions { - sessionDetails?: SharedSessionDetails | null; - error?: string; - shareToken?: string; - baseUrl?: string; - - [key: string]: unknown; -} - -/** - * Handles opening a shared session from a deep link - * @param url The deep link URL (goose://sessions/:shareToken) - * @param setView Function to set the current view - * @param baseUrl Optional base URL for the session sharing API - * @returns Promise that resolves when the session is opened - */ -export async function openSharedSessionFromDeepLink( - url: string, - setView: (view: View, options?: SessionLinksViewOptions) => void, - baseUrl?: string -): Promise { - try { - if (!url.startsWith('goose://sessions/')) { - throw new Error('Invalid URL: URL must use the goose://sessions/ scheme'); - } - - // Extract the share token from the URL - const shareToken: string = url.replace('goose://sessions/', ''); - - if (!shareToken || shareToken.trim() === '') { - throw new Error('Invalid URL: Missing share token'); - } - - // If no baseUrl is provided, check if there's one in localStorage - if (!baseUrl) { - const savedSessionConfig = localStorage.getItem('session_sharing_config'); - if (savedSessionConfig) { - try { - const config = JSON.parse(savedSessionConfig); - if (config.enabled && config.baseUrl) { - baseUrl = config.baseUrl; - } else { - throw new Error( - 'Session sharing is not enabled or base URL is not configured. Check the settings page.' - ); - } - } catch (error) { - console.error('Error parsing session sharing config:', error); - throw new Error( - 'Session sharing is not enabled or base URL is not configured. Check the settings page.' - ); - } - } else { - throw new Error('Session sharing is not configured'); - } - } - - // Fetch the shared session details - const sessionDetails = await fetchSharedSessionDetails(baseUrl!, shareToken); - - // Navigate to the shared session view - setView('sharedSession', { - sessionDetails, - shareToken, - baseUrl, - }); - - return sessionDetails; - } catch (error) { - const errorMessage = `Failed to open shared session: ${error instanceof Error ? error.message : 'Unknown error'}`; - console.error(errorMessage); - - // Navigate to the shared session view with the error instead of throwing - setView('sharedSession', { - sessionDetails: null, - error: error instanceof Error ? error.message : 'Unknown error', - shareToken: url.replace('goose://sessions/', ''), - baseUrl, - }); - - return null; - } -} diff --git a/ui/desktop/src/sharedSessions.ts b/ui/desktop/src/sharedSessions.ts deleted file mode 100644 index 2dd469aa2ad..00000000000 --- a/ui/desktop/src/sharedSessions.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Message } from './types/message'; -import { safeJsonParse } from './utils/jsonUtils'; - -export interface SharedSessionDetails { - share_token: string; - created_at: number; - base_url: string; - description: string; - working_dir: string; - messages: Message[]; - message_count: number; - total_tokens: number | null; -} - -/** - * Fetches details for a specific shared session - * @param baseUrl The base URL for session sharing API - * @param shareToken The share token of the session to fetch - * @returns Promise with shared session details - */ -export async function fetchSharedSessionDetails( - baseUrl: string, - shareToken: string -): Promise { - try { - const response = await fetch(`${baseUrl}/sessions/share/${shareToken}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - // Origin: 'http://localhost:5173', // required to bypass Cloudflare security filter - }, - credentials: 'include', - }); - - if (!response.ok) { - throw new Error(`Failed to fetch shared session: ${response.status} ${response.statusText}`); - } - - const data = await safeJsonParse( - response, - 'Failed to parse shared session' - ); - - if (baseUrl != data.base_url) { - throw new Error(`Base URL mismatch for shared session: ${baseUrl} != ${data.base_url}`); - } - - return { - share_token: data.share_token, - created_at: data.created_at, - base_url: data.base_url, - description: data.description, - working_dir: data.working_dir, - messages: data.messages, - message_count: data.message_count, - total_tokens: data.total_tokens, - }; - } catch (error) { - console.error('Error fetching shared session:', error); - throw error; - } -} - -/** - * Creates a new shared session - * @param baseUrl The base URL for session sharing API - * @param workingDir The working directory for the shared session - * @param messages The messages to include in the shared session - * @param description Description for the shared session - * @param totalTokens Total token count for the session, or null if not available - * @param userName The user name for who is sharing the session - * @returns Promise with the share token - */ -export async function createSharedSession( - baseUrl: string, - workingDir: string, - messages: Message[], - description: string, - totalTokens: number | null -): Promise { - try { - const response = await fetch(`${baseUrl}/sessions/share`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - working_dir: workingDir, - messages, - description: description, - base_url: baseUrl, - total_tokens: totalTokens ?? null, - }), - }); - - if (!response.ok) { - if (response.status === 302) { - throw new Error( - `Failed to create shared session. Please check that you are connected to VPN - ${response.status} ${response.statusText}` - ); - } - throw new Error(`Failed to create shared session: ${response.status} ${response.statusText}`); - } - - const data = await safeJsonParse<{ share_token: string }>( - response, - 'Failed to parse shared session response' - ); - return data.share_token; - } catch (error) { - console.error('Error creating shared session:', error); - throw error; - } -} diff --git a/ui/desktop/src/utils/appInitialization.ts b/ui/desktop/src/utils/appInitialization.ts index 054142036ca..d45674b3910 100644 --- a/ui/desktop/src/utils/appInitialization.ts +++ b/ui/desktop/src/utils/appInitialization.ts @@ -181,7 +181,7 @@ const handleViewTypeDeepLink = (viewType: string, recipeConfig: unknown) => { recipes: '#/recipes', permission: '#/permission', ConfigureProviders: '#/configure-providers', - sharedSession: '#/shared-session', + recipeEditor: '#/recipe-editor', welcome: '#/welcome', }; diff --git a/ui/desktop/src/utils/navigationUtils.ts b/ui/desktop/src/utils/navigationUtils.ts index dd866b0a3b8..59f727ea9cd 100644 --- a/ui/desktop/src/utils/navigationUtils.ts +++ b/ui/desktop/src/utils/navigationUtils.ts @@ -13,7 +13,6 @@ export type View = | 'settingsV2' | 'sessions' | 'schedules' - | 'sharedSession' | 'loading' | 'recipeEditor' | 'recipes' @@ -61,9 +60,6 @@ export const createNavigationHandler = (navigate: NavigateFunction) => { case 'ConfigureProviders': navigate('/configure-providers', { state: options }); break; - case 'sharedSession': - navigate('/shared-session', { state: options }); - break; case 'recipeEditor': navigate('/recipe-editor', { state: options }); break;