From a4102ccad4a64b6796f3ef4ac0fb15ee1c9006e6 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sat, 2 May 2026 21:54:57 -0500 Subject: [PATCH] fix(tui): allow selection across all transcript cell types The selection-tightening from 7125172f restricted copy/select to user and assistant cell bodies only, which made it impossible to copy text from system notes, thinking blocks, or tool output. Drop the body-start gate so the rendered transcript block is selectable in full. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/tui/src/tui/ui.rs | 29 +--------------------------- crates/tui/src/tui/ui/tests.rs | 26 +++++++++++++++---------- crates/tui/src/tui/widgets/mod.rs | 32 +------------------------------ 3 files changed, 18 insertions(+), 69 deletions(-) diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 12b8ec7b..48c74dba 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6451,9 +6451,6 @@ fn selection_to_text(app: &App) -> Option { let mut selected_lines = Vec::new(); #[allow(clippy::needless_range_loop)] for line_index in start_index..=end_index { - let Some(body_start) = llm_io_selection_body_start(app, line_index) else { - continue; - }; let line_text = line_to_plain(&lines[line_index]); let line_width = text_display_width(&line_text); let (col_start, col_end) = if start_index == end_index { @@ -6466,36 +6463,12 @@ fn selection_to_text(app: &App) -> Option { (0, line_width) }; - if col_end <= body_start { - continue; - } - let slice = slice_text(&line_text, col_start.max(body_start), col_end); + let slice = slice_text(&line_text, col_start, col_end); selected_lines.push(slice); } Some(selected_lines.join("\n")) } -fn llm_io_selection_body_start(app: &App, line_index: usize) -> Option { - const MESSAGE_BODY_START_COLUMN: usize = 2; - - let (filtered_cell_index, _) = app - .viewport - .transcript_cache - .line_meta() - .get(line_index)? - .cell_line()?; - let cell_index = app - .collapsed_cell_map - .get(filtered_cell_index) - .copied() - .unwrap_or(filtered_cell_index); - - match app.cell_at_virtual_index(cell_index)? { - HistoryCell::User { .. } | HistoryCell::Assistant { .. } => Some(MESSAGE_BODY_START_COLUMN), - _ => None, - } -} - fn open_pager_for_selection(app: &mut App) -> bool { let Some(text) = selection_to_text(app) else { return false; diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index ce2ac4e3..650bcaf0 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -94,24 +94,31 @@ fn selection_to_text_handles_multiline_and_reversed_endpoints() { column: 6, }); - assert_eq!(selection_to_text(&app).as_deref(), Some("a beta\ngam")); + assert_eq!(selection_to_text(&app).as_deref(), Some("a beta\n gam")); } #[test] -fn selection_to_text_only_copies_user_and_assistant_bodies() { +fn selection_to_text_copies_rendered_transcript_block() { let mut app = create_test_app(); app.history = vec![ HistoryCell::System { - content: "skip system".to_string(), + content: "copy system".to_string(), }, HistoryCell::User { content: "copy user".to_string(), }, HistoryCell::Thinking { - content: "skip thinking".to_string(), + content: "copy thinking".to_string(), streaming: false, duration_secs: Some(1.0), }, + HistoryCell::Tool(ToolCell::Generic(GenericToolCell { + name: "exec_shell".to_string(), + status: ToolStatus::Success, + input_summary: Some("cargo check".to_string()), + output: Some("tool output line".to_string()), + prompts: None, + })), HistoryCell::Assistant { content: "copy assistant".to_string(), streaming: false, @@ -139,12 +146,11 @@ fn selection_to_text_only_copies_user_and_assistant_bodies() { }); let selected = selection_to_text(&app).expect("selection text"); - assert!(selected.contains("copy user"), "{selected:?}"); - assert!(selected.contains("copy assistant"), "{selected:?}"); - assert!(!selected.contains("skip system"), "{selected:?}"); - assert!(!selected.contains("skip thinking"), "{selected:?}"); - assert!(!selected.contains('▎'), "{selected:?}"); - assert!(!selected.contains('●'), "{selected:?}"); + assert!(selected.contains("Note copy system"), "{selected:?}"); + assert!(selected.contains("▎ copy user"), "{selected:?}"); + assert!(selected.contains("copy thinking"), "{selected:?}"); + assert!(selected.contains("tool output line"), "{selected:?}"); + assert!(selected.contains("● copy assistant"), "{selected:?}"); } #[test] diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 5a5b09f6..10905c73 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -46,7 +46,6 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; const SEND_FLASH_DURATION: Duration = Duration::from_millis(500); const COMPOSER_PANEL_HEIGHT: u16 = 2; -const MESSAGE_SELECTION_BODY_START_COLUMN: usize = 2; pub struct ChatWidget { content_area: Rect, @@ -1302,11 +1301,8 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) { if line_index < start.line_index || line_index > end.line_index { continue; } - let Some(body_start) = llm_io_selection_body_start(app, line_index) else { - continue; - }; - let (mut col_start, col_end) = if start.line_index == end.line_index { + let (col_start, col_end) = if start.line_index == end.line_index { (start.column, end.column) } else if line_index == start.line_index { (start.column, usize::MAX) @@ -1316,11 +1312,6 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) { (0, usize::MAX) }; - if col_end <= body_start { - continue; - } - col_start = col_start.max(body_start); - if col_start == 0 && col_end == usize::MAX { for span in &mut line.spans { span.style = span.style.patch(selection_style); @@ -1332,27 +1323,6 @@ fn apply_selection(lines: &mut [Line<'static>], top: usize, app: &App) { } } -fn llm_io_selection_body_start(app: &App, line_index: usize) -> Option { - let (filtered_cell_index, _) = app - .viewport - .transcript_cache - .line_meta() - .get(line_index)? - .cell_line()?; - let cell_index = app - .collapsed_cell_map - .get(filtered_cell_index) - .copied() - .unwrap_or(filtered_cell_index); - - match app.cell_at_virtual_index(cell_index)? { - HistoryCell::User { .. } | HistoryCell::Assistant { .. } => { - Some(MESSAGE_SELECTION_BODY_START_COLUMN) - } - _ => None, - } -} - fn apply_detail_target_highlight( lines: &mut [Line<'static>], top: usize,