Skip to content

Fix double-paste: kitty router socket + daemon dedup window/normalization#1

Merged
NagyVikt merged 9 commits into
mainfrom
fix/paste-double-dedup-window-and-kitty-socket
Jun 10, 2026
Merged

Fix double-paste: kitty router socket + daemon dedup window/normalization#1
NagyVikt merged 9 commits into
mainfrom
fix/paste-double-dedup-window-and-kitty-socket

Conversation

@NagyVikt

Copy link
Copy Markdown
Collaborator

Single paste gesture landed twice. Causes: (1) kitty-paste-router.sh probed the wrong kitty socket (kitty-main vs real kitty-main-; KITTY_LISTEN_ON absent under launch --copy-env) so it never suppressed kitty's redundant fire; (2) daemon dedup window (1000ms) < measured 1.84s gap between tmux's immediate paste and the kitty router's background paste, and the two differ by a trailing newline so signatures didn't match. Fix: router globs real kitty-main- socket; dedup 1000->2500ms + paste_signature trims trailing newlines. Verified user-confirmed fixed; 15/15 unit tests pass.

NagyVikt and others added 9 commits June 9, 2026 23:54
A single Ctrl+V inside kitty+tmux fired two handlers (kitty `map ctrl+v` +
tmux `bind -n C-v`), both calling flashpaste-trigger ~hundreds of ms apart,
which the daemon's dedup window had to race. The new router suppresses
kitty's redundant handler when it can confidently detect tmux in the focused
window (kitty remote control + foreground process is tmux), so tmux's bind is
the only fire. On any uncertainty it falls through to the original path and
the daemon dedup still backstops — a detection miss degrades to today's
behaviour, never to a broken paste.

- bin/kitty-paste-router.sh: the launch target for kitty's `map ctrl+v`
- bin/kitty-focused-is-tmux.py: parses `kitten @ ls --match state:focused`,
  exits 0 only when the focused window's foreground process is tmux
- kitty.conf `map ctrl+v` now points at the router (deployed to ~/.local/bin)

Parser verified on 5 cases (tmux, non-tmux, empty, garbage, tmux-in-other-
window). Latency: the kitten round-trip only delays kitty's REDUNDANT
handler; tmux's fast bind already did the user-visible paste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon logged "PASTED text/image" = "I dispatched", which can't tell a
clean paste from a leak — this session's binary-wall and blob bugs were
invisible in the journal until the user screenshotted them.

- dispatch_text_paste now refuses to paste a payload that fails
  looks_like_text (image magic / NUL / non-UTF8) and logs outcome=
  rejected-nontext. The daemon must never dump image bytes into a pane as
  text; this is the single chokepoint all text pastes flow through, so it's
  the right last gate even though upstream staging should already reject.
- "PASTED text" now carries sent_bytes, html_sanitized, outcome=clean so a
  sanitized or suppressed paste is visible in the log.
- looks_like_text made pub(crate) so paste.rs shares the ipc.rs check.

30/30 tests pass (+2 covering the guard decision). Daemon rebuilt + restarted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every bug this session shipped because only Rust unit tests existed and
nothing exercised the actual paste-decision logic end to end. Add a headless,
mocked-clipboard regression suite that IS that net:

- tests/wl-paste-guard.test.sh: the image-coexistence guard (7 checks) —
  image present ⇒ text reads return empty; image/png + list-types still work.
- tests/kitty-focused-is-tmux.test.sh: the router's tmux detector (5 checks) —
  exit 0 only when the focused window's foreground process is tmux.
- bin/flashpaste-selftest.sh: one command runs the suite (+ `--rust` for
  cargo test). Mocks the clipboard, so it needs no display and never touches
  the user's real selection.
- flashpaste-doctor now runs the suite and a failure counts as a doctor fail.
- CI: new `behavior-tests` job in lint.yml runs the suite on every push/PR.

