assistant2: Add ability to open the active thread as Markdown (#26690)

Marshall Bowers created

This PR adds a new `assistant2: open active thread as markdown` action
that opens up the active thread in a Markdown representation:

<img width="1394" alt="Screenshot 2025-03-13 at 12 25 33 PM"
src="https://github.com/user-attachments/assets/363baaaa-c74b-4e93-af36-a3e04a114af0"
/>

Release Notes:

- N/A

Change summary

crates/assistant2/src/assistant.rs       |  3 
crates/assistant2/src/assistant_panel.rs | 72 +++++++++++++++++++++++++
crates/assistant2/src/thread.rs          | 45 ++++++++++++++++
3 files changed, 117 insertions(+), 3 deletions(-)

Detailed changes

crates/assistant2/src/assistant.rs 🔗

@@ -53,7 +53,8 @@ actions!(
         FocusLeft,
         FocusRight,
         RemoveFocusedContext,
-        AcceptSuggestedContext
+        AcceptSuggestedContext,
+        OpenActiveThreadAsMarkdown
     ]
 );
 

crates/assistant2/src/assistant_panel.rs 🔗

@@ -11,7 +11,7 @@ use assistant_slash_command::SlashCommandWorkingSet;
 use assistant_tool::ToolWorkingSet;
 
 use client::zed_urls;
-use editor::Editor;
+use editor::{Editor, MultiBuffer};
 use fs::Fs;
 use gpui::{
     prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
@@ -38,7 +38,10 @@ use crate::message_editor::MessageEditor;
 use crate::thread::{Thread, ThreadError, ThreadId};
 use crate::thread_history::{PastContext, PastThread, ThreadHistory};
 use crate::thread_store::ThreadStore;
-use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
+use crate::{
+    InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
+    OpenHistory,
+};
 
 pub fn init(cx: &mut App) {
     cx.observe_new(
@@ -411,6 +414,70 @@ impl AssistantPanel {
         }
     }
 
+    pub(crate) fn open_active_thread_as_markdown(
+        &mut self,
+        _: &OpenActiveThreadAsMarkdown,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(workspace) = self
+            .workspace
+            .upgrade()
+            .ok_or_else(|| anyhow!("workspace dropped"))
+            .log_err()
+        else {
+            return;
+        };
+
+        let markdown_language_task = workspace
+            .read(cx)
+            .app_state()
+            .languages
+            .language_for_name("Markdown");
+        let thread = self.active_thread(cx);
+        cx.spawn_in(window, |_this, mut cx| async move {
+            let markdown_language = markdown_language_task.await?;
+
+            workspace.update_in(&mut cx, |workspace, window, cx| {
+                let thread = thread.read(cx);
+                let markdown = thread.to_markdown()?;
+                let thread_summary = thread
+                    .summary()
+                    .map(|summary| summary.to_string())
+                    .unwrap_or_else(|| "Thread".to_string());
+
+                let project = workspace.project().clone();
+                let buffer = project.update(cx, |project, cx| {
+                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
+                });
+                let buffer = cx.new(|cx| {
+                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
+                });
+
+                workspace.add_item_to_active_pane(
+                    Box::new(cx.new(|cx| {
+                        let mut editor = Editor::for_multibuffer(
+                            buffer,
+                            Some(project.clone()),
+                            true,
+                            window,
+                            cx,
+                        );
+                        editor.set_breadcrumb_header(thread_summary);
+                        editor
+                    })),
+                    None,
+                    true,
+                    window,
+                    cx,
+                );
+
+                anyhow::Ok(())
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
     fn handle_assistant_configuration_event(
         &mut self,
         _entity: &Entity<AssistantConfiguration>,
@@ -1011,6 +1078,7 @@ impl Render for AssistantPanel {
             .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
                 this.open_history(window, cx);
             }))
+            .on_action(cx.listener(Self::open_active_thread_as_markdown))
             .on_action(cx.listener(Self::deploy_prompt_library))
             .child(self.render_toolbar(cx))
             .map(|parent| match self.active_view {

crates/assistant2/src/thread.rs 🔗

@@ -1,3 +1,4 @@
+use std::io::Write;
 use std::sync::Arc;
 
 use anyhow::{Context as _, Result};
@@ -794,6 +795,50 @@ impl Thread {
             false
         }
     }
+
+    pub fn to_markdown(&self) -> Result<String> {
+        let mut markdown = Vec::new();
+
+        for message in self.messages() {
+            writeln!(
+                markdown,
+                "## {role}\n",
+                role = match message.role {
+                    Role::User => "User",
+                    Role::Assistant => "Assistant",
+                    Role::System => "System",
+                }
+            )?;
+            writeln!(markdown, "{}\n", message.text)?;
+
+            for tool_use in self.tool_uses_for_message(message.id) {
+                writeln!(
+                    markdown,
+                    "**Use Tool: {} ({})**",
+                    tool_use.name, tool_use.id
+                )?;
+                writeln!(markdown, "```json")?;
+                writeln!(
+                    markdown,
+                    "{}",
+                    serde_json::to_string_pretty(&tool_use.input)?
+                )?;
+                writeln!(markdown, "```")?;
+            }
+
+            for tool_result in self.tool_results_for_message(message.id) {
+                write!(markdown, "**Tool Results: {}", tool_result.tool_use_id)?;
+                if tool_result.is_error {
+                    write!(markdown, " (Error)")?;
+                }
+
+                writeln!(markdown, "**\n")?;
+                writeln!(markdown, "{}", tool_result.content)?;
+            }
+        }
+
+        Ok(String::from_utf8_lossy(&markdown).to_string())
+    }
 }
 
 #[derive(Debug, Clone)]