bun-app-window 0.5.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,76 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.5.1] - 2026-10-21
8
+
9
+ **0.5.0's drag-and-drop fix was wrong and made dropped files be refused outright. This release replaces it, and adds folder support. The public API is unchanged.**
10
+
11
+ ### Fixed
12
+
13
+ - **Dropped files and folders are accepted again, and the page receives absolute paths.** 0.5.0 set `ICoreWebView2Controller4::put_AllowExternalDrop(FALSE)` on the belief that it stepped aside from WebView2's internal `IDropTarget` and let the OLE chain fall through to the parent window's `DragAcceptFiles` handler. **Both halves of that reasoning were wrong**:
14
+
15
+ 1. `DragAcceptFiles` only sets `WS_EX_ACCEPTFILES`; it does **not** register an `IDropTarget`. Reading the `OleDropTargetInterface` window property across the whole chain (`webview` → `webview_widget` → `Chrome_WidgetWin_0` → `Chrome_WidgetWin_1` → `Chrome_RenderWidgetHostHWND` → `Intermediate D3D Window`) returns false for every window. Modern Windows does not fall back to that legacy path during OLE hit-testing.
16
+ 2. `AllowExternalDrop` means *allow external drop*. `FALSE` refuses drops; it does not forward them.
17
+
18
+ The user-visible symptom was a red "not allowed" cursor over the window on every drag, with no drop and no `WM_DROPFILES`.
19
+
20
+ The fix uses the upstream API built for this case. With `FLAG_ACCEPT_DROP` on, the shell injects a script into the page that forwards the dropped objects to the host; the host reads them back with `ICoreWebView2WebMessageReceivedEventArgs2::get_AdditionalObjects` and converts each to an absolute path. **Pages still need no code of their own** — the listener is injected, like `window.__shell.recv`. On a runtime without `EventArgs2` the `QueryInterface` fails and every other web message keeps working.
21
+
22
+ The `WM_DROPFILES` path (`handle_drop`) stays as a fallback and keeps its own probe. Both paths emit the same shape.
23
+
24
+ ### Added
25
+
26
+ - **Folders now work, and `dirs` tells them apart.** A dropped folder arrives as a `FileSystemDirectoryHandle`, but on the host side that handle does **not** survive — measured: the collection holds one entry, `QueryInterface` to both `ICoreWebView2File` and `ICoreWebView2FileSystemHandle` fails, and the item is simply unusable. What *does* work is that a folder also appears in `dataTransfer.files` as an ordinary `File`, whose `get_Path` returns the correct path.
27
+
28
+ So the injected script posts **handles and files together**, the host tries `ICoreWebView2File` first and the handle second, and duplicate paths are collapsed. Because the surviving object is a `File`, `get_Kind` is unavailable — "is this a folder" is decided by `GetFileAttributesW` + `FILE_ATTRIBUTE_DIRECTORY`, the same predicate the `WM_DROPFILES` fallback uses, which is why both paths produce identical output:
29
+
30
+ ```json
31
+ { "t": "drop", "paths": ["<every path>"], "dirs": ["<the subset that are folders>"] }
32
+ ```
33
+
34
+ `paths` keeps every entry in order; `dirs` is a subset and is always present (possibly empty).
35
+
36
+ - **The host now always answers.** Previously, when `AdditionalObjects` yielded no usable path, the handler returned silently — the page waited, the host said nothing, and neither side had a clue. Every drop now produces exactly one `shell-msg`, carrying `ok`, `err`, and the counts `nFile` / `nHandle` / `nNoPath` that made this diagnosis possible in the first place.
37
+
38
+ - **`scripts/probe-drop-live.ts`** — an interactive probe for the half that cannot be synthesised. `AdditionalObjects` only ever contains objects produced by a real drag; page JavaScript cannot fabricate one with a path, and `keybd_event` cannot hold a mouse button across processes. The probe opens a window, waits for one manual drop, and does the judging automatically: the page reports the received payload back over `fetch`, and the probe asserts paths are non-empty, absolute, existing, and classified exactly as the filesystem says. Twelve checks; a failure names *which* of the five gates closed.
39
+
40
+ `--file` and `--dir` are mutually exclusive — one target per run, because a run that asks for both will always fail the one you did not drag. It must run **non-elevated**; the file comment says why and how.
41
+
42
+ ### Note
43
+
44
+ - **This release went through five rounds of "nothing happens".** Each was a genuinely different cause with an identical symptom, and only the last two were in code this release changed. They are recorded as `docs/pitfalls.md` 2.16 and 2.17, and the injected script now carries a marker for each so the next failure names its own stage:
45
+
46
+ | Round | Cause | What made it visible |
47
+ | --- | --- | --- |
48
+ | 1 | The host ran elevated (High integrity) while Explorer runs at Medium; UIPI refuses the drag before any message is sent | Not a single `dragenter` |
49
+ | 2 | `preventDefault()` alone does not accept a drop — `dataTransfer.dropEffect` stayed `none` | `dragover=72` with `drop=0` |
50
+ | 3 | `chrome.webview.postMessage` is *not* the pair of `get_AdditionalObjects`; `File` serialises to `{}`, so the collection was empty while the call succeeded | `drop=1 files=1`, `count=0` |
51
+ | 4 | The probe reported its own post as failed — it fetched the marker 250 ms later, after the host had already closed the window | the only red check in an otherwise green run |
52
+ | 5 | A folder's `FileSystemHandle` does not survive the bridge; only the `File` in `dataTransfer.files` carries a path | `nHandle=0` while the collection was non-empty |
53
+
54
+ Rounds 3 and 5 are the same lesson twice: **a call that does not throw is not a call that worked.** `__shellDropReady` / `__shellDropPosted` / `__shellDropVia` / `__shellDropError` now exist so "did the injection run", "which route was posted", "was the drop delivered" and "did the host receive a usable object" are four separate observations instead of one silent guess.
55
+
56
+ ### Documentation
57
+
58
+ - `docs/pitfalls.md` gains 2.15 (`AllowExternalDrop(FALSE)` refuses drops), 2.16 (the four gates, with the measured reading at each) and 2.17 (folder handles do not survive); 2.14 now points forward at them instead of recommending the wrong fix.
59
+ - `AGENTS.md` replaces the "step aside from WebView2's `IDropTarget`" rule with the `AdditionalObjects` route, states the file/folder asymmetry and the always-answer rule, and registers the new probe.
60
+ - Three READMEs state that files **and folders** arrive with absolute paths, show how to tell them apart with `dirs`, and confirm the page needs no `drop` handler of its own.
61
+ - `tests/capabilities.test.ts` pins all four gates plus the folder route statically, including that `chrome.webview.postMessage(` must not reappear.
62
+
63
+ ### Verification
64
+
65
+ | Command | Result |
66
+ | --- | --- |
67
+ | `bun check` | 69 files |
68
+ | `bun test` | 558 pass / 0 fail |
69
+ | `bun run scripts/gate.ts` | PASSED |
70
+ | `bun run scripts/verify-dll.ts` | PASSED, same source as `native/shell.cc` (1,016,832 B) |
71
+ | `scripts/probe-window-events.ts` | PASSED (21 checks) |
72
+ | `scripts/probe-accelerator-keys.ts` | PASSED (6 checks) |
73
+ | `scripts/probe-drop.ts` | PASSED (6 checks) |
74
+ | `scripts/probe-drop-live.ts --file` | **PASSED (12 checks)** — one real file drag, absolute path |
75
+ | `scripts/probe-drop-live.ts --dir` | **PASSED (12 checks)** — one real folder drag, path **and** folder classification |
76
+
7
77
  ## [0.5.0] - 2026-10-21
