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:
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 =