Files
deepseek-harness/packages/host/directory-picker-native/src/win32-dialog.ts
Huanqi Cao 089f4dfad8 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).
2026-08-05 00:31:43 +08:00

129 lines
5.1 KiB
TypeScript

/**
* Main-thread driver for the Win32 folder dialog: spawns the dialog worker
* (which blocks inside the modal `Show`), maps its message protocol onto a
* promise, and services aborts by posting `WM_CLOSE` to the dialog thread's
* windows until the worker reports back. The real worker/window surface is
* injectable so every driver path is testable on any platform.
*/
import { closeThreadWindows as hostCloseThreadWindows, spawnDialogWorker } from './win32-dialog-host.ts'
import type { Win32DialogWorkerData, Win32DialogWorkerMessage } from './win32-dialog-worker.ts'
/** The worker surface the driver drives (satisfied by `node:worker_threads`). */
export interface Win32DialogWorkerLike {
/**
* Subscribe to a worker event.
* @param event - `message`, `error`, or `exit`.
* @param listener - the event consumer.
*/
on(event: 'message', listener: (message: Win32DialogWorkerMessage) => void): unknown
on(event: 'error', listener: (error: Error) => void): unknown
on(event: 'exit', listener: (code: number) => void): unknown
/**
* Force-stop the worker; the abort path's last resort when `WM_CLOSE`
* never lands (e.g. the dialog window was never created).
* @returns settles when the thread is gone.
*/
terminate(): Promise<number>
/**
* Release the event-loop reference. Called once the pick settles so a
* worker stuck in the native modal call (terminate cannot interrupt
* native code) never blocks process exit.
*/
unref?(): void
}
/** Injectable process surface for deterministic driver tests. */
export interface Win32DialogInternals {
/** Replaces the real worker spawn (`win32-dialog-host.ts`). */
spawnWorker?: (data: Win32DialogWorkerData) => Win32DialogWorkerLike
/** Replaces the real `WM_CLOSE` poster (`win32-dialog-host.ts`). */
closeThreadWindows?: (threadId: number) => Promise<void>
/** Abort-service cadence override so tests never wait wall-clock time. */
closeRetryMs?: number
}
/** The dialog title every host shows. */
export const DIALOG_TITLE = 'Select Workspace Directory'
/** `WM_CLOSE` re-post cadence while an abort waits for the worker to unwind. */
const CLOSE_RETRY_MS = 150
/** Abort-service attempts before force-terminating the worker. */
const CLOSE_MAX_ATTEMPTS = 20
/**
* Open the modern Win32 folder picker off the event loop.
* @param signal - caller lifetime; abort closes the dialog and rejects.
* @param internals - worker/window seams for deterministic tests.
* @returns the selected path, or null when the user cancels.
*/
export async function pickWin32Directory(
signal: AbortSignal,
internals: Win32DialogInternals = {},
): Promise<string | null> {
if (signal.aborted) throw new Error('native directory picker aborted')
const spawnWorker = internals.spawnWorker ?? spawnDialogWorker
const closeWindows = internals.closeThreadWindows ?? hostCloseThreadWindows
const closeRetryMs = internals.closeRetryMs ?? CLOSE_RETRY_MS
const worker = spawnWorker({ title: DIALOG_TITLE })
let dialogThreadId: number | undefined
let closeTimer: NodeJS.Timeout | undefined
let settled = false
return await new Promise<string | null>((resolve, reject) => {
const settle = (outcome: () => void): void => {
if (settled) return
settled = true
if (closeTimer !== undefined) clearInterval(closeTimer)
signal.removeEventListener('abort', onAbort)
worker.unref?.()
outcome()
}
const serviceAbort = (): void => {
let attempts = 0
// The `showing` notice precedes the blocking `Show`, so the very first
// WM_CLOSE can race the window's creation; re-post until the worker
// reports back, then force-terminate as a last resort.
closeTimer = setInterval(() => {
attempts += 1
if (attempts > CLOSE_MAX_ATTEMPTS) {
settle(() => {
void worker.terminate()
reject(new Error('native directory picker aborted (dialog unresponsive; worker terminated)'))
})
return
}
void closeWindows(dialogThreadId as number).catch(() => undefined)
}, closeRetryMs)
void closeWindows(dialogThreadId as number).catch(() => undefined)
}
const onAbort = (): void => {
if (dialogThreadId !== undefined) serviceAbort()
// Not shown yet: the `showing` handler below starts the service loop.
}
signal.addEventListener('abort', onAbort, { once: true })
worker.on('message', (message: Win32DialogWorkerMessage) => {
switch (message.kind) {
case 'showing':
dialogThreadId = message.threadId
if (signal.aborted) serviceAbort()
return
case 'done':
settle(() => {
if (signal.aborted) reject(new Error('native directory picker aborted'))
else resolve(message.path)
})
return
case 'error':
settle(() =>{ reject(new Error(`win32 folder dialog failed: ${message.message}`)) })
}
})
worker.on('error', (error: Error) =>{ settle(() =>{ reject(error) }) })
worker.on('exit', () =>{ settle(() =>{ reject(new Error('win32 folder dialog worker exited before reporting a result')) }) })
})
}