No longer instantiate recently opened agent threads on startup (#32285)
This was causing a lot of work on startup, particularly due to instantiating edit tool cards. The minor downside is that now these threads don't open quite as fast. Includes a few other improvements: * On text thread rename, now immediately updates the metadata for display in the UI instead of waiting for reload. * On text thread rename, first renames the file before writing. Before if the file removal failed you'd end up with a duplicate. * Now only stores text thread file names instead of full paths. This is more concise and allows for the app data dir changing location. * Renames `ThreadStore::unordered_threads` to `ThreadStore::reverse_chronological_threads` (and removes the old one that sorted), since the recent change to use a SQL database queries them in that order. * Removes `ContextStore::reverse_chronological_contexts` since it was only used in one location where it does sorting anyway - no need to sort twice. * `SavedContextMetadata::title` is now `SharedString` instead of `String`. Release Notes: - Fixed regression in startup performance by not deserializing and instantiating recently opened agent threads.
This commit is contained in:
parent
1552198b55
commit
cabd22f36b
8 changed files with 299 additions and 261 deletions
|
@ -57,7 +57,7 @@ use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||||
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
||||||
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
||||||
use crate::agent_diff::AgentDiff;
|
use crate::agent_diff::AgentDiff;
|
||||||
use crate::history_store::{HistoryStore, RecentEntry};
|
use crate::history_store::{HistoryEntryId, HistoryStore};
|
||||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||||
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
|
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
|
||||||
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
|
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
|
||||||
|
@ -257,6 +257,7 @@ impl ActiveView {
|
||||||
|
|
||||||
pub fn prompt_editor(
|
pub fn prompt_editor(
|
||||||
context_editor: Entity<ContextEditor>,
|
context_editor: Entity<ContextEditor>,
|
||||||
|
history_store: Entity<HistoryStore>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
@ -322,6 +323,19 @@ impl ActiveView {
|
||||||
editor.set_text(summary, window, cx);
|
editor.set_text(summary, window, cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ContextEvent::PathChanged { old_path, new_path } => {
|
||||||
|
history_store.update(cx, |history_store, cx| {
|
||||||
|
if let Some(old_path) = old_path {
|
||||||
|
history_store
|
||||||
|
.replace_recently_opened_text_thread(old_path, new_path, cx);
|
||||||
|
} else {
|
||||||
|
history_store.push_recently_opened_entry(
|
||||||
|
HistoryEntryId::Context(new_path.clone()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -516,8 +530,7 @@ impl AgentPanel {
|
||||||
HistoryStore::new(
|
HistoryStore::new(
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
[RecentEntry::Thread(thread_id, thread.clone())],
|
[HistoryEntryId::Thread(thread_id)],
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -544,7 +557,13 @@ impl AgentPanel {
|
||||||
editor.insert_default_prompt(window, cx);
|
editor.insert_default_prompt(window, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
ActiveView::prompt_editor(context_editor, language_registry.clone(), window, cx)
|
ActiveView::prompt_editor(
|
||||||
|
context_editor,
|
||||||
|
history_store.clone(),
|
||||||
|
language_registry.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -581,86 +600,9 @@ impl AgentPanel {
|
||||||
let panel = weak_panel.clone();
|
let panel = weak_panel.clone();
|
||||||
let assistant_navigation_menu =
|
let assistant_navigation_menu =
|
||||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||||
let recently_opened = panel
|
if let Some(panel) = panel.upgrade() {
|
||||||
.update(cx, |this, cx| {
|
menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
|
||||||
this.history_store.update(cx, |history_store, cx| {
|
|
||||||
history_store.recently_opened_entries(cx)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if !recently_opened.is_empty() {
|
|
||||||
menu = menu.header("Recently Opened");
|
|
||||||
|
|
||||||
for entry in recently_opened.iter() {
|
|
||||||
if let RecentEntry::Context(context) = entry {
|
|
||||||
if context.read(cx).path().is_none() {
|
|
||||||
log::error!(
|
|
||||||
"bug: text thread in recent history list was never saved"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let summary = entry.summary(cx);
|
|
||||||
|
|
||||||
menu = menu.entry_with_end_slot_on_hover(
|
|
||||||
summary,
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
let panel = panel.clone();
|
|
||||||
let entry = entry.clone();
|
|
||||||
move |window, cx| {
|
|
||||||
panel
|
|
||||||
.update(cx, {
|
|
||||||
let entry = entry.clone();
|
|
||||||
move |this, cx| match entry {
|
|
||||||
RecentEntry::Thread(_, thread) => {
|
|
||||||
this.open_thread(thread, window, cx)
|
|
||||||
}
|
|
||||||
RecentEntry::Context(context) => {
|
|
||||||
let Some(path) = context.read(cx).path()
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
this.open_saved_prompt_editor(
|
|
||||||
path.clone(),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.detach_and_log_err(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IconName::Close,
|
|
||||||
"Close Entry".into(),
|
|
||||||
{
|
|
||||||
let panel = panel.clone();
|
|
||||||
let entry = entry.clone();
|
|
||||||
move |_window, cx| {
|
|
||||||
panel
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.history_store.update(
|
|
||||||
cx,
|
|
||||||
|history_store, cx| {
|
|
||||||
history_store.remove_recently_opened_entry(
|
|
||||||
&entry, cx,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu = menu.separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.action("View All", Box::new(OpenHistory))
|
menu.action("View All", Box::new(OpenHistory))
|
||||||
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
|
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
|
||||||
.fixed_width(px(320.).into())
|
.fixed_width(px(320.).into())
|
||||||
|
@ -898,6 +840,7 @@ impl AgentPanel {
|
||||||
self.set_active_view(
|
self.set_active_view(
|
||||||
ActiveView::prompt_editor(
|
ActiveView::prompt_editor(
|
||||||
context_editor.clone(),
|
context_editor.clone(),
|
||||||
|
self.history_store.clone(),
|
||||||
self.language_registry.clone(),
|
self.language_registry.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -984,7 +927,13 @@ impl AgentPanel {
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
self.set_active_view(
|
self.set_active_view(
|
||||||
ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
|
ActiveView::prompt_editor(
|
||||||
|
editor.clone(),
|
||||||
|
self.history_store.clone(),
|
||||||
|
self.language_registry.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1383,16 +1332,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActiveView::TextThread { context_editor, .. } => {
|
|
||||||
let context = context_editor.read(cx).context();
|
|
||||||
// When switching away from an unsaved text thread, delete its entry.
|
|
||||||
if context.read(cx).path().is_none() {
|
|
||||||
let context = context.clone();
|
|
||||||
self.history_store.update(cx, |store, cx| {
|
|
||||||
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1400,13 +1339,14 @@ impl AgentPanel {
|
||||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||||
if let Some(thread) = thread.upgrade() {
|
if let Some(thread) = thread.upgrade() {
|
||||||
let id = thread.read(cx).id().clone();
|
let id = thread.read(cx).id().clone();
|
||||||
store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
|
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ActiveView::TextThread { context_editor, .. } => {
|
ActiveView::TextThread { context_editor, .. } => {
|
||||||
self.history_store.update(cx, |store, cx| {
|
self.history_store.update(cx, |store, cx| {
|
||||||
let context = context_editor.read(cx).context().clone();
|
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
|
||||||
store.push_recently_opened_entry(RecentEntry::Context(context), cx)
|
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -1425,6 +1365,70 @@ impl AgentPanel {
|
||||||
|
|
||||||
self.focus_handle(cx).focus(window);
|
self.focus_handle(cx).focus(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_recently_opened_menu_section(
|
||||||
|
mut menu: ContextMenu,
|
||||||
|
panel: Entity<Self>,
|
||||||
|
cx: &mut Context<ContextMenu>,
|
||||||
|
) -> ContextMenu {
|
||||||
|
let entries = panel
|
||||||
|
.read(cx)
|
||||||
|
.history_store
|
||||||
|
.read(cx)
|
||||||
|
.recently_opened_entries(cx);
|
||||||
|
|
||||||
|
if entries.is_empty() {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = menu.header("Recently Opened");
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
let title = entry.title().clone();
|
||||||
|
let id = entry.id();
|
||||||
|
|
||||||
|
menu = menu.entry_with_end_slot_on_hover(
|
||||||
|
title,
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
let panel = panel.downgrade();
|
||||||
|
let id = id.clone();
|
||||||
|
move |window, cx| {
|
||||||
|
let id = id.clone();
|
||||||
|
panel
|
||||||
|
.update(cx, move |this, cx| match id {
|
||||||
|
HistoryEntryId::Thread(id) => this
|
||||||
|
.open_thread_by_id(&id, window, cx)
|
||||||
|
.detach_and_log_err(cx),
|
||||||
|
HistoryEntryId::Context(path) => this
|
||||||
|
.open_saved_prompt_editor(path.clone(), window, cx)
|
||||||
|
.detach_and_log_err(cx),
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IconName::Close,
|
||||||
|
"Close Entry".into(),
|
||||||
|
{
|
||||||
|
let panel = panel.downgrade();
|
||||||
|
let id = id.clone();
|
||||||
|
move |_window, cx| {
|
||||||
|
panel
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.history_store.update(cx, |history_store, cx| {
|
||||||
|
history_store.remove_recently_opened_entry(&id, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = menu.separator();
|
||||||
|
|
||||||
|
menu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AgentPanel {
|
impl Focusable for AgentPanel {
|
||||||
|
|
|
@ -282,7 +282,10 @@ pub fn unordered_thread_entries(
|
||||||
text_thread_store: Entity<TextThreadStore>,
|
text_thread_store: Entity<TextThreadStore>,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
|
) -> impl Iterator<Item = (DateTime<Utc>, ThreadContextEntry)> {
|
||||||
let threads = thread_store.read(cx).unordered_threads().map(|thread| {
|
let threads = thread_store
|
||||||
|
.read(cx)
|
||||||
|
.reverse_chronological_threads()
|
||||||
|
.map(|thread| {
|
||||||
(
|
(
|
||||||
thread.updated_at,
|
thread.updated_at,
|
||||||
ThreadContextEntry::Thread {
|
ThreadContextEntry::Thread {
|
||||||
|
@ -300,7 +303,7 @@ pub fn unordered_thread_entries(
|
||||||
context.mtime.to_utc(),
|
context.mtime.to_utc(),
|
||||||
ThreadContextEntry::Context {
|
ThreadContextEntry::Context {
|
||||||
path: context.path.clone(),
|
path: context.path.clone(),
|
||||||
title: context.title.clone().into(),
|
title: context.title.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
use std::{collections::VecDeque, path::Path, sync::Arc};
|
use std::{collections::VecDeque, path::Path, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::{Context as _, Result};
|
||||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
use assistant_context_editor::SavedContextMetadata;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::future::{TryFutureExt as _, join_all};
|
use gpui::{AsyncApp, Entity, SharedString, Task, prelude::*};
|
||||||
use gpui::{Entity, Task, prelude::*};
|
use itertools::Itertools;
|
||||||
|
use paths::contexts_dir;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smol::future::FutureExt;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use ui::{App, SharedString, Window};
|
use ui::App;
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Thread,
|
|
||||||
thread::ThreadId,
|
thread::ThreadId,
|
||||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||||
};
|
};
|
||||||
|
@ -41,52 +40,34 @@ impl HistoryEntry {
|
||||||
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
|
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn title(&self) -> &SharedString {
|
||||||
|
match self {
|
||||||
|
HistoryEntry::Thread(thread) => &thread.summary,
|
||||||
|
HistoryEntry::Context(context) => &context.title,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generic identifier for a history entry.
|
/// Generic identifier for a history entry.
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum HistoryEntryId {
|
pub enum HistoryEntryId {
|
||||||
Thread(ThreadId),
|
Thread(ThreadId),
|
||||||
Context(Arc<Path>),
|
Context(Arc<Path>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) enum RecentEntry {
|
|
||||||
Thread(ThreadId, Entity<Thread>),
|
|
||||||
Context(Entity<AssistantContext>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for RecentEntry {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(Self::Thread(l0, _), Self::Thread(r0, _)) => l0 == r0,
|
|
||||||
(Self::Context(l0), Self::Context(r0)) => l0 == r0,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for RecentEntry {}
|
|
||||||
|
|
||||||
impl RecentEntry {
|
|
||||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
|
||||||
match self {
|
|
||||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
|
|
||||||
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
enum SerializedRecentEntry {
|
enum SerializedRecentOpen {
|
||||||
Thread(String),
|
Thread(String),
|
||||||
|
ContextName(String),
|
||||||
|
/// Old format which stores the full path
|
||||||
Context(String),
|
Context(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HistoryStore {
|
pub struct HistoryStore {
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||||
recently_opened_entries: VecDeque<RecentEntry>,
|
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||||
_subscriptions: Vec<gpui::Subscription>,
|
_subscriptions: Vec<gpui::Subscription>,
|
||||||
_save_recently_opened_entries_task: Task<()>,
|
_save_recently_opened_entries_task: Task<()>,
|
||||||
}
|
}
|
||||||
|
@ -95,8 +76,7 @@ impl HistoryStore {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||||
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
|
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
|
@ -104,66 +84,18 @@ impl HistoryStore {
|
||||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||||
];
|
];
|
||||||
|
|
||||||
window
|
cx.spawn(async move |this, cx| {
|
||||||
.spawn(cx, {
|
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
|
||||||
let thread_store = thread_store.downgrade();
|
|
||||||
let context_store = context_store.downgrade();
|
|
||||||
let this = cx.weak_entity();
|
|
||||||
async move |cx| {
|
|
||||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
|
||||||
let contents = cx
|
|
||||||
.background_spawn(async move { std::fs::read_to_string(path) })
|
|
||||||
.await
|
|
||||||
.ok()?;
|
|
||||||
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
|
|
||||||
.context("deserializing persisted agent panel navigation history")
|
|
||||||
.log_err()?
|
|
||||||
.into_iter()
|
|
||||||
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
|
||||||
.map(|serialized| match serialized {
|
|
||||||
SerializedRecentEntry::Thread(id) => thread_store
|
|
||||||
.update_in(cx, |thread_store, window, cx| {
|
|
||||||
let thread_id = ThreadId::from(id.as_str());
|
|
||||||
thread_store
|
|
||||||
.open_thread(&thread_id, window, cx)
|
|
||||||
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
async {
|
|
||||||
anyhow::bail!("no thread store");
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
}),
|
|
||||||
SerializedRecentEntry::Context(id) => context_store
|
|
||||||
.update(cx, |context_store, cx| {
|
|
||||||
context_store
|
|
||||||
.open_local_context(Path::new(&id).into(), cx)
|
|
||||||
.map_ok(RecentEntry::Context)
|
|
||||||
.boxed()
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
async {
|
|
||||||
anyhow::bail!("no context store");
|
|
||||||
}
|
|
||||||
.boxed()
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
let entries = join_all(entries)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|result| result.log_with_level(log::Level::Debug))
|
|
||||||
.collect::<VecDeque<_>>();
|
|
||||||
|
|
||||||
this.update(cx, |this, _| {
|
this.update(cx, |this, _| {
|
||||||
this.recently_opened_entries.extend(entries);
|
|
||||||
this.recently_opened_entries
|
this.recently_opened_entries
|
||||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
.extend(
|
||||||
|
entries.into_iter().take(
|
||||||
|
MAX_RECENTLY_OPENED_ENTRIES
|
||||||
|
.saturating_sub(this.recently_opened_entries.len()),
|
||||||
|
),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok()
|
||||||
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -184,19 +116,20 @@ impl HistoryStore {
|
||||||
return history_entries;
|
return history_entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
for thread in self
|
history_entries.extend(
|
||||||
.thread_store
|
self.thread_store
|
||||||
.update(cx, |this, _cx| this.reverse_chronological_threads())
|
.read(cx)
|
||||||
{
|
.reverse_chronological_threads()
|
||||||
history_entries.push(HistoryEntry::Thread(thread));
|
.cloned()
|
||||||
}
|
.map(HistoryEntry::Thread),
|
||||||
|
);
|
||||||
for context in self
|
history_entries.extend(
|
||||||
.context_store
|
self.context_store
|
||||||
.update(cx, |this, _cx| this.reverse_chronological_contexts())
|
.read(cx)
|
||||||
{
|
.unordered_contexts()
|
||||||
history_entries.push(HistoryEntry::Context(context));
|
.cloned()
|
||||||
}
|
.map(HistoryEntry::Context),
|
||||||
|
);
|
||||||
|
|
||||||
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
|
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
|
||||||
history_entries
|
history_entries
|
||||||
|
@ -206,15 +139,62 @@ impl HistoryStore {
|
||||||
self.entries(cx).into_iter().take(limit).collect()
|
self.entries(cx).into_iter().take(limit).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let thread_entries = self
|
||||||
|
.thread_store
|
||||||
|
.read(cx)
|
||||||
|
.reverse_chronological_threads()
|
||||||
|
.flat_map(|thread| {
|
||||||
|
self.recently_opened_entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(index, entry)| match entry {
|
||||||
|
HistoryEntryId::Thread(id) if &thread.id == id => {
|
||||||
|
Some((index, HistoryEntry::Thread(thread.clone())))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let context_entries =
|
||||||
|
self.context_store
|
||||||
|
.read(cx)
|
||||||
|
.unordered_contexts()
|
||||||
|
.flat_map(|context| {
|
||||||
|
self.recently_opened_entries
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(index, entry)| match entry {
|
||||||
|
HistoryEntryId::Context(path) if &context.path == path => {
|
||||||
|
Some((index, HistoryEntry::Context(context.clone())))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
thread_entries
|
||||||
|
.chain(context_entries)
|
||||||
|
// optimization to halt iteration early
|
||||||
|
.take(self.recently_opened_entries.len())
|
||||||
|
.sorted_unstable_by_key(|(index, _)| *index)
|
||||||
|
.map(|(_, entry)| entry)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
||||||
let serialized_entries = self
|
let serialized_entries = self
|
||||||
.recently_opened_entries
|
.recently_opened_entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|entry| match entry {
|
.filter_map(|entry| match entry {
|
||||||
RecentEntry::Context(context) => Some(SerializedRecentEntry::Context(
|
HistoryEntryId::Context(path) => path.file_name().map(|file| {
|
||||||
context.read(cx).path()?.to_str()?.to_owned(),
|
SerializedRecentOpen::ContextName(file.to_string_lossy().to_string())
|
||||||
)),
|
}),
|
||||||
RecentEntry::Thread(id, _) => Some(SerializedRecentEntry::Thread(id.to_string())),
|
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
@ -233,7 +213,33 @@ impl HistoryStore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_recently_opened_entry(&mut self, entry: RecentEntry, cx: &mut Context<Self>) {
|
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||||
|
let contents = smol::fs::read_to_string(path).await?;
|
||||||
|
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
|
||||||
|
.context("deserializing persisted agent panel navigation history")?
|
||||||
|
.into_iter()
|
||||||
|
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
||||||
|
.flat_map(|entry| match entry {
|
||||||
|
SerializedRecentOpen::Thread(id) => {
|
||||||
|
Some(HistoryEntryId::Thread(id.as_str().into()))
|
||||||
|
}
|
||||||
|
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
|
||||||
|
contexts_dir().join(file_name).into(),
|
||||||
|
)),
|
||||||
|
SerializedRecentOpen::Context(path) => {
|
||||||
|
Path::new(&path).file_name().map(|file_name| {
|
||||||
|
HistoryEntryId::Context(contexts_dir().join(file_name).into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(entries)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
|
||||||
self.recently_opened_entries
|
self.recently_opened_entries
|
||||||
.retain(|old_entry| old_entry != &entry);
|
.retain(|old_entry| old_entry != &entry);
|
||||||
self.recently_opened_entries.push_front(entry);
|
self.recently_opened_entries.push_front(entry);
|
||||||
|
@ -244,24 +250,33 @@ impl HistoryStore {
|
||||||
|
|
||||||
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
|
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
|
||||||
self.recently_opened_entries.retain(|entry| match entry {
|
self.recently_opened_entries.retain(|entry| match entry {
|
||||||
RecentEntry::Thread(thread_id, _) if thread_id == &id => false,
|
HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
|
||||||
_ => true,
|
_ => true,
|
||||||
});
|
});
|
||||||
self.save_recently_opened_entries(cx);
|
self.save_recently_opened_entries(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_recently_opened_entry(&mut self, entry: &RecentEntry, cx: &mut Context<Self>) {
|
pub fn replace_recently_opened_text_thread(
|
||||||
|
&mut self,
|
||||||
|
old_path: &Path,
|
||||||
|
new_path: &Arc<Path>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
for entry in &mut self.recently_opened_entries {
|
||||||
|
match entry {
|
||||||
|
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
|
||||||
|
*entry = HistoryEntryId::Context(new_path.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.save_recently_opened_entries(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
|
||||||
self.recently_opened_entries
|
self.recently_opened_entries
|
||||||
.retain(|old_entry| old_entry != entry);
|
.retain(|old_entry| old_entry != entry);
|
||||||
self.save_recently_opened_entries(cx);
|
self.save_recently_opened_entries(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recently_opened_entries(&self, _cx: &mut Context<Self>) -> VecDeque<RecentEntry> {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
|
||||||
return VecDeque::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.recently_opened_entries.clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -671,7 +671,7 @@ impl RenderOnce for HistoryEntryElement {
|
||||||
),
|
),
|
||||||
HistoryEntry::Context(context) => (
|
HistoryEntry::Context(context) => (
|
||||||
context.path.to_string_lossy().to_string(),
|
context.path.to_string_lossy().to_string(),
|
||||||
context.title.clone().into(),
|
context.title.clone(),
|
||||||
context.mtime.timestamp(),
|
context.mtime.timestamp(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -393,16 +393,11 @@ impl ThreadStore {
|
||||||
self.threads.len()
|
self.threads.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unordered_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
|
pub fn reverse_chronological_threads(&self) -> impl Iterator<Item = &SerializedThreadMetadata> {
|
||||||
|
// ordering is from "ORDER BY" in `list_threads`
|
||||||
self.threads.iter()
|
self.threads.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reverse_chronological_threads(&self) -> Vec<SerializedThreadMetadata> {
|
|
||||||
let mut threads = self.threads.iter().cloned().collect::<Vec<_>>();
|
|
||||||
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.updated_at));
|
|
||||||
threads
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
Thread::new(
|
Thread::new(
|
||||||
|
|
|
@ -11,7 +11,7 @@ use assistant_slash_commands::FileCommandMetadata;
|
||||||
use client::{self, proto, telemetry::Telemetry};
|
use client::{self, proto, telemetry::Telemetry};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::{Fs, RemoveOptions};
|
use fs::{Fs, RenameOptions};
|
||||||
use futures::{FutureExt, StreamExt, future::Shared};
|
use futures::{FutureExt, StreamExt, future::Shared};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||||
|
@ -452,6 +452,10 @@ pub enum ContextEvent {
|
||||||
MessagesEdited,
|
MessagesEdited,
|
||||||
SummaryChanged,
|
SummaryChanged,
|
||||||
SummaryGenerated,
|
SummaryGenerated,
|
||||||
|
PathChanged {
|
||||||
|
old_path: Option<Arc<Path>>,
|
||||||
|
new_path: Arc<Path>,
|
||||||
|
},
|
||||||
StreamedCompletion,
|
StreamedCompletion,
|
||||||
StartedThoughtProcess(Range<language::Anchor>),
|
StartedThoughtProcess(Range<language::Anchor>),
|
||||||
EndedThoughtProcess(language::Anchor),
|
EndedThoughtProcess(language::Anchor),
|
||||||
|
@ -2894,22 +2898,34 @@ impl AssistantContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.create_dir(contexts_dir().as_ref()).await?;
|
fs.create_dir(contexts_dir().as_ref()).await?;
|
||||||
fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap())
|
|
||||||
.await?;
|
// rename before write ensures that only one file exists
|
||||||
if let Some(old_path) = old_path {
|
if let Some(old_path) = old_path.as_ref() {
|
||||||
if new_path.as_path() != old_path.as_ref() {
|
if new_path.as_path() != old_path.as_ref() {
|
||||||
fs.remove_file(
|
fs.rename(
|
||||||
&old_path,
|
&old_path,
|
||||||
RemoveOptions {
|
&new_path,
|
||||||
recursive: false,
|
RenameOptions {
|
||||||
ignore_if_not_exists: true,
|
overwrite: true,
|
||||||
|
ignore_if_exists: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(cx, |this, _| this.path = Some(new_path.into()))?;
|
// update path before write in case it fails
|
||||||
|
this.update(cx, {
|
||||||
|
let new_path: Arc<Path> = new_path.clone().into();
|
||||||
|
move |this, cx| {
|
||||||
|
this.path = Some(new_path.clone());
|
||||||
|
cx.emit(ContextEvent::PathChanged { old_path, new_path });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
fs.atomic_write(new_path, serde_json::to_string(&context).unwrap())
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -3277,7 +3293,7 @@ impl SavedContextV0_1_0 {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SavedContextMetadata {
|
pub struct SavedContextMetadata {
|
||||||
pub title: String,
|
pub title: SharedString,
|
||||||
pub path: Arc<Path>,
|
pub path: Arc<Path>,
|
||||||
pub mtime: chrono::DateTime<chrono::Local>,
|
pub mtime: chrono::DateTime<chrono::Local>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -580,6 +580,7 @@ impl ContextEditor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ContextEvent::SummaryGenerated => {}
|
ContextEvent::SummaryGenerated => {}
|
||||||
|
ContextEvent::PathChanged { .. } => {}
|
||||||
ContextEvent::StartedThoughtProcess(range) => {
|
ContextEvent::StartedThoughtProcess(range) => {
|
||||||
let creases = self.insert_thought_process_output_sections(
|
let creases = self.insert_thought_process_output_sections(
|
||||||
[(
|
[(
|
||||||
|
|
|
@ -347,12 +347,6 @@ impl ContextStore {
|
||||||
self.contexts_metadata.iter()
|
self.contexts_metadata.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reverse_chronological_contexts(&self) -> Vec<SavedContextMetadata> {
|
|
||||||
let mut contexts = self.contexts_metadata.iter().cloned().collect::<Vec<_>>();
|
|
||||||
contexts.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.mtime));
|
|
||||||
contexts
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
|
pub fn create(&mut self, cx: &mut Context<Self>) -> Entity<AssistantContext> {
|
||||||
let context = cx.new(|cx| {
|
let context = cx.new(|cx| {
|
||||||
AssistantContext::local(
|
AssistantContext::local(
|
||||||
|
@ -618,6 +612,16 @@ impl ContextStore {
|
||||||
ContextEvent::SummaryChanged => {
|
ContextEvent::SummaryChanged => {
|
||||||
self.advertise_contexts(cx);
|
self.advertise_contexts(cx);
|
||||||
}
|
}
|
||||||
|
ContextEvent::PathChanged { old_path, new_path } => {
|
||||||
|
if let Some(old_path) = old_path.as_ref() {
|
||||||
|
for metadata in &mut self.contexts_metadata {
|
||||||
|
if &metadata.path == old_path {
|
||||||
|
metadata.path = new_path.clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ContextEvent::Operation(operation) => {
|
ContextEvent::Operation(operation) => {
|
||||||
let context_id = context.read(cx).id().to_proto();
|
let context_id = context.read(cx).id().to_proto();
|
||||||
let operation = operation.to_proto();
|
let operation = operation.to_proto();
|
||||||
|
@ -792,7 +796,7 @@ impl ContextStore {
|
||||||
.next()
|
.next()
|
||||||
{
|
{
|
||||||
contexts.push(SavedContextMetadata {
|
contexts.push(SavedContextMetadata {
|
||||||
title: title.to_string(),
|
title: title.to_string().into(),
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
mtime: metadata.mtime.timestamp_for_user().into(),
|
mtime: metadata.mtime.timestamp_for_user().into(),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue