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 <mgsloan@gmail.com>
This commit is contained in:
parent
ddfeb202a3
commit
f735c90c3f
4 changed files with 157 additions and 33 deletions
|
@ -5,8 +5,9 @@ use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use assistant_context_editor::{
|
use assistant_context_editor::{
|
||||||
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
|
AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
|
||||||
humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
|
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
|
||||||
|
render_remaining_tokens,
|
||||||
};
|
};
|
||||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||||
use assistant_slash_command::SlashCommandWorkingSet;
|
use assistant_slash_command::SlashCommandWorkingSet;
|
||||||
|
@ -116,6 +117,8 @@ enum ActiveView {
|
||||||
},
|
},
|
||||||
PromptEditor {
|
PromptEditor {
|
||||||
context_editor: Entity<ContextEditor>,
|
context_editor: Entity<ContextEditor>,
|
||||||
|
title_editor: Entity<Editor>,
|
||||||
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
},
|
},
|
||||||
History,
|
History,
|
||||||
Configuration,
|
Configuration,
|
||||||
|
@ -176,6 +179,83 @@ impl ActiveView {
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prompt_editor(
|
||||||
|
context_editor: Entity<ContextEditor>,
|
||||||
|
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 {
|
pub struct AssistantPanel {
|
||||||
|
@ -502,9 +582,7 @@ impl AssistantPanel {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.set_active_view(
|
self.set_active_view(
|
||||||
ActiveView::PromptEditor {
|
ActiveView::prompt_editor(context_editor.clone(), window, cx),
|
||||||
context_editor: context_editor.clone(),
|
|
||||||
},
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -578,10 +656,9 @@ impl AssistantPanel {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.set_active_view(
|
this.set_active_view(
|
||||||
ActiveView::PromptEditor {
|
ActiveView::prompt_editor(editor.clone(), window, cx),
|
||||||
context_editor: editor,
|
|
||||||
},
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -821,7 +898,7 @@ impl AssistantPanel {
|
||||||
|
|
||||||
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
||||||
match &self.active_view {
|
match &self.active_view {
|
||||||
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
|
ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -864,7 +941,7 @@ impl Focusable for AssistantPanel {
|
||||||
match &self.active_view {
|
match &self.active_view {
|
||||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||||
ActiveView::History => self.history.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 => {
|
ActiveView::Configuration => {
|
||||||
if let Some(configuration) = self.configuration.as_ref() {
|
if let Some(configuration) = self.configuration.as_ref() {
|
||||||
configuration.focus_handle(cx)
|
configuration.focus_handle(cx)
|
||||||
|
@ -988,9 +1065,34 @@ impl AssistantPanel {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActiveView::PromptEditor { context_editor } => {
|
ActiveView::PromptEditor {
|
||||||
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
|
title_editor,
|
||||||
Label::new(title).ml_2().truncate().into_any_element()
|
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::History => Label::new("History").truncate().into_any_element(),
|
||||||
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
|
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
|
||||||
|
@ -1263,7 +1365,7 @@ impl AssistantPanel {
|
||||||
|
|
||||||
Some(token_count)
|
Some(token_count)
|
||||||
}
|
}
|
||||||
ActiveView::PromptEditor { context_editor } => {
|
ActiveView::PromptEditor { context_editor, .. } => {
|
||||||
let element = render_remaining_tokens(context_editor, cx)?;
|
let element = render_remaining_tokens(context_editor, cx)?;
|
||||||
|
|
||||||
Some(element.into_any_element())
|
Some(element.into_any_element())
|
||||||
|
@ -1871,7 +1973,9 @@ impl Render for AssistantPanel {
|
||||||
.child(h_flex().child(self.message_editor.clone()))
|
.child(h_flex().child(self.message_editor.clone()))
|
||||||
.children(self.render_last_error(cx)),
|
.children(self.render_last_error(cx)),
|
||||||
ActiveView::History => parent.child(self.history.clone()),
|
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()),
|
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -476,7 +476,7 @@ impl AssistantPanel {
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.custom_summary(new_summary, cx)
|
context.set_custom_summary(new_summary, cx)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -459,6 +459,7 @@ pub enum ContextEvent {
|
||||||
ShowMaxMonthlySpendReachedError,
|
ShowMaxMonthlySpendReachedError,
|
||||||
MessagesEdited,
|
MessagesEdited,
|
||||||
SummaryChanged,
|
SummaryChanged,
|
||||||
|
SummaryGenerated,
|
||||||
StreamedCompletion,
|
StreamedCompletion,
|
||||||
StartedThoughtProcess(Range<language::Anchor>),
|
StartedThoughtProcess(Range<language::Anchor>),
|
||||||
EndedThoughtProcess(language::Anchor),
|
EndedThoughtProcess(language::Anchor),
|
||||||
|
@ -482,7 +483,7 @@ pub enum ContextEvent {
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ContextSummary {
|
pub struct ContextSummary {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
done: bool,
|
pub done: bool,
|
||||||
timestamp: clock::Lamport,
|
timestamp: clock::Lamport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -640,7 +641,7 @@ pub struct AssistantContext {
|
||||||
contents: Vec<Content>,
|
contents: Vec<Content>,
|
||||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||||
summary: Option<ContextSummary>,
|
summary: Option<ContextSummary>,
|
||||||
pending_summary: Task<Option<()>>,
|
summary_task: Task<Option<()>>,
|
||||||
completion_count: usize,
|
completion_count: usize,
|
||||||
pending_completions: Vec<PendingCompletion>,
|
pending_completions: Vec<PendingCompletion>,
|
||||||
token_count: Option<usize>,
|
token_count: Option<usize>,
|
||||||
|
@ -741,7 +742,7 @@ impl AssistantContext {
|
||||||
thought_process_output_sections: Vec::new(),
|
thought_process_output_sections: Vec::new(),
|
||||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||||
summary: None,
|
summary: None,
|
||||||
pending_summary: Task::ready(None),
|
summary_task: Task::ready(None),
|
||||||
completion_count: Default::default(),
|
completion_count: Default::default(),
|
||||||
pending_completions: Default::default(),
|
pending_completions: Default::default(),
|
||||||
token_count: None,
|
token_count: None,
|
||||||
|
@ -951,7 +952,7 @@ impl AssistantContext {
|
||||||
|
|
||||||
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
|
fn flush_ops(&mut self, cx: &mut Context<AssistantContext>) {
|
||||||
let mut changed_messages = HashSet::default();
|
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());
|
self.pending_ops.sort_unstable_by_key(|op| op.timestamp());
|
||||||
for op in mem::take(&mut self.pending_ops) {
|
for op in mem::take(&mut self.pending_ops) {
|
||||||
|
@ -993,7 +994,7 @@ impl AssistantContext {
|
||||||
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
||||||
{
|
{
|
||||||
self.summary = Some(new_summary);
|
self.summary = Some(new_summary);
|
||||||
summary_changed = true;
|
summary_generated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextOperation::SlashCommandStarted {
|
ContextOperation::SlashCommandStarted {
|
||||||
|
@ -1072,8 +1073,9 @@ impl AssistantContext {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
if summary_changed {
|
if summary_generated {
|
||||||
cx.emit(ContextEvent::SummaryChanged);
|
cx.emit(ContextEvent::SummaryChanged);
|
||||||
|
cx.emit(ContextEvent::SummaryGenerated);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2947,7 +2949,7 @@ impl AssistantContext {
|
||||||
self.message_anchors.insert(insertion_ix, new_anchor);
|
self.message_anchors.insert(insertion_ix, new_anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
|
pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
|
||||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -2967,7 +2969,18 @@ impl AssistantContext {
|
||||||
cache: false,
|
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 {
|
async move {
|
||||||
let stream = model.model.stream_completion_text(request, &cx);
|
let stream = model.model.stream_completion_text(request, &cx);
|
||||||
let mut messages = stream.await?;
|
let mut messages = stream.await?;
|
||||||
|
@ -2992,6 +3005,7 @@ impl AssistantContext {
|
||||||
};
|
};
|
||||||
this.push_op(operation, cx);
|
this.push_op(operation, cx);
|
||||||
cx.emit(ContextEvent::SummaryChanged);
|
cx.emit(ContextEvent::SummaryChanged);
|
||||||
|
cx.emit(ContextEvent::SummaryGenerated);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Stop if the LLM generated multiple lines.
|
// Stop if the LLM generated multiple lines.
|
||||||
|
@ -3012,6 +3026,7 @@ impl AssistantContext {
|
||||||
};
|
};
|
||||||
this.push_op(operation, cx);
|
this.push_op(operation, cx);
|
||||||
cx.emit(ContextEvent::SummaryChanged);
|
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<Self>) {
|
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||||
let timestamp = self.next_timestamp();
|
let timestamp = self.next_timestamp();
|
||||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||||
summary.timestamp = timestamp;
|
summary.timestamp = timestamp;
|
||||||
|
@ -3192,6 +3207,15 @@ impl AssistantContext {
|
||||||
summary.text = custom_summary;
|
summary.text = custom_summary;
|
||||||
cx.emit(ContextEvent::SummaryChanged);
|
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<text::Anchor>) -> String {
|
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
|
||||||
|
|
|
@ -48,7 +48,7 @@ use project::{Project, Worktree};
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore, update_settings_file};
|
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 text::SelectionGoal;
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||||
|
@ -618,6 +618,7 @@ impl ContextEditor {
|
||||||
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
|
context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
ContextEvent::SummaryGenerated => {}
|
||||||
ContextEvent::StartedThoughtProcess(range) => {
|
ContextEvent::StartedThoughtProcess(range) => {
|
||||||
let creases = self.insert_thought_process_output_sections(
|
let creases = self.insert_thought_process_output_sections(
|
||||||
[(
|
[(
|
||||||
|
@ -2179,13 +2180,8 @@ impl ContextEditor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title(&self, cx: &App) -> Cow<str> {
|
pub fn title(&self, cx: &App) -> SharedString {
|
||||||
self.context
|
self.context.read(cx).summary_or_default()
|
||||||
.read(cx)
|
|
||||||
.summary()
|
|
||||||
.map(|summary| summary.text.clone())
|
|
||||||
.map(Cow::Owned)
|
|
||||||
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_patch_block(
|
fn render_patch_block(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue