diff --git a/src/main/ipc/app.test.ts b/src/main/ipc/app.test.ts index 68c3403c8..1718134d4 100644 --- a/src/main/ipc/app.test.ts +++ b/src/main/ipc/app.test.ts @@ -1,14 +1,24 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { handlers, appExitMock, appQuitMock, appRelaunchMock, execFileMock, destroySystemTrayMock } = - vi.hoisted(() => ({ - handlers: new Map unknown>(), - appExitMock: vi.fn(), - appQuitMock: vi.fn(), - appRelaunchMock: vi.fn(), - execFileMock: vi.fn(), - destroySystemTrayMock: vi.fn() - })) +const { + handlers, + appExitMock, + appQuitMock, + appRelaunchMock, + execFileMock, + destroySystemTrayMock, + showOpenDialogMock, + grantFloatingWorkspaceDirectoryMock +} = vi.hoisted(() => ({ + handlers: new Map unknown>(), + appExitMock: vi.fn(), + appQuitMock: vi.fn(), + appRelaunchMock: vi.fn(), + execFileMock: vi.fn(), + destroySystemTrayMock: vi.fn(), + showOpenDialogMock: vi.fn(), + grantFloatingWorkspaceDirectoryMock: vi.fn() +})) vi.mock('node:child_process', () => ({ execFile: execFileMock @@ -26,7 +36,7 @@ vi.mock('electron', () => ({ fromWebContents: vi.fn(() => null) }, dialog: { - showOpenDialog: vi.fn() + showOpenDialog: showOpenDialogMock }, ipcMain: { handle: vi.fn((channel: string, handler: (_event: unknown, args?: unknown) => unknown) => { @@ -43,6 +53,12 @@ vi.mock('../tray/system-tray', () => ({ destroySystemTray: destroySystemTrayMock })) +vi.mock('./floating-workspace-directory', () => ({ + ensureDefaultFloatingWorkspacePath: vi.fn(), + grantFloatingWorkspaceDirectory: grantFloatingWorkspaceDirectoryMock, + resolveFloatingTerminalCwd: vi.fn() +})) + import { registerAppHandlers } from './app' describe('registerAppHandlers', () => { @@ -56,6 +72,8 @@ describe('registerAppHandlers', () => { appRelaunchMock.mockReset() execFileMock.mockReset() destroySystemTrayMock.mockReset() + showOpenDialogMock.mockReset() + grantFloatingWorkspaceDirectoryMock.mockReset() Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }) }) @@ -177,4 +195,21 @@ describe('registerAppHandlers', () => { await expect(resultPromise).resolves.toBeNull() expect(killMock).toHaveBeenCalled() }) + + it('picks an existing floating workspace directory without enabling native directory creation', async () => { + const store = {} + showOpenDialogMock.mockResolvedValue({ + canceled: false, + filePaths: ['/Users/kaylee/notes'] + }) + registerAppHandlers(store as never) + + await expect( + handlers.get('app:pickFloatingWorkspaceDirectory')?.({ sender: {} }) + ).resolves.toBe('/Users/kaylee/notes') + expect(showOpenDialogMock).toHaveBeenCalledWith({ + properties: ['openDirectory'] + }) + expect(grantFloatingWorkspaceDirectoryMock).toHaveBeenCalledWith(store, '/Users/kaylee/notes') + }) }) diff --git a/src/main/ipc/app.ts b/src/main/ipc/app.ts index e16b70b40..be33ef389 100644 --- a/src/main/ipc/app.ts +++ b/src/main/ipc/app.ts @@ -57,7 +57,9 @@ async function pickFloatingWorkspaceDirectory( ): Promise { const parentWindow = BrowserWindow.fromWebContents(event.sender) const options = { - properties: ['openDirectory', 'createDirectory'] + // Why: this picker grants access to an existing workspace directory. + // Creation belongs to explicit file/write actions, not typeahead input. + properties: ['openDirectory'] } satisfies Electron.OpenDialogOptions const result = parentWindow ? await dialog.showOpenDialog(parentWindow, options) diff --git a/src/main/ipc/repos-picker.test.ts b/src/main/ipc/repos-picker.test.ts index 5d454bb1a..fb713ef6a 100644 --- a/src/main/ipc/repos-picker.test.ts +++ b/src/main/ipc/repos-picker.test.ts @@ -64,6 +64,14 @@ describe('repos folder pickers', () => { return handler(null, undefined) as Promise } + const callPickDirectory = (): Promise => { + const handler = handlers.get('repos:pickDirectory') + if (!handler) { + throw new Error('repos:pickDirectory handler was never registered') + } + return handler(null, undefined) as Promise + } + beforeEach(() => { handlers.clear() handleMock.mockReset() @@ -101,4 +109,18 @@ describe('repos folder pickers', () => { await expect(callPickFolders()).resolves.toEqual([]) }) + + it('picks an existing directory without enabling native directory creation', async () => { + const parentDir = join(sep, 'projects') + showOpenDialogMock.mockResolvedValue({ + canceled: false, + filePaths: [parentDir] + }) + + await expect(callPickDirectory()).resolves.toBe(parentDir) + + expect(showOpenDialogMock).toHaveBeenCalledWith(mockWindow, { + properties: ['openDirectory'] + }) + }) }) diff --git a/src/main/ipc/repos.ts b/src/main/ipc/repos.ts index 55827fe1b..43d048a22 100644 --- a/src/main/ipc/repos.ts +++ b/src/main/ipc/repos.ts @@ -2070,7 +2070,9 @@ export function registerRepoHandlers(mainWindow: BrowserWindow, store: Store): v // destination directory that may not be a git repo yet. ipcMain.handle('repos:pickDirectory', async () => { const result = await dialog.showOpenDialog(mainWindow, { - properties: ['openDirectory', 'createDirectory'] + // Why: macOS can materialize typed partial paths when directory creation + // is enabled; clone/create actions already create the final path on submit. + properties: ['openDirectory'] }) if (result.canceled || result.filePaths.length === 0) { return null diff --git a/src/main/ipc/shell.test.ts b/src/main/ipc/shell.test.ts index ad7b03e06..24ac41e2d 100644 --- a/src/main/ipc/shell.test.ts +++ b/src/main/ipc/shell.test.ts @@ -132,6 +132,22 @@ describe('registerShellHandlers', () => { await expect(handler({})).resolves.toBeNull() }) + it('picks an existing directory without enabling native directory creation', async () => { + showOpenDialogMock.mockResolvedValue({ + canceled: false, + filePaths: ['/Users/kaylee/projects'] + }) + + const handler = getHandler('shell:pickDirectory') + await expect(handler({}, { defaultPath: '/Users/kaylee' })).resolves.toBe( + '/Users/kaylee/projects' + ) + expect(showOpenDialogMock).toHaveBeenCalledWith({ + defaultPath: '/Users/kaylee', + properties: ['openDirectory'] + }) + }) + describe('shell:openPath', () => { it('ignores relative paths', async () => { const handler = getHandler('shell:openPath') diff --git a/src/main/ipc/shell.ts b/src/main/ipc/shell.ts index 85cccbc7c..84e7fc731 100644 --- a/src/main/ipc/shell.ts +++ b/src/main/ipc/shell.ts @@ -205,7 +205,9 @@ export function registerShellHandlers(): void { async (_event, args: { defaultPath?: string }): Promise => { const result = await dialog.showOpenDialog({ defaultPath: args.defaultPath, - properties: ['openDirectory', 'createDirectory'] + // Why: callers only need an existing folder grant; enabling native + // creation can leave typed prefix directories behind on macOS. + properties: ['openDirectory'] }) if (result.canceled || result.filePaths.length === 0) { return null