Skip to content

feat(terminal): open file paths on Cmd/Ctrl+click#117

Merged
simion merged 3 commits into
simion:mainfrom
dancras:feat/terminal-path-click
Jul 17, 2026
Merged

feat(terminal): open file paths on Cmd/Ctrl+click#117
simion merged 3 commits into
simion:mainfrom
dancras:feat/terminal-path-click

Conversation

@dancras

@dancras dancras commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Cmd/Ctrl+click a file path printed in a terminal to open it in the editor. Agents reference files (src/foo.ts:42); this makes those references jump straight to the file instead of requiring a manual search.

The flow

  1. An agent (or any TUI) prints a file path in a terminal pane.
  2. Hold Cmd/Ctrl: file paths underline like URLs.
  3. Click: the file opens in the editor preview, jumping to :line:col when present.
  4. If the path matches more than one file in the project, a small picker lists the candidates.
pathclick-demo.mp4

Design notes

  • Capture-phase opener. Reuses the existing GH Bug: Title: Links do not open in browser with click or Cmd+Click on macOS #58 gesture handling, so it works even when an agent TUI has xterm mouse reporting on.
  • Segment-boundary suffix match against the project's file list: one match opens directly, zero or many show the picker.
  • Custom link provider for the hover underline. WebLinksAddon can't be reused, its new URL() gate silently drops scheme-less paths. Third party option xterm-link-provider rejected to avoid unjustified dependency.
  • Scoped to agent panes; the scratch shell keeps plain URL behavior.

Testing

Unit tests for path parsing and suffix matching. make check-all clean.

Also have an uncommitted e2e test script and demo recording script.

Recognize file-path references in terminal output and open them in the
editor on Cmd/Ctrl+click, showing a picker when a path matches more than
one file in the task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dancras
dancras force-pushed the feat/terminal-path-click branch from 49bf2ee to e4a64ec Compare July 17, 2026 15:15

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking, nits only. None of these gate a merge.

  • Wrap-boundary off-by-one in the provider: a token ending exactly at a soft wrap makes toPos(end) advance to the next row at offset 0, so the range ends at x: 0 in xterm's 1-based coordinates. Clamp to the previous row's last cell.
  • AuxTerminal still arms on path targets: onDown swallows the gesture, then the aux callback drops it, so Cmd+click on path-like text in the scratch shell eats the click and does nothing (including for a TUI with mouse reporting that wanted it). Either a URLs-only mode that skips arming, or support path-open there too, AuxTerminal has taskId.
  • handlePathTarget's .catch(() => {}) swallows a failed file-list fetch: the click does nothing with no explanation. A toast matches how the Git panel reports IPC failures.
  • Nit: "and/or" and bare domains ("termic.dev") match the token regex, so they hover-underline and click through to a "No matches" picker. Fine for v1.
  • Nit: after the picker closes, focus lands on the invisible anchor (not focusable), so the terminal drops focus. Refocus the terminal in onPick/onClose.

Checked the integration: openPreviewTab/revealAt and the task-relative path form match the store and EditorPane, so single-match opens and line jumps work as written. The capture opener swallows the whole gesture, so the provider's activate can't double-fire; the WebLinksAddon new URL() rationale is accurate.

dancras and others added 2 commits July 17, 2026 19:56
Wrap-boundary link range, scratch shell no longer swallows path clicks,
toast on a failed file-list fetch, refocus after the picker, and split
linkAt into url/path matchers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bound each path-segment run to {1,255} so a long slash-less/dot-less
blob (base64, a hash) can't drive the token regex into O(n^2)
backtracking and stall the hover-underline pass.

Replace the single path scan with one url|junk|path regex so a fragment
of one can't leak as another:
  url  consume schemed URLs (WebLinksAddon already underlines them) to
       stop the double underline on the host/path inside a URL.
  junk consume scp-style host:path git remotes so neither half
       underlines; the colon only disqualifies when glued to a path
       char, keeping :line:col refs and "(path): description" colons.
  path now allows @ so retina assets (logo@2x.png) and scoped packages
       (@types/node) resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dancras

dancras commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Resolved 1,2,3 and 5 and also found some extra edge cases regarding regexes in the follow up review.

Some common formats (domains, emails) are technically valid paths so for simplicity I've just left them matching. Worst case UX is a "No matches" popup.

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged. No follow-up needed.

The key insight (splitting linkAt into url|junk|path and folding the scan into one regex) is exactly right — it kills the class of bug where a URL fragment leaks through as a bogus path. The {1,255} segment bound is the real win: caught the O(n²) hover-underline stall before it shipped, and there's a 40k-char blob timing test guarding it. 20/20 tests pass, tsc clean.

Checked the two things I'd worry about:

  • urlsOnly on the scratch shell → path tokens no longer arm the opener in AuxTerminal, so no swallowed clicks under a mouse-reporting TUI. Confirmed.
  • picker focus → onCloseAutoFocus routes to termRef on dismiss, leaves it for the editor on pick.

One cosmetic note: Windows C:/ drive paths get eaten by the scp-remote branch — don't care, this is a Mac app. Bare domains/emails still underline and resolve to nothing, already called out. Fine tradeoff.

Thanks, nice work.

@simion
simion merged commit 7f2cd10 into simion:main Jul 17, 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