copy details window

This commit is contained in:
Tyler Longwell
2025-11-26 11:36:11 -05:00
parent 1d7ad9e544
commit c5eda35f27
4 changed files with 196 additions and 9 deletions
Generated
+119 -7
View File
@@ -190,6 +190,26 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image 0.25.5",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
"x11rb",
]
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
@@ -2188,6 +2208,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -2713,6 +2743,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix 1.0.7",
"windows-link 0.2.1",
]
[[package]]
name = "getopts"
version = "0.2.24"
@@ -3108,6 +3148,7 @@ name = "goose-tui"
version = "1.14.0"
dependencies = [
"anyhow",
"arboard",
"axum 0.8.1",
"chrono",
"clap",
@@ -4737,13 +4778,49 @@ dependencies = [
[[package]]
name = "objc2"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2-core-graphics",
"objc2-foundation",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.9.0",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
dependencies = [
"bitflags 2.9.0",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
@@ -4752,12 +4829,24 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2-core-foundation",
]
[[package]]
@@ -8589,6 +8678,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -8624,7 +8719,7 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8643,7 +8738,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
"windows-link 0.1.3",
]
[[package]]
@@ -8734,7 +8829,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link",
"windows-link 0.1.3",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -9005,6 +9100,23 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix 1.0.7",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "xattr"
version = "1.5.0"
+1
View File
@@ -40,6 +40,7 @@ syntect = "5.2"
chrono = { version = "0.4", features = ["serde"] }
termimad = "0.34.0"
unicode-width = "0.2.0"
arboard = "3.6.1"
[dev-dependencies]
tokio = { version = "1.43", features = ["full"] }
@@ -49,6 +49,7 @@ impl Component for HelpPopup {
Line::from(" Esc Switch to Normal mode / Close popups"),
Line::from(" i Switch to Editing mode"),
Line::from(" j / k Scroll / Navigate"),
Line::from(" Ctrl+S Toggle copy mode (disable mouse capture)"),
Line::from(" Ctrl+T Toggle Todo List"),
Line::from(" Ctrl+C Interrupt / Clear / Quit"),
Line::from(""),
@@ -57,6 +58,7 @@ impl Component for HelpPopup {
Style::default().add_modifier(Modifier::UNDERLINED),
)),
Line::from(" /help Show this help"),
Line::from(" /copy Toggle copy mode"),
Line::from(" /config Open configuration"),
Line::from(" /session Open session picker"),
Line::from(" /theme Change theme (e.g. /theme light)"),
@@ -21,6 +21,8 @@ pub struct MessagePopup {
last_scroll_time: Option<Instant>,
content_height: u16,
viewport_height: u16,
cached_message_idx: Option<usize>,
cached_plain_text: String,
}
impl Default for MessagePopup {
@@ -36,6 +38,8 @@ impl MessagePopup {
last_scroll_time: None,
content_height: 0,
viewport_height: 0,
cached_message_idx: None,
cached_plain_text: String::new(),
}
}
@@ -47,6 +51,55 @@ impl MessagePopup {
}
}
fn content_as_plain_text(message: &goose::conversation::message::Message) -> String {
let mut lines = Vec::new();
for content in &message.content {
match content {
MessageContent::Text(t) => {
lines.push(t.text.clone());
}
MessageContent::ToolRequest(req) => {
lines.push("Tool Request:".to_string());
if let Ok(call) = &req.tool_call {
lines.push(format!("Name: {}", call.name));
if let Some(args) = &call.arguments {
if let Ok(json_str) = serde_json::to_string_pretty(args) {
lines.push(json_str);
}
}
}
}
MessageContent::ToolResponse(resp) => {
lines.push("Tool Output:".to_string());
if let Ok(contents) = &resp.tool_result {
for content in contents {
if let rmcp::model::Content {
raw: rmcp::model::RawContent::Text(text_content),
..
} = content
{
let text = &text_content.text;
let display_string = if let Ok(v) =
serde_json::from_str::<serde_json::Value>(text)
{
serde_json::to_string_pretty(&v)
.unwrap_or_else(|_| text.to_string())
} else {
text.to_string()
};
lines.push(display_string);
}
}
}
}
_ => {}
}
lines.push(String::new());
}
lines.join("\n")
}
fn render_content(
&self,
message: &goose::conversation::message::Message,
@@ -113,6 +166,14 @@ impl MessagePopup {
}
lines
}
fn copy_to_clipboard(&self) -> Result<(), String> {
let mut clipboard =
arboard::Clipboard::new().map_err(|e| format!("Clipboard error: {e}"))?;
clipboard
.set_text(&self.cached_plain_text)
.map_err(|e| format!("Clipboard error: {e}"))
}
}
impl Component for MessagePopup {
@@ -120,6 +181,12 @@ impl Component for MessagePopup {
match event {
Event::Input(key) => match key.code {
KeyCode::Esc | KeyCode::Char('q') => return Ok(Some(Action::ClosePopup)),
KeyCode::Char('c') => {
return match self.copy_to_clipboard() {
Ok(()) => Ok(Some(Action::ShowFlash("Copied to clipboard".to_string()))),
Err(e) => Ok(Some(Action::ShowFlash(e))),
};
}
KeyCode::Char('j') | KeyCode::Down => {
self.scroll = self.scroll.saturating_add(1).min(self.max_scroll());
self.last_scroll_time = Some(Instant::now());
@@ -157,18 +224,23 @@ impl Component for MessagePopup {
None => return,
};
if self.cached_message_idx != Some(msg_idx) {
self.cached_message_idx = Some(msg_idx);
self.cached_plain_text = Self::content_as_plain_text(message);
self.scroll = 0;
}
let area = centered_rect(80, 80, area);
f.render_widget(Clear, area);
let lines = self.render_content(message);
let block = Block::default()
.title("Message Details (Esc to Close)")
.title("Message Details (Esc to close, c to copy)")
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().bg(state.config.theme.base.background));
// Calculate height with wrapping estimation
let inner_width = area.width.saturating_sub(2).max(1);
let mut wrapped_height = 0;
for line in &lines {