@deepseek-ai/dsh-host-directory-picker-native 0.0.1-rc.1
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/LICENSE +28 -0
- package/README.i18n.yaml +6 -0
- package/README.md +20 -0
- package/README.zh.md +20 -0
- package/lib/client.js +82 -0
- package/lib/index.js +300 -0
- package/lib/invariant.js +20 -0
- package/lib/types/client/flow.d.ts +17 -0
- package/lib/types/client/index.d.ts +20 -0
- package/lib/types/index.d.ts +24 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/native-picker.d.ts +19 -0
- package/lib/types/win32-dialog-bindings.d.ts +27 -0
- package/lib/types/win32-dialog-host.d.ts +22 -0
- package/lib/types/win32-dialog-logic.d.ts +93 -0
- package/lib/types/win32-dialog-worker.d.ts +26 -0
- package/lib/types/win32-dialog.d.ts +49 -0
- package/lib/worker.cjs +233 -0
- package/package.json +76 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeepSeek
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/host/directory-picker-native/README.md
|
|
5
|
+
README.md: 4dd0c79d080fe097d063cfd2200e30aafc2d2d42
|
|
6
|
+
README.zh.md: e7d0ec99c1aaca8f22f8d5dc29fcbddd73d9f7c0
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-host-directory-picker-native
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The **native-OS-chooser backend** of the [directory-picker seam](../directory-picker/README.md): `NativeDirectoryPicker` registers `ctx.directoryPicker` with the `native` capability, whose `pick(signal)` opens one native chooser per call and resolves the chosen absolute path (`null` on cancel). 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. Only viable when the operator sits at the host's display — remote deployments compose [`-browse`](../directory-picker-browse/README.md) instead. The command boundary (`DirectoryPickerRunner`) and platform facts are injectable. The shared no-shell subprocess runner lives in [`dsh-native-command`](../../util/native-command/README.md).
|
|
6
|
+
|
|
7
|
+
**Dual-face package**: the browser half (`./client`) registers a renderless flow occupant into [ui-workspace's](../../client/ui-workspace/README.md) two directory-flow holes — each `open` request drives `host.pickDirectory` and reports the one outcome (picked path / cancel / failure) through the hole's owner conversation. Both directory-flow declarations must be live before either contribution installs. One cordis.yml row therefore composes both sides of the native interaction; the client carries no capability-kind branching, and mounting a second flow package fails at load (the holes are `single` kind).
|
|
8
|
+
|
|
9
|
+
## Model Experience
|
|
10
|
+
|
|
11
|
+
None, as the backend serves the GUI host's directory selection; nothing here reaches a model request.
|
|
12
|
+
|
|
13
|
+
#### KV Cache effect
|
|
14
|
+
|
|
15
|
+
None; this package neither assembles nor sends a provider request.
|
|
16
|
+
|
|
17
|
+
## Known Limitations and Deferred Work
|
|
18
|
+
|
|
19
|
+
- **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).
|
|
20
|
+
- **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.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @deepseek-ai/dsh-host-directory-picker-native
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
[目录选择 seam](../directory-picker/README.md) 的**原生 OS 选择器后端**:`NativeDirectoryPicker` 以 `native` 能力注册 `ctx.directoryPicker`,其 `pick(signal)` 每次调用打开一个原生选择器并解析出所选绝对路径(取消时为 `null`)。平台工具不经 shell 调用:macOS 使用 `osascript`,Linux 使用 Zenity 并以 KDialog 回退;调用方的中止信号会终止原生进程。Windows 在 spawn 的子进程中打开现代 `IFileOpenDialog`——由 koffi 在子进程主线程上驱动的 COM 会话,采用宿主接受的最佳线程 DPI 感知(优先 per-monitor-v2),中止时向对话框线程投递 `WM_CLOSE`。只有操作者坐在宿主屏幕前时才可用——远程部署应组合 [`-browse`](../directory-picker-browse/README.md)。命令边界(`DirectoryPickerRunner`)与平台事实可注入。共享的免 shell 子进程运行器位于 [`dsh-native-command`](../../util/native-command/README.md)。
|
|
6
|
+
|
|
7
|
+
**双面包**:浏览器端(`./client`)向 [ui-workspace](../../client/ui-workspace/README.md) 的两个目录流 slot 注册一个无渲染的流程占用者——每次 `open` 请求驱动 `host.pickDirectory`,并通过 slot 的属主交互约定上报唯一结果(所选路径/取消/失败)。两个目录流程声明必须同时处于有效状态,任一贡献才会安装。因此一行 cordis.yml 同时组合原生交互的两侧;客户端不包含任何按能力类型进行的分支,挂载第二个流程包会在加载期失败(slot 的 kind 为 `single`)。
|
|
8
|
+
|
|
9
|
+
## 模型体验
|
|
10
|
+
|
|
11
|
+
无。该后端服务于 GUI 宿主的目录选择;这里没有任何内容进入模型请求。
|
|
12
|
+
|
|
13
|
+
#### KV Cache 影响
|
|
14
|
+
|
|
15
|
+
无;该包既不组装也不发送提供方请求。
|
|
16
|
+
|
|
17
|
+
## 已知限制与延期工作
|
|
18
|
+
|
|
19
|
+
- **Linux 依赖桌面工具**——Zenity 与 KDialog 均未安装时,`pick` 以包含解决建议的错误拒绝;它不会回退为手输路径提示(组合层面的回退是 browse 后端)。
|
|
20
|
+
- **Windows 没有机制级回退**——通过打包依赖 koffi 运行的子进程选择器是唯一原生层级,因此 COM 拒绝或对话框崩溃会直接上报失败。组合层面的回退仍是 browse 后端。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@deepseek-ai/dsh-host-directory-picker-native",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
//#region lib/types/client/flow.js
|
|
9
|
+
/**
|
|
10
|
+
* The native picking occupant (package-internal; the `./client` surface
|
|
11
|
+
* exposes only the Loader exports). Same-package tests exercise it directly
|
|
12
|
+
* through this module.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Renderless flow occupant: each rising `open` edge runs exactly one pick and
|
|
16
|
+
* reports exactly one outcome; the ref arms once per open so re-renders (and
|
|
17
|
+
* an adoption keeping `open` true while `busy`) never launch a second
|
|
18
|
+
* chooser. The owner withdrawing `open` re-arms the next request.
|
|
19
|
+
* @param props - owner conversation plus the injected pick call.
|
|
20
|
+
* @returns nothing — the native chooser renders on the host display.
|
|
21
|
+
*/
|
|
22
|
+
function NativeDirectoryFlow(props) {
|
|
23
|
+
const { open, pick } = props;
|
|
24
|
+
const armed = (0, react.useRef)(false);
|
|
25
|
+
const outcome = (0, react.useRef)(props);
|
|
26
|
+
outcome.current = props;
|
|
27
|
+
const alive = (0, react.useRef)(true);
|
|
28
|
+
(0, react.useEffect)(() => {
|
|
29
|
+
alive.current = true;
|
|
30
|
+
return () => {
|
|
31
|
+
alive.current = false;
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
(0, react.useEffect)(() => {
|
|
35
|
+
if (!open) {
|
|
36
|
+
armed.current = false;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (armed.current) return;
|
|
40
|
+
armed.current = true;
|
|
41
|
+
pick().then((path) => {
|
|
42
|
+
if (!alive.current) return;
|
|
43
|
+
if (path === null) outcome.current.onCancel();
|
|
44
|
+
else outcome.current.onPicked(path);
|
|
45
|
+
}, (reason) => {
|
|
46
|
+
if (!alive.current) return;
|
|
47
|
+
outcome.current.onError(reason instanceof Error ? reason.message : String(reason));
|
|
48
|
+
});
|
|
49
|
+
}, [open, pick]);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region lib/types/client/index.js
|
|
54
|
+
/** Required services (cordis fiber inject): the slot registry and the wire-facing workspace service. */
|
|
55
|
+
const inject = ["slots", "workspaces"];
|
|
56
|
+
/**
|
|
57
|
+
* Client plugin body: register the renderless native flow into both
|
|
58
|
+
* directory-flow holes through `slots.inject()` because the ui-workspace
|
|
59
|
+
* entries may activate later or replace their declarations.
|
|
60
|
+
* @param ctx - client root context.
|
|
61
|
+
*/
|
|
62
|
+
function apply(ctx) {
|
|
63
|
+
const injected = () => ({ pick: () => ctx.workspaces.pickDirectory() });
|
|
64
|
+
ctx.slots.inject("conversation.hero.workspace.directoryFlow", () => ctx.slots.inject("sidebar.workspaces.directoryFlow", function* () {
|
|
65
|
+
yield ctx.slots.register({
|
|
66
|
+
name: "conversation.hero.workspace.directoryFlow",
|
|
67
|
+
inject: injected
|
|
68
|
+
}, NativeDirectoryFlow);
|
|
69
|
+
yield ctx.slots.register({
|
|
70
|
+
name: "sidebar.workspaces.directoryFlow",
|
|
71
|
+
inject: injected
|
|
72
|
+
}, NativeDirectoryFlow);
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
exports.apply = apply;
|
|
77
|
+
exports.inject = inject;
|
|
78
|
+
return module.exports;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { DirectoryPicker } from "@deepseek-ai/dsh-host-directory-picker";
|
|
2
|
+
import { runNativeCommand } from "@deepseek-ai/dsh-native-command";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region lib/types/win32-dialog-bindings.js
|
|
6
|
+
const WM_CLOSE = 16;
|
|
7
|
+
/**
|
|
8
|
+
* Encode a canonical GUID string as its 16 little-endian bytes.
|
|
9
|
+
* @param text - the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` form.
|
|
10
|
+
* @returns the in-memory GUID bytes CoCreateInstance expects.
|
|
11
|
+
*/
|
|
12
|
+
function guidBytes(text) {
|
|
13
|
+
const match = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(text);
|
|
14
|
+
const bytes = Buffer.alloc(16);
|
|
15
|
+
bytes.writeUInt32LE(parseInt(match[1], 16), 0);
|
|
16
|
+
bytes.writeUInt16LE(parseInt(match[2], 16), 4);
|
|
17
|
+
bytes.writeUInt16LE(parseInt(match[3], 16), 6);
|
|
18
|
+
Buffer.from(match[4] + match[5], "hex").copy(bytes, 8);
|
|
19
|
+
return bytes;
|
|
20
|
+
}
|
|
21
|
+
guidBytes("dc1c5a9c-e88a-4dde-a5a1-60f82a20aef7");
|
|
22
|
+
guidBytes("d57c7288-d4ad-4768-be02-9d969532d960");
|
|
23
|
+
/**
|
|
24
|
+
* Post `WM_CLOSE` to every window of a native thread — the driver's abort
|
|
25
|
+
* lever against the worker blocked inside `Show`, after which `Show` returns
|
|
26
|
+
* `HRESULT_CANCELLED` and the worker unwinds normally.
|
|
27
|
+
* @param threadId - the dialog thread's native id (from the `showing` notice).
|
|
28
|
+
*/
|
|
29
|
+
async function closeThreadWindows(threadId) {
|
|
30
|
+
const koffi = (await import("koffi")).default;
|
|
31
|
+
const user32 = koffi.load("user32.dll");
|
|
32
|
+
const enumThreadWindows = user32.func("__stdcall", "EnumThreadWindows", "int", [
|
|
33
|
+
"uint32",
|
|
34
|
+
"void *",
|
|
35
|
+
"intptr"
|
|
36
|
+
]);
|
|
37
|
+
const postMessageW = user32.func("__stdcall", "PostMessageW", "int", [
|
|
38
|
+
"void *",
|
|
39
|
+
"uint32",
|
|
40
|
+
"uintptr",
|
|
41
|
+
"intptr"
|
|
42
|
+
]);
|
|
43
|
+
const protoEnumProc = koffi.proto("int __stdcall DshEnumThreadWndProc(void *hwnd, intptr lparam)");
|
|
44
|
+
const callback = koffi.register((hwnd) => {
|
|
45
|
+
postMessageW(hwnd, WM_CLOSE, 0, 0);
|
|
46
|
+
return 1;
|
|
47
|
+
}, koffi.pointer(protoEnumProc));
|
|
48
|
+
try {
|
|
49
|
+
enumThreadWindows(threadId, callback, 0);
|
|
50
|
+
} finally {
|
|
51
|
+
koffi.unregister(callback);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region lib/types/win32-dialog-host.js
|
|
56
|
+
/**
|
|
57
|
+
* Real-process half of the Win32 dialog driver: spawn the dialog child
|
|
58
|
+
* process (source or built plane) and close a dialog thread's windows. The
|
|
59
|
+
* module itself loads everywhere (the import chain from native-picker.ts is
|
|
60
|
+
* static); what stays win32-only is koffi, imported dynamically inside the
|
|
61
|
+
* bindings' functions. The driver's logic is tested against fakes of this
|
|
62
|
+
* surface instead.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Spawn the dialog child process. Built consumers launch the bundled CJS
|
|
66
|
+
* entry next to this module under plain node; unbuilt (source) consumers
|
|
67
|
+
* bootstrap tsx first, mirroring the dsh CLI's source launch. The dialog is
|
|
68
|
+
* the child's first window, so Windows activates it without a foreground
|
|
69
|
+
* call.
|
|
70
|
+
* @param data - the child payload (dialog title).
|
|
71
|
+
* @returns the spawned child process.
|
|
72
|
+
*/
|
|
73
|
+
function spawnDialogWorker(data) {
|
|
74
|
+
const env = {
|
|
75
|
+
...process.env,
|
|
76
|
+
DSH_DIALOG_TITLE: data.title
|
|
77
|
+
};
|
|
78
|
+
const stdio = [
|
|
79
|
+
"ignore",
|
|
80
|
+
"inherit",
|
|
81
|
+
"inherit",
|
|
82
|
+
"ipc"
|
|
83
|
+
];
|
|
84
|
+
/* v8 ignore next 3 -- the built-output arm: tests always run unbuilt (src/) */
|
|
85
|
+
if (!import.meta.url.endsWith(".ts")) return spawn(process.execPath, [fileURLToPath(new URL("./worker.cjs", import.meta.url))], {
|
|
86
|
+
env,
|
|
87
|
+
stdio,
|
|
88
|
+
windowsHide: true
|
|
89
|
+
});
|
|
90
|
+
return spawn(process.execPath, [
|
|
91
|
+
"--import",
|
|
92
|
+
import.meta.resolve("tsx/esm"),
|
|
93
|
+
fileURLToPath(new URL("./win32-dialog-worker.ts", import.meta.url))
|
|
94
|
+
], {
|
|
95
|
+
env,
|
|
96
|
+
stdio,
|
|
97
|
+
windowsHide: true
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region lib/types/win32-dialog.js
|
|
102
|
+
/**
|
|
103
|
+
* Main-thread driver for the Win32 folder dialog: spawns the dialog child
|
|
104
|
+
* process (which blocks inside the modal `Show`), maps its message protocol
|
|
105
|
+
* onto a promise, and services aborts by posting `WM_CLOSE` to the dialog
|
|
106
|
+
* thread's windows until the child reports back. The real process/window
|
|
107
|
+
* surface is injectable so every driver path is testable on any platform.
|
|
108
|
+
*/
|
|
109
|
+
/** The dialog title every host shows. */
|
|
110
|
+
const DIALOG_TITLE = "Select Workspace Directory";
|
|
111
|
+
/** `WM_CLOSE` re-post cadence while an abort waits for the worker to unwind. */
|
|
112
|
+
const CLOSE_RETRY_MS = 150;
|
|
113
|
+
/** Abort-service attempts before force-terminating the worker. */
|
|
114
|
+
const CLOSE_MAX_ATTEMPTS = 20;
|
|
115
|
+
/** Fail loudly if the closed worker-to-driver union gains an unhandled member. */
|
|
116
|
+
/* v8 ignore start -- closed-union backstop; unreachable without a TypeScript contract violation */
|
|
117
|
+
function assertNever(value) {
|
|
118
|
+
throw new TypeError(`unknown win32 dialog worker message kind: ${String(value)}`);
|
|
119
|
+
}
|
|
120
|
+
/* v8 ignore stop */
|
|
121
|
+
/**
|
|
122
|
+
* Open the modern Win32 folder picker off the event loop.
|
|
123
|
+
* @param signal - caller lifetime; abort closes the dialog and rejects.
|
|
124
|
+
* @param internals - Worker/window hooks for deterministic tests.
|
|
125
|
+
* @returns the selected path, or null when the user cancels.
|
|
126
|
+
*/
|
|
127
|
+
async function pickWin32Directory(signal, internals = {}) {
|
|
128
|
+
if (signal.aborted) throw new Error("native directory picker aborted");
|
|
129
|
+
const spawnWorker = internals.spawnWorker ?? spawnDialogWorker;
|
|
130
|
+
const closeWindows = internals.closeThreadWindows ?? closeThreadWindows;
|
|
131
|
+
const closeRetryMs = internals.closeRetryMs ?? CLOSE_RETRY_MS;
|
|
132
|
+
const worker = spawnWorker({ title: DIALOG_TITLE });
|
|
133
|
+
let dialogThreadId;
|
|
134
|
+
let closeTimer;
|
|
135
|
+
let settled = false;
|
|
136
|
+
return await new Promise((resolve, reject) => {
|
|
137
|
+
const settle = (outcome) => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
settled = true;
|
|
140
|
+
if (closeTimer !== void 0) clearInterval(closeTimer);
|
|
141
|
+
signal.removeEventListener("abort", onAbort);
|
|
142
|
+
worker.unref?.();
|
|
143
|
+
outcome();
|
|
144
|
+
};
|
|
145
|
+
const postClose = () => {
|
|
146
|
+
if (dialogThreadId !== void 0) closeWindows(dialogThreadId).catch(() => void 0);
|
|
147
|
+
};
|
|
148
|
+
const serviceAbort = () => {
|
|
149
|
+
let attempts = 0;
|
|
150
|
+
closeTimer = setInterval(() => {
|
|
151
|
+
attempts += 1;
|
|
152
|
+
if (attempts > CLOSE_MAX_ATTEMPTS) {
|
|
153
|
+
settle(() => {
|
|
154
|
+
worker.kill();
|
|
155
|
+
reject(/* @__PURE__ */ new Error("native directory picker aborted (dialog unresponsive; worker killed)"));
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
postClose();
|
|
160
|
+
}, closeRetryMs);
|
|
161
|
+
postClose();
|
|
162
|
+
};
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
serviceAbort();
|
|
165
|
+
};
|
|
166
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
167
|
+
worker.on("message", (message) => {
|
|
168
|
+
switch (message.kind) {
|
|
169
|
+
case "showing":
|
|
170
|
+
dialogThreadId = message.threadId;
|
|
171
|
+
if (signal.aborted) postClose();
|
|
172
|
+
return;
|
|
173
|
+
case "done":
|
|
174
|
+
settle(() => {
|
|
175
|
+
if (signal.aborted) reject(/* @__PURE__ */ new Error("native directory picker aborted"));
|
|
176
|
+
else resolve(message.path);
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
case "error":
|
|
180
|
+
settle(() => {
|
|
181
|
+
reject(/* @__PURE__ */ new Error(`win32 folder dialog failed: ${message.message}`));
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
/* v8 ignore next 2 -- closed worker-owned union; a fourth kind becomes a compile error */
|
|
185
|
+
default: assertNever(message);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
worker.on("error", (error) => {
|
|
189
|
+
settle(() => {
|
|
190
|
+
reject(error);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
worker.on("exit", () => {
|
|
194
|
+
settle(() => {
|
|
195
|
+
reject(/* @__PURE__ */ new Error("win32 folder dialog worker exited before reporting a result"));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region lib/types/native-picker.js
|
|
202
|
+
/** Cross-platform native single-directory chooser behind the native backend's capability. */
|
|
203
|
+
function outputPath(stdout) {
|
|
204
|
+
const path = stdout.replace(/[\r\n]+$/, "");
|
|
205
|
+
return path === "" ? null : path;
|
|
206
|
+
}
|
|
207
|
+
function errorCode(error) {
|
|
208
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
209
|
+
const code = error.code;
|
|
210
|
+
return typeof code === "string" || typeof code === "number" ? code : void 0;
|
|
211
|
+
}
|
|
212
|
+
function errorStderr(error) {
|
|
213
|
+
if (typeof error !== "object" || error === null || !("stderr" in error)) return "";
|
|
214
|
+
const stderr = error.stderr;
|
|
215
|
+
return typeof stderr === "string" ? stderr : "";
|
|
216
|
+
}
|
|
217
|
+
function isMissingCommand(error) {
|
|
218
|
+
return errorCode(error) === "ENOENT";
|
|
219
|
+
}
|
|
220
|
+
function rethrowIfAborted(signal, error) {
|
|
221
|
+
if (signal.aborted) throw error;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Open the platform directory picker.
|
|
225
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
226
|
+
* @param internals - Platform and runner hooks for deterministic tests.
|
|
227
|
+
* @returns the selected path, or null when the user cancels.
|
|
228
|
+
*/
|
|
229
|
+
async function pickNativeDirectory(signal, internals = {}) {
|
|
230
|
+
const platform = internals.platform ?? process.platform;
|
|
231
|
+
const run = internals.run ?? runNativeCommand;
|
|
232
|
+
if (platform === "darwin") try {
|
|
233
|
+
return outputPath((await run("osascript", [
|
|
234
|
+
"-e",
|
|
235
|
+
"set selectedFolder to choose folder with prompt \"Select Workspace Directory\"",
|
|
236
|
+
"-e",
|
|
237
|
+
"POSIX path of selectedFolder"
|
|
238
|
+
], signal)).stdout);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (!signal.aborted && errorCode(error) === 1 && /(?:User canceled|-128)/i.test(errorStderr(error))) return null;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
if (platform === "win32") return await (internals.pickWin32Dialog ?? pickWin32Directory)(signal);
|
|
244
|
+
if (platform === "linux") {
|
|
245
|
+
try {
|
|
246
|
+
return outputPath((await run("zenity", [
|
|
247
|
+
"--file-selection",
|
|
248
|
+
"--directory",
|
|
249
|
+
"--title=Select Workspace Directory"
|
|
250
|
+
], signal)).stdout);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
rethrowIfAborted(signal, error);
|
|
253
|
+
if (errorCode(error) === 1) return null;
|
|
254
|
+
if (!isMissingCommand(error)) throw error;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
return outputPath((await run("kdialog", [
|
|
258
|
+
"--getexistingdirectory",
|
|
259
|
+
".",
|
|
260
|
+
"--title",
|
|
261
|
+
"Select Workspace Directory"
|
|
262
|
+
], signal)).stdout);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
rethrowIfAborted(signal, error);
|
|
265
|
+
if (errorCode(error) === 1) return null;
|
|
266
|
+
if (isMissingCommand(error)) throw new Error("no supported native directory picker found (install zenity or kdialog)");
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`native directory picker is unsupported on ${platform}`);
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region lib/types/index.js
|
|
274
|
+
/**
|
|
275
|
+
* Native backend of the directory-picker seam: registers `ctx.directoryPicker`
|
|
276
|
+
* with the `native` capability, opening one native OS chooser on the host
|
|
277
|
+
* display per pick (macOS `osascript`, Linux Zenity with a KDialog fallback;
|
|
278
|
+
* Windows opens the modern `IFileOpenDialog` in a spawned child process — a
|
|
279
|
+
* koffi-driven COM conversation on the child's main thread). Only viable when
|
|
280
|
+
* the operator sits at the host's screen; remote deployments compose the
|
|
281
|
+
* browse backend instead.
|
|
282
|
+
* @module @deepseek-ai/dsh-host-directory-picker-native
|
|
283
|
+
*/
|
|
284
|
+
/** The `ctx.directoryPicker` native implementation (stable capability object per service life). */
|
|
285
|
+
var NativeDirectoryPicker = class extends DirectoryPicker {
|
|
286
|
+
nativeCapability = {
|
|
287
|
+
kind: "native",
|
|
288
|
+
/* v8 ignore next -- pure forward to pickNativeDirectory (its spec owns behavior); invoking here opens a real chooser. */
|
|
289
|
+
pick: (signal) => pickNativeDirectory(signal)
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* The native interaction capability.
|
|
293
|
+
* @returns the stable `native` capability object.
|
|
294
|
+
*/
|
|
295
|
+
capability() {
|
|
296
|
+
return this.nativeCapability;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
//#endregion
|
|
300
|
+
export { NativeDirectoryPicker as default, pickNativeDirectory };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for the native directory-picker backend.
|
|
4
|
+
* @module @deepseek-ai/dsh-host-directory-picker-native/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-host-directory-picker-native";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "host-directory-picker-native-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/** No runtime invariant: each pick is one stateless subprocess round trip; the chooser outcome is only the returned path. */
|
|
12
|
+
const install = () => {};
|
|
13
|
+
/**
|
|
14
|
+
* Register the native directory-picker invariant companion.
|
|
15
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
16
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
17
|
+
*/
|
|
18
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client';
|
|
3
|
+
/** Injected face: the wire call the flow drives (bound in apply's closure). */
|
|
4
|
+
export interface NativeFlowInjected {
|
|
5
|
+
/** Ask the local Host to open its native single-directory chooser. */
|
|
6
|
+
pick: () => Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Renderless flow occupant: each rising `open` edge runs exactly one pick and
|
|
10
|
+
* reports exactly one outcome; the ref arms once per open so re-renders (and
|
|
11
|
+
* an adoption keeping `open` true while `busy`) never launch a second
|
|
12
|
+
* chooser. The owner withdrawing `open` re-arms the next request.
|
|
13
|
+
* @param props - owner conversation plus the injected pick call.
|
|
14
|
+
* @returns nothing — the native chooser renders on the host display.
|
|
15
|
+
*/
|
|
16
|
+
export declare function NativeDirectoryFlow(props: DirectoryFlowOwnerProps & NativeFlowInjected): ReactElement | null;
|
|
17
|
+
//# sourceMappingURL=flow.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the native directory-picker backend: fills ui-workspace's
|
|
3
|
+
* two directory-flow holes with a renderless occupant that answers each
|
|
4
|
+
* `open` by driving `host.pickDirectory` (the node half's OS chooser) and
|
|
5
|
+
* reporting the one outcome — picked path, cancellation, or failure — back
|
|
6
|
+
* through the owner conversation. Mounting this package therefore composes
|
|
7
|
+
* both sides of the native interaction with one cordis.yml row; no client
|
|
8
|
+
* code branches on a capability kind.
|
|
9
|
+
*/
|
|
10
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
11
|
+
/** Required services (cordis fiber inject): the slot registry and the wire-facing workspace service. */
|
|
12
|
+
export declare const inject: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Client plugin body: register the renderless native flow into both
|
|
15
|
+
* directory-flow holes through `slots.inject()` because the ui-workspace
|
|
16
|
+
* entries may activate later or replace their declarations.
|
|
17
|
+
* @param ctx - client root context.
|
|
18
|
+
*/
|
|
19
|
+
export declare function apply(ctx: ClientContext): void;
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native backend of the directory-picker seam: registers `ctx.directoryPicker`
|
|
3
|
+
* with the `native` capability, opening one native OS chooser on the host
|
|
4
|
+
* display per pick (macOS `osascript`, Linux Zenity with a KDialog fallback;
|
|
5
|
+
* Windows opens the modern `IFileOpenDialog` in a spawned child process — a
|
|
6
|
+
* koffi-driven COM conversation on the child's main thread). Only viable when
|
|
7
|
+
* the operator sits at the host's screen; remote deployments compose the
|
|
8
|
+
* browse backend instead.
|
|
9
|
+
* @module @deepseek-ai/dsh-host-directory-picker-native
|
|
10
|
+
*/
|
|
11
|
+
import { DirectoryPicker } from '@deepseek-ai/dsh-host-directory-picker';
|
|
12
|
+
import type { DirectoryPickerCapability } from '@deepseek-ai/dsh-host-directory-picker';
|
|
13
|
+
export type { DirectoryPickerInternals, DirectoryPickerRunner } from './native-picker.ts';
|
|
14
|
+
export { pickNativeDirectory } from './native-picker.ts';
|
|
15
|
+
/** The `ctx.directoryPicker` native implementation (stable capability object per service life). */
|
|
16
|
+
export default class NativeDirectoryPicker extends DirectoryPicker {
|
|
17
|
+
private readonly nativeCapability;
|
|
18
|
+
/**
|
|
19
|
+
* The native interaction capability.
|
|
20
|
+
* @returns the stable `native` capability object.
|
|
21
|
+
*/
|
|
22
|
+
capability(): DirectoryPickerCapability;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for the native directory-picker backend.
|
|
3
|
+
* @module @deepseek-ai/dsh-host-directory-picker-native/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "host-directory-picker-native-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register the native directory-picker invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Cross-platform native single-directory chooser behind the native backend's capability. */
|
|
2
|
+
import { type NativeCommandRunner } from '@deepseek-ai/dsh-native-command';
|
|
3
|
+
/** Testable command boundary; native implementations never invoke a shell. */
|
|
4
|
+
export type DirectoryPickerRunner = NativeCommandRunner;
|
|
5
|
+
/** Injectable platform facts for deterministic adapter tests. */
|
|
6
|
+
export interface DirectoryPickerInternals {
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
run?: DirectoryPickerRunner;
|
|
9
|
+
/** Replaces the in-process Win32 dialog (`pickWin32Directory`) for deterministic tests. */
|
|
10
|
+
pickWin32Dialog?: (signal: AbortSignal) => Promise<string | null>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Open the platform directory picker.
|
|
14
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
15
|
+
* @param internals - Platform and runner hooks for deterministic tests.
|
|
16
|
+
* @returns the selected path, or null when the user cancels.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pickNativeDirectory(signal: AbortSignal, internals?: DirectoryPickerInternals): Promise<string | null>;
|
|
19
|
+
//# sourceMappingURL=native-picker.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* koffi-backed Win32 bindings for the folder dialog: the COM vtable calls
|
|
3
|
+
* behind {@link Win32DialogBindings} plus the cross-thread window closer the
|
|
4
|
+
* driver uses to service aborts. The module loads on every platform; koffi
|
|
5
|
+
* itself is imported lazily inside each function, so non-Windows processes
|
|
6
|
+
* never load it — the same containment as the repo's other `win32.ts`
|
|
7
|
+
* modules.
|
|
8
|
+
*
|
|
9
|
+
* The COM surface used here (IModalWindow/IFileDialog/IFileOpenDialog and
|
|
10
|
+
* IShellItem vtable order, the GUIDs, `FOS_*` and `SIGDN_FILESYSPATH`) is
|
|
11
|
+
* frozen Windows ABI since Vista; slots are offsets into the vtable at the
|
|
12
|
+
* object's first pointer.
|
|
13
|
+
*/
|
|
14
|
+
import type { Win32DialogBindings } from './win32-dialog-logic.ts';
|
|
15
|
+
/**
|
|
16
|
+
* Load koffi and expose the dialog bindings for this thread.
|
|
17
|
+
* @returns the bindings {@link runFolderDialog} sequences against.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadWin32DialogBindings(): Promise<Win32DialogBindings>;
|
|
20
|
+
/**
|
|
21
|
+
* Post `WM_CLOSE` to every window of a native thread — the driver's abort
|
|
22
|
+
* lever against the worker blocked inside `Show`, after which `Show` returns
|
|
23
|
+
* `HRESULT_CANCELLED` and the worker unwinds normally.
|
|
24
|
+
* @param threadId - the dialog thread's native id (from the `showing` notice).
|
|
25
|
+
*/
|
|
26
|
+
export declare function closeThreadWindows(threadId: number): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=win32-dialog-bindings.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-process half of the Win32 dialog driver: spawn the dialog child
|
|
3
|
+
* process (source or built plane) and close a dialog thread's windows. The
|
|
4
|
+
* module itself loads everywhere (the import chain from native-picker.ts is
|
|
5
|
+
* static); what stays win32-only is koffi, imported dynamically inside the
|
|
6
|
+
* bindings' functions. The driver's logic is tested against fakes of this
|
|
7
|
+
* surface instead.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import type { Win32DialogWorkerData } from './win32-dialog-worker.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Spawn the dialog child process. Built consumers launch the bundled CJS
|
|
13
|
+
* entry next to this module under plain node; unbuilt (source) consumers
|
|
14
|
+
* bootstrap tsx first, mirroring the dsh CLI's source launch. The dialog is
|
|
15
|
+
* the child's first window, so Windows activates it without a foreground
|
|
16
|
+
* call.
|
|
17
|
+
* @param data - the child payload (dialog title).
|
|
18
|
+
* @returns the spawned child process.
|
|
19
|
+
*/
|
|
20
|
+
export declare function spawnDialogWorker(data: Win32DialogWorkerData): ReturnType<typeof spawn>;
|
|
21
|
+
export { closeThreadWindows } from './win32-dialog-bindings.ts';
|
|
22
|
+
//# sourceMappingURL=win32-dialog-host.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure sequencing of the Win32 `IFileOpenDialog` folder-picker COM
|
|
3
|
+
* conversation over injectable platform bindings, so every outcome path
|
|
4
|
+
* (selection, cancellation, HRESULT failure, cleanup ordering) is testable on
|
|
5
|
+
* any platform. The koffi-backed bindings live in
|
|
6
|
+
* `win32-dialog-bindings.ts`, which only a real win32 process ever loads.
|
|
7
|
+
*/
|
|
8
|
+
/** `HRESULT_FROM_WIN32(ERROR_CANCELLED)`: the user dismissed the dialog. */
|
|
9
|
+
export declare const HRESULT_CANCELLED: number;
|
|
10
|
+
/** `FOS_PICKFOLDERS`: the dialog selects directories, not files. */
|
|
11
|
+
export declare const FOS_PICKFOLDERS = 32;
|
|
12
|
+
/** `FOS_FORCEFILESYSTEM`: only results with a filesystem path can be chosen. */
|
|
13
|
+
export declare const FOS_FORCEFILESYSTEM = 64;
|
|
14
|
+
/** `FOS_NOCHANGEDIR`: never mutate the process working directory. */
|
|
15
|
+
export declare const FOS_NOCHANGEDIR = 8;
|
|
16
|
+
/** One created folder dialog: the vtable calls the sequencing needs. */
|
|
17
|
+
export interface Win32FolderDialog {
|
|
18
|
+
/**
|
|
19
|
+
* `IFileDialog::SetOptions`.
|
|
20
|
+
* @param options - the `FOS_*` flag union to apply.
|
|
21
|
+
* @returns the call's HRESULT.
|
|
22
|
+
*/
|
|
23
|
+
setOptions(options: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* `IFileDialog::SetTitle`.
|
|
26
|
+
* @param title - the dialog title text.
|
|
27
|
+
* @returns the call's HRESULT.
|
|
28
|
+
*/
|
|
29
|
+
setTitle(title: string): number;
|
|
30
|
+
/**
|
|
31
|
+
* `IModalWindow::Show` with no owner window; blocks the calling thread
|
|
32
|
+
* until the user selects or dismisses.
|
|
33
|
+
* @returns the call's HRESULT (`HRESULT_CANCELLED` on dismissal).
|
|
34
|
+
*/
|
|
35
|
+
show(): number;
|
|
36
|
+
/**
|
|
37
|
+
* `IFileDialog::GetResult` + `IShellItem::GetDisplayName(SIGDN_FILESYSPATH)`,
|
|
38
|
+
* releasing the shell item and freeing the COM string.
|
|
39
|
+
* @returns the call chain's HRESULT and, on success, the selected path.
|
|
40
|
+
*/
|
|
41
|
+
resultPath(): {
|
|
42
|
+
hr: number;
|
|
43
|
+
path?: string;
|
|
44
|
+
};
|
|
45
|
+
/** Release the dialog's COM reference. */
|
|
46
|
+
release(): void;
|
|
47
|
+
}
|
|
48
|
+
/** The thread-level native surface the dialog sequencing runs against. */
|
|
49
|
+
export interface Win32DialogBindings {
|
|
50
|
+
/**
|
|
51
|
+
* Opt the calling thread into the best supported DPI awareness
|
|
52
|
+
* (per-monitor-v2, then per-monitor, then system-aware), checking each
|
|
53
|
+
* call's result. Best-effort on purpose: a host accepting none of them
|
|
54
|
+
* (or lacking the API, pre-1607) still shows the modern dialog — possibly
|
|
55
|
+
* blurry above 100 % scaling — because a cosmetic degradation must not
|
|
56
|
+
* cost the tier.
|
|
57
|
+
*/
|
|
58
|
+
setThreadDpiAwareness(): void;
|
|
59
|
+
/**
|
|
60
|
+
* `CoInitializeEx(COINIT_APARTMENTTHREADED)` on the calling thread.
|
|
61
|
+
* @returns the call's HRESULT (`S_FALSE` re-entry is still a success).
|
|
62
|
+
*/
|
|
63
|
+
coInitializeSta(): number;
|
|
64
|
+
/**
|
|
65
|
+
* `CoUninitialize` on the calling thread — COM requires one pairing call
|
|
66
|
+
* for every successful (including `S_FALSE`) `CoInitializeEx`, even on a
|
|
67
|
+
* thread that exits right after the conversation.
|
|
68
|
+
*/
|
|
69
|
+
coUninitialize(): void;
|
|
70
|
+
/**
|
|
71
|
+
* `CoCreateInstance(CLSID_FileOpenDialog)`.
|
|
72
|
+
* @returns the created dialog surface; throws when creation fails.
|
|
73
|
+
*/
|
|
74
|
+
createFolderDialog(): Win32FolderDialog;
|
|
75
|
+
/**
|
|
76
|
+
* `GetCurrentThreadId` — the native id a driver needs to close this
|
|
77
|
+
* thread's windows from outside.
|
|
78
|
+
* @returns the calling thread's native id.
|
|
79
|
+
*/
|
|
80
|
+
currentThreadId(): number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Run one modal folder-picker conversation on the calling thread: DPI opt-in,
|
|
84
|
+
* STA init, dialog creation, `Show`, and result extraction, releasing the
|
|
85
|
+
* dialog on every path.
|
|
86
|
+
* @param bindings - the native surface (koffi-backed in production, fakes in tests).
|
|
87
|
+
* @param title - the dialog title text.
|
|
88
|
+
* @param onShowing - called with the native thread id immediately before the
|
|
89
|
+
* blocking `Show`, so a driver on another thread can close the dialog.
|
|
90
|
+
* @returns the selected filesystem path, or null when the user cancels.
|
|
91
|
+
*/
|
|
92
|
+
export declare function runFolderDialog(bindings: Win32DialogBindings, title: string, onShowing: (threadId: number) => void): string | null;
|
|
93
|
+
//# sourceMappingURL=win32-dialog-logic.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process entry for the Win32 folder dialog: blocks THIS process
|
|
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 the
|
|
5
|
+
* dialog is the process's first window and Windows activates it without a
|
|
6
|
+
* manual foreground call. Protocol: `{kind:'showing',threadId}` right
|
|
7
|
+
* before the blocking call (the driver's abort lever needs the native
|
|
8
|
+
* thread id), then exactly one of `{kind:'done',path}` or
|
|
9
|
+
* `{kind:'error',message}`.
|
|
10
|
+
*/
|
|
11
|
+
/** The driver-to-child payload: the dialog title (passed via env). */
|
|
12
|
+
export interface Win32DialogWorkerData {
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
15
|
+
/** One notice or outcome posted back to the driver. */
|
|
16
|
+
export type Win32DialogWorkerMessage = {
|
|
17
|
+
kind: 'showing';
|
|
18
|
+
threadId: number;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'done';
|
|
21
|
+
path: string | null;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'error';
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=win32-dialog-worker.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread driver for the Win32 folder dialog: spawns the dialog child
|
|
3
|
+
* process (which blocks inside the modal `Show`), maps its message protocol
|
|
4
|
+
* onto a promise, and services aborts by posting `WM_CLOSE` to the dialog
|
|
5
|
+
* thread's windows until the child reports back. The real process/window
|
|
6
|
+
* surface is injectable so every driver path is testable on any platform.
|
|
7
|
+
*/
|
|
8
|
+
import type { Win32DialogWorkerData, Win32DialogWorkerMessage } from './win32-dialog-worker.ts';
|
|
9
|
+
/** The child-process surface the driver drives (satisfied by `node:child_process`). */
|
|
10
|
+
export interface Win32DialogWorkerLike {
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to a child-process event.
|
|
13
|
+
* @param event - `message`, `error`, or `exit`.
|
|
14
|
+
* @param listener - the event consumer.
|
|
15
|
+
*/
|
|
16
|
+
on(event: 'message', listener: (message: Win32DialogWorkerMessage) => void): unknown;
|
|
17
|
+
on(event: 'error', listener: (error: Error) => void): unknown;
|
|
18
|
+
on(event: 'exit', listener: (code: number) => void): unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Force-stop the child; the abort path's last resort when `WM_CLOSE`
|
|
21
|
+
* never lands (e.g. the dialog window was never created).
|
|
22
|
+
* @returns whether a kill signal was delivered.
|
|
23
|
+
*/
|
|
24
|
+
kill(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Release the event-loop reference. Called once the pick settles so a
|
|
27
|
+
* child stuck in the native modal call never blocks process exit.
|
|
28
|
+
*/
|
|
29
|
+
unref?(): void;
|
|
30
|
+
}
|
|
31
|
+
/** Injectable process surface for deterministic driver tests. */
|
|
32
|
+
export interface Win32DialogInternals {
|
|
33
|
+
/** Replaces the real child spawn (`win32-dialog-host.ts`). */
|
|
34
|
+
spawnWorker?: (data: Win32DialogWorkerData) => Win32DialogWorkerLike;
|
|
35
|
+
/** Replaces the real `WM_CLOSE` poster (`win32-dialog-host.ts`). */
|
|
36
|
+
closeThreadWindows?: (threadId: number) => Promise<void>;
|
|
37
|
+
/** Abort-service cadence override so tests never wait wall-clock time. */
|
|
38
|
+
closeRetryMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** The dialog title every host shows. */
|
|
41
|
+
export declare const DIALOG_TITLE = "Select Workspace Directory";
|
|
42
|
+
/**
|
|
43
|
+
* Open the modern Win32 folder picker off the event loop.
|
|
44
|
+
* @param signal - caller lifetime; abort closes the dialog and rejects.
|
|
45
|
+
* @param internals - Worker/window hooks for deterministic tests.
|
|
46
|
+
* @returns the selected path, or null when the user cancels.
|
|
47
|
+
*/
|
|
48
|
+
export declare function pickWin32Directory(signal: AbortSignal, internals?: Win32DialogInternals): Promise<string | null>;
|
|
49
|
+
//# sourceMappingURL=win32-dialog.d.ts.map
|
package/lib/worker.cjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
//#region lib/types/win32-dialog-bindings.js
|
|
2
|
+
/**
|
|
3
|
+
* koffi-backed Win32 bindings for the folder dialog: the COM vtable calls
|
|
4
|
+
* behind {@link Win32DialogBindings} plus the cross-thread window closer the
|
|
5
|
+
* driver uses to service aborts. The module loads on every platform; koffi
|
|
6
|
+
* itself is imported lazily inside each function, so non-Windows processes
|
|
7
|
+
* never load it — the same containment as the repo's other `win32.ts`
|
|
8
|
+
* modules.
|
|
9
|
+
*
|
|
10
|
+
* The COM surface used here (IModalWindow/IFileDialog/IFileOpenDialog and
|
|
11
|
+
* IShellItem vtable order, the GUIDs, `FOS_*` and `SIGDN_FILESYSPATH`) is
|
|
12
|
+
* frozen Windows ABI since Vista; slots are offsets into the vtable at the
|
|
13
|
+
* object's first pointer.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Read a NUL-terminated UTF-16 string at a native address. koffi's
|
|
17
|
+
* `_Out_ void **` out-params surface a raw address, and
|
|
18
|
+
* `koffi.decode(addr, 'str16')` would dereference it as a pointer — crash
|
|
19
|
+
* on real Windows — so view the memory directly instead.
|
|
20
|
+
*/
|
|
21
|
+
function readUtf16(koffi, address) {
|
|
22
|
+
const bytes = Buffer.from(koffi.view(address, 32768));
|
|
23
|
+
let end = 0;
|
|
24
|
+
while (end + 1 < bytes.length && bytes[end] !== 0) end += 2;
|
|
25
|
+
return bytes.toString("utf16le", 0, end);
|
|
26
|
+
}
|
|
27
|
+
const COINIT_APARTMENTTHREADED = 2;
|
|
28
|
+
const CLSCTX_INPROC_SERVER = 1;
|
|
29
|
+
const SIGDN_FILESYSPATH = -2147123200;
|
|
30
|
+
/**
|
|
31
|
+
* Thread DPI awareness contexts, best first: per-monitor-v2 (Windows 10
|
|
32
|
+
* 1703+), per-monitor (1607+), then system-aware. `SetThreadDpiAwarenessContext`
|
|
33
|
+
* returns NULL for an unsupported context instead of throwing, so the caller
|
|
34
|
+
* cascades to the best one the host accepts; DPI stays a cosmetic
|
|
35
|
+
* best-effort — an unsupported host still gets the modern dialog.
|
|
36
|
+
*/
|
|
37
|
+
const DPI_AWARENESS_CONTEXTS = [
|
|
38
|
+
-4,
|
|
39
|
+
-3,
|
|
40
|
+
-2
|
|
41
|
+
];
|
|
42
|
+
/** IFileOpenDialog vtable slots (IUnknown 0-2, IModalWindow 3, IFileDialog 4+). */
|
|
43
|
+
const SLOT_RELEASE = 2;
|
|
44
|
+
const SLOT_SHOW = 3;
|
|
45
|
+
const SLOT_SET_OPTIONS = 9;
|
|
46
|
+
const SLOT_SET_TITLE = 17;
|
|
47
|
+
const SLOT_GET_RESULT = 20;
|
|
48
|
+
/** IShellItem vtable slot for `GetDisplayName`. */
|
|
49
|
+
const SLOT_GET_DISPLAY_NAME = 5;
|
|
50
|
+
/**
|
|
51
|
+
* Encode a canonical GUID string as its 16 little-endian bytes.
|
|
52
|
+
* @param text - the `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` form.
|
|
53
|
+
* @returns the in-memory GUID bytes CoCreateInstance expects.
|
|
54
|
+
*/
|
|
55
|
+
function guidBytes(text) {
|
|
56
|
+
const match = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(text);
|
|
57
|
+
const bytes = Buffer.alloc(16);
|
|
58
|
+
bytes.writeUInt32LE(parseInt(match[1], 16), 0);
|
|
59
|
+
bytes.writeUInt16LE(parseInt(match[2], 16), 4);
|
|
60
|
+
bytes.writeUInt16LE(parseInt(match[3], 16), 6);
|
|
61
|
+
Buffer.from(match[4] + match[5], "hex").copy(bytes, 8);
|
|
62
|
+
return bytes;
|
|
63
|
+
}
|
|
64
|
+
const CLSID_FILE_OPEN_DIALOG = guidBytes("dc1c5a9c-e88a-4dde-a5a1-60f82a20aef7");
|
|
65
|
+
const IID_IFILE_OPEN_DIALOG = guidBytes("d57c7288-d4ad-4768-be02-9d969532d960");
|
|
66
|
+
/**
|
|
67
|
+
* Load koffi and expose the dialog bindings for this thread.
|
|
68
|
+
* @returns the bindings {@link runFolderDialog} sequences against.
|
|
69
|
+
*/
|
|
70
|
+
async function loadWin32DialogBindings() {
|
|
71
|
+
const koffi = (await import("koffi")).default;
|
|
72
|
+
const ole32 = koffi.load("ole32.dll");
|
|
73
|
+
const user32 = koffi.load("user32.dll");
|
|
74
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
75
|
+
const pointerSize = koffi.sizeof("void *");
|
|
76
|
+
const coInitializeEx = ole32.func("__stdcall", "CoInitializeEx", "int32", ["void *", "uint32"]);
|
|
77
|
+
const coUninitialize = ole32.func("__stdcall", "CoUninitialize", "void", []);
|
|
78
|
+
const coCreateInstance = ole32.func("__stdcall", "CoCreateInstance", "int32", [
|
|
79
|
+
"void *",
|
|
80
|
+
"void *",
|
|
81
|
+
"uint32",
|
|
82
|
+
"void *",
|
|
83
|
+
"void *"
|
|
84
|
+
]);
|
|
85
|
+
const coTaskMemFree = ole32.func("__stdcall", "CoTaskMemFree", "void", ["void *"]);
|
|
86
|
+
const getCurrentThreadId = kernel32.func("__stdcall", "GetCurrentThreadId", "uint32", []);
|
|
87
|
+
const protoShow = koffi.proto("int32 __stdcall DshDialogShow(void *self, void *owner)");
|
|
88
|
+
const protoSetOptions = koffi.proto("int32 __stdcall DshDialogSetOptions(void *self, uint32 options)");
|
|
89
|
+
const protoSetTitle = koffi.proto("int32 __stdcall DshDialogSetTitle(void *self, str16 title)");
|
|
90
|
+
const protoGetResult = koffi.proto("int32 __stdcall DshDialogGetResult(void *self, _Out_ void **item)");
|
|
91
|
+
const protoGetDisplayName = koffi.proto("int32 __stdcall DshItemGetDisplayName(void *self, int32 form, _Out_ void **name)");
|
|
92
|
+
const protoRelease = koffi.proto("uint32 __stdcall DshComRelease(void *self)");
|
|
93
|
+
/** Bind vtable slot `slot` of COM object `self` to a caller through `proto`. */
|
|
94
|
+
const method = (self, slot, proto) => {
|
|
95
|
+
const vtable = koffi.decode(self, "void *");
|
|
96
|
+
const fn = koffi.decode(vtable, slot * pointerSize, "void *");
|
|
97
|
+
return (...args) => koffi.call(fn, proto, self, ...args);
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
setThreadDpiAwareness: () => {
|
|
101
|
+
let setContext;
|
|
102
|
+
try {
|
|
103
|
+
setContext = user32.func("__stdcall", "SetThreadDpiAwarenessContext", "void *", ["intptr"]);
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const context of DPI_AWARENESS_CONTEXTS) if (setContext(context) !== null) return;
|
|
108
|
+
},
|
|
109
|
+
coInitializeSta: () => coInitializeEx(null, COINIT_APARTMENTTHREADED),
|
|
110
|
+
coUninitialize: () => {
|
|
111
|
+
coUninitialize();
|
|
112
|
+
},
|
|
113
|
+
currentThreadId: () => getCurrentThreadId(),
|
|
114
|
+
createFolderDialog: () => {
|
|
115
|
+
const out = Buffer.alloc(pointerSize);
|
|
116
|
+
const created = coCreateInstance(CLSID_FILE_OPEN_DIALOG, null, CLSCTX_INPROC_SERVER, IID_IFILE_OPEN_DIALOG, out);
|
|
117
|
+
if (created < 0) throw new Error(`CoCreateInstance(FileOpenDialog) failed: HRESULT 0x${(created >>> 0).toString(16)}`);
|
|
118
|
+
const dialog = koffi.decode(out, "void *");
|
|
119
|
+
return {
|
|
120
|
+
setOptions: (options) => method(dialog, SLOT_SET_OPTIONS, protoSetOptions)(options),
|
|
121
|
+
setTitle: (title) => method(dialog, SLOT_SET_TITLE, protoSetTitle)(title),
|
|
122
|
+
show: () => method(dialog, SLOT_SHOW, protoShow)(null),
|
|
123
|
+
resultPath: () => {
|
|
124
|
+
const itemOut = [null];
|
|
125
|
+
const gotItem = method(dialog, SLOT_GET_RESULT, protoGetResult)(itemOut);
|
|
126
|
+
if (gotItem < 0) return { hr: gotItem };
|
|
127
|
+
const item = itemOut[0];
|
|
128
|
+
try {
|
|
129
|
+
const nameOut = [null];
|
|
130
|
+
const gotName = method(item, SLOT_GET_DISPLAY_NAME, protoGetDisplayName)(SIGDN_FILESYSPATH, nameOut);
|
|
131
|
+
if (gotName < 0) return { hr: gotName };
|
|
132
|
+
const path = readUtf16(koffi, nameOut[0]);
|
|
133
|
+
coTaskMemFree(nameOut[0]);
|
|
134
|
+
return {
|
|
135
|
+
hr: gotName,
|
|
136
|
+
path
|
|
137
|
+
};
|
|
138
|
+
} finally {
|
|
139
|
+
method(item, SLOT_RELEASE, protoRelease)();
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
release: () => {
|
|
143
|
+
method(dialog, SLOT_RELEASE, protoRelease)();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Throw when an HRESULT signals failure.
|
|
151
|
+
* @param hr - the HRESULT to check.
|
|
152
|
+
* @param what - the failing call's name for the error message.
|
|
153
|
+
* @returns the (successful) HRESULT unchanged.
|
|
154
|
+
*/
|
|
155
|
+
function check(hr, what) {
|
|
156
|
+
if (hr < 0) throw new Error(`${what} failed: HRESULT 0x${(hr >>> 0).toString(16)}`);
|
|
157
|
+
return hr;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Run one modal folder-picker conversation on the calling thread: DPI opt-in,
|
|
161
|
+
* STA init, dialog creation, `Show`, and result extraction, releasing the
|
|
162
|
+
* dialog on every path.
|
|
163
|
+
* @param bindings - the native surface (koffi-backed in production, fakes in tests).
|
|
164
|
+
* @param title - the dialog title text.
|
|
165
|
+
* @param onShowing - called with the native thread id immediately before the
|
|
166
|
+
* blocking `Show`, so a driver on another thread can close the dialog.
|
|
167
|
+
* @returns the selected filesystem path, or null when the user cancels.
|
|
168
|
+
*/
|
|
169
|
+
function runFolderDialog(bindings, title, onShowing) {
|
|
170
|
+
bindings.setThreadDpiAwareness();
|
|
171
|
+
check(bindings.coInitializeSta(), "CoInitializeEx");
|
|
172
|
+
try {
|
|
173
|
+
const dialog = bindings.createFolderDialog();
|
|
174
|
+
try {
|
|
175
|
+
check(dialog.setOptions(104), "SetOptions");
|
|
176
|
+
check(dialog.setTitle(title), "SetTitle");
|
|
177
|
+
onShowing(bindings.currentThreadId());
|
|
178
|
+
const shown = dialog.show();
|
|
179
|
+
if (shown === -2147023673) return null;
|
|
180
|
+
check(shown, "Show");
|
|
181
|
+
const result = dialog.resultPath();
|
|
182
|
+
check(result.hr, "GetResult");
|
|
183
|
+
return result.path;
|
|
184
|
+
} finally {
|
|
185
|
+
dialog.release();
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
bindings.coUninitialize();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region lib/types/win32-dialog-worker.js
|
|
193
|
+
/**
|
|
194
|
+
* Child-process entry for the Win32 folder dialog: blocks THIS process
|
|
195
|
+
* inside the modal `Show` so the host event loop stays live, reporting over
|
|
196
|
+
* the IPC channel. Spawned as a child process (not a worker thread) so the
|
|
197
|
+
* dialog is the process's first window and Windows activates it without a
|
|
198
|
+
* manual foreground call. Protocol: `{kind:'showing',threadId}` right
|
|
199
|
+
* before the blocking call (the driver's abort lever needs the native
|
|
200
|
+
* thread id), then exactly one of `{kind:'done',path}` or
|
|
201
|
+
* `{kind:'error',message}`.
|
|
202
|
+
*/
|
|
203
|
+
const title = process.env.DSH_DIALOG_TITLE ?? "";
|
|
204
|
+
if (title === "") throw new Error("win32-dialog-worker: DSH_DIALOG_TITLE is required");
|
|
205
|
+
if (process.send === void 0) throw new Error("win32-dialog-worker must run as a child process with an IPC channel");
|
|
206
|
+
const send = process.send.bind(process);
|
|
207
|
+
const post = (message) => {
|
|
208
|
+
/* v8 ignore next 3 -- disconnect needs a live IPC channel the unit lane must not sever (built-worker.e2e.ts owns the real close path). */
|
|
209
|
+
send(message, () => {
|
|
210
|
+
if (process.connected) process.disconnect();
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
/* v8 ignore next 3 -- the handler exits(0), which would kill the unit lane; built-worker.e2e.ts owns the real disconnect lifecycle. */
|
|
214
|
+
process.on("disconnect", () => process.exit(0));
|
|
215
|
+
(async () => {
|
|
216
|
+
try {
|
|
217
|
+
post({
|
|
218
|
+
kind: "done",
|
|
219
|
+
path: runFolderDialog(await loadWin32DialogBindings(), title, (threadId) => {
|
|
220
|
+
post({
|
|
221
|
+
kind: "showing",
|
|
222
|
+
threadId
|
|
223
|
+
});
|
|
224
|
+
})
|
|
225
|
+
});
|
|
226
|
+
} catch (error) {
|
|
227
|
+
post({
|
|
228
|
+
kind: "error",
|
|
229
|
+
message: error instanceof Error ? error.stack ?? error.message : String(error)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
})();
|
|
233
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepseek-ai/dsh-host-directory-picker-native",
|
|
3
|
+
"description": "Native-OS-chooser backend of the directory-picker seam for the DeepSeek Harness web GUI host",
|
|
4
|
+
"version": "0.0.1-rc.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "restricted"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/host/directory-picker-native"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": {
|
|
26
|
+
"types": "./lib/types/client/index.d.ts",
|
|
27
|
+
"default": "./lib/client.js"
|
|
28
|
+
},
|
|
29
|
+
"./worker": {
|
|
30
|
+
"types": "./lib/types/win32-dialog-worker.d.ts",
|
|
31
|
+
"default": "./lib/worker.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./src/*": "./src/*",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"lib/index.js",
|
|
38
|
+
"lib/invariant.js",
|
|
39
|
+
"lib/worker.cjs",
|
|
40
|
+
"lib/client.js",
|
|
41
|
+
"lib/types/**/*.d.ts"
|
|
42
|
+
],
|
|
43
|
+
"license": "BSD-3-Clause",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"koffi": "^3.1.0",
|
|
46
|
+
"@deepseek-ai/dsh-host-directory-picker": "^0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-native-command": "^0.0.1-rc.1"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^18.2.0",
|
|
51
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.1",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.1",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/react": "~18.3.1",
|
|
59
|
+
"react": "^18.2.0",
|
|
60
|
+
"tsx": "^4.19.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.0.1-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
64
|
+
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.1"
|
|
66
|
+
},
|
|
67
|
+
"dsh": {
|
|
68
|
+
"client": {
|
|
69
|
+
"inject": [
|
|
70
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
71
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
72
|
+
],
|
|
73
|
+
"platform": "web"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|