Cargo.lock π
@@ -409,7 +409,6 @@ dependencies = [
"theme",
"theme_settings",
"time",
- "time_format",
"tree-sitter-md",
"ui",
"ui_input",
Bennet Bo Fenner created
Self-Review Checklist:
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable
Release Notes:
- N/A
Cargo.lock | 1
assets/keymaps/default-linux.json | 2
assets/keymaps/default-macos.json | 2
assets/keymaps/default-windows.json | 2
crates/agent_ui/Cargo.toml | 1
crates/agent_ui/src/agent_connection_store.rs | 22
crates/agent_ui/src/agent_panel.rs | 363 ------
crates/agent_ui/src/agent_ui.rs | 10
crates/agent_ui/src/completion_provider.rs | 77
crates/agent_ui/src/conversation_view.rs | 240 ----
crates/agent_ui/src/conversation_view/thread_view.rs | 31
crates/agent_ui/src/entry_view_state.rs | 7
crates/agent_ui/src/inline_assistant.rs | 15
crates/agent_ui/src/inline_prompt_editor.rs | 7
crates/agent_ui/src/message_editor.rs | 21
crates/agent_ui/src/terminal_inline_assistant.rs | 3
crates/agent_ui/src/thread_history.rs | 772 --------------
crates/agent_ui/src/thread_history_view.rs | 751 -------------
docs/src/ai/agent-panel.md | 7
19 files changed, 85 insertions(+), 2,249 deletions(-)
@@ -409,7 +409,6 @@ dependencies = [
"theme",
"theme_settings",
"time",
- "time_format",
"tree-sitter-md",
"ui",
"ui_input",
@@ -224,7 +224,6 @@
"context": "AgentPanel",
"bindings": {
"ctrl-n": "agent::NewThread",
- "ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::ManageProfiles",
"ctrl-alt-l": "agent::OpenRulesLibrary",
@@ -234,7 +233,6 @@
"alt-tab": "agent::CycleFavoriteModels",
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Linux under the `AgentPanel` context
"alt-l": "agent::CycleFavoriteModels",
- "shift-alt-j": "agent::ToggleNavigationMenu",
"shift-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
@@ -264,7 +264,6 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewThread",
- "cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-l": "agent::OpenRulesLibrary",
"cmd-alt-p": "agent::ManageProfiles",
@@ -272,7 +271,6 @@
"shift-tab": "agent::CycleModeSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"alt-tab": "agent::CycleFavoriteModels",
- "cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
@@ -225,7 +225,6 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-n": "agent::NewThread",
- "ctrl-shift-h": "agent::OpenHistory",
"shift-alt-c": "agent::OpenSettings",
"shift-alt-l": "agent::OpenRulesLibrary",
"shift-alt-p": "agent::ManageProfiles",
@@ -235,7 +234,6 @@
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Windows under the `AgentPanel` context
"alt-l": "agent::CycleFavoriteModels",
"shift-alt-/": "agent::ToggleModelSelector",
- "shift-alt-j": "agent::ToggleNavigationMenu",
"shift-alt-i": "agent::ToggleOptionsMenu",
"ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
@@ -102,7 +102,6 @@ text.workspace = true
theme.workspace = true
theme_settings.workspace = true
time.workspace = true
-time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
url.workspace = true
@@ -10,7 +10,7 @@ use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Subscri
use project::{AgentServerStore, AgentServersUpdated, Project};
use watch::Receiver;
-use crate::{Agent, ThreadHistory};
+use crate::Agent;
pub enum AgentConnectionEntry {
Connecting {
@@ -25,7 +25,6 @@ pub enum AgentConnectionEntry {
#[derive(Clone)]
pub struct AgentConnectedState {
pub connection: Rc<dyn AgentConnection>,
- pub history: Option<Entity<ThreadHistory>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -44,13 +43,6 @@ impl AgentConnectionEntry {
}
}
- pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
- match self {
- AgentConnectionEntry::Connected(state) => state.history.as_ref(),
- _ => None,
- }
- }
-
pub fn status(&self) -> AgentConnectionStatus {
match self {
AgentConnectionEntry::Connecting { .. } => AgentConnectionStatus::Connecting,
@@ -241,16 +233,8 @@ impl AgentConnectionStore {
let delegate = AgentServerDelegate::new(agent_server_store, Some(new_version_tx));
let connect_task = server.connect(delegate, self.project.clone(), cx);
- let connect_task = cx.spawn(async move |_this, cx| match connect_task.await {
- Ok(connection) => cx.update(|cx| {
- let history = connection
- .session_list(cx)
- .map(|session_list| cx.new(|cx| ThreadHistory::new(session_list, cx)));
- Ok(AgentConnectedState {
- connection,
- history,
- })
- }),
+ let connect_task = cx.spawn(async move |_this, _cx| match connect_task.await {
+ Ok(connection) => Ok(AgentConnectedState { connection }),
Err(err) => match err.downcast::<LoadError>() {
Ok(load_error) => Err(load_error),
Err(err) => Err(LoadError::Other(SharedString::from(err.to_string()))),
@@ -30,12 +30,15 @@ use zed_actions::{
};
use crate::DEFAULT_THREAD_TITLE;
+use crate::ExpandMessageEditor;
+use crate::ManageProfiles;
+use crate::agent_connection_store::AgentConnectionStore;
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore};
use crate::{
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow,
InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
- OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata,
- ShowThreadMetadata, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
+ ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata,
+ ToggleNewThreadMenu, ToggleOptionsMenu,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
conversation_view::{AcpThreadViewEvent, ThreadView},
ui::EndTrialUpsell,
@@ -44,12 +47,9 @@ use crate::{
Agent, AgentInitialContent, ExternalSourcePrompt, NewExternalAgentThread,
NewNativeAgentThreadFromSummary,
};
-use crate::{ExpandMessageEditor, ThreadHistoryView};
-use crate::{ManageProfiles, ThreadHistoryViewEvent};
-use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
-use anyhow::{Context as _, Result};
+use anyhow::Result;
use chrono::{DateTime, Utc};
use client::UserStore;
use cloud_api_types::Plan;
@@ -60,8 +60,8 @@ use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem,
- DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
- Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
+ Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription,
+ Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
@@ -78,7 +78,7 @@ use ui::{
Button, Callout, ContextMenu, ContextMenuEntry, IconButton, PopoverMenu, PopoverMenuHandle,
Tab, Tooltip, prelude::*, utils::WithRemSize,
};
-use util::{ResultExt as _, debug_panic};
+use util::ResultExt as _;
use workspace::{
CollaboratorId, DraggedSelection, DraggedTab, PathList, SerializedPathList,
ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
@@ -87,7 +87,6 @@ use workspace::{
const AGENT_PANEL_KEY: &str = "agent_panel";
const MIN_PANEL_WIDTH: Pixels = px(300.);
-const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
fn agent_v2_enabled(cx: &App) -> bool {
@@ -202,12 +201,6 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
}
})
- .register_action(|workspace, _: &OpenHistory, window, cx| {
- if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
- workspace.focus_panel::<AgentPanel>(window, cx);
- panel.update(cx, |panel, cx| panel.open_history(window, cx));
- }
- })
.register_action(|workspace, _: &OpenSettings, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -248,14 +241,6 @@ pub fn init(cx: &mut App) {
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
})
- .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
- if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
- workspace.focus_panel::<AgentPanel>(window, cx);
- panel.update(cx, |panel, cx| {
- panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
- });
- }
- })
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -666,14 +651,12 @@ impl From<AgentThread> for BaseView {
}
enum OverlayView {
- History { view: Entity<ThreadHistoryView> },
Configuration,
}
enum VisibleSurface<'a> {
Uninitialized,
AgentThread(&'a Entity<ConversationView>),
- History(&'a Entity<ThreadHistoryView>),
Configuration(Option<&'a Entity<AgentConfiguration>>),
}
@@ -691,7 +674,6 @@ impl BaseView {
impl OverlayView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
- OverlayView::History { .. } => WhichFontSize::AgentFont,
OverlayView::Configuration => WhichFontSize::None,
}
}
@@ -718,8 +700,6 @@ pub struct AgentPanel {
retained_threads: HashMap<ThreadId, Entity<ConversationView>>,
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
- agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
- agent_navigation_menu: Option<Entity<ContextMenu>>,
_extension_subscription: Option<Subscription>,
_project_subscription: Subscription,
zoomed: bool,
@@ -950,7 +930,7 @@ impl AgentPanel {
pub(crate) fn new(
workspace: &Workspace,
prompt_store: Option<Entity<PromptStore>>,
- window: &mut Window,
+ _window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let fs = workspace.app_state().fs.clone();
@@ -968,48 +948,6 @@ impl AgentPanel {
let base_view = BaseView::Uninitialized;
- let weak_panel = cx.entity().downgrade();
-
- window.defer(cx, move |window, cx| {
- let panel = weak_panel.clone();
- let agent_navigation_menu =
- ContextMenu::build_persistent(window, cx, move |mut menu, window, cx| {
- if let Some(panel) = panel.upgrade() {
- if let Some(history) = panel
- .update(cx, |panel, cx| panel.history_for_selected_agent(window, cx))
- {
- menu = Self::populate_recently_updated_menu_section(
- menu, panel, history, cx,
- );
- menu = menu.action("View All", Box::new(OpenHistory));
- }
- }
-
- menu = menu
- .fixed_width(px(320.).into())
- .keep_open_on_confirm(false)
- .key_context("NavigationMenu");
-
- menu
- });
- weak_panel
- .update(cx, |panel, cx| {
- cx.subscribe_in(
- &agent_navigation_menu,
- window,
- |_, menu, _: &DismissEvent, window, cx| {
- menu.update(cx, |menu, _| {
- menu.clear_selected();
- });
- cx.focus_self(window);
- },
- )
- .detach();
- panel.agent_navigation_menu = Some(agent_navigation_menu);
- })
- .ok();
- });
-
let weak_panel = cx.entity().downgrade();
let onboarding = cx.new(|cx| {
AgentPanelOnboarding::new(
@@ -1082,8 +1020,7 @@ impl AgentPanel {
retained_threads: HashMap::default(),
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
- agent_navigation_menu_handle: PopoverMenuHandle::default(),
- agent_navigation_menu: None,
+
_extension_subscription: extension_subscription,
_project_subscription,
zoomed: false,
@@ -1426,31 +1363,30 @@ impl AgentPanel {
) {
let session_id = action.from_session_id.clone();
- let Some(history) = self
- .connection_store
+ let Some(thread) = ThreadStore::global(cx)
.read(cx)
- .entry(&Agent::NativeAgent)
- .and_then(|e| e.read(cx).history().cloned())
+ .entries()
+ .find(|t| t.id == session_id)
else {
- debug_panic!("Native agent is not registered");
+ log::error!("No session found for summarization with id {}", session_id);
+ return;
+ };
+
+ let Some(parent_session_id) = thread.parent_session_id else {
+ log::error!("Session {} has no parent session", session_id);
return;
};
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
- let thread = history
- .read(cx)
- .session_for_id(&session_id)
- .context("Session not found")?;
-
this.external_thread(
Some(Agent::NativeAgent),
None,
None,
None,
Some(AgentInitialContent::ThreadSummary {
- session_id: thread.session_id,
- title: thread.title,
+ session_id: parent_session_id,
+ title: Some(thread.title),
}),
true,
"agent_panel",
@@ -1521,79 +1457,6 @@ impl AgentPanel {
})
}
- fn has_history_for_selected_agent(&self, cx: &App) -> bool {
- match &self.selected_agent {
- Agent::NativeAgent => true,
- Agent::Custom { .. } => self
- .connection_store
- .read(cx)
- .entry(&self.selected_agent)
- .map_or(false, |entry| entry.read(cx).history().is_some()),
- #[cfg(any(test, feature = "test-support"))]
- Agent::Stub => false,
- }
- }
-
- fn history_for_selected_agent(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Option<Entity<ThreadHistoryView>> {
- let agent = self.selected_agent.clone();
- let history = self
- .connection_store
- .read(cx)
- .entry(&agent)?
- .read(cx)
- .history()?
- .clone();
- Some(self.create_thread_history_view(agent, history, window, cx))
- }
-
- fn create_thread_history_view(
- &self,
- agent: Agent,
- history: Entity<ThreadHistory>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Entity<ThreadHistoryView> {
- let view = cx.new(|cx| ThreadHistoryView::new(history.clone(), window, cx));
- cx.subscribe_in(
- &view,
- window,
- move |this, _, event, window, cx| match event {
- ThreadHistoryViewEvent::Open(thread) => {
- this.load_agent_thread(
- agent.clone(),
- thread.session_id.clone(),
- thread.work_dirs.clone(),
- thread.title.clone(),
- true,
- "agent_panel",
- window,
- cx,
- );
- }
- },
- )
- .detach();
- view
- }
-
- fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- if matches!(self.overlay_view, Some(OverlayView::History { .. })) {
- self.clear_overlay(true, window, cx);
- return;
- }
-
- let Some(view) = self.history_for_selected_agent(window, cx) else {
- return;
- };
-
- self.set_overlay(OverlayView::History { view }, true, window, cx);
- cx.notify();
- }
-
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
if self.overlay_view.is_some() {
self.clear_overlay(true, window, cx);
@@ -1601,18 +1464,6 @@ impl AgentPanel {
}
}
- pub fn toggle_navigation_menu(
- &mut self,
- _: &ToggleNavigationMenu,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if !self.has_history_for_selected_agent(cx) {
- return;
- }
- self.agent_navigation_menu_handle.toggle(window, cx);
- }
-
pub fn toggle_options_menu(
&mut self,
_: &ToggleOptionsMenu,
@@ -2256,18 +2107,7 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let was_in_history = matches!(self.overlay_view, Some(OverlayView::History { .. }));
self.overlay_view = Some(overlay);
-
- if let Some(OverlayView::History { view }) = &self.overlay_view
- && !was_in_history
- {
- view.update(cx, |view, cx| {
- view.history()
- .update(cx, |history, cx| history.refresh_full_history(cx))
- });
- }
-
if focus {
self.focus_handle(cx).focus(window, cx);
}
@@ -2324,7 +2164,6 @@ impl AgentPanel {
fn visible_surface(&self) -> VisibleSurface<'_> {
if let Some(overlay_view) = &self.overlay_view {
return match overlay_view {
- OverlayView::History { view } => VisibleSurface::History(view),
OverlayView::Configuration => {
VisibleSurface::Configuration(self.configuration.as_ref())
}
@@ -2343,10 +2182,6 @@ impl AgentPanel {
self.overlay_view.is_some()
}
- fn is_history_or_configuration_visible(&self) -> bool {
- self.is_overlay_open()
- }
-
fn visible_font_size(&self) -> WhichFontSize {
self.overlay_view.as_ref().map_or_else(
|| self.base_view.which_font_size_used(),
@@ -2354,63 +2189,6 @@ impl AgentPanel {
)
}
- fn populate_recently_updated_menu_section(
- mut menu: ContextMenu,
- panel: Entity<Self>,
- view: Entity<ThreadHistoryView>,
- cx: &mut Context<ContextMenu>,
- ) -> ContextMenu {
- let entries = view
- .read(cx)
- .history()
- .read(cx)
- .sessions()
- .iter()
- .take(RECENTLY_UPDATED_MENU_LIMIT)
- .cloned()
- .collect::<Vec<_>>();
-
- if entries.is_empty() {
- return menu;
- }
-
- menu = menu.header("Recently Updated");
-
- for entry in entries {
- let title = entry
- .title
- .as_ref()
- .filter(|title| !title.is_empty())
- .cloned()
- .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
-
- menu = menu.entry(title, None, {
- let panel = panel.downgrade();
- let entry = entry.clone();
- move |window, cx| {
- let entry = entry.clone();
- panel
- .update(cx, move |this, cx| {
- let agent = this.selected_agent(cx);
- this.load_agent_thread(
- agent,
- entry.session_id.clone(),
- entry.work_dirs.clone(),
- entry.title.clone(),
- true,
- "agent_panel",
- window,
- cx,
- );
- })
- .ok();
- }
- });
- }
-
- menu.separator()
- }
-
fn subscribe_to_active_thread_view(
server_view: &Entity<ConversationView>,
window: &mut Window,
@@ -2680,7 +2458,6 @@ impl Focusable for AgentPanel {
match self.visible_surface() {
VisibleSurface::Uninitialized => self.focus_handle.clone(),
VisibleSurface::AgentThread(conversation_view) => conversation_view.focus_handle(cx),
- VisibleSurface::History(view) => view.read(cx).focus_handle(cx),
VisibleSurface::Configuration(configuration) => {
if let Some(configuration) = configuration {
configuration.focus_handle(cx)
@@ -3016,7 +2793,6 @@ impl AgentPanel {
.into_any_element()
}
}
- VisibleSurface::History(_) => Label::new("History").truncate().into_any_element(),
VisibleSurface::Configuration(_) => {
Label::new("Settings").truncate().into_any_element()
}
@@ -3143,47 +2919,6 @@ impl AgentPanel {
})
}
- fn render_recent_entries_menu(
- &self,
- icon: IconName,
- corner: Anchor,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- let focus_handle = self.focus_handle(cx);
-
- PopoverMenu::new("agent-nav-menu")
- .trigger_with_tooltip(
- IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
- {
- move |_window, cx| {
- Tooltip::for_action_in(
- "Toggle Recently Updated Threads",
- &ToggleNavigationMenu,
- &focus_handle,
- cx,
- )
- }
- },
- )
- .anchor(corner)
- .with_handle(self.agent_navigation_menu_handle.clone())
- .menu({
- let menu = self.agent_navigation_menu.clone();
- move |window, cx| {
- telemetry::event!("View Thread History Clicked");
-
- if let Some(menu) = menu.as_ref() {
- menu.update(cx, |_, cx| {
- cx.defer_in(window, |menu, window, cx| {
- menu.rebuild(window, cx);
- });
- })
- }
- menu.clone()
- }
- })
- }
-
fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
@@ -3453,11 +3188,10 @@ impl AgentPanel {
selected_agent.into_any_element()
};
- let show_history_menu = self.has_history_for_selected_agent(cx);
let agent_v2_enabled = agent_v2_enabled(cx);
let is_empty_state = !self.active_thread_has_messages(cx);
- let is_in_history_or_config = self.is_history_or_configuration_visible();
+ let is_in_history_or_config = self.is_overlay_open();
let is_full_screen = self.is_zoomed(window, cx);
let full_screen_button = if is_full_screen {
@@ -3552,13 +3286,6 @@ impl AgentPanel {
.gap_1()
.pl_1()
.pr_1()
- .when(show_history_menu && !agent_v2_enabled, |this| {
- this.child(self.render_recent_entries_menu(
- IconName::MenuAltTemp,
- Anchor::TopRight,
- cx,
- ))
- })
.child(full_screen_button)
.child(self.render_panel_options_menu(window, cx)),
)
@@ -3604,13 +3331,6 @@ impl AgentPanel {
.pl_1()
.pr_1()
.child(new_thread_menu)
- .when(show_history_menu && !agent_v2_enabled, |this| {
- this.child(self.render_recent_entries_menu(
- IconName::MenuAltTemp,
- Anchor::TopRight,
- cx,
- ))
- })
.child(full_screen_button)
.child(self.render_panel_options_menu(window, cx)),
)
@@ -3698,17 +3418,13 @@ impl AgentPanel {
match &self.base_view {
BaseView::Uninitialized => false,
- BaseView::AgentThread { conversation_view }
- if conversation_view.read(cx).as_native_thread(cx).is_none() =>
- {
- false
- }
BaseView::AgentThread { conversation_view } => {
- let history_is_empty = conversation_view
- .read(cx)
- .history()
- .is_none_or(|h| h.read(cx).is_empty());
- history_is_empty || !has_configured_non_zed_providers
+ if conversation_view.read(cx).as_native_thread(cx).is_some() {
+ let history_is_empty = ThreadStore::global(cx).read(cx).is_empty();
+ history_is_empty || !has_configured_non_zed_providers
+ } else {
+ false
+ }
}
}
}
@@ -3896,16 +3612,12 @@ impl Render for AgentPanel {
.on_action(cx.listener(|this, action: &NewThread, window, cx| {
this.new_thread(action, window, cx);
}))
- .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
- this.open_history(window, cx);
- }))
.on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))
.on_action(cx.listener(Self::deploy_rules_library))
.on_action(cx.listener(Self::go_back))
- .on_action(cx.listener(Self::toggle_navigation_menu))
.on_action(cx.listener(Self::toggle_options_menu))
.on_action(cx.listener(Self::increase_font_size))
.on_action(cx.listener(Self::decrease_font_size))
@@ -3926,7 +3638,6 @@ impl Render for AgentPanel {
VisibleSurface::AgentThread(conversation_view) => parent
.child(conversation_view.clone())
.child(self.render_drag_target(cx)),
- VisibleSurface::History(view) => parent.child(view.clone()),
VisibleSurface::Configuration(configuration) => {
parent.children(configuration.cloned())
}
@@ -3970,13 +3681,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return;
};
- let history = panel
- .read(cx)
- .connection_store()
- .read(cx)
- .entry(&crate::Agent::NativeAgent)
- .and_then(|s| s.read(cx).history())
- .map(|h| h.downgrade());
let project = workspace.read(cx).project().downgrade();
let panel = panel.read(cx);
let thread_store = panel.thread_store().clone();
@@ -3986,7 +3690,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
project,
thread_store,
None,
- history,
initial_prompt,
window,
cx,
@@ -4060,14 +3763,6 @@ impl AgentPanel {
self.active_conversation_view()
}
- /// Opens the history view.
- ///
- /// This is a test-only helper that exposes the private `open_history()`
- /// method for visual tests.
- pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.open_history(window, cx);
- }
-
/// Creates a draft thread using a stub server and sets it as the active view.
#[cfg(any(test, feature = "test-support"))]
pub fn open_draft_with_server(
@@ -27,8 +27,6 @@ mod terminal_codegen;
mod terminal_inline_assistant;
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
-mod thread_history;
-mod thread_history_view;
mod thread_import;
pub mod thread_metadata_store;
pub mod thread_worktree_archive;
@@ -73,8 +71,6 @@ pub use external_source_prompt::ExternalSourcePrompt;
pub(crate) use mode_selector::ModeSelector;
pub(crate) use model_selector::ModelSelector;
pub(crate) use model_selector_popover::ModelSelectorPopover;
-pub(crate) use thread_history::ThreadHistory;
-pub(crate) use thread_history_view::*;
pub use thread_import::{
AcpThreadImportOnboarding, CrossChannelImportOnboarding, ThreadImportModal,
channels_with_threads, import_threads_from_other_channels,
@@ -89,8 +85,6 @@ actions!(
[
/// Toggles the menu to create new agent threads.
ToggleNewThreadMenu,
- /// Toggles the navigation menu for switching between threads and views.
- ToggleNavigationMenu,
/// Toggles the options menu for agent settings and preferences.
ToggleOptionsMenu,
/// Toggles the profile or mode selector for switching between agent profiles.
@@ -101,10 +95,6 @@ actions!(
CycleFavoriteModels,
/// Expands the message editor to full size.
ExpandMessageEditor,
- /// Removes all thread history.
- RemoveHistory,
- /// Opens the conversation history view.
- OpenHistory,
/// Adds a context server to the configuration.
AddContextServer,
/// Archives the currently selected thread.
@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::DEFAULT_THREAD_TITLE;
-use crate::ThreadHistory;
+use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use anyhow::Result;
@@ -222,7 +222,6 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
}
@@ -232,7 +231,6 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
source: T,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
) -> Self {
@@ -241,7 +239,6 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
editor,
mention_set,
workspace,
- history,
prompt_store,
}
}
@@ -918,16 +915,8 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
}
Some(PromptContextType::Thread) => {
- if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
- let sessions = history
- .read(cx)
- .sessions()
- .iter()
- .map(|session| SessionMatch {
- session_id: session.session_id.clone(),
- title: session_title(session.title.clone()),
- })
- .collect::<Vec<_>>();
+ let sessions = collect_session_matches(cx);
+ if !sessions.is_empty() {
let search_task =
filter_sessions_by_query(query, cancellation_flag, sessions, cx);
cx.spawn(async move |_cx| {
@@ -1144,29 +1133,21 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
return Task::ready(recent);
}
- if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
- const RECENT_COUNT: usize = 2;
- recent.extend(
- history
- .read(cx)
- .sessions()
- .into_iter()
- .map(|session| SessionMatch {
- session_id: session.session_id.clone(),
- title: session_title(session.title.clone()),
- })
- .filter(|session| {
- let uri = MentionUri::Thread {
- id: session.session_id.clone(),
- name: session.title.to_string(),
- };
- !mentions.contains(&uri)
- })
- .take(RECENT_COUNT)
- .map(Match::RecentThread),
- );
- return Task::ready(recent);
- }
+ let sessions = collect_session_matches(cx);
+ const RECENT_COUNT: usize = 2;
+ recent.extend(
+ sessions
+ .into_iter()
+ .filter(|session| {
+ let uri = MentionUri::Thread {
+ id: session.session_id.clone(),
+ name: session.title.to_string(),
+ };
+ !mentions.contains(&uri)
+ })
+ .take(RECENT_COUNT)
+ .map(Match::RecentThread),
+ );
Task::ready(recent)
}
@@ -2030,6 +2011,28 @@ pub(crate) fn search_symbols(
})
}
+fn collect_session_matches(cx: &App) -> Vec<SessionMatch> {
+ let Some(store) = ThreadMetadataStore::try_global(cx) else {
+ return Vec::new();
+ };
+ let mut entries: Vec<&ThreadMetadata> = store
+ .read(cx)
+ .entries()
+ .filter(|t| !t.archived && t.agent_id == *agent::ZED_AGENT_ID)
+ .collect();
+ entries.sort_by_key(|t| Reverse(t.updated_at));
+ entries
+ .into_iter()
+ .map(|metadata| {
+ let info = acp_thread::AgentSessionInfo::from(metadata);
+ SessionMatch {
+ session_id: info.session_id,
+ title: session_title(info.title),
+ }
+ })
+ .collect()
+}
+
fn filter_sessions_by_query(
query: String,
cancellation_flag: Arc<AtomicBool>,
@@ -74,7 +74,6 @@ use zed_actions::assistant::OpenRulesLibrary;
use super::config_options::ConfigOptionsView;
use super::entry_view_state::EntryViewState;
-use super::thread_history::ThreadHistory;
use crate::ModeSelector;
use crate::ModelSelectorPopover;
use crate::agent_connection_store::{
@@ -556,7 +555,6 @@ pub struct ConnectedServerState {
active_id: Option<acp::SessionId>,
pub(crate) threads: HashMap<acp::SessionId, Entity<ThreadView>>,
connection: Rc<dyn AgentConnection>,
- history: Option<Entity<ThreadHistory>>,
conversation: Entity<Conversation>,
_connection_entry_subscription: Subscription,
}
@@ -791,11 +789,8 @@ impl ConversationView {
};
let load_task = cx.spawn_in(window, async move |this, cx| {
- let (connection, history) = match connect_result.await {
- Ok(AgentConnectedState {
- connection,
- history,
- }) => (connection, history),
+ let connection = match connect_result.await {
+ Ok(AgentConnectedState { connection, .. }) => connection,
Err(err) => {
this.update_in(cx, |this, window, cx| {
this.handle_load_error(err, window, cx);
@@ -891,7 +886,6 @@ impl ConversationView {
conversation.clone(),
resumed_without_history,
initial_content,
- history.clone(),
window,
cx,
);
@@ -912,7 +906,6 @@ impl ConversationView {
active_id: Some(root_session_id.clone()),
threads: HashMap::from_iter([(root_session_id, current)]),
conversation,
- history,
_connection_entry_subscription: connection_entry_subscription,
}),
cx,
@@ -945,7 +938,6 @@ impl ConversationView {
conversation: Entity<Conversation>,
resumed_without_history: bool,
initial_content: Option<AgentInitialContent>,
- history: Option<Entity<ThreadHistory>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ThreadView> {
@@ -962,7 +954,6 @@ impl ConversationView {
self.workspace.clone(),
self.project.downgrade(),
self.thread_store.clone(),
- history.as_ref().map(|h| h.downgrade()),
self.prompt_store.clone(),
session_capabilities.clone(),
self.agent.agent_id(),
@@ -1130,7 +1121,6 @@ impl ConversationView {
resumed_without_history,
self.project.downgrade(),
self.thread_store.clone(),
- history,
self.prompt_store.clone(),
initial_content,
subscriptions,
@@ -1212,7 +1202,6 @@ impl ConversationView {
threads: HashMap::default(),
connection,
conversation: cx.new(|_cx| Conversation::default()),
- history: None,
_connection_entry_subscription: Subscription::new(|| {}),
}),
cx,
@@ -1787,9 +1776,9 @@ impl ConversationView {
cx.spawn_in(window, async move |this, cx| {
let subagent_thread = subagent_thread_task.await?;
this.update_in(cx, |this, window, cx| {
- let Some((conversation, history)) = this
+ let Some(conversation) = this
.as_connected()
- .map(|connected| (connected.conversation.clone(), connected.history.clone()))
+ .map(|connected| connected.conversation.clone())
else {
return;
};
@@ -1797,15 +1786,8 @@ impl ConversationView {
conversation.update(cx, |conversation, cx| {
conversation.register_thread(subagent_thread.clone(), cx);
});
- let view = this.new_thread_view(
- subagent_thread,
- conversation,
- false,
- None,
- history,
- window,
- cx,
- );
+ let view =
+ this.new_thread_view(subagent_thread, conversation, false, None, window, cx);
let Some(connected) = this.as_connected_mut() else {
return;
};
@@ -2264,7 +2246,6 @@ impl ConversationView {
let Some(connected) = self.as_connected() else {
return;
};
- let history = connected.history.as_ref().map(|h| h.downgrade());
let Some(thread) = connected.active_view() else {
return;
};
@@ -2305,7 +2286,6 @@ impl ConversationView {
workspace.clone(),
project.clone(),
None,
- history.clone(),
None,
session_capabilities.clone(),
agent_name.clone(),
@@ -2709,10 +2689,6 @@ impl ConversationView {
Self::handle_auth_required(this, AuthRequired::new(), agent_id, connection, window, cx);
})
}
-
- pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
- self.as_connected().and_then(|c| c.history.as_ref())
- }
}
fn loading_contents_spinner(size: IconSize) -> AnyElement {
@@ -2862,9 +2838,7 @@ fn plan_label_markdown_style(
#[cfg(test)]
pub(crate) mod tests {
- use acp_thread::{
- AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
- };
+ use acp_thread::StubAgentConnection;
use action_log::ActionLog;
use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
use agent_client_protocol::SessionId;
@@ -3029,66 +3003,6 @@ pub(crate) mod tests {
);
}
- #[gpui::test]
- async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
- let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
-
- // Use a connection that provides a session list so ThreadHistory is created
- let (conversation_view, history, cx) = setup_thread_view_with_history(
- StubAgentServer::new(SessionHistoryConnection::new(vec![session_a.clone()])),
- cx,
- )
- .await;
-
- // Initially has session_a from the connection's session list
- active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
- assert_eq!(view.recent_history_entries.len(), 1);
- assert_eq!(
- view.recent_history_entries[0].session_id,
- session_a.session_id
- );
- });
-
- // Swap to a different session list
- let list_b: Rc<dyn AgentSessionList> =
- Rc::new(StubSessionList::new(vec![session_b.clone()]));
- history.update(cx, |history, cx| {
- history.set_session_list(list_b, cx);
- });
- cx.run_until_parked();
-
- active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
- assert_eq!(view.recent_history_entries.len(), 1);
- assert_eq!(
- view.recent_history_entries[0].session_id,
- session_b.session_id
- );
- });
- }
-
- #[gpui::test]
- async fn test_new_thread_creation_triggers_session_list_refresh(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session = AgentSessionInfo::new(SessionId::new("history-session"));
- let (conversation_view, _history, cx) = setup_thread_view_with_history(
- StubAgentServer::new(SessionHistoryConnection::new(vec![session.clone()])),
- cx,
- )
- .await;
-
- active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
- assert_eq!(view.recent_history_entries.len(), 1);
- assert_eq!(
- view.recent_history_entries[0].session_id,
- session.session_id
- );
- });
- }
-
#[gpui::test]
async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) {
init_test(cx);
@@ -4040,22 +3954,7 @@ pub(crate) mod tests {
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,
) -> (Entity<ConversationView>, &mut VisualTestContext) {
- let (conversation_view, _history, cx) =
- setup_conversation_view_with_history_and_initial_content(agent, None, cx).await;
- (conversation_view, cx)
- }
-
- async fn setup_thread_view_with_history(
- agent: impl AgentServer + 'static,
- cx: &mut TestAppContext,
- ) -> (
- Entity<ConversationView>,
- Entity<ThreadHistory>,
- &mut VisualTestContext,
- ) {
- let (conversation_view, history, cx) =
- setup_conversation_view_with_history_and_initial_content(agent, None, cx).await;
- (conversation_view, history.expect("Missing history"), cx)
+ setup_conversation_view_with_initial_content_opt(agent, None, cx).await
}
async fn setup_conversation_view_with_initial_content(
@@ -4063,25 +3962,14 @@ pub(crate) mod tests {
initial_content: AgentInitialContent,
cx: &mut TestAppContext,
) -> (Entity<ConversationView>, &mut VisualTestContext) {
- let (conversation_view, _history, cx) =
- setup_conversation_view_with_history_and_initial_content(
- agent,
- Some(initial_content),
- cx,
- )
- .await;
- (conversation_view, cx)
+ setup_conversation_view_with_initial_content_opt(agent, Some(initial_content), cx).await
}
- async fn setup_conversation_view_with_history_and_initial_content(
+ async fn setup_conversation_view_with_initial_content_opt(
agent: impl AgentServer + 'static,
initial_content: Option<AgentInitialContent>,
cx: &mut TestAppContext,
- ) -> (
- Entity<ConversationView>,
- Option<Entity<ThreadHistory>>,
- &mut VisualTestContext,
- ) {
+ ) -> (Entity<ConversationView>, &mut VisualTestContext) {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (multi_workspace, cx) =
@@ -4117,14 +4005,7 @@ pub(crate) mod tests {
});
cx.run_until_parked();
- let history = cx.update(|_window, cx| {
- connection_store
- .read(cx)
- .entry(&agent_key)
- .and_then(|e| e.read(cx).history().cloned())
- });
-
- (conversation_view, history, cx)
+ (conversation_view, cx)
}
fn add_to_workspace(conversation_view: Entity<ConversationView>, cx: &mut VisualTestContext) {
@@ -4256,42 +4137,6 @@ pub(crate) mod tests {
}
}
- #[derive(Clone)]
- struct StubSessionList {
- sessions: Vec<AgentSessionInfo>,
- }
-
- impl StubSessionList {
- fn new(sessions: Vec<AgentSessionInfo>) -> Self {
- Self { sessions }
- }
- }
-
- impl AgentSessionList for StubSessionList {
- fn list_sessions(
- &self,
- _request: AgentSessionListRequest,
- _cx: &mut App,
- ) -> Task<anyhow::Result<AgentSessionListResponse>> {
- Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
- }
-
- fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
- self
- }
- }
-
- #[derive(Clone)]
- struct SessionHistoryConnection {
- sessions: Vec<AgentSessionInfo>,
- }
-
- impl SessionHistoryConnection {
- fn new(sessions: Vec<AgentSessionInfo>) -> Self {
- Self { sessions }
- }
- }
-
fn build_test_thread(
connection: Rc<dyn AgentConnection>,
project: Entity<Project>,
@@ -4320,67 +4165,6 @@ pub(crate) mod tests {
})
}
- impl AgentConnection for SessionHistoryConnection {
- fn agent_id(&self) -> AgentId {
- AgentId::new("history-connection")
- }
-
- fn telemetry_id(&self) -> SharedString {
- "history-connection".into()
- }
-
- fn new_session(
- self: Rc<Self>,
- project: Entity<Project>,
- _work_dirs: PathList,
- cx: &mut App,
- ) -> Task<anyhow::Result<Entity<AcpThread>>> {
- let thread = build_test_thread(
- self,
- project,
- "SessionHistoryConnection",
- SessionId::new("history-session"),
- cx,
- );
- Task::ready(Ok(thread))
- }
-
- fn supports_load_session(&self) -> bool {
- true
- }
-
- fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
- Some(Rc::new(StubSessionList::new(self.sessions.clone())))
- }
-
- fn auth_methods(&self) -> &[acp::AuthMethod] {
- &[]
- }
-
- fn authenticate(
- &self,
- _method_id: acp::AuthMethodId,
- _cx: &mut App,
- ) -> Task<anyhow::Result<()>> {
- Task::ready(Ok(()))
- }
-
- fn prompt(
- &self,
- _id: acp_thread::UserMessageId,
- _params: acp::PromptRequest,
- _cx: &mut App,
- ) -> Task<anyhow::Result<acp::PromptResponse>> {
- Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
- }
-
- fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
-
- fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
- self
- }
- }
-
#[derive(Clone)]
struct ResumeOnlyAgentConnection;
@@ -326,14 +326,10 @@ pub struct ThreadView {
pub add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
pub thinking_effort_menu_handle: PopoverMenuHandle<ContextMenu>,
pub project: WeakEntity<Project>,
- pub recent_history_entries: Vec<AgentSessionInfo>,
- pub hovered_recent_history_item: Option<usize>,
pub show_external_source_prompt_warning: bool,
pub show_codex_windows_warning: bool,
pub multi_root_callout_dismissed: bool,
pub generating_indicator_in_list: bool,
- pub history: Option<Entity<ThreadHistory>>,
- pub _history_subscription: Option<Subscription>,
}
impl Focusable for ThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -375,7 +371,6 @@ impl ThreadView {
resumed_without_history: bool,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
- history: Option<Entity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
initial_content: Option<AgentInitialContent>,
mut subscriptions: Vec<Subscription>,
@@ -388,12 +383,6 @@ impl ThreadView {
let has_commands = !session_capabilities.read().available_commands().is_empty();
let placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
- let history_subscription = history.as_ref().map(|h| {
- cx.observe(h, |this, history, cx| {
- this.update_recent_history_from_cache(&history, cx);
- })
- });
-
let mut should_auto_submit = false;
let mut show_external_source_prompt_warning = false;
@@ -402,7 +391,6 @@ impl ThreadView {
workspace.clone(),
project.clone(),
thread_store,
- history.as_ref().map(|h| h.downgrade()),
prompt_store,
session_capabilities.clone(),
agent_id.clone(),
@@ -501,11 +489,6 @@ impl ThreadView {
}));
}));
- let recent_history_entries = history
- .as_ref()
- .map(|h| h.read(cx).get_recent_sessions(3))
- .unwrap_or_default();
-
let mut this = Self {
session_id,
parent_session_id,
@@ -568,11 +551,7 @@ impl ThreadView {
add_context_menu_handle: PopoverMenuHandle::default(),
thinking_effort_menu_handle: PopoverMenuHandle::default(),
project,
- recent_history_entries,
- hovered_recent_history_item: None,
show_external_source_prompt_warning,
- history,
- _history_subscription: history_subscription,
show_codex_windows_warning,
multi_root_callout_dismissed: false,
generating_indicator_in_list: false,
@@ -8666,16 +8645,6 @@ impl ThreadView {
.into_any_element()
}
- fn update_recent_history_from_cache(
- &mut self,
- history: &Entity<ThreadHistory>,
- cx: &mut Context<Self>,
- ) {
- self.recent_history_entries = history.read(cx).get_recent_sessions(3);
- self.hovered_recent_history_item = None;
- cx.notify();
- }
-
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Callout {
Callout::new()
.icon(IconName::Warning)
@@ -1,6 +1,5 @@
use std::ops::Range;
-use super::thread_history::ThreadHistory;
use acp_thread::{AcpThread, AgentThreadEntry};
use agent::ThreadStore;
use agent_client_protocol::ToolCallId;
@@ -26,7 +25,6 @@ pub struct EntryViewState {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
session_capabilities: SharedSessionCapabilities,
@@ -38,7 +36,6 @@ impl EntryViewState {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
session_capabilities: SharedSessionCapabilities,
agent_id: AgentId,
@@ -47,7 +44,6 @@ impl EntryViewState {
workspace,
project,
thread_store,
- history,
prompt_store,
entries: Vec::new(),
session_capabilities,
@@ -90,7 +86,6 @@ impl EntryViewState {
self.workspace.clone(),
self.project.clone(),
self.thread_store.clone(),
- self.history.clone(),
self.prompt_store.clone(),
self.session_capabilities.clone(),
self.agent_id.clone(),
@@ -544,14 +539,12 @@ mod tests {
});
let thread_store = None;
- let history: Option<gpui::WeakEntity<crate::ThreadHistory>> = None;
let view_state = cx.new(|_cx| {
EntryViewState::new(
workspace.downgrade(),
project.downgrade(),
thread_store,
- history,
None,
Arc::new(RwLock::new(SessionCapabilities::default())),
"Test Agent".into(),
@@ -6,7 +6,6 @@ use std::ops::Range;
use std::sync::Arc;
use uuid::Uuid;
-use crate::ThreadHistory;
use crate::context::load_context;
use crate::mention_set::MentionSet;
use crate::{
@@ -231,11 +230,6 @@ impl InlineAssistant {
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = agent_panel.thread_store().clone();
- let history = agent_panel
- .connection_store()
- .read(cx)
- .entry(&crate::Agent::NativeAgent)
- .and_then(|s| s.read(cx).history().cloned());
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -247,7 +241,6 @@ impl InlineAssistant {
workspace.project().downgrade(),
thread_store,
prompt_store,
- history.as_ref().map(|h| h.downgrade()),
action.prompt.clone(),
window,
cx,
@@ -262,7 +255,6 @@ impl InlineAssistant {
workspace.project().downgrade(),
thread_store,
prompt_store,
- history.as_ref().map(|h| h.downgrade()),
action.prompt.clone(),
window,
cx,
@@ -446,7 +438,6 @@ impl InlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
codegen_ranges: &[Range<Anchor>],
@@ -493,7 +484,6 @@ impl InlineAssistant {
self.fs.clone(),
thread_store.clone(),
prompt_store.clone(),
- history.clone(),
project.clone(),
workspace.clone(),
window,
@@ -585,7 +575,6 @@ impl InlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -604,7 +593,6 @@ impl InlineAssistant {
project,
thread_store,
prompt_store,
- history,
initial_prompt,
window,
&codegen_ranges,
@@ -630,7 +618,6 @@ impl InlineAssistant {
workspace: Entity<Workspace>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
window: &mut Window,
cx: &mut App,
) -> InlineAssistId {
@@ -650,7 +637,6 @@ impl InlineAssistant {
project,
thread_store,
prompt_store,
- history,
Some(initial_prompt),
window,
&[range],
@@ -1975,7 +1961,6 @@ pub mod evals {
project.downgrade(),
thread_store,
None,
- None,
Some(prompt),
window,
cx,
@@ -1,4 +1,3 @@
-use crate::ThreadHistory;
use agent::ThreadStore;
use agent_settings::AgentSettings;
use collections::{HashMap, VecDeque};
@@ -64,7 +63,6 @@ pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
mode: PromptEditorMode,
mention_set: Entity<MentionSet>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
model_selector: Entity<AgentModelSelector>,
@@ -335,7 +333,6 @@ impl<T: 'static> PromptEditor<T> {
PromptEditorCompletionProviderDelegate,
cx.weak_entity(),
self.mention_set.clone(),
- self.history.clone(),
self.prompt_store.clone(),
self.workspace.clone(),
))));
@@ -1227,7 +1224,6 @@ impl PromptEditor<BufferCodegen> {
fs: Arc<dyn Fs>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
@@ -1274,7 +1270,6 @@ impl PromptEditor<BufferCodegen> {
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
mention_set,
- history,
prompt_store,
workspace,
model_selector: cx.new(|cx| {
@@ -1386,7 +1381,6 @@ impl PromptEditor<TerminalCodegen> {
fs: Arc<dyn Fs>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
@@ -1428,7 +1422,6 @@ impl PromptEditor<TerminalCodegen> {
let mut this = Self {
editor: prompt_editor.clone(),
mention_set,
- history,
prompt_store,
workspace,
model_selector: cx.new(|cx| {
@@ -1,6 +1,5 @@
use crate::DEFAULT_THREAD_TITLE;
use crate::SendImmediately;
-use crate::ThreadHistory;
use crate::{
ChatWithFollow,
completion_provider::{
@@ -394,7 +393,6 @@ impl MessageEditor {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
session_capabilities: SharedSessionCapabilities,
agent_id: AgentId,
@@ -458,7 +456,6 @@ impl MessageEditor {
},
editor.downgrade(),
mention_set.clone(),
- history,
prompt_store.clone(),
workspace.clone(),
));
@@ -2053,7 +2050,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -2155,7 +2151,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
session_capabilities.clone(),
"Claude Agent".into(),
"Test",
@@ -2322,7 +2317,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
session_capabilities.clone(),
"Test Agent".into(),
"Test",
@@ -2549,7 +2543,6 @@ mod tests {
project.downgrade(),
Some(thread_store),
None,
- None,
session_capabilities.clone(),
"Test Agent".into(),
"Test",
@@ -3042,7 +3035,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3144,7 +3136,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3214,7 +3205,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3268,7 +3258,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3326,7 +3315,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3385,7 +3373,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3448,7 +3435,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3609,7 +3595,6 @@ mod tests {
project.downgrade(),
thread_store.clone(),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3724,7 +3709,6 @@ mod tests {
project.downgrade(),
Some(thread_store.clone()),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3804,7 +3788,6 @@ mod tests {
project.downgrade(),
Some(thread_store),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -3903,7 +3886,6 @@ mod tests {
project.downgrade(),
Some(thread_store),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -4159,7 +4141,6 @@ mod tests {
project.downgrade(),
Some(thread_store),
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -4253,7 +4234,6 @@ mod tests {
project.downgrade(),
None,
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -4403,7 +4383,6 @@ mod tests {
project.downgrade(),
None,
None,
- None,
Default::default(),
"Test Agent".into(),
"Test",
@@ -1,5 +1,4 @@
use crate::{
- ThreadHistory,
context::load_context,
inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
@@ -66,7 +65,6 @@ impl TerminalInlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
- history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -92,7 +90,6 @@ impl TerminalInlineAssistant {
self.fs.clone(),
thread_store.clone(),
prompt_store.clone(),
- history,
project.clone(),
workspace.clone(),
window,
@@ -1,772 +0,0 @@
-use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate};
-use agent_client_protocol as acp;
-use gpui::{App, Task};
-use std::rc::Rc;
-use ui::prelude::*;
-
-pub struct ThreadHistory {
- session_list: Rc<dyn AgentSessionList>,
- sessions: Vec<AgentSessionInfo>,
- _refresh_task: Task<()>,
- _watch_task: Option<Task<()>>,
-}
-
-impl ThreadHistory {
- pub fn new(session_list: Rc<dyn AgentSessionList>, cx: &mut Context<Self>) -> Self {
- let mut this = Self {
- session_list,
- sessions: Vec::new(),
- _refresh_task: Task::ready(()),
- _watch_task: None,
- };
-
- this.start_watching(cx);
- this
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub fn set_session_list(
- &mut self,
- session_list: Rc<dyn AgentSessionList>,
- cx: &mut Context<Self>,
- ) {
- if Rc::ptr_eq(&self.session_list, &session_list) {
- return;
- }
-
- self.session_list = session_list;
- self.sessions.clear();
- self._refresh_task = Task::ready(());
- self.start_watching(cx);
- }
-
- fn start_watching(&mut self, cx: &mut Context<Self>) {
- let Some(rx) = self.session_list.watch(cx) else {
- self._watch_task = None;
- self.refresh_sessions(false, cx);
- return;
- };
- self.session_list.notify_refresh();
-
- self._watch_task = Some(cx.spawn(async move |this, cx| {
- while let Ok(first_update) = rx.recv().await {
- let mut updates = vec![first_update];
- while let Ok(update) = rx.try_recv() {
- updates.push(update);
- }
-
- this.update(cx, |this, cx| {
- let needs_refresh = updates
- .iter()
- .any(|u| matches!(u, SessionListUpdate::Refresh));
-
- if needs_refresh {
- this.refresh_sessions(false, cx);
- } else {
- for update in updates {
- if let SessionListUpdate::SessionInfo { session_id, update } = update {
- this.apply_info_update(session_id, update, cx);
- }
- }
- }
- })
- .ok();
- }
- }));
- }
-
- pub(crate) fn refresh_full_history(&mut self, cx: &mut Context<Self>) {
- self.refresh_sessions(true, cx);
- }
-
- fn apply_info_update(
- &mut self,
- session_id: acp::SessionId,
- info_update: acp::SessionInfoUpdate,
- cx: &mut Context<Self>,
- ) {
- let Some(session) = self
- .sessions
- .iter_mut()
- .find(|s| s.session_id == session_id)
- else {
- return;
- };
-
- match info_update.title {
- acp::MaybeUndefined::Value(title) => {
- session.title = Some(title.into());
- }
- acp::MaybeUndefined::Null => {
- session.title = None;
- }
- acp::MaybeUndefined::Undefined => {}
- }
- match info_update.updated_at {
- acp::MaybeUndefined::Value(date_str) => {
- if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&date_str) {
- session.updated_at = Some(dt.with_timezone(&chrono::Utc));
- }
- }
- acp::MaybeUndefined::Null => {
- session.updated_at = None;
- }
- acp::MaybeUndefined::Undefined => {}
- }
- if let Some(meta) = info_update.meta {
- session.meta = Some(meta);
- }
-
- cx.notify();
- }
-
- fn refresh_sessions(&mut self, load_all_pages: bool, cx: &mut Context<Self>) {
- let session_list = self.session_list.clone();
-
- self._refresh_task = cx.spawn(async move |this, cx| {
- let mut cursor: Option<String> = None;
- let mut is_first_page = true;
-
- loop {
- let request = AgentSessionListRequest {
- cursor: cursor.clone(),
- ..Default::default()
- };
- let task = cx.update(|cx| session_list.list_sessions(request, cx));
- let response = match task.await {
- Ok(response) => response,
- Err(error) => {
- log::error!("Failed to load session history: {error:#}");
- return;
- }
- };
-
- let acp_thread::AgentSessionListResponse {
- sessions: page_sessions,
- next_cursor,
- ..
- } = response;
-
- this.update(cx, |this, cx| {
- if is_first_page {
- this.sessions = page_sessions;
- } else {
- this.sessions.extend(page_sessions);
- }
- cx.notify();
- })
- .ok();
-
- is_first_page = false;
- if !load_all_pages {
- break;
- }
-
- match next_cursor {
- Some(next_cursor) => {
- if cursor.as_ref() == Some(&next_cursor) {
- log::warn!(
- "Session list pagination returned the same cursor; stopping to avoid a loop."
- );
- break;
- }
- cursor = Some(next_cursor);
- }
- None => break,
- }
- }
- });
- }
-
- pub(crate) fn is_empty(&self) -> bool {
- self.sessions.is_empty()
- }
-
- pub fn refresh(&mut self, _cx: &mut Context<Self>) {
- self.session_list.notify_refresh();
- }
-
- pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
- self.sessions
- .iter()
- .find(|entry| &entry.session_id == session_id)
- .cloned()
- }
-
- pub(crate) fn sessions(&self) -> &[AgentSessionInfo] {
- &self.sessions
- }
-
- pub(crate) fn get_recent_sessions(&self, limit: usize) -> Vec<AgentSessionInfo> {
- self.sessions.iter().take(limit).cloned().collect()
- }
-
- pub fn supports_delete(&self) -> bool {
- self.session_list.supports_delete()
- }
-
- pub(crate) fn delete_session(
- &self,
- session_id: &acp::SessionId,
- cx: &mut App,
- ) -> Task<anyhow::Result<()>> {
- self.session_list.delete_session(session_id, cx)
- }
-
- pub(crate) fn delete_sessions(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
- self.session_list.delete_sessions(cx)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use acp_thread::AgentSessionListResponse;
- use gpui::TestAppContext;
- use std::{
- any::Any,
- sync::{Arc, Mutex},
- };
-
- fn init_test(cx: &mut TestAppContext) {
- cx.update(|cx| {
- let settings_store = settings::SettingsStore::test(cx);
- cx.set_global(settings_store);
- theme_settings::init(theme::LoadThemes::JustBase, cx);
- });
- }
-
- #[derive(Clone)]
- struct TestSessionList {
- sessions: Vec<AgentSessionInfo>,
- updates_tx: smol::channel::Sender<SessionListUpdate>,
- updates_rx: smol::channel::Receiver<SessionListUpdate>,
- }
-
- impl TestSessionList {
- fn new(sessions: Vec<AgentSessionInfo>) -> Self {
- let (tx, rx) = smol::channel::unbounded();
- Self {
- sessions,
- updates_tx: tx,
- updates_rx: rx,
- }
- }
-
- fn send_update(&self, update: SessionListUpdate) {
- self.updates_tx.try_send(update).ok();
- }
- }
-
- impl AgentSessionList for TestSessionList {
- fn list_sessions(
- &self,
- _request: AgentSessionListRequest,
- _cx: &mut App,
- ) -> Task<anyhow::Result<AgentSessionListResponse>> {
- Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
- }
-
- fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
- Some(self.updates_rx.clone())
- }
-
- fn notify_refresh(&self) {
- self.send_update(SessionListUpdate::Refresh);
- }
-
- fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
- self
- }
- }
-
- #[derive(Clone)]
- struct PaginatedTestSessionList {
- first_page_sessions: Vec<AgentSessionInfo>,
- second_page_sessions: Vec<AgentSessionInfo>,
- requested_cursors: Arc<Mutex<Vec<Option<String>>>>,
- async_responses: bool,
- updates_tx: smol::channel::Sender<SessionListUpdate>,
- updates_rx: smol::channel::Receiver<SessionListUpdate>,
- }
-
- impl PaginatedTestSessionList {
- fn new(
- first_page_sessions: Vec<AgentSessionInfo>,
- second_page_sessions: Vec<AgentSessionInfo>,
- ) -> Self {
- let (tx, rx) = smol::channel::unbounded();
- Self {
- first_page_sessions,
- second_page_sessions,
- requested_cursors: Arc::new(Mutex::new(Vec::new())),
- async_responses: false,
- updates_tx: tx,
- updates_rx: rx,
- }
- }
-
- fn with_async_responses(mut self) -> Self {
- self.async_responses = true;
- self
- }
-
- fn requested_cursors(&self) -> Vec<Option<String>> {
- self.requested_cursors.lock().unwrap().clone()
- }
-
- fn clear_requested_cursors(&self) {
- self.requested_cursors.lock().unwrap().clear()
- }
-
- fn send_update(&self, update: SessionListUpdate) {
- self.updates_tx.try_send(update).ok();
- }
- }
-
- impl AgentSessionList for PaginatedTestSessionList {
- fn list_sessions(
- &self,
- request: AgentSessionListRequest,
- cx: &mut App,
- ) -> Task<anyhow::Result<AgentSessionListResponse>> {
- let requested_cursors = self.requested_cursors.clone();
- let first_page_sessions = self.first_page_sessions.clone();
- let second_page_sessions = self.second_page_sessions.clone();
-
- let respond = move || {
- requested_cursors
- .lock()
- .unwrap()
- .push(request.cursor.clone());
-
- match request.cursor.as_deref() {
- None => AgentSessionListResponse {
- sessions: first_page_sessions,
- next_cursor: Some("page-2".to_string()),
- meta: None,
- },
- Some("page-2") => AgentSessionListResponse::new(second_page_sessions),
- _ => AgentSessionListResponse::new(Vec::new()),
- }
- };
-
- if self.async_responses {
- cx.foreground_executor().spawn(async move {
- smol::future::yield_now().await;
- Ok(respond())
- })
- } else {
- Task::ready(Ok(respond()))
- }
- }
-
- fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
- Some(self.updates_rx.clone())
- }
-
- fn notify_refresh(&self) {
- self.send_update(SessionListUpdate::Refresh);
- }
-
- fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
- self
- }
- }
-
- fn test_session(session_id: &str, title: &str) -> AgentSessionInfo {
- AgentSessionInfo {
- session_id: acp::SessionId::new(session_id),
- work_dirs: None,
- title: Some(title.to_string().into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }
- }
-
- #[gpui::test]
- async fn test_refresh_only_loads_first_page_by_default(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_list = Rc::new(PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- ));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 1);
- assert_eq!(
- history.sessions[0].session_id,
- acp::SessionId::new("session-1")
- );
- });
- assert_eq!(session_list.requested_cursors(), vec![None]);
- }
-
- #[gpui::test]
- async fn test_enabling_full_pagination_loads_all_pages(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_list = Rc::new(PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- ));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
- session_list.clear_requested_cursors();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 2);
- assert_eq!(
- history.sessions[0].session_id,
- acp::SessionId::new("session-1")
- );
- assert_eq!(
- history.sessions[1].session_id,
- acp::SessionId::new("session-2")
- );
- });
- assert_eq!(
- session_list.requested_cursors(),
- vec![None, Some("page-2".to_string())]
- );
- }
-
- #[gpui::test]
- async fn test_standard_refresh_replaces_with_first_page_after_full_history_refresh(
- cx: &mut TestAppContext,
- ) {
- init_test(cx);
-
- let session_list = Rc::new(PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- ));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
- session_list.clear_requested_cursors();
-
- history.update(cx, |history, cx| {
- history.refresh(cx);
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 1);
- assert_eq!(
- history.sessions[0].session_id,
- acp::SessionId::new("session-1")
- );
- });
- assert_eq!(session_list.requested_cursors(), vec![None]);
- }
-
- #[gpui::test]
- async fn test_re_entering_full_pagination_reloads_all_pages(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_list = Rc::new(PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- ));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
- session_list.clear_requested_cursors();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 2);
- });
- assert_eq!(
- session_list.requested_cursors(),
- vec![None, Some("page-2".to_string())]
- );
- }
-
- #[gpui::test]
- async fn test_partial_refresh_batch_drops_non_first_page_sessions(cx: &mut TestAppContext) {
- init_test(cx);
-
- let second_page_session_id = acp::SessionId::new("session-2");
- let session_list = Rc::new(PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- ));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
-
- session_list.clear_requested_cursors();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: second_page_session_id.clone(),
- update: acp::SessionInfoUpdate::new().title("Updated Second"),
- });
- session_list.send_update(SessionListUpdate::Refresh);
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 1);
- assert_eq!(
- history.sessions[0].session_id,
- acp::SessionId::new("session-1")
- );
- assert!(
- history
- .sessions
- .iter()
- .all(|session| session.session_id != second_page_session_id)
- );
- });
- assert_eq!(session_list.requested_cursors(), vec![None]);
- }
-
- #[gpui::test]
- async fn test_full_pagination_works_with_async_page_fetches(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_list = Rc::new(
- PaginatedTestSessionList::new(
- vec![test_session("session-1", "First")],
- vec![test_session("session-2", "Second")],
- )
- .with_async_responses(),
- );
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
- session_list.clear_requested_cursors();
-
- history.update(cx, |history, cx| history.refresh_full_history(cx));
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 2);
- });
- assert_eq!(
- session_list.requested_cursors(),
- vec![None, Some("page-2".to_string())]
- );
- }
-
- #[gpui::test]
- async fn test_apply_info_update_title(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("test-session");
- let sessions = vec![AgentSessionInfo {
- session_id: session_id.clone(),
- work_dirs: None,
- title: Some("Original Title".into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new().title("New Title"),
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- let session = history.sessions.iter().find(|s| s.session_id == session_id);
- assert_eq!(
- session.unwrap().title.as_ref().map(|s| s.as_ref()),
- Some("New Title")
- );
- });
- }
-
- #[gpui::test]
- async fn test_apply_info_update_clears_title_with_null(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("test-session");
- let sessions = vec![AgentSessionInfo {
- session_id: session_id.clone(),
- work_dirs: None,
- title: Some("Original Title".into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new().title(None::<String>),
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- let session = history.sessions.iter().find(|s| s.session_id == session_id);
- assert_eq!(session.unwrap().title, None);
- });
- }
-
- #[gpui::test]
- async fn test_apply_info_update_ignores_undefined_fields(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("test-session");
- let sessions = vec![AgentSessionInfo {
- session_id: session_id.clone(),
- work_dirs: None,
- title: Some("Original Title".into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new(),
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- let session = history.sessions.iter().find(|s| s.session_id == session_id);
- assert_eq!(
- session.unwrap().title.as_ref().map(|s| s.as_ref()),
- Some("Original Title")
- );
- });
- }
-
- #[gpui::test]
- async fn test_multiple_info_updates_applied_in_order(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("test-session");
- let sessions = vec![AgentSessionInfo {
- session_id: session_id.clone(),
- work_dirs: None,
- title: None,
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new().title("First Title"),
- });
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new().title("Second Title"),
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- let session = history.sessions.iter().find(|s| s.session_id == session_id);
- assert_eq!(
- session.unwrap().title.as_ref().map(|s| s.as_ref()),
- Some("Second Title")
- );
- });
- }
-
- #[gpui::test]
- async fn test_refresh_supersedes_info_updates(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("test-session");
- let sessions = vec![AgentSessionInfo {
- session_id: session_id.clone(),
- work_dirs: None,
- title: Some("Server Title".into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: session_id.clone(),
- update: acp::SessionInfoUpdate::new().title("Local Update"),
- });
- session_list.send_update(SessionListUpdate::Refresh);
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- let session = history.sessions.iter().find(|s| s.session_id == session_id);
- assert_eq!(
- session.unwrap().title.as_ref().map(|s| s.as_ref()),
- Some("Server Title")
- );
- });
- }
-
- #[gpui::test]
- async fn test_info_update_for_unknown_session_is_ignored(cx: &mut TestAppContext) {
- init_test(cx);
-
- let session_id = acp::SessionId::new("known-session");
- let sessions = vec![AgentSessionInfo {
- session_id,
- work_dirs: None,
- title: Some("Original".into()),
- updated_at: None,
- created_at: None,
- meta: None,
- }];
- let session_list = Rc::new(TestSessionList::new(sessions));
-
- let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
- cx.run_until_parked();
-
- session_list.send_update(SessionListUpdate::SessionInfo {
- session_id: acp::SessionId::new("unknown-session"),
- update: acp::SessionInfoUpdate::new().title("Should Be Ignored"),
- });
- cx.run_until_parked();
-
- history.update(cx, |history, _cx| {
- assert_eq!(history.sessions.len(), 1);
- assert_eq!(
- history.sessions[0].title.as_ref().map(|s| s.as_ref()),
- Some("Original")
- );
- });
- }
-}
@@ -1,751 +0,0 @@
-use crate::thread_history::ThreadHistory;
-use crate::{DEFAULT_THREAD_TITLE, RemoveHistory, RemoveSelectedThread};
-use acp_thread::AgentSessionInfo;
-use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
-use editor::{Editor, EditorEvent};
-use fuzzy::StringMatchCandidate;
-use gpui::{
- AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
- UniformListScrollHandle, Window, uniform_list,
-};
-use std::{fmt::Display, ops::Range};
-use text::Bias;
-use time::{OffsetDateTime, UtcOffset};
-use ui::{
- HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
- prelude::*,
-};
-
-pub(crate) fn thread_title(entry: &AgentSessionInfo) -> SharedString {
- entry
- .title
- .clone()
- .and_then(|title| if title.is_empty() { None } else { Some(title) })
- .unwrap_or_else(|| DEFAULT_THREAD_TITLE.into())
-}
-
-pub struct ThreadHistoryView {
- history: Entity<ThreadHistory>,
- scroll_handle: UniformListScrollHandle,
- selected_index: usize,
- hovered_index: Option<usize>,
- search_editor: Entity<Editor>,
- search_query: SharedString,
- visible_items: Vec<ListItemType>,
- local_timezone: UtcOffset,
- confirming_delete_history: bool,
- _visible_items_task: Task<()>,
- _subscriptions: Vec<gpui::Subscription>,
-}
-
-enum ListItemType {
- BucketSeparator(TimeBucket),
- Entry {
- entry: AgentSessionInfo,
- format: EntryTimeFormat,
- },
- SearchResult {
- entry: AgentSessionInfo,
- positions: Vec<usize>,
- },
-}
-
-impl ListItemType {
- fn history_entry(&self) -> Option<&AgentSessionInfo> {
- match self {
- ListItemType::Entry { entry, .. } => Some(entry),
- ListItemType::SearchResult { entry, .. } => Some(entry),
- _ => None,
- }
- }
-}
-
-pub enum ThreadHistoryViewEvent {
- Open(AgentSessionInfo),
-}
-
-impl EventEmitter<ThreadHistoryViewEvent> for ThreadHistoryView {}
-
-impl ThreadHistoryView {
- pub fn new(
- history: Entity<ThreadHistory>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let search_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Search all threadsβ¦", window, cx);
- editor
- });
-
- let search_editor_subscription =
- cx.subscribe(&search_editor, |this, search_editor, event, cx| {
- if let EditorEvent::BufferEdited = event {
- let query = search_editor.read(cx).text(cx);
- if this.search_query != query {
- this.search_query = query.into();
- this.update_visible_items(false, cx);
- }
- }
- });
-
- let history_subscription = cx.observe(&history, |this, _, cx| {
- this.update_visible_items(true, cx);
- });
-
- let scroll_handle = UniformListScrollHandle::default();
-
- let mut this = Self {
- history,
- scroll_handle,
- selected_index: 0,
- hovered_index: None,
- visible_items: Default::default(),
- search_editor,
- local_timezone: UtcOffset::from_whole_seconds(
- chrono::Local::now().offset().local_minus_utc(),
- )
- .unwrap(),
- search_query: SharedString::default(),
- confirming_delete_history: false,
- _subscriptions: vec![search_editor_subscription, history_subscription],
- _visible_items_task: Task::ready(()),
- };
- this.update_visible_items(false, cx);
- this
- }
-
- pub fn history(&self) -> &Entity<ThreadHistory> {
- &self.history
- }
-
- fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
- let entries = self.history.read(cx).sessions().to_vec();
- let new_list_items = if self.search_query.is_empty() {
- self.add_list_separators(entries, cx)
- } else {
- self.filter_search_results(entries, cx)
- };
- let selected_history_entry = if preserve_selected_item {
- self.selected_history_entry().cloned()
- } else {
- None
- };
-
- self._visible_items_task = cx.spawn(async move |this, cx| {
- let new_visible_items = new_list_items.await;
- this.update(cx, |this, cx| {
- let new_selected_index = if let Some(history_entry) = selected_history_entry {
- new_visible_items
- .iter()
- .position(|visible_entry| {
- visible_entry
- .history_entry()
- .is_some_and(|entry| entry.session_id == history_entry.session_id)
- })
- .unwrap_or(0)
- } else {
- 0
- };
-
- this.visible_items = new_visible_items;
- this.set_selected_index(new_selected_index, Bias::Right, cx);
- cx.notify();
- })
- .ok();
- });
- }
-
- fn add_list_separators(
- &self,
- entries: Vec<AgentSessionInfo>,
- cx: &App,
- ) -> Task<Vec<ListItemType>> {
- cx.background_spawn(async move {
- let mut items = Vec::with_capacity(entries.len() + 1);
- let mut bucket = None;
- let today = Local::now().naive_local().date();
-
- for entry in entries.into_iter() {
- let entry_bucket = entry
- .updated_at
- .map(|timestamp| {
- let entry_date = timestamp.with_timezone(&Local).naive_local().date();
- TimeBucket::from_dates(today, entry_date)
- })
- .unwrap_or(TimeBucket::All);
-
- if Some(entry_bucket) != bucket {
- bucket = Some(entry_bucket);
- items.push(ListItemType::BucketSeparator(entry_bucket));
- }
-
- items.push(ListItemType::Entry {
- entry,
- format: entry_bucket.into(),
- });
- }
- items
- })
- }
-
- fn filter_search_results(
- &self,
- entries: Vec<AgentSessionInfo>,
- cx: &App,
- ) -> Task<Vec<ListItemType>> {
- let query = self.search_query.clone();
- cx.background_spawn({
- let executor = cx.background_executor().clone();
- async move {
- let mut candidates = Vec::with_capacity(entries.len());
-
- for (idx, entry) in entries.iter().enumerate() {
- candidates.push(StringMatchCandidate::new(idx, &thread_title(entry)));
- }
-
- const MAX_MATCHES: usize = 100;
-
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- true,
- MAX_MATCHES,
- &Default::default(),
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|search_match| ListItemType::SearchResult {
- entry: entries[search_match.candidate_id].clone(),
- positions: search_match.positions,
- })
- .collect()
- }
- })
- }
-
- fn search_produced_no_matches(&self) -> bool {
- self.visible_items.is_empty() && !self.search_query.is_empty()
- }
-
- fn selected_history_entry(&self) -> Option<&AgentSessionInfo> {
- self.get_history_entry(self.selected_index)
- }
-
- fn get_history_entry(&self, visible_items_ix: usize) -> Option<&AgentSessionInfo> {
- self.visible_items.get(visible_items_ix)?.history_entry()
- }
-
- fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
- if self.visible_items.len() == 0 {
- self.selected_index = 0;
- return;
- }
- while matches!(
- self.visible_items.get(index),
- None | Some(ListItemType::BucketSeparator(..))
- ) {
- index = match bias {
- Bias::Left => {
- if index == 0 {
- self.visible_items.len() - 1
- } else {
- index - 1
- }
- }
- Bias::Right => {
- if index >= self.visible_items.len() - 1 {
- 0
- } else {
- index + 1
- }
- }
- };
- }
- self.selected_index = index;
- self.scroll_handle
- .scroll_to_item(index, ScrollStrategy::Top);
- cx.notify()
- }
-
- fn select_previous(
- &mut self,
- _: &menu::SelectPrevious,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if self.selected_index == 0 {
- self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
- } else {
- self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
- }
- }
-
- fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
- if self.selected_index == self.visible_items.len() - 1 {
- self.set_selected_index(0, Bias::Right, cx);
- } else {
- self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
- }
- }
-
- fn select_first(
- &mut self,
- _: &menu::SelectFirst,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.set_selected_index(0, Bias::Right, cx);
- }
-
- fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
- self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
- }
-
- fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirm_entry(self.selected_index, cx);
- }
-
- fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
- let Some(entry) = self.get_history_entry(ix) else {
- return;
- };
- cx.emit(ThreadHistoryViewEvent::Open(entry.clone()));
- }
-
- fn remove_selected_thread(
- &mut self,
- _: &RemoveSelectedThread,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.remove_thread(self.selected_index, cx)
- }
-
- fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
- let Some(entry) = self.get_history_entry(visible_item_ix) else {
- return;
- };
- if !self.history.read(cx).supports_delete() {
- return;
- }
- let session_id = entry.session_id.clone();
- self.history.update(cx, |history, cx| {
- history
- .delete_session(&session_id, cx)
- .detach_and_log_err(cx);
- });
- }
-
- fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- if !self.history.read(cx).supports_delete() {
- return;
- }
- self.history.update(cx, |history, cx| {
- history.delete_sessions(cx).detach_and_log_err(cx);
- });
- self.confirming_delete_history = false;
- cx.notify();
- }
-
- fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirming_delete_history = true;
- cx.notify();
- }
-
- fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
- self.confirming_delete_history = false;
- cx.notify();
- }
-
- fn render_list_items(
- &mut self,
- range: Range<usize>,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Vec<AnyElement> {
- self.visible_items
- .get(range.clone())
- .into_iter()
- .flatten()
- .enumerate()
- .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
- .collect()
- }
-
- fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
- match item {
- ListItemType::Entry { entry, format } => self
- .render_history_entry(entry, *format, ix, Vec::default(), cx)
- .into_any(),
- ListItemType::SearchResult { entry, positions } => self.render_history_entry(
- entry,
- EntryTimeFormat::DateAndTime,
- ix,
- positions.clone(),
- cx,
- ),
- ListItemType::BucketSeparator(bucket) => div()
- .px(DynamicSpacing::Base06.rems(cx))
- .pt_2()
- .pb_1()
- .child(
- Label::new(bucket.to_string())
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .into_any_element(),
- }
- }
-
- fn render_history_entry(
- &self,
- entry: &AgentSessionInfo,
- format: EntryTimeFormat,
- ix: usize,
- highlight_positions: Vec<usize>,
- cx: &Context<Self>,
- ) -> AnyElement {
- let selected = ix == self.selected_index;
- let hovered = Some(ix) == self.hovered_index;
- let entry_time = entry.updated_at;
- let display_text = match (format, entry_time) {
- (EntryTimeFormat::DateAndTime, Some(entry_time)) => {
- let now = Utc::now();
- let duration = now.signed_duration_since(entry_time);
- let days = duration.num_days();
-
- format!("{}d", days)
- }
- (EntryTimeFormat::TimeOnly, Some(entry_time)) => {
- format.format_timestamp(entry_time.timestamp(), self.local_timezone)
- }
- (_, None) => "β".to_string(),
- };
-
- let title = thread_title(entry);
- let full_date = entry_time
- .map(|time| {
- EntryTimeFormat::DateAndTime.format_timestamp(time.timestamp(), self.local_timezone)
- })
- .unwrap_or_else(|| "Unknown".to_string());
-
- let supports_delete = self.history.read(cx).supports_delete();
-
- h_flex()
- .w_full()
- .pb_1()
- .child(
- ListItem::new(ix)
- .rounded()
- .toggle_state(selected)
- .spacing(ListItemSpacing::Sparse)
- .start_slot(
- h_flex()
- .w_full()
- .gap_2()
- .justify_between()
- .child(
- HighlightedLabel::new(thread_title(entry), highlight_positions)
- .size(LabelSize::Small)
- .truncate(),
- )
- .child(
- Label::new(display_text)
- .color(Color::Muted)
- .size(LabelSize::XSmall),
- ),
- )
- .tooltip(move |_, cx| {
- Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
- })
- .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
- if *is_hovered {
- this.hovered_index = Some(ix);
- } else if this.hovered_index == Some(ix) {
- this.hovered_index = None;
- }
-
- cx.notify();
- }))
- .end_slot::<IconButton>(if hovered && supports_delete {
- Some(
- IconButton::new("delete", IconName::Trash)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .tooltip(move |_window, cx| {
- Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
- })
- .on_click(cx.listener(move |this, _, _, cx| {
- this.remove_thread(ix, cx);
- cx.stop_propagation()
- })),
- )
- } else {
- None
- })
- .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
- )
- .into_any_element()
- }
-}
-
-impl Focusable for ThreadHistoryView {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.search_editor.focus_handle(cx)
- }
-}
-
-impl Render for ThreadHistoryView {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let has_no_history = self.history.read(cx).is_empty();
- let supports_delete = self.history.read(cx).supports_delete();
-
- v_flex()
- .key_context("ThreadHistory")
- .size_full()
- .bg(cx.theme().colors().panel_background)
- .on_action(cx.listener(Self::select_previous))
- .on_action(cx.listener(Self::select_next))
- .on_action(cx.listener(Self::select_first))
- .on_action(cx.listener(Self::select_last))
- .on_action(cx.listener(Self::confirm))
- .on_action(cx.listener(Self::remove_selected_thread))
- .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
- this.remove_history(window, cx);
- }))
- .child(
- h_flex()
- .h(Tab::container_height(cx))
- .w_full()
- .py_1()
- .px_2()
- .gap_2()
- .justify_between()
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .child(
- Icon::new(IconName::MagnifyingGlass)
- .color(Color::Muted)
- .size(IconSize::Small),
- )
- .child(self.search_editor.clone()),
- )
- .child({
- let view = v_flex()
- .id("list-container")
- .relative()
- .overflow_hidden()
- .flex_grow();
-
- if has_no_history {
- view.justify_center().items_center().child(
- Label::new("You don't have any past threads yet.")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- } else if self.search_produced_no_matches() {
- view.justify_center()
- .items_center()
- .child(Label::new("No threads match your search.").size(LabelSize::Small))
- } else {
- view.child(
- uniform_list(
- "thread-history",
- self.visible_items.len(),
- cx.processor(|this, range: Range<usize>, window, cx| {
- this.render_list_items(range, window, cx)
- }),
- )
- .p_1()
- .pr_4()
- .track_scroll(&self.scroll_handle)
- .flex_grow(),
- )
- .vertical_scrollbar_for(&self.scroll_handle, window, cx)
- }
- })
- .when(!has_no_history && supports_delete, |this| {
- this.child(
- h_flex()
- .p_2()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .when(!self.confirming_delete_history, |this| {
- this.child(
- Button::new("delete_history", "Delete All History")
- .full_width()
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- this.prompt_delete_history(window, cx);
- })),
- )
- })
- .when(self.confirming_delete_history, |this| {
- this.w_full()
- .gap_2()
- .flex_wrap()
- .justify_between()
- .child(
- h_flex()
- .flex_wrap()
- .gap_1()
- .child(
- Label::new("Delete all threads?")
- .size(LabelSize::Small),
- )
- .child(
- Label::new("You won't be able to recover them later.")
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(
- h_flex()
- .gap_1()
- .child(
- Button::new("cancel_delete", "Cancel")
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- this.cancel_delete_history(window, cx);
- })),
- )
- .child(
- Button::new("confirm_delete", "Delete")
- .style(ButtonStyle::Tinted(ui::TintColor::Error))
- .color(Color::Error)
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|_, _, window, cx| {
- window.dispatch_action(
- Box::new(RemoveHistory),
- cx,
- );
- })),
- ),
- )
- }),
- )
- })
- }
-}
-
-#[derive(Clone, Copy)]
-pub enum EntryTimeFormat {
- DateAndTime,
- TimeOnly,
-}
-
-impl EntryTimeFormat {
- fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
- let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
-
- match self {
- EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
- timestamp,
- OffsetDateTime::now_utc(),
- timezone,
- time_format::TimestampFormat::EnhancedAbsolute,
- ),
- EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
- }
- }
-}
-
-impl From<TimeBucket> for EntryTimeFormat {
- fn from(bucket: TimeBucket) -> Self {
- match bucket {
- TimeBucket::Today => EntryTimeFormat::TimeOnly,
- TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
- TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
- TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
- TimeBucket::All => EntryTimeFormat::DateAndTime,
- }
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Copy, Debug)]
-enum TimeBucket {
- Today,
- Yesterday,
- ThisWeek,
- PastWeek,
- All,
-}
-
-impl TimeBucket {
- fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
- if date == reference {
- return TimeBucket::Today;
- }
-
- if date == reference - TimeDelta::days(1) {
- return TimeBucket::Yesterday;
- }
-
- let week = date.iso_week();
-
- if reference.iso_week() == week {
- return TimeBucket::ThisWeek;
- }
-
- let last_week = (reference - TimeDelta::days(7)).iso_week();
-
- if week == last_week {
- return TimeBucket::PastWeek;
- }
-
- TimeBucket::All
- }
-}
-
-impl Display for TimeBucket {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- TimeBucket::Today => write!(f, "Today"),
- TimeBucket::Yesterday => write!(f, "Yesterday"),
- TimeBucket::ThisWeek => write!(f, "This Week"),
- TimeBucket::PastWeek => write!(f, "Past Week"),
- TimeBucket::All => write!(f, "All"),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use chrono::NaiveDate;
-
- #[test]
- fn test_time_bucket_from_dates() {
- let today = NaiveDate::from_ymd_opt(2025, 1, 15).unwrap();
-
- assert_eq!(TimeBucket::from_dates(today, today), TimeBucket::Today);
-
- let yesterday = NaiveDate::from_ymd_opt(2025, 1, 14).unwrap();
- assert_eq!(
- TimeBucket::from_dates(today, yesterday),
- TimeBucket::Yesterday
- );
-
- let this_week = NaiveDate::from_ymd_opt(2025, 1, 13).unwrap();
- assert_eq!(
- TimeBucket::from_dates(today, this_week),
- TimeBucket::ThisWeek
- );
-
- let past_week = NaiveDate::from_ymd_opt(2025, 1, 7).unwrap();
- assert_eq!(
- TimeBucket::from_dates(today, past_week),
- TimeBucket::PastWeek
- );
-
- let old = NaiveDate::from_ymd_opt(2024, 12, 1).unwrap();
- assert_eq!(TimeBucket::from_dates(today, old), TimeBucket::All);
- }
-}
@@ -71,12 +71,7 @@ In long conversations, use the scroll arrow buttons at the bottom of the panel t
When focus is in the message editor, you can also use {#kb agent::ScrollOutputPageUp}, {#kb agent::ScrollOutputPageDown}, {#kb agent::ScrollOutputToTop}, {#kb agent::ScrollOutputToBottom}, {#kb agent::ScrollOutputLineUp}, and {#kb agent::ScrollOutputLineDown} to navigate the thread, or {#kb agent::ScrollOutputToPreviousMessage} and {#kb agent::ScrollOutputToNextMessage} to jump between your prompts.
-### Navigating History {#navigating-history}
-
-To quickly navigate through recently updated threads, use the {#kb agent::ToggleNavigationMenu} binding when focused on the panel's editor, or click the menu icon button at the top right of the panel.
-Doing that will open a dropdown that shows you your six most recently updated threads.
-
-To view all historical conversations, reach for the `View All` option from within the same menu or via the {#kb agent::OpenHistory} binding.
+### Thread titles {#thread-titles}
Thread titles are auto-generated based on the content of the conversation.
But you can also edit them manually by clicking the title and typing, or regenerate them by clicking the "Regenerate Thread Title" button in the ellipsis menu in the top right of the panel.