feat: Add clipboard image paste support for macOS (#1580)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: Scott Densmore <scottdensmore@mac.com>
This commit is contained in:
Jayson Dasher
2025-07-12 00:06:49 -04:00
committed by GitHub
parent c4ea17692f
commit c9e194ec6a
5 changed files with 412 additions and 4 deletions

View File

@@ -19,6 +19,12 @@ import { useKeypress, Key } from '../hooks/useKeypress.js';
import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
import { CommandContext, SlashCommand } from '../commands/types.js';
import { Config } from '@google/gemini-cli-core';
import {
clipboardHasImage,
saveClipboardImage,
cleanupOldClipboardImages,
} from '../utils/clipboardUtils.js';
import * as path from 'path';
export interface InputPromptProps {
buffer: TextBuffer;
@@ -52,7 +58,6 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
setShellModeActive,
}) => {
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const completion = useCompletion(
buffer.text,
config.getTargetDir(),
@@ -178,6 +183,54 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
[resetCompletionState, buffer, completionSuggestions, slashCommands],
);
// Handle clipboard image pasting with Ctrl+V
const handleClipboardImage = useCallback(async () => {
try {
if (await clipboardHasImage()) {
const imagePath = await saveClipboardImage(config.getTargetDir());
if (imagePath) {
// Clean up old images
cleanupOldClipboardImages(config.getTargetDir()).catch(() => {
// Ignore cleanup errors
});
// Get relative path from current directory
const relativePath = path.relative(config.getTargetDir(), imagePath);
// Insert @path reference at cursor position
const insertText = `@${relativePath}`;
const currentText = buffer.text;
const [row, col] = buffer.cursor;
// Calculate offset from row/col
let offset = 0;
for (let i = 0; i < row; i++) {
offset += buffer.lines[i].length + 1; // +1 for newline
}
offset += col;
// Add spaces around the path if needed
let textToInsert = insertText;
const charBefore = offset > 0 ? currentText[offset - 1] : '';
const charAfter =
offset < currentText.length ? currentText[offset] : '';
if (charBefore && charBefore !== ' ' && charBefore !== '\n') {
textToInsert = ' ' + textToInsert;
}
if (!charAfter || (charAfter !== ' ' && charAfter !== '\n')) {
textToInsert = textToInsert + ' ';
}
// Insert at cursor position
buffer.replaceRangeByOffset(offset, offset, textToInsert);
}
}
} catch (error) {
console.error('Error handling clipboard image:', error);
}
}, [buffer, config]);
const handleInput = useCallback(
(key: Key) => {
if (!focus) {
@@ -315,6 +368,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return;
}
// Ctrl+V for clipboard image paste
if (key.ctrl && key.name === 'v') {
handleClipboardImage();
return;
}
// Fallback to the text buffer's default input handling for all other keys
buffer.handleInput(key);
},
@@ -329,6 +388,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
handleAutocomplete,
handleSubmitAndClear,
shellHistory,
handleClipboardImage,
],
);
@@ -372,6 +432,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (visualIdxInRenderedSet === cursorVisualRow) {
const relativeVisualColForHighlight = cursorVisualColAbsolute;
if (relativeVisualColForHighlight >= 0) {
if (relativeVisualColForHighlight < cpLen(display)) {
const charToHighlight =