Skip to content
Closed
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
20 changes: 8 additions & 12 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ remove Tauri:
- public visibility opened on the service surface the app consumes

Goose is pinned to a fork revision based on upstream v1.53.0 in this
component’s Cargo manifests and lockfile. The fork forwards subagent approvals
to the parent and supports ephemeral native clients for embedded CUA. Research
has an independent dependency graph.
component’s Cargo manifests and lockfile. The fork supports ephemeral native
clients for embedded CUA. Research has an independent dependency graph.

## Features

Expand All @@ -69,21 +68,18 @@ has an independent dependency graph.
never sees, plus `/compact`, `/new`, `/pin`, `/web`, `/model`, and
`/help`. The account's skills appear in the same list.
- The task's latest todo list stays pinned above the composer.
- Subagents: the task can give a piece of work to a subagent with the
`delegate` tool, which runs it in its own context. Subagents show above
the composer with the tool each one runs and how long it has worked. A subagent that runs in the background
keeps its row after the turn ends, and Maple tells the task when it
finishes, with a bounded result in the running turn or a new turn Maple
starts automatically. The task can use `load` to retrieve any truncated output.
- External agents: a task can hand work to Codex or Claude Code installed on
this computer with the `agent_start`, `agent_send`, `agent_status`,
`agent_cancel`, and `list_agent_providers` tools, once the provider is enabled
under Settings > Integrations. Each agent runs in the project with its own
account, context, and sandbox settings; Maple accepts its approval
requests, and its questions still come to you through Maple's question
card. Its progress streams into the tool call's row and its row above the
composer has a Stop button. Three skills, `/handoff`, `/committee`, and `/advisor`, teach the
task when and how to delegate. See
card. Its progress streams into the tool call's row, and its row above
the composer shows how long it has worked, with a Stop button. A
background agent keeps its row after the turn ends, and Maple tells the
task when it finishes, with a bounded result in the running turn or a new
turn Maple starts automatically. Three skills, `/handoff`, `/committee`,
and `/advisor`, teach the task when and how to delegate. See
[`docs/external-agents.md`](docs/external-agents.md).
- Voice: dictate a message with the microphone button, and read any
message aloud. Both use Maple's speech models; the voice and speed
Expand Down
2 changes: 1 addition & 1 deletion apps/maple-agent/app/src/ui/chat/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ impl ChatScreen {
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(gpui::rgb(theme::text_primary()))
.child("Subagents"),
.child("External agents"),
)
.child(
div()
Expand Down
4 changes: 2 additions & 2 deletions apps/maple-agent/app/src/ui/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,8 @@ pub struct ChatScreen {
btw_sequence: u64,
/// The pinned plan card shows only its header.
plan_collapsed: bool,
/// Subagents working for the selected task, pinned above the
/// composer. Empty unless a `delegate` call is in flight.
/// External agents working for the selected task, pinned above the
/// composer. Empty unless an external agent is working.
subagents: Vec<ActiveSubagent>,
/// A repaint that keeps the subagent elapsed times moving is already
/// on its way; one at a time is enough.
Expand Down
4 changes: 1 addition & 3 deletions apps/maple-agent/app/src/ui/chat/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ pub(super) struct PlanEntry {
/// One subagent working for the selected task.
#[derive(Clone, Debug)]
pub(super) struct ActiveSubagent {
/// Request ID of the `delegate` call that started it.
/// Row ID of the agent, shared by every event about it.
pub(super) id: String,
/// What the subagent was asked to do.
pub(super) task: SharedString,
Expand Down Expand Up @@ -902,8 +902,6 @@ fn render_tool_with_diff(
pub(super) fn tool_label_title(title: &str) -> &str {
const LABELS: &[&str] = &[
"Terminal",
"Subagent",
"Load",
"Editor",
"Web Search",
"Read file",
Expand Down
18 changes: 0 additions & 18 deletions apps/maple-agent/crates/maple-agent/src/acp/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,24 +349,6 @@ pub(super) fn acp_tool_update(
}
}

/// Live subagent progress as an update to the `delegate` tool call that
/// owns it.
///
/// ACP has no concept of a subagent, so the tool call is where its work
/// belongs. The content of a tool call is replaced, not appended, so this
/// carries the latest line only, like the desktop card. The final content
/// is the tool's own result, which arrives with the call's completion.
pub(super) fn subagent_tool_update(delegate_id: &str, line: String) -> SessionUpdate {
SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
delegate_id.to_string(),
ToolCallUpdateFields::new()
.status(ToolCallStatus::InProgress)
.content(vec![ToolCallContent::from(ContentBlock::Text(
TextContent::new(line),
))]),
))
}

pub(super) fn timeline_tool_kind(item: &AgentTimelineItem) -> ToolKind {
let title = item
.title
Expand Down
52 changes: 5 additions & 47 deletions apps/maple-agent/crates/maple-agent/src/acp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use convert::{
acp_config_options, acp_session_config_options, acp_usage, bounded_chars, event_error_text,
internal_acp_error, outbound_error, parse_slash_command, project_trust_elicitation_request,
project_trust_permission_decision, project_trust_permission_options, prompt_images,
prompt_result_from_terminal, prompt_text, subagent_tool_update, timeline_update,
prompt_result_from_terminal, prompt_text, timeline_update,
};
use handler::{AcpCallerSessionFields, MapleAcpHandler};
use session::{
Expand Down Expand Up @@ -1582,34 +1582,11 @@ impl AcpConnectionContext {
Err(AcpOutboundSendError::Transport(error)) => break Err(error),
}
}
// A subagent has no place of its own in ACP, so its work
// rides the `delegate` tool call that owns it.
// External agents report through the host's event sink, not
// the run stream, so nothing arrives here for them.
Some(
AgentRunEvent::SubagentStarted { id, task, .. }
| AgentRunEvent::SubagentActivity { id, tool: task },
) => {
match self
.send_session_update(
cx,
SessionNotification::new(
protocol_session_id.clone(),
subagent_tool_update(&id, task),
),
&prompt_lifetime,
)
.await
{
Ok(()) | Err(AcpOutboundSendError::UpdateTooLarge) => {}
Err(AcpOutboundSendError::Cancelled) => {
cancel_after_result = true;
break Ok(PromptResponse::new(StopReason::Cancelled));
}
Err(AcpOutboundSendError::Transport(error)) => break Err(error),
}
}
// The end of a subagent is the end of its tool call, which
// carries the result. Replacing that content with a notice
// would drop what the caller came for.
AgentRunEvent::SubagentStarted { .. } | AgentRunEvent::SubagentActivity { .. },
) => {}
// A semantic title landing mid-run renames the task; keep
// the caller's session list in sync.
Some(AgentRunEvent::SessionUpdated(summary)) => {
Expand Down Expand Up @@ -2490,25 +2467,6 @@ mod tests {
assert_eq!(encoded["rawInput"]["command"], "pwd");
}

/// ACP has no subagent of its own, so live subagent work rides the
/// `delegate` tool call, the way Goose's own ACP server reports it.
#[test]
fn subagent_progress_updates_the_delegate_tool_call() {
let encoded = serde_json::to_value(subagent_tool_update(
"delegate-1",
"Terminal: cargo test".to_string(),
))
.unwrap();

assert_eq!(encoded["sessionUpdate"], "tool_call_update");
assert_eq!(encoded["toolCallId"], "delegate-1");
assert_eq!(encoded["status"], "in_progress");
assert_eq!(
encoded["content"][0]["content"]["text"],
"Terminal: cargo test"
);
}

#[test]
fn slash_commands_parse_like_the_desktop_composer() {
assert_eq!(
Expand Down
Loading
Loading