Skip to content

Commit 738dd24

Browse files
committed
adds slash command
1 parent bb6204e commit 738dd24

File tree

10 files changed

+280
-52
lines changed

10 files changed

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

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)