Suite green: 12/12 checks. Doctor integration verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The double-paste bug was config drift: kitty.conf and tmux.conf each bound
Ctrl+V independently and got out of sync. Rather than rewrite the user's
hand-tuned, richly-commented dotfiles (high risk, low marginal value now that
#2 fixed the actual handler), declare ONE canonical source and enforce it
read-only:

- config/keybindings.canonical: the source of truth — kitty ctrl+v must route
  through kitty-paste-router.sh, tmux C-v through flashpaste-trigger.
- bin/flashpaste-keybindings-check.sh: read-only checker (env-overridable for
  tests) that flags drift. Escapes ERE metachars so the literal '+' in ctrl+v
  matches correctly (caught by its own test).
- flashpaste-doctor warns (not fails) on drift — it degrades to daemon dedup.
- tests/keybindings-check.test.sh: 4 fixture cases (consistent, kitty drift,
  missing tmux, the ctrl+v literal-plus regression). In CI via the suite.

Live check passes: kitty + tmux consistent. Suite green: 16/16.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…py lints)

handle_paste was a ~290-line god-function where the probe/override races
hide. Extract its three phases verbatim into named helpers so the hot path
reads as a sequence and each phase is reviewable in isolation:

- eager_screenshot_pickup() — inotify-lag screenshot pickup
- eager_live_image_pickup() — browser "Copy Image" bridge
- resolve_paste_intent() — the staged-slot + live-clipboard override cascade,
  returns the StagedSelection to dispatch

handle_paste now reads: pickup → pickup → resolve → dispatch-match. Pure code
move, no behaviour change — proven by the unchanged 30/30 test suite, clippy
(-D warnings) and fmt both clean.

