diff --git a/cli/src/commands/__tests__/image.test.ts b/cli/src/commands/__tests__/image.test.ts index 2f5e2b985d..e2050e575e 100644 --- a/cli/src/commands/__tests__/image.test.ts +++ b/cli/src/commands/__tests__/image.test.ts @@ -1,26 +1,6 @@ import { describe, test, expect } from 'bun:test' -/** - * Tests for the handleImageCommand argument parsing behavior. - * - * These tests verify the parsing logic independently of the actual - * validateAndAddImage implementation by testing the parsing function directly. - */ - -// Extract the parsing logic that handleImageCommand uses -// New simplified implementation: split on whitespace -function parseImageCommandArgs(args: string): { - imagePath: string | null - message: string -} { - const [imagePath, ...rest] = args.trim().split(/\s+/) - - if (!imagePath) { - return { imagePath: null, message: '' } - } - - return { imagePath, message: rest.join(' ') } -} +import { parseImageCommandArgs } from '../parse-image-args' describe('handleImageCommand parsing', () => { describe('argument parsing', () => { @@ -61,9 +41,38 @@ describe('handleImageCommand parsing', () => { test('handles multiple spaces between path and message', () => { const result = parseImageCommandArgs('./image.png hello world') expect(result.imagePath).toBe('./image.png') - // The regex only captures content after the first whitespace group expect(result.message).toBe('hello world') }) + + test('handles double-quoted path with spaces and message', () => { + const result = parseImageCommandArgs( + '"Screenshot 2026-09-02 at 10.00.00.png" please analyze this', + ) + expect(result.imagePath).toBe('Screenshot 2026-09-02 at 10.00.00.png') + expect(result.message).toBe('please analyze this') + }) + + test('handles single-quoted path with spaces and message', () => { + const result = parseImageCommandArgs( + "'./Screenshots/My Capture.png' what is in this picture?", + ) + expect(result.imagePath).toBe('./Screenshots/My Capture.png') + expect(result.message).toBe('what is in this picture?') + }) + + test('handles quoted path only without message', () => { + const result = parseImageCommandArgs('"my screenshot.png"') + expect(result.imagePath).toBe('my screenshot.png') + expect(result.message).toBe('') + }) + + test('handles backslash-escaped spaces in path', () => { + const result = parseImageCommandArgs( + './My\\ Screenshot.png check this out', + ) + expect(result.imagePath).toBe('./My Screenshot.png') + expect(result.message).toBe('check this out') + }) }) describe('invalid input handling', () => { @@ -95,5 +104,11 @@ describe('handleImageCommand parsing', () => { const result = parseImageCommandArgs('~/Downloads/image.png') expect(result.imagePath).toBe('~/Downloads/image.png') }) + + test('handles empty quotes gracefully', () => { + const result = parseImageCommandArgs('"" please analyze this') + expect(result.imagePath).toBeNull() + expect(result.message).toBe('please analyze this') + }) }) }) diff --git a/cli/src/commands/command-registry.ts b/cli/src/commands/command-registry.ts index 820df1deca..75ac90d488 100644 --- a/cli/src/commands/command-registry.ts +++ b/cli/src/commands/command-registry.ts @@ -453,9 +453,17 @@ const ALL_COMMANDS: CommandDefinition[] = [ // If user provided a path directly, process it immediately if (trimmedArgs) { - await handleImageCommand(trimmedArgs) + const remainingMessage = await handleImageCommand(trimmedArgs) params.saveToHistory(params.inputValue.trim()) - clearInput(params) + if (remainingMessage) { + params.setInputValue({ + text: remainingMessage, + cursorPosition: remainingMessage.length, + lastEditDueToNav: false, + }) + } else { + clearInput(params) + } return } diff --git a/cli/src/commands/image.ts b/cli/src/commands/image.ts index f9a27fd041..86139ca490 100644 --- a/cli/src/commands/image.ts +++ b/cli/src/commands/image.ts @@ -1,6 +1,9 @@ +import { parseImageCommandArgs } from './parse-image-args' import { getProjectRoot } from '../project-files' import { validateAndAddImage } from '../utils/pending-attachments' +export { parseImageCommandArgs } + /** * Handle the /image command to attach an image file. * Usage: /image [message] @@ -10,11 +13,11 @@ import { validateAndAddImage } from '../utils/pending-attachments' * Errors are shown in the pending images banner with auto-remove. */ export async function handleImageCommand(args: string): Promise { - const [imagePath, ...rest] = args.trim().split(/\s+/) - + const { imagePath, message } = parseImageCommandArgs(args) + if (imagePath) { await validateAndAddImage(imagePath, getProjectRoot()) } - - return rest.join(' ') + + return message } diff --git a/cli/src/commands/parse-image-args.ts b/cli/src/commands/parse-image-args.ts new file mode 100644 index 0000000000..cad2ce1c6c --- /dev/null +++ b/cli/src/commands/parse-image-args.ts @@ -0,0 +1,54 @@ +/** + * Parse image command arguments into image path and optional accompanying message. + * Supports quoted paths ("path with spaces" or 'path with spaces') and backslash-escaped spaces. + */ +export function parseImageCommandArgs(args: string): { + imagePath: string | null + message: string +} { + const trimmed = args.trim() + if (!trimmed) { + return { imagePath: null, message: '' } + } + + const firstChar = trimmed[0] + if (firstChar === '"' || firstChar === "'") { + const closingQuoteIndex = trimmed.indexOf(firstChar, 1) + if (closingQuoteIndex !== -1) { + const imagePath = trimmed.slice(1, closingQuoteIndex) + const message = trimmed.slice(closingQuoteIndex + 1).trim() + return { imagePath: imagePath || null, message } + } + } + + let i = 0 + let inEscape = false + let pathEnd = -1 + + for (; i < trimmed.length; i++) { + const char = trimmed[i] + if (inEscape) { + inEscape = false + continue + } + if (char === '\\') { + inEscape = true + continue + } + if (/\s/.test(char)) { + pathEnd = i + break + } + } + + if (pathEnd === -1) { + const imagePath = trimmed.replace(/\\(\s)/g, '$1') + return { imagePath: imagePath || null, message: '' } + } + + const rawPath = trimmed.slice(0, pathEnd) + const imagePath = rawPath.replace(/\\(\s)/g, '$1') + const message = trimmed.slice(pathEnd).trim() + + return { imagePath: imagePath || null, message } +}