Skip to content

Commit b364bf0

Browse files
authored
Upgrade delegate with better UX for notifications and change the file dependence. (#3337)
1 parent 846669f commit b364bf0

File tree

7 files changed

+261
-58
lines changed

7 files changed

+261
-58
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,10 @@ impl ConversationState {
379379
}
380380

381381
pub async fn set_next_user_message(&mut self, input: String) {
382+
self.set_next_user_message_with_context(input, String::new()).await;
383+
}
384+
385+
pub async fn set_next_user_message_with_context(&mut self, input: String, additional_context: String) {
382386
debug_assert!(self.next_message.is_none(), "next_message should not exist");
383387
if let Some(next_message) = self.next_message.as_ref() {
384388
warn!(?next_message, "next_message should not exist");
@@ -391,7 +395,8 @@ impl ConversationState {
391395
input
392396
};
393397

394-
let msg = UserMessage::new_prompt(input, Some(Local::now().fixed_offset()));
398+
let mut msg = UserMessage::new_prompt(input, Some(Local::now().fixed_offset()));
399+
msg.additional_context = additional_context;
395400
self.next_message = Some(msg);
396401
}
397402

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

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ use tool_manager::{
127127
ToolManager,
128128
ToolManagerBuilder,
129129
};
130-
use tools::delegate::status_all_agents;
130+
use tools::delegate::{
131+
AgentExecution,
132+
save_agent_execution,
133+
status_all_agents,
134+
};
131135
use tools::gh_issue::GhIssueContext;
132136
use tools::{
133137
NATIVE_TOOLS,
@@ -465,12 +469,71 @@ fn trust_all_text() -> String {
465469
ui_text::trust_all_warning()
466470
}
467471

472+
fn format_rich_notification(executions: &[AgentExecution]) -> String {
473+
let count = executions.len();
474+
let header = if count == 1 {
475+
"1 Background Task Completed".to_string()
476+
} else {
477+
format!("{} Background Tasks Completed", count)
478+
};
479+
480+
// Plain text notification - will be colored by highlight_prompt
481+
let mut notification = format!("{}\n\n", header);
482+
483+
for (i, execution) in executions.iter().enumerate() {
484+
let status_icon = match execution.status {
485+
tools::delegate::AgentStatus::Completed => "✓ SUCCESS",
486+
tools::delegate::AgentStatus::Failed => "✗ FAILED",
487+
tools::delegate::AgentStatus::Running => "⏳ RUNNING", // shouldn't happen but just in case
488+
};
489+
490+
let time_ago = if let Some(completed_at) = execution.completed_at {
491+
let duration = chrono::Utc::now().signed_duration_since(completed_at);
492+
if duration.num_minutes() < 1 {
493+
"Completed just now".to_string()
494+
} else if duration.num_minutes() < 60 {
495+
format!("Completed {} min ago", duration.num_minutes())
496+
} else if duration.num_hours() < 24 {
497+
format!("Completed {} hr ago", duration.num_hours())
498+
} else {
499+
format!("Completed {} days ago", duration.num_days())
500+
}
501+
} else {
502+
"unknown".to_string()
503+
};
504+
505+
// Shorten CWD path - replace home directory with ~
506+
let shortened_cwd = if let Ok(home) = std::env::var("HOME") {
507+
execution.cwd.replace(&home, "~")
508+
} else {
509+
execution.cwd.clone()
510+
};
511+
512+
let summary = execution.summary.as_deref().unwrap_or("No summary available");
513+
514+
notification.push_str(&format!(
515+
"[{}] {} · {} · {} · {}\n\nTask: {}\n\n{}\n\n",
516+
i + 1,
517+
execution.agent,
518+
shortened_cwd,
519+
status_icon,
520+
time_ago,
521+
execution.task,
522+
summary
523+
));
524+
}
525+
526+
// Add footer with instructions
527+
notification.push_str("To read the full details of any task, ask the delegate tool.\n");
528+
529+
notification
530+
}
531+
468532
const TOOL_BULLET: &str = " ● ";
469533
const CONTINUATION_LINE: &str = " ⋮ ";
470534
const PURPOSE_ARROW: &str = " ↳ ";
471535
const SUCCESS_TICK: &str = " ✓ ";
472536
const ERROR_EXCLAMATION: &str = " ❗ ";
473-
const DELEGATE_NOTIFIER: &str = "[BACKGROUND TASK READY]";
474537

475538
/// Enum used to denote the origin of a tool use event
476539
enum ToolUseStatus {
@@ -611,6 +674,8 @@ pub struct ChatSession {
611674
ctrlc_rx: broadcast::Receiver<()>,
612675
wrap: Option<WrapMode>,
613676
prompt_ack_rx: std::sync::mpsc::Receiver<()>,
677+
/// Additional context to be added to the next user message (e.g., delegate task summaries)
678+
pending_additional_context: Option<String>,
614679
}
615680

616681
impl ChatSession {
@@ -747,6 +812,7 @@ impl ChatSession {
747812
ctrlc_rx,
748813
wrap,
749814
prompt_ack_rx,
815+
pending_additional_context: None,
750816
})
751817
}
752818

@@ -2219,7 +2285,11 @@ impl ChatSession {
22192285
};
22202286
self.conversation.abandon_tool_use(&self.tool_uses, user_input);
22212287
} else {
2222-
self.conversation.set_next_user_message(user_input).await;
2288+
// Add additional context if available (e.g., delegate summaries)
2289+
let context = self.pending_additional_context.take().unwrap_or_default();
2290+
self.conversation
2291+
.set_next_user_message_with_context(user_input, context)
2292+
.await;
22232293
}
22242294

22252295
self.reset_user_turn();
@@ -3486,8 +3556,24 @@ impl ChatSession {
34863556
let mut generated_prompt =
34873557
prompt::generate_prompt(profile.as_deref(), all_trusted, tangent_mode, usage_percentage);
34883558

3489-
if ExperimentManager::is_enabled(os, ExperimentName::Delegate) && status_all_agents(os).await.is_ok() {
3490-
generated_prompt = format!("{DELEGATE_NOTIFIER}\n{generated_prompt}");
3559+
if ExperimentManager::is_enabled(os, ExperimentName::Delegate) {
3560+
if let Ok(mut executions) = status_all_agents(os).await {
3561+
if !executions.is_empty() {
3562+
let rich_notification = format_rich_notification(&executions);
3563+
generated_prompt = format!("{}\n{}", rich_notification, generated_prompt);
3564+
3565+
// Use the notification text as context for the model (it's already plain text)
3566+
self.pending_additional_context = Some(rich_notification.clone());
3567+
3568+
// Mark all shown tasks as user_notified
3569+
for execution in &mut executions {
3570+
execution.user_notified = true;
3571+
if let Err(e) = save_agent_execution(os, execution).await {
3572+
eprintln!("Failed to mark agent execution as notified: {}", e);
3573+
}
3574+
}
3575+
}
3576+
}
34913577
}
34923578

34933579
generated_prompt

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,14 @@ impl Highlighter for ChatHelper {
459459
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&'s self, prompt: &'p str, _default: bool) -> Cow<'b, str> {
460460
use crate::theme::StyledText;
461461

462-
// Parse the plain text prompt to extract profile and warning information
463-
// and apply colors using crossterm's ANSI escape codes
462+
// Parse the plain text prompt to extract components
464463
if let Some(components) = parse_prompt_components(prompt) {
465464
let mut result = String::new();
466465

467-
// Add notifier part if present (info blue)
466+
// Add delegate notifier if present (colored as warning)
468467
if let Some(notifier) = components.delegate_notifier {
469-
result.push_str(&StyledText::info(&format!("[{}]\n", notifier)));
468+
result.push_str(&StyledText::warning(&notifier));
469+
result.push('\n');
470470
}
471471

472472
// Add profile part if present (profile indicator cyan)

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@ pub fn parse_prompt_components(prompt: &str) -> Option<PromptComponents> {
1818
let mut warning = false;
1919
let mut tangent_mode = false;
2020
let mut usage_percentage = None;
21-
let mut remaining = prompt.trim();
2221

23-
// Check for delegate notifier first
24-
if let Some(start) = remaining.find('[') {
25-
if let Some(end) = remaining.find(']') {
26-
if start < end {
27-
let content = &remaining[start + 1..end];
28-
// Only set profile if it's not "BACKGROUND TASK READY" or if it doesn't end with newline
29-
if content == "BACKGROUND TASK READY" && remaining[end + 1..].starts_with('\n') {
30-
delegate_notifier = Some(content.to_string());
31-
remaining = remaining[end + 1..].trim_start();
32-
}
33-
}
22+
// Check if multi-line prompt (e.g., with rich notification)
23+
// Everything before the last line is treated as delegate_notifier
24+
let remaining = if prompt.contains('\n') {
25+
let lines: Vec<&str> = prompt.lines().collect();
26+
if lines.len() > 1 {
27+
// Everything except last line is the notification
28+
delegate_notifier = Some(lines[..lines.len() - 1].join("\n"));
3429
}
35-
}
30+
// Parse only the last line for prompt components
31+
lines.last().unwrap_or(&"").trim()
32+
} else {
33+
prompt.trim()
34+
};
35+
36+
let mut remaining = remaining;
3637

3738
// Check for agent pattern [agent] first
3839
if let Some(start) = remaining.find('[') {

0 commit comments

Comments
 (0)