diff --git a/assets/icons/menu_alt.svg b/assets/icons/menu_alt.svg
new file mode 100644
index 0000000000..ae3581ba01
--- /dev/null
+++ b/assets/icons/menu_alt.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 00cc9805e3..90b509e9cf 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -250,6 +250,12 @@
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
+ {
+ "context": "AgentPanel > NavigationMenu",
+ "bindings": {
+ "ctrl-backspace": "agent::DeleteRecentlyOpenThread"
+ }
+ },
{
"context": "AgentPanel > Markdown",
"bindings": {
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 7a606d91b6..e9711e09bf 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -290,11 +290,18 @@
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
+ "cmd-ctrl-a": "agent::ToggleNavigationMenu",
"shift-escape": "agent::ExpandMessageEditor",
"cmd-e": "agent::ChatMode",
"cmd-alt-e": "agent::RemoveAllContext"
}
},
+ {
+ "context": "AgentPanel > NavigationMenu",
+ "bindings": {
+ "ctrl-backspace": "agent::DeleteRecentlyOpenThread"
+ }
+ },
{
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs
index c465308a50..77acba1c6d 100644
--- a/crates/agent/src/active_thread.rs
+++ b/crates/agent/src/active_thread.rs
@@ -709,7 +709,7 @@ fn open_markdown_link(
if let Some(panel) = workspace.panel::(cx) {
panel.update(cx, |panel, cx| {
panel
- .open_thread(&thread_id, window, cx)
+ .open_thread_by_id(&thread_id, window, cx)
.detach_and_log_err(cx)
});
}
@@ -3275,7 +3275,7 @@ pub(crate) fn open_context(
panel.update(cx, |panel, cx| {
let thread_id = thread_context.thread.read(cx).id().clone();
panel
- .open_thread(&thread_id, window, cx)
+ .open_thread_by_id(&thread_id, window, cx)
.detach_and_log_err(cx)
});
}
diff --git a/crates/agent/src/assistant.rs b/crates/agent/src/assistant.rs
index 80c69665fb..f09bdfd34e 100644
--- a/crates/agent/src/assistant.rs
+++ b/crates/agent/src/assistant.rs
@@ -50,6 +50,8 @@ actions!(
[
NewTextThread,
ToggleContextPicker,
+ ToggleNavigationMenu,
+ DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
diff --git a/crates/agent/src/assistant_panel.rs b/crates/agent/src/assistant_panel.rs
index 0258bd6955..29c9fde5df 100644
--- a/crates/agent/src/assistant_panel.rs
+++ b/crates/agent/src/assistant_panel.rs
@@ -1,5 +1,5 @@
use std::ops::Range;
-use std::path::PathBuf;
+use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
@@ -18,8 +18,8 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels,
- Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
+ Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
+ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
@@ -41,15 +41,16 @@ use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
-use crate::history_store::{HistoryEntry, HistoryStore};
+use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::UsageBanner;
use crate::{
- AddContextServer, AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
- OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
+ AddContextServer, AgentDiff, DeleteRecentlyOpenThread, ExpandMessageEditor, InlineAssistant,
+ NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent,
+ ToggleContextPicker, ToggleNavigationMenu,
};
pub fn init(cx: &mut App) {
@@ -104,6 +105,14 @@ pub fn init(cx: &mut App) {
});
});
}
+ })
+ .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
+ if let Some(panel) = workspace.panel::(cx) {
+ workspace.focus_panel::(window, cx);
+ panel.update(cx, |panel, cx| {
+ panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
+ });
+ }
});
},
)
@@ -113,6 +122,7 @@ pub fn init(cx: &mut App) {
enum ActiveView {
Thread {
change_title_editor: Entity,
+ thread: WeakEntity,
_subscriptions: Vec,
},
PromptEditor {
@@ -130,7 +140,7 @@ impl ActiveView {
let editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_text(summary, window, cx);
+ editor.set_text(summary.clone(), window, cx);
editor
});
@@ -176,6 +186,7 @@ impl ActiveView {
Self::Thread {
change_title_editor: editor,
+ thread: thread.downgrade(),
_subscriptions: subscriptions,
}
}
@@ -279,6 +290,8 @@ pub struct AssistantPanel {
history_store: Entity,
history: Entity,
assistant_dropdown_menu_handle: PopoverMenuHandle,
+ assistant_navigation_menu_handle: PopoverMenuHandle,
+ assistant_navigation_menu: Option>,
width: Option,
height: Option,
}
@@ -380,8 +393,14 @@ impl AssistantPanel {
}
});
- let history_store =
- cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
+ let history_store = cx.new(|cx| {
+ HistoryStore::new(
+ thread_store.clone(),
+ context_store.clone(),
+ [RecentEntry::Thread(thread.clone())],
+ cx,
+ )
+ });
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
@@ -392,7 +411,7 @@ impl AssistantPanel {
cx.notify();
}
});
- let thread = cx.new(|cx| {
+ let active_thread = cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
@@ -403,10 +422,111 @@ impl AssistantPanel {
)
});
- let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
- ActiveThreadEvent::EditingMessageTokenCountChanged => {
- cx.notify();
- }
+ let active_thread_subscription =
+ cx.subscribe(&active_thread, |_, _, event, cx| match &event {
+ ActiveThreadEvent::EditingMessageTokenCountChanged => {
+ cx.notify();
+ }
+ });
+
+ let weak_panel = weak_self.clone();
+
+ window.defer(cx, move |window, cx| {
+ let panel = weak_panel.clone();
+ let assistant_navigation_menu =
+ ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
+ let recently_opened = panel
+ .update(cx, |this, cx| {
+ this.history_store.update(cx, |history_store, cx| {
+ history_store.recently_opened_entries(cx)
+ })
+ })
+ .unwrap_or_default();
+
+ if !recently_opened.is_empty() {
+ menu = menu.header("Recently Opened");
+
+ for entry in recently_opened.iter() {
+ let summary = entry.summary(cx);
+ menu = menu.entry_with_end_slot(
+ summary,
+ None,
+ {
+ let panel = panel.clone();
+ let entry = entry.clone();
+ move |window, cx| {
+ panel
+ .update(cx, {
+ let entry = entry.clone();
+ move |this, cx| match entry {
+ RecentEntry::Thread(thread) => {
+ this.open_thread(thread, window, cx)
+ }
+ RecentEntry::Context(context) => {
+ let Some(path) = context.read(cx).path()
+ else {
+ return;
+ };
+ this.open_saved_prompt_editor(
+ path.clone(),
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx)
+ }
+ }
+ })
+ .ok();
+ }
+ },
+ IconName::Close,
+ "Close Entry".into(),
+ {
+ let panel = panel.clone();
+ let entry = entry.clone();
+ move |_window, cx| {
+ panel
+ .update(cx, |this, cx| {
+ this.history_store.update(
+ cx,
+ |history_store, cx| {
+ history_store.remove_recently_opened_entry(
+ &entry, cx,
+ );
+ },
+ );
+ })
+ .ok();
+ }
+ },
+ );
+ }
+
+ menu = menu.separator();
+ }
+
+ menu.action("View All", Box::new(OpenHistory))
+ .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
+ .fixed_width(px(320.).into())
+ .keep_open_on_confirm(false)
+ .key_context("NavigationMenu")
+ });
+ weak_panel
+ .update(cx, |panel, cx| {
+ cx.subscribe_in(
+ &assistant_navigation_menu,
+ window,
+ |_, menu, _: &DismissEvent, window, cx| {
+ menu.update(cx, |menu, _| {
+ menu.clear_selected();
+ });
+ cx.focus_self(window);
+ },
+ )
+ .detach();
+ panel.assistant_navigation_menu = Some(assistant_navigation_menu);
+ })
+ .ok();
});
let _default_model_subscription = cx.subscribe(
@@ -431,7 +551,7 @@ impl AssistantPanel {
fs: fs.clone(),
language_registry,
thread_store: thread_store.clone(),
- thread,
+ thread: active_thread,
message_editor,
_active_thread_subscriptions: vec![
thread_subscription,
@@ -451,6 +571,8 @@ impl AssistantPanel {
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
+ assistant_navigation_menu_handle: PopoverMenuHandle::default(),
+ assistant_navigation_menu: None,
width: None,
height: None,
}
@@ -645,13 +767,13 @@ impl AssistantPanel {
pub(crate) fn open_saved_prompt_editor(
&mut self,
- path: PathBuf,
+ path: Arc,
window: &mut Window,
cx: &mut Context,
) -> Task> {
let context = self
.context_store
- .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
+ .update(cx, |store, cx| store.open_local_context(path, cx));
let fs = self.fs.clone();
let project = self.project.clone();
let workspace = self.workspace.clone();
@@ -685,7 +807,7 @@ impl AssistantPanel {
})
}
- pub(crate) fn open_thread(
+ pub(crate) fn open_thread_by_id(
&mut self,
thread_id: &ThreadId,
window: &mut Window,
@@ -694,73 +816,83 @@ impl AssistantPanel {
let open_thread_task = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx));
-
cx.spawn_in(window, async move |this, cx| {
let thread = open_thread_task.await?;
this.update_in(cx, |this, window, cx| {
- let thread_view = ActiveView::thread(thread.clone(), window, cx);
- this.set_active_view(thread_view, window, cx);
- let message_editor_context_store = cx.new(|_cx| {
- crate::context_store::ContextStore::new(
- this.project.downgrade(),
- Some(this.thread_store.downgrade()),
- )
- });
- let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
- if let ThreadEvent::MessageAdded(_) = &event {
- // needed to leave empty state
- cx.notify();
- }
- });
-
- this.thread = cx.new(|cx| {
- ActiveThread::new(
- thread.clone(),
- this.thread_store.clone(),
- this.language_registry.clone(),
- this.workspace.clone(),
- window,
- cx,
- )
- });
-
- let active_thread_subscription =
- cx.subscribe(&this.thread, |_, _, event, cx| match &event {
- ActiveThreadEvent::EditingMessageTokenCountChanged => {
- cx.notify();
- }
- });
-
- this.message_editor = cx.new(|cx| {
- MessageEditor::new(
- this.fs.clone(),
- this.workspace.clone(),
- message_editor_context_store,
- this.prompt_store.clone(),
- this.thread_store.downgrade(),
- thread,
- window,
- cx,
- )
- });
- this.message_editor.focus_handle(cx).focus(window);
-
- let message_editor_subscription =
- cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
- MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
- cx.notify();
- }
- });
-
- this._active_thread_subscriptions = vec![
- thread_subscription,
- active_thread_subscription,
- message_editor_subscription,
- ];
- })
+ this.open_thread(thread, window, cx);
+ anyhow::Ok(())
+ })??;
+ Ok(())
})
}
+ pub(crate) fn open_thread(
+ &mut self,
+ thread: Entity,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ let thread_view = ActiveView::thread(thread.clone(), window, cx);
+ self.set_active_view(thread_view, window, cx);
+ let message_editor_context_store = cx.new(|_cx| {
+ crate::context_store::ContextStore::new(
+ self.project.downgrade(),
+ Some(self.thread_store.downgrade()),
+ )
+ });
+ let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
+ if let ThreadEvent::MessageAdded(_) = &event {
+ // needed to leave empty state
+ cx.notify();
+ }
+ });
+
+ self.thread = cx.new(|cx| {
+ ActiveThread::new(
+ thread.clone(),
+ self.thread_store.clone(),
+ self.language_registry.clone(),
+ self.workspace.clone(),
+ window,
+ cx,
+ )
+ });
+
+ let active_thread_subscription =
+ cx.subscribe(&self.thread, |_, _, event, cx| match &event {
+ ActiveThreadEvent::EditingMessageTokenCountChanged => {
+ cx.notify();
+ }
+ });
+
+ self.message_editor = cx.new(|cx| {
+ MessageEditor::new(
+ self.fs.clone(),
+ self.workspace.clone(),
+ message_editor_context_store,
+ self.prompt_store.clone(),
+ self.thread_store.downgrade(),
+ thread,
+ window,
+ cx,
+ )
+ });
+ self.message_editor.focus_handle(cx).focus(window);
+
+ let message_editor_subscription =
+ cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
+ MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
+ cx.notify();
+ }
+ });
+
+ self._active_thread_subscriptions = vec![
+ thread_subscription,
+ active_thread_subscription,
+ message_editor_subscription,
+ ];
+ }
+
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context) {
match self.active_view {
ActiveView::Configuration | ActiveView::History => {
@@ -773,6 +905,15 @@ impl AssistantPanel {
}
}
+ pub fn toggle_navigation_menu(
+ &mut self,
+ _: &ToggleNavigationMenu,
+ window: &mut Window,
+ cx: &mut Context,
+ ) {
+ self.assistant_navigation_menu_handle.toggle(window, cx);
+ }
+
pub fn open_agent_diff(
&mut self,
_: &OpenAgentDiff,
@@ -921,7 +1062,7 @@ impl AssistantPanel {
pub(crate) fn delete_context(
&mut self,
- path: PathBuf,
+ path: Arc,
cx: &mut Context,
) -> Task> {
self.context_store
@@ -937,6 +1078,32 @@ impl AssistantPanel {
let current_is_history = matches!(self.active_view, ActiveView::History);
let new_is_history = matches!(new_view, ActiveView::History);
+ match &self.active_view {
+ ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
+ if let Some(thread) = thread.upgrade() {
+ if thread.read(cx).is_empty() {
+ store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
+ }
+ }
+ }),
+ _ => {}
+ }
+
+ match &new_view {
+ ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
+ if let Some(thread) = thread.upgrade() {
+ store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
+ }
+ }),
+ ActiveView::PromptEditor { context_editor, .. } => {
+ self.history_store.update(cx, |store, cx| {
+ let context = context_editor.read(cx).context().clone();
+ store.push_recently_opened_entry(RecentEntry::Context(context), cx)
+ })
+ }
+ _ => {}
+ }
+
if current_is_history && !new_is_history {
self.active_view = new_view;
} else if !current_is_history && new_is_history {
@@ -1066,16 +1233,13 @@ impl AssistantPanel {
if is_empty {
Label::new(Thread::DEFAULT_SUMMARY.clone())
.truncate()
- .ml_2()
.into_any_element()
} else if summary.is_none() {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
- .ml_2()
.truncate()
.into_any_element()
} else {
div()
- .ml_2()
.w_full()
.child(change_title_editor.clone())
.into_any_element()
@@ -1092,18 +1256,15 @@ impl AssistantPanel {
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()
}
@@ -1130,7 +1291,6 @@ impl AssistantPanel {
let thread = active_thread.thread().read(cx);
let thread_id = thread.id().clone();
let is_empty = active_thread.is_empty();
- let is_history = matches!(self.active_view, ActiveView::History);
let show_token_count = match &self.active_view {
ActiveView::Thread { .. } => !is_empty,
@@ -1140,30 +1300,98 @@ impl AssistantPanel {
let focus_handle = self.focus_handle(cx);
- let go_back_button = match &self.active_view {
- ActiveView::History | ActiveView::Configuration => Some(
- div().pl_1().child(
- IconButton::new("go-back", IconName::ArrowLeft)
+ let go_back_button = div().child(
+ IconButton::new("go-back", IconName::ArrowLeft)
+ .icon_size(IconSize::Small)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.go_back(&workspace::GoBack, window, cx);
+ }))
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Go Back",
+ &workspace::GoBack,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ }),
+ );
+
+ let recent_entries_menu = div().child(
+ PopoverMenu::new("agent-nav-menu")
+ .trigger_with_tooltip(
+ IconButton::new("agent-nav-menu", IconName::MenuAlt)
.icon_size(IconSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- this.go_back(&workspace::GoBack, window, cx);
- }))
- .tooltip({
- let focus_handle = focus_handle.clone();
- move |window, cx| {
- Tooltip::for_action_in(
- "Go Back",
- &workspace::GoBack,
- &focus_handle,
- window,
- cx,
- )
- }
+ .style(ui::ButtonStyle::Subtle),
+ {
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Toggle Panel Menu",
+ &ToggleNavigationMenu,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
+ },
+ )
+ .anchor(Corner::TopLeft)
+ .with_handle(self.assistant_navigation_menu_handle.clone())
+ .menu({
+ let menu = self.assistant_navigation_menu.clone();
+ move |window, cx| {
+ if let Some(menu) = menu.as_ref() {
+ menu.update(cx, |_, cx| {
+ cx.defer_in(window, |menu, window, cx| {
+ menu.rebuild(window, cx);
+ });
+ })
+ }
+ menu.clone()
+ }
+ }),
+ );
+
+ let agent_extra_menu = PopoverMenu::new("assistant-menu")
+ .trigger_with_tooltip(
+ IconButton::new("new", IconName::Ellipsis)
+ .icon_size(IconSize::Small)
+ .style(ButtonStyle::Subtle),
+ Tooltip::text("Toggle Agent Menu"),
+ )
+ .anchor(Corner::TopRight)
+ .with_handle(self.assistant_dropdown_menu_handle.clone())
+ .menu(move |window, cx| {
+ Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+ menu.when(!is_empty, |menu| {
+ menu.action(
+ "Start New From Summary",
+ Box::new(NewThread {
+ from_thread_id: Some(thread_id.clone()),
+ }),
+ )
+ .separator()
+ })
+ .action("New Text Thread", NewTextThread.boxed_clone())
+ .action("Rules Library", Box::new(OpenRulesLibrary::default()))
+ .action("Settings", Box::new(OpenConfiguration))
+ .separator()
+ .header("MCPs")
+ .action(
+ "View Server Extensions",
+ Box::new(zed_actions::Extensions {
+ category_filter: Some(
+ zed_actions::ExtensionCategoryFilter::ContextServers,
+ ),
}),
- ),
- ),
- _ => None,
- };
+ )
+ .action("Add Custom Server", Box::new(AddContextServer))
+ }))
+ });
h_flex()
.id("assistant-toolbar")
@@ -1177,18 +1405,22 @@ impl AssistantPanel {
.border_color(cx.theme().colors().border)
.child(
h_flex()
- .w_full()
+ .size_full()
+ .pl_1()
.gap_1()
- .children(go_back_button)
+ .child(match &self.active_view {
+ ActiveView::History | ActiveView::Configuration => go_back_button,
+ _ => recent_entries_menu,
+ })
.child(self.render_title_view(window, cx)),
)
.child(
h_flex()
.h_full()
.gap_2()
- .when(show_token_count, |parent|
+ .when(show_token_count, |parent| {
parent.children(self.render_token_count(&thread, cx))
- )
+ })
.child(
h_flex()
.h_full()
@@ -1216,72 +1448,7 @@ impl AssistantPanel {
);
}),
)
- .child(
- IconButton::new("open-history", IconName::HistoryRerun)
- .icon_size(IconSize::Small)
- .toggle_state(is_history)
- .selected_icon_color(Color::Accent)
- .tooltip({
- let focus_handle = self.focus_handle(cx);
- move |window, cx| {
- Tooltip::for_action_in(
- "History",
- &OpenHistory,
- &focus_handle,
- window,
- cx,
- )
- }
- })
- .on_click(move |_event, window, cx| {
- window.dispatch_action(OpenHistory.boxed_clone(), cx);
- }),
- )
- .child(
- PopoverMenu::new("assistant-menu")
- .trigger_with_tooltip(
- IconButton::new("new", IconName::Ellipsis)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle),
- Tooltip::text("Toggle Agent Menu"),
- )
- .anchor(Corner::TopRight)
- .with_handle(self.assistant_dropdown_menu_handle.clone())
- .menu(move |window, cx| {
- Some(ContextMenu::build(
- window,
- cx,
- |menu, _window, _cx| {
- menu
- .when(!is_empty, |menu| {
- menu.action(
- "Start New From Summary",
- Box::new(NewThread {
- from_thread_id: Some(thread_id.clone()),
- }),
- ).separator()
- })
- .action(
- "New Text Thread",
- NewTextThread.boxed_clone(),
- )
- .action("Rules Library", Box::new(OpenRulesLibrary::default()))
- .action("Settings", Box::new(OpenConfiguration))
- .separator()
- .header("MCPs")
- .action(
- "View Server Extensions",
- Box::new(zed_actions::Extensions {
- category_filter: Some(
- zed_actions::ExtensionCategoryFilter::ContextServers,
- ),
- }),
- )
- .action("Add Custom Server", Box::new(AddContextServer))
- },
- ))
- }),
- ),
+ .child(agent_extra_menu),
),
)
}
@@ -1982,6 +2149,7 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::deploy_rules_library))
.on_action(cx.listener(Self::open_agent_diff))
.on_action(cx.listener(Self::go_back))
+ .on_action(cx.listener(Self::toggle_navigation_menu))
.child(self.render_toolbar(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent
@@ -2066,7 +2234,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn open_saved_context(
&self,
workspace: &mut Workspace,
- path: std::path::PathBuf,
+ path: Arc,
window: &mut Window,
cx: &mut Context,
) -> Task> {
diff --git a/crates/agent/src/context_picker.rs b/crates/agent/src/context_picker.rs
index e8adb03ef3..98bd00b9bd 100644
--- a/crates/agent/src/context_picker.rs
+++ b/crates/agent/src/context_picker.rs
@@ -267,7 +267,7 @@ impl ContextPicker {
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
})
}))
- .keep_open_on_confirm()
+ .keep_open_on_confirm(true)
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs
index 029fe1b381..be1b621b59 100644
--- a/crates/agent/src/history_store.rs
+++ b/crates/agent/src/history_store.rs
@@ -1,10 +1,27 @@
-use assistant_context_editor::SavedContextMetadata;
+use std::{collections::VecDeque, path::Path};
+
+use anyhow::{Context as _, anyhow};
+use assistant_context_editor::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
-use gpui::{Entity, prelude::*};
+use futures::future::{TryFutureExt as _, join_all};
+use gpui::{Entity, Task, prelude::*};
+use serde::{Deserialize, Serialize};
+use smol::future::FutureExt;
+use std::time::Duration;
+use ui::{App, SharedString};
+use util::ResultExt as _;
-use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
+use crate::{
+ Thread,
+ thread::ThreadId,
+ thread_store::{SerializedThreadMetadata, ThreadStore},
+};
-#[derive(Debug)]
+const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
+const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
+const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
+
+#[derive(Clone, Debug)]
pub enum HistoryEntry {
Thread(SerializedThreadMetadata),
Context(SavedContextMetadata),
@@ -19,16 +36,40 @@ impl HistoryEntry {
}
}
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum RecentEntry {
+ Thread(Entity),
+ Context(Entity),
+}
+
+impl RecentEntry {
+ pub(crate) fn summary(&self, cx: &App) -> SharedString {
+ match self {
+ RecentEntry::Thread(thread) => thread.read(cx).summary_or_default(),
+ RecentEntry::Context(context) => context.read(cx).summary_or_default(),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize)]
+enum SerializedRecentEntry {
+ Thread(String),
+ Context(String),
+}
+
pub struct HistoryStore {
thread_store: Entity,
context_store: Entity,
+ recently_opened_entries: VecDeque,
_subscriptions: Vec,
+ _save_recently_opened_entries_task: Task<()>,
}
impl HistoryStore {
pub fn new(
thread_store: Entity,
context_store: Entity,
+ initial_recent_entries: impl IntoIterator- ,
cx: &mut Context,
) -> Self {
let subscriptions = vec![
@@ -36,10 +77,61 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
+ cx.spawn({
+ let thread_store = thread_store.downgrade();
+ let context_store = context_store.downgrade();
+ async move |this, cx| {
+ let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
+ let contents = cx
+ .background_spawn(async move { std::fs::read_to_string(path) })
+ .await
+ .context("reading persisted agent panel navigation history")?;
+ let entries = serde_json::from_str::>(&contents)
+ .context("deserializing persisted agent panel navigation history")?
+ .into_iter()
+ .take(MAX_RECENTLY_OPENED_ENTRIES)
+ .map(|serialized| match serialized {
+ SerializedRecentEntry::Thread(id) => thread_store
+ .update(cx, |thread_store, cx| {
+ thread_store
+ .open_thread(&ThreadId::from(id.as_str()), cx)
+ .map_ok(RecentEntry::Thread)
+ .boxed()
+ })
+ .unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
+ SerializedRecentEntry::Context(id) => context_store
+ .update(cx, |context_store, cx| {
+ context_store
+ .open_local_context(Path::new(&id).into(), cx)
+ .map_ok(RecentEntry::Context)
+ .boxed()
+ })
+ .unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
+ });
+ let entries = join_all(entries)
+ .await
+ .into_iter()
+ .filter_map(|result| result.log_err())
+ .collect::>();
+
+ this.update(cx, |this, _| {
+ this.recently_opened_entries.extend(entries);
+ this.recently_opened_entries
+ .truncate(MAX_RECENTLY_OPENED_ENTRIES);
+ })
+ .ok();
+
+ anyhow::Ok(())
+ }
+ })
+ .detach_and_log_err(cx);
+
Self {
thread_store,
context_store,
+ recently_opened_entries: initial_recent_entries.into_iter().collect(),
_subscriptions: subscriptions,
+ _save_recently_opened_entries_task: Task::ready(()),
}
}
@@ -69,4 +161,57 @@ impl HistoryStore {
pub fn recent_entries(&self, limit: usize, cx: &mut Context) -> Vec {
self.entries(cx).into_iter().take(limit).collect()
}
+
+ fn save_recently_opened_entries(&mut self, cx: &mut Context) {
+ let serialized_entries = self
+ .recently_opened_entries
+ .iter()
+ .filter_map(|entry| match entry {
+ RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
+ context.read(cx).path()?.to_str()?.to_owned(),
+ )),
+ RecentEntry::Thread(thread) => Some(SerializedRecentEntry::Thread(
+ thread.read(cx).id().to_string(),
+ )),
+ })
+ .collect::>();
+
+ self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
+ cx.background_executor()
+ .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
+ .await;
+ cx.background_spawn(async move {
+ let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
+ let content = serde_json::to_string(&serialized_entries)?;
+ std::fs::write(path, content)?;
+ anyhow::Ok(())
+ })
+ .await
+ .log_err();
+ });
+ }
+
+ pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context) {
+ self.recently_opened_entries
+ .retain(|old_entry| old_entry != &entry);
+ self.recently_opened_entries.push_front(entry);
+ self.recently_opened_entries
+ .truncate(MAX_RECENTLY_OPENED_ENTRIES);
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context) {
+ self.recently_opened_entries
+ .retain(|old_entry| old_entry != entry);
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn recently_opened_entries(&self, _cx: &mut Context) -> VecDeque {
+ #[cfg(debug_assertions)]
+ if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
+ return VecDeque::new();
+ }
+
+ self.recently_opened_entries.clone()
+ }
}
diff --git a/crates/agent/src/thread_history.rs b/crates/agent/src/thread_history.rs
index ecf5e958a7..cfd50a2dd5 100644
--- a/crates/agent/src/thread_history.rs
+++ b/crates/agent/src/thread_history.rs
@@ -270,9 +270,9 @@ impl ThreadHistory {
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
- HistoryEntry::Thread(thread) => self
- .assistant_panel
- .update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
+ HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
+ this.open_thread_by_id(&thread.id, window, cx)
+ }),
HistoryEntry::Context(context) => {
self.assistant_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
@@ -525,7 +525,8 @@ impl RenderOnce for PastThread {
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
- this.open_thread(&id, window, cx).detach_and_log_err(cx);
+ this.open_thread_by_id(&id, window, cx)
+ .detach_and_log_err(cx);
})
.ok();
}
diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs
index 7a8717b590..d19bb9f396 100644
--- a/crates/assistant/src/assistant_panel.rs
+++ b/crates/assistant/src/assistant_panel.rs
@@ -33,6 +33,7 @@ use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::ops::Range;
+use std::path::Path;
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
@@ -1080,7 +1081,7 @@ impl AssistantPanel {
pub fn open_saved_context(
&mut self,
- path: PathBuf,
+ path: Arc,
window: &mut Window,
cx: &mut Context,
) -> Task> {
@@ -1391,7 +1392,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
fn open_saved_context(
&self,
workspace: &mut Workspace,
- path: PathBuf,
+ path: Arc,
window: &mut Window,
cx: &mut Context,
) -> Task> {
diff --git a/crates/assistant_context_editor/src/context.rs b/crates/assistant_context_editor/src/context.rs
index 5f5a6e8ee8..f29bcbe753 100644
--- a/crates/assistant_context_editor/src/context.rs
+++ b/crates/assistant_context_editor/src/context.rs
@@ -35,7 +35,7 @@ use std::{
fmt::Debug,
iter, mem,
ops::Range,
- path::{Path, PathBuf},
+ path::Path,
str::FromStr as _,
sync::Arc,
time::{Duration, Instant},
@@ -46,7 +46,7 @@ use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
use uuid::Uuid;
-#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContextId(String);
impl ContextId {
@@ -648,7 +648,7 @@ pub struct AssistantContext {
pending_token_count: Task