diff --git a/ui/desktop/src/components/settings/extensions/utils.test.ts b/ui/desktop/src/components/settings/extensions/utils.test.ts index ab886aa17..5fd4cd089 100644 --- a/ui/desktop/src/components/settings/extensions/utils.test.ts +++ b/ui/desktop/src/components/settings/extensions/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { nameToKey, getDefaultFormData, @@ -346,6 +346,36 @@ describe('Extension Utils', () => { ])('splits %j correctly', (input, expected) => { expect(splitCmdAndArgs(input)).toEqual(expected); }); + + describe('on Windows', () => { + beforeEach(() => { + vi.stubGlobal('window', { electron: { platform: 'win32' } }); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('preserves backslashes in a Windows path', () => { + expect(splitCmdAndArgs('C:\\Users\\name\\path\\to\\extension.js')).toEqual({ + cmd: 'C:\\Users\\name\\path\\to\\extension.js', + args: [], + }); + }); + + it('preserves backslashes in cmd and args', () => { + expect(splitCmdAndArgs('node C:\\Users\\name\\ext.js')).toEqual({ + cmd: 'node', + args: ['C:\\Users\\name\\ext.js'], + }); + }); + + it('handles a quoted Windows path containing spaces', () => { + expect(splitCmdAndArgs('"C:\\Program Files\\app\\ext.js"')).toEqual({ + cmd: 'C:\\Program Files\\app\\ext.js', + args: [], + }); + }); + }); }); describe('extractCommand', () => { diff --git a/ui/desktop/src/components/settings/extensions/utils.ts b/ui/desktop/src/components/settings/extensions/utils.ts index 00e29c1fa..121726102 100644 --- a/ui/desktop/src/components/settings/extensions/utils.ts +++ b/ui/desktop/src/components/settings/extensions/utils.ts @@ -168,13 +168,23 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon } } +function isWindowsPlatform(): boolean { + return typeof window !== 'undefined' && window.electron?.platform === 'win32'; +} + export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } { const trimmed = str.trim(); if (!trimmed) { return { cmd: '', args: [] }; } - const parsed = parseShellQuote(trimmed); + // shell-quote treats `\` as a POSIX escape character, so a Windows path like + // `C:\Users\name\ext.js` would lose its backslashes and become `C:Usersnameext.js`. + // Doubling backslashes on Windows lets them survive parsing (shell-quote unescapes + // `\\` back to `\`), while still honoring quotes for paths containing spaces. + const toParse = isWindowsPlatform() ? trimmed.replace(/\\/g, '\\\\') : trimmed; + + const parsed = parseShellQuote(toParse); const words = parsed.filter((item): item is string => typeof item === 'string').map(String); const cmd = words[0] || '';