Skip to content

feat(terminal): clickable file references open the editor at the line - #173

Merged
ghackett merged 2 commits into
mainfrom
feat/clickable-file-references
Aug 3, 2026
Merged

feat(terminal): clickable file references open the editor at the line#173
ghackett merged 2 commits into
mainfrom
feat/clickable-file-references

Conversation

@ghackett

@ghackett ghackett commented Aug 3, 2026

Copy link
Copy Markdown
Member

Implements ~/specs/collins/clickable-file-references.md: the file references Claude Code prints constantly — collins/terminal.py:152, /abs/path/foo.py, ~/x/y.md — were inert unless the CLI wrapped them in file:// or OSC 8. Now they hover-underline and Ctrl+click opens them in the tab's own editor pane at the referenced line.

How it works

  • FILE_PATTERN (linkpatterns.py, GTK-free like URL_PATTERN): matches absolute paths, ~/ paths, and relative paths containing at least one /, with an optional :line[:col] suffix. Same ending discipline as URLs, so (collins/foo.py). and collins/foo.py:12. shed the trailing punctuation. Bare filenames and paths with spaces are out of scope per the spec. A negative lookbehind keeps the grammar out of URLs entirely — https://a.b/c never produces a file match.
  • Click-time resolution is the false-positive gate: the regex only nominates candidates. resolve_file_reference strips the suffix, expands ~, and tries the agent's current cwd (worktree-aware) then the tab's project root. If nothing exists on disk, the click falls through to the terminal unclaimed — over-matched prose (a/b, dates) costs the user nothing. (The hover underline can still show on prose; VTE has no per-match validation hook.)
  • Opening: images go to the lightbox from PR 164; files inside the project open in the tab's editor at the line (win.open-in-editor now carries (path, line, col), presents a popped-out editor window, and re-places the cursor when the file is already open); directories, outside-project files, and no-editor tabs fall back to the default app. file: URIs and OSC 8 file: hyperlinks route through this same path, so a reference behaves identically however the CLI emitted it.
  • Scroll fix: cursor restore now uses scroll_to_markscroll_to_iter on a freshly loaded buffer runs before line-height validation and silently left the view at the top, which this feature made visible (cursor at line 42, view at line 1).

Verification

  • tests/test_linkpatterns.py: positives/negatives table for the new grammar plus resolve_file_reference coverage (roots order, ~, suffix vs literal-colon filenames, directories, misses). 51 tests pass.
  • Both patterns verified to compile under VTE's PCRE2 and Python re.
  • Driven end-to-end in a live headless instance: VTE's check_match_at matched exactly the three staged references on the terminal grid (relative + :line:col, relative + :line, absolute), the gate rejected a/b, and the editor landed at models.py 42:16 including the jump-within-already-open-file case.
  • GPL notices bumped on the renamed pre-fork files (window.py, chatsessionview.py; terminal.py was already dated today).

Ctrl+clicked app/models.py:42:15 in the panel — editor opens at line 42, col 16 (status bar):

full window: references in the terminal, editor at line 42

detail: the reference text and the landed cursor

Notes for review

  • The spec's remaining manual check: clicks while the CLI has mouse reporting on ride the existing capture-phase gesture (unchanged from URL clicks), but a live interactive confirmation wouldn't hurt.
  • Worktree references resolve into the editor because .claude/worktrees/<name> sits under the project root (is_inside accepts it), and the agent-cwd root is tried first.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dpjh2MJkFJNTjUbbZm1tSN

A second VTE match regex (FILE_PATTERN) underlines path-shaped text —
absolute, ~/, or relative with at least one slash, with Claude Code's
optional :line[:col] suffix. The regex only nominates candidates: at
Ctrl+click, resolve_file_reference checks the filesystem (agent cwd
first, then the tab's project root) and an unresolvable candidate falls
through to the terminal unclaimed, so over-matched prose costs nothing.

Resolved references open per kind: images in the lightbox, files inside
the project in the tab's editor at the referenced line (the
win.open-in-editor action now carries line/col, presents a popped-out
pane, and re-places the cursor in already-open pages), everything else
in the default app. file: URIs and OSC 8 file: hyperlinks route through
the same path, so a reference behaves identically however the CLI
emitted it. Cursor restore now scrolls via scroll_to_mark — on a
freshly loaded buffer scroll_to_iter ran before line-height validation
and silently stayed at the top.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpjh2MJkFJNTjUbbZm1tSN
@ghackett
ghackett marked this pull request as ready for review August 3, 2026 12:50
@ghackett

ghackett commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown

