From 041c5ca0308909ea3733088cd8518a854ca1650d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 29 Sep 2026 13:54:45 -0400 Subject: [PATCH] fix(tui): a bracketed paste that is not the clipboard never attaches its image The origin check ran only on Otty. An IME commit or a diverged tmux buffer on any other terminal attached whatever image sat on the clipboard. --- .../kigi-pager-render/src/terminal/mod.rs | 5 --- .../kigi-pager-render/src/terminal/test.rs | 18 ---------- crates/codegen/kigi-tui/src/app/actions.rs | 2 +- .../kigi-tui/src/app/effects/helpers.rs | 21 ++++++++++++ .../codegen/kigi-tui/src/app/effects/mod.rs | 17 +++------- .../codegen/kigi-tui/src/app/effects/tests.rs | 34 +++++++++++++++++++ 6 files changed, 61 insertions(+), 36 deletions(-) diff --git a/crates/codegen/kigi-pager-render/src/terminal/mod.rs b/crates/codegen/kigi-pager-render/src/terminal/mod.rs index b6b7e9d..92436a2 100644 --- a/crates/codegen/kigi-pager-render/src/terminal/mod.rs +++ b/crates/codegen/kigi-pager-render/src/terminal/mod.rs @@ -189,11 +189,6 @@ impl TerminalName { | Self::Zed ) } - - /// Only Otty is known to wrap macOS IME commits in bracketed paste. - pub fn delivers_ime_as_bracketed_paste(self) -> bool { - matches!(self, Self::Otty) - } } impl TerminalContext { diff --git a/crates/codegen/kigi-pager-render/src/terminal/test.rs b/crates/codegen/kigi-pager-render/src/terminal/test.rs index 10b86eb..ff0b733 100644 --- a/crates/codegen/kigi-pager-render/src/terminal/test.rs +++ b/crates/codegen/kigi-pager-render/src/terminal/test.rs @@ -51,24 +51,6 @@ fn brand_otty_from_term_program() { assert_eq!(detect_terminal_brand_from_env(&env), TerminalName::Otty); } -#[test] -fn otty_delivers_ime_as_bracketed_paste_only() { - assert!(TerminalName::Otty.delivers_ime_as_bracketed_paste()); - for brand in [ - TerminalName::AppleTerminal, - TerminalName::Ghostty, - TerminalName::Iterm2, - TerminalName::Unknown, - TerminalName::WezTerm, - TerminalName::Kitty, - ] { - assert!( - !brand.delivers_ime_as_bracketed_paste(), - "{brand:?} must not gate IME bracketed-paste origin" - ); - } -} - #[test] fn otty_is_capability_unclassified_like_unknown() { assert!(TerminalName::Otty.is_capability_unclassified()); diff --git a/crates/codegen/kigi-tui/src/app/actions.rs b/crates/codegen/kigi-tui/src/app/actions.rs index 97b3026..eef1af2 100644 --- a/crates/codegen/kigi-tui/src/app/actions.rs +++ b/crates/codegen/kigi-tui/src/app/actions.rs @@ -1258,7 +1258,7 @@ pub enum ProbedAttachment { PersistFailed(String), /// The pasteboard probe completed normally without raster data. NoRaster, - /// The attachment result was intentionally discarded because its baseline went stale. + /// The attachment result was discarded: stale baseline, or the paste was not the clipboard. ProbeDropped, /// The attachment probe task failed or timed out. ProbeFailed, diff --git a/crates/codegen/kigi-tui/src/app/effects/helpers.rs b/crates/codegen/kigi-tui/src/app/effects/helpers.rs index 338b5b4..ff0982e 100644 --- a/crates/codegen/kigi-tui/src/app/effects/helpers.rs +++ b/crates/codegen/kigi-tui/src/app/effects/helpers.rs @@ -1103,6 +1103,27 @@ pub(super) fn parse_usage_response( .map(|usage| usage.rows) .map_err(|e| format!("Parse error: {e}")) } +/// `Some` ends a clipboard probe before any attachment read. +pub(super) fn bracketed_origin_verdict( + probe_bracketed: bool, + payload: Option<&str>, +) -> Option { + use crate::app::actions::ProbedAttachment; + if !probe_bracketed { + return None; + } + match crate::clipboard::bracketed_payload_came_from_clipboard_result(payload.unwrap_or("")) { + Ok(true) => None, + Ok(false) => { + tracing::debug!("bracketed paste differs from the clipboard; no attachment probe"); + Some(ProbedAttachment::ProbeDropped) + } + Err(_) => { + tracing::debug!("clipboard unreadable for a bracketed paste; no attachment probe"); + Some(ProbedAttachment::ProbeDropped) + } + } +} /// A blocking flock on the shared, possibly-network `~/.kigi` lock must never /// stall the event-loop thread (and would hang exit on `/quit`); the registry /// is best-effort, so skip on contention. diff --git a/crates/codegen/kigi-tui/src/app/effects/mod.rs b/crates/codegen/kigi-tui/src/app/effects/mod.rs index f805649..c580aa6 100644 --- a/crates/codegen/kigi-tui/src/app/effects/mod.rs +++ b/crates/codegen/kigi-tui/src/app/effects/mod.rs @@ -1637,18 +1637,11 @@ pub(crate) fn execute( { return (ProbedAttachment::ProbeDropped, None); } - if probe_bracketed - && crate::terminal::terminal_context() - .brand - .delivers_ime_as_bracketed_paste() - { - match crate::clipboard::bracketed_payload_came_from_clipboard_result( - probe_text.as_deref().unwrap_or(""), - ) { - Ok(true) => {} - Ok(false) => return (ProbedAttachment::ProbeDropped, None), - Err(_) => return (ProbedAttachment::ProbeFailed, None), - } + if let Some(verdict) = helpers::bracketed_origin_verdict( + probe_bracketed, + probe_text.as_deref(), + ) { + return (verdict, None); } let (image_data, file_urls) = match crate::clipboard::system_clipboard_probe_attachments( probe_text.as_deref(), diff --git a/crates/codegen/kigi-tui/src/app/effects/tests.rs b/crates/codegen/kigi-tui/src/app/effects/tests.rs index 6a8fc62..26363fb 100644 --- a/crates/codegen/kigi-tui/src/app/effects/tests.rs +++ b/crates/codegen/kigi-tui/src/app/effects/tests.rs @@ -1664,3 +1664,37 @@ fn session_picker_entry_maps_to_dormant_roster_row() { assert_eq!(roster.origin.kind, "local"); assert_eq!(roster.origin.host.as_deref(), Some("box")); } + +fn origin_verdict_with_clipboard( + clipboard: Option<&str>, + read_failed: bool, + probe_bracketed: bool, + payload: &str, +) -> Option { + crate::clipboard::set_clipboard_probe_hook(crate::clipboard::ClipboardProbeHook { + text: clipboard.map(str::to_owned), + text_read_failed: read_failed, + ..Default::default() + }); + let verdict = bracketed_origin_verdict(probe_bracketed, Some(payload)); + crate::clipboard::clear_clipboard_probe_hook(); + verdict +} +#[test] +fn bracketed_paste_that_is_not_the_clipboard_drops_the_probe() { + let verdict = origin_verdict_with_clipboard(Some("copied text"), false, true, "typed by ime"); + assert!(matches!(verdict, Some(crate::app::actions::ProbedAttachment::ProbeDropped))); +} +#[test] +fn bracketed_paste_matching_the_clipboard_keeps_probing() { + assert!(origin_verdict_with_clipboard(Some("copied text"), false, true, "copied text").is_none()); +} +#[test] +fn unreadable_clipboard_drops_a_bracketed_probe_without_an_error() { + let verdict = origin_verdict_with_clipboard(None, true, true, "anything"); + assert!(matches!(verdict, Some(crate::app::actions::ProbedAttachment::ProbeDropped))); +} +#[test] +fn clipboard_key_probe_skips_the_origin_check() { + assert!(origin_verdict_with_clipboard(Some("copied text"), false, false, "other").is_none()); +}