[Bug] revealNativePath ("Show in File Explorer") silently fails for non-ASCII/CJK paths on Windows #6259
Replies: 3 comments
|
Follow-up: one correction, more measurements, and a tested patch Further testing on the same host produced a correction to my original report and narrowed the failure condition. Summary first, evidence below. 1. Correction:
|
argument after /select, (ASCII path) |
file selected |
|---|---|
C:\...\a,b.txt (literal path) |
❌ Explorer truncates at the comma |
file:///C:/.../a%2Cb.txt (%2C escaped) |
✅ |
So the file URI form is not gratuitous: it is what currently makes comma-containing paths work, and simply passing windowsPath would fix CJK while silently breaking that.
2. Narrowed failure condition: non-ASCII percent-escapes never resolve
The same CJK path escaped with the system ANSI code page (GBK/936) instead of UTF-8 also fails, so this is not a UTF-8-vs-ACP decoding mismatch — Explorer resolves a file:// URI only while every escape it carries is ASCII. An unescaped (raw CJK) URI resolves fine.
argument after /select, (CJK path I:\工作文档\…\完形高频固定搭配.docx) |
file selected |
|---|---|
literal path I:\工作文档\… |
✅ |
raw CJK URI file:///I:/工作文档/… |
✅ |
UTF-8-escaped URI file:///I:/%E5%B7%A5… ← current code |
❌ |
GBK-escaped URI file:///I:/%B9%A4… |
❌ |
3. Full argv matrix — what is reachable from Node
Selection verified through Shell.Application.Windows().Document.FocusedItem.Path, not through exit codes. execFile shapes:
| target | argv | result |
|---|---|---|
| CJK | ['/select,', path] |
✅ |
| CJK | ['/select,' + path] |
✅ |
| CJK | ['/select,', '"' + path + '"'] |
❌ libuv escapes the embedded quotes |
| CJK | ['/select,"' + path + '"'] |
❌ same |
| comma | any of the four shapes | ❌ |
From Node, no execFile argv shape covers both cases. Only a verbatim command line does:
spawn('explorer.exe', [`/select,"${path}"`], { windowsVerbatimArguments: true, detached: true, stdio: 'ignore' })| target | verbatim result |
|---|---|
| CJK path | ✅ |
| ASCII path with comma | ✅ |
| CJK + comma + space in one name | ✅ |
4. Suggested patch (attached, git apply --check-clean against master)
Keep the URI form where it is the only thing that survives a comma, and hand Explorer the literal path otherwise — no regression for ASCII paths, CJK fixed:
- // Explorer parses commas itself; a file URI preserves commas and whitespace in the path.
- const target = pathToFileURL(windowsPath, { windows: true }).href.replaceAll(',', '%2C')
+ const target = /^[\x20-\x7E]*$/.test(windowsPath)
+ ? pathToFileURL(windowsPath, { windows: true }).href.replaceAll(',', '%2C')
+ : windowsPathIf comma support for non-ASCII paths is also required, the verbatim form above is the complete fix — but it needs the shared runner to expose a verbatim argv path (or a local spawn), which trades away the runner's "argv, never a shell string" property. That is why it is not my primary suggestion.
5. Test vector
A unit test over revealNativePath() internals with a non-ASCII windowsPath asserting the argv handed to the runner is /select, + the literal path (not a file:// URI), plus one with a comma-containing ASCII path asserting the URI form is kept. On Windows, assert Shell.Application.Windows().Document.FocusedItem.Path rather than the command's exit code — Explorer's exit 1 is not evidence of success, which is exactly how this stayed silent.
|
Your correction in the follow-up is the most useful thing in this thread — and there is one more consequence of it that I think changes the shape of the right fix. 1. Your patch's "safe" branch is the one that is brokenYour split keeps the URI form for pure-ASCII paths and hands over the literal path otherwise: const target = /^[\x20-\x7E]*$/.test(windowsPath)
? pathToFileURL(windowsPath, { windows: true }).href.replaceAll(',', '%2C')
: windowsPathBut your own matrix says:
So the discriminator that matters is not "is the path ASCII" but "does the path contain a comma" — and those two predicates are independent. Your branch assigns each case by the wrong one: ASCII-without-comma would be sent as a URI where the literal path also works, and (the real bug) a path that is non-ASCII and contains a comma still falls into the literal branch and still breaks, which you already demonstrated in your verbatim-argv matrix ("CJK + comma + space in one name" only passes under That makes the shipped patch a strict improvement over today with a residual hole that is easy to miss, since it looks fully general. 2.
|
|
Thanks — the test pinning is the part I could not see from outside, and it changes what a patch has to contain. I verified your citations against Verified as stated
Correction 1: the predicate swap changes no resolution outcomeYou are right that the discriminator is the comma, not ASCII, and I have adopted that. But the comma predicate does not shrink the broken set — it relabels it. All four cases, measured:
The residual hole is identical ( I also measured the pinned vector itself on Windows —
So Correction 2:
|
| path | result |
|---|---|
C:\...\my files\报告,#%.txt (non-ASCII + comma + # + % + space) |
✅ |
C:\...\my files\a,b.txt (comma) |
✅ |
I:\工作文档\...\完形高频固定搭配.docx (non-ASCII) |
✅ |
C:\...\my files\plain name.txt (space) |
✅ |
Cost: ~320 ms cold, ~174 ms warm for the powershell.exe hop.
Your point 5 comes along for free. With PowerShell as the launcher, the exit status is trustworthy — there is no "Explorer delegated to the running desktop process and exited 1" case to tolerate. So patch v3 drops the tolerance: cancellation still wins, but a real failure now reaches the caller instead of rendering as presented.revealed.
Test changes that must accompany either patch
| site | change |
|---|---|
:334 win32 row |
command/args become powershell.exe + the script; the URI expectation goes away |
:350 WSL translation |
same shape; assert the script carries the literal C:\work\报告.txt |
:383-390 "accepts Explorer delegate exit 1" |
invert: with a trustworthy launcher this rejects (v3), or keep with v2 |
:392-396, :398-405 |
unchanged |
:408-415 special characters |
both vectors assert the script with the literal path; keep the UNC comma vector |
The two vectors in :408-415 inject run and assert argv only, exactly as you said — which is why the resolution behavior never entered the suite. A Windows-gated case asserting FocusedItem.Path (or a smoke test that the launcher argv reaches Explorer) is the observable that would have caught this; I could add one if you want it in the same patch.
Patch
Discussions has no attachments, so the change is inline. This is v3 (recommended): the PowerShell literal route plus the removal of the delegated-exit-1 tolerance. It is git apply --check-clean against dsh-v0.1.5-rc.2.
--- a/packages/util/native-command/src/path-opener.ts
+++ b/packages/util/native-command/src/path-opener.ts
@@ -11,7 +11,6 @@
import { release as osRelease } from 'node:os'
import { dirname, extname } from 'node:path'
-import { pathToFileURL } from 'node:url'
import { runNativeCommand, type NativeCommandRunner } from './runner.ts'
/** Testable command boundary; native implementations never invoke a shell. */
@@ -244,14 +243,24 @@
windowsPath = translated.stdout.replace(/[\r\n]+$/, '')
if (windowsPath === '') throw new Error('wslpath returned no Windows path')
}
- // Explorer parses commas itself; a file URI preserves commas and whitespace in the path.
- const target = pathToFileURL(windowsPath, { windows: true }).href.replaceAll(',', '%2C')
+ // Explorer parses commas itself, and it resolves a file:// URI only while every
+ // escape it carries is ASCII: pathToFileURL() percent-encodes non-ASCII as
+ // UTF-8, which Explorer cannot resolve, so it silently falls back to its default
+ // folder instead of selecting the file. No argv shape an execFile runner can
+ // produce covers a path that carries both a comma and a non-ASCII character, so
+ // hand the quoted literal path to PowerShell, which passes it through verbatim —
+ // the same launcher openWindowsPath() already uses for the default-application
+ // intent on Windows.
+ const reveal = `Start-Process explorer.exe -ArgumentList ('/select,"' + ${powershellLiteral(windowsPath)} + '"')`
try {
- await run('explorer.exe', ['/select,', target], signal)
+ await run('powershell.exe', ['-NoProfile', '-Command', reveal], signal)
} catch (error) {
+ // Cancellation keeps priority over the launcher's own failure. Unlike a direct
+ // explorer.exe call, this launcher's exit status is trustworthy, so no
+ // delegated-exit-1 tolerance is applied: a failure reaches the caller instead
+ // of being reported to the user as a successful reveal.
signal.throwIfAborted()
- // Explorer can exit 1 after delegating to the existing desktop process.
- if (!(error instanceof Error) || !('code' in error) || error.code !== 1) throw error
+ throw error
}
return
}A conservative variant keeps the original catch body (exit-1 tolerated) with the same launcher change — a smaller behavioural diff if you would rather not change failure reporting in the same commit. Say which shape you want and I will fold the spec updates into the same patch.
Uh oh!
There was an error while loading. Please reload this page.
Title:
[Bug] revealNativePath ("Show in File Explorer") silently fails for non-ASCII/CJK paths on WindowsTL;DR(中文):Windows 上「在文件资源管理器中显示」对含中文的路径无效。
revealNativePath()把路径转成百分号编码的file://URL 再交给explorer.exe /select,,而 Explorer 的/select,只认字面文件路径、不做 URL 解码,于是定位失败并静默回退到默认文件夹(桌面);同时 explorer 的退出码 1 被容忍,UI 还显示"已请求…"的假成功。纯 ASCII 路径正常,所以现有测试覆盖不到。Package:
@deepseek-ai/dsh-native-command@0.1.5-rc.1File:
packages/util/native-command/src/path-opener.ts→revealNativePath()Host: Windows 11 23H2, build 22631.4890, zh-CN (ACP 936), Node v24.19.0
Verified against:
master(source fetched 2026-09-11 — the code below is still present)Summary
On a Windows host, the "Show in File Explorer" action of a presented deliverable does nothing when the file path contains non-ASCII characters. The request reports success and the UI shows a success status, so the failure is silent and looks like a client-side or OS problem.
Steps to reproduce
I:\工作文档\测试\report.docx.presentthe file, then pick 在文件资源管理器中显示 / Show in File Explorer from the card menu.presented.revealed), but no Explorer window selects the file — usually a window opens at the Desktop instead.Expected
Explorer opens the containing folder with the file selected.
Actual
Nothing is selected. Silent failure, reported to the UI as success.
Root cause
pathToFileURL()percent-encodes non-ASCII path segments as UTF-8, so the argument becomes:explorer.exe /select,takes a literal filesystem path and never URL-decodes its argument, so the target cannot be resolved. Explorer falls back to its default folder instead of failing loudly, and the tolerated exit code 1 meansrevealNativePath()resolves normally — the caller has no way to detect the failure.Minimal reproduction (no dsh required)
Measured results (clean baseline, selection verified via
Shell.Application.Windows().Document.FocusedItem.Path):/select,I:\工作文档\…\完形高频固定搭配.docx(literal path)file:///I:/工作文档/…/完形高频固定搭配.docx(raw CJK URI)file:///I:/%E5%B7%A5%E4%BD%9C…/%E9%85%8D.docx(percent-encoded — current code)file:///C:/Windows/win.ini(ASCII URI)The last row is why this went unnoticed: a URI with nothing to escape works, so the code behaves correctly for ASCII-only paths.
Suggested fix
Hand Explorer the literal path:
If comma preservation is genuinely required, note that the current approach probably does not achieve it either — Explorer does not URL-decode
%2C, so a path containing a comma is likely broken today as well. Worth verifying before keeping the URI form; a conservative alternative is to keep it only for pure-ASCII paths:Suggested test vector
A unit test over
revealNativePath()internals with a non-ASCIIwindowsPathasserting the argv handed to the runner is/select,+ the literal path (not afile://URI). A Windows-only integration test that assertsShell.Application.Windows().Document.FocusedItem.Path, rather than the command's exit code, would have caught this.Scope
Any Windows host whose workspace path contains non-ASCII characters — including a Web UI served from such a host, since the command runs on the Host, not in the browser.
All reactions