Also fixes two pre-existing lints that failed release-clippy: is_text_target
is test-only (now #[cfg(test)]); extract_blob_domain uses an array char
pattern. These predate this work but blocked the clippy gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rather than force-implement two items that shouldn't be done blind, record
the decision and the concrete path for each:

- ADR 0006 (#3): the big bash surface (tmux-paste-dispatch.sh, 657 lines) is
  the Tier-1 zero-dependency fallback BY DESIGN (ADR 0001), so folding it into
  the daemon is partly a non-goal — it would delete the no-daemon guarantee.
  Reframes #3 as bounded, test-first dedup of shim/daemon overlap, not a
  multi-day rewrite. First slice named.
- ADR 0007 (#7): owning the Wayland clipboard via data-control on
  wlroots/KDE would fix the blob leak at source (no shim needed), hooking the
  existing WAYLAND_WEDGED latch. Deferred + guarded: untestable on the
  maintainer's Mutter box, and shipping an unexercisable correctness path is a
  liability. Test matrix specified before it can be Accepted.

Both indexed in docs/adr/README.md as Proposed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two LOW findings from independent review, both on the "never drop a paste"
guarantee:

- kitty-focused-is-tmux.py: require is_focused IS True (was: not False). On
  old kitty that ignores --match and omits is_focused, no window qualifies, so
  we report "not tmux" and the router lets the paste happen — instead of
  risking a false-positive tmux match in a non-focused window that would
  suppress a paste kitty should have handled. Added a regression test.
- kitty-paste-router.sh: only call flashpaste-trigger when the pane id is
  non-empty; an empty pane (no tmux) goes straight to the image fallback
  rather than handing the daemon a blank pane.

Self-test: 17/17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… dedup

Two independent causes made a single paste land twice:

1. kitty-paste-router.sh could never reach kitty's remote-control socket.
   It looked for `unix:$XDG_RUNTIME_DIR/kitty-main`, but kitty appends the
   pid (`kitty-main-<pid>`), and KITTY_LISTEN_ON is absent from the env that
   `launch --copy-env` copies. So `kitten @ ls` failed every time, the
   focused-window-is-tmux probe always returned false, and the router never
   suppressed kitty's redundant ctrl+v fire. Now resolves the socket via
   KITTY_LISTEN_ON, else globs the real `kitty-main-<pid>` socket.

2. The daemon dedup missed the second fire. kitty launches the router as a
   BACKGROUND process, so its paste can land well after tmux's bind already
   pasted — measured 1.84s apart, past the 1000ms window. And the two paths
   differ by a trailing newline ("+N" vs "+N+1 lines"), so they hashed to
   different signatures and dodged content-dedup entirely. Widen the window
   to 2500ms and trim trailing \n/\r in paste_signature so the variants
   collide and the second is absorbed.

Verified: the daemon journal showed one keypress producing two requests
~1.8s apart with identical payload; real-wire capture confirmed flashpaste
itself delivers exactly one bracketed paste. All 15 flashpasted unit tests
pass (dedup window + trailing-newline cases added).
…om PR #1)

Avoids word-splitting on socket paths with spaces and drops the ls dependency.
Behaviour unchanged for the single-kitty-instance case.
@NagyVikt NagyVikt merged commit d81ae3c into main Jun 10, 2026
4 of 6 checks passed
@NagyVikt NagyVikt deleted the fix/paste-double-dedup-window-and-kitty-socket branch June 10, 2026 13:07
NagyVikt added a commit that referenced this pull request Jun 10, 2026
shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array
expansion and errors. Introduced with the checker in PR #1; this was the
shellcheck regression that turned main's Lint job red. Braces quiet it
with no behavior change (tests 6/6, live check consistent).
NagyVikt added a commit that referenced this pull request Jun 10, 2026
…etection (#2)

* fix: retire kitty ctrl+v interception — XWayland breaks router tmux detection

Under XWayland kitty, `kitten @ ls` reports is_focused=false for every OS
window and empty foreground_processes, so kitty-paste-router.sh's
focused_window_is_tmux() can never succeed. Every Ctrl+V took the slow
fallthrough (background launch, seconds late under load) and the late
second fire landed outside the daemon's 2.5s dedup window — journal shows
identical 162-byte text pastes 4s apart (pane %23, 15:14:50 + 15:14:54).

Fix: kitty must not bind ctrl+v at all. tmux's own `bind -n C-v` is the
single handler (~5ms, pane id from the pressing client — also kills the
wrong-pane hazard of the router's target-less `tmux display-message`).
Non-tmux kitty windows: raw 0x16 reaches Claude Code, which reads the
clipboard itself via the wl-paste shim; plain shells use ctrl+shift+v.

- keybindings.canonical: kitty ctrl+v -> UNBOUND (new keyword)
- flashpaste-keybindings-check.sh: support UNBOUND (binding must not exist)
- kitty-paste-router.sh: retired-status header + clip-pipeline-log tracing
  (it was the only paste path with zero logging)
- tests/keybindings-check.test.sh: cover UNBOUND pass/violation + keep
  positive-rule and literal-+ coverage via fixture canonicals

* fix: SC1087 in keybindings-check — brace ${key_re} before [[:space:]]

shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array
expansion and errors. Introduced with the checker in PR #1; this was the
shellcheck regression that turned main's Lint job red. Braces quiet it
with no behavior change (tests 6/6, live check consistent).
NagyVikt added a commit that referenced this pull request Jun 12, 2026
* fix: retire kitty ctrl+v interception — XWayland breaks router tmux detection

Under XWayland kitty, `kitten @ ls` reports is_focused=false for every OS
window and empty foreground_processes, so kitty-paste-router.sh's
focused_window_is_tmux() can never succeed. Every Ctrl+V took the slow
fallthrough (background launch, seconds late under load) and the late
second fire landed outside the daemon's 2.5s dedup window — journal shows
identical 162-byte text pastes 4s apart (pane %23, 15:14:50 + 15:14:54).

Fix: kitty must not bind ctrl+v at all. tmux's own `bind -n C-v` is the
single handler (~5ms, pane id from the pressing client — also kills the
wrong-pane hazard of the router's target-less `tmux display-message`).
Non-tmux kitty windows: raw 0x16 reaches Claude Code, which reads the
clipboard itself via the wl-paste shim; plain shells use ctrl+shift+v.

- keybindings.canonical: kitty ctrl+v -> UNBOUND (new keyword)
- flashpaste-keybindings-check.sh: support UNBOUND (binding must not exist)
- kitty-paste-router.sh: retired-status header + clip-pipeline-log tracing
  (it was the only paste path with zero logging)
- tests/keybindings-check.test.sh: cover UNBOUND pass/violation + keep
  positive-rule and literal-+ coverage via fixture canonicals

* fix: SC1087 in keybindings-check — brace ${key_re} before [[:space:]]

shellcheck -S warning (CI gate) parses $key_re[[:space:]] as an array
expansion and errors. Introduced with the checker in PR #1; this was the
shellcheck regression that turned main's Lint job red. Braces quiet it
with no behavior change (tests 6/6, live check consistent).

* add
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant