Keep side conversations open when switching threads (#35011)

## What changed

- Add a configurable `toggle_side_conversation` TUI action, bound to `ctrl-/` by default, to switch between a side conversation and its parent without closing either.
- Show the active binding in the footer and distinguish switching from closing with `ctrl-c`.
- Allow starting a replacement side conversation from the parent, while cleaning up the previous side thread and preserving it if cleanup fails.
- Clean up retained side conversations when shutting down the current thread.

## Testing

- Cover shortcut remapping, conflicts, and compatibility with existing `ctrl-/` and `ctrl-7` bindings.
- Cover side-thread replacement and footer states for both the parent and side conversation.

GitOrigin-RevId: a621c21e1c45ba73197d59750b2e05efa5ee1081
This commit is contained in:
Eric Traut authored and copyberry committed 2026-07-23 21:03:40 +00:00
1 parent 62ba648136
commit 7d4b417cd1
20 files changed
+244 -27

No files matched your search

@@ -6,4 +6,4 @@ expression: terminal.backend()
" "
"› Check recently modified functions for compatibility "
" "
" gpt-5.6-sol Side from main thread · Ctrl+C to return "
" gpt-5.6-sol Side from main thread · ctrl + / to switch · ctrl + c to close "
@@ -0,0 +1,9 @@
---
source: tui/src/chatwidget/tests/side.rs
expression: terminal.backend()
---
" "
" "
"› Ask Codex to do anything "
" "
" gpt-5.6-sol ctrl + / for side "
@@ -6,4 +6,4 @@ expression: terminal.backend()
" "
"› Check recently modified functions for compatibility "
" "
" gpt-5.6-sol def… Side from main thread · main needs input · Ctrl+C to return "
" Side from main thread · main needs input · ctrl + / to switch · ctrl + c to cl"
+27 -2
View File
@@ -389,7 +389,7 @@ async fn side_context_label_preserves_status_line_snapshot() {
chat.refresh_status_line();
chat.set_side_conversation_active(/*active*/ true);
chat.set_side_conversation_context_label(Some(
"Side from main thread · Ctrl+C to return".to_string(),
"Side from main thread · ctrl + / to switch · ctrl + c to close".to_string(),
));
let width = 80;
@@ -410,7 +410,8 @@ async fn side_context_label_shows_parent_status_snapshot() {
chat.show_welcome_banner = false;
chat.set_side_conversation_active(/*active*/ true);
chat.set_side_conversation_context_label(Some(
"Side from main thread · main needs input · Ctrl+C to return".to_string(),
"Side from main thread · main needs input · ctrl + / to switch · ctrl + c to close"
.to_string(),
));
let width = 80;
@@ -421,3 +422,27 @@ async fn side_context_label_shows_parent_status_snapshot() {
.expect("draw side conversation footer");
assert_chatwidget_snapshot!("side_context_label_shows_parent_status", terminal.backend());
}
#[tokio::test]
async fn side_context_label_shows_hidden_side_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.show_welcome_banner = false;
chat.config.tui_status_line = Some(vec!["model-name".to_string()]);
chat.refresh_status_line();
chat.set_side_conversation_context_label(Some("ctrl + / for side".to_string()));
let width = 80;
let height = chat.desired_height(width);
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw hidden side conversation footer");
let hint_start = width - "ctrl + / for side".len() as u16 - 2;
assert_eq!(
terminal.backend().buffer()[(hint_start, height - 1)]
.style()
.fg,
Some(ratatui::style::Color::Magenta)
);
assert_chatwidget_snapshot!("side_context_label_shows_hidden_side", terminal.backend());
}