notedeck

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

commit 5cd46712995d85ceedc7e7ed149f4aa4faba8f11
parent b1992cf43f52e2b2eedafaec174864dba9fb99be
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 19 Feb 2026 11:17:39 -0800

dave: merge ToolResult into ToolResponse as ExecutedTool variant

Remove the separate Message::ToolResult variant and unify tool
responses under Message::ToolResponse. ToolResult is renamed to
ExecutedTool and added as a ToolResponses variant, representing
tools already executed by an agentic backend. This eliminates the
duplication between ToolResult and ToolResponse in the Message enum.

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

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 9++++-----
Mcrates/notedeck_dave/src/lib.rs | 21+++++++++++++--------
Mcrates/notedeck_dave/src/messages.rs | 23++++++++++++-----------
Mcrates/notedeck_dave/src/session.rs | 12++++++++----
Mcrates/notedeck_dave/src/session_loader.rs | 19+++++++++++--------
Mcrates/notedeck_dave/src/tools.rs | 11+++++++++++
Mcrates/notedeck_dave/src/ui/dave.rs | 33++++++++++++++++++---------------
7 files changed, 77 insertions(+), 51 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -5,8 +5,8 @@ use crate::backend::tool_summary::{ }; use crate::backend::traits::AiBackend; use crate::messages::{ - CompactionInfo, DaveApiResponse, ParsedMarkdown, PendingPermission, PermissionRequest, - PermissionResponse, SubagentInfo, SubagentStatus, ToolResult, + CompactionInfo, DaveApiResponse, ExecutedTool, ParsedMarkdown, PendingPermission, + PermissionRequest, PermissionResponse, SubagentInfo, SubagentStatus, }; use crate::tools::Tool; use crate::Message; @@ -105,10 +105,9 @@ impl ClaudeBackend { | Message::ToolResponse(_) | Message::Error(_) | Message::PermissionRequest(_) - | Message::ToolResult(_) | Message::CompactionComplete(_) | Message::Subagent(_) => { - // Skip tool-related, error, permission, tool result, compaction, and subagent messages + // Skip tool-related, error, permission, compaction, and subagent messages } } } @@ -523,7 +522,7 @@ 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, parent_task_id }; + let tool_result = ExecutedTool { 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 @@ -40,8 +40,9 @@ use std::time::Instant; pub use avatar::DaveAvatar; pub use config::{AiMode, AiProvider, DaveSettings, ModelConfig}; pub use messages::{ - AskUserQuestionInput, AssistantMessage, DaveApiResponse, Message, PermissionResponse, - PermissionResponseType, QuestionAnswer, SessionInfo, SubagentInfo, SubagentStatus, ToolResult, + AskUserQuestionInput, AssistantMessage, DaveApiResponse, ExecutedTool, Message, + PermissionResponse, PermissionResponseType, QuestionAnswer, SessionInfo, SubagentInfo, + SubagentStatus, }; pub use quaternion::Quaternion; pub use session::{ChatSession, SessionId, SessionManager}; @@ -675,7 +676,9 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr } } if let Some(result) = session.fold_tool_result(result) { - session.chat.push(Message::ToolResult(result)); + session + .chat + .push(Message::ToolResponse(ToolResponse::executed_tool(result))); } } @@ -1747,11 +1750,13 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .to_string(); session .chat - .push(Message::ToolResult(crate::messages::ToolResult { - tool_name, - summary, - parent_task_id: None, - })); + .push(Message::ToolResponse(ToolResponse::executed_tool( + crate::messages::ExecutedTool { + tool_name, + summary, + parent_task_id: None, + }, + ))); } Some("permission_request") => { if let Ok(content_json) = serde_json::from_str::<serde_json::Value>(content) diff --git a/crates/notedeck_dave/src/messages.rs b/crates/notedeck_dave/src/messages.rs @@ -1,4 +1,4 @@ -use crate::tools::{ToolCall, ToolResponse}; +use crate::tools::{ToolCall, ToolResponse, ToolResponses}; use async_openai::types::*; use md_stream::{MdElement, Partial, StreamParser}; @@ -102,9 +102,10 @@ pub enum PermissionResponseType { Denied, } -/// Metadata about a completed tool execution -#[derive(Debug, Clone)] -pub struct ToolResult { +/// Metadata about a completed tool execution from an agentic backend. +/// Used as a variant in `ToolResponses` to unify with other tool responses. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExecutedTool { 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 @@ -159,7 +160,7 @@ pub struct SubagentInfo { /// 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>, + pub tool_results: Vec<ExecutedTool>, } /// An assistant message with incremental markdown parsing support. @@ -303,8 +304,6 @@ pub enum Message { ToolResponse(ToolResponse), /// A permission request from the AI that needs user response PermissionRequest(PermissionRequest), - /// Result metadata from a completed tool execution - ToolResult(ToolResult), /// Conversation was compacted CompactionComplete(CompactionInfo), /// A subagent spawned by Task tool @@ -327,7 +326,7 @@ pub enum DaveApiResponse { /// A permission request that needs to be displayed to the user PermissionRequest(PendingPermission), /// Metadata from a completed tool execution - ToolResult(ToolResult), + ToolResult(ExecutedTool), /// Session initialization info from Claude Code CLI SessionInfo(SessionInfo), /// Subagent spawned by Task tool @@ -388,6 +387,11 @@ impl Message { )), Message::ToolResponse(resp) => { + // ExecutedTool results are UI-only, not sent to the API + if matches!(resp.responses(), ToolResponses::ExecutedTool(_)) { + return None; + } + let tool_response = resp.responses().format_for_dave(txn, ndb); Some(ChatCompletionRequestMessage::Tool( @@ -401,9 +405,6 @@ impl Message { // Permission requests are UI-only, not sent to the API Message::PermissionRequest(_) => None, - // Tool results are UI-only, not sent to the API - Message::ToolResult(_) => None, - // Compaction complete is UI-only, not sent to the API Message::CompactionComplete(_) => None, diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -6,8 +6,8 @@ use crate::agent_status::AgentStatus; use crate::config::AiMode; use crate::git_status::GitStatusCache; use crate::messages::{ - AnswerSummary, CompactionInfo, PermissionResponse, PermissionResponseType, QuestionAnswer, - SessionInfo, SubagentStatus, ToolResult, + AnswerSummary, CompactionInfo, ExecutedTool, PermissionResponse, PermissionResponseType, + QuestionAnswer, SessionInfo, SubagentStatus, }; use crate::session_events::ThreadingState; use crate::{DaveApiResponse, Message}; @@ -255,7 +255,11 @@ 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> { + pub fn fold_tool_result( + &self, + chat: &mut [Message], + result: ExecutedTool, + ) -> Option<ExecutedTool> { 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) { @@ -421,7 +425,7 @@ 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> { + pub fn fold_tool_result(&mut self, result: ExecutedTool) -> Option<ExecutedTool> { if let Some(ref agentic) = self.agentic { agentic.fold_tool_result(&mut self.chat, result) } else { diff --git a/crates/notedeck_dave/src/session_loader.rs b/crates/notedeck_dave/src/session_loader.rs @@ -4,9 +4,10 @@ //! orders them by created_at, and converts them into `Message` variants //! for populating the chat UI. -use crate::messages::{AssistantMessage, PermissionRequest, PermissionResponseType, ToolResult}; +use crate::messages::{AssistantMessage, ExecutedTool, PermissionRequest, PermissionResponseType}; use crate::session::PermissionTracker; use crate::session_events::{get_tag_value, is_conversation_role, AI_CONVERSATION_KIND}; +use crate::tools::ToolResponse; use crate::Message; use nostrdb::{Filter, Ndb, NoteKey, Transaction}; use std::collections::HashSet; @@ -164,13 +165,15 @@ pub fn load_session_messages(ndb: &Ndb, txn: &Transaction, session_id: &str) -> )), Some("tool_result") => { let summary = truncate(content, 200); - Some(Message::ToolResult(ToolResult { - tool_name: get_tag_value(note, "tool-name") - .unwrap_or("tool") - .to_string(), - summary, - parent_task_id: None, - })) + Some(Message::ToolResponse(ToolResponse::executed_tool( + ExecutedTool { + tool_name: get_tag_value(note, "tool-name") + .unwrap_or("tool") + .to_string(), + summary, + parent_task_id: None, + }, + ))) } Some("permission_request") => { if let Ok(content_json) = serde_json::from_str::<serde_json::Value>(content) { diff --git a/crates/notedeck_dave/src/tools.rs b/crates/notedeck_dave/src/tools.rs @@ -1,3 +1,4 @@ +use crate::messages::ExecutedTool; use async_openai::types::*; use chrono::DateTime; use enostr::{NoteId, Pubkey}; @@ -98,6 +99,7 @@ pub enum ToolResponses { Error(String), Query(QueryResponse), PresentNotes(i32), + ExecutedTool(ExecutedTool), } #[derive(Debug, Clone)] @@ -344,6 +346,13 @@ impl ToolResponse { } } + pub fn executed_tool(result: ExecutedTool) -> Self { + Self { + id: String::new(), + typ: ToolResponses::ExecutedTool(result), + } + } + pub fn responses(&self) -> &ToolResponses { &self.typ } @@ -565,6 +574,8 @@ fn format_tool_response_for_ai(txn: &Transaction, ndb: &Ndb, resp: &ToolResponse serde_json::to_string(&json!({"search_results": simple_notes})).unwrap() } + + ToolResponses::ExecutedTool(r) => format!("{}: {}", r.tool_name, r.summary), } } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -9,12 +9,12 @@ use crate::{ file_update::FileUpdate, git_status::GitStatusCache, messages::{ - AskUserQuestionInput, AssistantMessage, CompactionInfo, Message, PermissionRequest, - PermissionResponse, PermissionResponseType, QuestionAnswer, SubagentInfo, SubagentStatus, - ToolResult, + AskUserQuestionInput, AssistantMessage, CompactionInfo, ExecutedTool, Message, + PermissionRequest, PermissionResponse, PermissionResponseType, QuestionAnswer, + SubagentInfo, SubagentStatus, }, session::{PermissionMessageState, SessionDetails, SessionId}, - tools::{PresentNotesCall, ToolCall, ToolCalls, ToolResponse}, + tools::{PresentNotesCall, ToolCall, ToolCalls, ToolResponse, ToolResponses}, }; use bitflags::bitflags; use egui::{Align, Key, KeyboardShortcut, Layout, Modifiers}; @@ -402,7 +402,7 @@ impl<'a> DaveUi<'a> { self.assistant_chat(msg, ui); } Message::ToolResponse(msg) => { - Self::tool_response_ui(msg, ui); + Self::tool_response_ui(msg, is_agentic, ui); } Message::System(_msg) => { // system prompt is not rendered. Maybe we could @@ -421,12 +421,6 @@ impl<'a> DaveUi<'a> { } } } - Message::ToolResult(result) => { - // Tool results only in Agentic mode - if is_agentic { - Self::tool_result_ui(result, ui); - } - } Message::CompactionComplete(info) => { // Compaction only in Agentic mode if is_agentic { @@ -473,8 +467,17 @@ impl<'a> DaveUi<'a> { response } - fn tool_response_ui(_tool_response: &ToolResponse, _ui: &mut egui::Ui) { - //ui.label(format!("tool_response: {:?}", tool_response)); + fn tool_response_ui(tool_response: &ToolResponse, is_agentic: bool, ui: &mut egui::Ui) { + match tool_response.responses() { + ToolResponses::ExecutedTool(result) => { + if is_agentic { + Self::executed_tool_ui(result, ui); + } + } + _ => { + //ui.label(format!("tool_response: {:?}", tool_response)); + } + } } /// Render a permission request with Allow/Deny buttons or response state @@ -828,7 +831,7 @@ impl<'a> DaveUi<'a> { } /// Render tool result metadata as a compact line - fn tool_result_ui(result: &ToolResult, ui: &mut egui::Ui) { + fn executed_tool_ui(result: &ExecutedTool, ui: &mut egui::Ui) { // Compact single-line display with subdued styling ui.horizontal(|ui| { // Tool name in slightly brighter text @@ -924,7 +927,7 @@ impl<'a> DaveUi<'a> { if expanded { ui.indent(("subagent_tools", &info.task_id), |ui| { for result in &info.tool_results { - Self::tool_result_ui(result, ui); + Self::executed_tool_ui(result, ui); } }); }