Detailed changes
@@ -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",
"ctrl-shift-t": "agent::CycleStartThreadIn",
@@ -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",
"cmd-shift-t": "agent::CycleStartThreadIn",
@@ -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",
"ctrl-shift-t": "agent::CycleStartThreadIn",
@@ -24,12 +24,13 @@ use zed_actions::agent::{
ResolveConflictsWithAgent, ReviewBranchDiff,
};
+use crate::agent_connection_store::AgentConnectionStore;
use crate::thread_metadata_store::ThreadMetadataStore;
use crate::{
AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn,
Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget,
- OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
- StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
+ OpenActiveThreadAsMarkdown, OpenAgentDiff, ResetTrialEndUpsell, ResetTrialUpsell,
+ StartThreadIn, ToggleNewThreadMenu, ToggleOptionsMenu,
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
conversation_view::{AcpThreadViewEvent, ThreadView},
thread_branch_picker::ThreadBranchPicker,
@@ -41,12 +42,10 @@ use crate::{
NewNativeAgentThreadFromSummary,
};
use crate::{DEFAULT_THREAD_TITLE, ui::AcpOnboardingModal};
-use crate::{ExpandMessageEditor, ThreadHistoryView};
-use crate::{ManageProfiles, ThreadHistoryViewEvent};
-use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
+use crate::{ExpandMessageEditor, ManageProfiles};
use agent_settings::{AgentSettings, WindowLayout};
use ai_onboarding::AgentPanelOnboarding;
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
use client::UserStore;
use cloud_api_types::Plan;
use collections::HashMap;
@@ -56,8 +55,8 @@ use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
- 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;
@@ -76,7 +75,7 @@ use ui::{
Button, ButtonLike, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, 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,
@@ -89,7 +88,6 @@ use zed_actions::{
};
const AGENT_PANEL_KEY: &str = "agent_panel";
-const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
#[derive(Serialize, Deserialize)]
@@ -191,12 +189,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);
@@ -247,14 +239,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);
@@ -606,9 +590,6 @@ enum ActiveView {
AgentThread {
conversation_view: Entity<ConversationView>,
},
- History {
- view: Entity<ThreadHistoryView>,
- },
Configuration,
}
@@ -776,9 +757,7 @@ enum WorktreeCreationArgs {
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
- ActiveView::Uninitialized
- | ActiveView::AgentThread { .. }
- | ActiveView::History { .. } => WhichFontSize::AgentFont,
+ ActiveView::Uninitialized | ActiveView::AgentThread { .. } => WhichFontSize::AgentFont,
ActiveView::Configuration => WhichFontSize::None,
}
}
@@ -806,8 +785,6 @@ pub struct AgentPanel {
start_thread_in_menu_handle: PopoverMenuHandle<ThreadWorktreePicker>,
thread_branch_menu_handle: PopoverMenuHandle<ThreadBranchPicker>,
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,
_git_store_subscription: Subscription,
@@ -990,7 +967,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();
@@ -1008,48 +985,6 @@ impl AgentPanel {
let active_view = ActiveView::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(
@@ -1184,8 +1119,6 @@ impl AgentPanel {
start_thread_in_menu_handle: PopoverMenuHandle::default(),
thread_branch_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,
_git_store_subscription,
@@ -1348,40 +1281,28 @@ 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;
};
- 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,
- }),
- true,
- window,
- cx,
- );
- anyhow::Ok(())
- })
- })
- .detach_and_log_err(cx);
+ self.external_thread(
+ Some(Agent::NativeAgent),
+ None,
+ None,
+ None,
+ Some(AgentInitialContent::ThreadSummary {
+ session_id: thread.id,
+ title: Some(thread.title),
+ }),
+ true,
+ window,
+ cx,
+ );
}
fn external_thread(
@@ -1456,102 +1377,13 @@ 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()),
- }
- }
-
- 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,
- window,
- cx,
- );
- }
- },
- )
- .detach();
- view
- }
-
- fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(view) = self.history_for_selected_agent(window, cx) else {
- return;
- };
-
- if let ActiveView::History { view: active_view } = &self.active_view {
- if active_view == &view {
- if let Some(previous_view) = self.previous_view.take() {
- self.set_active_view(previous_view, true, window, cx);
- }
- return;
- }
- }
-
- self.set_active_view(ActiveView::History { view }, true, window, cx);
- cx.notify();
- }
-
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
- match self.active_view {
- ActiveView::Configuration | ActiveView::History { .. } => {
- if let Some(previous_view) = self.previous_view.take() {
- self.set_active_view(previous_view, true, window, cx);
- }
- cx.notify();
+ if let ActiveView::Configuration = self.active_view {
+ if let Some(previous_view) = self.previous_view.take() {
+ self.set_active_view(previous_view, true, window, cx);
}
- _ => {}
- }
- }
-
- pub fn toggle_navigation_menu(
- &mut self,
- _: &ToggleNavigationMenu,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if !self.has_history_for_selected_agent(cx) {
- return;
+ cx.notify();
}
- self.agent_navigation_menu_handle.toggle(window, cx);
}
pub fn toggle_options_menu(
@@ -2043,16 +1875,12 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let was_in_agent_history = matches!(self.active_view, ActiveView::History { .. });
let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
- let current_is_history = matches!(self.active_view, ActiveView::History { .. });
- let new_is_history = matches!(new_view, ActiveView::History { .. });
-
let current_is_config = matches!(self.active_view, ActiveView::Configuration);
let new_is_config = matches!(new_view, ActiveView::Configuration);
- let current_is_overlay = current_is_history || current_is_config;
- let new_is_overlay = new_is_history || new_is_config;
+ let current_is_overlay = current_is_config;
+ let new_is_overlay = new_is_config;
if current_is_uninitialized || (current_is_overlay && !new_is_overlay) {
self.active_view = new_view;
@@ -2101,78 +1929,12 @@ impl AgentPanel {
}
};
- if let ActiveView::History { view } = &self.active_view {
- if !was_in_agent_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);
}
cx.emit(AgentPanelEvent::ActiveViewChanged);
}
- 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| {
- if let Some(agent) = this.selected_agent() {
- this.load_agent_thread(
- agent,
- entry.session_id.clone(),
- entry.work_dirs.clone(),
- entry.title.clone(),
- true,
- window,
- cx,
- );
- }
- })
- .ok();
- }
- });
- }
-
- menu.separator()
- }
-
fn subscribe_to_active_thread_view(
server_view: &Entity<ConversationView>,
window: &mut Window,
@@ -3324,7 +3086,6 @@ impl Focusable for AgentPanel {
ActiveView::AgentThread {
conversation_view, ..
} => conversation_view.focus_handle(cx),
- ActiveView::History { view } => view.read(cx).focus_handle(cx),
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
configuration.focus_handle(cx)
@@ -3507,7 +3268,6 @@ impl AgentPanel {
.into_any_element()
}
}
- ActiveView::History { .. } => Label::new("History").truncate().into_any_element(),
ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
};
@@ -3774,9 +3534,7 @@ impl AgentPanel {
ActiveView::AgentThread { conversation_view } => {
conversation_view.read(cx).as_native_thread(cx)
}
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
- None
- }
+ ActiveView::Uninitialized | ActiveView::Configuration => None,
};
let new_thread_menu_builder: Rc<
@@ -4005,10 +3763,7 @@ impl AgentPanel {
let is_empty_state = !self.active_thread_has_messages(cx);
- let is_in_history_or_config = matches!(
- &self.active_view,
- ActiveView::History { .. } | ActiveView::Configuration
- );
+ let is_in_history_or_config = matches!(&self.active_view, ActiveView::Configuration);
let is_full_screen = self.is_zoomed(window, cx);
let full_screen_button = if is_full_screen {
@@ -4143,7 +3898,7 @@ impl AgentPanel {
.gap(DynamicSpacing::Base04.rems(cx))
.pl(DynamicSpacing::Base04.rems(cx))
.child(match &self.active_view {
- ActiveView::History { .. } | ActiveView::Configuration => {
+ ActiveView::Configuration => {
self.render_toolbar_back_button(cx).into_any_element()
}
_ => selected_agent.into_any_element(),
@@ -4238,7 +3993,7 @@ impl AgentPanel {
return false;
}
}
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
+ ActiveView::Uninitialized | ActiveView::Configuration => {
return false;
}
}
@@ -4265,9 +4020,7 @@ impl AgentPanel {
}
match &self.active_view {
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
- false
- }
+ ActiveView::Uninitialized | ActiveView::Configuration => false,
ActiveView::AgentThread { .. } => {
let existing_user = self
.new_user_onboarding_upsell_dismissed
@@ -4338,17 +4091,12 @@ impl AgentPanel {
});
match &self.active_view {
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
- false
- }
+ ActiveView::Uninitialized | ActiveView::Configuration => false,
ActiveView::AgentThread {
conversation_view, ..
} if conversation_view.read(cx).as_native_thread(cx).is_none() => false,
- ActiveView::AgentThread { conversation_view } => {
- let history_is_empty = conversation_view
- .read(cx)
- .history()
- .is_none_or(|h| h.read(cx).is_empty());
+ ActiveView::AgentThread { .. } => {
+ let history_is_empty = ThreadStore::global(cx).read(cx).is_empty();
history_is_empty || !has_configured_non_zed_providers
}
}
@@ -4471,7 +4219,7 @@ impl AgentPanel {
conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
});
}
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
+ ActiveView::Uninitialized | ActiveView::Configuration => {}
}
}
@@ -4512,7 +4260,7 @@ impl AgentPanel {
key_context.add("AgentPanel");
match &self.active_view {
ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
- ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
+ ActiveView::Uninitialized | ActiveView::Configuration => {}
}
key_context
}
@@ -4537,16 +4285,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))
@@ -4570,7 +4314,6 @@ impl Render for AgentPanel {
} => parent
.child(conversation_view.clone())
.child(self.render_drag_target(cx)),
- ActiveView::History { view } => parent.child(view.clone()),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
.children(self.render_worktree_creation_status(cx))
@@ -4732,14 +4475,6 @@ impl AgentPanel {
cx.notify();
}
- /// 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);
- }
-
/// Opens the start_thread_in selector popover menu.
///
/// This is a test-only helper for visual tests.
@@ -30,7 +30,6 @@ mod terminal_inline_assistant;
pub mod test_support;
mod thread_branch_picker;
mod thread_history;
-mod thread_history_view;
mod thread_import;
pub mod thread_metadata_store;
pub mod thread_worktree_archive;
@@ -75,7 +74,6 @@ 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, ThreadImportModal};
use zed_actions;
@@ -88,8 +86,6 @@ actions!(
ToggleNewThreadMenu,
/// Cycles through the options for where new threads start (current project or new worktree).
CycleStartThreadIn,
- /// 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.
@@ -100,10 +96,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,
/// Removes the currently selected thread.
@@ -2644,10 +2644,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 {
@@ -2797,9 +2793,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;
@@ -2934,66 +2928,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);
@@ -3852,19 +3786,6 @@ pub(crate) mod tests {
(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)
- }
-
async fn setup_conversation_view_with_initial_content(
agent: impl AgentServer + 'static,
initial_content: AgentInitialContent,
@@ -4061,42 +3982,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>,
@@ -4125,67 +4010,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: Option<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,13 +326,9 @@ 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 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 {
@@ -387,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;
@@ -500,11 +490,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 {
id,
parent_id,
@@ -567,11 +552,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,
generating_indicator_in_list: false,
};
@@ -8276,16 +8257,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,6 @@
use acp_thread::{AgentSessionInfo, AgentSessionList, AgentSessionListRequest, SessionListUpdate};
use agent_client_protocol as acp;
-use gpui::{App, Task};
+use gpui::Task;
use std::rc::Rc;
use ui::prelude::*;
@@ -75,10 +75,6 @@ impl ThreadHistory {
}));
}
- 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,
@@ -178,10 +174,6 @@ impl ThreadHistory {
});
}
- 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();
}
@@ -196,26 +188,6 @@ impl ThreadHistory {
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)]
@@ -420,7 +392,7 @@ mod tests {
cx.run_until_parked();
session_list.clear_requested_cursors();
- history.update(cx, |history, cx| history.refresh_full_history(cx));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
history.update(cx, |history, _cx| {
@@ -454,7 +426,7 @@ mod tests {
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));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
session_list.clear_requested_cursors();
@@ -485,11 +457,11 @@ mod tests {
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));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
session_list.clear_requested_cursors();
- history.update(cx, |history, cx| history.refresh_full_history(cx));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
history.update(cx, |history, _cx| {
@@ -514,7 +486,7 @@ mod tests {
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));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
session_list.clear_requested_cursors();
@@ -558,7 +530,7 @@ mod tests {
cx.run_until_parked();
session_list.clear_requested_cursors();
- history.update(cx, |history, cx| history.refresh_full_history(cx));
+ history.update(cx, |history, cx| history.refresh_sessions(true, cx));
cx.run_until_parked();
history.update(cx, |history, _cx| {
@@ -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()
- .filter(|title| !title.is_empty())
- .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 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);
- }
-}