feat(picker): open the Win32 folder dialog in-process over koffi
The modern IFileOpenDialog becomes the primary win32 tier: a koffi-driven COM conversation on a worker_threads worker (the modal Show never blocks the host event loop), per-monitor-v2 DPI via SetThreadDpiAwarenessContext, and abort service by re-posting WM_CLOSE to the dialog thread's windows, with terminate+unref as the last resort (Node cannot interrupt a thread blocked in native code, and such a worker must never hold the process open). The PowerShell chain stays as the fallback tier with its trigger widened from ENOENT to any pwsh failure, closing the review-flagged PowerShell 6 regression (no WinForms: exit 1, not ENOENT, so 5.1 never ran). Layering keeps per-file coverage honest on every host: pure sequencing and the driver test against fakes anywhere; the bindings run against a mocked koffi COM world (the session-persistence-jsonl technique); POSIX hosts drive the real spawn plumbing to its koffi-load rejection; win32 hosts run a real open-and-abort-close smoke. The smoke joins processBoundTests: a worker blocked in a native modal wedges the threads pool's teardown, while a fork contains it. The worker bundles as its own CJS tsdown entry (workflow-workerthread's pattern; no TLA), and the host module is imported statically so the node-half bundle stays chunk-free. Built-plane and real-COM behavior verified on native Windows: standalone probes for the source worker, the built CJS worker, and the driver's abort path all open and close the real dialog. Agent Notes: new implemented/feature/2026-08-02-win32-in-process-folder-dialog (bilingual) owns the decision; the DPI note is re-scoped to the fallback tier it now describes and its AutoUpgradeEnabled attribution corrected (.NET Core 3.0 rewrote FolderBrowserDialog; the opt-out arrived in .NET 6).
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** Cross-platform native single-directory chooser behind the native backend's capability. */
|
||||
|
||||
import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command'
|
||||
import { pickWin32Directory } from './win32-dialog.ts'
|
||||
|
||||
/** Testable command boundary; native implementations never invoke a shell. */
|
||||
export type DirectoryPickerRunner = NativeCommandRunner
|
||||
@@ -9,6 +10,8 @@ export type DirectoryPickerRunner = NativeCommandRunner
|
||||
export interface DirectoryPickerInternals {
|
||||
platform?: NodeJS.Platform
|
||||
run?: DirectoryPickerRunner
|
||||
/** Replaces the in-process Win32 dialog (`pickWin32Directory`) for deterministic tests. */
|
||||
pickWin32Dialog?: (signal: AbortSignal) => Promise<string | null>
|
||||
}
|
||||
|
||||
function outputPath(stdout: string): string | null {
|
||||
@@ -64,13 +67,26 @@ export async function pickNativeDirectory(
|
||||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
// PowerShell 7 renders the modern IFileDialog folder picker, while Windows
|
||||
// PowerShell 5.1's FolderBrowserDialog is hardwired to the legacy
|
||||
// SHBrowseForFolder tree; prefer pwsh and fall back only when it is absent.
|
||||
// Both hosts spawn DPI-unaware, so the script opts the process into system
|
||||
// DPI awareness before any window is created. No Description is set: the
|
||||
// modern dialog renders it as a bottom strip and the classic dialog as an
|
||||
// unthemed box.
|
||||
// Primary: the in-process koffi-backed IFileOpenDialog worker — the modern
|
||||
// picker with per-monitor-v2 DPI, no PowerShell dependency, and abort
|
||||
// support. Any non-abort failure (koffi unavailable, ancient Windows, COM
|
||||
// refusal) falls back to the PowerShell chain below.
|
||||
const pickDialog = internals.pickWin32Dialog ?? pickWin32Directory
|
||||
try {
|
||||
return await pickDialog(signal)
|
||||
} catch (error: unknown) {
|
||||
rethrowIfAborted(signal, error)
|
||||
}
|
||||
|
||||
// PowerShell fallback: PowerShell 7 renders the modern IFileDialog folder
|
||||
// picker, while Windows PowerShell 5.1's FolderBrowserDialog is hardwired
|
||||
// to the legacy SHBrowseForFolder tree. Prefer pwsh, but ANY pwsh failure
|
||||
// falls back to 5.1 (which every Windows ships): a resolvable pwsh can
|
||||
// still be unable to deliver the dialog — PowerShell 6 has no WinForms,
|
||||
// so its Add-Type exits 1, not ENOENT. Both hosts spawn DPI-unaware, so
|
||||
// the script opts the process into system DPI awareness before any window
|
||||
// is created. No Description is set: the modern dialog renders it as a
|
||||
// bottom strip and the classic dialog as an unthemed box.
|
||||
const script = [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
"Add-Type -TypeDefinition 'using System; using System.Runtime.InteropServices; public static class DpiAware { [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); }'",
|
||||
@@ -89,7 +105,6 @@ export async function pickNativeDirectory(
|
||||
return outputPath(result.stdout)
|
||||
} catch (error: unknown) {
|
||||
rethrowIfAborted(signal, error)
|
||||
if (!isMissingCommand(error)) throw error
|
||||
}
|
||||
const result = await run('powershell.exe', ['-NoProfile', '-STA', '-Command', script], signal)
|
||||
return outputPath(result.stdout)
|
||||
|
||||
Reference in New Issue
Block a user