Revert "Rename agent threads from the sidebar" (#57654)
Reverts zed-industries/zed#57521 Forgot review approval Release Notes: - N/A
This commit is contained in:
1 parent
b31bc1507b
commit
d7fc2cae01
11 files changed
+53
-666
No files matched your search
@@ -729,7 +729,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"space": "menu::Confirm",
|
||||
"shift-r": "agent::RenameSelectedThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -785,7 +785,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"space": "menu::Confirm",
|
||||
"shift-r": "agent::RenameSelectedThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -732,7 +732,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"space": "menu::Confirm",
|
||||
"shift-r": "agent::RenameSelectedThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -596,68 +596,6 @@ impl ThreadsDatabase {
|
||||
.spawn(async move { Self::save_thread_sync(&connection, id, thread, &folder_paths) })
|
||||
}
|
||||
|
||||
pub fn update_thread_title(
|
||||
&self,
|
||||
id: acp::SessionId,
|
||||
title: SharedString,
|
||||
) -> Task<Result<bool>> {
|
||||
let connection = self.connection.clone();
|
||||
|
||||
self.executor
|
||||
.spawn(async move { Self::update_thread_title_sync(&connection, id, title) })
|
||||
}
|
||||
|
||||
fn update_thread_title_sync(
|
||||
connection: &Arc<Mutex<Connection>>,
|
||||
id: acp::SessionId,
|
||||
title: SharedString,
|
||||
) -> Result<bool> {
|
||||
const COMPRESSION_LEVEL: i32 = 3;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SerializedThread {
|
||||
#[serde(flatten)]
|
||||
thread: DbThread,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
let connection = connection.lock();
|
||||
let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {"
|
||||
SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
|
||||
"})?;
|
||||
|
||||
let Some((data_type, data)) = select(id.0.clone())?.into_iter().next() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let json_data = match data_type {
|
||||
DataType::Zstd => {
|
||||
let decompressed = zstd::decode_all(&data[..])?;
|
||||
String::from_utf8(decompressed)?
|
||||
}
|
||||
DataType::Json => String::from_utf8(data)?,
|
||||
};
|
||||
let mut thread = DbThread::from_json(json_data.as_bytes())?;
|
||||
thread.title = title.clone();
|
||||
|
||||
let json_data = serde_json::to_string(&SerializedThread {
|
||||
thread,
|
||||
version: DbThread::VERSION,
|
||||
})?;
|
||||
let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?;
|
||||
let data_type = DataType::Zstd;
|
||||
let data = compressed;
|
||||
let title = title.to_string();
|
||||
|
||||
let mut update =
|
||||
connection.exec_bound::<(String, DataType, Vec<u8>, Arc<str>)>(indoc! {"
|
||||
UPDATE threads SET summary = ?1, data_type = ?2, data = ?3 WHERE id = ?4
|
||||
"})?;
|
||||
|
||||
update((title, data_type, data, id.0))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> {
|
||||
let connection = self.connection.clone();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{DbThread, DbThreadMetadata, ThreadsDatabase};
|
||||
use agent_client_protocol::schema as acp;
|
||||
use anyhow::{Result, anyhow};
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{App, Context, Entity, Global, SharedString, Task, prelude::*};
|
||||
use gpui::{App, Context, Entity, Global, Task, prelude::*};
|
||||
use util::path_list::PathList;
|
||||
|
||||
struct GlobalThreadStore(Entity<ThreadStore>);
|
||||
@@ -74,32 +74,6 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update_thread_title(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
title: SharedString,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let database = database_future.await.map_err(|err| anyhow!(err))?;
|
||||
let updated = database
|
||||
.update_thread_title(id.clone(), title.clone())
|
||||
.await?;
|
||||
if updated {
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(thread) = this.threads.iter_mut().find(|thread| thread.id == id) {
|
||||
thread.title = title;
|
||||
cx.notify();
|
||||
} else {
|
||||
this.reload(cx);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Ok(updated)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_thread(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
@@ -257,40 +231,6 @@ mod tests {
|
||||
assert!(thread_store.read_with(cx, |store, _cx| store.is_empty()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_update_thread_title_updates_metadata_and_thread_data(cx: &mut TestAppContext) {
|
||||
let thread_store = cx.new(|cx| ThreadStore::new(cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let thread_id = session_id("thread-a");
|
||||
let updated_at = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
|
||||
let thread = make_thread("Old Title", updated_at);
|
||||
|
||||
let save_task = thread_store.update(cx, |store, cx| {
|
||||
store.save_thread(thread_id.clone(), thread, PathList::default(), cx)
|
||||
});
|
||||
save_task.await.unwrap();
|
||||
|
||||
let update_task = thread_store.update(cx, |store, cx| {
|
||||
store.update_thread_title(thread_id.clone(), "New Title".into(), cx)
|
||||
});
|
||||
assert!(update_task.await.unwrap());
|
||||
cx.run_until_parked();
|
||||
|
||||
let entries: Vec<_> = thread_store.read_with(cx, |store, _cx| store.entries().collect());
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|entry| entry.id == thread_id)
|
||||
.expect("updated thread should be present");
|
||||
assert_eq!(entry.title.as_ref(), "New Title");
|
||||
assert_eq!(entry.updated_at, updated_at);
|
||||
|
||||
let load_task = thread_store.update(cx, |store, cx| store.load_thread(thread_id, cx));
|
||||
let loaded_thread = load_task.await.unwrap().expect("thread should load");
|
||||
assert_eq!(loaded_thread.title.as_ref(), "New Title");
|
||||
assert_eq!(loaded_thread.updated_at, updated_at);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_thread_removes_only_target(cx: &mut TestAppContext) {
|
||||
let thread_store = cx.new(|cx| ThreadStore::new(cx));
|
||||
|
||||
@@ -1211,7 +1211,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();
|
||||
@@ -1270,21 +1270,12 @@ impl AgentPanel {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let _thread_metadata_store_subscription = cx.subscribe_in(
|
||||
let _thread_metadata_store_subscription = cx.subscribe(
|
||||
&ThreadMetadataStore::global(cx),
|
||||
window,
|
||||
|this, _store, event, window, cx| match event {
|
||||
ThreadMetadataStoreEvent::ThreadArchived(thread_id) => {
|
||||
if this.retained_threads.remove(thread_id).is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
ThreadMetadataStoreEvent::ThreadTitleUpdated { thread_id, title } => {
|
||||
let thread_id = *thread_id;
|
||||
let title = title.clone();
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
this.update_loaded_thread_title(thread_id, title, window, cx);
|
||||
});
|
||||
|this, _store, event, cx| {
|
||||
let ThreadMetadataStoreEvent::ThreadArchived(thread_id) = event;
|
||||
if this.retained_threads.remove(thread_id).is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1645,7 +1636,7 @@ impl AgentPanel {
|
||||
None,
|
||||
Some(thread_id),
|
||||
Some(metadata.folder_paths().clone()),
|
||||
metadata.title(),
|
||||
metadata.title.clone(),
|
||||
initial_content,
|
||||
AgentThreadSource::AgentPanel,
|
||||
window,
|
||||
@@ -2836,42 +2827,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_loaded_thread_title(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
title: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut updated_loaded_thread = false;
|
||||
let conversation_views = self.conversation_views();
|
||||
for conversation_view in conversation_views {
|
||||
if conversation_view.read(cx).thread_id != thread_id {
|
||||
continue;
|
||||
}
|
||||
let Some(thread_view) = conversation_view.read(cx).root_thread_view() else {
|
||||
continue;
|
||||
};
|
||||
updated_loaded_thread = true;
|
||||
let (title_editor, thread) = thread_view.read_with(cx, |thread_view, _| {
|
||||
(thread_view.title_editor.clone(), thread_view.thread.clone())
|
||||
});
|
||||
if title_editor.read(cx).text(cx) != title {
|
||||
title_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(title.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
thread.update(cx, |thread, cx| {
|
||||
if thread.can_set_title(cx) {
|
||||
thread.set_title(title.clone(), cx).detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
if updated_loaded_thread {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops a thread — retained or the active ephemeral draft — from
|
||||
/// the panel and deletes its metadata row. Used by the sidebar when
|
||||
/// the user dismisses a parked draft.
|
||||
|
||||
@@ -199,8 +199,6 @@ actions!(
|
||||
ArchiveSelectedThread,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
/// Renames the currently selected thread.
|
||||
RenameSelectedThread,
|
||||
/// Starts a chat conversation with follow-up enabled.
|
||||
ChatWithFollow,
|
||||
/// Cycles to the next inline assist suggestion.
|
||||
|
||||
@@ -704,50 +704,18 @@ impl ThreadMetadataStore {
|
||||
thread_id: ThreadId,
|
||||
title_override: SharedString,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let Some(existing) = self.entry(thread_id).cloned() else {
|
||||
return false;
|
||||
) {
|
||||
let Some(existing) = self.entry(thread_id) else {
|
||||
return;
|
||||
};
|
||||
if existing.title_override.as_ref() == Some(&title_override) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
let metadata = ThreadMetadata {
|
||||
title_override: Some(title_override.clone()),
|
||||
..existing
|
||||
title_override: Some(title_override),
|
||||
..existing.clone()
|
||||
};
|
||||
self.save(metadata, cx);
|
||||
cx.emit(ThreadMetadataStoreEvent::ThreadTitleUpdated {
|
||||
thread_id,
|
||||
title: title_override,
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub fn rename_thread(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
title: SharedString,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let metadata = self.entry(thread_id).cloned();
|
||||
self.set_title_override(thread_id, title.clone(), cx);
|
||||
|
||||
let Some(metadata) = metadata else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let Some(session_id) = metadata.session_id else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
if metadata.agent_id.as_ref() != ZED_AGENT_ID.as_ref() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let Some(thread_store) = ThreadStore::try_global(cx) else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let update_title = thread_store.update(cx, |store, cx| {
|
||||
store.update_thread_title(session_id, title, cx)
|
||||
});
|
||||
cx.spawn(async move |_, _| update_title.await.map(|_| ()))
|
||||
}
|
||||
|
||||
fn save_internal(&mut self, metadata: ThreadMetadata) {
|
||||
@@ -1373,10 +1341,6 @@ impl Global for ThreadMetadataStore {}
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ThreadMetadataStoreEvent {
|
||||
ThreadArchived(ThreadId),
|
||||
ThreadTitleUpdated {
|
||||
thread_id: ThreadId,
|
||||
title: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
impl gpui::EventEmitter<ThreadMetadataStoreEvent> for ThreadMetadataStore {}
|
||||
@@ -2020,60 +1984,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_thread_updates_native_thread_database(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let session_id = acp::SessionId::new("session-1");
|
||||
let updated_at = Utc::now();
|
||||
let save_task = cx.update(|cx| {
|
||||
ThreadStore::global(cx).update(cx, |store, cx| {
|
||||
store.save_thread(
|
||||
session_id.clone(),
|
||||
make_db_thread("Agent Generated Title", updated_at),
|
||||
PathList::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
save_task.await.unwrap();
|
||||
|
||||
let metadata = make_metadata(
|
||||
"session-1",
|
||||
"Agent Generated Title",
|
||||
updated_at,
|
||||
PathList::default(),
|
||||
);
|
||||
let thread_id = metadata.thread_id;
|
||||
let rename_task = cx.update(|cx| {
|
||||
let store = ThreadMetadataStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
store.save(metadata, cx);
|
||||
store.rename_thread(thread_id, "User Title".into(), cx)
|
||||
})
|
||||
});
|
||||
rename_task.await.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|cx| {
|
||||
let store = ThreadMetadataStore::global(cx);
|
||||
let metadata = store
|
||||
.read(cx)
|
||||
.entry(thread_id)
|
||||
.expect("metadata should be cached")
|
||||
.clone();
|
||||
assert_eq!(metadata.title.as_deref(), Some("Agent Generated Title"));
|
||||
assert_eq!(metadata.title_override.as_deref(), Some("User Title"));
|
||||
});
|
||||
|
||||
let load_task = cx.update(|cx| {
|
||||
ThreadStore::global(cx).update(cx, |store, cx| store.load_thread(session_id, cx))
|
||||
});
|
||||
let loaded_thread = load_task.await.unwrap().expect("thread should load");
|
||||
assert_eq!(loaded_thread.title.as_ref(), "User Title");
|
||||
assert_eq!(loaded_thread.updated_at, updated_at);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_store_initializes_cache_from_database(cx: &mut TestAppContext) {
|
||||
let first_paths = PathList::new(&[Path::new("/project-a")]);
|
||||
|
||||
+38
-192
@@ -18,8 +18,8 @@ use agent_ui::threads_archive_view::{
|
||||
use agent_ui::{
|
||||
AcpThreadImportOnboarding, Agent, AgentPanel, AgentPanelEvent, AgentThreadSource,
|
||||
ArchiveSelectedThread, CrossChannelImportOnboarding, DEFAULT_THREAD_TITLE, NewTerminalThread,
|
||||
NewThread, RenameSelectedThread, TerminalId, ThreadId, ThreadImportModal,
|
||||
channels_with_threads, import_threads_from_other_channels,
|
||||
NewThread, TerminalId, ThreadId, ThreadImportModal, channels_with_threads,
|
||||
import_threads_from_other_channels,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use editor::Editor;
|
||||
@@ -637,7 +637,6 @@ pub struct Sidebar {
|
||||
width: Pixels,
|
||||
focus_handle: FocusHandle,
|
||||
filter_editor: Entity<Editor>,
|
||||
thread_rename_editor: Entity<Editor>,
|
||||
list_state: ListState,
|
||||
contents: SidebarContents,
|
||||
/// The index of the list item that currently has the keyboard focus
|
||||
@@ -647,7 +646,6 @@ pub struct Sidebar {
|
||||
/// Tracks which sidebar entry is currently active (highlighted).
|
||||
active_entry: Option<ActiveEntry>,
|
||||
hovered_thread_index: Option<usize>,
|
||||
renaming_thread_id: Option<ThreadId>,
|
||||
|
||||
/// Updated only in response to explicit user actions (clicking a
|
||||
/// thread, confirming in the thread switcher, etc.) — never from
|
||||
@@ -693,7 +691,6 @@ impl Sidebar {
|
||||
editor.set_placeholder_text("Search threads…", window, cx);
|
||||
editor
|
||||
});
|
||||
let thread_rename_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
cx.subscribe_in(
|
||||
&multi_workspace,
|
||||
@@ -730,17 +727,6 @@ impl Sidebar {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(
|
||||
&thread_rename_editor,
|
||||
window,
|
||||
|this, _, event, window, cx| {
|
||||
if matches!(event, editor::EditorEvent::Blurred) {
|
||||
this.confirm_thread_rename(window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.observe(&ThreadMetadataStore::global(cx), |this, _store, cx| {
|
||||
this.update_entries(cx);
|
||||
})
|
||||
@@ -770,13 +756,11 @@ impl Sidebar {
|
||||
width: DEFAULT_WIDTH,
|
||||
focus_handle,
|
||||
filter_editor,
|
||||
thread_rename_editor,
|
||||
list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
|
||||
contents: SidebarContents::default(),
|
||||
selection: None,
|
||||
active_entry: None,
|
||||
hovered_thread_index: None,
|
||||
renaming_thread_id: None,
|
||||
|
||||
thread_last_accessed: HashMap::new(),
|
||||
terminal_last_accessed: HashMap::new(),
|
||||
@@ -1691,8 +1675,12 @@ impl Sidebar {
|
||||
|
||||
let mut matched_threads: Vec<ThreadEntry> = Vec::new();
|
||||
for mut thread in threads {
|
||||
let title = thread.metadata.display_title();
|
||||
if let Some(positions) = fuzzy_match_positions(&query, title.as_ref()) {
|
||||
let title: &str = thread
|
||||
.metadata
|
||||
.title
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_THREAD_TITLE, |t| t.as_ref());
|
||||
if let Some(positions) = fuzzy_match_positions(&query, title) {
|
||||
thread.highlight_positions = positions;
|
||||
}
|
||||
let mut worktree_matched = false;
|
||||
@@ -2744,17 +2732,10 @@ impl Sidebar {
|
||||
|
||||
let is_archived_search_focused = matches!(&self.view, SidebarView::Archive(archive) if archive.read(cx).is_filter_editor_focused(window, cx));
|
||||
|
||||
let is_renaming_thread = self
|
||||
.thread_rename_editor
|
||||
.focus_handle(cx)
|
||||
.is_focused(window);
|
||||
|
||||
let identifier = if self.filter_editor.focus_handle(cx).is_focused(window)
|
||||
|| is_archived_search_focused
|
||||
{
|
||||
"searching"
|
||||
} else if is_renaming_thread {
|
||||
"editing"
|
||||
} else {
|
||||
"not_searching"
|
||||
};
|
||||
@@ -2779,11 +2760,6 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.renaming_thread_id.is_some() {
|
||||
self.cancel_thread_rename(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.filter_editor.read(cx).is_focused(window) {
|
||||
if self.reset_filter_editor_text(window, cx) {
|
||||
self.selection = None;
|
||||
@@ -2844,61 +2820,6 @@ impl Sidebar {
|
||||
!self.filter_editor.read(cx).text(cx).is_empty()
|
||||
}
|
||||
|
||||
fn start_renaming_thread(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
thread_id: ThreadId,
|
||||
title: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.renaming_thread_id.is_some() && self.renaming_thread_id != Some(thread_id) {
|
||||
self.confirm_thread_rename(window, cx);
|
||||
}
|
||||
|
||||
self.selection = Some(ix);
|
||||
self.renaming_thread_id = Some(thread_id);
|
||||
self.list_state.scroll_to_reveal_item(ix);
|
||||
self.thread_rename_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(title, window, cx);
|
||||
editor.select_all(&editor::actions::SelectAll, window, cx);
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm_thread_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
||||
let Some(thread_id) = self.renaming_thread_id.take() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let new_title = self
|
||||
.thread_rename_editor
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.trim()
|
||||
.to_string();
|
||||
if !new_title.is_empty() {
|
||||
let title = SharedString::from(new_title);
|
||||
ThreadMetadataStore::global(cx).update(cx, |store, cx| {
|
||||
store
|
||||
.rename_thread(thread_id, title, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.update_entries(cx);
|
||||
true
|
||||
}
|
||||
|
||||
fn cancel_thread_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.renaming_thread_id.take().is_some() {
|
||||
self.focus_handle.focus(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.select_next(&SelectNext, window, cx);
|
||||
if self.selection.is_some() {
|
||||
@@ -2973,10 +2894,6 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.confirm_thread_rename(window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(ix) = self.selection else { return };
|
||||
let Some(entry) = self.contents.entries.get(ix) else {
|
||||
return;
|
||||
@@ -3068,7 +2985,7 @@ impl Sidebar {
|
||||
Agent::from(metadata.agent_id.clone()),
|
||||
metadata.thread_id,
|
||||
Some(metadata.folder_paths().clone()),
|
||||
metadata.title(),
|
||||
metadata.title.clone(),
|
||||
focus,
|
||||
AgentThreadSource::Sidebar,
|
||||
window,
|
||||
@@ -5077,23 +4994,6 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
fn rename_selected_thread(
|
||||
&mut self,
|
||||
_: &RenameSelectedThread,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(ix) = self.selection else {
|
||||
return;
|
||||
};
|
||||
let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
|
||||
return;
|
||||
};
|
||||
let thread_id = thread.metadata.thread_id;
|
||||
let title = thread.metadata.display_title();
|
||||
self.start_renaming_thread(ix, thread_id, title, window, cx);
|
||||
}
|
||||
|
||||
fn record_thread_access(&mut self, id: &ThreadId) {
|
||||
self.thread_last_accessed.insert(*id, Utc::now());
|
||||
}
|
||||
@@ -5524,12 +5424,10 @@ impl Sidebar {
|
||||
thread.status,
|
||||
AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
|
||||
);
|
||||
let is_renaming = self.renaming_thread_id == Some(thread.metadata.thread_id);
|
||||
|
||||
let thread_id_for_actions = thread.metadata.thread_id;
|
||||
let session_id_for_delete = thread.metadata.session_id.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let title_editor = self.thread_rename_editor.clone();
|
||||
|
||||
let id = SharedString::from(format!("thread-entry-{}", ix));
|
||||
|
||||
@@ -5553,7 +5451,7 @@ impl Sidebar {
|
||||
(thread.icon, thread.icon_from_external_svg.clone())
|
||||
};
|
||||
|
||||
ThreadItem::new(id, title.clone())
|
||||
ThreadItem::new(id, title)
|
||||
.base_bg(sidebar_bg)
|
||||
.icon(icon)
|
||||
.when(is_draft, |this| {
|
||||
@@ -5586,84 +5484,22 @@ impl Sidebar {
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.when(is_renaming, |this| {
|
||||
this.title_slot(
|
||||
div()
|
||||
.h_6()
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
.capture_action(cx.listener(
|
||||
|this, _: &editor::actions::Newline, window, cx| {
|
||||
this.confirm_thread_rename(window, cx);
|
||||
},
|
||||
))
|
||||
.on_action(cx.listener(|this, _: &Confirm, window, cx| {
|
||||
this.confirm_thread_rename(window, cx);
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &editor::actions::Cancel, window, cx| {
|
||||
this.cancel_thread_rename(window, cx);
|
||||
}),
|
||||
)
|
||||
.child(title_editor),
|
||||
)
|
||||
})
|
||||
.when(is_hovered && !is_renaming, |this| {
|
||||
let rename_button = IconButton::new(("rename-thread", ix), IconName::Pencil)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Rename Thread",
|
||||
&RenameSelectedThread,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let title = title.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.start_renaming_thread(
|
||||
ix,
|
||||
thread_id_for_actions,
|
||||
title.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
let contextual_action = if is_running {
|
||||
.when(is_hovered && is_running, |this| {
|
||||
this.action_slot(
|
||||
IconButton::new("stop-thread", IconName::Stop)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Error)
|
||||
.style(ButtonStyle::Tinted(TintColor::Error))
|
||||
.tooltip(Tooltip::text("Stop Generation"))
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.stop_thread(&thread_id_for_actions, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else if is_draft {
|
||||
IconButton::new("discard_thread", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Discard Draft"))
|
||||
.on_click({
|
||||
let thread_workspace = thread_workspace.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.remove_draft(
|
||||
thread_id_for_actions,
|
||||
&thread_workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.listener(move |this, _, _window, cx| {
|
||||
this.stop_thread(&thread_id_for_actions, cx);
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
} else {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(is_hovered && !is_running && !is_draft, |this| {
|
||||
this.action_slot(
|
||||
IconButton::new("archive-thread", IconName::Archive)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -5685,15 +5521,26 @@ impl Sidebar {
|
||||
this.archive_thread(session_id, window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(is_hovered && !is_running && is_draft, |this| {
|
||||
this.action_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(rename_button)
|
||||
.child(contextual_action),
|
||||
IconButton::new("discard_thread", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Discard Draft"))
|
||||
.on_click({
|
||||
let thread_workspace = thread_workspace.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.remove_draft(
|
||||
thread_id_for_actions,
|
||||
&thread_workspace,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
@@ -7120,7 +6967,6 @@ impl Render for Sidebar {
|
||||
.on_action(cx.listener(Self::unfold_all))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::archive_selected_thread))
|
||||
.on_action(cx.listener(Self::rename_selected_thread))
|
||||
.on_action(cx.listener(Self::new_thread_in_group))
|
||||
.on_action(cx.listener(Self::new_terminal_thread))
|
||||
.on_action(cx.listener(Self::toggle_archive))
|
||||
|
||||
@@ -4739,194 +4739,6 @@ async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_thread_from_sidebar_updates_title_override(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk::new("Hi there!".into()),
|
||||
)]);
|
||||
open_thread_with_connection(&panel, connection, cx);
|
||||
send_message(&panel, cx);
|
||||
|
||||
let session_id = active_session_id(&panel, cx);
|
||||
save_test_thread_metadata(&session_id, &project, cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let (entry_ix, thread_id, title) = sidebar.read_with(cx, |sidebar, _cx| {
|
||||
sidebar
|
||||
.contents
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(ix, entry)| match entry {
|
||||
ListEntry::Thread(thread) => Some((
|
||||
ix,
|
||||
thread.metadata.thread_id,
|
||||
thread.metadata.display_title(),
|
||||
)),
|
||||
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
|
||||
})
|
||||
.expect("sidebar should have a thread entry")
|
||||
});
|
||||
|
||||
let renamed_title = "abcdefghijklmnopqrstuvwxyé renamed";
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.start_renaming_thread(entry_ix, thread_id, title, window, cx);
|
||||
sidebar.thread_rename_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(renamed_title, window, cx);
|
||||
});
|
||||
sidebar.confirm_thread_rename(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let metadata = cx.update(|_, cx| {
|
||||
ThreadMetadataStore::global(cx)
|
||||
.read(cx)
|
||||
.entry(thread_id)
|
||||
.cloned()
|
||||
.expect("thread metadata should exist")
|
||||
});
|
||||
assert_eq!(metadata.title_override.as_deref(), Some(renamed_title));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
vec![
|
||||
//
|
||||
"v [my-project]",
|
||||
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
|
||||
]
|
||||
);
|
||||
|
||||
let active_thread = panel.read_with(cx, |panel, cx| panel.active_agent_thread(cx).unwrap());
|
||||
assert_eq!(
|
||||
active_thread.read_with(cx, |thread, _| thread.title()),
|
||||
Some(renamed_title.into())
|
||||
);
|
||||
let active_thread_view = panel.read_with(cx, |panel, cx| panel.active_thread_view(cx).unwrap());
|
||||
let title_editor_text =
|
||||
active_thread_view.read_with(cx, |view, cx| view.title_editor.read(cx).text(cx));
|
||||
assert_eq!(title_editor_text, renamed_title);
|
||||
|
||||
active_thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.set_title("abcdefghijklmnopqrstuvwxyz0".into(), cx)
|
||||
.detach();
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
vec![
|
||||
//
|
||||
"v [my-project]",
|
||||
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
|
||||
]
|
||||
);
|
||||
|
||||
type_in_search(&sidebar, "0", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
Vec::<String>::new()
|
||||
);
|
||||
|
||||
type_in_search(&sidebar, "é", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
vec![
|
||||
//
|
||||
"v [my-project]",
|
||||
" abcdefghijklmnopqrstuvwxyé renamed * <== selected",
|
||||
]
|
||||
);
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
let thread = sidebar
|
||||
.contents
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
ListEntry::Thread(thread) => Some(thread),
|
||||
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
|
||||
})
|
||||
.expect("renamed thread should match the search");
|
||||
let title = thread.metadata.display_title();
|
||||
assert!(
|
||||
thread
|
||||
.highlight_positions
|
||||
.iter()
|
||||
.all(|position| { title.is_char_boundary(*position) })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_selected_thread_action_renames_selected_thread(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk::new("Hi there!".into()),
|
||||
)]);
|
||||
open_thread_with_connection(&panel, connection, cx);
|
||||
send_message(&panel, cx);
|
||||
|
||||
let session_id = active_session_id(&panel, cx);
|
||||
save_test_thread_metadata(&session_id, &project, cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let (entry_ix, thread_id) = sidebar.read_with(cx, |sidebar, _cx| {
|
||||
sidebar
|
||||
.contents
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(ix, entry)| match entry {
|
||||
ListEntry::Thread(thread) => Some((ix, thread.metadata.thread_id)),
|
||||
ListEntry::ProjectHeader { .. } | ListEntry::Terminal(_) => None,
|
||||
})
|
||||
.expect("sidebar should have a thread entry")
|
||||
});
|
||||
|
||||
focus_sidebar(&sidebar, cx);
|
||||
sidebar.update_in(cx, |sidebar, _window, _cx| {
|
||||
sidebar.selection = Some(entry_ix);
|
||||
});
|
||||
cx.dispatch_action(RenameSelectedThread);
|
||||
cx.run_until_parked();
|
||||
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.renaming_thread_id,
|
||||
Some(thread_id),
|
||||
"dispatching RenameSelectedThread should start renaming the selected thread"
|
||||
);
|
||||
});
|
||||
|
||||
let renamed_title = "Renamed via action";
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.thread_rename_editor.update(cx, |editor, cx| {
|
||||
editor.set_text(renamed_title, window, cx);
|
||||
});
|
||||
sidebar.confirm_thread_rename(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let metadata = cx.update(|_, cx| {
|
||||
ThreadMetadataStore::global(cx)
|
||||
.read(cx)
|
||||
.entry(thread_id)
|
||||
.cloned()
|
||||
.expect("thread metadata should exist")
|
||||
});
|
||||
assert_eq!(metadata.title_override.as_deref(), Some(renamed_title));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
|
||||
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
|
||||
|
||||
@@ -39,7 +39,6 @@ pub struct ThreadItem {
|
||||
icon_visible: bool,
|
||||
custom_icon_from_external_svg: Option<SharedString>,
|
||||
title: SharedString,
|
||||
title_slot: Option<AnyElement>,
|
||||
title_label_color: Option<Color>,
|
||||
title_generating: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
@@ -72,7 +71,6 @@ impl ThreadItem {
|
||||
icon_visible: true,
|
||||
custom_icon_from_external_svg: None,
|
||||
title: title.into(),
|
||||
title_slot: None,
|
||||
title_label_color: None,
|
||||
title_generating: false,
|
||||
highlight_positions: Vec::new(),
|
||||
@@ -142,11 +140,6 @@ impl ThreadItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title_slot(mut self, element: impl IntoElement) -> Self {
|
||||
self.title_slot = Some(element.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
|
||||
self.highlight_positions = positions;
|
||||
self
|
||||
@@ -324,9 +317,7 @@ impl RenderOnce for ThreadItem {
|
||||
let title = self.title;
|
||||
let highlight_positions = self.highlight_positions;
|
||||
|
||||
let title_label = if let Some(title_slot) = self.title_slot {
|
||||
title_slot
|
||||
} else if self.title_generating {
|
||||
let title_label = if self.title_generating {
|
||||
Label::new(title)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
|
||||
Reference in new issue
Block a user