feat(terminal): cmd-click bare file paths to open them - #73
Open
jiweiyuan wants to merge 6 commits into
Open
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
jiweiyuan
force-pushed
the
feat/clickable-file-paths
branch
from
July 24, 2026 20:03
cec3e5b to
07d2273
Compare
Agent CLIs print bare file references like `BranchModel.swift`, `src/foo.ts:42`, or `App.swift:88:15`. These are plain text — not OSC 8 hyperlinks and not a URL scheme — so libghostty's link detector never lights them up (its regex only matches real schemes, and Ghostty upstream has declined to add row/column parsing). VS Code makes the same paths clickable with its own terminalLinkParsing layer; what makes that layer stable rather than a false-positive machine is filesystem validation, not the regex. Mirror that shape: on a cmd-click that libghostty didn't resolve to a link, reconstruct the token under the click from the grid text (readViewportText), peel a trailing `:line[:col]`, resolve it against the surface's working directory, and open it in the read-only preview only if it resolves to a real file on disk. An imprecise click column or a stray word is silently dropped — every candidate on the row is validated and the nearest hit wins, so column precision is never load-bearing. - TerminalPathScanner: pure, tested token → path/line parsing + fs gate - TermioStore.openBarePathUnderCommandClick: click → cell → row → open, reusing the wrapper's own point→cell convention and the delegate-is-TerminalViewState surface lookup - wired as a fallback in the existing cmd-click monitor; the hovered-URL path (OSC 8 / detected URLs) is unchanged
A single base directory is not enough to resolve a clicked bare path. The OSC 7 working directory is stale whenever the shell doesn't report it — it still reads `~` after a plain `cd` — so `package.json` misses even though the file is right there. Resolve against an ordered list of roots instead, most-specific first: the live kernel cwd (PROC_PIDVNODEPATHINFO, which tracks `cd` with no shell cooperation), then the session's worktree and its project root. The filesystem gate still decides, so extra roots only ever add hits that are real files.
A path that contains a space (`Application Support/settings.json`, `my notes.md`) is torn in half by the whitespace split, so rejoin adjacent tokens into wider candidates — but only spans that cover the clicked column, so the widening can never wander onto a file the user never pointed at. Also try the shell-unescaped spelling, since a shell echoes `my\ file.ts` for a name that has no backslash on disk. Skip the fallback entirely on an `ssh` session: those paths live on the remote box, and `src/main.rs` exists on both machines, so resolving against the local filesystem would quietly open the wrong file. Drop the bring-up NSLog trace.
…t ssh The fallback skipped `ssh` sessions but not termiod ones, and a termiod session runs the same `.inMemory` backend as a local shell — so a path printed by a shell on a VPS was resolved against this Mac's filesystem, and `src/main.rs` exists on both. Ask the device instead: `isOnThisMac`, over the `termiodRemoteHost ?? sshHost` rule that `DeviceContext` owns. That rule was written out by hand in three places, which is how one copy came to know about only one of the two ways of being elsewhere. Give it a single home (`deviceAlias(of:)`) and route the others through it. An unidentifiable surface is now declined rather than assumed local, and the selected session is no longer a resolution root — on a multi-device window it can belong to another machine entirely.
jiweiyuan
force-pushed
the
feat/clickable-file-paths
branch
from
August 17, 2026 16:43
93e735c to
00c9038
Compare
`openTerminalLink` resolves a scheme-less link against local disk after only a `fileExists` check. Both link detectors end there, and the device gate sat on one of them, so the bug the fallback just fixed was still live through the front door: a bare path printed by a session on a VPS opened this Mac's file of the same name. It is live, not latent. Our pinned libghostty (1.0.16, ghostty 9d8fbd1) has three branches in its URL regex, not one — the third matches bare relative paths like `src/main.rs`, and `MOUSE_OVER_LINK` reports the matched text verbatim. An OSC 8 target needs no detection at all: ghostty hands back the URI a program wrote, whatever it says. So gate where the resolution happens rather than where a path is spotted. `openTerminalLink` now takes the session the link was clicked in and asks `isOnThisMac` before it touches the filesystem; a real scheme is untouched, being a handler's business and not this Mac's disk. The hovered-link path finds its surface by geometry, the same way the bare-path fallback does, and a surface that can't be named is declined rather than assumed local. Relative links also resolve against the clicked session's own workspace now. The selected session stood in before, and on a multi-device window that is another machine's tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Agent CLIs print bare file references —
BranchModel.swift,src/foo.ts:42,App.swift:88:15. They are plain text: not OSC 8 hyperlinks, not a URL scheme, so libghostty's link detector doesn't light most of them up, and upstream has declined row/column parsing.TerminalLinkOpeningand the cmd-click interceptor already route everything libghostty does detect; this PR is the fallback for everything it doesn't.VS Code makes the same paths clickable with its own
terminalLinkParsinglayer. What keeps that layer stable rather than a false-positive machine is not the regex — it is that every candidate is validated against the filesystem before it becomes a link. This mirrors that shape.What it does
On a cmd-click that libghostty did not resolve to a link, reconstruct the token under the pointer from the grid text, peel a trailing
:line[:col], resolve it against the surface's working directory, and open it in the read-only preview only if it resolves to a real file on disk. Anything that doesn't validate is silently dropped, so an imprecise click column or a stray word never opens the wrong thing — the click just falls through to the terminal as before.TerminalPathScanner— pure, tested token → path/line parsing plus the filesystem gate. Handles:line/:line:colsuffixes, wrapping punctuation ((file.swift:10).), git-diffa/b/prefixes, shell backslash escapes (my\ file.ts), and paths containing spaces (adjacent tokens are rejoined, but only into spans that cover the clicked column, so widening can never wander onto a file the user never pointed at).TermioStore.openBarePathUnderCommandClick— click → cell → row → open, reusing the wrapper's own point→cell convention and finding the surface by geometry rather thanhitTest(ghostty disables link detection under mouse reporting, so the fallback must not depend on the z-orderhitTestgives).PROC_PIDVNODEPATHINFO, which tracks a plaincdeven when the shell never emits OSC 7), then the session's worktree and project root. The OSC 7 cwd alone is stale too often — it still reads~after acd, which is exactly whenpackage.jsonshould still open.Device correctness
Anything that reads local disk may only ever run for a session whose filesystem is this Mac's. The original branch skipped
sshHostsessions; that was written before termiod, and a termiod remote session runs the same.inMemorybackend as a local shell — so a path printed by a shell on a VPS would have been resolved against this Mac, wheresrc/main.rsalso exists.It now asks the device:
isOnThisMac, over thetermiodRemoteHost ?? sshHostruleDeviceContextowns. That rule was spelled out by hand in three places, which is how one copy came to know about only one of the two ways of being elsewhere; it now has a single home (deviceAlias(of:)) and the other call sites route through it. A surface that can't be identified is declined rather than assumed local, and the selected session is no longer a resolution root — on a multi-device window it can belong to another machine entirely.The gate now sits at both places that read local disk, because they are two resolution points, not one. The fallback scanner validates candidates itself and never calls
openTerminalLink, so its gate stays.openTerminalLinkis where every link libghostty did detect lands, and its no-scheme branch resolved a path against local disk after only afileExistscheck — no device check at all. It now takes the session the link was clicked in and asks the same question before touching the filesystem; the hovered-link path finds its surface by geometry the way the fallback does, and an unnameable surface is declined. Real schemes (http,https,mailto,file:) are untouched — a handler's business, not this Mac's disk. Relative links from a surface also resolve against that session's own workspace now, instead of the selected session's.That hole is live at our pin, not theoretical. The URL regex compiled into libghostty-swift 1.0.16 (ghostty
9d8fbd1) has three branches, not one: scheme URLs, rooted/dot-relative paths, and bare relative paths (src/main.rs).MOUSE_OVER_LINKreports the matched text verbatim, and an OSC 8 target is whatever string a program wrote — so a scheme-less path from a remote session already reached that branch.Measured against the shipped regex, ghostty detects
src/main.rs,Sources/App/App.swift:88:15(suffix included in the match),./package.json,/etc/hosts,a/Sources/App.swift. It does not detect a filename with no slash (BranchModel.swift,package.json,config.yaml:3) or any path containing a space, and it refreshes hover links only while mouse reporting is off — i.e. never inside an agent TUI, which is this PR's main case. Two follow-ups fall out of that, deliberately not done here:openTerminalLinkdoes not peel a:line[:col]suffix, so in a plain shell a detectedsrc/foo.ts:42is consumed by the hover path and opens nothing (the fallback never sees it); and the hover path consumes the click even when it opens nothing.Verification
swift testgreen (393 tests, 0 failures), including 16TerminalPathScannerTestsand 6TerminalLinkDeviceGateTeststhat drive the resolution point directly (local session opens; termiod andsshsessions open nothing; an absolute path from a remote session is declined too; an unidentified or unknown surface is declined).scripts/check-strings.shclean; adds no user-facing strings.TERMIO_CHANNEL=dev ./scripts/build-app.shbuilds and signs.The click itself is a GUI interaction and is not covered by any of that — the click→cell mapping and the click→surface lookup are AppKit geometry over a live surface, and a green build proves nothing about either. To check by hand, in a dev build:
lsa repo and cmd-click a printed filename → opens in the read-only preview.cdinto a subdirectory without OSC 7 reporting, cmd-click a barepackage.json→ still opens (this is the kernel-cwd root).:42suffix → opens at that line.(Sources/App.swift:88).→ opens.ssh), cmd-click a bare filename with no slash (README.md, printed byls) that also exists in the Mac's current project → nothing should open.src/main.rs, orprintf '\e]8;;src/main.rs\e\\click\e]8;;\e\\\n'for the OSC 8 spelling — and cmd-click it → nothing should open. Doing the same in a local session on the same repo → opens. This is the regression the second gate fixes; before it, the Mac's file of that name opened silently.