diff --git a/Cargo.lock b/Cargo.lock index f3b33f59272b..a91b337cf890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2971,7 +2971,7 @@ dependencies = [ [[package]] name = "goose" -version = "1.21.1" +version = "1.21.2" dependencies = [ "agent-client-protocol-schema", "ahash", @@ -3059,7 +3059,7 @@ dependencies = [ [[package]] name = "goose-acp" -version = "1.21.1" +version = "1.21.2" dependencies = [ "anyhow", "assert-json-diff", @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "goose-bench" -version = "1.21.1" +version = "1.21.2" dependencies = [ "anyhow", "async-trait", @@ -3106,7 +3106,7 @@ dependencies = [ [[package]] name = "goose-cli" -version = "1.21.1" +version = "1.21.2" dependencies = [ "anstream", "anyhow", @@ -3156,7 +3156,7 @@ dependencies = [ [[package]] name = "goose-mcp" -version = "1.21.1" +version = "1.21.2" dependencies = [ "anyhow", "base64 0.21.7", @@ -3209,7 +3209,7 @@ dependencies = [ [[package]] name = "goose-server" -version = "1.21.1" +version = "1.21.2" dependencies = [ "anyhow", "async-trait", @@ -3255,7 +3255,7 @@ dependencies = [ [[package]] name = "goose-test" -version = "1.21.1" +version = "1.21.2" dependencies = [ "clap", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index cf86bf09accb..070ad5fa704c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ resolver = "2" [workspace.package] edition = "2021" -version = "1.21.1" +version = "1.21.2" authors = ["Block "] license = "Apache-2.0" repository = "https://github.com/block/goose" diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index e62c5c33dbf8..48a3382526f6 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1288,7 +1288,7 @@ impl Agent { let mut combined = stream::select_all(with_id); let mut all_install_successful = true; - while let Some((request_id, item)) = combined.next().await { + loop { if is_token_cancelled(&cancel_token) { break; } @@ -1297,23 +1297,55 @@ impl Agent { yield AgentEvent::Message(msg); } - match item { - ToolStreamItem::Result(output) => { - let output = call_tool_result::validate(output); - - if enable_extension_request_ids.contains(&request_id) - && output.is_err() - { - all_install_successful = false; - } - if let Some(response_msg) = request_to_response_map.get(&request_id) { - let metadata = request_metadata.get(&request_id).and_then(|m| m.as_ref()); - let mut response = response_msg.lock().await; - *response = response.clone().with_tool_response_with_metadata(request_id, output, metadata); + tokio::select! { + biased; + + tool_item = combined.next() => { + match tool_item { + Some((request_id, item)) => { + match item { + ToolStreamItem::Result(output) => { + let output = call_tool_result::validate(output); + + if let Ok(ref call_result) = output { + if let Some(ref meta) = call_result.meta { + if let Some(notification_data) = meta.0.get("platform_notification") { + if let Some(method) = notification_data.get("method").and_then(|v| v.as_str()) { + let params = notification_data.get("params").cloned(); + let custom_notification = rmcp::model::CustomNotification::new( + method.to_string(), + params, + ); + + let server_notification = rmcp::model::ServerNotification::CustomNotification(custom_notification); + yield AgentEvent::McpNotification((request_id.clone(), server_notification)); + } + } + } + } + + if enable_extension_request_ids.contains(&request_id) + && output.is_err() + { + all_install_successful = false; + } + if let Some(response_msg) = request_to_response_map.get(&request_id) { + let metadata = request_metadata.get(&request_id).and_then(|m| m.as_ref()); + let mut response = response_msg.lock().await; + *response = response.clone().with_tool_response_with_metadata(request_id, output, metadata); + } + } + ToolStreamItem::Message(msg) => { + yield AgentEvent::McpNotification((request_id, msg)); + } + } + } + None => break, } } - ToolStreamItem::Message(msg) => { - yield AgentEvent::McpNotification((request_id, msg)); + + _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => { + // Continue loop to drain elicitation messages } } } diff --git a/crates/goose/src/providers/canonical/data/canonical_mapping_report.json b/crates/goose/src/providers/canonical/data/canonical_mapping_report.json index 66ba367e44c6..b26fc540a716 100644 --- a/crates/goose/src/providers/canonical/data/canonical_mapping_report.json +++ b/crates/goose/src/providers/canonical/data/canonical_mapping_report.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-01-22T19:03:10.825247197+00:00", + "timestamp": "2026-01-23T17:16:32.706106474+00:00", "unmapped_models": [ { "provider": "google", diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 0335faaf5b4f..37c7b23490f7 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.21.1" + "version": "1.21.2" }, "paths": { "/action-required/tool-confirmation": { diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 073f184fe01c..1e0f9162ec8d 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "goose-app", - "version": "1.21.1", + "version": "1.21.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goose-app", - "version": "1.21.1", + "version": "1.21.2", "license": "Apache-2.0", "dependencies": { "@mcp-ui/client": "^5.17.3", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index cffa19915a0e..dca5c29dca4a 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -1,7 +1,7 @@ { "name": "goose-app", "productName": "Goose", - "version": "1.21.1", + "version": "1.21.2", "description": "Goose App", "engines": { "node": "^24.0.0" diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index a91e4507b4a9..c63c1f713b77 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1282,8 +1282,9 @@ export default function ChatInput({ maxHeight: `${maxHeight}px`, overflowY: 'auto', opacity: isRecording ? 0 : 1, + paddingRight: dictationSettings?.enabled ? '180px' : '120px', }} - className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 pr-32 text-sm resize-none text-textStandard placeholder:text-textPlaceholder" + className="w-full outline-none border-none focus:ring-0 bg-transparent px-3 pt-3 pb-1.5 text-sm resize-none text-textStandard placeholder:text-textPlaceholder" /> {isRecording && (
diff --git a/ui/desktop/src/components/ElicitationRequest.tsx b/ui/desktop/src/components/ElicitationRequest.tsx index 9dcc95fef767..98deb045a4ca 100644 --- a/ui/desktop/src/components/ElicitationRequest.tsx +++ b/ui/desktop/src/components/ElicitationRequest.tsx @@ -1,8 +1,10 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { ActionRequired } from '../api'; import JsonSchemaForm from './ui/JsonSchemaForm'; import type { JsonSchema } from './ui/JsonSchemaForm'; +const ELICITATION_TIMEOUT_SECONDS = 300; + interface ElicitationRequestProps { isCancelledMessage: boolean; isClicked: boolean; @@ -10,6 +12,12 @@ interface ElicitationRequestProps { onSubmit: (elicitationId: string, userData: Record) => void; } +function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + export default function ElicitationRequest({ isCancelledMessage, isClicked, @@ -17,6 +25,24 @@ export default function ElicitationRequest({ onSubmit, }: ElicitationRequestProps) { const [submitted, setSubmitted] = useState(isClicked); + const [timeRemaining, setTimeRemaining] = useState(ELICITATION_TIMEOUT_SECONDS); + const startTimeRef = useRef(Date.now()); + + useEffect(() => { + if (submitted || isCancelledMessage || isClicked) return; + + const interval = setInterval(() => { + const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000); + const remaining = Math.max(0, ELICITATION_TIMEOUT_SECONDS - elapsed); + setTimeRemaining(remaining); + + if (remaining === 0) { + clearInterval(interval); + } + }, 1000); + + return () => clearInterval(interval); + }, [submitted, isCancelledMessage, isClicked]); if (actionRequiredContent.data.actionType !== 'elicitation') { return null; @@ -57,10 +83,39 @@ export default function ElicitationRequest({ ); } + const isUrgent = timeRemaining <= 60; + const isExpired = timeRemaining === 0; + + if (isExpired) { + return ( +
+
+ + + + This request has expired. The extension will need to ask again. +
+
+ ); + } + return (
- {message || 'Goose needs some information from you.'} +
+ {message || 'Goose needs some information from you.'} +
+
+ + + + + Waiting for your response ({formatTime(timeRemaining)} remaining) + +
);