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) <noreply@anthropic.com>
This commit is contained in:
@@ -6451,9 +6451,6 @@ fn selection_to_text(app: &App) -> Option<String> {
|
||||
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<String> {
|
||||
(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<usize> {
|
||||
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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<usize> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user