Skip to content

Commit 2ae2a12

Browse files
committed
adds slash command
1 parent bb6204e commit 2ae2a12

File tree

10 files changed

+277
-52
lines changed

10 files changed

+277
-52
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use std::collections::HashMap;
2+
use std::io::Write;
3+
use std::path::PathBuf;
4+
5+
use eyre::Result;
6+
use serde::{
7+
Deserialize,
8+
Serialize,
9+
};
10+
11+
use crate::cli::DEFAULT_AGENT_NAME;
12+
use crate::cli::chat::tools::delegate::agent::subagents_dir;
13+
use crate::cli::chat::tools::delegate::{
14+
AgentExecution,
15+
AgentStatus,
16+
launch_agent,
17+
};
18+
use crate::cli::chat::{
19+
ChatError,
20+
ChatSession,
21+
ChatState,
22+
};
23+
use crate::database::settings::Setting;
24+
use crate::os::Os;
25+
26+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
27+
pub struct SubagentHeader {
28+
pub launched_at: String,
29+
pub agent: Option<String>,
30+
pub prompt: String,
31+
pub status: String, // "active", "completed", "failed"
32+
pub pid: u32,
33+
pub completed_at: Option<String>,
34+
}
35+
36+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
37+
pub struct SubagentContent {
38+
pub output: String,
39+
pub exit_code: Option<i32>,
40+
}
41+
42+
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
43+
pub struct StatusFile {
44+
pub subagents: HashMap<String, SubagentHeader>,
45+
pub last_updated: String,
46+
}
47+
48+
#[derive(Debug, PartialEq, clap::Subcommand)]
49+
pub enum DelegateArgs {
50+
/// Show status of tasks
51+
Status {
52+
/// Specific task agent name (optional)
53+
agent_name: Option<String>,
54+
},
55+
/// Read output from a task
56+
Read {
57+
/// Task agent name
58+
agent_name: String,
59+
},
60+
/// Delete a task and its files
61+
Delete {
62+
/// Task agent name
63+
agent_name: String,
64+
},
65+
/// Launch a new task
66+
Launch {
67+
/// Agent to use for the task
68+
#[arg(long)]
69+
agent: Option<String>,
70+
/// Task description
71+
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
72+
prompt: Vec<String>,
73+
},
74+
}
75+
76+
impl DelegateArgs {
77+
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
78+
if !is_enabled(os) {
79+
return Err(ChatError::Custom(
80+
"Delegate feature is not enabled. Enable it with /experiment command.".into(),
81+
));
82+
}
83+
84+
let executions = gather_executions(os)
85+
.await
86+
.map_err(|e| ChatError::Custom(e.to_string().into()))?;
87+
88+
let result = match self {
89+
DelegateArgs::Status { agent_name } => {
90+
show_status(
91+
agent_name.as_deref(),
92+
&executions.iter().map(|(e, _)| e).collect::<Vec<_>>(),
93+
)
94+
.await
95+
},
96+
DelegateArgs::Read { agent_name } => {
97+
let (execution, path) = executions
98+
.iter()
99+
.find(|(e, _)| e.agent.as_str() == agent_name)
100+
.ok_or(ChatError::Custom("No task found".into()))?;
101+
102+
let execution_as_str =
103+
serde_json::to_string(&execution).map_err(|e| ChatError::Custom(e.to_string().into()))?;
104+
105+
_ = os.fs.remove_file(path).await;
106+
107+
return Ok(ChatState::HandleInput {
108+
input: format!(
109+
"Delegate task with agent {} has concluded with the following content: {}",
110+
&execution.agent, execution_as_str,
111+
),
112+
});
113+
},
114+
DelegateArgs::Delete { agent_name } => {
115+
let (_, path) = executions
116+
.iter()
117+
.find(|(e, _)| e.agent.as_str() == agent_name)
118+
.ok_or(ChatError::Custom("No task found".into()))?;
119+
os.fs.remove_file(path).await?;
120+
121+
Ok(format!("Task with agent {agent_name} has been deleted"))
122+
},
123+
DelegateArgs::Launch { agent, prompt } => {
124+
let prompt_str = prompt.join(" ");
125+
if prompt_str.trim().is_empty() {
126+
return Err(ChatError::Custom("Please provide a prompt for the task".into()));
127+
}
128+
129+
launch_agent(
130+
os,
131+
agent.as_deref().unwrap_or(DEFAULT_AGENT_NAME),
132+
&session.conversation.agents,
133+
&prompt_str,
134+
)
135+
.await
136+
},
137+
};
138+
139+
match result {
140+
Ok(output) => {
141+
crossterm::queue!(session.stderr, crossterm::style::Print(format!("{}\n", output)))?;
142+
},
143+
Err(e) => {
144+
crossterm::queue!(session.stderr, crossterm::style::Print(format!("Error: {}\n", e)))?;
145+
},
146+
}
147+
148+
session.stderr.flush()?;
149+
150+
Ok(ChatState::PromptUser {
151+
skip_printing_tools: false,
152+
})
153+
}
154+
}
155+
156+
fn is_enabled(os: &Os) -> bool {
157+
os.database.settings.get_bool(Setting::EnabledDelegate).unwrap_or(false)
158+
}
159+
160+
async fn gather_executions(os: &Os) -> Result<Vec<(AgentExecution, PathBuf)>> {
161+
let mut dir_walker = os.fs.read_dir(subagents_dir(os).await?).await?;
162+
let mut executions = Vec::<(AgentExecution, PathBuf)>::new();
163+
164+
while let Ok(Some(file)) = dir_walker.next_entry().await {
165+
let bytes = os.fs.read(file.path()).await?;
166+
let execution = serde_json::from_slice::<AgentExecution>(&bytes)?;
167+
168+
executions.push((execution, file.path()));
169+
}
170+
171+
Ok(executions)
172+
}
173+
174+
async fn show_status(agent_name: Option<&str>, executions: &[&AgentExecution]) -> Result<String> {
175+
if let Some(agent_name) = agent_name {
176+
let execution = executions
177+
.iter()
178+
.find(|e| e.agent.as_str() == agent_name)
179+
.ok_or(eyre::eyre!("Execution not found"))?;
180+
181+
Ok(format!(
182+
"📦 Subagent Status: {}\n🤖 agent: {}\n📋 Task: {}\n⏰ Launched: {}",
183+
execution.status, execution.agent, execution.task, execution.launched_at
184+
))
185+
} else {
186+
let mut active_count = 0;
187+
let mut completed_count = 0;
188+
let mut failed_count = 0;
189+
190+
for execution in executions {
191+
match execution.status {
192+
AgentStatus::Running => active_count += 1,
193+
AgentStatus::Completed => completed_count += 1,
194+
AgentStatus::Failed => failed_count += 1,
195+
}
196+
}
197+
198+
Ok(format!(
199+
"📊 Subagent Summary:\n🟢 Active: {}\n✅ Completed: {}\n❌ Failed: {}\n📈 Total: {}",
200+
active_count,
201+
completed_count,
202+
failed_count,
203+
executions.len()
204+
))
205+
}
206+
}

