@deepseek-ai/dsh-host-directory-picker-native 0.1.3-alpha.2 → 0.1.5-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +2 -1
- package/README.zh.md +2 -1
- package/lib/index.js +7 -4
- package/lib/types/index.d.ts +3 -1
- package/lib/types/win32-dialog-host.d.ts +4 -3
- package/lib/types/win32-dialog-logic.d.ts +14 -0
- package/lib/types/win32-dialog-worker.d.ts +6 -3
- package/lib/worker.cjs +21 -3
- package/package.json +3 -3
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/host/directory-picker-native/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 3f980831848965b976e32b0e5ef25abb77fe4e6e
|
|
6
|
+
README.zh.md: 66f77a0b715aee8b4087dd03d4d4eea69d39bb09
|
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The backend is a thin service over a platform chooser: `NativeDirectoryPicker` r
|
|
|
53
53
|
|
|
54
54
|
### Platform mechanics
|
|
55
55
|
|
|
56
|
-
Platform tools run without a shell: `osascript` on macOS, and Zenity with a KDialog fallback on Linux; the caller's abort terminates the native process. Windows opens the modern `IFileOpenDialog` in a spawned child process — a koffi-driven COM conversation on the child's main thread with the best thread DPI awareness the host accepts (per-monitor-v2 first), aborted by posting `WM_CLOSE` to the dialog thread.
|
|
56
|
+
Platform tools run without a shell: `osascript` on macOS, and Zenity with a KDialog fallback on Linux; the caller's abort terminates the native process. Windows opens the modern `IFileOpenDialog` in a spawned child process — a koffi-driven COM conversation on the child's main thread with the best thread DPI awareness the host accepts (per-monitor-v2 first), aborted by posting `WM_CLOSE` to the dialog thread. Immediately before `Show`, the child synthesizes one Alt press through `keybd_event`, which lets the dialog activate as the foreground window even when a background host process spawned the child.
|
|
57
57
|
|
|
58
58
|
### Source map
|
|
59
59
|
|
|
@@ -98,6 +98,7 @@ These limits define when the native interaction is unavailable or fragile. They
|
|
|
98
98
|
|
|
99
99
|
- **Linux requires desktop tooling** — with neither Zenity nor KDialog installed, `pick` rejects with an actionable error; it does not fall back to a typed-path prompt (the browse backend is that fallback at the composition level).
|
|
100
100
|
- **Windows has no mechanism fallback** — the child-process picker through packaged koffi is the only native tier, so a COM refusal or dialog crash surfaces the failure; the browse backend remains the fallback at the composition level.
|
|
101
|
+
- **Windows foreground grant relies on injected input** — the child synthesizes an Alt press before `Show` so the dialog can take the foreground from a background host; where synthesized input is suppressed (secure desktops, restricted remote sessions, an elevated foreground window), the dialog may still open behind other windows. The technique is validated on Windows 11 only.
|
|
101
102
|
|
|
102
103
|
<a id="dev-note"></a>
|
|
103
104
|
### Dev Note
|
package/README.zh.md
CHANGED
|
@@ -53,7 +53,7 @@ kind: "package-reference"
|
|
|
53
53
|
|
|
54
54
|
### 平台机制
|
|
55
55
|
|
|
56
|
-
平台工具不经 shell 调用:macOS 使用 `osascript`,Linux 使用 Zenity 并以 KDialog 回退;调用方的中止信号会终止原生进程。Windows 在 spawn 的子进程中打开现代 `IFileOpenDialog`——由 koffi 在子进程主线程上驱动的 COM 会话,采用宿主接受的最佳线程 DPI 感知(优先 per-monitor-v2),中止时向对话框线程投递 `WM_CLOSE
|
|
56
|
+
平台工具不经 shell 调用:macOS 使用 `osascript`,Linux 使用 Zenity 并以 KDialog 回退;调用方的中止信号会终止原生进程。Windows 在 spawn 的子进程中打开现代 `IFileOpenDialog`——由 koffi 在子进程主线程上驱动的 COM 会话,采用宿主接受的最佳线程 DPI 感知(优先 per-monitor-v2),中止时向对话框线程投递 `WM_CLOSE`。在 `Show` 之前,子进程立即通过 `keybd_event` 合成一次 Alt 按键,让对话框即使由后台宿主进程 spawn 也能激活为前台窗口。
|
|
57
57
|
|
|
58
58
|
### 源码地图
|
|
59
59
|
|
|
@@ -98,6 +98,7 @@ kind: "package-reference"
|
|
|
98
98
|
|
|
99
99
|
- **Linux 依赖桌面工具**——Zenity 与 KDialog 均未安装时,`pick` 以包含解决建议的错误拒绝;它不会回退为手输路径提示(组合层面的回退是浏览后端)。
|
|
100
100
|
- **Windows 没有机制级回退**——通过打包依赖 koffi 运行的子进程选择器是唯一原生层级,因此 COM 拒绝或对话框崩溃会直接上报失败;组合层面的回退仍是浏览后端。
|
|
101
|
+
- **Windows 前台授权依赖注入的输入**——子进程在 `Show` 之前合成一次 Alt 按键,对话框才能从后台宿主取得前台;在合成输入被抑制的环境(安全桌面、受限远程会话、提权前台窗口)中,对话框仍可能在其他窗口后面打开。该技术仅在 Windows 11 上验证过。
|
|
101
102
|
|
|
102
103
|
<a id="dev-note"></a>
|
|
103
104
|
### 开发备注
|
package/lib/index.js
CHANGED
|
@@ -64,9 +64,10 @@ async function closeThreadWindows(threadId) {
|
|
|
64
64
|
/**
|
|
65
65
|
* Spawn the dialog child process. Built consumers launch the bundled CJS
|
|
66
66
|
* entry next to this module under plain node; unbuilt (source) consumers
|
|
67
|
-
* bootstrap tsx first, mirroring the dsh CLI's source launch. The
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* bootstrap tsx first, mirroring the dsh CLI's source launch. The child
|
|
68
|
+
* opens its dialog as foreground on its own: `runFolderDialog` synthesizes
|
|
69
|
+
* an Alt press before `Show`, which matters when a background host spawned
|
|
70
|
+
* the child.
|
|
70
71
|
* @param data - the child payload (dialog title).
|
|
71
72
|
* @returns the spawned child process.
|
|
72
73
|
*/
|
|
@@ -276,7 +277,9 @@ async function pickNativeDirectory(signal, internals = {}) {
|
|
|
276
277
|
* with the `native` capability, opening one native OS chooser on the host
|
|
277
278
|
* display per pick (macOS `osascript`, Linux Zenity with a KDialog fallback;
|
|
278
279
|
* Windows opens the modern `IFileOpenDialog` in a spawned child process — a
|
|
279
|
-
* koffi-driven COM conversation on the child's main thread
|
|
280
|
+
* koffi-driven COM conversation on the child's main thread, preceded by a
|
|
281
|
+
* synthesized Alt press so the dialog activates as foreground even when a
|
|
282
|
+
* background host spawned the child). Only viable when
|
|
280
283
|
* the operator sits at the host's screen; remote deployments compose the
|
|
281
284
|
* browse backend instead.
|
|
282
285
|
* @module @deepseek-ai/dsh-host-directory-picker-native
|
package/lib/types/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* with the `native` capability, opening one native OS chooser on the host
|
|
4
4
|
* display per pick (macOS `osascript`, Linux Zenity with a KDialog fallback;
|
|
5
5
|
* Windows opens the modern `IFileOpenDialog` in a spawned child process — a
|
|
6
|
-
* koffi-driven COM conversation on the child's main thread
|
|
6
|
+
* koffi-driven COM conversation on the child's main thread, preceded by a
|
|
7
|
+
* synthesized Alt press so the dialog activates as foreground even when a
|
|
8
|
+
* background host spawned the child). Only viable when
|
|
7
9
|
* the operator sits at the host's screen; remote deployments compose the
|
|
8
10
|
* browse backend instead.
|
|
9
11
|
* @module @deepseek-ai/dsh-host-directory-picker-native
|
|
@@ -11,9 +11,10 @@ import type { Win32DialogWorkerData } from './win32-dialog-worker.ts';
|
|
|
11
11
|
/**
|
|
12
12
|
* Spawn the dialog child process. Built consumers launch the bundled CJS
|
|
13
13
|
* entry next to this module under plain node; unbuilt (source) consumers
|
|
14
|
-
* bootstrap tsx first, mirroring the dsh CLI's source launch. The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* bootstrap tsx first, mirroring the dsh CLI's source launch. The child
|
|
15
|
+
* opens its dialog as foreground on its own: `runFolderDialog` synthesizes
|
|
16
|
+
* an Alt press before `Show`, which matters when a background host spawned
|
|
17
|
+
* the child.
|
|
17
18
|
* @param data - the child payload (dialog title).
|
|
18
19
|
* @returns the spawned child process.
|
|
19
20
|
*/
|
|
@@ -78,6 +78,20 @@ export interface Win32DialogBindings {
|
|
|
78
78
|
* @returns the calling thread's native id.
|
|
79
79
|
*/
|
|
80
80
|
currentThreadId(): number;
|
|
81
|
+
/**
|
|
82
|
+
* Make the dialog that `Show` is about to create able to take the
|
|
83
|
+
* foreground. Windows grants activation only to the foreground process,
|
|
84
|
+
* to a process it started, or to a process that received recent input; a
|
|
85
|
+
* worker spawned by a background host (the web GUI server) qualifies for
|
|
86
|
+
* none, so the dialog would open behind every other window. Synthesizing
|
|
87
|
+
* one Alt press (down, then up) through `keybd_event` counts this process
|
|
88
|
+
* as the most recent input owner — a community-documented technique with
|
|
89
|
+
* no documented contract. Call immediately before `Show`. When the
|
|
90
|
+
* process already holds foreground rights (a console-launched CLI), the
|
|
91
|
+
* press is inert, but the focused window still receives the lone Alt and
|
|
92
|
+
* may briefly highlight its menu bar before the dialog activates.
|
|
93
|
+
*/
|
|
94
|
+
pressAltForForeground(): void;
|
|
81
95
|
}
|
|
82
96
|
/**
|
|
83
97
|
* Run one modal folder-picker conversation on the calling thread: DPI opt-in,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Child-process entry for the Win32 folder dialog: blocks THIS process
|
|
3
3
|
* inside the modal `Show` so the host event loop stays live, reporting over
|
|
4
|
-
* the IPC channel. Spawned as a child process (not a worker thread) so
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* the IPC channel. Spawned as a child process (not a worker thread) so a
|
|
5
|
+
* native fault stays contained and the modal call never wedges the host.
|
|
6
|
+
* A background host (the web GUI server) leaves this process without
|
|
7
|
+
* foreground rights, so `runFolderDialog` synthesizes an Alt press
|
|
8
|
+
* immediately before `Show` and the dialog then activates as foreground.
|
|
9
|
+
* Protocol: `{kind:'showing',threadId}` right
|
|
7
10
|
* before the blocking call (the driver's abort lever needs the native
|
|
8
11
|
* thread id), then exactly one of `{kind:'done',path}` or
|
|
9
12
|
* `{kind:'error',message}`.
|
package/lib/worker.cjs
CHANGED
|
@@ -41,6 +41,10 @@ const DPI_AWARENESS_CONTEXTS = [
|
|
|
41
41
|
-3,
|
|
42
42
|
-2
|
|
43
43
|
];
|
|
44
|
+
/** `VK_MENU`: the synthesized Alt press's virtual key. */
|
|
45
|
+
const VK_MENU = 18;
|
|
46
|
+
/** `KEYEVENTF_KEYUP`: the synthesized Alt press's release flag. */
|
|
47
|
+
const KEYEVENTF_KEYUP = 2;
|
|
44
48
|
/** IFileOpenDialog vtable slots (IUnknown 0-2, IModalWindow 3, IFileDialog 4+). */
|
|
45
49
|
const SLOT_RELEASE = 2;
|
|
46
50
|
const SLOT_SHOW = 3;
|
|
@@ -86,6 +90,12 @@ async function loadWin32DialogBindings() {
|
|
|
86
90
|
]);
|
|
87
91
|
const coTaskMemFree = ole32.func("__stdcall", "CoTaskMemFree", "void", ["void *"]);
|
|
88
92
|
const getCurrentThreadId = kernel32.func("__stdcall", "GetCurrentThreadId", "uint32", []);
|
|
93
|
+
const keybdEvent = user32.func("__stdcall", "keybd_event", "void", [
|
|
94
|
+
"uint8",
|
|
95
|
+
"uint8",
|
|
96
|
+
"uint32",
|
|
97
|
+
"uintptr"
|
|
98
|
+
]);
|
|
89
99
|
const protoShow = koffi.proto("int32 __stdcall DshDialogShow(void *self, void *owner)");
|
|
90
100
|
const protoSetOptions = koffi.proto("int32 __stdcall DshDialogSetOptions(void *self, uint32 options)");
|
|
91
101
|
const protoSetTitle = koffi.proto("int32 __stdcall DshDialogSetTitle(void *self, str16 title)");
|
|
@@ -113,6 +123,10 @@ async function loadWin32DialogBindings() {
|
|
|
113
123
|
coUninitialize();
|
|
114
124
|
},
|
|
115
125
|
currentThreadId: () => getCurrentThreadId(),
|
|
126
|
+
pressAltForForeground: () => {
|
|
127
|
+
keybdEvent(VK_MENU, 0, 0, 0);
|
|
128
|
+
keybdEvent(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
|
|
129
|
+
},
|
|
116
130
|
createFolderDialog: () => {
|
|
117
131
|
const out = Buffer.alloc(pointerSize);
|
|
118
132
|
const created = coCreateInstance(CLSID_FILE_OPEN_DIALOG, null, CLSCTX_INPROC_SERVER, IID_IFILE_OPEN_DIALOG, out);
|
|
@@ -177,6 +191,7 @@ function runFolderDialog(bindings, title, onShowing) {
|
|
|
177
191
|
check(dialog.setOptions(104), "SetOptions");
|
|
178
192
|
check(dialog.setTitle(title), "SetTitle");
|
|
179
193
|
onShowing(bindings.currentThreadId());
|
|
194
|
+
bindings.pressAltForForeground();
|
|
180
195
|
const shown = dialog.show();
|
|
181
196
|
if (shown === -2147023673) return null;
|
|
182
197
|
check(shown, "Show");
|
|
@@ -195,9 +210,12 @@ function runFolderDialog(bindings, title, onShowing) {
|
|
|
195
210
|
/**
|
|
196
211
|
* Child-process entry for the Win32 folder dialog: blocks THIS process
|
|
197
212
|
* inside the modal `Show` so the host event loop stays live, reporting over
|
|
198
|
-
* the IPC channel. Spawned as a child process (not a worker thread) so
|
|
199
|
-
*
|
|
200
|
-
*
|
|
213
|
+
* the IPC channel. Spawned as a child process (not a worker thread) so a
|
|
214
|
+
* native fault stays contained and the modal call never wedges the host.
|
|
215
|
+
* A background host (the web GUI server) leaves this process without
|
|
216
|
+
* foreground rights, so `runFolderDialog` synthesizes an Alt press
|
|
217
|
+
* immediately before `Show` and the dialog then activates as foreground.
|
|
218
|
+
* Protocol: `{kind:'showing',threadId}` right
|
|
201
219
|
* before the blocking call (the driver's abort lever needs the native
|
|
202
220
|
* thread id), then exactly one of `{kind:'done',path}` or
|
|
203
221
|
* `{kind:'error',message}`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-host-directory-picker-native",
|
|
3
3
|
"description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"koffi": "^3.1.0",
|
|
36
|
-
"@deepseek-ai/dsh-host-directory-picker": "^0.1.
|
|
37
|
-
"@deepseek-ai/dsh-native-command": "^0.1.
|
|
36
|
+
"@deepseek-ai/dsh-host-directory-picker": "^0.1.5-alpha.2",
|
|
37
|
+
"@deepseek-ai/dsh-native-command": "^0.1.5-alpha.2"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.2"
|