From f735c90c3fa5c15c3c02711b9b9d07b1d51309dc Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 28 Apr 2025 09:09:19 -0300 Subject: [PATCH] agent: Bring title editing back to text threads (#29425) This also fixes a little UI bug where the text thread title would push the buttons away from the UI when there was still space. Release Notes: - agent: Made text thread titles editable again. --------- Co-authored-by: Michael Sloan --- crates/agent/src/assistant_panel.rs | 134 ++++++++++++++++-- crates/assistant/src/assistant_panel.rs | 2 +- .../assistant_context_editor/src/context.rs | 42 ++++-- .../src/context_editor.rs | 12 +- 4 files changed, 157 insertions(+), 33 deletions(-) diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs index 404702c0b2a09b245e1b7252a1acf6195294fd07..ce96388dfffecad0e19dac9261f75adee2a08f07 100644 --- a/crates/agent/src/assistant_panel.rs +++ b/crates/agent/src/assistant_panel.rs @@ -5,8 +5,9 @@ use std::time::Duration; use anyhow::{Result, anyhow}; use assistant_context_editor::{ - AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider, - humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens, + AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent, + SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate, + render_remaining_tokens, }; use assistant_settings::{AssistantDockPosition, AssistantSettings}; use assistant_slash_command::SlashCommandWorkingSet; @@ -116,6 +117,8 @@ enum ActiveView { }, PromptEditor { context_editor: Entity, + title_editor: Entity, + _subscriptions: Vec, }, History, Configuration, @@ -176,6 +179,83 @@ impl ActiveView { _subscriptions: subscriptions, } } + + pub fn prompt_editor( + context_editor: Entity, + window: &mut Window, + cx: &mut App, + ) -> Self { + let title = context_editor.read(cx).title(cx).to_string(); + + let editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_text(title, window, cx); + editor + }); + + // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would + // cause a custom summary to be set. The presence of this custom summary would cause + // summarization to not happen. + let mut suppress_first_edit = true; + + let subscriptions = vec![ + window.subscribe(&editor, cx, { + { + let context_editor = context_editor.clone(); + move |editor, event, window, cx| match event { + EditorEvent::BufferEdited => { + if suppress_first_edit { + suppress_first_edit = false; + return; + } + let new_summary = editor.read(cx).text(cx); + + context_editor.update(cx, |context_editor, cx| { + context_editor + .context() + .update(cx, |assistant_context, cx| { + assistant_context.set_custom_summary(new_summary, cx); + }) + }) + } + EditorEvent::Blurred => { + if editor.read(cx).text(cx).is_empty() { + let summary = context_editor + .read(cx) + .context() + .read(cx) + .summary_or_default(); + + editor.update(cx, |editor, cx| { + editor.set_text(summary, window, cx); + }); + } + } + _ => {} + } + } + }), + window.subscribe(&context_editor.read(cx).context().clone(), cx, { + let editor = editor.clone(); + move |assistant_context, event, window, cx| match event { + ContextEvent::SummaryGenerated => { + let summary = assistant_context.read(cx).summary_or_default(); + + editor.update(cx, |editor, cx| { + editor.set_text(summary, window, cx); + }) + } + _ => {} + } + }), + ]; + + Self::PromptEditor { + context_editor, + title_editor: editor, + _subscriptions: subscriptions, + } + } } pub struct AssistantPanel { @@ -502,9 +582,7 @@ impl AssistantPanel { }); self.set_active_view( - ActiveView::PromptEditor { - context_editor: context_editor.clone(), - }, + ActiveView::prompt_editor(context_editor.clone(), window, cx), window, cx, ); @@ -578,10 +656,9 @@ impl AssistantPanel { cx, ) }); + this.set_active_view( - ActiveView::PromptEditor { - context_editor: editor, - }, + ActiveView::prompt_editor(editor.clone(), window, cx), window, cx, ); @@ -821,7 +898,7 @@ impl AssistantPanel { pub(crate) fn active_context_editor(&self) -> Option> { match &self.active_view { - ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()), + ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()), _ => None, } } @@ -864,7 +941,7 @@ impl Focusable for AssistantPanel { match &self.active_view { ActiveView::Thread { .. } => self.message_editor.focus_handle(cx), ActiveView::History => self.history.focus_handle(cx), - ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx), + ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -988,9 +1065,34 @@ impl AssistantPanel { .into_any_element() } } - ActiveView::PromptEditor { context_editor } => { - let title = SharedString::from(context_editor.read(cx).title(cx).to_string()); - Label::new(title).ml_2().truncate().into_any_element() + ActiveView::PromptEditor { + title_editor, + context_editor, + .. + } => { + let context_editor = context_editor.read(cx); + let summary = context_editor.context().read(cx).summary(); + + match summary { + None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone()) + .truncate() + .ml_2() + .into_any_element(), + Some(summary) => { + if summary.done { + div() + .ml_2() + .w_full() + .child(title_editor.clone()) + .into_any_element() + } else { + Label::new(LOADING_SUMMARY_PLACEHOLDER) + .ml_2() + .truncate() + .into_any_element() + } + } + } } ActiveView::History => Label::new("History").truncate().into_any_element(), ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(), @@ -1263,7 +1365,7 @@ impl AssistantPanel { Some(token_count) } - ActiveView::PromptEditor { context_editor } => { + ActiveView::PromptEditor { context_editor, .. } => { let element = render_remaining_tokens(context_editor, cx)?; Some(element.into_any_element()) @@ -1871,7 +1973,9 @@ impl Render for AssistantPanel { .child(h_flex().child(self.message_editor.clone())) .children(self.render_last_error(cx)), ActiveView::History => parent.child(self.history.clone()), - ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()), + ActiveView::PromptEditor { context_editor, .. } => { + parent.child(context_editor.clone()) + } ActiveView::Configuration => parent.children(self.configuration.clone()), }) } diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d58ef2d58edcf3becfd42908a258b3670d25ab75..7a8717b5909224c8f2804420185767aaa3b8ec0d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -476,7 +476,7 @@ impl AssistantPanel { { return; } - context.custom_summary(new_summary, cx) + context.set_custom_summary(new_summary, cx) }); }); } diff --git a/crates/assistant_context_editor/src/context.rs b/crates/assistant_context_editor/src/context.rs index 3c8548022ed3a3837ba380737dbdabcc4a3d9476..37a0327d295e3583ffae827e5d1eb0c57e73d9ee 100644 --- a/crates/assistant_context_editor/src/context.rs +++ b/crates/assistant_context_editor/src/context.rs @@ -459,6 +459,7 @@ pub enum ContextEvent { ShowMaxMonthlySpendReachedError, MessagesEdited, SummaryChanged, + SummaryGenerated, StreamedCompletion, StartedThoughtProcess(Range), EndedThoughtProcess(language::Anchor), @@ -482,7 +483,7 @@ pub enum ContextEvent { #[derive(Clone, Default, Debug)] pub struct ContextSummary { pub text: String, - done: bool, + pub done: bool, timestamp: clock::Lamport, } @@ -640,7 +641,7 @@ pub struct AssistantContext { contents: Vec, messages_metadata: HashMap, summary: Option, - pending_summary: Task>, + summary_task: Task>, completion_count: usize, pending_completions: Vec, token_count: Option, @@ -741,7 +742,7 @@ impl AssistantContext { thought_process_output_sections: Vec::new(), edits_since_last_parse: edits_since_last_slash_command_parse, summary: None, - pending_summary: Task::ready(None), + summary_task: Task::ready(None), completion_count: Default::default(), pending_completions: Default::default(), token_count: None, @@ -951,7 +952,7 @@ impl AssistantContext { fn flush_ops(&mut self, cx: &mut Context) { let mut changed_messages = HashSet::default(); - let mut summary_changed = false; + let mut summary_generated = false; self.pending_ops.sort_unstable_by_key(|op| op.timestamp()); for op in mem::take(&mut self.pending_ops) { @@ -993,7 +994,7 @@ impl AssistantContext { .map_or(true, |summary| new_summary.timestamp > summary.timestamp) { self.summary = Some(new_summary); - summary_changed = true; + summary_generated = true; } } ContextOperation::SlashCommandStarted { @@ -1072,8 +1073,9 @@ impl AssistantContext { cx.notify(); } - if summary_changed { + if summary_generated { cx.emit(ContextEvent::SummaryChanged); + cx.emit(ContextEvent::SummaryGenerated); cx.notify(); } } @@ -2947,7 +2949,7 @@ impl AssistantContext { self.message_anchors.insert(insertion_ix, new_anchor); } - pub fn summarize(&mut self, replace_old: bool, cx: &mut Context) { + pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context) { let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else { return; }; @@ -2967,7 +2969,18 @@ impl AssistantContext { cache: false, }); - self.pending_summary = cx.spawn(async move |this, cx| { + // If there is no summary, it is set with `done: false` so that "Loading Summary…" can + // be displayed. + if self.summary.is_none() { + self.summary = Some(ContextSummary { + text: "".to_string(), + done: false, + timestamp: clock::Lamport::default(), + }); + replace_old = true; + } + + self.summary_task = cx.spawn(async move |this, cx| { async move { let stream = model.model.stream_completion_text(request, &cx); let mut messages = stream.await?; @@ -2992,6 +3005,7 @@ impl AssistantContext { }; this.push_op(operation, cx); cx.emit(ContextEvent::SummaryChanged); + cx.emit(ContextEvent::SummaryGenerated); })?; // Stop if the LLM generated multiple lines. @@ -3012,6 +3026,7 @@ impl AssistantContext { }; this.push_op(operation, cx); cx.emit(ContextEvent::SummaryChanged); + cx.emit(ContextEvent::SummaryGenerated); } })?; @@ -3184,7 +3199,7 @@ impl AssistantContext { }); } - pub fn custom_summary(&mut self, custom_summary: String, cx: &mut Context) { + pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context) { let timestamp = self.next_timestamp(); let summary = self.summary.get_or_insert(ContextSummary::default()); summary.timestamp = timestamp; @@ -3192,6 +3207,15 @@ impl AssistantContext { summary.text = custom_summary; cx.emit(ContextEvent::SummaryChanged); } + + pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread"); + + pub fn summary_or_default(&self) -> SharedString { + self.summary + .as_ref() + .map(|summary| summary.text.clone().into()) + .unwrap_or(Self::DEFAULT_SUMMARY) + } } fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range) -> String { diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index 840ed0eda34e2286ec684da82a8d6ef60faec892..b2dd4c95a10708def0127080c4ca6b6884734319 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -48,7 +48,7 @@ use project::{Project, Worktree}; use rope::Point; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore, update_settings_file}; -use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration}; +use std::{any::TypeId, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration}; use text::SelectionGoal; use ui::{ ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, @@ -618,6 +618,7 @@ impl ContextEditor { context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); }); } + ContextEvent::SummaryGenerated => {} ContextEvent::StartedThoughtProcess(range) => { let creases = self.insert_thought_process_output_sections( [( @@ -2179,13 +2180,8 @@ impl ContextEditor { }); } - pub fn title(&self, cx: &App) -> Cow { - self.context - .read(cx) - .summary() - .map(|summary| summary.text.clone()) - .map(Cow::Owned) - .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE)) + pub fn title(&self, cx: &App) -> SharedString { + self.context.read(cx).summary_or_default() } fn render_patch_block(