feat(desktop): add Windows workspace folder picker

This commit is contained in:
Qiuner
2026-08-18 10:48:14 +08:00
parent cf24a93588
commit bc9772ffc6
17 changed files with 534 additions and 8 deletions
@@ -0,0 +1,49 @@
import {
DESKTOP_DIRECTORY_PICKER_PATH,
type DesktopDirectoryPickerResponse,
} from '../directory-picker-contract.ts'
/** Window seam consumed by the patched upstream browse panel. */
export interface DesktopDirectoryPickerWindow {
__DSH_DESKTOP_PICK_DIRECTORY__?: () => Promise<string | null>
}
type DirectoryPickerRequest = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>
function isResponse(value: unknown): value is DesktopDirectoryPickerResponse {
if (typeof value !== 'object' || value === null || !('path' in value)) return false
const path = (value as { path?: unknown }).path
return path === null || typeof path === 'string'
}
/** Ask the desktop Host to open the platform folder chooser. */
export async function requestDesktopDirectory(
request: DirectoryPickerRequest = window.fetch.bind(window),
): Promise<string | null> {
const response = await request(DESKTOP_DIRECTORY_PICKER_PATH, {
method: 'POST',
headers: { accept: 'application/json' },
})
if (!response.ok) throw new Error('DSH Desktop could not open the system folder picker')
const value: unknown = await response.json()
if (!isResponse(value)) throw new Error('DSH Desktop received an invalid response from the system folder picker')
return value.path
}
/** Publish the Windows-only picker bridge for the browse panel's icon action. */
export function installDesktopDirectoryPickerBridge(
target: DesktopDirectoryPickerWindow = window as DesktopDirectoryPickerWindow,
request: DirectoryPickerRequest = window.fetch.bind(window),
): () => void {
const previous = target.__DSH_DESKTOP_PICK_DIRECTORY__
const pick = async (): Promise<string | null> => await requestDesktopDirectory(request)
target.__DSH_DESKTOP_PICK_DIRECTORY__ = pick
return () => {
if (target.__DSH_DESKTOP_PICK_DIRECTORY__ !== pick) return
if (previous === undefined) delete target.__DSH_DESKTOP_PICK_DIRECTORY__
else target.__DSH_DESKTOP_PICK_DIRECTORY__ = previous
}
}
+7
View File
@@ -7,6 +7,7 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import type {} from '@deepseek-ai/dsh-client-ui-theme/client'
import { applyAdvancedShell } from './advanced-shell.ts'
import { startRendererBootReporter } from './boot-health.ts'
import { installDesktopDirectoryPickerBridge } from './directory-picker.ts'
import { parseDesktopClientEnvironment } from './environment.ts'
export { applyAdvancedShell } from './advanced-shell.ts'
@@ -35,5 +36,11 @@ export function apply(ctx: ClientContext): void {
() => startRendererBootReporter(ctx.loader),
'dsh-plugin-desktop: renderer boot health report',
)
if (environment.platform === 'win32') {
ctx.effect(
() => installDesktopDirectoryPickerBridge(),
'dsh-plugin-desktop: native directory picker bridge',
)
}
if (environment.mode === 'advanced') applyAdvancedShell(ctx, environment)
}
@@ -0,0 +1,8 @@
/** Same-origin endpoint used by the Windows browse panel's native-picker shortcut. */
export const DESKTOP_DIRECTORY_PICKER_PATH = '/_dsh/desktop/pick-directory'
/** Successful native directory-picker response. */
export interface DesktopDirectoryPickerResponse {
/** Absolute selected path, or null when the chooser was cancelled. */
path: string | null
}
@@ -0,0 +1,27 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
import type { DesktopDirectoryPickerResponse } from './directory-picker-contract.ts'
function finishJson(res: ServerResponse, statusCode: number, value: object): void {
res.statusCode = statusCode
res.setHeader('content-type', 'application/json; charset=utf-8')
res.end(JSON.stringify(value))
}
/** Validate and serve one native directory-picker request from the desktop renderer. */
export async function handleDesktopDirectoryPickerRequest(
req: IncomingMessage,
res: ServerResponse,
expectedOrigin: string,
pickDirectory: () => Promise<string | null>,
reportError: (cause: unknown) => void = () => {},
): Promise<void> {
if (req.method !== 'POST') return finishJson(res, 405, { error: 'method not allowed' })
if (req.headers.origin !== expectedOrigin) return finishJson(res, 403, { error: 'forbidden' })
try {
const response: DesktopDirectoryPickerResponse = { path: await pickDirectory() }
finishJson(res, 200, response)
} catch (cause: unknown) {
reportError(cause)
finishJson(res, 500, { error: 'native directory picker failed' })
}
}
@@ -110,6 +110,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
private readonly trayItems = new Map<symbol, DesktopTrayItem>()
private terminalSpec: DesktopTerminalSpec | undefined
private diagnosticExport: Promise<void> | undefined
private directoryPickTask: Promise<string | null> | undefined
private rendererBootReported = false
constructor(
@@ -181,6 +182,33 @@ export class ElectronDesktopRuntime implements DesktopRuntime {
window.focus()
}
/** @inheritdoc */
async pickDirectory(): Promise<string | null> {
if (this.platform !== 'win32') {
throw new Error(`dsh-plugin-desktop: native workspace picker is unavailable on ${this.platform}`)
}
if (this.directoryPickTask !== undefined) return await this.directoryPickTask
const task = this.showDirectoryPicker()
this.directoryPickTask = task
try {
return await task
} finally {
if (this.directoryPickTask === task) this.directoryPickTask = undefined
}
}
private async showDirectoryPicker(): Promise<string | null> {
const options: Electron.OpenDialogOptions = {
title: this.currentLocale === 'zh' ? '选择工作区目录' : 'Select Workspace Directory',
properties: ['openDirectory', 'dontAddToRecent'],
}
const window = this.window
const result = window === undefined || window.isDestroyed()
? await dialog.showOpenDialog(options)
: await dialog.showOpenDialog(window, options)
return result.canceled ? null : result.filePaths[0] ?? null
}
/** @inheritdoc */
registerTrayItem(item: DesktopTrayItem): DesktopTrayItemRegistration {
const key = Symbol()
+20
View File
@@ -18,6 +18,8 @@ import {
handleRendererBootRequest,
RENDERER_BOOT_REPORT_PATH,
} from './renderer-boot.ts'
import { DESKTOP_DIRECTORY_PICKER_PATH } from './directory-picker-contract.ts'
import { handleDesktopDirectoryPickerRequest } from './directory-picker-route.ts'
import type { DesktopShellMode } from './runtime.ts'
import type {} from './runtime.ts'
@@ -145,6 +147,24 @@ export function apply(ctx: Context, config: Config): void {
}),
'dsh-plugin-desktop: renderer boot report route',
)
if (runtime.platform === 'win32') {
ctx.effect(
() => ctx.webServer.register({
kind: 'exact',
path: DESKTOP_DIRECTORY_PICKER_PATH,
handler: (req, res) => handleDesktopDirectoryPickerRequest(
req,
res,
rendererOrigin,
() => runtime.pickDirectory(),
cause => {
ctx.logger.error(`dsh-plugin-desktop: native directory picker failed: ${cause instanceof Error ? cause.message : String(cause)}`)
},
),
}),
'dsh-plugin-desktop: native directory picker route',
)
}
ctx.effect(() => {
let pending: ReturnType<typeof setImmediate> | undefined
const stopWatching = settings.watch((next) => {
+3
View File
@@ -181,6 +181,9 @@ export interface DesktopRuntime {
/** Export a diagnostics zip and reveal it in the system file manager. */
exportDiagnostics(): Promise<void>
/** Open the desktop operating system's native workspace-folder chooser. */
pickDirectory(): Promise<string | null>
/** Accept the terminal client Loader outcome for the mounted generation. */
reportRendererBoot(report: RendererBootReport): void
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from 'vitest'
import {
installDesktopDirectoryPickerBridge,
requestDesktopDirectory,
type DesktopDirectoryPickerWindow,
} from '../src/client/directory-picker.ts'
import { DESKTOP_DIRECTORY_PICKER_PATH } from '../src/directory-picker-contract.ts'
describe('desktop directory picker client bridge', () => {
it('returns a native path from the same-origin desktop route', async () => {
const request = vi.fn(async () => new Response(JSON.stringify({ path: 'C:\\Work' }), {
status: 200,
headers: { 'content-type': 'application/json' },
}))
await expect(requestDesktopDirectory(request)).resolves.toBe('C:\\Work')
expect(request).toHaveBeenCalledWith(DESKTOP_DIRECTORY_PICKER_PATH, {
method: 'POST',
headers: { accept: 'application/json' },
})
})
it('rejects invalid and failed route responses', async () => {
await expect(requestDesktopDirectory(async () => new Response('{}')))
.rejects.toThrow('invalid response')
await expect(requestDesktopDirectory(async () => new Response('', { status: 500 })))
.rejects.toThrow('could not open the system folder picker')
})
it('installs and restores the window bridge consumed by the browse panel', async () => {
const previous = vi.fn(async () => null)
const target = { __DSH_DESKTOP_PICK_DIRECTORY__: previous } as DesktopDirectoryPickerWindow
const request = vi.fn(async () => new Response(JSON.stringify({ path: null })))
const dispose = installDesktopDirectoryPickerBridge(target, request)
expect(target.__DSH_DESKTOP_PICK_DIRECTORY__).not.toBe(previous)
await expect(target.__DSH_DESKTOP_PICK_DIRECTORY__?.()).resolves.toBeNull()
dispose()
expect(target.__DSH_DESKTOP_PICK_DIRECTORY__).toBe(previous)
})
})
@@ -0,0 +1,79 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
import { describe, expect, it, vi } from 'vitest'
import { handleDesktopDirectoryPickerRequest } from '../src/directory-picker-route.ts'
function request(origin = 'http://127.0.0.1:43120', method = 'POST'): IncomingMessage {
return { method, headers: { origin } } as IncomingMessage
}
function response(): ServerResponse & {
body: string
end: ReturnType<typeof vi.fn>
setHeader: ReturnType<typeof vi.fn>
} {
const res = {
body: '',
statusCode: 200,
setHeader: vi.fn(),
end: vi.fn((body?: string) => { res.body = body ?? '' }),
}
return res as unknown as ServerResponse & typeof res
}
describe('desktop directory picker route', () => {
it('returns the path selected by the native desktop adapter', async () => {
const pick = vi.fn(async () => 'C:\\Work')
const res = response()
await handleDesktopDirectoryPickerRequest(
request(),
res,
'http://127.0.0.1:43120',
pick,
)
expect(pick).toHaveBeenCalledOnce()
expect(res.statusCode).toBe(200)
expect(res.setHeader).toHaveBeenCalledWith('content-type', 'application/json; charset=utf-8')
expect(JSON.parse(res.body)).toEqual({ path: 'C:\\Work' })
})
it('keeps cancellation distinct from route failure', async () => {
const res = response()
await handleDesktopDirectoryPickerRequest(
request(),
res,
'http://127.0.0.1:43120',
async () => null,
)
expect(res.statusCode).toBe(200)
expect(JSON.parse(res.body)).toEqual({ path: null })
})
it('rejects cross-origin and non-POST requests without opening a dialog', async () => {
const pick = vi.fn(async () => null)
for (const req of [request('https://example.com'), request(undefined, 'GET')]) {
const res = response()
await handleDesktopDirectoryPickerRequest(req, res, 'http://127.0.0.1:43120', pick)
expect(res.statusCode).toBe(req.method === 'GET' ? 405 : 403)
}
expect(pick).not.toHaveBeenCalled()
})
it('returns a stable error without exposing Electron details', async () => {
const res = response()
await handleDesktopDirectoryPickerRequest(
request(),
res,
'http://127.0.0.1:43120',
async () => { throw new Error('private native failure') },
)
expect(res.statusCode).toBe(500)
expect(JSON.parse(res.body)).toEqual({ error: 'native directory picker failed' })
})
})
@@ -58,6 +58,7 @@ const electron = vi.hoisted(() => {
let zoomLevel = 0
const dialog = {
showErrorBox: vi.fn(),
showOpenDialog: vi.fn(async () => ({ canceled: true, filePaths: [] as string[] })),
showMessageBox: vi.fn(async () => ({ response: 0, checkboxChecked: false })),
}
const appIcon = {
@@ -232,6 +233,7 @@ describe('Electron compatibility runtime', () => {
electron.loadURL.mockReset()
electron.loadURL.mockResolvedValue(undefined)
electron.dialog.showMessageBox.mockResolvedValue({ response: 0, checkboxChecked: false })
electron.dialog.showOpenDialog.mockResolvedValue({ canceled: true, filePaths: [] })
electron.shell.openPath.mockResolvedValue('')
electron.nativeTheme.themeSource = 'system'
electron.resetZoomLevel()
@@ -323,6 +325,26 @@ describe('Electron compatibility runtime', () => {
expect(electron.trays[0]?.off).toHaveBeenCalledWith('click', expect.any(Function))
})
it('opens one parented Windows folder chooser and returns its selected path', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
electron.dialog.showOpenDialog.mockResolvedValue({ canceled: false, filePaths: ['C:\\Work'] })
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
const runtime = new ElectronDesktopRuntime(async () => {})
const release = runtime.schedule(spec)
await runtime.mountScheduled()
await expect(runtime.pickDirectory()).resolves.toBe('C:\\Work')
expect(electron.dialog.showOpenDialog).toHaveBeenCalledWith(
electron.browserWindows[0],
{
title: 'Select Workspace Directory',
properties: ['openDirectory', 'dontAddToRecent'],
},
)
await release()
})
it('logs renderer crashes with the Windows exception code', async () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const { ElectronDesktopRuntime } = await import('../src/electron-runtime.ts')
+24
View File
@@ -118,6 +118,30 @@ describe('published package surface', () => {
expect(manifest.optionalDependencies ?? {}).not.toHaveProperty('dshmarket')
})
it('patches the browse panel with the Windows native-picker icon bridge', () => {
const patchPath = './patches/[email protected]'
expect(workspaceManifest.resolutions).toMatchObject({
'@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:0.1.0-rc.7': expect.stringContaining(patchPath),
'@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:^0.1.0-rc.7': expect.stringContaining(patchPath),
})
const patch = readFileSync(new URL(patchPath, workspaceRoot), 'utf8')
const installedClient = readFileSync(new URL(
'node_modules/@deepseek-ai/dsh-client-ui-directory-picker-browse/lib/client.js',
packageRoot,
), 'utf8')
for (const marker of [
'__DSH_DESKTOP_PICK_DIRECTORY__',
'IconFolderOpen16',
'nativePickerButton',
'browser.nativePicker',
'border:1px solid var(--dsw-alias-border-l2)',
'background:var(--dsw-alias-bg-layer-2)',
]) {
expect(patch).toContain(marker)
expect(installedClient).toContain(marker)
}
})
it('builds public Host plugins and their private native bootstraps', () => {
const config = readFileSync(new URL('tsdown.config.ts', packageRoot), 'utf8')
+38 -6
View File
@@ -16,6 +16,7 @@ import {
type Config as DesktopConfig,
type DesktopSettings,
} from '../src/index.ts'
import { DESKTOP_DIRECTORY_PICKER_PATH } from '../src/directory-picker-contract.ts'
import type { DesktopRuntime, DesktopShellSpec } from '../src/runtime.ts'
import { RENDERER_BOOT_REPORT_PATH, type RendererBootReport } from '../src/renderer-boot-contract.ts'
@@ -38,7 +39,8 @@ interface PluginHarness {
setLocalePreference: ReturnType<typeof vi.fn<(locale: LocaleId | undefined) => void>>
setThemeSource: ReturnType<typeof vi.fn<(source: ThemePreference) => void>>
rendererBoot: ReturnType<typeof vi.fn<(report: RendererBootReport) => void>>
rendererRoute(): WebRoute | undefined
pickDirectory: ReturnType<typeof vi.fn<() => Promise<string | null>>>
route(path: string): WebRoute | undefined
notify(next: DesktopSettings, prev: DesktopSettings): Promise<void>
notifyLocale(preference: LocaleId | undefined): void
notifyTheme(preference: ThemePreference): void
@@ -52,7 +54,8 @@ function createHarness(platform: DesktopRuntime['platform'] = 'darwin'): PluginH
const setLocalePreference = vi.fn<(locale: LocaleId | undefined) => void>()
const setThemeSource = vi.fn<(source: ThemePreference) => void>()
const rendererBoot = vi.fn<(report: RendererBootReport) => void>()
let rendererRoute: WebRoute | undefined
const pickDirectory = vi.fn(async () => null)
const routes = new Map<string, WebRoute>()
const settingsUpdated = new Set<(namespace: unknown, next: unknown) => void>()
let localePreference: LocaleId | undefined
let themePreference: ThemePreference = 'system'
@@ -79,6 +82,7 @@ function createHarness(platform: DesktopRuntime['platform'] = 'darwin'): PluginH
registerTrayItem: () => ({ refresh: () => {}, dispose: () => {} }),
openTerminal: () => {},
exportDiagnostics: async () => {},
pickDirectory,
reportRendererBoot: rendererBoot,
setLocalePreference,
setThemeSource,
@@ -107,8 +111,8 @@ function createHarness(platform: DesktopRuntime['platform'] = 'darwin'): PluginH
host: '127.0.0.1',
port: 43120,
register: vi.fn((route: WebRoute) => {
rendererRoute = route
return () => { if (rendererRoute === route) rendererRoute = undefined }
routes.set(route.path, route)
return () => { if (routes.get(route.path) === route) routes.delete(route.path) }
}),
},
settings,
@@ -129,7 +133,8 @@ function createHarness(platform: DesktopRuntime['platform'] = 'darwin'): PluginH
setLocalePreference,
setThemeSource,
rendererBoot,
rendererRoute: () => rendererRoute,
pickDirectory,
route: path => routes.get(path),
notify: async (next, prev) => { await watcher?.(next, prev) },
notifyLocale: (preference) => {
localePreference = preference
@@ -220,7 +225,7 @@ describe('desktop Host plugin', () => {
it('forwards same-origin renderer boot reports through the Host route', async () => {
const harness = createHarness()
apply(harness.ctx, config)
const route = harness.rendererRoute()
const route = harness.route(RENDERER_BOOT_REPORT_PATH)
expect(route).toEqual(expect.objectContaining({
kind: 'exact',
path: RENDERER_BOOT_REPORT_PATH,
@@ -242,6 +247,33 @@ describe('desktop Host plugin', () => {
expect(res.statusCode).toBe(204)
})
it('serves the Windows native picker through a same-origin desktop route', async () => {
const harness = createHarness('win32')
harness.pickDirectory.mockResolvedValue('C:\\Work')
apply(harness.ctx, config)
const route = harness.route(DESKTOP_DIRECTORY_PICKER_PATH)
expect(route).toEqual(expect.objectContaining({
kind: 'exact',
path: DESKTOP_DIRECTORY_PICKER_PATH,
}))
const req = {
method: 'POST',
headers: { origin: 'http://127.0.0.1:43120' },
} as unknown as IncomingMessage
let body = ''
const res = {
statusCode: 200,
setHeader: vi.fn(),
end: vi.fn((value?: string) => { body = value ?? '' }),
} as unknown as ServerResponse
await route?.handler(req, res)
expect(harness.pickDirectory).toHaveBeenCalledOnce()
expect(res.statusCode).toBe(200)
expect(JSON.parse(body)).toEqual({ path: 'C:\\Work' })
})
it.each(['win32', 'linux'] as const)(
'keeps the full-size application icon on %s',
(platform) => {
+1 -1
View File
@@ -319,7 +319,7 @@ describe('desktop profile composition', {
expect(() => readDesktopShellMode({ path })).toThrow('invalid settings document')
})
it('pins the Windows browse picker and desktop pwsh provider without replacing process boundaries', () => {
it('keeps the Windows browse panel and desktop pwsh provider without replacing process boundaries', () => {
const home = temporaryHome()
writeFileSync(join(home, 'cordis.patch.yml'), [
'- id: pwsh-sandbox',
@@ -9,6 +9,7 @@
"src/client/**/*.ts",
"src/client/**/*.tsx",
"tests/client-boot-health.spec.ts",
"tests/client-directory-picker.spec.ts",
"tests/client-environment.spec.ts"
]
}
+2
View File
@@ -17,6 +17,8 @@
"app-builder-lib@npm:26.15.3": "patch:app-builder-lib@npm%3A26.15.3#./patches/[email protected]",
"@deepseek-ai/dsh-sandbox-windows-acl@npm:0.1.0-rc.7": "patch:@deepseek-ai/dsh-sandbox-windows-acl@npm%3A0.1.0-rc.7#./patches/[email protected]",
"@deepseek-ai/dsh-sandbox-windows-acl@npm:^0.1.0-rc.7": "patch:@deepseek-ai/dsh-sandbox-windows-acl@npm%3A0.1.0-rc.7#./patches/[email protected]",
"@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:0.1.0-rc.7": "patch:@deepseek-ai/dsh-client-ui-directory-picker-browse@npm%3A0.1.0-rc.7#./patches/[email protected]",
"@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:^0.1.0-rc.7": "patch:@deepseek-ai/dsh-client-ui-directory-picker-browse@npm%3A0.1.0-rc.7#./patches/[email protected]",
"koffi@npm:^3.1.0": "3.1.5"
},
"dependenciesMeta": {
File diff suppressed because one or more lines are too long
+19 -1
View File
@@ -1131,7 +1131,7 @@ __metadata:
languageName: node
linkType: hard
"@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:0.1.0-rc.7, @deepseek-ai/dsh-client-ui-directory-picker-browse@npm:^0.1.0-rc.7":
"@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:0.1.0-rc.7":
version: 0.1.0-rc.7
resolution: "@deepseek-ai/dsh-client-ui-directory-picker-browse@npm:0.1.0-rc.7"
dependencies:
@@ -1149,6 +1149,24 @@ __metadata:
languageName: node
linkType: hard
"@deepseek-ai/dsh-client-ui-directory-picker-browse@patch:@deepseek-ai/dsh-client-ui-directory-picker-browse@npm%3A0.1.0-rc.7#./patches/[email protected]::locator=deepseek-harness-desktop%40workspace%3A.":
version: 0.1.0-rc.7
resolution: "@deepseek-ai/dsh-client-ui-directory-picker-browse@patch:@deepseek-ai/dsh-client-ui-directory-picker-browse@npm%3A0.1.0-rc.7#./patches/[email protected]::version=0.1.0-rc.7&hash=8f4c64&locator=deepseek-harness-desktop%40workspace%3A."
dependencies:
clsx: "npm:^2.0.0"
peerDependencies:
"@deepseek-ai/cordis": ^4.0.1
"@deepseek-ai/dsh-client-locale": ^0.1.0-rc.7
"@deepseek-ai/dsh-client-runtime": ^0.1.0-rc.7
"@deepseek-ai/dsh-client-ui-primitives": ^0.1.0-rc.7
"@deepseek-ai/dsh-client-ui-slots": ^0.1.0-rc.7
"@deepseek-ai/dsh-client-ui-workspace": ^0.1.0-rc.7
"@deepseek-ai/dsh-invariants": ^0.1.0-rc.7
react: ^18.2.0
checksum: 10c0/cd5b06cee91d6b16210ca75a6d25a53fc8c60bbdfd0ec20176f02f444aeb3d9b9eab71f4dd16381363f52ae2d6f618a79cf831713fc91d2a4299846891318e13
languageName: node
linkType: hard
"@deepseek-ai/dsh-client-ui-directory-picker-native@npm:0.1.0-rc.7, @deepseek-ai/dsh-client-ui-directory-picker-native@npm:^0.1.0-rc.7":
version: 0.1.0-rc.7
resolution: "@deepseek-ai/dsh-client-ui-directory-picker-native@npm:0.1.0-rc.7"