notedeck

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

commit c10d594f62a33346c63d30925ed0a0eef76f2f67
parent e7763d873568f9d689b3a23b4806d2d0035dd5c7
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 26 Jan 2026 00:45:30 -0800

dave: use StreamEvent for incremental text display

Switch from receiving complete text blocks via Message::Assistant to
using Message::StreamEvent with content_block_delta events. This enables
text to appear incrementally as it streams in, matching the OpenAI
backend behavior.

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

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 34++++++++++++++++++++++++----------
1 file changed, 24 insertions(+), 10 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -6,7 +6,7 @@ use crate::tools::Tool; use crate::Message; use claude_agent_sdk_rs::{ ClaudeAgentOptions, ClaudeClient, ContentBlock, Message as ClaudeMessage, PermissionMode, - PermissionResult, PermissionResultAllow, PermissionResultDeny, TextBlock, ToolUseBlock, + PermissionResult, PermissionResultAllow, PermissionResultDeny, ToolUseBlock, }; use futures::future::BoxFuture; use futures::StreamExt; @@ -210,12 +210,14 @@ impl AiBackend for ClaudeBackend { .permission_mode(PermissionMode::Default) .stderr_callback(stderr_callback.clone()) .can_use_tool(can_use_tool) + .include_partial_messages(true) .build() } else { ClaudeAgentOptions::builder() .permission_mode(PermissionMode::Default) .stderr_callback(stderr_callback.clone()) .can_use_tool(can_use_tool) + .include_partial_messages(true) .continue_conversation(true) .build() }; @@ -241,11 +243,29 @@ impl AiBackend for ClaudeBackend { match result { Ok(message) => match message { ClaudeMessage::Assistant(assistant_msg) => { + // Text is handled by StreamEvent for incremental display for block in &assistant_msg.message.content { - match block { - ContentBlock::Text(TextBlock { text }) => { + if let ContentBlock::ToolUse(ToolUseBlock { id, name, input }) = + block + { + // Store for later correlation with tool result + pending_tools.insert(id.clone(), (name.clone(), input.clone())); + } + } + } + ClaudeMessage::StreamEvent(event) => { + if let Some(event_type) = + event.event.get("type").and_then(|v| v.as_str()) + { + if event_type == "content_block_delta" { + if let Some(text) = event + .event + .get("delta") + .and_then(|d| d.get("text")) + .and_then(|t| t.as_str()) + { if let Err(err) = - tx.send(DaveApiResponse::Token(text.clone())) + tx.send(DaveApiResponse::Token(text.to_string())) { tracing::error!("Failed to send token to UI: {}", err); drop(stream); @@ -254,12 +274,6 @@ impl AiBackend for ClaudeBackend { } ctx.request_repaint(); } - ContentBlock::ToolUse(ToolUseBlock { id, name, input }) => { - // Store for later correlation with tool result - pending_tools - .insert(id.clone(), (name.clone(), input.clone())); - } - _ => {} } } }