Persist local workspace side chats and add tab close controls (#60152)

## What changed

- Save side chats as attachments to their parent conversation when local workspace history supports them, and let users reopen them from the sidebar after a restart.
- Show chat names or first-prompt previews in sidebar labels, and allow saved side chats to be renamed with `/rename`.
- Add tab close buttons, middle-click closing, and the configurable `close_sidebar_tab` shortcut, defaulting to `Ctrl-X W`. Closing a saved chat keeps it available in the picker; archiving removes it from Side chats.
- Confirm actions that stop running work, discard unsent input, or lose temporary history. Delete saved TUI side chats when deleting their parent conversation.

## Testing

Add coverage for reopening saved side chats after restart, archive handling, close confirmations, and preventing close-confirmation keys from accepting a replacement approval dialog.

GitOrigin-RevId: 3d47533d007286a7354660c08df13a83dd48d5d7
This commit is contained in:
Felipe Coury
2026-11-07 17:31:27 +00:00
committed by copyberry
parent 57a808a392
commit 57e64682f8
54 changed files with 2047 additions and 254 deletions
+1
View File
@@ -165,6 +165,7 @@ pub use session_index::append_thread_name;
pub use session_index::find_thread_meta_by_name_str;
pub use session_index::find_thread_meta_candidates_by_name_str;
pub use session_index::find_thread_name_by_id;
pub use session_index::find_thread_name_entries_by_ids;
pub use session_index::find_thread_names_by_ids;
pub use session_index::remove_thread_name_entries;
pub use state_db::StateDbHandle;
+13 -1
View File
@@ -173,6 +173,18 @@ pub async fn find_thread_names_by_ids(
codex_home: &Path,
thread_ids: &HashSet<ThreadId>,
) -> std::io::Result<HashMap<ThreadId, String>> {
Ok(find_thread_name_entries_by_ids(codex_home, thread_ids)
.await?
.into_iter()
.map(|(id, entry)| (id, entry.thread_name.trim().to_owned()))
.collect())
}
/// Returns each thread's latest indexed name and when it was written.
pub async fn find_thread_name_entries_by_ids(
codex_home: &Path,
thread_ids: &HashSet<ThreadId>,
) -> std::io::Result<HashMap<ThreadId, SessionIndexEntry>> {
if thread_ids.is_empty() {
return Ok(HashMap::new());
}
@@ -191,7 +203,7 @@ pub async fn find_thread_names_by_ids(
let name = entry.thread_name.trim();
// The first nonempty name seen for an id is its latest usable name.
if !name.is_empty() && remaining_ids.remove(&entry.id) {
names.insert(entry.id, name.to_string());
names.insert(entry.id, entry);
if remaining_ids.is_empty() {
break;
}