Skip to content

Raise the Explorer window when "Open in Explorer" is used (Windows) - #1051

Closed
planb788 wants to merge 4 commits into
agegr:mainfrom
planb788:fix/windows-raise-explorer-window
Closed

planb788 wants to merge 4 commits into
agegr:mainfrom
planb788:fix/windows-raise-explorer-window

Conversation

@planb788

@planb788 planb788 commented Oct 3, 2026

Copy link
Copy Markdown

Raise the Explorer window when "Open in Explorer" is used (Windows)

Problem

POST /api/open-in-explorer spawns explorer.exe <cwd>. The folder does open, but
Explorer does not bring the window in front of the other applications the user
already has open, so the window appears behind all of them and the button looks
like it did nothing. Reproduced here by clicking the sidebar button with a
terminal and two chat windows open: the folder window came up fully covered.

Fix

lib/open-in-file-manager.ts keeps spawning the same file manager command, then
on Windows only follows it with a short-lived PowerShell helper
(fileManagerFocusCommand) that

  1. polls the shell's own window list (Shell.Application) until a window shows the
    folder — Explorer creates it asynchronously, after the launch has returned,
  2. restores it if it was minimized,
  3. raises it with SetWindowPos(HWND_TOPMOST … HWND_NOTOPMOST), and
  4. asks for the foreground as a best effort.

Why z-order instead of the foreground

Windows only grants the foreground to a process the user last interacted with,
and the process that receives the click is the browser, not the server, so
SetForegroundWindow fails from here (verified: it returns false). The usual
workaround — synthesizing a bare Alt press to unlock it — makes the window that
was in front re-activate itself, so the folder loses the race to a chat app
(measured 4/5 on this machine). SetWindowPos carries no such restriction.

A plain HWND_TOP raise is not enough on its own: measured with two throwaway
Explorer windows, one opened over the other, HWND_TOP leaves the covered
window below (index 4 under index 3), because Windows draws the active window
above the rest of its band. The momentary topmost pass is what carries it past
that: with a 30 ms gap the window ends up above its cover and the topmost flag
dropped (verified indices and WS_EX_TOPMOST with EnumWindows/GetWindowLong).
The gap is slack, not load-bearing — every gap from 0 ms to 200 ms lands in the
same place — and it stays short on purpose: the one way this can misbehave is
the helper being killed between the two calls, which leaves the folder pinned
above everything until it is closed or Always on top is unticked in its
context menu. A test asserts the gap stays at or under 100 ms. It runs after the
response, so the request does not wait for it, and it is best effort throughout:
the folder is already open by the time it polls, so a missing PowerShell, a
blocked Add-Type, or a window that never appears must not fail the request
that opened the folder.

Other details

  • The helper is spawned without detached: true: DETACHED_PROCESS leaves
    PowerShell without a console and it exits without running the script. unref()
    is what keeps the request from waiting for it.
  • The folder is matched through normalizeExplorerLocationUrl(), because
    Explorer's LocationURL and pathToFileURL() disagree on escaping and on the
    trailing slash. The path is embedded in the script as a single-quoted
    PowerShell string, so any character a path can contain survives.
  • IsIconic gates the SW_RESTORE: an unconditional restore would shrink a
    maximized window back to a normal one, so the helper returns minimized
    windows and leaves every other state alone.
  • Finder and desktop file managers activate their own windows, so they keep the
    old behaviour.
  • -ExecutionPolicy Bypass only overrides a user-level preference such as a
    box left on AllSigned (which really does refuse -EncodedCommand); a Group
    Policy lockdown or ConstrainedLanguage still wins, and then the helper dies
    quietly with the window merely opened. -EncodedCommand lands in Script
    Block Logging in the clear (verified in event 4104), so in an audited shop
    this reads as machine-generated automation rather than anything hiding.
  • PI_WEB_FILE_MANAGER_FOCUS=0 turns the raise off.

Testing

  • lib/open-in-file-manager.test.mjs: 6 tests (URL folding, the focus command
    per platform, the raised script including the maximized-window gate and the
    short topmost gap, PowerShell string escaping, the env switch). npm test,
    tsc --noEmit, and npm run lint are unchanged otherwise (the 12 failures in
    npm test are pre-existing on Windows: PTY, worktree, symlink and PATH
    cases).
  • Manually on Windows 11, measuring the z-order instead of trusting a
    screenshot: from behind 37 other windows the folder ends up above every normal
    window, a minimized window is restored and raised, the window is not left
    pinned, and five simultaneous clicks cost 0.83 s in total. An A/B check with
    two throwaway Explorer windows shows HWND_TOP alone cannot pass the active
    window while the 30 ms topmost pass can.

agegr and others added 4 commits October 3, 2026 23:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restricted stops script files, not Add-Type; the Bypass flag only overrides
a per-machine preference, while Group Policy and ConstrainedLanguage still
win and the helper then dies quietly by design.

Co-Authored-By: pi <pi@local>
SW_RESTORE on a maximized window shrinks it to a normal window, which is a
gratuitous layout change the user never asked for. Gate the restore behind
IsIconic so the helper touches minimized windows and nothing else. The
raised state it produces is identical either way.

Co-Authored-By: pi <pi@local>
AllSigned really does refuse -EncodedCommand, while a default Restricted box
runs the helper either way, so say exactly that. -EncodedCommand lands in
Script Block Logging in the clear (verified in event 4104), which is what an
auditor wants to see.

Co-Authored-By: pi <pi@local>
@planb788
planb788 force-pushed the fix/windows-raise-explorer-window branch from ebb5c2a to 8ffd962 Compare October 3, 2026 16:00
@agegr

agegr commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Thanks for the detailed write-up and the careful measurements. We're going to close this one for now.

We tested the current version on Windows. Clicking "Open in Explorer" brings the folder window up above all other windows. We couldn't reproduce the window landing behind other apps, so we don't have a problem here that this change would fix.

Even if it does reproduce in some setups, we'd rather not ship this approach:

Security tools may flag it. A hidden PowerShell process with -EncodedCommand, -ExecutionPolicy Bypass and Add-Type user32 P/Invoke is the kind of pattern antivirus and EDR tools flag. pi-web runs on a lot of work machines.
We can't maintain it. It is about 200 lines of Windows-only code that we can't test day to day.

Two smaller notes, in case you want to keep the branch:

Unrelated AGENTS.md changes. The first commit adds about 138 lines to AGENTS.md: duplicated file-map entries and topic notes that now live in docs/agents/.
A test fails off Windows. the raise helper waits for the folder window and brings it forward fails on macOS and Linux. pathToFileURL() turns D:\work\my repo into a relative POSIX path there, and the backslashes are not escaped in the regex.

If you can still reproduce the original problem, please open an issue. Include your Windows version, the browser you use, how pi-web was started (terminal, service, startup task), and any focus or foreground settings you've changed. We're happy to look at a lighter fix once we can reproduce it.

@agegr agegr closed this Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants