mirror of
https://github.com/anywhere-labs/dsh-desktop.git
synced 2026-09-28 05:03:17 +08:00
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:
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user