Skip to content

Fix three terminal clipboard copy failures (Cmd+C swallowed on agent TUI panes, empty writes clearing the clipboard, VT-export capture race) - #8064

Closed
a05031113 wants to merge 6 commits into
manaflow-ai:mainfrom
a05031113:fix/clipboard-copy-failures
Closed

a05031113 wants to merge 6 commits into
manaflow-ai:mainfrom
a05031113:fix/clipboard-copy-failures

Conversation

@a05031113

@a05031113 a05031113 commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Copying terminal text failed intermittently — and on agent TUI panes (Claude Code) almost always. Dogfooding with DEBUG probes on a live Claude Code pane isolated three independent bugs that stack:

1. Cmd+C swallowed by stale selection validation (main user-facing failure)

On a pane whose TUI redraws continuously, ghostty_surface_has_selection reports false while the runtime still holds a live selection — the highlight stays visible and copy-on-select still produces the full text. validateUserInterfaceItem gated Copy on that flag, so the Edit ▸ Copy item goes disabled, and a disabled menu item swallows ⌘C at key-equivalent dispatch — the event never reaches the surface, so the runtime's own copy_to_clipboard binding can't run either.

Repro fingerprint: select text in a pane running Claude Code → Copy is greyed in the Edit menu and ⌘C is a silent no-op — but holding the Edit menu open and pressing ⌘C copies fine (menu tracking lets the key fall through to the surface).

Fix: enable Copy whenever a surface exists. Copying with no selection remains a harmless no-op.

2. Empty clipboard writes destroy the clipboard

TerminalPasteboardService.writeString ran clearContents() before writing even when the payload was empty (e.g. copy-on-select firing after the selection was already invalidated by a redraw). The user's previous clipboard content is silently destroyed — paste turns up empty and clipboard managers record nothing. Empty standard writes are now ignored (and logged).

3. VT-export clipboard capture swallows concurrent user copies

The one-shot standardClipboardWriteCapture armed by readTerminalTextFromVTExportForSnapshot (session snapshots, mobile replay, agent naming — all frequent on agent panes) diverted the next standard-clipboard write from any source. A user copy racing an in-flight VT export was captured instead of reaching NSPasteboard. The capture now carries a predicate and only consumes writes that match the export payload shape (a single absolute path to an existing file); non-matching writes pass through with the capture left armed. Also checks the previously ignored setString result (retry once + log), and injects the standard pasteboard so tests cover the write path without touching the real general pasteboard.

Tests

  • CmuxTerminalTests: non-matching writes pass through an armed capture and leave it armed; empty writes don't clear existing content; standard writes land on the injected pasteboard after the capture disarms.
  • cmuxTests/SessionPersistenceTests: isPlausibleExportedScreenPath accepts an existing file path / file URL and rejects user text, missing paths, and directories.

Note on the two-commit red/green policy: the regression tests exercise seams introduced by the fixes themselves (matching: predicate, injected standard pasteboard), so a tests-only first commit would not compile. Happy to restructure where feasible if preferred.

Localization audit

No user-facing strings added or changed (code comments and DEBUG-only cmuxDebugLog probes only), so no Localizable.xcstrings / web message catalog changes are needed.

Verification

Built with ./scripts/reload.sh --tag fix-clipboard and dogfooded on a live Claude Code pane on macOS 26.5: select → ⌘C → paste now works with the Edit menu closed; copy-on-select content survives subsequent selection-clear events; package syntax verified and behavior confirmed by the reporting user across multiple rounds.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes three clipboard copy failures in terminal panes, especially fast‑redrawing agent TUIs. Cmd+C now works reliably, VT‑export no longer swallows user copies, empty writes don’t wipe the clipboard, and overlapping captures are rejected; fixes now live in writeRepresentations and cover all text formats.

  • Bug Fixes
    • Always enable Copy when a terminal surface exists; copying with no selection is a no‑op.
    • Ignore empty standard-clipboard writes; on write failure after clear, retry once if changeCount is unchanged and log.
    • VT‑export capture only diverts writes that match a single absolute path or file URL under the process temp directory (accepts both /var and /private/var); non‑matching writes pass through and leave the capture armed; capture claim is atomic.
    • Reject overlapping clipboard captures; arming while one is in flight returns nil.
    • Apply capture and empty‑write guards in writeRepresentations (match on preferred text/plain); inject the standard pasteboard in tests.

