sidebar: Add title renaming for Terminal Threads (#63494)

The Agent Panel already lets users rename Terminal Threads. This adds
the same option to the Threads Sidebar. A rename in either place updates
the other.

<p>
<img width="321" height="89" alt="image"
src="https://github.com/user-attachments/assets/189a8598-f0da-4622-a5b0-b6a18a5750b7"
/>
</p>
<p>
<img width="324" height="77" alt="image"
src="https://github.com/user-attachments/assets/2fc9f7aa-c791-4568-8729-fd3a78322d63"
/>
</p>

Release Notes:

- Added support for renaming Terminal Threads from the Threads Sidebar.
This commit is contained in:
Smit Barmase authored and GitHub committed 2026-08-31 17:38:52 +00:00
1 parent 72dc742101
commit 7402d91b26
5 files changed
+381 -96

No files matched your search

+27 -9
View File
@@ -41,7 +41,7 @@ use crate::agent_connection_store::AgentConnectionStore;
use crate::completion_provider::{AgentContextSelection, AgentContextSource};
use crate::terminal_thread_metadata_store::{
TerminalThreadMetadata, TerminalThreadMetadataStore, compose_terminal_thread_title,
terminal_title_without_prefix,
normalize_terminal_custom_title, terminal_title_without_prefix,
};
use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};
use crate::{
@@ -2612,17 +2612,15 @@ impl AgentPanel {
if initial_title.as_deref() == Some(new_title.as_str()) {
return;
}
let label = if new_title.trim().is_empty()
|| new_title == terminal_title_without_prefix(terminal_title.as_ref())
{
None
} else {
Some(new_title)
};
let custom_title = normalize_terminal_custom_title(
terminal_title.as_ref(),
SharedString::from(new_title),
);
cx.defer(move |cx| {
terminal_view.update(cx, |terminal_view, cx| {
terminal_view.set_custom_title(label, cx);
terminal_view
.set_custom_title(custom_title.map(|title| title.to_string()), cx);
});
});
}
@@ -3373,6 +3371,26 @@ impl AgentPanel {
self.terminals.contains_key(&terminal_id)
}
pub fn rename_terminal(
&mut self,
terminal_id: TerminalId,
title: SharedString,
cx: &mut Context<Self>,
) -> bool {
let Some(terminal) = self.terminals.get(&terminal_id) else {
return false;
};
let terminal_title = terminal.terminal_title(cx);
let custom_title = normalize_terminal_custom_title(terminal_title.as_ref(), title);
let terminal_view = terminal.view.clone();
cx.defer(move |cx| {
terminal_view.update(cx, |terminal_view, cx| {
terminal_view.set_custom_title(custom_title.map(|title| title.to_string()), cx);
});
});
true
}
pub fn terminals(&self, cx: &App) -> Vec<AgentPanelTerminalInfo> {
self.terminals
.iter()
@@ -70,6 +70,12 @@ impl TerminalThreadMetadata {
self.custom_title.as_ref().map(|title| title.as_ref()),
)
}
pub fn editable_title(&self) -> SharedString {
self.custom_title.clone().unwrap_or_else(|| {
SharedString::from(terminal_title_without_prefix(self.title.as_ref()).to_string())
})
}
}
pub(crate) fn compose_terminal_thread_title(
@@ -93,6 +99,19 @@ pub(crate) fn terminal_title_without_prefix(title: &str) -> &str {
.unwrap_or(title)
}
pub(crate) fn normalize_terminal_custom_title(
terminal_title: &str,
edited_title: SharedString,
) -> Option<SharedString> {
if edited_title.trim().is_empty()
|| edited_title == terminal_title_without_prefix(terminal_title)
{
None
} else {
Some(edited_title)
}
}
pub fn terminal_title_prefix(title: &str) -> Option<&str> {
let mut prefix_byte_len = 0;
let mut saw_prefix_character = false;
@@ -264,6 +283,25 @@ impl TerminalThreadMetadataStore {
cx.notify();
}
pub fn rename_terminal(
&mut self,
terminal_id: TerminalId,
title: SharedString,
cx: &mut Context<Self>,
) {
let Some(mut metadata) = self.entry(terminal_id).cloned() else {
return;
};
let custom_title = normalize_terminal_custom_title(metadata.title.as_ref(), title);
if metadata.custom_title == custom_title {
return;
}
metadata.custom_title = custom_title;
self.save_internal(metadata);
cx.notify();
}
pub fn change_worktree_paths(
&mut self,
current_folder_paths: &PathList,
@@ -659,6 +697,47 @@ mod tests {
assert_eq!(metadata.display_title().as_ref(), "Fix bug");
}
#[gpui::test]
async fn test_rename_terminal_updates_stored_custom_title(cx: &mut TestAppContext) {
init_test(cx);
let metadata = metadata(
"⠋ Dev Server",
WorktreePaths::from_folder_paths(&PathList::default()),
);
let terminal_id = metadata.terminal_id;
cx.update(|cx| {
TerminalThreadMetadataStore::global(cx).update(cx, |store, cx| {
store.save(metadata, cx);
store.rename_terminal(terminal_id, "Renamed Terminal".into(), cx);
});
});
cx.update(|cx| {
let store = TerminalThreadMetadataStore::global(cx);
let metadata = store
.read(cx)
.entry(terminal_id)
.expect("renamed terminal metadata should exist");
assert_eq!(metadata.custom_title.as_deref(), Some("Renamed Terminal"));
assert_eq!(metadata.display_title().as_ref(), "⠋ Renamed Terminal");
});
cx.update(|cx| {
TerminalThreadMetadataStore::global(cx).update(cx, |store, cx| {
store.rename_terminal(terminal_id, "Dev Server".into(), cx);
});
let store = TerminalThreadMetadataStore::global(cx);
let metadata = store
.read(cx)
.entry(terminal_id)
.expect("renamed terminal metadata should exist");
assert_eq!(metadata.custom_title, None);
assert_eq!(metadata.display_title().as_ref(), "⠋ Dev Server");
});
}
#[gpui::test]
async fn test_change_worktree_paths_reindexes_terminal_metadata(cx: &mut TestAppContext) {
init_test(cx);
+166 -65
View File
@@ -406,6 +406,28 @@ enum ListEntry {
Terminal(TerminalEntry),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RenameTarget {
Thread(ThreadId),
Terminal(TerminalId),
}
impl RenameTarget {
fn from_entry(entry: &ListEntry) -> Option<(Self, SharedString)> {
match entry {
ListEntry::Thread(thread) => Some((
Self::Thread(thread.metadata.thread_id),
thread.metadata.display_title(),
)),
ListEntry::Terminal(terminal) => Some((
Self::Terminal(terminal.metadata.terminal_id),
terminal.metadata.editable_title(),
)),
ListEntry::ProjectHeader { .. } => None,
}
}
}
#[derive(Clone)]
enum ActivatableEntry {
Thread {
@@ -743,7 +765,7 @@ pub struct Sidebar {
width: Pixels,
focus_handle: FocusHandle,
filter_editor: Entity<Editor>,
thread_rename_editor: Entity<Editor>,
rename_editor: Entity<Editor>,
list_state: ListState,
contents: SidebarContents,
/// The index of the list item that currently has the keyboard focus
@@ -753,11 +775,11 @@ 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>,
rename_target: Option<RenameTarget>,
/// Threads in the database-backed regeneration path need their own loading
/// state because they do not have a live `agent::Thread` to report it.
regenerating_titles: HashSet<ThreadId>,
/// start_renaming_thread must seed current title into the title editor
/// Starting a rename must seed the current title into the title editor,
/// so this prevents that BufferEdited event from being interpreted as user input.
suppress_next_rename_edit: bool,
@@ -815,7 +837,7 @@ impl Sidebar {
editor.set_placeholder_text("Search threads…", window, cx);
editor
});
let thread_rename_editor = cx.new(|cx| Editor::single_line(window, cx));
let rename_editor = cx.new(|cx| Editor::single_line(window, cx));
cx.subscribe_in(
&multi_workspace,
@@ -850,10 +872,10 @@ impl Sidebar {
.detach();
cx.subscribe_in(
&thread_rename_editor,
&rename_editor,
window,
|this, title_editor, event, window, cx| {
this.handle_thread_rename_editor_event(title_editor, event, window, cx);
this.handle_rename_editor_event(title_editor, event, window, cx);
},
)
.detach();
@@ -898,13 +920,13 @@ impl Sidebar {
width: DEFAULT_WIDTH,
focus_handle,
filter_editor,
thread_rename_editor,
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,
rename_target: None,
regenerating_titles: HashSet::new(),
suppress_next_rename_edit: false,
@@ -3251,16 +3273,13 @@ 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 is_renaming = self.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 {
} else if is_renaming {
"editing"
} else {
"not_searching"
@@ -3286,8 +3305,8 @@ impl Sidebar {
}
fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.renaming_thread_id.is_some() {
self.finish_thread_rename(window, cx);
if self.rename_target.is_some() {
self.finish_entry_rename(window, cx);
return;
}
@@ -3351,23 +3370,30 @@ impl Sidebar {
!self.filter_editor.read(cx).text(cx).is_empty()
}
fn start_renaming_thread(
fn start_renaming_entry(
&mut self,
ix: usize,
thread_id: ThreadId,
target: RenameTarget,
title: SharedString,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.renaming_thread_id.is_some() && self.renaming_thread_id != Some(thread_id) {
self.finish_thread_rename(window, cx);
if self.rename_target == Some(target) {
self.rename_editor.update(cx, |editor, cx| {
editor.select_all(&editor::actions::SelectAll, window, cx);
editor.focus_handle(cx).focus(window, cx);
});
return;
}
if self.rename_target.is_some() {
self.finish_entry_rename(window, cx);
}
self.selection = Some(ix);
self.renaming_thread_id = Some(thread_id);
self.rename_target = Some(target);
self.suppress_next_rename_edit = true;
self.list_state.scroll_to_reveal_item(ix);
self.thread_rename_editor.update(cx, |editor, cx| {
self.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);
@@ -3375,7 +3401,7 @@ impl Sidebar {
cx.notify();
}
fn handle_thread_rename_editor_event(
fn handle_rename_editor_event(
&mut self,
title_editor: &Entity<Editor>,
event: &editor::EditorEvent,
@@ -3392,16 +3418,24 @@ impl Sidebar {
return;
}
let new_title = title_editor.read(cx).text(cx);
if new_title.is_empty() {
return;
}
let Some(thread_id) = self.renaming_thread_id else {
let Some(target) = self.rename_target else {
return;
};
self.apply_thread_rename(thread_id, SharedString::from(new_title), window, cx);
if matches!(target, RenameTarget::Thread(_)) && new_title.is_empty() {
return;
}
let new_title = SharedString::from(new_title);
match target {
RenameTarget::Thread(thread_id) => {
self.apply_thread_rename(thread_id, new_title, window, cx);
}
RenameTarget::Terminal(terminal_id) => {
self.apply_terminal_rename(terminal_id, new_title, cx);
}
}
}
editor::EditorEvent::Blurred => {
self.finish_thread_rename(window, cx);
self.finish_entry_rename(window, cx);
}
_ => {}
}
@@ -3440,8 +3474,35 @@ impl Sidebar {
}
}
fn finish_thread_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if self.renaming_thread_id.take().is_none() {
fn apply_terminal_rename(
&mut self,
terminal_id: TerminalId,
title: SharedString,
cx: &mut Context<Self>,
) {
let mut found = false;
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
let workspaces: Vec<_> = multi_workspace.read(cx).workspaces().cloned().collect();
for workspace in workspaces {
if let Some(agent_panel) = workspace.read(cx).panel::<AgentPanel>(cx)
&& agent_panel.update(cx, |agent_panel, cx| {
agent_panel.rename_terminal(terminal_id, title.clone(), cx)
})
{
found = true;
}
}
}
if !found {
TerminalThreadMetadataStore::global(cx).update(cx, |store, cx| {
store.rename_terminal(terminal_id, title, cx);
});
}
}
fn finish_entry_rename(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if self.rename_target.take().is_none() {
return false;
}
self.focus_handle.focus(window, cx);
@@ -3523,7 +3584,7 @@ impl Sidebar {
}
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
if self.finish_thread_rename(window, cx) {
if self.finish_entry_rename(window, cx) {
return;
}
@@ -5684,12 +5745,15 @@ impl Sidebar {
let Some(ix) = self.selection else {
return;
};
let Some(ListEntry::Thread(thread)) = self.contents.entries.get(ix) else {
let Some((target, title)) = self
.contents
.entries
.get(ix)
.and_then(RenameTarget::from_entry)
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);
self.start_renaming_entry(ix, target, title, window, cx);
}
fn record_thread_access(&mut self, id: &ThreadId) {
@@ -6107,6 +6171,28 @@ impl Sidebar {
window.focus(&focus, cx);
}
fn render_rename_title_editor(&self, cx: &mut Context<Self>) -> AnyElement {
div()
.h_full()
.min_w_0()
.flex_1()
.capture_action(
cx.listener(|this, _: &editor::actions::Newline, window, cx| {
this.finish_entry_rename(window, cx);
}),
)
.on_action(cx.listener(|this, _: &Confirm, window, cx| {
this.finish_entry_rename(window, cx);
}))
.on_action(
cx.listener(|this, _: &editor::actions::Cancel, window, cx| {
this.finish_entry_rename(window, cx);
}),
)
.child(self.rename_editor.clone())
.into_any_element()
}
fn render_thread(
&self,
ix: usize,
@@ -6129,12 +6215,13 @@ impl Sidebar {
thread.status,
AgentThreadStatus::Running | AgentThreadStatus::WaitingForConfirmation
);
let is_renaming = self.renaming_thread_id == Some(thread.metadata.thread_id);
let is_renaming =
self.rename_target == Some(RenameTarget::Thread(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 rename_title_editor = is_renaming.then(|| self.render_rename_title_editor(cx));
let id = SharedString::from(format!("thread-entry-{}", ix));
@@ -6200,27 +6287,8 @@ impl Sidebar {
}
cx.notify();
}))
.when(is_renaming, |this| {
this.is_truncated(false).title_slot(
div()
.h_full()
.min_w_0()
.flex_1()
.capture_action(cx.listener(
|this, _: &editor::actions::Newline, window, cx| {
this.finish_thread_rename(window, cx);
},
))
.on_action(cx.listener(|this, _: &Confirm, window, cx| {
this.finish_thread_rename(window, cx);
}))
.on_action(
cx.listener(|this, _: &editor::actions::Cancel, window, cx| {
this.finish_thread_rename(window, cx);
}),
)
.child(title_editor),
)
.when_some(rename_title_editor, |this, title_editor| {
this.is_truncated(false).title_slot(title_editor)
})
.when(is_hovered && !is_renaming, |this| {
let rename_button = IconButton::new(("rename-thread", ix), IconName::Pencil)
@@ -6239,9 +6307,9 @@ impl Sidebar {
.on_click({
let title = title.clone();
cx.listener(move |this, _, window, cx| {
this.start_renaming_thread(
this.start_renaming_entry(
ix,
thread_id_for_actions,
RenameTarget::Thread(thread_id_for_actions),
title.clone(),
window,
cx,
@@ -6381,9 +6449,9 @@ impl Sidebar {
move |window, cx| {
sidebar
.update(cx, |sidebar, cx| {
sidebar.start_renaming_thread(
sidebar.start_renaming_entry(
ix,
thread_id,
RenameTarget::Thread(thread_id),
rename_title.clone(),
window,
cx,
@@ -6492,6 +6560,9 @@ impl Sidebar {
cx.flag_value::<AgentThreadWorktreeLabelFlag>(),
);
let is_remote = terminal.workspace.is_remote(cx);
let is_renaming =
self.rename_target == Some(RenameTarget::Terminal(terminal.metadata.terminal_id));
let rename_title_editor = is_renaming.then(|| self.render_rename_title_editor(cx));
let display_title = terminal.metadata.display_title();
let (icon_char, title, highlight_positions) =
@@ -6500,7 +6571,7 @@ impl Sidebar {
None => (None, display_title, terminal.highlight_positions.clone()),
};
ThreadItem::new(id, title)
let terminal_item = ThreadItem::new(id, title)
.base_bg(sidebar_bg)
.icon(IconName::Terminal)
.when_some(icon_char, |this, icon_char| this.icon_char(icon_char))
@@ -6520,7 +6591,10 @@ impl Sidebar {
}
cx.notify();
}))
.when(is_hovered, |this| {
.when_some(rename_title_editor, |this, title_editor| {
this.is_truncated(false).title_slot(title_editor)
})
.when(is_hovered && !is_renaming, |this| {
this.action_slot(
IconButton::new("close-terminal", IconName::Close)
.icon_size(IconSize::Small)
@@ -6553,7 +6627,34 @@ impl Sidebar {
cx,
);
}
}))
}));
let context_menu_id = SharedString::from(format!("terminal-context-menu-{ix}"));
let terminal_id = terminal.metadata.terminal_id;
let rename_title = terminal.metadata.editable_title();
let sidebar = cx.weak_entity();
right_click_menu(context_menu_id)
.trigger(move |_, _, _| terminal_item)
.menu(move |window, cx| {
let sidebar = sidebar.clone();
let rename_title = rename_title.clone();
ContextMenu::build(window, cx, move |menu, _window, _cx| {
menu.entry("Rename Title", None, move |window, cx| {
sidebar
.update(cx, |sidebar, cx| {
sidebar.start_renaming_entry(
ix,
RenameTarget::Terminal(terminal_id),
rename_title.clone(),
window,
cx,
);
})
.ok();
})
})
})
.into_any_element()
}
+108 -21
View File
@@ -24,7 +24,23 @@ use std::{
};
use util::{path_list::PathList, rel_path::rel_path};
fn use_unique_metadata_databases(cx: &mut TestAppContext) {
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT_TEST_DATABASE: AtomicUsize = AtomicUsize::new(0);
let test_database_id = NEXT_TEST_DATABASE.fetch_add(1, Ordering::SeqCst);
cx.update(|cx| {
cx.set_global(agent_ui::thread_metadata_store::TestMetadataDbName(
format!("SIDEBAR_THREAD_METADATA_{test_database_id}"),
));
cx.set_global(TestTerminalMetadataDbName(format!(
"SIDEBAR_TERMINAL_THREAD_METADATA_{test_database_id}"
)));
});
}
fn init_test(cx: &mut TestAppContext) {
use_unique_metadata_databases(cx);
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
@@ -536,6 +552,27 @@ fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
cx.run_until_parked();
}
fn enter_renamed_title(
sidebar: &Entity<Sidebar>,
target: RenameTarget,
renamed_title: &str,
cx: &mut gpui::VisualTestContext,
) {
sidebar.read_with(cx, |sidebar, _cx| {
assert_eq!(sidebar.rename_target, Some(target));
});
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.rename_editor.update(cx, |editor, cx| {
editor.set_text(renamed_title, window, cx);
});
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.finish_entry_rename(window, cx);
});
cx.run_until_parked();
}
fn request_test_tool_authorization(
thread: &Entity<AcpThread>,
tool_call_id: &str,
@@ -1765,6 +1802,7 @@ async fn init_test_project_with_agent_panel(
worktree_path: &str,
cx: &mut TestAppContext,
) -> Entity<project::Project> {
use_unique_metadata_databases(cx);
agent_ui::test_support::init_test(cx);
cx.update(|cx| {
cx.set_global(agent_ui::MaxIdleRetainedThreads(1));
@@ -5124,17 +5162,17 @@ async fn test_rename_thread_from_sidebar_updates_title_override(cx: &mut TestApp
let renamed_title = "abcdefghijklmnopqrstuvwxyé renamed";
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.start_renaming_thread(entry_ix, thread_id, title, window, cx);
sidebar.start_renaming_entry(entry_ix, RenameTarget::Thread(thread_id), title, window, cx);
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.thread_rename_editor.update(cx, |editor, cx| {
sidebar.rename_editor.update(cx, |editor, cx| {
editor.set_text(renamed_title, window, cx);
});
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.finish_thread_rename(window, cx);
sidebar.finish_entry_rename(window, cx);
});
cx.run_until_parked();
@@ -5254,25 +5292,8 @@ async fn test_rename_selected_thread_action_renames_selected_thread(cx: &mut Tes
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);
});
});
cx.run_until_parked();
sidebar.update_in(cx, |sidebar, window, cx| {
sidebar.finish_thread_rename(window, cx);
});
cx.run_until_parked();
enter_renamed_title(&sidebar, RenameTarget::Thread(thread_id), renamed_title, cx);
let metadata = cx.update(|_, cx| {
ThreadMetadataStore::global(cx)
@@ -5284,6 +5305,72 @@ async fn test_rename_selected_thread_action_renames_selected_thread(cx: &mut Tes
assert_eq!(metadata.title_override.as_deref(), Some(renamed_title));
}
#[gpui::test]
async fn test_rename_selected_thread_action_renames_terminal(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 terminal_id = panel
.update_in(cx, |panel, window, cx| {
panel.insert_test_terminal("Dev Server", true, window, cx)
})
.expect("test terminal should be inserted");
cx.run_until_parked();
let entry_ix = sidebar.read_with(cx, |sidebar, _cx| {
sidebar
.contents
.entries
.iter()
.position(|entry| {
matches!(
entry,
ListEntry::Terminal(terminal)
if terminal.metadata.terminal_id == terminal_id
)
})
.expect("sidebar should have a terminal 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();
let renamed_title = "Renamed Terminal";
enter_renamed_title(
&sidebar,
RenameTarget::Terminal(terminal_id),
renamed_title,
cx,
);
panel.read_with(cx, |panel, cx| {
let terminal = panel
.terminals(cx)
.into_iter()
.find(|terminal| terminal.id == terminal_id)
.expect("terminal should remain open after renaming");
assert_eq!(terminal.custom_title.as_deref(), Some(renamed_title));
});
sidebar.read_with(cx, |_sidebar, cx| {
let metadata = TerminalThreadMetadataStore::global(cx)
.read(cx)
.entry(terminal_id)
.cloned()
.expect("renamed terminal metadata should exist");
assert_eq!(metadata.custom_title.as_deref(), Some(renamed_title));
});
assert_eq!(
visible_entries_as_strings(&sidebar, cx),
vec!["v [my-project]", " Renamed Terminal <== selected"]
);
}
#[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;
+1 -1
View File
@@ -54,7 +54,7 @@ You can also configure this from the Settings UI under **AI**, via the "Terminal
## Terminal Thread Titles {#terminal-thread-titles}
The terminal title in the toolbar updates automatically to reflect the running shell or process. You can also set a custom name by clicking the title or the pencil icon that appears on hover.
The terminal title in the toolbar updates automatically to reflect the running shell or process. You can set a custom name by clicking the title or the pencil icon that appears on hover. In the Threads Sidebar, right-click a Terminal Thread and select **Rename Title**, or select it and press {#kb agent::RenameSelectedThread}.
## Notifications {#terminal-thread-notifications}