commit 39f877e4e48530769802fb289671cb60ef0ab829
parent bdd53f4e0be7c3d1d01e6db501226d857e02b303
Author: William Casarin <jb55@jb55.com>
Date: Tue, 17 Feb 2026 11:12:50 -0800
link tool results to their parent subagent
Track active subagent nesting in the backend via a stack of Task
tool_use_ids. Each ToolResult now carries a parent_task_id linking it
to the subagent that produced it. Child tool results are folded into
the SubagentInfo entry instead of appearing flat in the chat stream.
The subagent UI widget shows a clickable tool count that expands to
reveal the nested tool results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
6 files changed, 81 insertions(+), 4 deletions(-)
diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs
@@ -278,6 +278,9 @@ async fn session_actor(
// Track pending tool uses: tool_use_id -> (tool_name, tool_input)
let mut pending_tools: HashMap<String, (String, serde_json::Value)> =
HashMap::new();
+ // Track active subagent nesting: tool results emitted while
+ // a Task is in-flight belong to the top-of-stack subagent.
+ let mut subagent_stack: Vec<String> = Vec::new();
// Stream response with select! to handle stream, permission requests, and interrupts
let mut stream = client.receive_response();
@@ -439,6 +442,7 @@ async fn session_actor(
.unwrap_or("unknown")
.to_string();
+ subagent_stack.push(id.clone());
let subagent_info = SubagentInfo {
task_id: id.clone(),
description,
@@ -446,6 +450,7 @@ async fn session_actor(
status: SubagentStatus::Running,
output: String::new(),
max_output_size: 4000,
+ tool_results: Vec::new(),
};
let _ = response_tx.send(DaveApiResponse::SubagentSpawned(subagent_info));
ctx.request_repaint();
@@ -505,6 +510,8 @@ async fn session_actor(
// Check if this is a Task tool completion
if tool_name == "Task" {
+ // Pop this subagent from the stack
+ subagent_stack.retain(|id| id != tool_use_id);
let result_text = extract_response_content(&result_value)
.unwrap_or_else(|| "completed".to_string());
let _ = response_tx.send(DaveApiResponse::SubagentCompleted {
@@ -513,8 +520,10 @@ async fn session_actor(
});
}
+ // Attach parent subagent context (top of stack)
+ let parent_task_id = subagent_stack.last().cloned();
let summary = format_tool_summary(&tool_name, &tool_input, &result_value);
- let tool_result = ToolResult { tool_name, summary };
+ let tool_result = ToolResult { tool_name, summary, parent_task_id };
let _ = response_tx.send(DaveApiResponse::ToolResult(tool_result));
ctx.request_repaint();
}
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -593,7 +593,9 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
agentic.git_status.invalidate();
}
}
- session.chat.push(Message::ToolResult(result));
+ if let Some(result) = session.fold_tool_result(result) {
+ session.chat.push(Message::ToolResult(result));
+ }
}
DaveApiResponse::SessionInfo(info) => {
@@ -1658,6 +1660,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
.push(Message::ToolResult(crate::messages::ToolResult {
tool_name,
summary,
+ parent_task_id: None,
}));
}
Some("permission_request") => {
diff --git a/crates/notedeck_dave/src/messages.rs b/crates/notedeck_dave/src/messages.rs
@@ -107,6 +107,8 @@ pub enum PermissionResponseType {
pub struct ToolResult {
pub tool_name: String,
pub summary: String, // e.g., "154 lines", "exit 0", "3 matches"
+ /// Which subagent (Task tool_use_id) produced this result, if any
+ pub parent_task_id: Option<String>,
}
/// Session initialization info from Claude Code CLI
@@ -156,6 +158,8 @@ pub struct SubagentInfo {
pub output: String,
/// Maximum output size to keep (for size-restricted window)
pub max_output_size: usize,
+ /// Tool results produced by this subagent
+ pub tool_results: Vec<ToolResult>,
}
/// An assistant message with incremental markdown parsing support.
diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs
@@ -7,7 +7,7 @@ use crate::config::AiMode;
use crate::git_status::GitStatusCache;
use crate::messages::{
AnswerSummary, CompactionInfo, PermissionResponse, PermissionResponseType, QuestionAnswer,
- SessionInfo, SubagentStatus,
+ SessionInfo, SubagentStatus, ToolResult,
};
use crate::session_events::ThreadingState;
use crate::{DaveApiResponse, Message};
@@ -245,6 +245,19 @@ impl AgenticSessionData {
}
}
}
+
+ /// Try to fold a tool result into its parent subagent.
+ /// Returns None if folded, Some(result) if it couldn't be folded.
+ pub fn fold_tool_result(&self, chat: &mut [Message], result: ToolResult) -> Option<ToolResult> {
+ let parent_id = result.parent_task_id.as_ref()?;
+ let &idx = self.subagent_indices.get(parent_id)?;
+ if let Some(Message::Subagent(subagent)) = chat.get_mut(idx) {
+ subagent.tool_results.push(result);
+ None
+ } else {
+ Some(result)
+ }
+ }
}
/// A single chat session with Dave
@@ -392,6 +405,16 @@ impl ChatSession {
}
}
+ /// Try to fold a tool result into its parent subagent.
+ /// Returns None if folded, Some(result) if it couldn't be folded.
+ pub fn fold_tool_result(&mut self, result: ToolResult) -> Option<ToolResult> {
+ if let Some(ref agentic) = self.agentic {
+ agentic.fold_tool_result(&mut self.chat, result)
+ } else {
+ Some(result)
+ }
+ }
+
/// 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/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs
@@ -169,6 +169,7 @@ pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) ->
.unwrap_or("tool")
.to_string(),
summary,
+ parent_task_id: None,
}))
}
Some("permission_request") => {
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -835,8 +835,11 @@ impl<'a> DaveUi<'a> {
});
}
- /// Render a single subagent's status
+ /// Render a single subagent's status with expandable tool results
fn subagent_ui(info: &SubagentInfo, ui: &mut egui::Ui) {
+ let tool_count = info.tool_results.len();
+ let has_tools = tool_count > 0;
+
ui.horizontal(|ui| {
// Status badge with color based on status
let variant = match info.status {
@@ -859,7 +862,41 @@ impl<'a> DaveUi<'a> {
if info.status == SubagentStatus::Running {
ui.add(egui::Spinner::new().size(11.0));
}
+
+ // Tool count indicator (clickable to expand)
+ if has_tools {
+ let id = ui.id().with(&info.task_id);
+ let expanded = ui.data(|d| d.get_temp::<bool>(id).unwrap_or(false));
+ let arrow = if expanded { "▾" } else { "▸" };
+ let label = format!("{} ({} tools)", arrow, tool_count);
+ if ui
+ .add(
+ egui::Label::new(
+ egui::RichText::new(label)
+ .size(10.0)
+ .color(ui.visuals().text_color().gamma_multiply(0.4)),
+ )
+ .sense(egui::Sense::click()),
+ )
+ .clicked()
+ {
+ ui.data_mut(|d| d.insert_temp(id, !expanded));
+ }
+ }
});
+
+ // Expanded tool results
+ if has_tools {
+ let id = ui.id().with(&info.task_id);
+ let expanded = ui.data(|d| d.get_temp::<bool>(id).unwrap_or(false));
+ if expanded {
+ ui.indent(("subagent_tools", &info.task_id), |ui| {
+ for result in &info.tool_results {
+ Self::tool_result_ui(result, ui);
+ }
+ });
+ }
+ }
}
fn search_call_ui(