From 5127173a08e3c5690074eff72d4bed9e60bbdfa8 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 31 Jul 2025 10:48:54 +1000 Subject: [PATCH 01/12] checkpoint --- crates/goose-mcp/src/developer/mod.rs | 37 +++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index f77749c07a91..f6785d251d64 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -748,9 +748,42 @@ impl DeveloperRouter { ))); } + // limit it to 100 lines result, anyting more we will shunt to a tmp file and include it in the result + let lines: Vec<&str> = output_str.lines().collect(); + let line_count = lines.len(); + + let final_output = if line_count > 100 { + // Create a temporary file with the full output + let tmp_file = tempfile::NamedTempFile::new().map_err(|e| { + ToolError::ExecutionError(format!("Failed to create temporary file: {}", e)) + })?; + + // Write the full output to the temp file + std::fs::write(tmp_file.path(), &output_str).map_err(|e| { + ToolError::ExecutionError(format!("Failed to write to temporary file: {}", e)) + })?; + + // Keep the temp file from being deleted + let (_, path) = tmp_file.keep().map_err(|e| { + ToolError::ExecutionError(format!("Failed to persist temporary file: {}", e)) + })?; + + // Take only the last 100 lines + let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); + + format!( + "The output is very large at {} lines. Below are last 100 lines. To see rest, please look in {}\n\n{}", + line_count, + path.display(), + last_100_lines.join("\n") + ) + } else { + output_str.clone() + }; + Ok(vec![ - Content::text(output_str.clone()).with_audience(vec![Role::Assistant]), - Content::text(output_str) + Content::text(final_output.clone()).with_audience(vec![Role::Assistant]), + Content::text(final_output) .with_audience(vec![Role::User]) .with_priority(0.0), ]) From 41793fdf9a7a5e32e13947a4eb10e151779ee1b0 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 31 Jul 2025 11:11:17 +1000 Subject: [PATCH 02/12] separate user output --- crates/goose-mcp/src/developer/mod.rs | 74 ++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index f6785d251d64..cc7e0a0c03ff 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -781,9 +781,17 @@ impl DeveloperRouter { output_str.clone() }; + // For the user message, if we truncated output, show only the last 100 lines with a prefix + let user_output = if line_count > 100 { + let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); + format!("... final 100 lines:\n{}", last_100_lines.join("\n")) + } else { + output_str.clone() + }; + Ok(vec![ - Content::text(final_output.clone()).with_audience(vec![Role::Assistant]), - Content::text(final_output) + Content::text(final_output).with_audience(vec![Role::Assistant]), + Content::text(user_output) .with_audience(vec![Role::User]) .with_priority(0.0), ]) @@ -3172,4 +3180,66 @@ mod tests { temp_dir.close().unwrap(); } + + #[tokio::test] + #[serial] + async fn test_bash_output_truncation() { + let router = get_router().await; + + let temp_dir = tempfile::tempdir().unwrap(); + std::env::set_current_dir(&temp_dir).unwrap(); + + // Create a command that generates > 100 lines of output + let command = if cfg!(windows) { + "for /L %i in (1,1,150) do @echo Line %i" + } else { + "for i in {1..150}; do echo \"Line $i\"; done" + }; + + let result = router + .call_tool("shell", json!({ "command": command }), dummy_sender()) + .await + .unwrap(); + + // Should have two Content items + assert_eq!(result.len(), 2); + + // Find the Assistant and User content + let assistant_content = result + .iter() + .find(|c| { + c.audience() + .is_some_and(|roles| roles.contains(&Role::Assistant)) + }) + .unwrap() + .as_text() + .unwrap(); + + let user_content = result + .iter() + .find(|c| { + c.audience() + .is_some_and(|roles| roles.contains(&Role::User)) + }) + .unwrap() + .as_text() + .unwrap(); + + // Assistant should get the full message with temp file info + assert!(assistant_content.text.contains("The output is very large at")); + assert!(assistant_content.text.contains("lines. Below are last 100 lines")); + assert!(assistant_content.text.contains("To see rest, please look in")); + + // User should only get the truncated output with prefix + assert!(user_content.text.starts_with("... final 100 lines:\n")); + assert!(!user_content.text.contains("The output is very large")); + assert!(!user_content.text.contains("To see rest")); + + // User output should contain lines 51-150 (last 100 lines) + assert!(user_content.text.contains("Line 51")); + assert!(user_content.text.contains("Line 150")); + assert!(!user_content.text.contains("Line 50")); + + temp_dir.close().unwrap(); + } } From 3188a4a1f44a5ebd36e0f66b37f7af4c763dfd84 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 31 Jul 2025 12:30:07 +1000 Subject: [PATCH 03/12] provide output for different audiences --- crates/goose-mcp/src/developer/mod.rs | 203 ++++++++++++++++++++------ 1 file changed, 157 insertions(+), 46 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index cc7e0a0c03ff..ba112b1cf0da 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -577,6 +577,51 @@ impl DeveloperRouter { self.ignore_patterns.matched(path, false).is_ignore() } + // shell output can be large, this will help manage that + fn process_shell_output(&self, output_str: &str) -> Result<(String, String), ToolError> { + let lines: Vec<&str> = output_str.lines().collect(); + let line_count = lines.len(); + + let final_output = if line_count > 100 { + // Create a temporary file with the full output + let tmp_file = tempfile::NamedTempFile::new().map_err(|e| { + ToolError::ExecutionError(format!("Failed to create temporary file: {}", e)) + })?; + + // Write the full output to the temp file + std::fs::write(tmp_file.path(), output_str).map_err(|e| { + ToolError::ExecutionError(format!("Failed to write to temporary file: {}", e)) + })?; + + // Keep the temp file from being deleted + let (_, path) = tmp_file.keep().map_err(|e| { + ToolError::ExecutionError(format!("Failed to persist temporary file: {}", e)) + })?; + + // Take only the last 100 lines + let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); + + format!( + "private note: output was {} lines and we are only showing the most recent lines, remainder of lines in {}. do not show tmp file to user, that file can be searched if extra context needed to fulfill request. truncated output: \n{}", + line_count, + path.display(), + last_100_lines.join("\n") + ) + } else { + output_str.to_string() + }; + + // For the user message, if we truncated output, show only the last 100 lines with a prefix + let user_output = if line_count > 100 { + let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); + format!("... \n{}", last_100_lines.join("\n")) + } else { + output_str.to_string() + }; + + Ok((final_output, user_output)) + } + // Helper method to resolve a path relative to cwd with platform-specific handling fn resolve_path(&self, path_str: &str) -> Result { let cwd = std::env::current_dir().expect("should have a current working dir"); @@ -748,46 +793,7 @@ impl DeveloperRouter { ))); } - // limit it to 100 lines result, anyting more we will shunt to a tmp file and include it in the result - let lines: Vec<&str> = output_str.lines().collect(); - let line_count = lines.len(); - - let final_output = if line_count > 100 { - // Create a temporary file with the full output - let tmp_file = tempfile::NamedTempFile::new().map_err(|e| { - ToolError::ExecutionError(format!("Failed to create temporary file: {}", e)) - })?; - - // Write the full output to the temp file - std::fs::write(tmp_file.path(), &output_str).map_err(|e| { - ToolError::ExecutionError(format!("Failed to write to temporary file: {}", e)) - })?; - - // Keep the temp file from being deleted - let (_, path) = tmp_file.keep().map_err(|e| { - ToolError::ExecutionError(format!("Failed to persist temporary file: {}", e)) - })?; - - // Take only the last 100 lines - let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); - - format!( - "The output is very large at {} lines. Below are last 100 lines. To see rest, please look in {}\n\n{}", - line_count, - path.display(), - last_100_lines.join("\n") - ) - } else { - output_str.clone() - }; - - // For the user message, if we truncated output, show only the last 100 lines with a prefix - let user_output = if line_count > 100 { - let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); - format!("... final 100 lines:\n{}", last_100_lines.join("\n")) - } else { - output_str.clone() - }; + let (final_output, user_output) = self.process_shell_output(&output_str)?; Ok(vec![ Content::text(final_output).with_audience(vec![Role::Assistant]), @@ -3184,11 +3190,11 @@ mod tests { #[tokio::test] #[serial] async fn test_bash_output_truncation() { - let router = get_router().await; - let temp_dir = tempfile::tempdir().unwrap(); std::env::set_current_dir(&temp_dir).unwrap(); + let router = get_router().await; + // Create a command that generates > 100 lines of output let command = if cfg!(windows) { "for /L %i in (1,1,150) do @echo Line %i" @@ -3226,15 +3232,21 @@ mod tests { .unwrap(); // Assistant should get the full message with temp file info - assert!(assistant_content.text.contains("The output is very large at")); - assert!(assistant_content.text.contains("lines. Below are last 100 lines")); - assert!(assistant_content.text.contains("To see rest, please look in")); + assert!(assistant_content + .text + .contains("The output is very large at")); + assert!(assistant_content + .text + .contains("lines. Below are last 100 lines")); + assert!(assistant_content + .text + .contains("To see rest, please look in")); // User should only get the truncated output with prefix assert!(user_content.text.starts_with("... final 100 lines:\n")); assert!(!user_content.text.contains("The output is very large")); assert!(!user_content.text.contains("To see rest")); - + // User output should contain lines 51-150 (last 100 lines) assert!(user_content.text.contains("Line 51")); assert!(user_content.text.contains("Line 150")); @@ -3242,4 +3254,103 @@ mod tests { temp_dir.close().unwrap(); } + + #[test] + fn test_process_shell_output_short() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let router = DeveloperRouter::new(); + + // Test with short output (< 100 lines) + let short_output = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"; + let result = router.process_shell_output(short_output).unwrap(); + + // Both outputs should be the same for short outputs + assert_eq!(result.0, short_output); + assert_eq!(result.1, short_output); + } + + #[test] + fn test_process_shell_output_long() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let router = DeveloperRouter::new(); + + // Test with long output (> 100 lines) + let lines: Vec = (1..=150).map(|i| format!("Line {}", i)).collect(); + let long_output = lines.join("\n"); + + let result = router.process_shell_output(&long_output).unwrap(); + let (assistant_output, user_output) = result; + + // Assistant output should contain the full message with temp file info + assert!(assistant_output.contains("The output is very large at 150 lines")); + assert!(assistant_output.contains("Below are last 100 lines")); + assert!(assistant_output.contains("To see rest, please look in")); + assert!(assistant_output.contains("Line 51")); + assert!(assistant_output.contains("Line 150")); + assert!(!assistant_output.contains("Line 50")); + + // User output should only have the prefix and last 100 lines + assert!(user_output.starts_with("... final 100 lines:\n")); + assert!(user_output.contains("Line 51")); + assert!(user_output.contains("Line 150")); + assert!(!user_output.contains("Line 50")); + assert!(!user_output.contains("The output is very large")); + assert!(!user_output.contains("To see rest")); + } + + #[test] + fn test_process_shell_output_exactly_100_lines() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let router = DeveloperRouter::new(); + + // Test with exactly 100 lines + let lines: Vec = (1..=100).map(|i| format!("Line {}", i)).collect(); + let output = lines.join("\n"); + + let result = router.process_shell_output(&output).unwrap(); + + // Both outputs should be the same for exactly 100 lines + assert_eq!(result.0, output); + assert_eq!(result.1, output); + } + + #[test] + fn test_process_shell_output_empty() { + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let router = DeveloperRouter::new(); + + // Test with empty output + let empty_output = ""; + let result = router.process_shell_output(empty_output).unwrap(); + + // Both outputs should be empty + assert_eq!(result.0, ""); + assert_eq!(result.1, ""); + } + + #[test] + fn test_process_shell_output_handles_temp_file_errors() { + // This test is harder to implement without mocking, but the function + // should handle temp file creation errors gracefully + + let dir = TempDir::new().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let router = DeveloperRouter::new(); + + // Normal usage should work without errors + let lines: Vec = (1..=150).map(|i| format!("Line {}", i)).collect(); + let output = lines.join("\n"); + + let result = router.process_shell_output(&output); + assert!(result.is_ok()); + } } From 333ae555e3eaf3c3fd61872eb5e96b80bafcf048 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 31 Jul 2025 13:21:02 +1000 Subject: [PATCH 04/12] tidy up --- crates/goose-mcp/src/developer/mod.rs | 29 ++++++--------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index ba112b1cf0da..b11641fa88fc 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -583,22 +583,18 @@ impl DeveloperRouter { let line_count = lines.len(); let final_output = if line_count > 100 { - // Create a temporary file with the full output let tmp_file = tempfile::NamedTempFile::new().map_err(|e| { ToolError::ExecutionError(format!("Failed to create temporary file: {}", e)) })?; - // Write the full output to the temp file std::fs::write(tmp_file.path(), output_str).map_err(|e| { ToolError::ExecutionError(format!("Failed to write to temporary file: {}", e)) })?; - // Keep the temp file from being deleted let (_, path) = tmp_file.keep().map_err(|e| { ToolError::ExecutionError(format!("Failed to persist temporary file: {}", e)) })?; - // Take only the last 100 lines let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); format!( @@ -611,7 +607,6 @@ impl DeveloperRouter { output_str.to_string() }; - // For the user message, if we truncated output, show only the last 100 lines with a prefix let user_output = if line_count > 100 { let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); format!("... \n{}", last_100_lines.join("\n")) @@ -3232,20 +3227,11 @@ mod tests { .unwrap(); // Assistant should get the full message with temp file info - assert!(assistant_content - .text - .contains("The output is very large at")); - assert!(assistant_content - .text - .contains("lines. Below are last 100 lines")); - assert!(assistant_content - .text - .contains("To see rest, please look in")); + assert!(assistant_content.text.contains("private note: output was")); // User should only get the truncated output with prefix - assert!(user_content.text.starts_with("... final 100 lines:\n")); - assert!(!user_content.text.contains("The output is very large")); - assert!(!user_content.text.contains("To see rest")); + assert!(user_content.text.starts_with("...")); + assert!(!user_content.text.contains("private note: output was")); // User output should contain lines 51-150 (last 100 lines) assert!(user_content.text.contains("Line 51")); @@ -3286,20 +3272,17 @@ mod tests { let (assistant_output, user_output) = result; // Assistant output should contain the full message with temp file info - assert!(assistant_output.contains("The output is very large at 150 lines")); - assert!(assistant_output.contains("Below are last 100 lines")); - assert!(assistant_output.contains("To see rest, please look in")); + assert!(assistant_output.contains("private note: output was")); assert!(assistant_output.contains("Line 51")); assert!(assistant_output.contains("Line 150")); assert!(!assistant_output.contains("Line 50")); // User output should only have the prefix and last 100 lines - assert!(user_output.starts_with("... final 100 lines:\n")); + assert!(user_output.starts_with("...")); assert!(user_output.contains("Line 51")); assert!(user_output.contains("Line 150")); assert!(!user_output.contains("Line 50")); - assert!(!user_output.contains("The output is very large")); - assert!(!user_output.contains("To see rest")); + assert!(!user_output.contains("private note: output was")); } #[test] From 6aefba2a1a747bdabd8d5a309a905bdbef6789c3 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 31 Jul 2025 15:38:27 +1000 Subject: [PATCH 05/12] remove junk test --- crates/goose-mcp/src/developer/mod.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index b11641fa88fc..773300743d78 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -3318,22 +3318,4 @@ mod tests { assert_eq!(result.0, ""); assert_eq!(result.1, ""); } - - #[test] - fn test_process_shell_output_handles_temp_file_errors() { - // This test is harder to implement without mocking, but the function - // should handle temp file creation errors gracefully - - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let router = DeveloperRouter::new(); - - // Normal usage should work without errors - let lines: Vec = (1..=150).map(|i| format!("Line {}", i)).collect(); - let output = lines.join("\n"); - - let result = router.process_shell_output(&output); - assert!(result.is_ok()); - } } From 6bdd2a3e786482274fb79e84f3c7c785294c035d Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 08:28:57 +1000 Subject: [PATCH 06/12] tidy up --- crates/goose-mcp/src/developer/mod.rs | 46 --------------------------- 1 file changed, 46 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 773300743d78..9d4bc97c09e5 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -3257,52 +3257,6 @@ mod tests { assert_eq!(result.1, short_output); } - #[test] - fn test_process_shell_output_long() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let router = DeveloperRouter::new(); - - // Test with long output (> 100 lines) - let lines: Vec = (1..=150).map(|i| format!("Line {}", i)).collect(); - let long_output = lines.join("\n"); - - let result = router.process_shell_output(&long_output).unwrap(); - let (assistant_output, user_output) = result; - - // Assistant output should contain the full message with temp file info - assert!(assistant_output.contains("private note: output was")); - assert!(assistant_output.contains("Line 51")); - assert!(assistant_output.contains("Line 150")); - assert!(!assistant_output.contains("Line 50")); - - // User output should only have the prefix and last 100 lines - assert!(user_output.starts_with("...")); - assert!(user_output.contains("Line 51")); - assert!(user_output.contains("Line 150")); - assert!(!user_output.contains("Line 50")); - assert!(!user_output.contains("private note: output was")); - } - - #[test] - fn test_process_shell_output_exactly_100_lines() { - let dir = TempDir::new().unwrap(); - std::env::set_current_dir(dir.path()).unwrap(); - - let router = DeveloperRouter::new(); - - // Test with exactly 100 lines - let lines: Vec = (1..=100).map(|i| format!("Line {}", i)).collect(); - let output = lines.join("\n"); - - let result = router.process_shell_output(&output).unwrap(); - - // Both outputs should be the same for exactly 100 lines - assert_eq!(result.0, output); - assert_eq!(result.1, output); - } - #[test] fn test_process_shell_output_empty() { let dir = TempDir::new().unwrap(); From f304791863688fd5759dfa08db4a80dc169a6de4 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 11:26:03 +1000 Subject: [PATCH 07/12] make tests serial --- crates/goose/src/model.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 6799c01ad9c0..a5739386b4cc 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -244,8 +244,10 @@ impl ModelConfig { mod tests { use super::*; use temp_env::with_var; + use serial_test::serial; #[test] + #[serial] fn test_model_config_context_limits() { let config = ModelConfig::new("claude-3-opus") .unwrap() @@ -263,6 +265,7 @@ mod tests { } #[test] + #[serial] fn test_invalid_context_limit() { with_var("GOOSE_CONTEXT_LIMIT", Some("abc"), || { let result = ModelConfig::new("test-model"); @@ -285,6 +288,7 @@ mod tests { } #[test] + #[serial] fn test_invalid_temperature() { with_var("GOOSE_TEMPERATURE", Some("hot"), || { let result = ModelConfig::new("test-model"); @@ -298,6 +302,7 @@ mod tests { } #[test] + #[serial] fn test_invalid_toolshim() { with_var("GOOSE_TOOLSHIM", Some("maybe"), || { let result = ModelConfig::new("test-model"); @@ -311,6 +316,7 @@ mod tests { } #[test] + #[serial] fn test_empty_toolshim_model() { with_var("GOOSE_TOOLSHIM_OLLAMA_MODEL", Some(""), || { let result = ModelConfig::new("test-model"); @@ -328,6 +334,7 @@ mod tests { } #[test] + #[serial] fn test_valid_configurations() { // Test with environment variables set with_var("GOOSE_CONTEXT_LIMIT", Some("50000"), || { From 05605c5878732e0eb1a6bad01ee162e9bfa72131 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 11:32:04 +1000 Subject: [PATCH 08/12] fmt --- crates/goose/src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index a5739386b4cc..be123c469e89 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -243,8 +243,8 @@ impl ModelConfig { #[cfg(test)] mod tests { use super::*; - use temp_env::with_var; use serial_test::serial; + use temp_env::with_var; #[test] #[serial] From edc6992c180ee0fe96f89ad2a8027e1041a7351a Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 11:44:11 +1000 Subject: [PATCH 09/12] fixing still --- ui/desktop/openapi.json | 1 - ui/desktop/src/api/types.gen.ts | 2 +- ui/desktop/src/components/context_management/index.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 34850d03d3c9..39fa7382981a 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -1862,7 +1862,6 @@ "description": "A message to or from an LLM", "required": [ "role", - "created", "content" ], "properties": { diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 79dd33acf3b1..bea406ee2131 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -280,7 +280,7 @@ export type ListSchedulesResponse = { */ export type Message = { content: Array; - created: number; + created?: number; id?: string | null; role: Role; }; diff --git a/ui/desktop/src/components/context_management/index.ts b/ui/desktop/src/components/context_management/index.ts index 6af637a07d51..22f6533b8dc9 100644 --- a/ui/desktop/src/components/context_management/index.ts +++ b/ui/desktop/src/components/context_management/index.ts @@ -60,7 +60,7 @@ export function convertApiMessageToFrontendMessage( sendToLLM: sendToLLM ?? true, id: generateId(), role: apiMessage.role as Role, - created: apiMessage.created, + created: apiMessage.created ?? 0, content: apiMessage.content .map((apiContent) => mapApiContentToFrontendMessageContent(apiContent)) .filter((content): content is FrontendMessageContent => content !== null), From 69680706ce712bf45f4f268a468b0a801b83efc7 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 12:10:02 +1000 Subject: [PATCH 10/12] precheck --- crates/goose-mcp/src/developer/mod.rs | 44 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 553d10c51ff2..db7fc6011c6a 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -599,6 +599,9 @@ impl DeveloperRouter { let lines: Vec<&str> = output_str.lines().collect(); let line_count = lines.len(); + let start = lines.len().saturating_sub(100); + let last_100_lines_str = lines[start..].join("\n"); + let final_output = if line_count > 100 { let tmp_file = tempfile::NamedTempFile::new().map_err(|e| { ToolError::ExecutionError(format!("Failed to create temporary file: {}", e)) @@ -612,21 +615,18 @@ impl DeveloperRouter { ToolError::ExecutionError(format!("Failed to persist temporary file: {}", e)) })?; - let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); - format!( - "private note: output was {} lines and we are only showing the most recent lines, remainder of lines in {}. do not show tmp file to user, that file can be searched if extra context needed to fulfill request. truncated output: \n{}", + "private note: output was {} lines and we are only showing the most recent lines, remainder of lines in {} do not show tmp file to user, that file can be searched if extra context needed to fulfill request. truncated output: \n{}", line_count, path.display(), - last_100_lines.join("\n") + last_100_lines_str ) } else { output_str.to_string() }; let user_output = if line_count > 100 { - let last_100_lines: Vec<&str> = lines.iter().rev().take(100).rev().copied().collect(); - format!("... \n{}", last_100_lines.join("\n")) + format!("... \n{}", last_100_lines_str) } else { output_str.to_string() }; @@ -1717,7 +1717,7 @@ mod tests { use super::*; use serde_json::json; use serial_test::serial; - use std::fs; + use std::fs::{self, read_to_string}; use tempfile::TempDir; use tokio::sync::OnceCell; @@ -3288,6 +3288,36 @@ mod tests { assert!(user_content.text.contains("Line 150")); assert!(!user_content.text.contains("Line 50")); + println!("assistant output: {}", assistant_content.text); + + let start_tag = "remainder of lines in"; + let end_tag = "do not show tmp file to user"; + + if let (Some(start), Some(end)) = ( + assistant_content.text.find(start_tag), + assistant_content.text.find(end_tag) + ) { + let start_idx = start + start_tag.len(); + if start_idx < end { + let path = assistant_content.text[start_idx..end].trim(); + println!("Extracted path: {}", path); + } + let file_contents = read_to_string(path).expect("Failed to read extracted temp file"); + + let lines: Vec<&str> = file_contents.lines().collect(); + + // Ensure we have exactly 150 lines + assert_eq!(lines.len(), 150, "Expected 150 lines in temp file"); + + // Ensure the first and last lines are correct + assert_eq!(lines.first(), Some(&"Line 1"), "First line mismatch"); + assert_eq!(lines.last(), Some(&"Line 150"), "Last line mismatch"); + } + + + + + temp_dir.close().unwrap(); } From 86201873ca7b8d5904bd3c15e7ba3415b1ec8b82 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 12:15:20 +1000 Subject: [PATCH 11/12] working --- crates/goose-mcp/src/developer/mod.rs | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index db7fc6011c6a..e39cd145ad96 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -1715,6 +1715,7 @@ impl Clone for DeveloperRouter { #[cfg(test)] mod tests { use super::*; + use core::panic; use serde_json::json; use serial_test::serial; use std::fs::{self, read_to_string}; @@ -3295,28 +3296,30 @@ mod tests { if let (Some(start), Some(end)) = ( assistant_content.text.find(start_tag), - assistant_content.text.find(end_tag) + assistant_content.text.find(end_tag), ) { let start_idx = start + start_tag.len(); if start_idx < end { let path = assistant_content.text[start_idx..end].trim(); println!("Extracted path: {}", path); - } - let file_contents = read_to_string(path).expect("Failed to read extracted temp file"); - - let lines: Vec<&str> = file_contents.lines().collect(); - // Ensure we have exactly 150 lines - assert_eq!(lines.len(), 150, "Expected 150 lines in temp file"); - - // Ensure the first and last lines are correct - assert_eq!(lines.first(), Some(&"Line 1"), "First line mismatch"); - assert_eq!(lines.last(), Some(&"Line 150"), "Last line mismatch"); - } + let file_contents = + read_to_string(path).expect("Failed to read extracted temp file"); - + let lines: Vec<&str> = file_contents.lines().collect(); + // Ensure we have exactly 150 lines + assert_eq!(lines.len(), 150, "Expected 150 lines in temp file"); + // Ensure the first and last lines are correct + assert_eq!(lines.first(), Some(&"Line 1"), "First line mismatch"); + assert_eq!(lines.last(), Some(&"Line 150"), "Last line mismatch"); + } else { + panic!("No path found in bash output truncation output"); + } + } else { + panic!("Failed to find start or end tag in bash output truncation output"); + } temp_dir.close().unwrap(); } From 9d24c554c73506a4a6010b1f9880d61be529dde5 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Fri, 1 Aug 2025 12:17:01 +1000 Subject: [PATCH 12/12] cleanup --- crates/goose-mcp/src/developer/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index e39cd145ad96..8accfe0e8298 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -3289,8 +3289,6 @@ mod tests { assert!(user_content.text.contains("Line 150")); assert!(!user_content.text.contains("Line 50")); - println!("assistant output: {}", assistant_content.text); - let start_tag = "remainder of lines in"; let end_tag = "do not show tmp file to user";