Merge pull request #1213 from anywhere-labs/codex/macos-native-workspace-picker

fix(desktop): use Electron picker for macOS workspace selection
This commit is contained in:
t4we fan
2026-09-25 23:57:39 +08:00
committed by GitHub
14 changed files with 116 additions and 4 deletions
@@ -1,6 +1,9 @@
/** Same-origin endpoint used by the Windows browse panel's native-picker shortcut. */
export const DESKTOP_DIRECTORY_PICKER_PATH = '/_dsh/desktop/pick-directory'
/** macOS preload-to-main picker, available before the official client plugins load. */
export const DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL = 'dsh-desktop:native-directory-picker'
/** Same-origin endpoint used before either workspace picker accepts a path. */
export const DESKTOP_DIRECTORY_VALIDATOR_PATH = '/_dsh/desktop/validate-directory'
@@ -51,7 +51,7 @@ class WindowsPlatformStrategy implements ElectronPlatformStrategy {
class MacPlatformStrategy implements ElectronPlatformStrategy {
readonly platform = 'darwin'
readonly updateDownloadPlatform = 'darwin'
readonly canPickDirectory = false
readonly canPickDirectory = true
readonly canToggleShellMode = true
readonly hidesWindowOnClose = true
@@ -326,6 +326,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
platform: this.platformStrategy,
spec,
preloadPath: desktopPreloadPath(),
pickDirectory: () => this.pickDirectory(),
buildApplicationMenuItems: () => this.buildApplicationMenuItems(),
isQuitting: () => this.quitting,
buildTrayTemplate: () => this.buildTrayTemplate(spec),
@@ -24,6 +24,7 @@ import {
type DesktopOpenWorkspaceDelivery,
} from './launch-workspace-contract.ts'
import { DESKTOP_RENDERER_ACTION_CHANNEL } from './renderer-actions-contract.ts'
import { DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL } from './directory-picker-contract.ts'
import { createDesktopRendererActionDispatcher } from './renderer-actions-dispatch.ts'
import type { DesktopNotification, DesktopShellSpec } from './runtime.ts'
import { prepareTrayIcon } from './tray-icons.ts'
@@ -195,6 +196,7 @@ export interface ElectronShellGenerationOptions {
readonly platform: ElectronPlatformStrategy
readonly spec: DesktopShellSpec
readonly preloadPath: string
readonly pickDirectory: () => Promise<string | null>
readonly buildApplicationMenuItems: () => readonly Electron.MenuItemConstructorOptions[]
readonly isQuitting: () => boolean
readonly buildTrayTemplate: () => Electron.MenuItemConstructorOptions[]
@@ -356,6 +358,17 @@ export class ElectronShellGeneration {
await dispatchRendererAction(action)
})
if (platform.platform === 'darwin') {
renderer.ipc.handle(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL, async event => {
if (this.released || event.sender !== renderer
|| event.senderFrame === null || event.senderFrame !== renderer.mainFrame
|| !sameOriginFrame(event.senderFrame.url, origin)) {
throw new Error('dsh-plugin-desktop: untrusted directory picker sender')
}
return await this.options.pickDirectory()
})
}
renderer.ipc.handle(SETUP_ONBOARDING_CHANNEL, async (event, request: unknown) => {
if (this.released || event.sender !== renderer || event.senderFrame !== renderer.mainFrame
|| !sameOriginFrame(event.senderFrame.url, origin)) throw new Error('Untrusted setup sender')
@@ -626,6 +639,7 @@ export class ElectronShellGeneration {
renderer.off('did-finish-load', loaded)
if (!renderer.isDestroyed()) {
renderer.ipc.removeHandler(DESKTOP_RENDERER_ACTION_CHANNEL)
if (platform.platform === 'darwin') renderer.ipc.removeHandler(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL)
renderer.ipc.removeHandler(SETUP_ONBOARDING_CHANNEL)
}
if (isolated) {
+10
View File
@@ -3,6 +3,7 @@
import { contextBridge, ipcRenderer, webUtils } from 'electron'
import { SETUP_ONBOARDING_CHANNEL } from './setup-onboarding-bridge.ts'
import { DESKTOP_FILE_PATH_BRIDGE } from './file-path-bridge-contract.ts'
import { DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL } from './directory-picker-contract.ts'
import {
DESKTOP_RENDERER_ACTION_CHANNEL,
DESKTOP_RENDERER_ACTIONS_BRIDGE,
@@ -23,6 +24,15 @@ const actions: DesktopRendererActionsBridge = {
}
contextBridge.exposeInMainWorld(DESKTOP_RENDERER_ACTIONS_BRIDGE, actions)
// The official native directory-flow plugin captures this seam at apply time.
// Install it before any client plugin runs; the Host's macOS osascript chooser
// otherwise waits for AppleEvents and may time out without showing a window.
if (process.platform === 'darwin') {
contextBridge.exposeInMainWorld('__DSH_DIRECTORY_PICKER__', Object.freeze({
pick: (): Promise<string | null> => ipcRenderer.invoke(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL),
}))
}
// Upstream client plugins recognize the Desktop renderer by this carrier. Version 1
// without `updates` or `browser` keeps upstream update badges and the embedded
// browser tab on their Web fallbacks, and turns on the DeepSeek account entry whose
@@ -66,7 +66,7 @@ describe('electronPlatformStrategy', () => {
expect(strategy.platform).toBe('darwin')
expect(strategy.updateDownloadPlatform).toBe('darwin')
expect(strategy.canPickDirectory).toBe(false)
expect(strategy.canPickDirectory).toBe(true)
expect(strategy.canToggleShellMode).toBe(true)
expect(strategy.hidesWindowOnClose).toBe(true)
@@ -868,6 +868,34 @@ describe('Electron desktop runtime', () => {
await release()
})
it('routes the macOS native flow through a trusted renderer and a parented Electron dialog', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showOpenDialog.mockResolvedValue({ canceled: false, filePaths: ['/Users/test/Work'] })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const runtime = new ElectronDesktopRuntime(async () => {})
const release = runtime.schedule(spec)
await runtime.mountScheduled()
const handler = electron.webContents.ipc.handle.mock.calls
.find(([name]) => name === 'dsh-desktop:native-directory-picker')?.[1]
expect(handler).toEqual(expect.any(Function))
const frame = electron.webContents.mainFrame
const previousUrl = frame.url
frame.url = spec.url
try {
await expect(handler({ sender: electron.webContents, senderFrame: frame })).resolves.toBe('/Users/test/Work')
expect(electron.dialog.showOpenDialog).toHaveBeenCalledWith(
electron.browserWindows[0],
{ title: 'Select Workspace Directory', properties: ['openDirectory', 'dontAddToRecent'] },
)
await expect(handler({ sender: electron.webContents, senderFrame: { url: spec.url } }))
.rejects.toThrow('untrusted directory picker sender')
} finally {
frame.url = previousUrl
await release()
}
})
it('blocks unsupported workspace volumes without returning a risky path', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
@@ -1,6 +1,9 @@
/** Same-origin endpoint used by the Windows browse panel's native-picker shortcut. */
export const DESKTOP_DIRECTORY_PICKER_PATH = '/_dsh/desktop/pick-directory'
/** macOS preload-to-main picker, available before the official client plugins load. */
export const DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL = 'dsh-desktop:native-directory-picker'
/** Same-origin endpoint used before either workspace picker accepts a path. */
export const DESKTOP_DIRECTORY_VALIDATOR_PATH = '/_dsh/desktop/validate-directory'
+1 -1
View File
@@ -51,7 +51,7 @@ class WindowsPlatformStrategy implements ElectronPlatformStrategy {
class MacPlatformStrategy implements ElectronPlatformStrategy {
readonly platform = 'darwin'
readonly updateDownloadPlatform = 'darwin'
readonly canPickDirectory = false
readonly canPickDirectory = true
readonly canToggleShellMode = true
readonly hidesWindowOnClose = true
@@ -326,6 +326,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
platform: this.platformStrategy,
spec,
preloadPath: desktopPreloadPath(),
pickDirectory: () => this.pickDirectory(),
buildApplicationMenuItems: () => this.buildApplicationMenuItems(),
isQuitting: () => this.quitting,
buildTrayTemplate: () => this.buildTrayTemplate(spec),
@@ -24,6 +24,7 @@ import {
type DesktopOpenWorkspaceDelivery,
} from './launch-workspace-contract.ts'
import { DESKTOP_RENDERER_ACTION_CHANNEL } from './renderer-actions-contract.ts'
import { DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL } from './directory-picker-contract.ts'
import { createDesktopRendererActionDispatcher } from './renderer-actions-dispatch.ts'
import type { DesktopNotification, DesktopShellSpec } from './runtime.ts'
import { prepareTrayIcon } from './tray-icons.ts'
@@ -195,6 +196,7 @@ export interface ElectronShellGenerationOptions {
readonly platform: ElectronPlatformStrategy
readonly spec: DesktopShellSpec
readonly preloadPath: string
readonly pickDirectory: () => Promise<string | null>
readonly buildApplicationMenuItems: () => readonly Electron.MenuItemConstructorOptions[]
readonly isQuitting: () => boolean
readonly buildTrayTemplate: () => Electron.MenuItemConstructorOptions[]
@@ -356,6 +358,17 @@ export class ElectronShellGeneration {
await dispatchRendererAction(action)
})
if (platform.platform === 'darwin') {
renderer.ipc.handle(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL, async event => {
if (this.released || event.sender !== renderer
|| event.senderFrame === null || event.senderFrame !== renderer.mainFrame
|| !sameOriginFrame(event.senderFrame.url, origin)) {
throw new Error('dsh-plugin-desktop: untrusted directory picker sender')
}
return await this.options.pickDirectory()
})
}
renderer.ipc.handle(SETUP_ONBOARDING_CHANNEL, async (event, request: unknown) => {
if (this.released || event.sender !== renderer || event.senderFrame !== renderer.mainFrame
|| !sameOriginFrame(event.senderFrame.url, origin)) throw new Error('Untrusted setup sender')
@@ -626,6 +639,7 @@ export class ElectronShellGeneration {
renderer.off('did-finish-load', loaded)
if (!renderer.isDestroyed()) {
renderer.ipc.removeHandler(DESKTOP_RENDERER_ACTION_CHANNEL)
if (platform.platform === 'darwin') renderer.ipc.removeHandler(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL)
renderer.ipc.removeHandler(SETUP_ONBOARDING_CHANNEL)
}
if (isolated) {
+10
View File
@@ -3,6 +3,7 @@
import { contextBridge, ipcRenderer, webUtils } from 'electron'
import { SETUP_ONBOARDING_CHANNEL } from './setup-onboarding-bridge.ts'
import { DESKTOP_FILE_PATH_BRIDGE } from './file-path-bridge-contract.ts'
import { DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL } from './directory-picker-contract.ts'
import {
DESKTOP_RENDERER_ACTION_CHANNEL,
DESKTOP_RENDERER_ACTIONS_BRIDGE,
@@ -23,6 +24,15 @@ const actions: DesktopRendererActionsBridge = {
}
contextBridge.exposeInMainWorld(DESKTOP_RENDERER_ACTIONS_BRIDGE, actions)
// The official native directory-flow plugin captures this seam at apply time.
// Install it before any client plugin runs; the Host's macOS osascript chooser
// otherwise waits for AppleEvents and may time out without showing a window.
if (process.platform === 'darwin') {
contextBridge.exposeInMainWorld('__DSH_DIRECTORY_PICKER__', Object.freeze({
pick: (): Promise<string | null> => ipcRenderer.invoke(DESKTOP_NATIVE_DIRECTORY_PICKER_CHANNEL),
}))
}
// Upstream client plugins recognize the Desktop renderer by this carrier. Version 1
// without `updates` or `browser` keeps upstream update badges and the embedded
// browser tab on their Web fallbacks, and turns on the DeepSeek account entry whose
@@ -66,7 +66,7 @@ describe('electronPlatformStrategy', () => {
expect(strategy.platform).toBe('darwin')
expect(strategy.updateDownloadPlatform).toBe('darwin')
expect(strategy.canPickDirectory).toBe(false)
expect(strategy.canPickDirectory).toBe(true)
expect(strategy.canToggleShellMode).toBe(true)
expect(strategy.hidesWindowOnClose).toBe(true)
@@ -869,6 +869,34 @@ describe('Electron desktop runtime', () => {
await release()
})
it('routes the macOS native flow through a trusted renderer and a parented Electron dialog', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
electron.dialog.showOpenDialog.mockResolvedValue({ canceled: false, filePaths: ['/Users/test/Work'] })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const runtime = new ElectronDesktopRuntime(async () => {})
const release = runtime.schedule(spec)
await runtime.mountScheduled()
const handler = electron.webContents.ipc.handle.mock.calls
.find(([name]) => name === 'dsh-desktop:native-directory-picker')?.[1]
expect(handler).toEqual(expect.any(Function))
const frame = electron.webContents.mainFrame
const previousUrl = frame.url
frame.url = spec.url
try {
await expect(handler({ sender: electron.webContents, senderFrame: frame })).resolves.toBe('/Users/test/Work')
expect(electron.dialog.showOpenDialog).toHaveBeenCalledWith(
electron.browserWindows[0],
{ title: 'Select Workspace Directory', properties: ['openDirectory', 'dontAddToRecent'] },
)
await expect(handler({ sender: electron.webContents, senderFrame: { url: spec.url } }))
.rejects.toThrow('untrusted directory picker sender')
} finally {
frame.url = previousUrl
await release()
}
})
it('blocks unsupported workspace volumes without returning a risky path', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')