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:
Huanqi Cao
2026-08-03 00:06:47 +08:00
parent da1b1ff87d
commit 089f4dfad8
23 changed files with 1174 additions and 41 deletions

View File

@@ -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)