Skip to content

feat(terminal): cmd-click bare file paths to open them - #73

Open
jiweiyuan wants to merge 3 commits into
mainfrom
feat/clickable-file-paths
Open

feat(terminal): cmd-click bare file paths to open them#73
jiweiyuan wants to merge 3 commits into
mainfrom
feat/clickable-file-paths

Conversation

@jiweiyuan

Copy link
Copy Markdown
Collaborator

What

Makes bare file paths in terminal output — the kind agent CLIs print, like BranchModel.swift, src/foo.ts:42, App.swift:88:15cmd-clickable to open in the read-only preview at the referenced line. This is the gap versus VS Code, where the same paths are already clickable.

Why they weren't clickable

libghostty only lights up a link when the program emits an OSC 8 hyperlink or the text matches its built-in URL regex (real schemes only: http, file, …). A bare relative path is neither, and Ghostty upstream has explicitly declined to add row/column detection ("zero standardization"). So hoveredURL stays nil and termio's cmd-click interceptor has nothing to open.

VS Code closes this gap with its own terminalLinkParsing layer. The thing that makes it stable — not a false-positive machine — is filesystem validation, not the regex. This PR mirrors that philosophy rather than Ghostty's regex.

How

On a cmd-click that libghostty didn't resolve to a link (the new fallback; the OSC 8 / detected-URL path is unchanged):

  1. Map the click to the grid cell under it, reusing the wrapper's own point→cell convention (view-local points, flipped y; cell size = surfaceSize.cell*Pixels / backingScaleFactor).
  2. Read the clicked row via readViewportText().
  3. Extract whitespace tokens, peel a trailing :line[:col], strip wrapping punctuation, try git-diff a/ b/ prefixes, expand ~.
  4. Validate every candidate against the filesystem and open the nearest hit — only if it's a real regular file. Otherwise the click falls through to the terminal untouched.

Because validation is the gate, an off-by-one click column or a stray English word never opens the wrong thing; column precision is never load-bearing.

Files

  • TerminalPathScanner — pure, unit-tested token → path/line parsing + fs gate
  • TermioStore.openBarePathUnderCommandClick — click → cell → row → open, reusing the delegate == TerminalViewState surface lookup (same pattern as TerminalPane.terminalView(matching:))
  • wired as a fallback branch in the existing cmd-click monitor

Tests

TerminalPathScannerTests (11 cases, against real temp files): bare/nested/absolute paths, :line and :line:col peeling, wrapping punctuation, git-diff prefix, nearest-column disambiguation, and the negative guards (nonexistent path, plain words, directory → no match). All green; full swift build clean.

Not in this first version

  • No hover underline / pointing-hand for bare paths (still cmd-click only; column is best-effort). Real URLs and OSC 8 links keep their existing hover cursor.
  • Column number is parsed off the path but the preview jumps by line only.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
landing Ready Ready Preview Aug 12, 2026 10:11pm

Request Review

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
Resolve a clicked bare path against multiple base dirs (live kernel cwd via
PROC_PIDVNODEPATHINFO, session worktree, project root) instead of only the
OSC 7 cwd, which is stale when the shell doesn't report it. Carries temporary
[PATHCLICK] trace logging (to be removed before merge).
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.
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.

1 participant