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
22 changes: 22 additions & 0 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ Cargo manifests and lockfile; Research has an independent dependency graph.
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: into the running turn, or into the next one.
- External agents: a task can hand work to the Codex CLI installed on
this computer with the `agent_start`, `agent_send`, `agent_status`,
`agent_cancel`, and `list_agent_providers` tools, once Codex is enabled
under Settings > Integrations. Codex runs in the project with its own
account, context, and sandbox settings; whatever it asks approval for
comes to you through Maple's permission card, and Allow all grants it. 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
[`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
are settings.
Expand Down Expand Up @@ -143,6 +153,17 @@ account configuration that may roam between devices.
The embedded design, migration rules, privacy boundary, and preview limits are
documented in [`docs/embedded-cua.md`](docs/embedded-cua.md).

#### Codex

Settings > Integrations also lists the Codex CLI when `codex` is on the PATH
(the login shell's PATH on macOS). The card shows the installed version and
whether Codex is signed in; Maple never runs Codex's sign-in itself. The
toggle is off by default. Enabling it gives new runs the external-agent
tools and installs the `handoff`, `committee`, and `advisor` skills into the
account's Goose skills directory; disabling removes only the files Maple
wrote. Codex needs version 0.143 or newer. See
[`docs/external-agents.md`](docs/external-agents.md).

### Composer Vim preview

Turn on **Vim mode in composer** in General settings. The composer opens in
Expand Down Expand Up @@ -404,6 +425,7 @@ The roots follow the platform, the same way the Tauri app's
| `<config>/settings.json` | App settings. |
| `<config>/agent/accounts/<scope>/config.json` | Per-account agent configuration (default root, model, custom MCP servers, project trust). May roam between machines. |
| `<config>/agent/accounts/<scope>/goose/config/` | Goose permission file for the account. |
| `<config>/agent/accounts/<scope>/goose/config/skills/` | Skills the account's tasks can load, including the delegation skills Maple installs while Codex is enabled. |
| `<config>/agent/goose-runtime/` | Goose process configuration. |
| `<local data>/auth.json` | Sign-in credentials (mode 0600). Device-local; never in a roaming profile. |
| `<local data>/agent/accounts/<scope>/integrations.json` | Per-account defaults and validated launch details for integrations detected on this device. |
Expand Down
1 change: 1 addition & 0 deletions apps/maple-agent/app/assets/icons/openai-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/maple-agent/app/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ assets!(
"icons/maple-wordmark.svg",
"icons/maximize-2.svg",
"icons/minimize-2.svg",
// OpenAI mark (simple-icons, CC0) for the Codex integration card.
"icons/openai-mark.svg",
"icons/paperclip.svg",
"icons/pin.svg",
"icons/plus.svg",
Expand Down
25 changes: 22 additions & 3 deletions apps/maple-agent/app/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1874,16 +1874,35 @@ impl AgentBackend {
.await)
}

/// Interrupt an external agent (Codex) from its row. The agent keeps
/// its thread so the task can continue it later.
pub async fn cancel_external_agent(
&self,
user_id: &str,
session_id: &str,
agent_id: &str,
) -> Result<(), String> {
self.service
.handle_for_user(user_id)
.await?
.cancel_external_agent(session_id, agent_id)
.await
}

/// Slash commands (installed skills) for a working directory. Filesystem
/// scan, so it runs on a blocking thread.
pub async fn list_slash_commands(
&self,
user_id: &str,
working_dir: Option<String>,
) -> Result<Vec<AgentSlashCommand>, String> {
let service = self.service.clone();
tokio::task::spawn_blocking(move || service.list_slash_commands(working_dir.as_deref()))
.await
.map_err(|error| format!("Slash command scan failed: {error}"))
let user_id = user_id.to_string();
tokio::task::spawn_blocking(move || {
service.list_slash_commands(Some(&user_id), working_dir.as_deref())
})
.await
.map_err(|error| format!("Slash command scan failed: {error}"))
}

/// Expand `/command args` into the skill prompt; `None` when the command
Expand Down
9 changes: 7 additions & 2 deletions apps/maple-agent/app/src/ui/chat/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ use std::collections::HashMap;
use std::sync::Arc;

use gpui::{AsyncApp, SharedString, Task, WeakEntity};
use maple_agent::agent::AgentTimelineItem;
use maple_agent::agent::{AgentTimelineItem, ExternalAgentActivity};

use super::ChatScreen;
use super::transcript::{
diff_lines_for, maple_display_text, tool_input_line, tool_output_markdown,
diff_lines_for, external_agent_activity, maple_display_text, tool_input_line,
tool_output_markdown,
};
use crate::ui::markdown;

Expand Down Expand Up @@ -259,6 +260,9 @@ pub(super) struct ItemDerived {
pub(super) input_line: Option<SharedString>,
/// +/- lines of an edit or write tool, capped at `MAX_DIFF_LINES`.
pub(super) diff_lines: Arc<Vec<(char, SharedString)>>,
/// What an external agent (Codex) did, for the row of the tool call
/// that ran it.
pub(super) external_agent: Option<Arc<ExternalAgentActivity>>,
}

pub(super) const MAX_DIFF_LINES: usize = 200;
Expand All @@ -273,6 +277,7 @@ impl ItemDerived {
output_text,
input_line: tool_input_line(item).map(SharedString::from),
diff_lines: Arc::new(diff_lines_for(item)),
external_agent: external_agent_activity(item).map(Arc::new),
}
}
}
Expand Down
22 changes: 14 additions & 8 deletions apps/maple-agent/app/src/ui/chat/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,7 @@ impl ChatScreen {

/// The subagents working for this task, pinned above the composer.
/// `None` when none are working, which is the common case.
pub(super) fn render_subagents_card(&self) -> Option<Div> {
pub(super) fn render_subagents_card(&self, cx: &mut Context<Self>) -> Option<Div> {
if self.subagents.is_empty() {
return None;
}
Expand Down Expand Up @@ -735,13 +735,19 @@ impl ChatScreen {
.border_color(gpui::rgb(theme::border_subtle()))
.overflow_hidden()
.child(header)
.child(
div().flex().flex_col().gap_1().px_3().pb_2().children(
self.subagents
.iter()
.map(|subagent| render_subagent_row(subagent, now)),
),
),
.child(div().flex().flex_col().gap_1().px_3().pb_2().children(
self.subagents.iter().map(|subagent| {
let on_stop = subagent.external.as_ref().map(|external| {
let agent_id = external.agent_id.clone();
let listener =
cx.listener(move |this: &mut ChatScreen, _event, _window, cx| {
this.stop_external_agent(&agent_id, cx);
});
Box::new(listener) as super::transcript::StopHandler
});
render_subagent_row(subagent, now, on_stop)
}),
)),
)
}

Expand Down
55 changes: 52 additions & 3 deletions apps/maple-agent/app/src/ui/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ pub struct ChatScreen {
/// landed while it was in flight; a stale one would wipe a row the
/// events already know about (or revive one they removed).
subagent_epoch: u64,
/// Whether a running runtime has been seen, so the skills list is
/// re-read once Goose knows the account's skills directory.
runtime_running_seen: bool,
/// Web tools on for the selected task (mirrors the session record).
web_enabled: bool,
/// Settings default applied to newly created tasks.
Expand Down Expand Up @@ -809,11 +812,17 @@ impl ChatScreen {
.iter()
.zip(&self.slash_entries)
.any(|(next, previous)| next.name != previous.name);
// The palette just opened: re-read the skills, so one installed
// since the last scan (a toggle in Settings) is in the list.
let palette_opened = self.slash_entries.is_empty() && !entries.is_empty();
if has_text != self.composer_has_text || entries_changed {
self.composer_has_text = has_text;
self.slash_entries = entries;
cx.notify();
}
if palette_opened {
self.refresh_slash_commands(cx);
}
}

/// Test seam: pure state without composer wiring or runtime start.
Expand Down Expand Up @@ -959,6 +968,7 @@ impl ChatScreen {
plan_collapsed: false,
subagents: Vec::new(),
subagent_tick_pending: std::cell::Cell::new(false),
runtime_running_seen: false,
subagent_epoch: 0,
audio: Arc::new(crate::audio::AudioEngine::new()),
audio_caps: maple_agent::agent::AudioCapabilities::default(),
Expand Down Expand Up @@ -1941,6 +1951,7 @@ impl ChatScreen {
task: subagent.task.into(),
background: subagent.background,
activity: subagent.activity.map(SharedString::from),
external: subagent.external,
}
})
.collect();
Expand Down Expand Up @@ -2240,16 +2251,20 @@ impl ChatScreen {
/// Reload the skill slash commands for the current project root.
pub fn refresh_slash_commands(&mut self, cx: &mut Context<Self>) {
let backend = self.backend.clone();
let user_id = self.user_id.clone();
let working_dir = self.project_root.clone();
let requested_root = working_dir.clone();
self.call(
async move { backend.list_slash_commands(working_dir).await },
async move { backend.list_slash_commands(&user_id, working_dir).await },
cx,
move |this, result, cx| {
if this.project_root == requested_root
&& let Ok(commands) = result
{
this.slash_commands = commands;
if let Some(composer) = this.composer.clone() {
this.composer_changed(&composer, cx);
}
cx.notify();
}
},
Expand Down Expand Up @@ -2483,6 +2498,7 @@ impl ChatScreen {
id: String,
task: String,
background: bool,
external: Option<maple_agent::agent::ExternalAgentRef>,
cx: &mut Context<Self>,
) {
if self.subagents.iter().any(|subagent| subagent.id == id) {
Expand All @@ -2495,10 +2511,36 @@ impl ChatScreen {
background,
started: std::time::Instant::now(),
activity: None,
external,
});
self.schedule_subagent_tick(cx);
}

/// Stop an external agent from its row. The row closes when the
/// runtime reports the turn's end.
fn stop_external_agent(&mut self, agent_id: &str, cx: &mut Context<Self>) {
let Some(session_id) = self.selected_session.clone() else {
return;
};
let backend = self.backend.clone();
let user_id = self.user_id.clone();
let agent_id = agent_id.to_string();
self.call(
async move {
backend
.cancel_external_agent(&user_id, &session_id, &agent_id)
.await
},
cx,
|this, result, cx| {
if let Err(message) = result {
this.notice = Some(message.into());
}
cx.notify();
},
);
}

/// Paint once a second while a subagent works, so the elapsed time on
/// its row stays true. The last subagent to end stops the timer.
fn schedule_subagent_tick(&self, cx: &mut Context<Self>) {
Expand Down Expand Up @@ -3943,6 +3985,12 @@ impl ChatScreen {
fn apply_service_event(&mut self, event: AgentServiceEvent, cx: &mut Context<Self>) -> bool {
match event {
AgentServiceEvent::RuntimeStatus(mut status) => {
// A runtime that just started points Goose at the account's
// skills, which the first scan ran before; ask again.
if status.running && !self.runtime_running_seen {
self.runtime_running_seen = true;
self.refresh_slash_commands(cx);
}
// The status snapshot is authoritative for active runs; one
// that repeats the known state changes nothing. A snapshot
// raced with a terminal event must not resurrect that run.
Expand Down Expand Up @@ -4137,11 +4185,12 @@ impl ChatScreen {
id,
task,
background,
external,
} => {
if !self.is_selected(session_id) {
return false;
}
self.start_subagent(id, task, background, cx);
self.start_subagent(id, task, background, external, cx);
}
AgentRunEvent::SubagentActivity { id, tool } => {
if !self.is_selected(session_id) {
Expand Down Expand Up @@ -4439,7 +4488,7 @@ impl Render for ChatScreen {
.flex_col()
})
.children(self.render_btw_card(cx))
.children(self.render_subagents_card())
.children(self.render_subagents_card(cx))
.children(self.render_plan_card(cx))
.child(self.render_composer(cx))
.children(self.render_slash_palette(cx))
Expand Down
4 changes: 4 additions & 0 deletions apps/maple-agent/app/src/ui/chat/summaries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ impl ChatScreen {
Some("summaries are off in settings")
} else if has_tool_input(item, "todos") {
Some("todo list")
} else if super::transcript::has_external_agent_activity(item) {
// The row draws the agent's own activity; a one-line summary
// would hide what it did.
Some("external agent activity")
} else if item.input.as_ref().is_none_or(serde_json::Value::is_null) {
Some("no input")
} else {
Expand Down
Loading
Loading