Display ACP plans (#34816)

Agus Zubiaga and Danilo Leal created

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

Cargo.lock                               |   4 
Cargo.toml                               |   2 
assets/icons/todo_complete.svg           |   4 
assets/icons/todo_pending.svg            |  10 +
assets/icons/todo_progress.svg           |  11 +
crates/acp_thread/src/acp_thread.rs      | 109 ++++++++++++
crates/agent_servers/src/claude.rs       |  19 ++
crates/agent_servers/src/claude/tools.rs |  30 +++
crates/agent_ui/src/acp/thread_view.rs   | 221 ++++++++++++++++++++++---
crates/icons/src/icons.rs                |   3 
10 files changed, 379 insertions(+), 34 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -279,9 +279,9 @@ dependencies = [
 
 [[package]]
 name = "agentic-coding-protocol"
-version = "0.0.9"
+version = "0.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7"
+checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
 dependencies = [
  "anyhow",
  "chrono",

Cargo.toml 🔗

@@ -410,7 +410,7 @@ zlog_settings = { path = "crates/zlog_settings" }
 # External crates
 #
 
-agentic-coding-protocol = "0.0.9"
+agentic-coding-protocol = "0.0.10"
 aho-corasick = "1.1"
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
 any_vec = "0.14"

assets/icons/todo_complete.svg 🔗

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/todo_pending.svg 🔗

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/todo_progress.svg 🔗

@@ -0,0 +1,11 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

crates/acp_thread/src/acp_thread.rs 🔗

@@ -453,9 +453,69 @@ impl Diff {
     }
 }
 
+#[derive(Debug, Default)]
+pub struct Plan {
+    pub entries: Vec<PlanEntry>,
+}
+
+#[derive(Debug)]
+pub struct PlanStats<'a> {
+    pub in_progress_entry: Option<&'a PlanEntry>,
+    pub pending: u32,
+    pub completed: u32,
+}
+
+impl Plan {
+    pub fn is_empty(&self) -> bool {
+        self.entries.is_empty()
+    }
+
+    pub fn stats(&self) -> PlanStats<'_> {
+        let mut stats = PlanStats {
+            in_progress_entry: None,
+            pending: 0,
+            completed: 0,
+        };
+
+        for entry in &self.entries {
+            match &entry.status {
+                acp::PlanEntryStatus::Pending => {
+                    stats.pending += 1;
+                }
+                acp::PlanEntryStatus::InProgress => {
+                    stats.in_progress_entry = stats.in_progress_entry.or(Some(entry));
+                }
+                acp::PlanEntryStatus::Completed => {
+                    stats.completed += 1;
+                }
+            }
+        }
+
+        stats
+    }
+}
+
+#[derive(Debug)]
+pub struct PlanEntry {
+    pub content: Entity<Markdown>,
+    pub priority: acp::PlanEntryPriority,
+    pub status: acp::PlanEntryStatus,
+}
+
+impl PlanEntry {
+    pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self {
+        Self {
+            content: cx.new(|cx| Markdown::new_text(entry.content.into(), cx)),
+            priority: entry.priority,
+            status: entry.status,
+        }
+    }
+}
+
 pub struct AcpThread {
-    entries: Vec<AgentThreadEntry>,
     title: SharedString,
+    entries: Vec<AgentThreadEntry>,
+    plan: Plan,
     project: Entity<Project>,
     action_log: Entity<ActionLog>,
     shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
@@ -515,6 +575,7 @@ impl AcpThread {
             action_log,
             shared_buffers: Default::default(),
             entries: Default::default(),
+            plan: Default::default(),
             title,
             project,
             send_task: None,
@@ -819,6 +880,29 @@ impl AcpThread {
         }
     }
 
+    pub fn plan(&self) -> &Plan {
+        &self.plan
+    }
+
+    pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context<Self>) {
+        self.plan = Plan {
+            entries: request
+                .entries
+                .into_iter()
+                .map(|entry| PlanEntry::from_acp(entry, cx))
+                .collect(),
+        };
+
+        cx.notify();
+    }
+
+    pub fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
+        self.plan
+            .entries
+            .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed));
+        cx.notify();
+    }
+
     pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) {
         self.project.update(cx, |project, cx| {
             let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
@@ -1136,6 +1220,17 @@ impl AcpClientDelegate {
         Self { thread, cx }
     }
 
+    pub async fn clear_completed_plan_entries(&self) -> Result<()> {
+        let cx = &mut self.cx.clone();
+        cx.update(|cx| {
+            self.thread
+                .update(cx, |thread, cx| thread.clear_completed_plan_entries(cx))
+        })?
+        .context("Failed to update thread")?;
+
+        Ok(())
+    }
+
     pub async fn request_existing_tool_call_confirmation(
         &self,
         tool_call_id: ToolCallId,
@@ -1233,6 +1328,18 @@ impl acp::Client for AcpClientDelegate {
         Ok(())
     }
 
+    async fn update_plan(&self, request: acp::UpdatePlanParams) -> Result<(), acp::Error> {
+        let cx = &mut self.cx.clone();
+
+        cx.update(|cx| {
+            self.thread
+                .update(cx, |thread, cx| thread.update_plan(request, cx))
+        })?
+        .context("Failed to update thread")?;
+
+        Ok(())
+    }
+
     async fn read_text_file(
         &self,
         request: acp::ReadTextFileParams,

crates/agent_servers/src/claude.rs 🔗

@@ -153,6 +153,7 @@ impl AgentServer for ClaudeCode {
                 let handler_task = cx.foreground_executor().spawn({
                     let end_turn_tx = end_turn_tx.clone();
                     let tool_id_map = tool_id_map.clone();
+                    let delegate = delegate.clone();
                     async move {
                         while let Some(message) = incoming_message_rx.next().await {
                             ClaudeAgentConnection::handle_message(
@@ -167,6 +168,7 @@ impl AgentServer for ClaudeCode {
                 });
 
                 let mut connection = ClaudeAgentConnection {
+                    delegate,
                     outgoing_tx,
                     end_turn_tx,
                     _handler_task: handler_task,
@@ -186,6 +188,7 @@ impl AgentConnection for ClaudeAgentConnection {
         &self,
         params: AnyAgentRequest,
     ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
+        let delegate = self.delegate.clone();
         let end_turn_tx = self.end_turn_tx.clone();
         let outgoing_tx = self.outgoing_tx.clone();
         async move {
@@ -201,6 +204,8 @@ impl AgentConnection for ClaudeAgentConnection {
                     Err(anyhow!("Authentication not supported"))
                 }
                 AnyAgentRequest::SendUserMessageParams(message) => {
+                    delegate.clear_completed_plan_entries().await?;
+
                     let (tx, rx) = oneshot::channel();
                     end_turn_tx.borrow_mut().replace(tx);
                     let mut content = String::new();
@@ -241,6 +246,7 @@ impl AgentConnection for ClaudeAgentConnection {
 }
 
 struct ClaudeAgentConnection {
+    delegate: AcpClientDelegate,
     outgoing_tx: UnboundedSender<SdkMessage>,
     end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
     _mcp_server: Option<ClaudeMcpServer>,
@@ -267,8 +273,17 @@ impl ClaudeAgentConnection {
                                 .log_err();
                         }
                         ContentChunk::ToolUse { id, name, input } => {
-                            if let Some(resp) = delegate
-                                .push_tool_call(ClaudeTool::infer(&name, input).as_acp())
+                            let claude_tool = ClaudeTool::infer(&name, input);
+
+                            if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
+                                delegate
+                                    .update_plan(acp::UpdatePlanParams {
+                                        entries: params.todos.into_iter().map(Into::into).collect(),
+                                    })
+                                    .await
+                                    .log_err();
+                            } else if let Some(resp) = delegate
+                                .push_tool_call(claude_tool.as_acp())
                                 .await
                                 .log_err()
                             {

crates/agent_servers/src/claude/tools.rs 🔗

@@ -614,6 +614,16 @@ pub enum TodoPriority {
     Low,
 }
 
+impl Into<acp::PlanEntryPriority> for TodoPriority {
+    fn into(self) -> acp::PlanEntryPriority {
+        match self {
+            TodoPriority::High => acp::PlanEntryPriority::High,
+            TodoPriority::Medium => acp::PlanEntryPriority::Medium,
+            TodoPriority::Low => acp::PlanEntryPriority::Low,
+        }
+    }
+}
+
 #[derive(Deserialize, Serialize, JsonSchema, Debug)]
 #[serde(rename_all = "snake_case")]
 pub enum TodoStatus {
@@ -622,6 +632,16 @@ pub enum TodoStatus {
     Completed,
 }
 
+impl Into<acp::PlanEntryStatus> for TodoStatus {
+    fn into(self) -> acp::PlanEntryStatus {
+        match self {
+            TodoStatus::Pending => acp::PlanEntryStatus::Pending,
+            TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
+            TodoStatus::Completed => acp::PlanEntryStatus::Completed,
+        }
+    }
+}
+
 #[derive(Deserialize, Serialize, JsonSchema, Debug)]
 pub struct Todo {
     /// Unique identifier
@@ -634,6 +654,16 @@ pub struct Todo {
     pub status: TodoStatus,
 }
 
+impl Into<acp::PlanEntry> for Todo {
+    fn into(self) -> acp::PlanEntry {
+        acp::PlanEntry {
+            content: self.content,
+            priority: self.priority.into(),
+            status: self.status.into(),
+        }
+    }
+}
+
 #[derive(Deserialize, JsonSchema, Debug)]
 pub struct TodoWriteToolParams {
     pub todos: Vec<Todo>,

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -1,3 +1,4 @@
+use acp_thread::Plan;
 use agent_servers::AgentServer;
 use std::cell::RefCell;
 use std::collections::BTreeMap;
@@ -66,7 +67,8 @@ pub struct AcpThreadView {
     expanded_tool_calls: HashSet<ToolCallId>,
     expanded_thinking_blocks: HashSet<(usize, usize)>,
     edits_expanded: bool,
-    editor_is_expanded: bool,
+    plan_expanded: bool,
+    editor_expanded: bool,
     message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
 }
 
@@ -186,7 +188,8 @@ impl AcpThreadView {
             expanded_tool_calls: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
             edits_expanded: false,
-            editor_is_expanded: false,
+            plan_expanded: false,
+            editor_expanded: false,
             message_history,
         }
     }
@@ -332,14 +335,14 @@ impl AcpThreadView {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        self.set_editor_is_expanded(!self.editor_is_expanded, cx);
+        self.set_editor_is_expanded(!self.editor_expanded, cx);
         cx.notify();
     }
 
     fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
-        self.editor_is_expanded = is_expanded;
+        self.editor_expanded = is_expanded;
         self.message_editor.update(cx, |editor, _| {
-            if self.editor_is_expanded {
+            if self.editor_expanded {
                 editor.set_mode(EditorMode::Full {
                     scale_ui_elements_with_buffer_font_size: false,
                     show_active_line_background: false,
@@ -1477,7 +1480,7 @@ impl AcpThreadView {
         container.into_any()
     }
 
-    fn render_edits_bar(
+    fn render_activity_bar(
         &self,
         thread_entity: &Entity<AcpThread>,
         window: &mut Window,
@@ -1486,8 +1489,9 @@ impl AcpThreadView {
         let thread = thread_entity.read(cx);
         let action_log = thread.action_log();
         let changed_buffers = action_log.read(cx).changed_buffers(cx);
+        let plan = thread.plan();
 
-        if changed_buffers.is_empty() {
+        if changed_buffers.is_empty() && plan.is_empty() {
             return None;
         }
 
@@ -1496,7 +1500,6 @@ impl AcpThreadView {
         let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
 
         let pending_edits = thread.has_pending_edit_tool_calls();
-        let expanded = self.edits_expanded;
 
         v_flex()
             .mt_1()
@@ -1512,27 +1515,165 @@ impl AcpThreadView {
                 blur_radius: px(3.),
                 spread_radius: px(0.),
             }])
-            .child(self.render_edits_bar_summary(
-                action_log,
-                &changed_buffers,
-                expanded,
-                pending_edits,
-                window,
-                cx,
-            ))
-            .when(expanded, |parent| {
-                parent.child(self.render_edits_bar_files(
-                    action_log,
-                    &changed_buffers,
-                    pending_edits,
-                    cx,
-                ))
+            .when(!plan.is_empty(), |this| {
+                this.child(self.render_plan_summary(plan, window, cx))
+                    .when(self.plan_expanded, |parent| {
+                        parent.child(self.render_plan_entries(plan, window, cx))
+                    })
+            })
+            .when(!changed_buffers.is_empty(), |this| {
+                this.child(Divider::horizontal())
+                    .child(self.render_edits_summary(
+                        action_log,
+                        &changed_buffers,
+                        self.edits_expanded,
+                        pending_edits,
+                        window,
+                        cx,
+                    ))
+                    .when(self.edits_expanded, |parent| {
+                        parent.child(self.render_edited_files(
+                            action_log,
+                            &changed_buffers,
+                            pending_edits,
+                            cx,
+                        ))
+                    })
             })
             .into_any()
             .into()
     }
 
-    fn render_edits_bar_summary(
+    fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
+        let stats = plan.stats();
+
+        let title = if let Some(entry) = stats.in_progress_entry
+            && !self.plan_expanded
+        {
+            h_flex()
+                .w_full()
+                .gap_1()
+                .text_xs()
+                .text_color(cx.theme().colors().text_muted)
+                .justify_between()
+                .child(
+                    h_flex()
+                        .gap_1()
+                        .child(
+                            Label::new("Current:")
+                                .size(LabelSize::Small)
+                                .color(Color::Muted),
+                        )
+                        .child(MarkdownElement::new(
+                            entry.content.clone(),
+                            plan_label_markdown_style(&entry.status, window, cx),
+                        )),
+                )
+                .when(stats.pending > 0, |this| {
+                    this.child(
+                        Label::new(format!("{} left", stats.pending))
+                            .size(LabelSize::Small)
+                            .color(Color::Muted)
+                            .mr_1(),
+                    )
+                })
+        } else {
+            let status_label = if stats.pending == 0 {
+                "All Done".to_string()
+            } else if stats.completed == 0 {
+                format!("{}", plan.entries.len())
+            } else {
+                format!("{}/{}", stats.completed, plan.entries.len())
+            };
+
+            h_flex()
+                .w_full()
+                .gap_1()
+                .justify_between()
+                .child(
+                    Label::new("Plan")
+                        .size(LabelSize::Small)
+                        .color(Color::Muted),
+                )
+                .child(
+                    Label::new(status_label)
+                        .size(LabelSize::Small)
+                        .color(Color::Muted)
+                        .mr_1(),
+                )
+        };
+
+        h_flex()
+            .p_1()
+            .justify_between()
+            .when(self.plan_expanded, |this| {
+                this.border_b_1().border_color(cx.theme().colors().border)
+            })
+            .child(
+                h_flex()
+                    .id("plan_summary")
+                    .w_full()
+                    .gap_1()
+                    .child(Disclosure::new("plan_disclosure", self.plan_expanded))
+                    .child(title)
+                    .on_click(cx.listener(|this, _, _, cx| {
+                        this.plan_expanded = !this.plan_expanded;
+                        cx.notify();
+                    })),
+            )
+    }
+
+    fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
+        v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
+            let element = h_flex()
+                .py_1()
+                .px_2()
+                .gap_2()
+                .justify_between()
+                .bg(cx.theme().colors().editor_background)
+                .when(index < plan.entries.len() - 1, |parent| {
+                    parent.border_color(cx.theme().colors().border).border_b_1()
+                })
+                .child(
+                    h_flex()
+                        .id(("plan_entry", index))
+                        .gap_1p5()
+                        .max_w_full()
+                        .overflow_x_scroll()
+                        .text_xs()
+                        .text_color(cx.theme().colors().text_muted)
+                        .child(match entry.status {
+                            acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
+                                .size(IconSize::Small)
+                                .color(Color::Muted)
+                                .into_any_element(),
+                            acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
+                                .size(IconSize::Small)
+                                .color(Color::Accent)
+                                .with_animation(
+                                    "running",
+                                    Animation::new(Duration::from_secs(2)).repeat(),
+                                    |icon, delta| {
+                                        icon.transform(Transformation::rotate(percentage(delta)))
+                                    },
+                                )
+                                .into_any_element(),
+                            acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
+                                .size(IconSize::Small)
+                                .color(Color::Success)
+                                .into_any_element(),
+                        })
+                        .child(MarkdownElement::new(
+                            entry.content.clone(),
+                            plan_label_markdown_style(&entry.status, window, cx),
+                        )),
+                );
+
+            Some(element)
+        }))
+    }
+
+    fn render_edits_summary(
         &self,
         action_log: &Entity<ActionLog>,
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@@ -1678,7 +1819,7 @@ impl AcpThreadView {
             )
     }
 
-    fn render_edits_bar_files(
+    fn render_edited_files(
         &self,
         action_log: &Entity<ActionLog>,
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
@@ -1831,7 +1972,7 @@ impl AcpThreadView {
     fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
         let focus_handle = self.message_editor.focus_handle(cx);
         let editor_bg_color = cx.theme().colors().editor_background;
-        let (expand_icon, expand_tooltip) = if self.editor_is_expanded {
+        let (expand_icon, expand_tooltip) = if self.editor_expanded {
             (IconName::Minimize, "Minimize Message Editor")
         } else {
             (IconName::Maximize, "Expand Message Editor")
@@ -1844,7 +1985,7 @@ impl AcpThreadView {
             .border_t_1()
             .border_color(cx.theme().colors().border)
             .bg(editor_bg_color)
-            .when(self.editor_is_expanded, |this| {
+            .when(self.editor_expanded, |this| {
                 this.h(vh(0.8, window)).size_full().justify_between()
             })
             .child(
@@ -2243,7 +2384,7 @@ impl Render for AcpThreadView {
                                 .child(LoadingLabel::new("").size(LabelSize::Small))
                                 .into(),
                         })
-                        .children(self.render_edits_bar(&thread, window, cx))
+                        .children(self.render_activity_bar(&thread, window, cx))
                     } else {
                         this.child(self.render_empty_state(cx))
                     }
@@ -2409,3 +2550,27 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd
         ..Default::default()
     }
 }
+
+fn plan_label_markdown_style(
+    status: &acp::PlanEntryStatus,
+    window: &Window,
+    cx: &App,
+) -> MarkdownStyle {
+    let default_md_style = default_markdown_style(false, window, cx);
+
+    MarkdownStyle {
+        base_text_style: TextStyle {
+            color: cx.theme().colors().text_muted,
+            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
+                Some(gpui::StrikethroughStyle {
+                    thickness: px(1.),
+                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
+                })
+            } else {
+                None
+            },
+            ..default_md_style.base_text_style
+        },
+        ..default_md_style
+    }
+}

crates/icons/src/icons.rs 🔗

@@ -256,6 +256,9 @@ pub enum IconName {
     TextSnippet,
     ThumbsDown,
     ThumbsUp,
+    TodoComplete,
+    TodoPending,
+    TodoProgress,
     ToolBulb,
     ToolCopy,
     ToolDeleteFile,