-
Notifications
You must be signed in to change notification settings - Fork 13.5k
feat: support multi-file drag and drop of images #14832
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7fdd52c
f610819
5f09e49
7e1b5b2
d2fa0bf
d8d29c9
a5a501e
94e804c
9357e6c
d9ca283
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,12 @@ | |
|
|
||
| import * as fs from 'node:fs/promises'; | ||
| import * as path from 'node:path'; | ||
| import { debugLogger, spawnAsync } from '@google/gemini-cli-core'; | ||
| import { | ||
| debugLogger, | ||
| spawnAsync, | ||
| unescapePath, | ||
| escapePath, | ||
| } from '@google/gemini-cli-core'; | ||
|
|
||
| /** | ||
| * Supported image file extensions based on Gemini API. | ||
|
|
@@ -21,6 +26,9 @@ export const IMAGE_EXTENSIONS = [ | |
| '.heif', | ||
| ]; | ||
|
|
||
| /** Matches strings that start with a path prefix (/, ~, ., Windows drive letter, or UNC path) */ | ||
| const PATH_PREFIX_PATTERN = /^([/~.]|[a-zA-Z]:|\\\\)/; | ||
|
|
||
| /** | ||
| * Checks if the system clipboard contains an image (macOS only for now) | ||
| * @returns true if clipboard contains an image | ||
|
|
@@ -101,16 +109,18 @@ export async function saveClipboardImage( | |
| if (stats.size > 0) { | ||
| return tempFilePath; | ||
| } | ||
| } catch { | ||
| } catch (e) { | ||
| // File doesn't exist, continue to next format | ||
| debugLogger.debug('Clipboard image file not found:', tempFilePath, e); | ||
| } | ||
| } | ||
|
|
||
| // Clean up failed attempt | ||
| try { | ||
| await fs.unlink(tempFilePath); | ||
| } catch { | ||
| } catch (e) { | ||
| // Ignore cleanup errors | ||
| debugLogger.debug('Failed to clean up temp file:', tempFilePath, e); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -146,7 +156,91 @@ export async function cleanupOldClipboardImages( | |
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } catch (e) { | ||
| // Ignore errors in cleanup | ||
| debugLogger.debug('Failed to clean up old clipboard images:', e); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Splits text into individual path segments, respecting escaped spaces. | ||
| * Unescaped spaces act as separators between paths, while "\ " is preserved | ||
| * as part of a filename. | ||
| * | ||
| * Example: "/img1.png /path/my\ image.png" → ["/img1.png", "/path/my\ image.png"] | ||
| * | ||
| * @param text The text to split | ||
| * @returns Array of path segments (still escaped) | ||
| */ | ||
| export function splitEscapedPaths(text: string): string[] { | ||
| const paths: string[] = []; | ||
| let current = ''; | ||
| let i = 0; | ||
|
|
||
| while (i < text.length) { | ||
| const char = text[i]; | ||
|
|
||
| if (char === '\\' && i + 1 < text.length && text[i + 1] === ' ') { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an edge case this will get confused by is you want to track the number of |
||
| // Escaped space - part of filename, preserve the escape sequence | ||
| current += '\\ '; | ||
| i += 2; | ||
| } else if (char === ' ') { | ||
| // Unescaped space - path separator | ||
| if (current.trim()) { | ||
| paths.push(current.trim()); | ||
| } | ||
| current = ''; | ||
| i++; | ||
| } else { | ||
| current += char; | ||
| i++; | ||
| } | ||
| } | ||
|
|
||
| // Don't forget the last segment | ||
| if (current.trim()) { | ||
| paths.push(current.trim()); | ||
| } | ||
|
|
||
| return paths; | ||
| } | ||
|
jackwotherspoon marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Processes pasted text containing file paths, adding @ prefix to valid paths. | ||
| * Handles both single and multiple space-separated paths. | ||
| * | ||
| * @param text The pasted text (potentially space-separated paths) | ||
| * @param isValidPath Function to validate if a path exists/is valid | ||
| * @returns Processed string with @ prefixes on valid paths, or null if no valid paths | ||
| */ | ||
| export function parsePastedPaths( | ||
| text: string, | ||
| isValidPath: (path: string) => boolean, | ||
| ): string | null { | ||
| // First, check if the entire text is a single valid path | ||
| if (PATH_PREFIX_PATTERN.test(text) && isValidPath(text)) { | ||
| return `@${escapePath(text)} `; | ||
| } | ||
|
|
||
| // Otherwise, try splitting on unescaped spaces | ||
| const segments = splitEscapedPaths(text); | ||
| if (segments.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| let anyValidPath = false; | ||
| const processedPaths = segments.map((segment) => { | ||
| // Quick rejection: skip segments that can't be paths | ||
| if (!PATH_PREFIX_PATTERN.test(segment)) { | ||
| return segment; | ||
| } | ||
| const unescaped = unescapePath(segment); | ||
| if (isValidPath(unescaped)) { | ||
| anyValidPath = true; | ||
| return `@${segment}`; | ||
| } | ||
| return segment; | ||
| }); | ||
|
|
||
| return anyValidPath ? processedPaths.join(' ') + ' ' : null; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.