Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "1.21.1"
version = "1.21.2"
authors = ["Block <[email protected]>"]
license = "Apache-2.0"
repository = "https://github.com/block/goose"
Expand Down
64 changes: 48 additions & 16 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"license": {
"name": "Apache-2.0"
},
"version": "1.21.1"
"version": "1.21.2"
},
"paths": {
"/action-required/tool-confirmation": {
Expand Down
4 changes: 2 additions & 2 deletions ui/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/desktop/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 2 additions & 1 deletion ui/desktop/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
<div className="absolute inset-0 flex items-center pl-4 pr-32 pt-3 pb-1.5">
Expand Down
80 changes: 78 additions & 2 deletions ui/desktop/src/components/ElicitationRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,48 @@
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;
actionRequiredContent: ActionRequired & { type: 'actionRequired' };
onSubmit: (elicitationId: string, userData: Record<string, unknown>) => 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,
actionRequiredContent,
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;
Expand Down Expand Up @@ -57,17 +83,67 @@ export default function ElicitationRequest({
);
}

const isUrgent = timeRemaining <= 60;
const isExpired = timeRemaining === 0;

if (isExpired) {
return (
<div className="goose-message-content bg-background-muted rounded-2xl px-4 py-2 text-textStandard">
<div className="flex items-center gap-2 text-textSubtle">
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>This request has expired. The extension will need to ask again.</span>
</div>
</div>
);
}

return (
<div className="flex flex-col">
<div className="goose-message-content bg-background-muted rounded-2xl rounded-b-none px-4 py-2 text-textStandard">
{message || 'Goose needs some information from you.'}
<div className="flex justify-between items-start gap-4">
<span>{message || 'Goose needs some information from you.'}</span>
</div>
</div>
<div className="goose-message-content bg-background-default border border-borderSubtle dark:border-gray-700 rounded-b-2xl px-4 py-3">
<JsonSchemaForm
schema={requested_schema as JsonSchema}
onSubmit={handleSubmit}
submitLabel="Submit"
/>
<div
className={`mt-3 pt-3 border-t border-borderSubtle flex items-center gap-2 text-sm ${isUrgent ? 'text-red-500' : 'text-textSubtle'}`}
>
<svg
className="w-4 h-4 animate-pulse"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>
Waiting for your response ({formatTime(timeRemaining)} remaining)
</span>
</div>
</div>
</div>
);
Expand Down
Loading