Written for commit 3b24426. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Clipboard export capture is now more conservative and won’t intercept unrelated standard clipboard writes.
    • Empty clipboard writes no longer clear existing clipboard contents.
    • Empty/invalid exported-screen payloads are rejected unless they look plausible and are under the app’s temp directory.
    • Copy menu/shortcut is enabled whenever a terminal surface is available.
  • Enhancements
    • Clipboard capture supports predicate-based matching, only diverting the first write that satisfies the predicate.
    • Improved clipboard write diagnostics for troubleshooting.
  • Tests
    • Added coverage for exported-screen path plausibility rules.

a05031113 and others added 2 commits July 14, 2026 21:31
The one-shot standardClipboardWriteCapture armed by
readTerminalTextFromVTExportForSnapshot (session snapshots, mobile
replay, agent naming) diverted the NEXT standard-clipboard write from
any source. A user copy (Cmd+C, copy-on-select, or copy mode) racing an
in-flight VT export was captured instead of reaching NSPasteboard,
silently leaving the clipboard empty.

The capture now carries a predicate and only consumes writes that match
it; non-matching writes pass through to the real pasteboard with the
capture left armed. The VT-export caller matches only a single absolute
path (or file URL) to an existing file — the shape write_screen_file /
write_active_file produce — so user copies can no longer be swallowed.

Also: check the previously ignored setString result (retry once and log
on failure, instead of leaving the clipboard cleared with nothing
written), log pass-through writes for diagnosis, and inject the
standard pasteboard so tests cover the write path without touching the
real general pasteboard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E
…ard writes

Two more clipboard failures found while dogfooding on a Claude Code
(agent TUI) pane, where output redraws continuously:

1. validateUserInterfaceItem gated Copy on
   ghostty_surface_has_selection, which can report false while the
   runtime still holds a live selection under constant TUI redraw (the
   selection highlight stays visible and copy-on-select still produces
   the full text). The disabled menu item then swallows Cmd+C at
   key-equivalent dispatch, so the runtime's own copy binding never
   runs. Repro fingerprint: Cmd+C only worked while the Edit menu was
   held open (menu tracking lets the key fall through to the surface).
   Copy is now enabled whenever a surface exists; copying with no
   selection remains a harmless no-op.

2. writeString cleared the pasteboard before writing even for an empty
   payload (e.g. copy-on-select firing after the selection was already
   invalidated), silently destroying whatever the user last copied —
   paste turns up empty and clipboard managers record nothing. Empty
   standard writes are now ignored and logged.

Also adds DEBUG-level cmuxDebugLog probes on the ghostty
write-clipboard callback, mirroring the existing read-side logging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

@a05031113 is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Clipboard capture now filters writes by payload, preserves unrelated clipboard writes, supports injected pasteboards, and validates VT export paths before capture. Ghostty clipboard diagnostics and copy command availability were also updated.

Changes

Clipboard capture and export handling

Layer / File(s) Summary
Predicate-aware capture service
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Clipboard/TerminalClipboardWriting.swift, Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Services/Pasteboard/TerminalPasteboardService.swift, Packages/macOS/CmuxTerminal/Tests/.../TerminalPasteboardServiceTests.swift
Capture APIs accept matching predicates; unmatched standard writes pass through, empty writes are ignored, pasteboards can be injected, writes retry when needed, and related behavior is tested.
VT export path validation
Sources/TerminalController.swift, cmuxTests/SessionPersistenceTests.swift
VT export capture accepts normalized paths under the temporary directory, including file URL and /var path variants, with regression coverage.
Ghostty clipboard and copy handling
Sources/GhosttyTerminalView.swift
Clipboard callbacks add debug logging, and copy validation enables the command whenever a terminal surface exists.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TerminalController
  participant TerminalPasteboardService
  participant NSPasteboard
  TerminalController->>TerminalPasteboardService: arm capture with exported-path predicate
  TerminalPasteboardService->>NSPasteboard: pass through unrelated clipboard write
  TerminalPasteboardService->>TerminalController: return matching exported file path
  TerminalPasteboardService->>NSPasteboard: write later clipboard payload
Loading

Suggested reviewers: azooz2003-bit

