770 lines · 27.6 KB · Rust
RawDownload
use std::sync::Arc; use crate::{    Agent, RemoveSelectedThread, agent_connection_store::AgentConnectionStore,    thread_history::ThreadHistory,};use acp_thread::AgentSessionInfo;use agent::ThreadStore;use agent_client_protocol as acp;use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};use editor::Editor;use fs::Fs;use gpui::{    AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, ListState, Render,    SharedString, Subscription, Task, Window, list, prelude::*, px,};use itertools::Itertools as _;use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};use project::{AgentId, AgentServerStore};use theme::ActiveTheme;use ui::{    ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, HighlightedLabel, ListItem,    PopoverMenu, PopoverMenuHandle, Tab, TintColor, Tooltip, WithScrollbar, prelude::*,};use util::ResultExt as _;use zed_actions::editor::{MoveDown, MoveUp}; #[derive(Clone)]enum ArchiveListItem {    BucketSeparator(TimeBucket),    Entry {        session: AgentSessionInfo,        highlight_positions: Vec<usize>,    },} #[derive(Clone, Copy, Debug, PartialEq, Eq)]enum TimeBucket {    Today,    Yesterday,    ThisWeek,    PastWeek,    Older,} 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::Older    }     fn label(&self) -> &'static str {        match self {            TimeBucket::Today => "Today",            TimeBucket::Yesterday => "Yesterday",            TimeBucket::ThisWeek => "This Week",            TimeBucket::PastWeek => "Past Week",            TimeBucket::Older => "Older",        }    }} fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {    let query = query.to_lowercase();    let text_lower = text.to_lowercase();    let mut positions = Vec::new();    let mut query_chars = query.chars().peekable();    for (i, c) in text_lower.chars().enumerate() {        if query_chars.peek() == Some(&c) {            positions.push(i);            query_chars.next();        }    }    if query_chars.peek().is_none() {        Some(positions)    } else {        None    }} pub enum ThreadsArchiveViewEvent {    Close,    OpenThread {        agent: Agent,        session_info: AgentSessionInfo,    },} impl EventEmitter<ThreadsArchiveViewEvent> for ThreadsArchiveView {} pub struct ThreadsArchiveView {    agent_connection_store: Entity<AgentConnectionStore>,    agent_server_store: Entity<AgentServerStore>,    thread_store: Entity<ThreadStore>,    fs: Arc<dyn Fs>,    history: Option<Entity<ThreadHistory>>,    _history_subscription: Subscription,    selected_agent: Agent,    focus_handle: FocusHandle,    list_state: ListState,    items: Vec<ArchiveListItem>,    selection: Option<usize>,    hovered_index: Option<usize>,    filter_editor: Entity<Editor>,    _subscriptions: Vec<gpui::Subscription>,    selected_agent_menu: PopoverMenuHandle<ContextMenu>,    _refresh_history_task: Task<()>,    is_loading: bool,} impl ThreadsArchiveView {    pub fn new(        agent_connection_store: Entity<AgentConnectionStore>,        agent_server_store: Entity<AgentServerStore>,        thread_store: Entity<ThreadStore>,        fs: Arc<dyn Fs>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focus_handle = cx.focus_handle();         let filter_editor = cx.new(|cx| {            let mut editor = Editor::single_line(window, cx);            editor.set_placeholder_text("Search archive…", window, cx);            editor        });         let filter_editor_subscription =            cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {                if let editor::EditorEvent::BufferEdited = event {                    this.update_items(cx);                }            });         let mut this = Self {            agent_connection_store,            agent_server_store,            thread_store,            fs,            history: None,            _history_subscription: Subscription::new(|| {}),            selected_agent: Agent::NativeAgent,            focus_handle,            list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),            items: Vec::new(),            selection: None,            hovered_index: None,            filter_editor,            _subscriptions: vec![filter_editor_subscription],            selected_agent_menu: PopoverMenuHandle::default(),            _refresh_history_task: Task::ready(()),            is_loading: true,        };        this.set_selected_agent(Agent::NativeAgent, window, cx);        this    }     fn set_selected_agent(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {        self.selected_agent = agent.clone();        self.is_loading = true;        self.history = None;        self.items.clear();        self.selection = None;        self.list_state.reset(0);        self.reset_filter_editor_text(window, cx);         let server = agent.server(self.fs.clone(), self.thread_store.clone());        let connection = self            .agent_connection_store            .update(cx, |store, cx| store.request_connection(agent, server, cx));         let task = connection.read(cx).wait_for_connection();        self._refresh_history_task = cx.spawn(async move |this, cx| {            if let Some(state) = task.await.log_err() {                this.update(cx, |this, cx| this.set_history(state.history, cx))                    .ok();            }        });         cx.notify();    }     fn set_history(&mut self, history: Entity<ThreadHistory>, cx: &mut Context<Self>) {        self._history_subscription = cx.observe(&history, |this, _, cx| {            this.update_items(cx);        });        history.update(cx, |history, cx| {            history.refresh_full_history(cx);        });        self.history = Some(history);        self.is_loading = false;        self.update_items(cx);        cx.notify();    }     fn update_items(&mut self, cx: &mut Context<Self>) {        let Some(history) = self.history.as_ref() else {            return;        };         let sessions = history.read(cx).sessions().to_vec();        let query = self.filter_editor.read(cx).text(cx).to_lowercase();        let today = Local::now().naive_local().date();         let mut items = Vec::with_capacity(sessions.len() + 5);        let mut current_bucket: Option<TimeBucket> = None;         for session in sessions {            let highlight_positions = if !query.is_empty() {                let title = session.title.as_ref().map(|t| t.as_ref()).unwrap_or("");                match fuzzy_match_positions(&query, title) {                    Some(positions) => positions,                    None => continue,                }            } else {                Vec::new()            };             let entry_bucket = session                .updated_at                .map(|timestamp| {                    let entry_date = timestamp.with_timezone(&Local).naive_local().date();                    TimeBucket::from_dates(today, entry_date)                })                .unwrap_or(TimeBucket::Older);             if Some(entry_bucket) != current_bucket {                current_bucket = Some(entry_bucket);                items.push(ArchiveListItem::BucketSeparator(entry_bucket));            }             items.push(ArchiveListItem::Entry {                session,                highlight_positions,            });        }         self.list_state.reset(items.len());        self.items = items;        cx.notify();    }     fn reset_filter_editor_text(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.filter_editor.update(cx, |editor, cx| {            editor.set_text("", window, cx);        });    }     fn go_back(&mut self, window: &mut Window, cx: &mut Context<Self>) {        self.reset_filter_editor_text(window, cx);        cx.emit(ThreadsArchiveViewEvent::Close);    }     fn open_thread(        &mut self,        session_info: AgentSessionInfo,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.selection = None;        self.reset_filter_editor_text(window, cx);        cx.emit(ThreadsArchiveViewEvent::OpenThread {            agent: self.selected_agent.clone(),            session_info,        });    }     fn delete_thread(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {        let Some(history) = &self.history else {            return;        };        if !history.read(cx).supports_delete() {            return;        }        let session_id = session_id.clone();        history.update(cx, |history, cx| {            history                .delete_session(&session_id, cx)                .detach_and_log_err(cx);        });    }     fn remove_selected_thread(        &mut self,        _: &RemoveSelectedThread,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        let Some(ix) = self.selection else {            return;        };        let Some(ArchiveListItem::Entry { session, .. }) = self.items.get(ix) else {            return;        };        let session_id = session.session_id.clone();        self.delete_thread(&session_id, cx);    }     fn is_selectable_item(&self, ix: usize) -> bool {        matches!(self.items.get(ix), Some(ArchiveListItem::Entry { .. }))    }     fn find_next_selectable(&self, start: usize) -> Option<usize> {        (start..self.items.len()).find(|&i| self.is_selectable_item(i))    }     fn find_previous_selectable(&self, start: usize) -> Option<usize> {        (0..=start).rev().find(|&i| self.is_selectable_item(i))    }     fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {        self.select_next(&SelectNext, window, cx);    }     fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {        self.select_previous(&SelectPrevious, window, cx);    }     fn select_next(&mut self, _: &SelectNext, _window: &mut Window, cx: &mut Context<Self>) {        let next = match self.selection {            Some(ix) => self.find_next_selectable(ix + 1),            None => self.find_next_selectable(0),        };        if let Some(next) = next {            self.selection = Some(next);            self.list_state.scroll_to_reveal_item(next);            cx.notify();        }    }     fn select_previous(        &mut self,        _: &SelectPrevious,        _window: &mut Window,        cx: &mut Context<Self>,    ) {        let prev = match self.selection {            Some(ix) if ix > 0 => self.find_previous_selectable(ix - 1),            None => {                let last = self.items.len().saturating_sub(1);                self.find_previous_selectable(last)            }            _ => return,        };        if let Some(prev) = prev {            self.selection = Some(prev);            self.list_state.scroll_to_reveal_item(prev);            cx.notify();        }    }     fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {        if let Some(first) = self.find_next_selectable(0) {            self.selection = Some(first);            self.list_state.scroll_to_reveal_item(first);            cx.notify();        }    }     fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {        let last = self.items.len().saturating_sub(1);        if let Some(last) = self.find_previous_selectable(last) {            self.selection = Some(last);            self.list_state.scroll_to_reveal_item(last);            cx.notify();        }    }     fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {        let Some(ix) = self.selection else { return };        let Some(ArchiveListItem::Entry { session, .. }) = self.items.get(ix) else {            return;        };        self.open_thread(session.clone(), window, cx);    }     fn render_list_entry(        &mut self,        ix: usize,        _window: &mut Window,        cx: &mut Context<Self>,    ) -> AnyElement {        let Some(item) = self.items.get(ix) else {            return div().into_any_element();        };         match item {            ArchiveListItem::BucketSeparator(bucket) => div()                .w_full()                .px_2()                .pt_3()                .pb_1()                .child(                    Label::new(bucket.label())                        .size(LabelSize::Small)                        .color(Color::Muted),                )                .into_any_element(),            ArchiveListItem::Entry {                session,                highlight_positions,            } => {                let is_selected = self.selection == Some(ix);                let hovered = self.hovered_index == Some(ix);                let supports_delete = self                    .history                    .as_ref()                    .map(|h| h.read(cx).supports_delete())                    .unwrap_or(false);                let title: SharedString =                    session.title.clone().unwrap_or_else(|| "Untitled".into());                let session_info = session.clone();                let session_id_for_delete = session.session_id.clone();                let focus_handle = self.focus_handle.clone();                let highlight_positions = highlight_positions.clone();                 let timestamp = session.created_at.or(session.updated_at).map(|entry_time| {                    let now = Utc::now();                    let duration = now.signed_duration_since(entry_time);                     let minutes = duration.num_minutes();                    let hours = duration.num_hours();                    let days = duration.num_days();                    let weeks = days / 7;                    let months = days / 30;                     if minutes < 60 {                        format!("{}m", minutes.max(1))                    } else if hours < 24 {                        format!("{}h", hours)                    } else if weeks < 4 {                        format!("{}w", weeks.max(1))                    } else {                        format!("{}mo", months.max(1))                    }                });                 let id = SharedString::from(format!("archive-entry-{}", ix));                 let title_label = if highlight_positions.is_empty() {                    Label::new(title)                        .size(LabelSize::Small)                        .truncate()                        .into_any_element()                } else {                    HighlightedLabel::new(title, highlight_positions)                        .size(LabelSize::Small)                        .truncate()                        .into_any_element()                };                 ListItem::new(id)                    .toggle_state(is_selected)                    .child(                        h_flex()                            .min_w_0()                            .w_full()                            .py_1()                            .pl_0p5()                            .pr_1p5()                            .gap_2()                            .justify_between()                            .child(title_label)                            .when(!(hovered && supports_delete), |this| {                                this.when_some(timestamp, |this, ts| {                                    this.child(                                        Label::new(ts).size(LabelSize::Small).color(Color::Muted),                                    )                                })                            }),                    )                    .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-thread", IconName::Trash)                                .icon_size(IconSize::Small)                                .icon_color(Color::Muted)                                .tooltip({                                    move |_window, cx| {                                        Tooltip::for_action_in(

Lines 1–500 of 770.