Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/cmdv-image-paste-macos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"kilo-code": patch
---

Support Cmd+V for pasting images on macOS in VSCode terminal

- Detect empty bracketed paste (when clipboard contains image instead of text)
- Trigger clipboard image check on empty paste or paste timeout
- Add Cmd+V (meta key) support alongside Ctrl+V for image paste
38 changes: 29 additions & 9 deletions cli/src/state/atoms/keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,10 @@ function handleTextInputKeys(get: Getter, set: Setter, key: Key) {
}

function handleGlobalHotkeys(get: Getter, set: Setter, key: Key): boolean {
// Debug logging for key detection
if (key.ctrl || key.sequence === "\x16") {
// Debug logging for key detection (Ctrl or Meta/Cmd keys)
if (key.ctrl || key.meta || key.sequence === "\x16") {
logs.debug(
`Key detected: name=${key.name}, ctrl=${key.ctrl}, meta=${key.meta}, sequence=${JSON.stringify(key.sequence)}`,
`Key detected: name=${key.name}, ctrl=${key.ctrl}, meta=${key.meta}, shift=${key.shift}, paste=${key.paste}, sequence=${JSON.stringify(key.sequence)}`,
"clipboard",
)
}
Expand All @@ -916,9 +916,9 @@ function handleGlobalHotkeys(get: Getter, set: Setter, key: Key): boolean {
}
break
case "v":
// Ctrl+V - check for clipboard image
if (key.ctrl) {
logs.debug("Detected Ctrl+V via key.name", "clipboard")
// Ctrl+V or Cmd+V (macOS) - check for clipboard image
if (key.ctrl || key.meta) {
logs.debug(`Detected ${key.meta ? "Cmd" : "Ctrl"}+V via key.name`, "clipboard")
// Handle clipboard image paste asynchronously
handleClipboardImagePaste(get, set).catch((err) =>
logs.error("Unhandled clipboard paste error", "clipboard", { error: err }),
Expand Down Expand Up @@ -977,19 +977,39 @@ function handleGlobalHotkeys(get: Getter, set: Setter, key: Key): boolean {
}

/**
* Handle clipboard image paste (Ctrl+V)
* Atom to trigger clipboard image paste from external components (e.g., KeyboardProvider)
* This is used when a paste timeout occurs (e.g., Cmd+V with image in clipboard)
*/
export const triggerClipboardImagePasteAtom = atom(null, async (get, set, fallbackText?: string) => {
await handleClipboardImagePaste(get, set, fallbackText)
})

/**
* Handle clipboard image paste (Ctrl+V or Cmd+V on macOS)
* Saves clipboard image to a temp file and inserts @path reference into text buffer
* If fallbackText is provided and no image is found, broadcasts the fallback text as a paste event
*/
async function handleClipboardImagePaste(get: Getter, set: Setter): Promise<void> {
async function handleClipboardImagePaste(get: Getter, set: Setter, fallbackText?: string): Promise<void> {
logs.debug("handleClipboardImagePaste called", "clipboard")
try {
// Check if clipboard has an image
logs.debug("Checking clipboard for image...", "clipboard")
const hasImage = await clipboardHasImage()
logs.debug(`clipboardHasImage returned: ${hasImage}`, "clipboard")
if (!hasImage) {
setClipboardStatusWithTimeout(set, "No image in clipboard", 2000)
logs.debug("No image in clipboard", "clipboard")
// If fallback text provided, broadcast it as paste event
if (fallbackText) {
logs.debug("Using fallback text for paste", "clipboard")
set(broadcastKeyEventAtom, {
name: "",
ctrl: false,
meta: false,
shift: false,
paste: true,
sequence: fallbackText,
})
}
return
}

Expand Down
26 changes: 17 additions & 9 deletions cli/src/ui/providers/KeyboardProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import {
setDebugLoggingAtom,
clearBuffersAtom,
setupKeyboardAtom,
triggerClipboardImagePasteAtom,
} from "../../state/atoms/keyboard.js"
import {
parseKittySequence,
isPasteModeBoundary,
isFocusEvent,
mapAltKeyCharacter,
parseReadlineKey,
createPasteKey,
createSpecialKey,
} from "../utils/keyParsing.js"
import { autoEnableKittyProtocol } from "../utils/terminalCapabilities.js"
Expand Down Expand Up @@ -68,6 +68,7 @@ export function KeyboardProvider({ children, config = {} }: KeyboardProviderProp
const setDebugLogging = useSetAtom(setDebugLoggingAtom)
const clearBuffers = useSetAtom(clearBuffersAtom)
const setupKeyboard = useSetAtom(setupKeyboardAtom)
const triggerClipboardImagePaste = useSetAtom(triggerClipboardImagePasteAtom)

// Jotai getters (for reading current state)
const pasteBuffer = useAtomValue(pasteBufferAtom)
Expand Down Expand Up @@ -97,17 +98,23 @@ export function KeyboardProvider({ children, config = {} }: KeyboardProviderProp
// Handle paste completion
const completePaste = useCallback(() => {
const currentBuffer = pasteBufferRef.current
if (isPasteRef.current && currentBuffer) {
const wasPasting = isPasteRef.current

// Reset paste state
setPasteMode(false)
isPasteRef.current = false
pasteBufferRef.current = ""

if (wasPasting) {
// Normalize line endings: convert \r\n and \r to \n
// This handles different line ending formats from various terminals/platforms
const normalizedBuffer = currentBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n")

broadcastKey(createPasteKey(normalizedBuffer))
setPasteMode(false)
isPasteRef.current = false
pasteBufferRef.current = ""
const normalizedBuffer = currentBuffer ? currentBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") : ""
// Always check clipboard for image first (prioritize image over text)
// If no image found, the fallback text will be used
// This handles: Cmd+V with image file copied from Finder (terminal sends filename as text)
triggerClipboardImagePaste(normalizedBuffer || undefined)
}
}, [broadcastKey, setPasteMode])
}, [setPasteMode, triggerClipboardImagePaste])

useEffect(() => {
// Save original raw mode state
Expand Down Expand Up @@ -432,6 +439,7 @@ export function KeyboardProvider({ children, config = {} }: KeyboardProviderProp
setKittyProtocol,
pasteBuffer,
kittyBuffer,
triggerClipboardImagePaste,
isKittyEnabled,
isDebugEnabled,
completePaste,
Expand Down