notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit cd4ab88e27520e70dc9ce96414654f5936c4e7ee
parent 670ac26aa818826e712fcc708a1dbc09f0bcb237
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 28 Jan 2026 19:56:04 -0800

dave: move subagent UI into chat history

Subagent status now appears inline with other messages in the chat
history instead of being stuck at the bottom. Uses Message::Subagent
variant and tracks indices to update status in place.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 5+++--
Mcrates/notedeck_dave/src/lib.rs | 5++++-
Mcrates/notedeck_dave/src/messages.rs | 5+++++
Mcrates/notedeck_dave/src/session.rs | 35++++++++++++++++-------------------
Mcrates/notedeck_dave/src/ui/dave.rs | 33++++++++++++++++++++++++++++++++-
5 files changed, 60 insertions(+), 23 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -92,8 +92,9 @@ impl ClaudeBackend { | Message::Error(_) | Message::PermissionRequest(_) | Message::ToolResult(_) - | Message::CompactionComplete(_) => { - // Skip tool-related, error, permission, tool result, and compaction messages + | Message::CompactionComplete(_) + | Message::Subagent(_) => { + // Skip tool-related, error, permission, tool result, compaction, and subagent messages } } } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -298,7 +298,10 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr subagent.subagent_type, subagent.description ); - session.subagents.insert(subagent.task_id.clone(), subagent); + let task_id = subagent.task_id.clone(); + let idx = session.chat.len(); + session.chat.push(Message::Subagent(subagent)); + session.subagent_indices.insert(task_id, idx); } DaveApiResponse::SubagentOutput { task_id, output } => { diff --git a/crates/notedeck_dave/src/messages.rs b/crates/notedeck_dave/src/messages.rs @@ -162,6 +162,8 @@ pub enum Message { ToolResult(ToolResult), /// Conversation was compacted CompactionComplete(CompactionInfo), + /// A subagent spawned by Task tool + Subagent(SubagentInfo), } /// Compaction info from compact_boundary system message @@ -259,6 +261,9 @@ impl Message { // Compaction complete is UI-only, not sent to the API Message::CompactionComplete(_) => None, + + // Subagent info is UI-only, not sent to the API + Message::Subagent(_) => None, } } } diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -54,8 +54,8 @@ pub struct ChatSession { pub cwd: Option<PathBuf>, /// Session info from Claude Code CLI (tools, model, agents, etc.) pub session_info: Option<SessionInfo>, - /// Active subagents spawned by Task tool (keyed by task_id) - pub subagents: HashMap<String, SubagentInfo>, + /// Indices of subagent messages in chat (keyed by task_id) + pub subagent_indices: HashMap<String, usize>, /// Whether conversation compaction is in progress pub is_compacting: bool, /// Info from the last completed compaction (for display) @@ -95,7 +95,7 @@ impl ChatSession { question_index: HashMap::new(), cwd: None, session_info: None, - subagents: HashMap::new(), + subagent_indices: HashMap::new(), is_compacting: false, last_compaction: None, } @@ -103,31 +103,28 @@ impl ChatSession { /// Update a subagent's output (appending new content, keeping only the tail) pub fn update_subagent_output(&mut self, task_id: &str, new_output: &str) { - if let Some(subagent) = self.subagents.get_mut(task_id) { - subagent.output.push_str(new_output); - // Keep only the most recent content up to max_output_size - if subagent.output.len() > subagent.max_output_size { - let keep_from = subagent.output.len() - subagent.max_output_size; - subagent.output = subagent.output[keep_from..].to_string(); + if let Some(&idx) = self.subagent_indices.get(task_id) { + if let Some(Message::Subagent(subagent)) = self.chat.get_mut(idx) { + subagent.output.push_str(new_output); + // Keep only the most recent content up to max_output_size + if subagent.output.len() > subagent.max_output_size { + let keep_from = subagent.output.len() - subagent.max_output_size; + subagent.output = subagent.output[keep_from..].to_string(); + } } } } /// Mark a subagent as completed pub fn complete_subagent(&mut self, task_id: &str, result: &str) { - if let Some(subagent) = self.subagents.get_mut(task_id) { - subagent.status = SubagentStatus::Completed; - subagent.output = result.to_string(); + if let Some(&idx) = self.subagent_indices.get(task_id) { + if let Some(Message::Subagent(subagent)) = self.chat.get_mut(idx) { + subagent.status = SubagentStatus::Completed; + subagent.output = result.to_string(); + } } } - /// Get all active (running) subagents - pub fn active_subagents(&self) -> impl Iterator<Item = &SubagentInfo> { - self.subagents - .values() - .filter(|s| s.status == SubagentStatus::Running) - } - /// Update the session title from the last message (user or assistant) pub fn update_title_from_last_message(&mut self) { for msg in self.chat.iter().rev() { diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -1,10 +1,11 @@ +use super::badge::{BadgeVariant, StatusBadge}; use super::diff; use crate::{ config::DaveSettings, file_update::FileUpdate, messages::{ AskUserQuestionInput, CompactionInfo, Message, PermissionRequest, PermissionResponse, - PermissionResponseType, QuestionAnswer, ToolResult, + PermissionResponseType, QuestionAnswer, SubagentInfo, SubagentStatus, ToolResult, }, session::PermissionMessageState, tools::{PresentNotesCall, QueryCall, ToolCall, ToolCalls, ToolResponse}, @@ -295,6 +296,9 @@ impl<'a> DaveUi<'a> { Message::CompactionComplete(info) => { Self::compaction_complete_ui(info, ui); } + Message::Subagent(info) => { + Self::subagent_ui(info, ui); + } }; } @@ -636,6 +640,33 @@ impl<'a> DaveUi<'a> { }); } + /// Render a single subagent's status + fn subagent_ui(info: &SubagentInfo, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + // Status badge with color based on status + let variant = match info.status { + SubagentStatus::Running => BadgeVariant::Warning, + SubagentStatus::Completed => BadgeVariant::Success, + SubagentStatus::Failed => BadgeVariant::Destructive, + }; + StatusBadge::new(&info.subagent_type) + .variant(variant) + .show(ui); + + // Description + ui.label( + egui::RichText::new(&info.description) + .size(11.0) + .color(ui.visuals().text_color().gamma_multiply(0.7)), + ); + + // Show spinner for running subagents + if info.status == SubagentStatus::Running { + ui.add(egui::Spinner::new().size(11.0)); + } + }); + } + fn search_call_ui(ctx: &mut AppContext, query_call: &QueryCall, ui: &mut egui::Ui) { ui.add(search_icon(16.0, 16.0)); ui.add_space(8.0);