From 79874872cbaa780b92b29f53b564dfa8cce477ef Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 13 Mar 2025 12:39:01 -0400 Subject: [PATCH] assistant2: Add ability to open the active thread as Markdown (#26690) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a new `assistant2: open active thread as markdown` action that opens up the active thread in a Markdown representation: Screenshot 2025-03-13 at 12 25 33 PM Release Notes: - N/A --- 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(-) diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index a7cb831b53348b4597f73dcdf61ed73823abf736..33da6d7a753af3d0e2cef0634256c4c196cc6040 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -53,7 +53,8 @@ actions!( FocusLeft, FocusRight, RemoveFocusedContext, - AcceptSuggestedContext + AcceptSuggestedContext, + OpenActiveThreadAsMarkdown ] ); diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 3b9c45bec0a260c0055dc51f63c9e02ec120bbe9..266ccb96ffc7598ea80a99f0d127b516e26689bd 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/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, + ) { + 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, @@ -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 { diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs index 397dff50e185a7dacff0011ddd5dd717e20c0f77..ae2ffa35029c26853976f11ce2ba7a19adabc6ee 100644 --- a/crates/assistant2/src/thread.rs +++ b/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 { + 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)]