diff --git a/assets/icons/countdown_timer.svg b/assets/icons/countdown_timer.svg
new file mode 100644
index 0000000000..b9b7479228
--- /dev/null
+++ b/assets/icons/countdown_timer.svg
@@ -0,0 +1 @@
+
diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs
index 46eeb4c095..ae436df59f 100644
--- a/crates/assistant/src/assistant.rs
+++ b/crates/assistant/src/assistant.rs
@@ -6,11 +6,8 @@ mod prompts;
mod saved_conversation;
mod streaming_diff;
-mod embedded_scope;
-
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel};
-use chrono::{DateTime, Local};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
@@ -26,7 +23,6 @@ use std::{
actions!(
assistant,
[
- NewConversation,
Assist,
Split,
CycleMessageRole,
@@ -35,6 +31,7 @@ actions!(
ResetKey,
InlineAssist,
ToggleIncludeConversation,
+ ToggleHistory,
]
);
@@ -93,8 +90,8 @@ impl LanguageModel {
pub fn display_name(&self) -> String {
match self {
- LanguageModel::OpenAi(model) => format!("openai/{}", model.display_name()),
- LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.display_name()),
+ LanguageModel::OpenAi(model) => model.display_name().into(),
+ LanguageModel::ZedDotDev(model) => model.display_name().into(),
}
}
@@ -178,7 +175,6 @@ pub struct LanguageModelChoiceDelta {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
- sent_at: DateTime,
status: MessageStatus,
}
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index 118b59bc04..d090c96357 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -1,15 +1,13 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind},
- embedded_scope::EmbeddedScope,
prompts::generate_content_prompt,
Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel,
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
- NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
- SavedMessage, Split, ToggleFocus, ToggleIncludeConversation,
+ QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
+ Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
};
use anyhow::{anyhow, Result};
-use chrono::{DateTime, Local};
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
actions::{MoveDown, MoveUp},
@@ -17,21 +15,24 @@ use editor::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
},
scroll::{Autoscroll, AutoscrollStrategy},
- Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MultiBufferSnapshot,
- RowExt, ToOffset as _, ToPoint,
+ Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, RowExt,
+ ToOffset as _, ToPoint,
};
use file_icons::FileIcons;
use fs::Fs;
use futures::StreamExt;
use gpui::{
- canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
- AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter,
- FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement,
- IntoElement, Model, ModelContext, ParentElement, Pixels, Render, SharedString,
- StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle,
- View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
+ canvas, div, point, relative, rems, uniform_list, Action, AnyView, AppContext, AsyncAppContext,
+ AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
+ FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model,
+ ModelContext, ParentElement, Pixels, Render, SharedString, StatefulInteractiveElement, Styled,
+ Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
+ WeakModel, WeakView, WhiteSpace, WindowContext,
+};
+use language::{
+ language_settings::SoftWrap, Buffer, BufferSnapshot, DiagnosticEntry, LanguageRegistry, Point,
+ ToOffset as _,
};
-use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, Point, ToOffset as _};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::Project;
@@ -40,19 +41,17 @@ use settings::Settings;
use std::{cmp, fmt::Write, iter, ops::Range, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::AssistantKind;
use theme::ThemeSettings;
-use ui::{
- prelude::*,
- utils::{DateTimeType, FormatDistance},
- ButtonLike, Tab, TabBar, Tooltip,
-};
+use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Tab, TabBar, Tooltip};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
-use workspace::notifications::NotificationId;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
searchable::Direction,
Event as WorkspaceEvent, Save, Toast, ToggleZoom, Toolbar, Workspace,
};
+use workspace::{notifications::NotificationId, NewFile};
+
+const MAX_RECENT_BUFFERS: usize = 3;
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
@@ -676,7 +675,7 @@ impl AssistantPanel {
messages.extend(
conversation
.messages(cx)
- .map(|message| message.to_open_ai_message(buffer)),
+ .map(|message| message.to_request_message(buffer)),
);
}
let model = self.model.clone();
@@ -853,6 +852,18 @@ impl AssistantPanel {
}
}
+ fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext) {
+ self.show_saved_conversations = !self.show_saved_conversations;
+ cx.notify();
+ }
+
+ fn show_history(&mut self, cx: &mut ViewContext) {
+ if !self.show_saved_conversations {
+ self.show_saved_conversations = true;
+ cx.notify();
+ }
+ }
+
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) {
let mut propagate = true;
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() {
@@ -907,36 +918,74 @@ impl AssistantPanel {
Some(&self.active_conversation_editor.as_ref()?.editor)
}
- fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement {
- IconButton::new("hamburger_button", IconName::Menu)
- .on_click(cx.listener(|this, _event, cx| {
- this.show_saved_conversations = !this.show_saved_conversations;
- cx.notify();
- }))
- .tooltip(|cx| Tooltip::text("Conversation History", cx))
+ fn render_popover_button(&self, cx: &mut ViewContext) -> impl IntoElement {
+ let assistant = cx.view().clone();
+ let zoomed = self.zoomed;
+ popover_menu("assistant-popover")
+ .trigger(IconButton::new("trigger", IconName::Menu))
+ .menu(move |cx| {
+ let assistant = assistant.clone();
+ ContextMenu::build(cx, |menu, _cx| {
+ menu.entry(
+ if zoomed { "Zoom Out" } else { "Zoom In" },
+ Some(Box::new(ToggleZoom)),
+ {
+ let assistant = assistant.clone();
+ move |cx| {
+ assistant.focus_handle(cx).dispatch_action(&ToggleZoom, cx);
+ }
+ },
+ )
+ .entry("New Context", Some(Box::new(NewFile)), {
+ let assistant = assistant.clone();
+ move |cx| {
+ assistant.focus_handle(cx).dispatch_action(&NewFile, cx);
+ }
+ })
+ .entry("History", Some(Box::new(ToggleHistory)), {
+ let assistant = assistant.clone();
+ move |cx| assistant.update(cx, |assistant, cx| assistant.show_history(cx))
+ })
+ })
+ .into()
+ })
}
- fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec {
- if self.active_conversation_editor().is_some() {
- vec![
- Self::render_split_button(cx).into_any_element(),
- Self::render_quote_button(cx).into_any_element(),
- Self::render_assist_button(cx).into_any_element(),
- ]
- } else {
- Default::default()
- }
- }
+ fn render_inject_context_menu(&self, _cx: &mut ViewContext) -> impl Element {
+ let workspace = self.workspace.clone();
- fn render_split_button(cx: &mut ViewContext) -> impl IntoElement {
- IconButton::new("split_button", IconName::Snip)
- .on_click(cx.listener(|this, _event, cx| {
- if let Some(active_editor) = this.active_conversation_editor() {
- active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
- }
+ popover_menu("inject-context-menu")
+ .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
+ // Tooltip::with_meta("Insert Context", None, "Type # to insert via keyboard", cx)
+ Tooltip::text("Insert Context", cx)
}))
- .icon_size(IconSize::Small)
- .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
+ .menu(move |cx| {
+ ContextMenu::build(cx, |menu, _cx| {
+ // menu.entry("Insert Search", None, {
+ // let assistant = assistant.clone();
+ // move |_cx| {}
+ // })
+ // .entry("Insert Docs", None, {
+ // let assistant = assistant.clone();
+ // move |cx| {}
+ // })
+ menu.entry("Quote Selection", None, {
+ let workspace = workspace.clone();
+ move |cx| {
+ workspace
+ .update(cx, |workspace, cx| {
+ ConversationEditor::quote_selection(
+ workspace,
+ &Default::default(),
+ cx,
+ )
+ })
+ .ok();
+ }
+ })
+ })
+ .into()
+ })
}
fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement {
@@ -950,44 +999,6 @@ impl AssistantPanel {
.tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
}
- fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement {
- IconButton::new("quote_button", IconName::Quote)
- .on_click(cx.listener(|this, _event, cx| {
- if let Some(workspace) = this.workspace.upgrade() {
- cx.window_context().defer(move |cx| {
- workspace.update(cx, |workspace, cx| {
- ConversationEditor::quote_selection(workspace, &Default::default(), cx)
- });
- });
- }
- }))
- .icon_size(IconSize::Small)
- .tooltip(|cx| Tooltip::for_action("Quote Selection", &QuoteSelection, cx))
- }
-
- fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement {
- IconButton::new("plus_button", IconName::Plus)
- .on_click(cx.listener(|this, _event, cx| {
- this.new_conversation(cx);
- }))
- .icon_size(IconSize::Small)
- .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
- }
-
- fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement {
- let zoomed = self.zoomed;
- IconButton::new("zoom_button", IconName::Maximize)
- .on_click(cx.listener(|this, _event, cx| {
- this.toggle_zoom(&ToggleZoom, cx);
- }))
- .selected(zoomed)
- .selected_icon(IconName::Minimize)
- .icon_size(IconSize::Small)
- .tooltip(move |cx| {
- Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
- })
- }
-
fn render_saved_conversation(
&mut self,
index: usize,
@@ -1058,9 +1069,7 @@ impl AssistantPanel {
fn render_signed_in(&mut self, cx: &mut ViewContext) -> impl IntoElement {
let header = TabBar::new("assistant_header")
- .start_child(
- h_flex().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
- )
+ .start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
.children(self.active_conversation_editor().map(|editor| {
h_flex()
.h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
@@ -1068,26 +1077,30 @@ impl AssistantPanel {
.px_2()
.child(Label::new(editor.read(cx).title(cx)).into_element())
}))
- .when(self.focus_handle.contains_focused(cx), |this| {
- this.end_child(
- h_flex()
- .gap_2()
- .when(self.active_conversation_editor().is_some(), |this| {
- this.child(h_flex().gap_1().children(self.render_editor_tools(cx)))
- .child(
- ui::Divider::vertical()
- .inset()
- .color(ui::DividerColor::Border),
- )
- })
- .child(
+ .end_child(
+ h_flex()
+ .gap_2()
+ .when_some(self.active_conversation_editor(), |this, editor| {
+ let conversation = editor.read(cx).conversation.clone();
+ this.child(
h_flex()
.gap_1()
- .child(Self::render_plus_button(cx))
- .child(self.render_zoom_button(cx)),
- ),
- )
- });
+ .child(self.render_model(&conversation, cx))
+ .children(self.render_remaining_tokens(&conversation, cx)),
+ )
+ .child(
+ ui::Divider::vertical()
+ .inset()
+ .color(ui::DividerColor::Border),
+ )
+ })
+ .child(
+ h_flex()
+ .gap_1()
+ .child(self.render_inject_context_menu(cx))
+ .child(Self::render_assist_button(cx)),
+ ),
+ );
let contents = if self.active_conversation_editor().is_some() {
let mut registrar = DivRegistrar::new(
@@ -1099,6 +1112,7 @@ impl AssistantPanel {
} else {
div()
};
+
v_flex()
.key_context("AssistantPanel")
.size_full()
@@ -1106,6 +1120,7 @@ impl AssistantPanel {
this.new_conversation(cx);
}))
.on_action(cx.listener(AssistantPanel::toggle_zoom))
+ .on_action(cx.listener(AssistantPanel::toggle_history))
.on_action(cx.listener(AssistantPanel::deploy))
.on_action(cx.listener(AssistantPanel::select_next_match))
.on_action(cx.listener(AssistantPanel::select_prev_match))
@@ -1150,20 +1165,7 @@ impl AssistantPanel {
.into_any_element()
} else if let Some(editor) = self.active_conversation_editor() {
let editor = editor.clone();
- let conversation = editor.read(cx).conversation.clone();
- div()
- .size_full()
- .child(editor.clone())
- .child(
- h_flex()
- .absolute()
- .gap_1()
- .top_3()
- .right_5()
- .child(self.render_model(&conversation, cx))
- .children(self.render_remaining_tokens(&conversation, cx)),
- )
- .into_any_element()
+ div().size_full().child(editor.clone()).into_any_element()
} else {
div().into_any_element()
},
@@ -1192,9 +1194,13 @@ impl AssistantPanel {
} else if remaining_tokens <= 500 {
Color::Warning
} else {
- Color::Default
+ Color::Muted
};
- Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color))
+ Some(
+ Label::new(remaining_tokens.to_string())
+ .size(LabelSize::Small)
+ .color(remaining_tokens_color),
+ )
}
}
@@ -1319,7 +1325,7 @@ struct Summary {
pub struct Conversation {
id: Option,
buffer: Model,
- embedded_scope: EmbeddedScope,
+ ambient_context: AmbientContext,
message_anchors: Vec,
messages_metadata: HashMap,
next_message_id: MessageId,
@@ -1335,13 +1341,40 @@ pub struct Conversation {
_subscriptions: Vec,
}
+#[derive(Default)]
+struct AmbientContext {
+ recent_buffers: RecentBuffersContext,
+}
+
+struct RecentBuffersContext {
+ enabled: bool,
+ buffers: Vec,
+ message: String,
+ pending_message: Option>,
+}
+
+struct RecentBuffer {
+ buffer: WeakModel,
+ _subscription: Subscription,
+}
+
+impl Default for RecentBuffersContext {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ buffers: Vec::new(),
+ message: String::new(),
+ pending_message: None,
+ }
+ }
+}
+
impl EventEmitter for Conversation {}
impl Conversation {
fn new(
model: LanguageModel,
language_registry: Arc,
- embedded_scope: EmbeddedScope,
cx: &mut ModelContext,
) -> Self {
let markdown = language_registry.language_for_name("Markdown");
@@ -1364,6 +1397,7 @@ impl Conversation {
message_anchors: Default::default(),
messages_metadata: Default::default(),
next_message_id: Default::default(),
+ ambient_context: AmbientContext::default(),
summary: None,
pending_summary: Task::ready(None),
completion_count: Default::default(),
@@ -1375,7 +1409,6 @@ impl Conversation {
pending_save: Task::ready(Ok(())),
path: None,
buffer,
- embedded_scope,
};
let message = MessageAnchor {
@@ -1387,7 +1420,6 @@ impl Conversation {
message.id,
MessageMetadata {
role: Role::User,
- sent_at: Local::now(),
status: MessageStatus::Done,
},
);
@@ -1460,6 +1492,7 @@ impl Conversation {
message_anchors,
messages_metadata: saved_conversation.message_metadata,
next_message_id,
+ ambient_context: AmbientContext::default(),
summary: Some(Summary {
text: saved_conversation.summary,
done: true,
@@ -1474,13 +1507,193 @@ impl Conversation {
pending_save: Task::ready(Ok(())),
path: Some(path),
buffer,
- embedded_scope: EmbeddedScope::new(),
};
this.count_remaining_tokens(cx);
this
})
}
+ fn toggle_recent_buffers(&mut self, cx: &mut ModelContext) {
+ self.ambient_context.recent_buffers.enabled = !self.ambient_context.recent_buffers.enabled;
+ self.update_recent_buffers_context(cx);
+ }
+
+ fn set_recent_buffers(
+ &mut self,
+ buffers: impl IntoIterator- >,
+ cx: &mut ModelContext,
+ ) {
+ self.ambient_context.recent_buffers.buffers.clear();
+ self.ambient_context
+ .recent_buffers
+ .buffers
+ .extend(buffers.into_iter().map(|buffer| RecentBuffer {
+ buffer: buffer.downgrade(),
+ _subscription: cx.observe(&buffer, |this, _, cx| {
+ this.update_recent_buffers_context(cx);
+ }),
+ }));
+ self.update_recent_buffers_context(cx);
+ }
+
+ fn update_recent_buffers_context(&mut self, cx: &mut ModelContext) {
+ let buffers = self
+ .ambient_context
+ .recent_buffers
+ .buffers
+ .iter()
+ .filter_map(|recent| {
+ recent
+ .buffer
+ .read_with(cx, |buffer, cx| {
+ (
+ buffer.file().map(|file| file.full_path(cx)),
+ buffer.snapshot(),
+ )
+ })
+ .ok()
+ })
+ .collect::>();
+
+ if !self.ambient_context.recent_buffers.enabled || buffers.is_empty() {
+ self.ambient_context.recent_buffers.message.clear();
+ self.ambient_context.recent_buffers.pending_message = None;
+ self.count_remaining_tokens(cx);
+ cx.notify();
+ } else {
+ self.ambient_context.recent_buffers.pending_message =
+ Some(cx.spawn(|this, mut cx| async move {
+ const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
+ cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
+
+ let message = cx
+ .background_executor()
+ .spawn(async move { Self::message_for_recent_buffers(&buffers) })
+ .await;
+ this.update(&mut cx, |this, cx| {
+ this.ambient_context.recent_buffers.message = message;
+ this.count_remaining_tokens(cx);
+ cx.notify();
+ })
+ .ok();
+ }));
+ }
+ }
+
+ fn message_for_recent_buffers(buffers: &[(Option, BufferSnapshot)]) -> String {
+ let mut message = String::new();
+ writeln!(
+ message,
+ "The following is a list of recent buffers that the user has opened."
+ )
+ .unwrap();
+ writeln!(
+ message,
+ "For every line in the buffer, I will include a row number that line corresponds to."
+ )
+ .unwrap();
+ writeln!(
+ message,
+ "Lines that don't have a number correspond to errors and warnings. For example:"
+ )
+ .unwrap();
+ writeln!(message, "path/to/file.md").unwrap();
+ writeln!(message, "```markdown").unwrap();
+ writeln!(message, "1 The quick brown fox").unwrap();
+ writeln!(message, "2 jumps over one active").unwrap();
+ writeln!(message, " --- error: should be 'the'").unwrap();
+ writeln!(message, " ------ error: should be 'lazy'").unwrap();
+ writeln!(message, "3 dog").unwrap();
+ writeln!(message, "```").unwrap();
+
+ message.push('\n');
+ writeln!(message, "Here's the actual recent buffer list:").unwrap();
+ for (path, buffer) in buffers {
+ if let Some(path) = path {
+ writeln!(message, "{}", path.display()).unwrap();
+ } else {
+ writeln!(message, "untitled").unwrap();
+ }
+
+ if let Some(language) = buffer.language() {
+ writeln!(message, "```{}", language.name().to_lowercase()).unwrap();
+ } else {
+ writeln!(message, "```").unwrap();
+ }
+
+ let mut diagnostics = buffer
+ .diagnostics_in_range::<_, Point>(
+ language::Anchor::MIN..language::Anchor::MAX,
+ false,
+ )
+ .peekable();
+
+ let mut active_diagnostics = Vec::>::new();
+ const GUTTER_PADDING: usize = 4;
+ let gutter_width =
+ ((buffer.max_point().row + 1) as f32).log10() as usize + 1 + GUTTER_PADDING;
+ for buffer_row in 0..=buffer.max_point().row {
+ let display_row = buffer_row + 1;
+ active_diagnostics.retain(|diagnostic| {
+ (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
+ });
+ while diagnostics.peek().map_or(false, |diagnostic| {
+ (diagnostic.range.start.row..=diagnostic.range.end.row).contains(&buffer_row)
+ }) {
+ active_diagnostics.push(diagnostics.next().unwrap());
+ }
+
+ let row_width = (display_row as f32).log10() as usize + 1;
+ write!(message, "{}", display_row).unwrap();
+ if row_width < gutter_width {
+ message.extend(iter::repeat(' ').take(gutter_width - row_width));
+ }
+
+ for chunk in buffer.text_for_range(
+ Point::new(buffer_row, 0)..Point::new(buffer_row, buffer.line_len(buffer_row)),
+ ) {
+ message.push_str(chunk);
+ }
+ message.push('\n');
+
+ for diagnostic in &active_diagnostics {
+ message.extend(iter::repeat(' ').take(gutter_width));
+
+ let start_column = if diagnostic.range.start.row == buffer_row {
+ message
+ .extend(iter::repeat(' ').take(diagnostic.range.start.column as usize));
+ diagnostic.range.start.column
+ } else {
+ 0
+ };
+ let end_column = if diagnostic.range.end.row == buffer_row {
+ diagnostic.range.end.column
+ } else {
+ buffer.line_len(buffer_row)
+ };
+
+ message.extend(iter::repeat('-').take((end_column - start_column) as usize));
+ writeln!(message, " {}", diagnostic.diagnostic.message).unwrap();
+ }
+ }
+
+ message.push('\n');
+ }
+
+ writeln!(
+ message,
+ "When quoting the above code, mention which rows the code occurs at."
+ )
+ .unwrap();
+ writeln!(
+ message,
+ "Never include rows in the quoted code itself and only report lines that didn't start with a row number."
+ )
+ .unwrap();
+
+ message
+ }
+
fn handle_buffer_event(
&mut self,
_: Model,
@@ -1656,20 +1869,27 @@ impl Conversation {
}
fn to_completion_request(&self, cx: &mut ModelContext) -> LanguageModelRequest {
- let mut request = LanguageModelRequest {
+ let messages = self
+ .ambient_context
+ .recent_buffers
+ .enabled
+ .then(|| LanguageModelRequestMessage {
+ role: Role::System,
+ content: self.ambient_context.recent_buffers.message.clone(),
+ })
+ .into_iter()
+ .chain(
+ self.messages(cx)
+ .filter(|message| matches!(message.status, MessageStatus::Done))
+ .map(|message| message.to_request_message(self.buffer.read(cx))),
+ );
+
+ LanguageModelRequest {
model: self.model.clone(),
- messages: self
- .messages(cx)
- .filter(|message| matches!(message.status, MessageStatus::Done))
- .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
- .collect(),
+ messages: messages.collect(),
stop: vec![],
temperature: 1.0,
- };
-
- let context_message = self.embedded_scope.message(cx);
- request.messages.extend(context_message);
- request
+ }
}
fn cancel_last_assist(&mut self) -> bool {
@@ -1721,14 +1941,8 @@ impl Conversation {
};
self.message_anchors
.insert(next_message_ix, message.clone());
- self.messages_metadata.insert(
- message.id,
- MessageMetadata {
- role,
- sent_at: Local::now(),
- status,
- },
- );
+ self.messages_metadata
+ .insert(message.id, MessageMetadata { role, status });
cx.emit(ConversationEvent::MessagesEdited);
Some(message)
} else {
@@ -1785,7 +1999,6 @@ impl Conversation {
suffix.id,
MessageMetadata {
role,
- sent_at: Local::now(),
status: MessageStatus::Done,
},
);
@@ -1830,7 +2043,6 @@ impl Conversation {
selection.id,
MessageMetadata {
role,
- sent_at: Local::now(),
status: MessageStatus::Done,
},
);
@@ -1855,7 +2067,7 @@ impl Conversation {
let messages = self
.messages(cx)
.take(2)
- .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
+ .map(|message| message.to_request_message(self.buffer.read(cx)))
.chain(Some(LanguageModelRequestMessage {
role: Role::User,
content: "Summarize the conversation into a short title without punctuation"
@@ -1962,7 +2174,6 @@ impl Conversation {
id: message_anchor.id,
anchor: message_anchor.start,
role: metadata.role,
- sent_at: metadata.sent_at,
status: metadata.status.clone(),
});
}
@@ -2061,8 +2272,7 @@ impl ConversationEditor {
workspace: View,
cx: &mut ViewContext,
) -> Self {
- let conversation = cx
- .new_model(|cx| Conversation::new(model, language_registry, EmbeddedScope::new(), cx));
+ let conversation = cx.new_model(|cx| Conversation::new(model, language_registry, cx));
Self::for_conversation(conversation, fs, workspace, cx)
}
@@ -2096,7 +2306,7 @@ impl ConversationEditor {
workspace: workspace.downgrade(),
_subscriptions,
};
- cx.defer(|this, cx| this.update_active_buffer(workspace, cx));
+ this.update_recent_editors(cx);
this.update_message_headers(cx);
this
}
@@ -2232,32 +2442,57 @@ impl ConversationEditor {
fn handle_workspace_event(
&mut self,
- workspace: View,
+ _: View,
event: &WorkspaceEvent,
cx: &mut ViewContext,
) {
- if let WorkspaceEvent::ActiveItemChanged = event {
- self.update_active_buffer(workspace, cx);
+ match event {
+ WorkspaceEvent::ActiveItemChanged
+ | WorkspaceEvent::ItemAdded
+ | WorkspaceEvent::ItemRemoved
+ | WorkspaceEvent::PaneAdded(_)
+ | WorkspaceEvent::PaneRemoved => self.update_recent_editors(cx),
+ _ => {}
}
}
- fn update_active_buffer(
- &mut self,
- workspace: View,
- cx: &mut ViewContext<'_, ConversationEditor>,
- ) {
- let active_buffer = workspace
- .read(cx)
- .active_item(cx)
- .and_then(|item| Some(item.act_as::(cx)?.read(cx).buffer().clone()));
+ fn update_recent_editors(&mut self, cx: &mut ViewContext) {
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
+
+ let mut timestamps_by_entity_id = HashMap::default();
+ for pane in workspace.read(cx).panes() {
+ let pane = pane.read(cx);
+ for entry in pane.activation_history() {
+ timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
+ }
+ }
+
+ let mut timestamps_by_buffer = HashMap::default();
+ for editor in workspace.read(cx).items_of_type::(cx) {
+ let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
+ continue;
+ };
+
+ let new_timestamp = timestamps_by_entity_id
+ .get(&editor.entity_id())
+ .copied()
+ .unwrap_or_default();
+ let timestamp = timestamps_by_buffer.entry(buffer).or_insert(new_timestamp);
+ *timestamp = cmp::max(*timestamp, new_timestamp);
+ }
+
+ let mut recent_buffers = timestamps_by_buffer.into_iter().collect::>();
+ recent_buffers.sort_unstable_by_key(|(_, timestamp)| *timestamp);
+ if recent_buffers.len() > MAX_RECENT_BUFFERS {
+ let excess = recent_buffers.len() - MAX_RECENT_BUFFERS;
+ recent_buffers.drain(..excess);
+ }
self.conversation.update(cx, |conversation, cx| {
conversation
- .embedded_scope
- .set_active_buffer(active_buffer.clone(), cx);
-
- conversation.count_remaining_tokens(cx);
- cx.notify();
+ .set_recent_buffers(recent_buffers.into_iter().map(|(buffer, _)| buffer), cx);
});
}
@@ -2295,7 +2530,8 @@ impl ConversationEditor {
.conversation
.read(cx)
.messages(cx)
- .map(|message| BlockProperties {
+ .enumerate()
+ .map(|(ix, message)| BlockProperties {
position: buffer
.anchor_in_excerpt(excerpt_id, message.anchor)
.unwrap(),
@@ -2303,7 +2539,7 @@ impl ConversationEditor {
style: BlockStyle::Sticky,
render: Box::new({
let conversation = self.conversation.clone();
- move |_cx| {
+ move |cx| {
let message_id = message.id;
let sender = ButtonLike::new("role")
.style(ButtonStyle::Filled)
@@ -2335,22 +2571,10 @@ impl ConversationEditor {
h_flex()
.id(("message_header", message_id.0))
.h_11()
+ .w_full()
.relative()
.gap_1()
.child(sender)
- // TODO: Only show this if the message if the message has been sent
- .child(
- Label::new(
- FormatDistance::from_now(DateTimeType::Local(
- message.sent_at,
- ))
- .hide_prefix(true)
- .add_suffix(true)
- .to_string(),
- )
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
.children(
if let MessageStatus::Error(error) = message.status.clone() {
Some(
@@ -2363,6 +2587,65 @@ impl ConversationEditor {
None
},
)
+ .children((ix == 0).then(|| {
+ div()
+ .h_flex()
+ .flex_1()
+ .justify_end()
+ .pr_4()
+ .gap_1()
+ .child(
+ IconButton::new("include_file", IconName::File)
+ .icon_size(IconSize::Small)
+ .selected(
+ conversation
+ .read(cx)
+ .ambient_context
+ .recent_buffers
+ .enabled,
+ )
+ .on_click({
+ let conversation = conversation.downgrade();
+ move |_, cx| {
+ conversation
+ .update(cx, |conversation, cx| {
+ conversation
+ .toggle_recent_buffers(cx);
+ })
+ .ok();
+ }
+ })
+ .tooltip(|cx| {
+ Tooltip::text("Include Open Files", cx)
+ }),
+ )
+ // .child(
+ // IconButton::new("include_terminal", IconName::Terminal)
+ // .icon_size(IconSize::Small)
+ // .tooltip(|cx| {
+ // Tooltip::text("Include Terminal", cx)
+ // }),
+ // )
+ // .child(
+ // IconButton::new(
+ // "include_edit_history",
+ // IconName::FileGit,
+ // )
+ // .icon_size(IconSize::Small)
+ // .tooltip(
+ // |cx| Tooltip::text("Include Edit History", cx),
+ // ),
+ // )
+ // .child(
+ // IconButton::new(
+ // "include_file_trees",
+ // IconName::FileTree,
+ // )
+ // .icon_size(IconSize::Small)
+ // .tooltip(|cx| Tooltip::text("Include File Trees", cx)),
+ // )
+ .into_any()
+ }))
.into_any_element()
}
}),
@@ -2500,104 +2783,6 @@ impl ConversationEditor {
.map(|summary| summary.text.clone())
.unwrap_or_else(|| "New Conversation".into())
}
-
- fn render_embedded_scope(&self, cx: &mut ViewContext) -> Option {
- let active_buffer = self
- .conversation
- .read(cx)
- .embedded_scope
- .active_buffer()?
- .clone();
-
- Some(
- div()
- .p_4()
- .v_flex()
- .child(
- div()
- .h_flex()
- .items_center()
- .child(Icon::new(IconName::File))
- .child(
- div()
- .h_6()
- .child(Label::new("File Contexts"))
- .ml_1()
- .font_weight(FontWeight::SEMIBOLD),
- ),
- )
- .child(
- div()
- .ml_4()
- .child(self.render_active_buffer(active_buffer, cx)),
- ),
- )
- }
-
- fn render_active_buffer(
- &self,
- buffer: Model,
- cx: &mut ViewContext,
- ) -> impl Element {
- let buffer = buffer.read(cx);
- let icon_path;
- let path;
- if let Some(singleton) = buffer.as_singleton() {
- let singleton = singleton.read(cx);
-
- path = singleton.file().map(|file| file.full_path(cx));
-
- icon_path = path
- .as_ref()
- .and_then(|path| FileIcons::get_icon(path.as_path(), cx))
- .map(SharedString::from)
- .unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
- } else {
- icon_path = SharedString::from("icons/file_icons/file.svg");
- path = None;
- }
-
- let file_name = path.map_or("Untitled".to_string(), |path| {
- path.to_string_lossy().to_string()
- });
-
- let enabled = self
- .conversation
- .read(cx)
- .embedded_scope
- .active_buffer_enabled();
-
- let file_name_text_color = if enabled {
- Color::Default
- } else {
- Color::Disabled
- };
-
- div()
- .id("active-buffer")
- .h_flex()
- .cursor_pointer()
- .child(Icon::from_path(icon_path).color(file_name_text_color))
- .child(
- div()
- .h_6()
- .child(Label::new(file_name).color(file_name_text_color))
- .ml_1(),
- )
- .children(enabled.then(|| {
- div()
- .child(Icon::new(IconName::Check).color(file_name_text_color))
- .ml_1()
- }))
- .on_click(cx.listener(move |this, _, cx| {
- this.conversation.update(cx, |conversation, cx| {
- conversation
- .embedded_scope
- .set_active_buffer_enabled(!enabled);
- cx.notify();
- })
- }))
- }
}
impl EventEmitter for ConversationEditor {}
@@ -2621,7 +2806,6 @@ impl Render for ConversationEditor {
.bg(cx.theme().colors().editor_background)
.child(self.editor.clone()),
)
- .child(div().flex_shrink().children(self.render_embedded_scope(cx)))
}
}
@@ -2644,12 +2828,11 @@ pub struct Message {
id: MessageId,
anchor: language::Anchor,
role: Role,
- sent_at: DateTime,
status: MessageStatus,
}
impl Message {
- fn to_open_ai_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
+ fn to_request_message(&self, buffer: &Buffer) -> LanguageModelRequestMessage {
let content = buffer
.text_for_range(self.offset_range.clone())
.collect::();
@@ -2997,9 +3180,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let conversation = cx.new_model(|cx| {
- Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
- });
+ let conversation =
+ cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3130,9 +3312,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let conversation = cx.new_model(|cx| {
- Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
- });
+ let conversation =
+ cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3230,9 +3411,8 @@ mod tests {
cx.set_global(settings_store);
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let conversation = cx.new_model(|cx| {
- Conversation::new(LanguageModel::default(), registry, EmbeddedScope::new(), cx)
- });
+ let conversation =
+ cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3316,14 +3496,8 @@ mod tests {
cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
cx.update(init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
- let conversation = cx.new_model(|cx| {
- Conversation::new(
- LanguageModel::default(),
- registry.clone(),
- EmbeddedScope::new(),
- cx,
- )
- });
+ let conversation =
+ cx.new_model(|cx| Conversation::new(LanguageModel::default(), registry.clone(), cx));
let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
let message_0 =
conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
diff --git a/crates/assistant/src/embedded_scope.rs b/crates/assistant/src/embedded_scope.rs
deleted file mode 100644
index 2bff3c3bfa..0000000000
--- a/crates/assistant/src/embedded_scope.rs
+++ /dev/null
@@ -1,91 +0,0 @@
-use editor::MultiBuffer;
-use gpui::{AppContext, Model, ModelContext, Subscription};
-
-use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
-
-#[derive(Default)]
-pub struct EmbeddedScope {
- active_buffer: Option>,
- active_buffer_enabled: bool,
- active_buffer_subscription: Option,
-}
-
-impl EmbeddedScope {
- pub fn new() -> Self {
- Self {
- active_buffer: None,
- active_buffer_enabled: true,
- active_buffer_subscription: None,
- }
- }
-
- pub fn set_active_buffer(
- &mut self,
- buffer: Option>,
- cx: &mut ModelContext,
- ) {
- self.active_buffer_subscription.take();
-
- if let Some(active_buffer) = buffer.clone() {
- self.active_buffer_subscription =
- Some(cx.subscribe(&active_buffer, |conversation, _, e, cx| {
- if let multi_buffer::Event::Edited { .. } = e {
- conversation.count_remaining_tokens(cx)
- }
- }));
- }
-
- self.active_buffer = buffer;
- }
-
- pub fn active_buffer(&self) -> Option<&Model> {
- self.active_buffer.as_ref()
- }
-
- pub fn active_buffer_enabled(&self) -> bool {
- self.active_buffer_enabled
- }
-
- pub fn set_active_buffer_enabled(&mut self, enabled: bool) {
- self.active_buffer_enabled = enabled;
- }
-
- /// Provide a message for the language model based on the active buffer.
- pub fn message(&self, cx: &AppContext) -> Option {
- if !self.active_buffer_enabled {
- return None;
- }
-
- let active_buffer = self.active_buffer.as_ref()?;
- let buffer = active_buffer.read(cx);
-
- if let Some(singleton) = buffer.as_singleton() {
- let singleton = singleton.read(cx);
-
- let filename = singleton
- .file()
- .map(|file| file.path().to_string_lossy())
- .unwrap_or("Untitled".into());
-
- let text = singleton.text();
-
- let language = singleton
- .language()
- .map(|l| {
- let name = l.code_fence_block_name();
- name.to_string()
- })
- .unwrap_or_default();
-
- let markdown =
- format!("User's active file `{filename}`:\n\n```{language}\n{text}```\n\n");
-
- return Some(LanguageModelRequestMessage {
- role: Role::System,
- content: markdown,
- });
- }
-
- None
- }
-}
diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs
index 17236c07c6..dfa49da230 100644
--- a/crates/gpui/src/window.rs
+++ b/crates/gpui/src/window.rs
@@ -200,6 +200,18 @@ impl FocusHandle {
pub fn contains(&self, other: &Self, cx: &WindowContext) -> bool {
self.id.contains(other.id, cx)
}
+
+ /// Dispatch an action on the element that rendered this focus handle
+ pub fn dispatch_action(&self, action: &dyn Action, cx: &mut WindowContext) {
+ if let Some(node_id) = cx
+ .window
+ .rendered_frame
+ .dispatch_tree
+ .focusable_node_id(self.id)
+ {
+ cx.dispatch_action_on_node(node_id, action)
+ }
+ }
}
impl Clone for FocusHandle {
diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs
index 651d2ba284..232c0b37c9 100644
--- a/crates/tab_switcher/src/tab_switcher.rs
+++ b/crates/tab_switcher/src/tab_switcher.rs
@@ -189,8 +189,8 @@ impl TabSwitcherDelegate {
let pane = pane.read(cx);
let mut history_indices = HashMap::default();
pane.activation_history().iter().rev().enumerate().for_each(
- |(history_index, entity_id)| {
- history_indices.insert(entity_id, history_index);
+ |(history_index, history_entry)| {
+ history_indices.insert(history_entry.entity_id, history_index);
},
);
diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs
index 77710a2309..75eae2a4f0 100644
--- a/crates/ui/src/components/icon.rs
+++ b/crates/ui/src/components/icon.rs
@@ -107,6 +107,7 @@ pub enum IconName {
CopilotError,
CopilotInit,
Copy,
+ CountdownTimer,
Dash,
Delete,
Disconnected,
@@ -221,6 +222,7 @@ impl IconName {
IconName::CopilotError => "icons/copilot_error.svg",
IconName::CopilotInit => "icons/copilot_init.svg",
IconName::Copy => "icons/copy.svg",
+ IconName::CountdownTimer => "icons/countdown_timer.svg",
IconName::Dash => "icons/dash.svg",
IconName::Delete => "icons/delete.svg",
IconName::Disconnected => "icons/disconnected.svg",
diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs
index aae840c982..b875dfdacf 100644
--- a/crates/workspace/src/pane.rs
+++ b/crates/workspace/src/pane.rs
@@ -191,7 +191,8 @@ pub struct Pane {
),
focus_handle: FocusHandle,
items: Vec>,
- activation_history: Vec,
+ activation_history: Vec,
+ next_activation_timestamp: Arc,
zoomed: bool,
was_focused: bool,
active_item_index: usize,
@@ -219,6 +220,11 @@ pub struct Pane {
double_click_dispatch_action: Box,
}
+pub struct ActivationHistoryEntry {
+ pub entity_id: EntityId,
+ pub timestamp: usize,
+}
+
pub struct ItemNavHistory {
history: NavHistory,
item: Arc,
@@ -296,6 +302,7 @@ impl Pane {
focus_handle,
items: Vec::new(),
activation_history: Vec::new(),
+ next_activation_timestamp: next_timestamp.clone(),
was_focused: false,
zoomed: false,
active_item_index: 0,
@@ -506,7 +513,7 @@ impl Pane {
self.active_item_index
}
- pub fn activation_history(&self) -> &Vec {
+ pub fn activation_history(&self) -> &[ActivationHistoryEntry] {
&self.activation_history
}
@@ -892,10 +899,13 @@ impl Pane {
if let Some(newly_active_item) = self.items.get(index) {
self.activation_history
- .retain(|&previously_active_item_id| {
- previously_active_item_id != newly_active_item.item_id()
- });
- self.activation_history.push(newly_active_item.item_id());
+ .retain(|entry| entry.entity_id != newly_active_item.item_id());
+ self.activation_history.push(ActivationHistoryEntry {
+ entity_id: newly_active_item.item_id(),
+ timestamp: self
+ .next_activation_timestamp
+ .fetch_add(1, Ordering::SeqCst),
+ });
}
self.update_toolbar(cx);
@@ -1211,7 +1221,7 @@ impl Pane {
cx: &mut ViewContext,
) {
self.activation_history
- .retain(|&history_entry| history_entry != self.items[item_index].item_id());
+ .retain(|entry| entry.entity_id != self.items[item_index].item_id());
if item_index == self.active_item_index {
let index_to_activate = self
@@ -1219,7 +1229,7 @@ impl Pane {
.pop()
.and_then(|last_activated_item| {
self.items.iter().enumerate().find_map(|(index, item)| {
- (item.item_id() == last_activated_item).then_some(index)
+ (item.item_id() == last_activated_item.entity_id).then_some(index)
})
})
// We didn't have a valid activation history entry, so fallback
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
index 430a63cde1..863ba42db8 100644
--- a/crates/workspace/src/workspace.rs
+++ b/crates/workspace/src/workspace.rs
@@ -532,6 +532,9 @@ impl DelayedDebouncedEditAction {
pub enum Event {
PaneAdded(View),
+ PaneRemoved,
+ ItemAdded,
+ ItemRemoved,
ActiveItemChanged,
ContactRequestedJoin(u64),
WorkspaceCreated(WeakView),
@@ -2513,7 +2516,10 @@ impl Workspace {
cx: &mut ViewContext,
) {
match event {
- pane::Event::AddItem { item } => item.added_to_pane(self, pane, cx),
+ pane::Event::AddItem { item } => {
+ item.added_to_pane(self, pane, cx);
+ cx.emit(Event::ItemAdded);
+ }
pane::Event::Split(direction) => {
self.split_and_clone(pane, *direction, cx);
}
@@ -2696,6 +2702,7 @@ impl Workspace {
} else {
self.active_item_path_changed(cx);
}
+ cx.emit(Event::PaneRemoved);
}
pub fn panes(&self) -> &[View] {