From bc9772ffc6c1200abfee97a42fc18e01a2d0262f Mon Sep 17 00:00:00 2001 From: Qiuner Date: Tue, 18 Aug 2026 10:47:37 +0800 Subject: [PATCH] feat(desktop): add Windows workspace folder picker --- .../src/client/directory-picker.ts | 49 ++++++ dsh-plugin-desktop/src/client/index.ts | 7 + .../src/directory-picker-contract.ts | 8 + .../src/directory-picker-route.ts | 27 +++ dsh-plugin-desktop/src/electron-runtime.ts | 28 +++ dsh-plugin-desktop/src/index.ts | 20 +++ dsh-plugin-desktop/src/runtime.ts | 3 + .../tests/client-directory-picker.spec.ts | 41 +++++ .../tests/directory-picker-route.spec.ts | 79 +++++++++ .../tests/electron-runtime.spec.ts | 22 +++ dsh-plugin-desktop/tests/package.spec.ts | 24 +++ dsh-plugin-desktop/tests/plugin.spec.ts | 44 ++++- dsh-plugin-desktop/tests/profile.spec.ts | 2 +- dsh-plugin-desktop/tsconfig.tests.client.json | 1 + package.json | 2 + ...i-directory-picker-browse@0.1.0-rc.7.patch | 165 ++++++++++++++++++ yarn.lock | 20 ++- 17 files changed, 534 insertions(+), 8 deletions(-) create mode 100644 dsh-plugin-desktop/src/client/directory-picker.ts create mode 100644 dsh-plugin-desktop/src/directory-picker-contract.ts create mode 100644 dsh-plugin-desktop/src/directory-picker-route.ts create mode 100644 dsh-plugin-desktop/tests/client-directory-picker.spec.ts create mode 100644 dsh-plugin-desktop/tests/directory-picker-route.spec.ts create mode 100644 patches/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch diff --git a/dsh-plugin-desktop/src/client/directory-picker.ts b/dsh-plugin-desktop/src/client/directory-picker.ts new file mode 100644 index 0000000000..7003c304a0 --- /dev/null +++ b/dsh-plugin-desktop/src/client/directory-picker.ts @@ -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 +} + +type DirectoryPickerRequest = ( + input: RequestInfo | URL, + init?: RequestInit, +) => Promise + +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 { + 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 => 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 + } +} diff --git a/dsh-plugin-desktop/src/client/index.ts b/dsh-plugin-desktop/src/client/index.ts index a424890553..de653a3d65 100644 --- a/dsh-plugin-desktop/src/client/index.ts +++ b/dsh-plugin-desktop/src/client/index.ts @@ -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) } diff --git a/dsh-plugin-desktop/src/directory-picker-contract.ts b/dsh-plugin-desktop/src/directory-picker-contract.ts new file mode 100644 index 0000000000..1bc0d5d8a5 --- /dev/null +++ b/dsh-plugin-desktop/src/directory-picker-contract.ts @@ -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 +} diff --git a/dsh-plugin-desktop/src/directory-picker-route.ts b/dsh-plugin-desktop/src/directory-picker-route.ts new file mode 100644 index 0000000000..69c9e198b4 --- /dev/null +++ b/dsh-plugin-desktop/src/directory-picker-route.ts @@ -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, + reportError: (cause: unknown) => void = () => {}, +): Promise { + 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' }) + } +} diff --git a/dsh-plugin-desktop/src/electron-runtime.ts b/dsh-plugin-desktop/src/electron-runtime.ts index 4e845934cc..90083d0f10 100644 --- a/dsh-plugin-desktop/src/electron-runtime.ts +++ b/dsh-plugin-desktop/src/electron-runtime.ts @@ -110,6 +110,7 @@ export class ElectronDesktopRuntime implements DesktopRuntime { private readonly trayItems = new Map() private terminalSpec: DesktopTerminalSpec | undefined private diagnosticExport: Promise | undefined + private directoryPickTask: Promise | undefined private rendererBootReported = false constructor( @@ -181,6 +182,33 @@ export class ElectronDesktopRuntime implements DesktopRuntime { window.focus() } + /** @inheritdoc */ + async pickDirectory(): Promise { + 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 { + 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() diff --git a/dsh-plugin-desktop/src/index.ts b/dsh-plugin-desktop/src/index.ts index 8c98f7f06f..c5c7fcc7a8 100644 --- a/dsh-plugin-desktop/src/index.ts +++ b/dsh-plugin-desktop/src/index.ts @@ -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 | undefined const stopWatching = settings.watch((next) => { diff --git a/dsh-plugin-desktop/src/runtime.ts b/dsh-plugin-desktop/src/runtime.ts index 43aacccd77..26c9d6ae1c 100644 --- a/dsh-plugin-desktop/src/runtime.ts +++ b/dsh-plugin-desktop/src/runtime.ts @@ -181,6 +181,9 @@ export interface DesktopRuntime { /** Export a diagnostics zip and reveal it in the system file manager. */ exportDiagnostics(): Promise + /** Open the desktop operating system's native workspace-folder chooser. */ + pickDirectory(): Promise + /** Accept the terminal client Loader outcome for the mounted generation. */ reportRendererBoot(report: RendererBootReport): void diff --git a/dsh-plugin-desktop/tests/client-directory-picker.spec.ts b/dsh-plugin-desktop/tests/client-directory-picker.spec.ts new file mode 100644 index 0000000000..542fb654e7 --- /dev/null +++ b/dsh-plugin-desktop/tests/client-directory-picker.spec.ts @@ -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) + }) +}) diff --git a/dsh-plugin-desktop/tests/directory-picker-route.spec.ts b/dsh-plugin-desktop/tests/directory-picker-route.spec.ts new file mode 100644 index 0000000000..0b17e39d49 --- /dev/null +++ b/dsh-plugin-desktop/tests/directory-picker-route.spec.ts @@ -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 + setHeader: ReturnType +} { + 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' }) + }) +}) diff --git a/dsh-plugin-desktop/tests/electron-runtime.spec.ts b/dsh-plugin-desktop/tests/electron-runtime.spec.ts index 91cf6f4c47..4f4ee9f61f 100644 --- a/dsh-plugin-desktop/tests/electron-runtime.spec.ts +++ b/dsh-plugin-desktop/tests/electron-runtime.spec.ts @@ -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') diff --git a/dsh-plugin-desktop/tests/package.spec.ts b/dsh-plugin-desktop/tests/package.spec.ts index 4d84b19521..b9dee0b1ba 100644 --- a/dsh-plugin-desktop/tests/package.spec.ts +++ b/dsh-plugin-desktop/tests/package.spec.ts @@ -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/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch' + 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') diff --git a/dsh-plugin-desktop/tests/plugin.spec.ts b/dsh-plugin-desktop/tests/plugin.spec.ts index 4657f1403e..1c548d93a6 100644 --- a/dsh-plugin-desktop/tests/plugin.spec.ts +++ b/dsh-plugin-desktop/tests/plugin.spec.ts @@ -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 void>> setThemeSource: ReturnType void>> rendererBoot: ReturnType void>> - rendererRoute(): WebRoute | undefined + pickDirectory: ReturnType Promise>> + route(path: string): WebRoute | undefined notify(next: DesktopSettings, prev: DesktopSettings): Promise 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() 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) => { diff --git a/dsh-plugin-desktop/tests/profile.spec.ts b/dsh-plugin-desktop/tests/profile.spec.ts index a4289183d5..be06a4ecb8 100644 --- a/dsh-plugin-desktop/tests/profile.spec.ts +++ b/dsh-plugin-desktop/tests/profile.spec.ts @@ -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', diff --git a/dsh-plugin-desktop/tsconfig.tests.client.json b/dsh-plugin-desktop/tsconfig.tests.client.json index b062d4eac5..92a3f4533d 100644 --- a/dsh-plugin-desktop/tsconfig.tests.client.json +++ b/dsh-plugin-desktop/tsconfig.tests.client.json @@ -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" ] } diff --git a/package.json b/package.json index 202dc827d8..8748f20131 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "app-builder-lib@npm:26.15.3": "patch:app-builder-lib@npm%3A26.15.3#./patches/app-builder-lib@26.15.3.patch", "@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/dsh-sandbox-windows-acl@0.1.0-rc.7.patch", "@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/dsh-sandbox-windows-acl@0.1.0-rc.7.patch", + "@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/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch", + "@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/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch", "koffi@npm:^3.1.0": "3.1.5" }, "dependenciesMeta": { diff --git a/patches/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch b/patches/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch new file mode 100644 index 0000000000..f7935a39e8 --- /dev/null +++ b/patches/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch @@ -0,0 +1,165 @@ +diff --git a/lib/client.js b/lib/client.js +index 88da094ba773c260d687e428f8888c4433d31d9f..572b5f62acdb915866908f4c8a9d1aa1d8941ed9 100644 +--- a/lib/client.js ++++ b/lib/client.js +@@ -24,7 +24,7 @@ window.__ModuleLoader__.load({ + } + //#endregion + //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-directory-picker-browse/src/client/DirectoryBrowser.module.css.mjs +- const css = ".ZuhsRW_dialog.ZuhsRW_dialog{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);gap:0;width:min(680px,100%);height:min(500px,100dvh - 32px);padding:0}.ZuhsRW_editorScope{display:contents}.ZuhsRW_header{border-bottom:1px solid var(--dsw-alias-border-l3);flex-direction:column;flex:none;gap:8px;padding:16px 14px 8px 24px;display:flex}.ZuhsRW_title{min-height:28px;color:var(--dsw-alias-label-primary);align-items:flex-end;margin:0;font-size:16px;font-weight:510;line-height:24px;display:flex}.ZuhsRW_crumbBar{box-sizing:border-box;border:1px solid #0000;border-radius:8px;align-items:center;gap:4px;min-height:24px;margin-left:-9px;padding:0 8px;display:flex}.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:enabled:hover),.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:focus-visible),.ZuhsRW_crumbBar:has(.ZuhsRW_pathInput){border-color:var(--dsw-alias-border-l2)}.ZuhsRW_millerRow{scrollbar-width:none;flex:1 1 0;align-items:stretch;gap:12px;min-height:0;display:flex;overflow-x:auto}.ZuhsRW_crumbTrail{scrollbar-width:none;flex:0 auto;align-items:center;gap:4px;min-width:0;display:flex;overflow-x:auto}.ZuhsRW_crumbSeat{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.ZuhsRW_crumb{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_crumb:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone{cursor:text;background:0 0;border:none;outline:none;flex:1 0 34px;justify-content:flex-end;align-items:center;min-width:34px;height:22px;padding:0;display:flex}.ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone:enabled:hover .ZuhsRW_crumbEditGlyph,.ZuhsRW_crumbEditZone:focus-visible .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbEditZone:disabled{cursor:default}.ZuhsRW_crumbEditZone:disabled .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-caption)}.ZuhsRW_pathInput{box-sizing:border-box;min-width:0;height:22px;color:var(--dsw-alias-label-primary);background:0 0;border:none;outline:none;flex:1 1 0;padding:0;font-size:13px;line-height:20px}.ZuhsRW_content{flex-direction:column;flex:1 1 0;min-height:0;padding:16px 16px 16px 24px;display:flex;position:relative}.ZuhsRW_column{flex-direction:column;flex:1 1 0;gap:2px;min-width:256px;padding-right:8px;display:flex;overflow-y:auto}.ZuhsRW_divider{background:var(--dsw-alias-border-l3);flex:none;width:1px}.ZuhsRW_rowSeat{flex:none;display:flex}.ZuhsRW_row{text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;align-items:center;gap:4px;width:100%;height:28px;padding:4px;display:flex}.ZuhsRW_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.ZuhsRW_rowSelected,.ZuhsRW_rowSelected:hover{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}.ZuhsRW_rowIcon{color:var(--dsw-alias-label-secondary);flex:none}.ZuhsRW_rowIconSelected{color:var(--dsw-alias-button-info-fill);flex:none}.ZuhsRW_rowName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:1 1 0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_rowChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_status,.ZuhsRW_error{padding:4px 120px 4px 4px;font-size:12px;line-height:18px}.ZuhsRW_status{color:var(--dsw-alias-label-secondary)}.ZuhsRW_error{color:var(--dsw-alias-state-error-primary)}.ZuhsRW_loadingFloat{background:var(--dsw-alias-bg-layer-2);padding:2px 8px;position:absolute;bottom:8px;right:16px}.ZuhsRW_footerBar{border-top:1px solid var(--dsw-alias-border-l3);flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:16px 24px;display:flex}.ZuhsRW_showHiddenToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.ZuhsRW_showHiddenToggle:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_showHiddenToggle:disabled{color:var(--dsw-alias-label-caption);cursor:default}.ZuhsRW_showHiddenToggleActive{color:var(--dsw-alias-label-primary)}.ZuhsRW_footerGap{flex:1 1 0}.ZuhsRW_footerAction{min-width:72px}.ZuhsRW_createDialog.ZuhsRW_createDialog{gap:0;width:min(380px,100%);padding:0}.ZuhsRW_createBody{flex-direction:column;gap:12px;padding:22px 24px 20px;display:flex}.ZuhsRW_createTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:510;line-height:24px}.ZuhsRW_createIn{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}.ZuhsRW_createInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;line-height:22px}.ZuhsRW_createInput::placeholder{color:var(--dsw-alias-label-caption)}.ZuhsRW_createActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:8px;display:flex}"; ++ const css = ".ZuhsRW_dialog.ZuhsRW_dialog{--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);gap:0;width:min(680px,100%);height:min(500px,100dvh - 32px);padding:0}.ZuhsRW_editorScope{display:contents}.ZuhsRW_header{border-bottom:1px solid var(--dsw-alias-border-l3);flex-direction:column;flex:none;gap:8px;padding:16px 14px 8px 24px;display:flex}.ZuhsRW_title{min-height:28px;color:var(--dsw-alias-label-primary);align-items:flex-end;margin:0;font-size:16px;font-weight:510;line-height:24px;display:flex}.ZuhsRW_crumbBar{box-sizing:border-box;border:1px solid #0000;border-radius:8px;align-items:center;gap:4px;min-height:24px;margin-left:-9px;padding:0 8px;display:flex}.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:enabled:hover),.ZuhsRW_crumbBar:has(.ZuhsRW_crumbEditZone:focus-visible),.ZuhsRW_crumbBar:has(.ZuhsRW_pathInput){border-color:var(--dsw-alias-border-l2)}.ZuhsRW_millerRow{scrollbar-width:none;flex:1 1 0;align-items:stretch;gap:12px;min-height:0;display:flex;overflow-x:auto}.ZuhsRW_crumbTrail{scrollbar-width:none;flex:0 auto;align-items:center;gap:4px;min-width:0;display:flex;overflow-x:auto}.ZuhsRW_crumbSeat{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.ZuhsRW_crumb{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_crumb:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone{cursor:text;background:0 0;border:none;outline:none;flex:1 0 34px;justify-content:flex-end;align-items:center;min-width:34px;height:22px;padding:0;display:flex}.ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_crumbEditZone:enabled:hover .ZuhsRW_crumbEditGlyph,.ZuhsRW_crumbEditZone:focus-visible .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-primary)}.ZuhsRW_crumbEditZone:disabled{cursor:default}.ZuhsRW_crumbEditZone:disabled .ZuhsRW_crumbEditGlyph{color:var(--dsw-alias-label-caption)}.ZuhsRW_pathInput{box-sizing:border-box;min-width:0;height:22px;color:var(--dsw-alias-label-primary);background:0 0;border:none;outline:none;flex:1 1 0;padding:0;font-size:13px;line-height:20px}.ZuhsRW_content{flex-direction:column;flex:1 1 0;min-height:0;padding:16px 16px 16px 24px;display:flex;position:relative}.ZuhsRW_column{flex-direction:column;flex:1 1 0;gap:2px;min-width:256px;padding-right:8px;display:flex;overflow-y:auto}.ZuhsRW_divider{background:var(--dsw-alias-border-l3);flex:none;width:1px}.ZuhsRW_rowSeat{flex:none;display:flex}.ZuhsRW_row{text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;align-items:center;gap:4px;width:100%;height:28px;padding:4px;display:flex}.ZuhsRW_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.ZuhsRW_rowSelected,.ZuhsRW_rowSelected:hover{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}.ZuhsRW_rowIcon{color:var(--dsw-alias-label-secondary);flex:none}.ZuhsRW_rowIconSelected{color:var(--dsw-alias-button-info-fill);flex:none}.ZuhsRW_rowName{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:1 1 0;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.ZuhsRW_rowChevron{color:var(--dsw-alias-label-tertiary);flex:none}.ZuhsRW_status,.ZuhsRW_error{padding:4px 120px 4px 4px;font-size:12px;line-height:18px}.ZuhsRW_status{color:var(--dsw-alias-label-secondary)}.ZuhsRW_error{color:var(--dsw-alias-state-error-primary)}.ZuhsRW_loadingFloat{background:var(--dsw-alias-bg-layer-2);padding:2px 8px;position:absolute;bottom:8px;right:16px}.ZuhsRW_footerBar{border-top:1px solid var(--dsw-alias-border-l3);flex-wrap:wrap;flex:none;align-items:center;gap:8px;padding:16px 24px;display:flex}.ZuhsRW_nativePickerButton{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;align-items:center;justify-content:center;width:28px;height:28px;padding:0;display:inline-flex}.ZuhsRW_nativePickerButton:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.ZuhsRW_nativePickerButton:disabled{color:var(--dsw-alias-label-caption);cursor:default;background:transparent}.ZuhsRW_showHiddenToggle{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;align-items:center;gap:4px;padding:0;font-size:13px;font-weight:500;line-height:20px;display:inline-flex}.ZuhsRW_showHiddenToggle:hover{color:var(--dsw-alias-label-primary)}.ZuhsRW_showHiddenToggle:disabled{color:var(--dsw-alias-label-caption);cursor:default}.ZuhsRW_showHiddenToggleActive{color:var(--dsw-alias-label-primary)}.ZuhsRW_footerGap{flex:1 1 0}.ZuhsRW_footerAction{min-width:72px}.ZuhsRW_createDialog.ZuhsRW_createDialog{gap:0;width:min(380px,100%);padding:0}.ZuhsRW_createBody{flex-direction:column;gap:12px;padding:22px 24px 20px;display:flex}.ZuhsRW_createTitle{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:510;line-height:24px}.ZuhsRW_createIn{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}.ZuhsRW_createInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;line-height:22px}.ZuhsRW_createInput::placeholder{color:var(--dsw-alias-label-caption)}.ZuhsRW_createActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:8px;display:flex}"; + const tagId = "@deepseek-ai/dsh-client-ui-directory-picker-browse/DirectoryBrowser.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) { + const tag = document.createElement("style"); +@@ -31,5 +31,5 @@ window.__ModuleLoader__.load({ + tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-directory-picker-browse"; + tag.dataset.pluginCss = tagId; +- tag.textContent = css; ++ tag.textContent = css + ".ZuhsRW_nativePickerButton{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2)}"; + document.head.appendChild(tag); + } +@@ -36,6 +36,7 @@ window.__ModuleLoader__.load({ + var DirectoryBrowser_module_css_default = { + "createActions": "ZuhsRW_createActions", + "footerBar": "ZuhsRW_footerBar", ++ "nativePickerButton": "ZuhsRW_nativePickerButton", + "crumbBar": "ZuhsRW_crumbBar", + "pathInput": "ZuhsRW_pathInput", + "title": "ZuhsRW_title", +@@ -279,7 +280,7 @@ window.__ModuleLoader__.load({ + * @param props - owner-controlled browser props. + * @returns the dialog element (null while closed, via Modal). + */ +- function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen, onClose, busy, t }) { ++ function DirectoryBrowser({ open, listDirectory, createDirectory, pickNativeDirectory, onOpen, onClose, busy, t }) { + const [parent, setParent] = (0, react.useState)(null); + const [selected, setSelected] = (0, react.useState)(null); + const [child, setChild] = (0, react.useState)(null); +@@ -292,6 +293,7 @@ window.__ModuleLoader__.load({ + const [folderDraft, setFolderDraft] = (0, react.useState)(null); + const [creatingFolder, setCreatingFolder] = (0, react.useState)(false); + const [createError, setCreateError] = (0, react.useState)(null); ++ const [nativePicking, setNativePicking] = (0, react.useState)(false); + const requestSeq = (0, react.useRef)(0); + const scanController = (0, react.useRef)(null); + const openGeneration = (0, react.useRef)(0); +@@ -669,12 +671,24 @@ window.__ModuleLoader__.load({ + }); + if (!open) return null; + const twoPane = selected !== null; +- const parentInert = busy || folderDraft !== null; ++ const parentInert = busy || folderDraft !== null || nativePicking; ++ const pickFromSystem = () => { ++ if (pickNativeDirectory === void 0) return; ++ setError(null); ++ setNativePicking(true); ++ pickNativeDirectory().then((path) => { ++ setNativePicking(false); ++ if (path !== null) onOpen(path); ++ }, (reason) => { ++ setNativePicking(false); ++ setError(failureText(reason)); ++ }); ++ }; + const draftPending = pathDraft !== null; + return (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, { + open, + onClose: () => { +- if (folderDraft === null && !busy) onClose(); ++ if (!parentInert) onClose(); + }, + title: t("browser.title"), + className: clsx(DirectoryBrowser_module_css_default.dialog), +@@ -832,6 +846,19 @@ window.__ModuleLoader__.load({ + }, + children: t("browser.newFolder") + }), ++ pickNativeDirectory !== void 0 && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, { ++ label: t("browser.nativePicker"), ++ side: "top", ++ children: (0, react_jsx_runtime.jsx)("button", { ++ type: "button", ++ className: DirectoryBrowser_module_css_default.nativePickerButton, ++ "aria-label": t("browser.nativePicker"), ++ "aria-busy": nativePicking || void 0, ++ disabled: parentInert, ++ onClick: pickFromSystem, ++ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, { size: 16 }) ++ }) ++ }), + (0, react_jsx_runtime.jsxs)("button", { + type: "button", + className: clsx(DirectoryBrowser_module_css_default.showHiddenToggle, showHidden && DirectoryBrowser_module_css_default.showHiddenToggleActive), +@@ -955,6 +982,7 @@ window.__ModuleLoader__.load({ + busy: props.busy, + listDirectory: props.listDirectory, + createDirectory: props.createDirectory, ++ pickNativeDirectory: props.pickNativeDirectory, + t: props.t, + onOpen: props.onPicked, + onClose: props.onCancel +@@ -992,7 +1020,8 @@ window.__ModuleLoader__.load({ + "browser.editPath": "编辑路径", + "browser.loading": "加载中…", + "browser.truncated": "文件夹过多,仅显示开头部分。", +- "browser.showHidden": "显示隐藏文件" ++ "browser.showHidden": "显示隐藏文件", ++ "browser.nativePicker": "使用 Windows 选择文件夹" + }], ["en", { + "browser.title": "Select Workspace Directory", + "browser.home": "Home", +@@ -1006,7 +1035,8 @@ window.__ModuleLoader__.load({ + "browser.editPath": "Edit path", + "browser.loading": "Loading…", + "browser.truncated": "Too many folders to list; only the beginning is shown.", +- "browser.showHidden": "Show hidden files" ++ "browser.showHidden": "Show hidden files", ++ "browser.nativePicker": "Choose with Windows" + }]]; + try { + for (const [locale, dict] of dictionaries) disposers.push(ctx.locale.register(LOCALE_NS, locale, dict)); +@@ -1021,6 +1051,11 @@ window.__ModuleLoader__.load({ + const injected = () => ({ + listDirectory: (path, signal) => ctx.workspaces.listDirectory(path, signal), + createDirectory: (path, name) => ctx.workspaces.createDirectory(path, name), ++ pickNativeDirectory: new URLSearchParams(window.location.search).get("dsh-desktop-platform") === "win32" ? () => { ++ const pick = window.__DSH_DESKTOP_PICK_DIRECTORY__; ++ if (typeof pick !== "function") throw new Error("DSH Desktop native directory picker is unavailable"); ++ return pick(); ++ } : void 0, + t: ctx.locale.bind(LOCALE_NS) + }); + ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => ctx.slots.inject("sidebar.workspaces.directoryFlow", function* () { +diff --git a/lib/types/client/DirectoryBrowser.d.ts b/lib/types/client/DirectoryBrowser.d.ts +index 51fcc428ab687d2703c8befdcf312469b047ccc4..186592f0d21156ae7c115250c77594cd6cf54759 100644 +--- a/lib/types/client/DirectoryBrowser.d.ts ++++ b/lib/types/client/DirectoryBrowser.d.ts +@@ -8,6 +8,8 @@ export interface DirectoryBrowserProps { + listDirectory: (path?: string, signal?: AbortSignal) => Promise; + /** Create one child directory under an existing parent. */ + createDirectory: (path: string, name: string) => Promise; ++ /** Optional native folder chooser contributed by a local desktop shell. */ ++ pickNativeDirectory?: () => Promise; + /** The operator confirmed a directory (the selection, else the listed level). */ + onOpen: (path: string) => void; + /** Close without picking (mask, Escape, Cancel). */ +@@ -22,5 +24,5 @@ export interface DirectoryBrowserProps { + * @param props - owner-controlled browser props. + * @returns the dialog element (null while closed, via Modal). + */ +-export declare function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen, onClose, busy, t }: DirectoryBrowserProps): import("react").JSX.Element | null; ++export declare function DirectoryBrowser({ open, listDirectory, createDirectory, pickNativeDirectory, onOpen, onClose, busy, t }: DirectoryBrowserProps): import("react").JSX.Element | null; + //# sourceMappingURL=DirectoryBrowser.d.ts.map +diff --git a/lib/types/client/flow.d.ts b/lib/types/client/flow.d.ts +index 689bb216f133826c2d4d4454645d643c81b48847..008f1727d4b8d5bfad646d46c7f9706bf41f3e42 100644 +--- a/lib/types/client/flow.d.ts ++++ b/lib/types/client/flow.d.ts +@@ -8,6 +8,8 @@ export interface BrowseFlowInjected { + listDirectory: (path?: string, signal?: AbortSignal) => Promise; + /** Create one child directory under an existing parent. */ + createDirectory: (path: string, name: string) => Promise; ++ /** Optional native folder chooser contributed by a local desktop shell. */ ++ pickNativeDirectory?: () => Promise; + /** Localized dialog copy (this package's namespace). */ + t: Translate; + } diff --git a/yarn.lock b/yarn.lock index 22d712cbe5..05b2e52b7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch::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/dsh-client-ui-directory-picker-browse@0.1.0-rc.7.patch::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"