Claude finished @ghackett's task in 3m 52s —— View job


Review: clickable file references (#173)

  • Gather context (PR diff, changed files)
  • Read changed files in detail (linkpatterns.py, terminal.py, editor.py, window.py, chatsessionview.py)
  • Check tests
  • Post review feedback

Summary

Clean design overall: pushing filesystem validation to click-time (instead of trying to make the VTE hover-match itself accurate) is the right call, and resolve_file_reference's root-then-suffix-fallback logic is well covered by tests/test_linkpatterns.py. The scroll_to_mark fix for the pre-existing top-of-buffer bug is a nice catch.

Bug: column is off by one when restoring cursor position

collins/window.py:2949-2951

def _on_open_in_editor(self, _action, param: GLib.Variant) -> None:
    path, line, col = param.unpack()
    self._open_in_editor(path, [line - 1, col] if line > 0 else None)

The win.open-in-editor action is documented (collins/window.py, action registration comment) as carrying both line and col 1-based. Here only line is converted to 0-based (line - 1); col is passed through unchanged. That raw value then becomes the 0-based TextIter line offset in EditorPane._apply_cursor (collins/editor.py:608-615):

it.set_line_offset(max(0, min(int(offset), it.get_chars_in_line())))

Since the status bar displays get_line_offset() + 1 (collins/editor.py:838), every restored cursor ends up one column too far right. This is actually visible in the PR description's own screenshot: clicking app/models.py:42:15 (col 15) lands the status bar at col 16 — that's the bug manifesting, not confirmation the feature works correctly.

Fix: convert col to 0-based the same way line is:

self._open_in_editor(path, [line - 1, max(0, col - 1)] if line > 0 else None)

No existing test exercises this path end-to-end (tests/test_linkpatterns.py only covers resolve_file_reference, not the window.py/editor.py cursor-restore leg), which is how this slipped through.

Fix this →

Minor notes

  • collins/terminal.py:226 _reference_roots and the module docstring both order roots as agent-cwd-then-project-root; consistent, no issue — just confirming while reading.
  • Test suite (tests/test_linkpatterns.py) looks correct by inspection, but I couldn't execute pytest in this sandbox (tool permissions don't allow running Python/test commands here), so the "51 tests pass" claim in the PR description wasn't independently re-verified — only reviewed statically.
  • GPL modification-notice headers on the pre-fork files touched here (terminal.py, window.py, chatsessionview.py) are correctly bumped to today's date; linkpatterns.py/editor.py are post-fork files so no notice is required there.

Branch: feat/clickable-file-references

… fully

Live-testing feedback on the clickable-references branch:

- `collins/` (single segment, trailing slash) never matched — the
  relative alternative demanded a character after the slash. Its tail is
  optional now; the absolute alternative keeps a mandatory tail so a
  lone `/` in prose never links to the filesystem root.
- Opening a large file at a deep line landed hundreds of lines short:
  scroll_to_mark's pending scroll is applied against estimated line
  heights on a freshly loaded buffer. The scroll is re-issued at
  PRIORITY_LOW, after the view's (higher-priority) validation idles
  have drained and heights are exact. Verified headlessly: a fresh open
  of a 1000-line file at :602 now shows line 602 (was stuck at ~44).

Also verified: VTE matches references across its own soft wraps in
full; references the CLI hard-wraps (real newline + indent) are two
separate strings on screen and stay inert by design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dpjh2MJkFJNTjUbbZm1tSN
@ghackett

ghackett commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

Follow-up from live testing:

  • Deep-line scroll: fresh opens of large files landed short of the target line (scroll_to_mark's pending scroll uses estimated line heights). Fixed by re-issuing the scroll at PRIORITY_LOW, after the view's validation idles drain; a headless check confirms a fresh open of a 1000-line file at :602 puts line 602 on screen (viewport y 10226 for cursor y 10818, was viewport y 794).
  • Bare directory references: collins/ now matches (optional tail on the relative alternative only — a lone / in prose still can't link to the filesystem root).
  • Wrapped long paths: verified VTE-side — a soft-wrapped absolute path matches in full across rows. What breaks is the CLI hard-wrapping its own output: the screen then genuinely contains two strings separated by a newline + indent, which no terminal-side matcher can rejoin (the fragments fail resolution and the click falls through harmlessly). Out of scope, same limitation as other terminals.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dpjh2MJkFJNTjUbbZm1tSN

@ghackett
ghackett merged commit 0076f9c into main Aug 3, 2026
2 checks passed
@ghackett
ghackett deleted the feat/clickable-file-references branch August 3, 2026 13:37
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