fix(dashboard): recognise Windows paths in the markdown path chip - #7969
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Purely widens path recognition behind the existing stat-probe gate; Windows users gain the intended click-to-open chip, and no visible surface, string, or affordance changes. [UX-REVIEWED] a44b51a |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Root-cause fix at the single POSIX-only predicate in an otherwise Windows-aware chain, with the UNC/SMB widening hazard explicitly closed and pinned by tests. Suggestions
[DESIGN-REVIEWED] a44b51a |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified. The First-Principles-Verdict: PASS Every item traces to the reported defect or the named external-content boundary, reuses the existing Windows predicate, and counts its one deferred sibling. What this change shipsIntent: make clicking a Windows path the agent wrote open the file instead of copying it — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] a44b51a |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
NicholasRBowers
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with clear root cause -- markdown path chip regex extended to recognise Windows drive/UNC paths, with test.
dda2892 to
70bc07d
Compare
|
|
70bc07d to
97eeed1
Compare
|
Fixed —
|
The path-chip pre-filter `isPathCandidate` required a forward slash and
admitted neither a backslash separator nor a drive-prefix colon, so every
Windows absolute path failed it. No candidate meant no stat probe, so the
span fell through to the generic click-to-copy inline-code branch: a Windows
user clicking a path the agent had just written got the address on their
clipboard instead of the file in the sidebar.
Accept either separator, add a drive-rooted shape whose colon precedes the
first separator, treat a Windows root as a positive signal by reusing the
existing WINDOWS_ABS_PATH_RE, and read the basename across either separator
so a dotted directory is not misread as the file's extension.
State the filename punctuation as a decided boundary instead of discovering
it one bug report at a time. Two review rounds each found one more legal
character -- parentheses (`C:\Program Files (x86)`) then an apostrophe
(`C:\Users\O'Neil`) -- so the rule is now: admit every character legal in a
filename on both platforms that is not a shell control operator, on both
shapes. In: `' ! # % = + , ( ) [ ] { }` alongside the existing set. Out, and
documented as such: `$` and backtick, `& ; |`, `< >`, `"`, `? *`, and `:`
outside the last segment. Widening the repertoire never widens the
positive-signal rule, so punctuated prose is still refused.
Refuse UNC by shape rather than by spelling. Windows reads ANY two leading
separators as a UNC root, so an alternation matching only `\\` or `//` left
the mixed `\/host\share\x.txt` and its `/\` mirror admitted -- the leading
separator is eaten by the relative-prefix group and the extension rule then
sends a real stat probe, which on Windows opens an outbound SMB connection
offering NTLM credentials. `/^[/\\]{2}/` closes the shape. The image path
(WINDOWS_ABS_PATH_RE) and the link path (MdAnchor) already hold this line;
the chip was the consumer-side predicate that had not.
17 new tests; reverting the component hunks turns 13 of them red.
97eeed1 to
a44b51a
Compare
|
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
On a Windows gateway, clicking a file path the agent just wrote in chat copies the
path to the clipboard instead of opening the file in the sidebar viewer. Reported
by a Windows desktop user on the latest version.
It affects every Windows absolute path, so on Windows the path chip — the
normal route from "the agent mentioned this file" to "the file is open in front of
me" — has never worked. Nothing looked broken, which is why it went unreported for
so long: the span still rendered as a chip and still did something on click, just
the wrong thing.
Why it matters
Two costs, one of them silent.
The visible one: every Windows user loses the click-to-open affordance entirely and
gets a clipboard write they did not ask for, with no way to tell that a different
behaviour was intended.
The silent one: the same pre-filter admitted
//host/share/x.txtas a pathcandidate, so rendering a message containing one made the dashboard ask the gateway
to stat it. On Windows that stat is an outbound SMB connection offering the host's
NTLM credentials. Since chat markdown can carry untrusted text (a fetched page, a
quoted file), that was a credential-leak vector reachable from rendering alone. It
is closed here — see the fourth bullet under What changed.
What changed (motivation → approach → change)
Symptom — a Windows path chip copies instead of opening.
Root cause —
isPathCandidateinwebsite/src/components/MarkdownRenderer.tsxis the pre-filter that decides whether an inline-code span is worth spending a stat
probe on. Its
PATH_SHAPE_RErequired a forward slash and admitted neither abackslash separator nor the colon in a drive prefix, so
C:\Users\me\notes.md,C:/Users/me/notes.mdandsrc\main.pyall failed it. No candidate means noprobe; no probe means
usePathKindnever reportsfile/dir; andInlineCodethen falls through to the
CopyableCodebranch, whose click handler copies. Thecopy was never a fallback for a failed path — it is the generic inline-code
affordance, and Windows paths were never recognised as paths at all.
The rest of the chain was already Windows-aware, which is what makes this a
one-place fix:
fileReadUrl.ts::isAbsolutealready classifies drive and UNCshapes,
MdImagealready routes a drive-qualifiedsrcthroughWINDOWS_ABS_PATH_RE(issue #3497), andsplitLineRef,activatePath,FilePathMenuand/api/revealare all separator-agnostic. Only the pre-filterwas POSIX-only.
Change — five parts, all in the pre-filter:
PATH_SHAPE_REaccepts either separator, which is what lets a relativeWindows path (
src\main.py,.\src\main.py) reach the probe.WIN_DRIVE_PATH_SHAPE_REcarries the drive-rooted form the general shapestructurally cannot: a drive prefix puts its colon before the first separator,
while the general shape allows a colon only in the last segment, where it serves
file:447.C:\Program Files (x86)is one of the most-trodden directories on Windows, so excluding itwould have left the defect in place for a large share of real paths — the first
revision of this PR did exactly that, and the GPT review lane caught it.
/Users/me/App (old).mdis the same filesystem convention, so it is admittedtoo rather than shipping an asymmetry that would just be the next report. A
closing paren may also end a path, so a directory named
App (old)classifies.This widens the character repertoire, not the positive-signal rule, so
parenthesised prose (
foo/bar (baz)) is still refused.UNC_PREFIX_RE(/^[/\\]{2}/) refuses any two leading separators,checked before every other rule because the others would readmit it — the
extension rule matches
\\\\host\share\x.txtand the leading-/rule matches//host/share/x. Per-CHARACTER, not per-spelling: Windows reads any two leadingseparators as a UNC root regardless of kind or order, so enumerating
\\\\and//would leave the mixed pairs\\/host\share\x.txtand/\\host\share\x.txtadmitted, and those resolve to the same share. This is the same line three other places in the
codebase already hold (
WINDOWS_ABS_PATH_REfor imagesrc,MdAnchorfor adecoded
//link destination, and the producer/consumer asymmetry documented onWIN_PRODUCER_PATH_RE); the chip was the one consumer-side predicate that hadnot adopted it.
WINDOWS_ABS_PATH_RErather than restating it, soC:\Windowsneeds noextension exactly as
/Usersdoes not. The extension gate now takes thebasename across either separator, so a dotted directory (
project\v1.2\notes)is no longer misread as an extension on the file.
Why widening the separator is safe. The existing "a bare two-segment
identifier with no extension is rejected" rule does the work: a
\-joinednon-path carries no extension, so
\n,HKEY_LOCAL_MACHINE\Software\FooandCORP\aliceare all still refused — on every platform, since the pre-filtercannot know the gateway's OS — and no request is issued for them. The probe
remains the decision; this only widens what is worth asking about.
Deliberately not in scope.
utils/terminalCompletion.tsandMarkdownPanel.tsx'sisAbscarry the same POSIX-only assumption on othersurfaces. They are separate behaviours with their own tests and are left alone
rather than folded in here; see Pattern harvest.
Tests
15 new tests in
website/src/test/MarkdownRenderer.test.tsx, 122 passing in thatfile (was 107).
Ten pin the pre-filter decision in isolation: drive-rooted paths in both separator
spellings; a drive-rooted path with no extension (and a bare
C:\root); UNCrefused in every spelling — both same-kind pairs, both MIXED pairs, and the Win32
extended-length prefix — with
/server/share/report.txtstill accepted to show oneslash is unaffected;
explicitly-relative and extension-bearing backslash paths; Unicode segments under
a drive root; backslash-joined non-paths still refused; the basename read across
either separator (
project\v1.2\notesvsproject\v1.2\notes.md); parenthesisedsegments on Windows and on POSIX; and parenthesised prose plus a parenthesised UNC
share still refused.
Five pin the rendered consequence, which is what the report was actually about: a
drive-qualified path renders as a
data-path-kind="file"chip and routes a clickto
onFileOpen;C:\Program Files (x86)\app\config.jsondoes the same; both drivespellings get probed while a backslash UNC path never does — the stub would have
answered
filefor it, so the absent request is the SMB guard's assertion; aWindows
file:linecitation carries its line through; and backslash text that isnot a path issues no probe at all and stays a copy chip.
Reverting only the component hunks and re-running turns 12 of the 15 red. The
three that stay green are pure negative guards, which held before the change too.
Five neighbouring suites that render chips or resolve Windows paths
(
MarkdownRendererCoverage,MarkdownRenderer.contextmenu,MarkdownRenderer.windowsImagePath,usePathKind,fileTokens) pass unchanged:115 tests.
tsc -bclean; eslint at its existing 597-warning ceiling, unchanged.Manual verification
Not performed — it is not reachable from a macOS host, and that is inherent to
the bug rather than a shortcut. The chip's affordance is gated on the gateway's
filesystem answering the stat probe, so confirming the fix end to end requires a
Windows gateway: on macOS no Windows path resolves, and the chip correctly stays
inert. The 15 tests above stand in by driving the same
pre-filter → probe → chip →
onFileOpenchain with the probe stubbed.What still wants a human on Windows: open a chat, have the agent write an absolute
path, and confirm a left click opens the sidebar viewer at that file (and at the
right line for a
path:42citation) rather than copying.Screenshots / video
Waived by the
no-screenshotslabel, applied by the maintainer.There is a rendered delta — on a Windows gateway the span gains the file glyph
and the confirmed-chip styling — but it is unreachable from the authoring host for
the same reason manual verification is: the chip only renders confirmed once the
gateway stats the path, and a macOS gateway stats no Windows path. Booting an
isolated instance to capture it was attempted twice (the repo dev stack and
kirocrew pod up) and both are blocked by sandbox permission errors on this host.Nothing about the chip's appearance is new; it is the existing confirmed-path
chip, already visible throughout the product for POSIX paths.
Related Issues
no linked issue: reported directly as customer feedback; no matching open issue
found (searched open issues for the windows/path/chip/copy terms).
Pattern harvest
Rule candidate:
review-promptPattern: a syntactic pre-filter over filesystem paths written against one
separator convention, in a codebase whose gateway can be Windows.
This one does generalize, and it has a specific shape worth catching: the shape
predicate was POSIX-only while every consumer downstream of it was already
Windows-aware, so the bug hid behind a chain that otherwise handled Windows
correctly.
utils/terminalCompletion.ts:238andMarkdownPanel.tsx:74carry thesame assumption today on other surfaces.
The second half is the one I would actually encode as a rule: when a
path-shaped predicate is widened, check whether a sibling predicate narrowed it on
purpose. Adding Windows support here naturally admitted UNC, and UNC is excluded
elsewhere in this codebase for a documented security reason — the widening would
have quietly reopened an SMB credential-probe vector that the image and link paths
both defend against.
A third, smaller lesson from the review round: a character-class allowlist over
real filenames is easy to under-build. Parentheses were missing from the first
revision, and
C:\Program Files (x86)is common enough that the fix would haveread as still broken to the very user who reported it.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)