notedeck

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

commit 8a85cf83fbd9ef21cc239bb92eb0150c82943f2b
parent d8c900d69ad3e77d86f2014a18fe7c09198dae50
Author: William Casarin <jb55@jb55.com>
Date:   Wed, 28 Jan 2026 18:56:50 -0800

dave: show compaction status indicator when /compact runs

Handle the status and compact_boundary system messages from Claude Code
CLI to show a "COMPACTING..." badge during conversation compaction.

- Add CompactionInfo and CompactionStarted/CompactionComplete responses
- Track is_compacting and last_compaction state in ChatSession
- Display amber warning badge in input area during compaction

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

Diffstat:
Mcrates/notedeck_dave/src/backend/claude.rs | 21+++++++++++++++++++--
Mcrates/notedeck_dave/src/lib.rs | 18++++++++++++++++++
Mcrates/notedeck_dave/src/messages.rs | 11+++++++++++
Mcrates/notedeck_dave/src/session.rs | 8+++++++-
Mcrates/notedeck_dave/src/ui/dave.rs | 16++++++++++++++++
5 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs @@ -1,7 +1,7 @@ use crate::backend::traits::AiBackend; use crate::messages::{ - DaveApiResponse, PendingPermission, PermissionRequest, PermissionResponse, SessionInfo, - SubagentInfo, SubagentStatus, ToolResult, + CompactionInfo, DaveApiResponse, PendingPermission, PermissionRequest, PermissionResponse, + SessionInfo, SubagentInfo, SubagentStatus, ToolResult, }; use crate::tools::Tool; use crate::Message; @@ -452,6 +452,23 @@ async fn session_actor( let session_info = parse_session_info(&system_msg); let _ = response_tx.send(DaveApiResponse::SessionInfo(session_info)); ctx.request_repaint(); + } else if system_msg.subtype == "status" { + // Handle status messages (compaction start/end) + let status = system_msg.data.get("status") + .and_then(|v| v.as_str()); + if status == Some("compacting") { + let _ = response_tx.send(DaveApiResponse::CompactionStarted); + ctx.request_repaint(); + } + // status: null means compaction finished (handled by compact_boundary) + } else if system_msg.subtype == "compact_boundary" { + // Compaction completed - extract token savings info + let pre_tokens = system_msg.data.get("pre_tokens") + .and_then(|v| v.as_u64()) + .unwrap_or(0); + let info = CompactionInfo { pre_tokens }; + let _ = response_tx.send(DaveApiResponse::CompactionComplete(info)); + ctx.request_repaint(); } else { tracing::debug!("Received system message subtype: {}", system_msg.subtype); } diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs @@ -309,6 +309,21 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr tracing::debug!("Subagent completed: {}", task_id); session.complete_subagent(&task_id, &result); } + + DaveApiResponse::CompactionStarted => { + tracing::debug!("Compaction started for session {}", session_id); + session.is_compacting = true; + } + + DaveApiResponse::CompactionComplete(info) => { + tracing::debug!( + "Compaction completed for session {}: pre_tokens={}", + session_id, + info.pre_tokens + ); + session.is_compacting = false; + session.last_compaction = Some(info); + } } } @@ -426,6 +441,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .permission_message_state(session.permission_message_state) .question_answers(&mut session.question_answers) .question_index(&mut session.question_index) + .is_compacting(session.is_compacting) .ui(app_ctx, ui); if response.action.is_some() { @@ -540,6 +556,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .permission_message_state(session.permission_message_state) .question_answers(&mut session.question_answers) .question_index(&mut session.question_index) + .is_compacting(session.is_compacting) .ui(app_ctx, ui) } else { DaveResponse::default() @@ -611,6 +628,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr .permission_message_state(session.permission_message_state) .question_answers(&mut session.question_answers) .question_index(&mut session.question_index) + .is_compacting(session.is_compacting) .ui(app_ctx, ui) } else { DaveResponse::default() diff --git a/crates/notedeck_dave/src/messages.rs b/crates/notedeck_dave/src/messages.rs @@ -162,6 +162,13 @@ pub enum Message { ToolResult(ToolResult), } +/// Compaction info from compact_boundary system message +#[derive(Debug, Clone)] +pub struct CompactionInfo { + /// Number of tokens before compaction + pub pre_tokens: u64, +} + /// The ai backends response. Since we are using streaming APIs these are /// represented as individual tokens or tool calls pub enum DaveApiResponse { @@ -186,6 +193,10 @@ pub enum DaveApiResponse { task_id: String, result: String, }, + /// Conversation compaction started + CompactionStarted, + /// Conversation compaction completed with token info + CompactionComplete(CompactionInfo), } impl Message { diff --git a/crates/notedeck_dave/src/session.rs b/crates/notedeck_dave/src/session.rs @@ -4,7 +4,7 @@ use std::sync::mpsc::Receiver; use crate::agent_status::AgentStatus; use crate::messages::{ - PermissionResponse, QuestionAnswer, SessionInfo, SubagentInfo, SubagentStatus, + CompactionInfo, PermissionResponse, QuestionAnswer, SessionInfo, SubagentInfo, SubagentStatus, }; use crate::{DaveApiResponse, Message}; use claude_agent_sdk_rs::PermissionMode; @@ -56,6 +56,10 @@ pub struct ChatSession { pub session_info: Option<SessionInfo>, /// Active subagents spawned by Task tool (keyed by task_id) pub subagents: HashMap<String, SubagentInfo>, + /// Whether conversation compaction is in progress + pub is_compacting: bool, + /// Info from the last completed compaction (for display) + pub last_compaction: Option<CompactionInfo>, } impl Drop for ChatSession { @@ -92,6 +96,8 @@ impl ChatSession { cwd: None, session_info: None, subagents: HashMap::new(), + is_compacting: false, + last_compaction: None, } } diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs @@ -35,6 +35,8 @@ pub struct DaveUi<'a> { question_answers: Option<&'a mut HashMap<Uuid, Vec<QuestionAnswer>>>, /// Current question index for multi-question AskUserQuestion question_index: Option<&'a mut HashMap<Uuid, usize>>, + /// Whether conversation compaction is in progress + is_compacting: bool, } /// The response the app generates. The response contains an optional @@ -125,6 +127,7 @@ impl<'a> DaveUi<'a> { permission_message_state: PermissionMessageState::None, question_answers: None, question_index: None, + is_compacting: false, } } @@ -168,6 +171,11 @@ impl<'a> DaveUi<'a> { self } + pub fn is_compacting(mut self, is_compacting: bool) -> Self { + self.is_compacting = is_compacting; + self + } + fn chat_margin(&self, ctx: &egui::Context) -> i8 { if self.compact || notedeck::ui::is_narrow(ctx) { 20 @@ -732,6 +740,14 @@ impl<'a> DaveUi<'a> { dave_response = DaveResponse::send(); } + // Show compaction indicator when compacting + if self.is_compacting { + super::badge::StatusBadge::new("COMPACTING...") + .variant(super::badge::BadgeVariant::Warning) + .show(ui) + .on_hover_text("Conversation is being compacted to save tokens"); + } + // Show plan mode indicator with optional keybind hint when Ctrl is held let ctrl_held = ui.input(|i| i.modifiers.ctrl); let mut badge =