feat(tui2): add copy selection shortcut + UI affordance (#8462)

- Detect Ctrl+Shift+C vs VS Code Ctrl+Y and surface in footer hints
- Render clickable “⧉ copy” pill near transcript selection (hidden while
dragging)
- Handle copy hotkey + click to copy selection
- Document updated copy UX

VSCode:
<img width="1095" height="413" alt="image"
src="https://github.com/user-attachments/assets/84be0c82-4762-4c3e-80a4-c751c078bdaa"
/>

Ghosty:
<img width="505" height="68" alt="image"
src="https://github.com/user-attachments/assets/109cc1a1-f029-4f7e-a141-4c6ed2da7338"
/>
This commit is contained in:
Josh McKinney authored and GitHub committed 2025-12-22 18:54:58 -08:00
1 parent 277babba79
commit 414fbe0da9
9 files changed
+596 -14

No files matched your search

@@ -1,3 +1,5 @@
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::key_hint::has_ctrl_or_alt;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -121,6 +123,7 @@ pub(crate) struct ChatComposer {
transcript_scrolled: bool,
transcript_selection_active: bool,
transcript_scroll_position: Option<(usize, usize)>,
transcript_copy_selection_key: KeyBinding,
skills: Option<Vec<SkillMetadata>>,
dismissed_skill_popup_token: Option<String>,
}
@@ -172,6 +175,7 @@ impl ChatComposer {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
skills: None,
dismissed_skill_popup_token: None,
};
@@ -1540,6 +1544,7 @@ impl ChatComposer {
transcript_scrolled: self.transcript_scrolled,
transcript_selection_active: self.transcript_selection_active,
transcript_scroll_position: self.transcript_scroll_position,
transcript_copy_selection_key: self.transcript_copy_selection_key,
}
}
@@ -1571,10 +1576,12 @@ impl ChatComposer {
scrolled: bool,
selection_active: bool,
scroll_position: Option<(usize, usize)>,
copy_selection_key: KeyBinding,
) {
self.transcript_scrolled = scrolled;
self.transcript_selection_active = selection_active;
self.transcript_scroll_position = scroll_position;
self.transcript_copy_selection_key = copy_selection_key;
}
fn sync_popups(&mut self) {
+11 -1
View File
@@ -25,6 +25,7 @@ pub(crate) struct FooterProps {
pub(crate) transcript_scrolled: bool,
pub(crate) transcript_selection_active: bool,
pub(crate) transcript_scroll_position: Option<(usize, usize)>,
pub(crate) transcript_copy_selection_key: KeyBinding,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -115,7 +116,7 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
}
if props.transcript_selection_active {
line.push_span(" · ".dim());
line.push_span(key_hint::ctrl(KeyCode::Char('y')));
line.push_span(props.transcript_copy_selection_key);
line.push_span(" copy selection".dim());
}
vec![line]
@@ -467,6 +468,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -482,6 +484,7 @@ mod tests {
transcript_scrolled: true,
transcript_selection_active: true,
transcript_scroll_position: Some((3, 42)),
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -497,6 +500,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -512,6 +516,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -527,6 +532,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -542,6 +548,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -557,6 +564,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -572,6 +580,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
@@ -587,6 +596,7 @@ mod tests {
transcript_scrolled: false,
transcript_selection_active: false,
transcript_scroll_position: None,
transcript_copy_selection_key: key_hint::ctrl_shift(KeyCode::Char('c')),
},
);
}
+7 -2
View File
@@ -386,9 +386,14 @@ impl BottomPane {
scrolled: bool,
selection_active: bool,
scroll_position: Option<(usize, usize)>,
copy_selection_key: crate::key_hint::KeyBinding,
) {
self.composer
.set_transcript_ui_state(scrolled, selection_active, scroll_position);
self.composer.set_transcript_ui_state(
scrolled,
selection_active,
scroll_position,
copy_selection_key,
);
self.request_redraw();
}