crates/chat-cli/src/cli/chat/cli/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod checkpoint;
33
pub mod clear;
44
pub mod compact;
55
pub mod context;
6+
pub mod delegate;
67
pub mod editor;
78
pub mod experiment;
89
pub mod hooks;
@@ -25,6 +26,7 @@ use clap::Parser;
2526
use clear::ClearArgs;
2627
use compact::CompactArgs;
2728
use context::ContextSubcommand;
29+
use delegate::DelegateArgs;
2830
use editor::EditorArgs;
2931
use experiment::ExperimentArgs;
3032
use hooks::HooksArgs;
@@ -122,6 +124,9 @@ pub enum SlashCommand {
122124
/// View, manage, and resume to-do lists
123125
#[command(subcommand)]
124126
Todos(TodoSubcommand),
127+
/// Launch and manage asynchronous subagent processes
128+
#[command(subcommand, hide = true)]
129+
Delegate(DelegateArgs),
125130
}
126131

127132
impl SlashCommand {
@@ -190,6 +195,7 @@ impl SlashCommand {
190195
// },
191196
Self::Checkpoint(subcommand) => subcommand.execute(os, session).await,
192197
Self::Todos(subcommand) => subcommand.execute(os, session).await,
198+
Self::Delegate(args) => args.execute(os, session).await,
193199
}
194200
}
195201

@@ -222,6 +228,7 @@ impl SlashCommand {
222228
},
223229
Self::Checkpoint(_) => "checkpoint",
224230
Self::Todos(_) => "todos",
231+
Self::Delegate(_) => "delegate",
225232
}
226233
}
227234

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ pub const EXTRA_HELP: &str = color_print::cstr! {"
200200
<black!>Change the keybind using: q settings chat.skimCommandKey x</black!>
201201
<em>Ctrl(^) + t</em> <black!>Toggle tangent mode for isolated conversations</black!>
202202
<black!>Change the keybind using: q settings chat.tangentModeKey x</black!>
203+
<em>Ctrl(^) + d</em> <black!>Start delegate command for task delegation</black!>
204+
<black!>Change the keybind using: q settings chat.delegateModeKey x</black!>
203205
<em>chat.editMode</em> <black!>The prompt editing mode (vim or emacs)</black!>
204206
<black!>Change using: q settings chat.skimCommandKey x</black!>
205207
"};

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,17 @@ pub fn rl(
502502
}
503503
}
504504