🚥 Pre-merge checks | ✅ 24 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (24 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No new Swift 6 isolation debt: the new shared state is lock-guarded/nonisolated, the path helper is nonisolated, and the UI change stays on AppKit types.
Cmux Swift Blocking Runtime ✅ Passed No new blocking waits/sleeps/syncs were introduced in production code; the only added sync is a small NSLock-guarded capture with a documented C-callback constraint.
Cmux Browser Automation Off-Main ✅ Passed PR only touches clipboard/capture paths; no browser.* router, .mainActor, socketWorkerMethods, or policy-test changes, so the off-main rule is unaffected.
Cmux Expensive Synchronous Load ✅ Passed Diff only changes clipboard/pasteboard logic and menu validation; no agent-history load, transcript/JSONL scan, or other expensive sync load was added.
Cmux Cache Substitution Correctness ✅ Passed No fresh authoritative read was swapped for a cache; the VT-export snapshot path still reads the exported file from disk and falls back on nil capture.
Cmux No Hacky Sleeps ✅ Passed The PR diff vs origin/main touches only Swift sources/tests, so the non-Swift runtime-sleeps rule does not apply.
Cmux Algorithmic Complexity ✅ Passed The new work is constant-time or bounded to tiny fixed-size arrays; no nested scans or hot-path rescans were introduced.
Cmux Swift Concurrency ✅ Passed The patch only adds synchronous, lock-guarded clipboard logic and callback-bound tests; it adds no new DispatchQueue, completion-handler, Combine, or fire-and-forget Task patterns.
Cmux Swift @Concurrent ✅ Passed No touched Swift file adds nonisolated async work or invalid @concurrent usage; the new VT-export predicate/capture paths are synchronous and UI-bound.
Cmux Swift File And Package Boundaries ✅ Passed PASS: changes are small, isolated glue in already oversized app files; core clipboard logic lives in package targets, with no new mixed-responsibility or oversized file.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, .gitignore, or workflow files changed; only source/test files were touched, so the SwiftPM lockfile rule isn’t triggered.
Cmux Swift Logging ✅ Passed New logs use Logger in a class, not file-scoped, and the added cmuxDebugLog probes are DEBUG-gated; no print/NSLog or sensitive data added.
Cmux User-Facing Error Privacy ✅ Passed Changed production code only adds internal logging/predicate plumbing; no new user-visible error text exposes vendor/internal names or secrets.
Cmux Full Internationalization ✅ Passed Only debug logs, protocol/config tokens, and test code were added; no new user-facing copy or locale assets were introduced.
Cmux Swiftui State Layout ✅ Passed PASS: the diff only touches pasteboard service/protocol/tests; no SwiftUI views, @Observable/@published, GeometryReader, lazy-row store refs, or render-time state writes appear.
Cmux Architecture Rethink ✅ Passed PASS: The PR keeps clipboard ownership in one service, adds pure validation and test seams, and introduces no sleeps/polling/observers or split lifecycle owners.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only changed clipboard services, tests, and clipboard-writing API; no NSWindow/WindowGroup/identifier changes were introduced.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/test files under Sources/ and Tests/, with no artifact or scratch directories in the diff.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam was introduced; the only DEBUG helper in the touched source pre-existed, and the new APIs are production-facing.
Cmux No Ambient Global State ✅ Passed PASS: changes stay on TerminalPasteboardService/protocol methods with injected standardPasteboard; no new file-scope funcs, mutable globals, static-helper namespaces, or singletons were added.
Title check ✅ Passed The title clearly summarizes the primary fix: three terminal clipboard copy failures.
Description check ✅ Passed The description is detailed and covers summary, testing, and verification, though it omits the template's demo video, review trigger, and checklist sections.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@a05031113
a05031113 marked this pull request as ready for review July 14, 2026 17:05
@greptile-apps

greptile-apps Bot commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes several terminal clipboard failures. The main changes are:

  • Keeps Copy enabled whenever a terminal surface exists.
  • Ignores empty clipboard writes instead of clearing existing content.
  • Filters VT-export captures by temporary-file path shape.
  • Makes one-shot capture claims atomic and rejects overlapping captures.
  • Adds guarded pasteboard-write retries and injectable test coverage.

Confidence Score: 5/5

This looks safe to merge.

  • The latest changes address the reported capture ownership, path filtering, callback blocking, and retry behavior.
  • No additional distinct blocking issue was found in the updated code.

Important Files Changed

Filename Overview
Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Services/Pasteboard/TerminalPasteboardService.swift Adds filtered and atomic clipboard capture, overlap rejection, empty-write protection, logging, pasteboard injection, and guarded retries.
Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Clipboard/TerminalClipboardWriting.swift Extends the clipboard capture contract with a payload predicate and preserves the accepting-all convenience overload.
Sources/GhosttyTerminalView.swift Keeps Copy available for active terminal surfaces and adds debug-only clipboard diagnostics.
Sources/TerminalController.swift Restricts VT-export capture to path-shaped values under the process temporary directory.

Reviews (3): Last reviewed commit: "Reject overlapping clipboard captures in..." | Re-trigger Greptile

Comment on lines +131 to +138
if armed.accepts(string) {
standardClipboardWriteCaptureLock.lock()
if standardClipboardWriteCapture === armed {
standardClipboardWriteCapture = nil
}
standardClipboardWriteCaptureLock.unlock()
armed.capture(string)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 One-Shot Capture Consumed Twice

Two matching clipboard callbacks can both read the same armed capture and pass accepts before either clears the slot. The callback that loses the identity check still calls armed.capture and returns, so it overwrites the first result and swallows its own write; concurrent VT exports can therefore return the wrong path while neither write reaches the pasteboard.

Comment on lines +159 to +162
// leaving the clipboard empty. Re-declare and retry once so the
// failure is at least not silent data loss.
pasteboard.clearContents()
let retried = pasteboard.setString(string, forType: .string)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Retry Clears Concurrent Successful Write

If another clipboard callback writes successfully after this callback's first setString fails, the retry calls clearContents again and deletes that newer value. Concurrent terminal copies can therefore leave the clipboard empty or replace the most recent copy with the older retry.

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +700 to +702
var isDirectory = ObjCBool(false)
return fileManager.fileExists(atPath: path, isDirectory: &isDirectory)
&& !isDirectory.boolValue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Existing User Path Matches Export

The predicate accepts any existing absolute file, not only the file created by the active VT export. If a user copies /etc/hosts or another existing path while capture is armed, that copy is swallowed and the snapshot reads the unrelated file as terminal output instead of waiting for the export's write.

Comment thread Sources/TerminalController.swift Outdated
Comment on lines +700 to +702
var isDirectory = ObjCBool(false)
return fileManager.fileExists(atPath: path, isDirectory: &isDirectory)
&& !isDirectory.boolValue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Filesystem Check Blocks Runtime Callback

This predicate runs synchronously inside the Ghostty clipboard callback. An absolute path on a slow network or FUSE volume makes fileExists block terminal runtime processing while capture is armed, causing input or rendering stalls; the callback should perform only a cheap export-owned shape check and defer filesystem validation.

Rule Used: Flag production Swift that reads, decodes, or scan... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Services/Pasteboard/TerminalPasteboardService.swift`:
- Around line 130-139: Update the matching branch in TerminalPasteboardService’s
standard clipboard write handling so armed.capture(string) and the early return
occur only after this thread successfully clears standardClipboardWriteCapture
under the lock. If the identity check fails because another thread or the
disarming defer won the race, unlock and continue to the normal pasteboard write
path without swallowing the value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eaa32b1e-6883-4441-81c1-ecf135644e5a

📥 Commits

Reviewing files that changed from the base of the PR and between 27810b9 and cae1ca1.

📒 Files selected for processing (6)
  • Packages/macOS/CmuxTerminal/Sources/CmuxTerminal/Services/Pasteboard/TerminalPasteboardService.swift
  • Packages/macOS/CmuxTerminal/Tests/CmuxTerminalTests/Services/TerminalPasteboardServiceTests.swift
  • Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Clipboard/TerminalClipboardWriting.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/TerminalController.swift
  • cmuxTests/SessionPersistenceTests.swift

…-string export predicate

- writeString: only the write that atomically clears the one-shot slot
  captures; a concurrent matching write that loses the claim race falls
  through to the pasteboard instead of being swallowed (Greptile P1,
  CodeRabbit critical).
- Retry after a failed setString only runs while changeCount still
  matches our clearContents, so it can never clobber a newer concurrent
  write (Greptile P1).
- isPlausibleExportedScreenPath: replaced the fileExists check with a
  pure string prefix check against the process temporary directory
  (accepting both /var and /private/var spellings) — no filesystem I/O
  inside the ghostty runtime callback, and existing absolute paths a
  user copies (e.g. /etc/hosts) no longer match (Greptile P1 + P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E
@a05031113

Copy link
Copy Markdown
Contributor Author

Addressed the automated review findings in 8eb7394:

  • One-shot capture consumed twice / TOCTOU (Greptile P1, CodeRabbit critical): the capture is now claimed atomically — only the write that clears the slot captures and returns; a concurrent matching write (or one racing the disarm in captureNextStandardClipboardWrite's defer) falls through to the real pasteboard instead of being swallowed.
  • Retry clobbering a newer concurrent write (Greptile P1): the retry only runs while changeCount still equals the value returned by our clearContents, so it can never destroy a value another writer landed in between.
  • Any existing absolute path matching the export predicate (Greptile P1) and filesystem I/O in the runtime callback (Greptile P2): isPlausibleExportedScreenPath is now a pure string prefix check against the process temporary directory (accepting both the /var/... and /private/var/... spellings), with interior newlines rejected. No fileExists on the hot path, and copied paths like /etc/hosts no longer match. Tests updated to the new contract.

🤖 Generated with Claude Code

) -> String? {
let capture = ClipboardWriteCapture(accepts: predicate)
standardClipboardWriteCaptureLock.lock()
standardClipboardWriteCapture = capture

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Overlapping captures replace ownership — Each call unconditionally replaces the process-wide capture slot. If two panes start VT exports concurrently, the second capture replaces the first. Both predicates accept temporary export paths, so the first export callback can satisfy the second capture. The first caller then receives nil, while the second can read the first pane's export. The atomic claim in writeString prevents two callbacks from consuming one capture, but it does not prevent separate capture operations from stealing each other's writes. Captures need export-specific ownership, or overlapping operations must be serialized or rejected.

…rites

Arming a capture while another is in flight replaced the process-wide
slot: with predicate-scoped captures, the first operation's export write
could satisfy the second capture (both predicates accept export paths),
handing pane A's export to pane B's snapshot while pane A got nil.

captureNextStandardClipboardWrite now returns nil without arming when
the slot is already held. Every caller already treats nil as "fall back
to a non-capture read", so an overlapping export degrades gracefully
instead of cross-delivering content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E
@a05031113

Copy link
Copy Markdown
Contributor Author

Addressed the follow-up finding in 8f81791:

  • Overlapping captures replace ownership (Greptile P1): captureNextStandardClipboardWrite now rejects a new capture (returns nil without arming) while another is in flight, instead of replacing the slot — so concurrent VT exports can no longer cross-deliver one pane's export to another pane's snapshot. Every caller already treats nil as "fall back to a non-capture read" (readTerminalTextForSnapshot falls back to the plain base64 read; mobile replay falls back to byte-tee state), so overlap degrades gracefully. Regression test added.

The earlier inline comments on TerminalController.swift:702 (existing-path match, fileExists in the runtime callback) were already addressed in 8eb7394 — the predicate is now a pure string prefix check against the process temporary directory.

🤖 Generated with Claude Code

@bogdanovich

Copy link
Copy Markdown

Bumped into the same issue. Can this get merged?

@a05031113

Copy link
Copy Markdown
Contributor Author

@bogdanovich Thanks for confirming — sorry you hit it too. Quick workaround until this lands: after selecting, hold the Edit menu open and press ⌘C (a disabled Copy menu item otherwise swallows the key equivalent before the terminal's own copy binding can run), or enable copy-on-select in settings.

Branch is refreshed against latest main (f440d86, no conflicts; the pasteboard files had no upstream changes) and re-verified locally: GhosttyKit + app build green, clipboard capture tests pass. Ready for maintainer review / CI approval.

🤖 Generated with Claude Code

Upstream replaced the single-string clipboard write path with
writeRepresentations (all textual MIME variants on one pasteboard
item). The predicate-gated capture claim, empty-payload guard, and
changeCount-guarded retry now live in writeRepresentations — matched
against the preferred plain-text representation — with writeString
forwarding as upstream intends. The retry rebuilds the pasteboard item,
since an item cannot be attached twice. Both test suites are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwGyYRt9QjDUhimcp2xB7E
teamleaderleo added a commit that referenced this pull request Sep 25, 2026
ghostty_surface_has_selection can report false while the runtime still
holds a live selection, for example under constant TUI redraw in agent
panes. Gating the Copy menu item on it greyed Copy out and swallowed
Cmd+C. Enable Copy whenever a surface exists; copying with no selection
is a no-op.

Salvaged from #8064.

Co-authored-by: a05031113 <a05031113@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thank you for this, @a05031113! Your Cmd+C fix (keeping Copy enabled so it works in agent TUI panes when Ghostty misses the selection) just landed in #14557 with you as co-author. The other two pieces here were covered by the #8838 pasteboard rewrite, so I'll close this one. Really appreciate it :D

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.

3 participants