@@ -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<ContextEditor>,
+ title_editor: Entity<Editor>,
+ _subscriptions: Vec<gpui::Subscription>,
},
History,
Configuration,
@@ -176,6 +179,83 @@ impl ActiveView {
_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 {
@@ -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<Entity<ContextEditor>> {
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()),
})
}
@@ -459,6 +459,7 @@ pub enum ContextEvent {
ShowMaxMonthlySpendReachedError,
MessagesEdited,
SummaryChanged,
+ SummaryGenerated,
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
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<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
summary: Option<ContextSummary>,
- pending_summary: Task<Option<()>>,
+ summary_task: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
token_count: Option<usize>,
@@ -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<AssistantContext>) {
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<Self>) {
+ pub fn summarize(&mut self, mut replace_old: bool, cx: &mut Context<Self>) {
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<Self>) {
+ pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
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<text::Anchor>) -> String {
@@ -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<str> {
- 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(