505+
// Add custom keybinding for Ctrl+D to open delegate command (configurable)
506+
let delegate_key_char = match os.database.settings.get_string(Setting::DelegateModeKey) {
507+
Some(key) if key.len() == 1 => key.chars().next().unwrap_or('d'),
508+
_ => 'd', // Default to 'd' if setting is missing or invalid
509+
};
510+
511+
rl.bind_sequence(
512+
KeyEvent(KeyCode::Char(delegate_key_char), Modifiers::CTRL),
513+
EventHandler::Simple(Cmd::Insert(1, "/delegate ".to_string())),
514+
);
515+
505516
// Add custom keybinding for Alt+Enter to insert a newline
506517
rl.bind_sequence(
507518
KeyEvent(KeyCode::Enter, Modifiers::ALT),

crates/chat-cli/src/cli/chat/tools/delegate/agent.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@ use eyre::Result;
44
use serde_json;
55

66
use crate::cli::agent::Agents;
7-
use crate::cli::chat::tools::delegate::types::{
8-
AgentConfig,
9-
AgentExecution,
10-
AgentExecution,
11-
};
7+
use crate::cli::chat::tools::delegate::types::AgentExecution;
128
use crate::cli::chat::tools::delegate::ui::{
139
display_agent_info,
14-
display_agent_info,
15-
get_user_confirmation,
1610
get_user_confirmation,
1711
};
1812
use crate::os::Os;
13+
use crate::util::directories::home_dir;
1914

2015
pub async fn validate_agent_availability(_os: &Os, _agent: &str) -> Result<()> {
2116
// For now, accept any agent name (no need to print here, will show in approval)
@@ -35,7 +30,6 @@ pub async fn request_user_approval(agent: &str, agents: &Agents, task: &str) ->
3530
}
3631

3732
pub async fn load_agent_execution(os: &Os, agent: &str) -> Result<Option<(AgentExecution, PathBuf)>> {
38-
tracing::info!("## delegate: running load_agent_execution for {agent}");
3933
let file_path = agent_file_path(os, agent).await?;
4034

4135
if file_path.exists() {

crates/chat-cli/src/cli/chat/tools/delegate/execution.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use eyre::{
55
Result,
66
bail,
77
};
8-
use tracing::info;
98

109
use super::agent::subagents_dir;
1110
use crate::cli::chat::tools::delegate::agent::{
@@ -109,10 +108,7 @@ pub async fn status_agent(os: &Os, agent: &str) -> Result<String> {
109108
save_agent_execution(os, &execution).await?;
110109
}
111110

112-
info!("## delegate: checking status for {}", path.display());
113-
114111
if execution.status == AgentStatus::Completed {
115-
info!("## delegate: attempting to delete {}", path.display());
116112
let _ = os.fs.remove_file(path).await;
117113
}
118114

@@ -130,16 +126,22 @@ pub async fn status_all_agents(os: &Os) -> Result<String> {
130126

131127
while let Ok(Some(file)) = dir_walker.next_entry().await {
132128
let file_name = file.file_name();
133-
let file_name = file_name
134-
.as_os_str()
135-
.to_str()
136-
.ok_or(eyre::eyre!("Error obtaining execution file name"))?;
137129

138-
if !status.is_empty() {
139-
status.push_str(", ");
140-
}
130+
let bytes = os.fs.read(file.path()).await?;
131+
let execution = serde_json::from_slice::<AgentExecution>(&bytes)?;
132+
133+
if execution.status != AgentStatus::Running {
134+
let file_name = file_name
135+
.as_os_str()
136+
.to_str()
137+
.ok_or(eyre::eyre!("Error obtaining execution file name"))?;
141138

142-
status.push_str(file_name);
139+
if !status.is_empty() {
140+
status.push_str(", ");
141+
}
142+
143+
status.push_str(file_name);
144+
}
143145
}
144146

145147
if status.is_empty() {

crates/chat-cli/src/cli/chat/tools/delegate/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mod agent;
1+
pub mod agent;
22
pub mod execution;
33
mod types;
44
mod ui;
@@ -91,8 +91,6 @@ impl Delegate {
9191
});
9292
}
9393

94-
tracing::info!("## delegate: invoking");
95-
9694
let result = match &self.operation {
9795
Operation::Launch => {
9896
let task = self
@@ -136,7 +134,7 @@ impl Delegate {
136134
}
137135
}
138136

139-
async fn launch_agent(os: &Os, agent: &str, agents: &Agents, task: &str) -> Result<String> {
137+
pub async fn launch_agent(os: &Os, agent: &str, agents: &Agents, task: &str) -> Result<String> {
140138
validate_agent_availability(os, agent).await?;
141139

142140
// Check if agent is already running

0 commit comments

Comments
 (0)