Skip to content

feat(links): Ctrl-click a bare name.ext after an agent-CLI file verb (plan 043) - #94

Merged
rockyway merged 3 commits into
developfrom
feature/verb-prefixed-file-links
Sep 14, 2026
Merged

rockyway merged 3 commits into
developfrom
feature/verb-prefixed-file-links

Conversation

@rockyway

Copy link
Copy Markdown
Contributor

What

Ctrl-click / Copy Path now works on an agent-CLI tool row whose file has no directory separator:

Update(015-terminal-watchdog-workflows-mockup.html)
  ⎿  Added 1 line

A bare name.ext is deliberately not a path in general (package.json, example.com, v5.91.0 all have that shape in prose), so PATH_RE gains a sixth alternative that admits one only when an agent-CLI file verb introduces it — Update(x.html), read(app.tsx), Read foo.ts, ReadFile: a.md, Edited main.rs. The verb sits in a positive lookbehind, so the underline, hit-test and Copy Path cover only the filename.

Rules (each pinned by a named test in findPathLinks.test.ts)

  • Two forms derived from ONE FILE_VERBS list: Verb( admits every verb; Verb / Verb: drops the prose starters Open/View/Create (Open example.com in your browser does not link).
  • ASCII \b before the verb (Unread foo.ts does not link).
  • Final extension starts with a letter (Update v1.2.3 does not link; documented cost: .7z after a verb).
  • The token must END at the filename — not followed by a word char, separator, or a dot-run leading to one (Read(archive.tar.7z), Read foo.ts/bar, Read foo.ts..bar do not link a prefix; read config.json. and Edited main.rs... still link).
  • Separator bounded [ \t]{1,8} — the lookbehind runs at every scan position, so an unbounded run was O(N²) per hover. Pinned by the 8-space / 9-space pair.
  • Branches 1–5 are unchanged (kept byte-identical via a literal's .source); :line:col still parses.

Not changed (decided)

find_descendants still rejects a bare filename for the BFS fallback: Claude Code prints cwd-relative paths, so a bare name means the file is in the terminal cwd and the direct join resolves it; a miss shows the existing "File not found" toast.

Verification

  • terminal-core: 829/829; bun run build:terminal-core, bunx tsc --noEmit, bun run build green.
  • Mutants killed independently by the orchestrator: drop \b, digit-first extension, consuming group instead of lookbehind, drop right boundary, unbound separator, empty prose-starter set, regress dot-run rule, paren form with the prose filter — each by the test that names its reason.
  • External review (codex gpt-5.6-sol): r1 3 D / 2 B / 2 C → 8e49db1; r2 3 D / 1 C → 0651775; r3 confirmation, zero findings, ready to merge.
  • Not verified here: a real Ctrl-click in the running app — GUI pass pending.

Plan: termflow-fabric docs/plan/043-verb-prefixed-bare-file-links.md; reviews docs/review/222–224.

An agent tool row such as `Update(015-terminal-watchdog-workflows-mockup.html)`
got no Ctrl-click link / Copy Path: a bare filename with no directory separator
matches none of PATH_RE's five alternatives, and that is deliberate — package.json,
example.com and v5.91.0 all have the same shape in prose.

Add a sixth alternative that admits a bare `name.ext` ONLY when an allowlisted
file verb introduces it — `Update(x.html)`, `read(app.tsx)`, `Read foo.ts`,
`ReadFile: a.md`, `Edited main.rs` — via a positive lookbehind so start/end cover
just the filename. `\b` before the verb keeps `Unread foo.ts` out; the final
extension must start with a letter so `Update v1.2.3` stays text (documented
cost: digit-first extensions such as `.7z` are not linked after a verb). Branch 6
sits after branch 5 so `Update(src.v2/main.rs)` still links the full path.

No resolver change: Claude Code prints cwd-relative paths, so a bare name means
the file is in the terminal cwd and resolve_terminal_path's direct join finds it.

Tests pin each hop — filename-only span, :line:col, precedence, soft-wrapped
`Update(` row, linkAtIndex hit/miss — plus negative controls for every non-file
lookalike. Plan: termflow-fabric docs/plan/043.
…or, drop prose verbs from the row form

Review round 1 (Sol) on cd0c2c1 — all fixed at the class, not the site:

- Branch 6 had no RIGHT boundary, so it linked a prefix of a longer token:
  `Read(archive.tar.7z)` → `archive.tar`, `Read foo.ts/bar` → `foo.ts`.
  A negative lookahead `(?![\w\/-]|\.[\w-])` now requires the token to end
  there; a sentence-final `.` (`read config.json.`) still links.
- The separator `[ \t]+` sat inside a lookbehind that is evaluated at every
  scan position, so `Read` + N spaces cost O(N²) per hover. Bounded to
  `[ \t]{1,8}`; a timing test pins it (unbounded: ~385 ms, bounded: ~9 ms).
- `Open example.com in your browser` / `Create Node.js project` linked.
  The space/colon row form now admits only row verbs; the paren form keeps
  Open/View/Create (`Create(e.rs)`). Residual, accepted: `Remove example.com`
  still links — the cost is the existing "File not found" toast.
- Tests: `:line:col` after a row verb; linkAtIndex at every boundary
  (`(`, first, last, `)`); a bare name hit through the getLinkAt composition
  helper on both rows of a soft wrap.
- Comment corrected in place: branches 1–5 are bounded by their char classes;
  branch 6 by its `{1,8}` separator and right-anchored tail.

With the right boundary, alternation order (branch 5 before 6) is no longer
load-bearing — `Update(src.v2/main.rs)` links the full path either way — so
that case pins the behaviour, not the source order. The previous commit's
"negative controls for every non-file lookalike" was an overclaim; the
controls are the ones enumerated by name in findPathLinks.test.ts.
…b forms from one list

Review round 2 (Sol):

- `Read foo.ts..bar` still linked `foo.ts`: the round-1 lookahead forbade
  `.`+word, and the char after the first dot was another dot. The rule is now
  "the token ends here — not followed by a word char, a separator, or a
  dot-run that leads to one" (`(?![\w\/-]|\.+[\w\/-])`), which closes the
  prefix class without eating an ellipsis (`Edited main.rs...` still links).
- The paren and row verb allowlists were two hand-copied lists and the paren
  copy had lost the past-tense verbs (`Edited(main.rs)` stopped linking).
  Both forms now derive from ONE `FILE_VERBS` list; the row form is that list
  minus `PROSE_STARTERS` (Open/View/Create). PATH_RE is composed with
  `new RegExp`, branches 1-5 kept byte-identical via a literal's `.source`.
- The 200 ms wall-clock assertion was a CI flake risk and redundant: the
  8-space / 9-space pair already pins the `{1,8}` separator bound (it is
  what kills the unbounded mutant). Kept the 20k-space scan as a smoke.
- Comment rewritten in place to state the admitted forms and the boundary rule.
@rockyway
rockyway merged commit d3794aa into develop Sep 14, 2026
5 checks passed
@rockyway
rockyway deleted the feature/verb-prefixed-file-links branch September 14, 2026 03:02
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