Skip to content
Open
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
59 changes: 37 additions & 22 deletions cli/src/commands/__tests__/image.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')
})
})
})
12 changes: 10 additions & 2 deletions cli/src/commands/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
11 changes: 7 additions & 4 deletions cli/src/commands/image.ts
Original file line number Diff line number Diff line change
@@ -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 <path> [message]
Expand All @@ -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<string> {
const [imagePath, ...rest] = args.trim().split(/\s+/)
const { imagePath, message } = parseImageCommandArgs(args)

if (imagePath) {
await validateAndAddImage(imagePath, getProjectRoot())
}
return rest.join(' ')

return message
}
54 changes: 54 additions & 0 deletions cli/src/commands/parse-image-args.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
Loading