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:
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);