Skip to content
Open
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
111 changes: 95 additions & 16 deletions crates/goose/src/acp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,24 @@
};
use sacp::schema::{
AgentCapabilities, Annotations, AuthMethod, AuthMethodAgent, AuthenticateRequest,
AuthenticateResponse, BlobResourceContents, CancelNotification, CloseSessionRequest,
CloseSessionResponse, ConfigOptionUpdate, Content, ContentBlock, ContentChunk,
CurrentModeUpdate, EmbeddedResource, EmbeddedResourceResource, FileSystemCapabilities,
ForkSessionRequest, ForkSessionResponse, ImageContent, InitializeRequest, InitializeResponse,
ListSessionsRequest, ListSessionsResponse, LoadSessionRequest, LoadSessionResponse,
McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest, NewSessionResponse,
PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse,
RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionCapabilities,
SessionCloseCapabilities, SessionConfigOption, SessionConfigOptionCategory,
SessionConfigSelectOption, SessionId, SessionInfo, SessionListCapabilities, SessionMode,
SessionModeId, SessionModeState, SessionModelState, SessionNotification, SessionUpdate,
SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, StopReason,
TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId, ToolCallLocation,
ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, Usage, UsageUpdate,
AuthenticateResponse, AvailableCommand, AvailableCommandInput, AvailableCommandsUpdate,
BlobResourceContents, CancelNotification, CloseSessionRequest, CloseSessionResponse,
ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource,
EmbeddedResourceResource, FileSystemCapabilities, ForkSessionRequest, ForkSessionResponse,
ImageContent, InitializeRequest, InitializeResponse, ListSessionsRequest, ListSessionsResponse,
LoadSessionRequest, LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo,
NewSessionRequest, NewSessionResponse, PermissionOption, PermissionOptionKind,
PromptCapabilities, PromptRequest, PromptResponse, RequestPermissionOutcome,
RequestPermissionRequest, ResourceLink, SessionCapabilities, SessionCloseCapabilities,
SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption, SessionId,
SessionInfo, SessionListCapabilities, SessionMode, SessionModeId, SessionModeState,
SessionModelState, SessionNotification, SessionUpdate, SetSessionConfigOptionRequest,
SetSessionConfigOptionResponse, SetSessionModeRequest, SetSessionModeResponse,
SetSessionModelRequest, SetSessionModelResponse, StopReason, TextContent, TextResourceContents,
ToolCall, ToolCallContent, ToolCallId, ToolCallLocation, ToolCallStatus, ToolCallUpdate,
ToolCallUpdateFields, ToolKind, UnstructuredCommandInput, Usage, UsageUpdate,
};

use sacp::util::MatchDispatchFrom;
use sacp::{
Agent as SacpAgent, ByteStreams, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled,
Expand Down Expand Up @@ -1563,6 +1565,59 @@

Ok(())
}

fn command_input_hint(recipe: &crate::recipe::Recipe) -> Option<String> {
let params = recipe.parameters.as_ref()?;
params
.iter()
.find(|p| p.key == "args")
.or_else(|| params.iter().find(|p| p.default.is_none()))
.or_else(|| params.first())
.map(|p| p.description.clone())
}

fn build_available_commands_from_slash_commands() -> Vec<AvailableCommand> {
crate::slash_commands::list_commands()
.into_iter()
.filter_map(|mapping| {
let recipe_path = std::path::PathBuf::from(&mapping.recipe_path);

if !recipe_path.exists() {
return None;
}

let recipe_content = std::fs::read_to_string(&recipe_path).ok()?;
let recipe = crate::recipe::Recipe::from_content(&recipe_content).ok()?;

let mut command =
AvailableCommand::new(mapping.command, recipe.description.clone());

if let Some(hint) = Self::command_input_hint(&recipe) {
command = command.input(AvailableCommandInput::Unstructured(
UnstructuredCommandInput::new(hint),
));
}

Some(command)
})
.collect()
}

fn send_available_commands_update(
&self,
cx: &ConnectionTo<Client>,
session_id: &SessionId,
) -> Result<(), sacp::Error> {
let commands = Self::build_available_commands_from_slash_commands();

cx.send_notification(SessionNotification::new(
session_id.clone(),
SessionUpdate::AvailableCommandsUpdate(AvailableCommandsUpdate::new(commands)),
))?;

Ok(())
}

}

fn outcome_to_confirmation(outcome: &RequestPermissionOutcome) -> PermissionConfirmation {
Expand Down Expand Up @@ -1764,10 +1819,13 @@
}
if let Some(usage_update) = initial_usage_update {
cx.send_notification(SessionNotification::new(
session_id,
session_id.clone(),
SessionUpdate::UsageUpdate(usage_update),
))?;
}

self.send_available_commands_update(cx, &session_id)?;

debug!(
target: "perf",
sid = %sid,
Expand All @@ -1780,7 +1838,7 @@
/// Create a new internal goose Session linked to a thread.
/// This is the agent's working state — invisible to ACP clients.
async fn create_internal_session(
&self,

Check warning on line 1841 in crates/goose/src/acp/server.rs

View workflow job for this annotation

GitHub Actions / Check Rust Code Format

Diff in /home/runner/work/goose/goose/crates/goose/src/acp/server.rs
thread_id: &str,
cwd: std::path::PathBuf,
provider_name: Option<&str>,
Expand Down Expand Up @@ -2175,6 +2233,9 @@
SessionUpdate::UsageUpdate(usage_update),
))?;
}

self.send_available_commands_update(cx, &args.session_id)?;

debug!(
target: "perf",
sid = %sid,
Expand Down Expand Up @@ -2218,6 +2279,24 @@

let user_message = Self::convert_acp_prompt_to_message(&args.prompt);

let message_text = user_message.as_concat_text();

if let Some(command) = message_text
.trim()
.split_whitespace()
.next()
.filter(|cmd| cmd.starts_with('/'))
Comment on lines +2286 to +2288
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse command parser when emitting recipe-run feedback

This slash-command detection path uses split_whitespace() to extract the command token, but actual command execution in agents/execute_commands.rs splits only on a literal space (split_once(' ')). As a result, prompts like /recipe\targ or /recipe\narg will emit Running recipe: /recipe here even though the command dispatcher won’t resolve that recipe, so ACP clients get false execution feedback for commands that never run.

Useful? React with 👍 / 👎.

{
if crate::slash_commands::get_recipe_for_command(command).is_some() {
cx.send_notification(SessionNotification::new(
args.session_id.clone(),
SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::Text(
TextContent::new(format!("Running recipe: {}", command)),
))),
))?;
}
}

// Persist user message (may contain assistant-only annotated blocks)
self.thread_manager
.append_message(&thread_id, Some(&internal_session_id), &user_message)
Expand Down Expand Up @@ -2445,7 +2524,7 @@
let model_state = build_model_state(current_model.as_str(), &inventory);
let mode_state = build_mode_state(goose_mode)?;
let provider_options = build_provider_options(Some(&provider_name)).await;
let config_options = build_config_options(

Check failure on line 2527 in crates/goose/src/acp/server.rs

View workflow job for this annotation

GitHub Actions / Lint Rust Code

found call to `str::trim` before `str::split_whitespace`
&mode_state,
&model_state,
session_provider_selection(&session),
Expand Down
Loading