diff --git a/Cargo.lock b/Cargo.lock index 7268ed204f01..9551b29d80f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2636,7 +2636,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.7.0" +version = "1.8.0" dependencies = [ "ahash", "anyhow", @@ -2710,7 +2710,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -2733,7 +2733,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.7.0" +version = "1.8.0" dependencies = [ "agent-client-protocol", "anstream", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -2838,7 +2838,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.7.0" +version = "1.8.0" dependencies = [ "anyhow", "async-trait", @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.7.0" +version = "1.8.0" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ff90733f4c9f..ec3aa42bd292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.7.0" +version = "1.8.0" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" diff --git a/crates/goose-server/src/routes/recipe_utils.rs b/crates/goose-server/src/routes/recipe_utils.rs index 8fba2fea6853..f55b19314638 100644 --- a/crates/goose-server/src/routes/recipe_utils.rs +++ b/crates/goose-server/src/routes/recipe_utils.rs @@ -31,7 +31,7 @@ fn short_id_from_path(path: &str) -> String { format!("{:016x}", h) } -fn load_recipes_from_path(path: &PathBuf) -> Result> { +fn load_recipes_from_path(path: &PathBuf, is_global: bool) -> Result> { let mut recipe_manifests_with_path = Vec::new(); if path.exists() { for entry in fs::read_dir(path)? { @@ -43,14 +43,18 @@ fn load_recipes_from_path(path: &PathBuf) -> Result> let Ok(recipe) = Recipe::from_content(&recipe_file.content) else { continue; }; - let Ok(recipe_metadata) = RecipeManifestMetadata::from_yaml_file(&path) else { - continue; - }; let Ok(last_modified) = fs::metadata(path.clone()).map(|m| { chrono::DateTime::::from(m.modified().unwrap()).to_rfc3339() }) else { continue; }; + let recipe_metadata = + RecipeManifestMetadata::from_yaml_file(&path).unwrap_or_else(|_| { + RecipeManifestMetadata { + name: recipe.title.clone(), + is_global, + } + }); let manifest_with_path = RecipeManifestWithPath { id: short_id_from_path(recipe_file.file_path.to_string_lossy().as_ref()), @@ -78,8 +82,8 @@ pub fn get_all_recipes_manifests() -> Result> { let mut recipe_manifests_with_path = Vec::new(); - recipe_manifests_with_path.extend(load_recipes_from_path(&local_recipe_path)?); - recipe_manifests_with_path.extend(load_recipes_from_path(&global_recipe_path)?); + recipe_manifests_with_path.extend(load_recipes_from_path(&local_recipe_path, false)?); + recipe_manifests_with_path.extend(load_recipes_from_path(&global_recipe_path, true)?); recipe_manifests_with_path.sort_by(|a, b| b.last_modified.cmp(&a.last_modified)); Ok(recipe_manifests_with_path) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 5e3eee242d8e..1dad5f494ad1 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1467,6 +1467,14 @@ impl Agent { tracing::debug!("Retrieved {} tools for recipe creation", tools.len()); messages.push(Message::user().with_text(recipe_prompt)); + + let (messages, issues) = fix_conversation(messages); + if !issues.is_empty() { + issues + .iter() + .for_each(|issue| tracing::warn!(recipe.conversation.issue = issue)); + } + tracing::debug!( "Added recipe prompt to messages, total messages: {}", messages.len() diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 46e18a0fd2a4..6e5b017a6710 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.7.0" + "version": "1.8.0" }, "paths": { "/agent/add_sub_recipes": { diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 7fcf091b689c..6d59e48ceada 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.7.0", + "version": "1.8.0", "license": "Apache-2.0", "dependencies": { "@ai-sdk/openai": "^2.0.14", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index b0b5a4af7cbf..b95f7757bf25 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "Goose", - "version": "1.7.0", + "version": "1.8.0", "description": "Goose App", "engines": { "node": "^22.17.1" diff --git a/ui/desktop/src/components/MarkdownContent.test.tsx b/ui/desktop/src/components/MarkdownContent.test.tsx index 889fb624968e..b3aa6b260ce8 100644 --- a/ui/desktop/src/components/MarkdownContent.test.tsx +++ b/ui/desktop/src/components/MarkdownContent.test.tsx @@ -322,4 +322,72 @@ more text`; }); }); }); + + describe('URL Overflow Handling', () => { + it('handles very long URLs without overflow', async () => { + const longUrl = + 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1'; + const content = `Check out this document: ${longUrl} + +Another very long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3¶m4=value4¶m5=value5`; + + const { container } = render(); + + await waitFor(() => { + expect(screen.getByText(/Check out this document/)).toBeInTheDocument(); + expect(screen.getByText(/Another very long URL/)).toBeInTheDocument(); + }); + + // Check that URLs are rendered as links + const links = container.querySelectorAll('a'); + expect(links.length).toBeGreaterThan(0); + + // Check that links have proper CSS classes for word breaking + links.forEach((link) => { + // The CSS should allow the text to break + expect(link).toBeInTheDocument(); + }); + }); + + it('handles markdown links with long URLs', async () => { + const longUrl = + 'https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1'; + const content = `[Click here for the document](${longUrl})`; + + render(); + + await waitFor(() => { + const link = screen.getByRole('link', { name: 'Click here for the document' }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', longUrl); + }); + }); + + it('handles multiple long URLs in the same message', async () => { + const content = `Here are some long URLs: + +1. Example Doc: https://example-docs.com/document/d/1oruk3lcrnhoOXMFzBJB8X6qQ5AtQTmj4XXxXk3xK-3g/edit?usp=sharing&mode=edit&version=1 +2. Another long URL: https://www.example.com/very/long/path/with/many/segments/and/parameters?param1=value1¶m2=value2¶m3=value3 +3. Third URL: https://api.example.com/v1/users/12345/documents/67890/attachments/abcdef123456789?format=json&include=metadata&sort=created_at`; + + render(); + + await waitFor(() => { + expect(screen.getByText(/Here are some long URLs/)).toBeInTheDocument(); + expect(screen.getByText(/Example Doc/)).toBeInTheDocument(); + expect(screen.getByText(/Another long URL/)).toBeInTheDocument(); + expect(screen.getByText(/Third URL/)).toBeInTheDocument(); + }); + }); + + it('applies word-break CSS classes to the container', () => { + const content = 'Test content'; + render(); + + const markdownContainer = document.querySelector('.prose'); + expect(markdownContainer).toBeInTheDocument(); + expect(markdownContainer).toHaveClass('prose-a:break-all'); + expect(markdownContainer).toHaveClass('prose-a:overflow-wrap-anywhere'); + }); + }); }); diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx index 5c4020fbda31..a8bb679b3516 100644 --- a/ui/desktop/src/components/MarkdownContent.tsx +++ b/ui/desktop/src/components/MarkdownContent.tsx @@ -139,21 +139,22 @@ const MarkdownContent = memo(function MarkdownContent({ return (
{ +async function decodeRecipeMain(deeplink: string): Promise { try { - const response = await fetch(getApiUrlMain('/recipes/decode', port), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ deeplink }), - }); - - if (response.ok) { - const data = await response.json(); - return data.recipe; - } - } catch { - console.error('Failed to decode recipe'); + return ( + await decodeRecipe({ + throwOnError: true, + body: { deeplink }, + }) + ).data.recipe; + } catch (e) { + console.error('Failed to decode recipe:', e); } return null; } @@ -758,7 +746,7 @@ const createChat = async ( console.log('[Main] Starting background recipe decoding for:', recipeDeeplink); // Decode recipe asynchronously after window is created - decodeRecipeMain(recipeDeeplink, port) + decodeRecipeMain(recipeDeeplink) .then((decodedRecipe) => { if (decodedRecipe) { console.log('[Main] Recipe decoded successfully, updating window config');