From 832d0857c0b2ae7e1c8c510724c54f644f770599 Mon Sep 17 00:00:00 2001 From: Ali Gasimzade Date: Wed, 27 May 2026 13:19:49 +0400 Subject: [PATCH 1/2] fix: forward Cmd+Delete and Option+Delete to PTY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cmd+Backspace now sends ^U (kill-line-to-beginning) and Option+Backspace sends ESC+DEL (backward-kill-word), matching macOS text-field conventions and what bash/zsh/Claude Code's input field interpret. Previously these key combos fell through to SwiftTerm, which forwarded a plain DEL byte for both — so neither shortcut did anything useful. Option-modified keys skip performKeyEquivalent entirely, so the existing keyDown monitor (already used for paste) is the only reliable place to intercept them. Co-Authored-By: Claude Opus 4.7 (1M context) --- Sources/Terminal/TerminalSurface.swift | 47 ++++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/Sources/Terminal/TerminalSurface.swift b/Sources/Terminal/TerminalSurface.swift index 554e959..5a41b59 100644 --- a/Sources/Terminal/TerminalSurface.swift +++ b/Sources/Terminal/TerminalSurface.swift @@ -54,7 +54,7 @@ private class DeckardTerminalView: LocalProcessTerminalView { } } } - private var pasteShortcutMonitor: Any? + private var inputShortcutMonitor: Any? private var syncOutputFilterPendingBytes: [UInt8] = [] func configureImagePasteShortcut(sessionType: String?) { @@ -64,18 +64,18 @@ private class DeckardTerminalView: LocalProcessTerminalView { override init(frame: CGRect) { super.init(frame: frame) registerForDraggedTypes([.fileURL]) - installPasteShortcutMonitor() + installInputShortcutMonitor() } required init?(coder: NSCoder) { super.init(coder: coder) registerForDraggedTypes([.fileURL]) - installPasteShortcutMonitor() + installInputShortcutMonitor() } deinit { - if let pasteShortcutMonitor { - NSEvent.removeMonitor(pasteShortcutMonitor) + if let inputShortcutMonitor { + NSEvent.removeMonitor(inputShortcutMonitor) } } @@ -129,21 +129,28 @@ private class DeckardTerminalView: LocalProcessTerminalView { feed(byteArray: filtered[...]) } - private func installPasteShortcutMonitor() { - pasteShortcutMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in - guard let self, - self.handlesPasteShortcuts, - Self.isPasteShortcut(event), - self.shouldHandlePasteShortcut(event) else { + private func installInputShortcutMonitor() { + inputShortcutMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in + guard let self, self.shouldHandleInputShortcut(event) else { return event } - - self.paste(event) - return nil + if Self.isPasteShortcut(event), self.handlesPasteShortcuts { + self.paste(event) + return nil + } + if Self.isKillLineShortcut(event) { + self.send([0x15]) + return nil + } + if Self.isDeleteWordShortcut(event) { + self.send([0x1B, 0x7F]) + return nil + } + return event } } - private func shouldHandlePasteShortcut(_ event: NSEvent) -> Bool { + private func shouldHandleInputShortcut(_ event: NSEvent) -> Bool { guard event.window === window else { return false } if hasFocus { return true } guard let firstResponder = window?.firstResponder else { return false } @@ -167,6 +174,16 @@ private class DeckardTerminalView: LocalProcessTerminalView { return flags == .command && event.charactersIgnoringModifiers?.lowercased() == "v" } + private static func isKillLineShortcut(_ event: NSEvent) -> Bool { + let flags = event.modifierFlags.intersection([.command, .shift, .option, .control]) + return flags == .command && event.charactersIgnoringModifiers == "\u{7F}" + } + + private static func isDeleteWordShortcut(_ event: NSEvent) -> Bool { + let flags = event.modifierFlags.intersection([.command, .shift, .option, .control]) + return flags == .option && event.charactersIgnoringModifiers == "\u{7F}" + } + private static func pasteboardContainsImage(_ pasteboard: NSPasteboard) -> Bool { if pasteboard.availableType(from: imagePasteboardTypes) != nil { return true From d019b297d7a46760e6c2b3907bbf1d8d1260d92e Mon Sep 17 00:00:00 2001 From: Gilles Dubuc Date: Fri, 29 May 2026 10:22:58 +0200 Subject: [PATCH 2/2] fix: gate kill-line and delete-word by startup suppression flag Cmd+Delete and Option+Delete wrote directly to the PTY without checking handlesPasteShortcuts, so they could escape the 0.3s startup keystroke suppression window (which exists so stray keys don't corrupt a new Claude tab's pending initial command). Hoist the handlesPasteShortcuts gate to cover all input shortcuts, matching the existing paste behavior. Co-Authored-By: Claude Opus 4.7 --- Sources/Terminal/TerminalSurface.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Terminal/TerminalSurface.swift b/Sources/Terminal/TerminalSurface.swift index 5a41b59..14ec9f4 100644 --- a/Sources/Terminal/TerminalSurface.swift +++ b/Sources/Terminal/TerminalSurface.swift @@ -131,10 +131,10 @@ private class DeckardTerminalView: LocalProcessTerminalView { private func installInputShortcutMonitor() { inputShortcutMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in - guard let self, self.shouldHandleInputShortcut(event) else { + guard let self, self.handlesPasteShortcuts, self.shouldHandleInputShortcut(event) else { return event } - if Self.isPasteShortcut(event), self.handlesPasteShortcuts { + if Self.isPasteShortcut(event) { self.paste(event) return nil }