Preserve transcript interaction when switching panes with arrow keys (#59472)

## Why

Directional pane switching cleared the parent conversation's transcript selection or Find state, so returning to it interrupted the interaction.

## What changed

Route left/right pane switches through the existing conversation toggle to park and restore each pane's transcript interaction. Explicitly focusing the main composer still clears transcript interaction.

## Testing

Extend the side-conversation test to cover restoring selections, Find text and caret position, and composer input after scrolling. Add a snapshot verifying that Find remains editable after switching away and back.

GitOrigin-RevId: 172f9be0df15517fbfe7f3773b63cbd1cef1cc77
This commit is contained in:
Felipe Coury
2026-11-04 18:56:44 +00:00
committed by copyberry
parent 9ff072db8a
commit 4f6acbf02d
3 changed files with 46 additions and 6 deletions
+7 -3
View File
@@ -40,12 +40,16 @@ impl App {
&& !on_child
&& (self.transcript_view.has_active_interaction()
|| self.backtrack.overlay_preview_active);
let result = if main || (left && on_child) {
let result = if main {
self.focus_main_composer(tui, app_server).await
} else if (sidebar || right)
} else if (left && on_child)
|| (right && !on_child && child.is_some_and(|id| self.side_threads[&id].pane_open))
{
// Switching threads parks and restores each pane's transcript interaction.
self.toggle_side_conversation(tui, app_server).await
} else if sidebar
&& !on_child
&& let Some(child) = child
&& (sidebar || self.side_threads[&child].pane_open)
{
let result = self.select_agent_thread(tui, app_server, child).await;
if result.is_ok() && self.current_displayed_thread_id() == Some(child) {
+34 -3
View File
@@ -3833,10 +3833,41 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
selected
);
app.transcript_view.end_select_mode();
// Ctrl-/ preserves interaction, so it exposes whether right actually cleaned up the parent.
keys!(toggle);
keys!(prefix, KeyCode::Left.into());
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
assert!(!app.transcript_view.has_active_interaction());
assert!(app.transcript_view.is_select_mode());
assert_eq!(
app.transcript_view.selected_text(&app.transcript_cells),
parent_text
);
keys!(prefix, main);
app.transcript_view.begin_search();
assert!(app.transcript_view.paste_search(parent_row));
keys!(KeyCode::Left.into());
let find = app.transcript_view.search_footer(size.width).unwrap();
keys!(prefix, KeyCode::Right.into());
keys!(prefix, KeyCode::Left.into());
assert_eq!(app.transcript_view.search_footer(size.width), Some(find));
keys!(KeyCode::Char('x').into());
let frame = side_frame(&mut app, &mut tui, size)?;
assert_app_snapshot!(
"direction_restores_parent_find",
frame.lines().find(|row| row.contains("Find:")).unwrap()
);
// Explicit composer focus ends Find. Scrolling alone keeps the composer active.
keys!(prefix, main);
read_side_history(&mut app, &mut tui, "parent")?;
keys!(prefix, KeyCode::Right.into());
keys!(prefix, KeyCode::Left.into());
assert!(side_frame(&mut app, &mut tui, size)?.contains(parent_row));
// End flushes the composer's pending paste-burst character.
keys!(KeyCode::Char('!').into(), KeyCode::End.into());
assert_eq!(
app.chat_widget.composer_text_with_pending(),
"parent draft-!"
);
keys!(toggle);
read_side_history(&mut app, &mut tui, "child")?;
@@ -0,0 +1,5 @@
---
source: tui/src/app/tests.rs
expression: "frame.lines().find(|row| row.contains(\"Find:\")).unwrap()"
---
Find: parent-2x4 │ GPT-6.1-Sol default