commit 9806974fac0e1325d335c68213a11f8b37703a32
parent bd1f9de4c564d2d681c673af5a63eb206b8df53c
Author: William Casarin <jb55@jb55.com>
Date: Fri, 13 Feb 2026 13:52:30 -0800
dave: render plan view with markdown instead of plain text
Pre-parse plan content into MdElement at PermissionRequest
construction time so we avoid parsing every frame. Falls back
to plain text label if cached elements are unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
4 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/crates/md-stream/src/parser.rs b/crates/md-stream/src/parser.rs
@@ -60,6 +60,11 @@ impl StreamParser {
&self.parsed
}
+ /// Consume the parser and return the completed elements.
+ pub fn into_parsed(self) -> Vec<MdElement> {
+ self.parsed
+ }
+
/// Get the current partial state (for speculative rendering).
pub fn partial(&self) -> Option<&Partial> {
self.partial.as_ref()
diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs
@@ -341,12 +341,26 @@ async fn session_actor(
let request_id = Uuid::new_v4();
let (ui_resp_tx, ui_resp_rx) = oneshot::channel();
+ let cached_plan_elements = if perm_req.tool_name == "ExitPlanMode" {
+ perm_req.tool_input.get("plan")
+ .and_then(|v| v.as_str())
+ .map(|plan| {
+ let mut parser = md_stream::StreamParser::new();
+ parser.push(plan);
+ parser.finalize();
+ parser.into_parsed()
+ })
+ } else {
+ None
+ };
+
let request = PermissionRequest {
id: request_id,
tool_name: perm_req.tool_name.clone(),
tool_input: perm_req.tool_input.clone(),
response: None,
answer_summary: None,
+ cached_plan_elements,
};
let pending = PendingPermission {
diff --git a/crates/notedeck_dave/src/messages.rs b/crates/notedeck_dave/src/messages.rs
@@ -52,6 +52,8 @@ pub struct PermissionRequest {
pub response: Option<PermissionResponseType>,
/// For AskUserQuestion: pre-computed summary of answers for display
pub answer_summary: Option<AnswerSummary>,
+ /// For ExitPlanMode: pre-parsed markdown elements from the plan content
+ pub cached_plan_elements: Option<Vec<MdElement>>,
}
/// A single entry in an answer summary
diff --git a/crates/notedeck_dave/src/ui/dave.rs b/crates/notedeck_dave/src/ui/dave.rs
@@ -679,14 +679,6 @@ impl<'a> DaveUi<'a> {
let inner_margin = 12.0;
let corner_radius = 8.0;
- // The plan content is in tool_input.plan field
- let plan_content = request
- .tool_input
- .get("plan")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
-
egui::Frame::new()
.fill(ui.visuals().widgets.noninteractive.bg_fill)
.inner_margin(inner_margin)
@@ -705,16 +697,15 @@ impl<'a> DaveUi<'a> {
ui.add_space(8.0);
- // Display the plan content as plain text (TODO: markdown rendering)
- ui.add(
- egui::Label::new(
- egui::RichText::new(&plan_content)
- .monospace()
- .size(11.0)
- .color(ui.visuals().text_color()),
- )
- .wrap_mode(egui::TextWrapMode::Wrap),
- );
+ // Render plan content as markdown (pre-parsed at construction)
+ if let Some(elements) = &request.cached_plan_elements {
+ markdown_ui::render_assistant_message(elements, None, ui);
+ } else if let Some(plan) =
+ request.tool_input.get("plan").and_then(|v| v.as_str())
+ {
+ // Fallback: render as plain text
+ ui.label(plan);
+ }
ui.add_space(8.0);