Skip to content

fix(tui): follow a bare URL across a wrapped row when it is clicked - #1324

Open
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/wrapped-url-click
Open

fix(tui): follow a bare URL across a wrapped row when it is clicked#1324
Hotragn wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
Hotragn:fix/wrapped-url-click

Conversation

@Hotragn

@Hotragn Hotragn commented Aug 13, 2026

Copy link
Copy Markdown

Follow-up to #1270 and #1299.

The gap

#1299 taught fullscreen hit-testing to recognize visible bare http(s) URLs alongside OSC 8 metadata. It resolves one painted row at a timehyperlinkAt reads this.lastFrame[row] and hands that single string to urlAtColumn. A URL longer than the terminal width is wrapped onto the next row, and each row is matched in isolation.

Probing urlAtColumn directly against an 80-column wrap:

full:  https://accounts.example.com/oauth/authorize?client_id=abc123&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Fcb&state=xyz
row 1: https://accounts.example.com/oauth/authorize?client_id=abc123&redirect_uri=http%
row 2: 3A%2F%2Flocalhost%3A8976%2Fcb&state=xyz

click row 1 col 10 -> "https://accounts.example.com/oauth/authorize?client_id=abc123&redirect_uri=http%"
click row 2 col 5  -> null

The continuation row failing to open is a nuisance. The head row is the real problem: it does not fail closed, it silently opens a truncated address. The user gets a wrong page with no indication anything was dropped.

This lands hardest on exactly the URLs #1270 and #1299 set out to make clickable. Login and OAuth URLs are long enough to wrap on any normal terminal; short doc links generally are not.

OSC 8 targets were never affected — the escape is re-emitted on each wrapped row, so explicit hyperlinks already resolved correctly. This is specific to the bare-URL path #1299 added.

The fix

Resolve the wrap group rather than the single row.

A row whose painted width fills the terminal had no room left for a line break, so its text continues on the row below. That is the exact wrap signal, and it needs no guessing: FullscreenViewport already records prevWidth, the width the frame was actually painted at, so hyperlinkAt passes it down instead of inferring wrapping from content.

urlAtFramePosition(lines, row, column, width) walks back to the start of the run of full-width rows, walks forward to its end, joins the normalized text, maps the clicked column into that joined text, and hit-tests the URL spans. OSC 8 is checked first and short-circuits, so explicit targets stay authoritative.

The bare-URL scanning and the tab/ANSI normalization that urlAtColumn already did are factored into bareUrlSpans and framePlainText and shared by both paths, so single-row behaviour — trailing-punctuation trimming, unbalanced bracket trimming, visible-column arithmetic — is unchanged and still exercised by the existing tests.

Two bounds keep the join conservative:

  • A full-width row only joins into the row below if a URL span actually crosses the boundary. Prose that happens to reach the last column resolves exactly as before.
  • The group is capped at 8 rows, so a run of full-width rows cannot walk the frame.

Tests

Three cases in packages/tui/test/fullscreen.test.ts, using the existing virtual-terminal harness (40-column frame) and driving real SGR mouse press/release like the neighbouring tests:

  • wrapped URL — a login URL split across rows 12/13; clicking the head row and the continuation row both open the full target. Verified this fails on main: with the fullscreen.ts change reverted, this case fails and the other 48 still pass.
  • no false join — a row padded to exactly 40 columns ending in a complete URL, followed by unrelated prose; still opens just https://example.com/docs.
  • OSC 8 precedence — a full-width OSC 8 row above a row containing a different bare URL; the explicit target wins.

Verified locally: npm run check clean, full packages/tui suite 764 passed / 0 failed, and packages/coding-agent fullscreen-mode.test.ts 11/11. The tui package's tests run fully on Windows, so unlike the daemon suites this one is verified end to end here rather than partially.

Note

Fix bare URL click resolution across wrapped rows in the TUI fullscreen viewport

  • Adds urlAtFramePosition in utils.ts to resolve bare HTTP(S) URLs that visually wrap across frame rows by joining adjacent rows when a URL spans the frame width boundary.
  • Updates FullscreenViewport.hyperlinkAt in fullscreen.ts to use urlAtFramePosition instead of urlAtColumn, so clicking either the head or continuation row of a wrapped URL opens the full address.
  • OSC 8 links remain authoritative; joined row logic is skipped when a full-width row ends at a complete URL to avoid false positives with following text.

Macroscope summarized 878a24d.

PrimeIntellect-ai#1299 taught fullscreen hit-testing to recognize visible bare http(s) URLs
alongside OSC 8 metadata, but it resolves one painted row at a time. A URL
longer than the terminal is wrapped onto the next row, and each row is matched
in isolation, so the result depends on which half was clicked:

  full: https://example.com/oauth/authorize?client_id=abcdefgh&state=xyz
  click row 1 -> "https://example.com/oauth/authorize?clien"   (opened)
  click row 2 -> null                                          (nothing)

The head row is the bad case. It does not fail closed; it silently opens a
truncated address. Login and OAuth URLs are the ones long enough to wrap, and
they are the exact case PrimeIntellect-ai#1270 and PrimeIntellect-ai#1299 set out to make clickable.

Resolve the wrap group instead of the single row. A row whose painted width
fills the terminal had no room for a break, so its text continues on the row
below; join that run, then hit-test the click against URL spans in the joined
text. The renderer already records the width it painted at, so the wrap signal
is exact rather than inferred from content.

OSC 8 is untouched and still resolved first: the escape is re-emitted on every
wrapped row, so explicit targets were already correct.

A row that merely fills the width does not join into the row below unless a URL
span actually crosses the boundary, so ordinary prose that happens to reach the
last column is unaffected. The join is capped at 8 rows.
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