8
78
 
9
79
  **Upgrading is enough for the first two items — no code changes on your side.** The third changes how you build a single-file executable, and it fixes something that never worked.
@@ -394,7 +464,8 @@ First public release.
394
464
  - Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
395
465
  - No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
396
466
 
397
- [Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.5.0...HEAD
467
+ [Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.5.1...HEAD
468
+ [0.5.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.5.0...v0.5.1
398
469
  [0.5.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.1...v0.5.0
399
470
  [0.4.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.0...v0.4.1
400
471
  [0.4.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.3.0...v0.4.0
package/README.md CHANGED
@@ -522,6 +522,17 @@ window.addEventListener("shell-msg", (e) => {
522
522
  });
523
523
  ```
524
524
 
525
+ Files **and folders** are forwarded by a script the shell injects, so the page does **not** need its own `drop` handler. Every entry is an **absolute path** (from `ICoreWebView2File::get_Path`), and `dirs` tells you which of them are directories:
526
+
527
+ ```js
528
+ window.addEventListener("shell-msg", (e) => {
529
+ if (e.detail.t !== "drop") return;
530
+ for (const p of e.detail.paths) {
531
+ console.log(e.detail.dirs.includes(p) ? "folder" : "file", p);
532
+ }
533
+ });
534
+ ```
535
+
525
536
  When running in an elevated administrator process (High Integrity Level) while Explorer runs at Medium Integrity Level, the shell automatically whitelists drop messages across UIPI via `ChangeWindowMessageFilterEx`.
526
537
 
527
538
  ### 4. Recommended Desktop CSS Reset
package/README.zh-CN.md CHANGED
@@ -504,6 +504,17 @@ window.addEventListener("shell-msg", (e) => {
504
504
  });
505
505
  ```
506
506
 
507
+ 落进窗口的**文件与文件夹**都由壳层注入的脚本自动转发,页面**不需要**自己监听 `drop`。每条都是**绝对路径**(来源 `ICoreWebView2File::get_Path`),而 `dirs` 告诉你其中哪些是目录:
508
+
509
+ ```js
510
+ window.addEventListener("shell-msg", (e) => {
511
+ if (e.detail.t !== "drop") return;
512
+ for (const p of e.detail.paths) {
513
+ console.log(e.detail.dirs.includes(p) ? "文件夹" : "文件", p);
514
+ }
515
+ });
516
+ ```
517
+
507
518
  针对管理员提权场景(High 完整性级别进程接收来自 Medium 级别资源管理器的消息),底层已通过 `ChangeWindowMessageFilterEx` 自动放行 UIPI 消息过滤。
508
519
 
509
520
  ### 4. 前端页面桌面化样式建议
package/README.zh-TW.md CHANGED
@@ -504,6 +504,17 @@ window.addEventListener("shell-msg", (e) => {
504
504
  });
505
505
  ```
506
506
 
507
+ 落進視窗的**檔案與資料夾**都由殼層注入的腳本自動轉發,頁面**不需要**自己監聽 `drop`。每條都是**絕對路徑**(來源 `ICoreWebView2File::get_Path`),而 `dirs` 告訴你其中哪些是資料夾:
508
+
509
+ ```js
510
+ window.addEventListener("shell-msg", (e) => {
511
+ if (e.detail.t !== "drop") return;
512
+ for (const p of e.detail.paths) {
513
+ console.log(e.detail.dirs.includes(p) ? "資料夾" : "檔案", p);
514
+ }
515
+ });
516
+ ```
517
+
507
518
  針對管理員提權場景(High 完整性層級行程接收來自 Medium 層級檔案總管的訊息),底層已透過 `ChangeWindowMessageFilterEx` 自動放行 UIPI 訊息過濾。
508
519
 
509
520
  ### 4. 前端頁面桌面化樣式建議
package/dist/libshell.dll CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bun-app-window",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "description": "Native WebView2 window shell for Bun — frameless or system-decorated windows, DPI-aware geometry, typed IPC, system-accurate title bar",
6
6
  "type": "module",
7
7
  "license": "MIT",