[Bug] Clicking a wrapped bare URL in fullscreen opens a truncated address #1403
Hotragn
started this conversation in
Bug reports
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Affected area
TUI
What happened?
Clicking a bare
http(s)URL that the fullscreen frame wrapped onto a second row opens a silently truncated address — a real but different destination, with no indication anything was dropped. Clicking the continuation row opens nothing.FullscreenViewport.hyperlinkAtresolves a click against a single painted row (this.lastFrame[row]) and passes that one string tourlAtColumn. A URL longer than the terminal occupies two rows, so the bare-URL regex only ever sees the fragment on the clicked row.This lands hardest on login and OAuth URLs, which are the ones long enough to wrap — and the ones #1270 and #1299 set out to make clickable. OSC 8 hyperlinks are unaffected, because the escape is re-emitted on each wrapped row; this is specific to the bare-URL path.
Steps to reproduce
The behaviour is fully determined by the exported hit-testing helper, so it reproduces without a terminal. Against
mainat 97b994c:Output:
Interactively: run in fullscreen in a terminal where click-to-open works (e.g. Ghostty after #1270), have the agent print a URL longer than the terminal width, and click its first half.
Expected behavior
Clicking any part of a wrapped URL opens the complete target, or opens nothing. Navigating to a truncated address is the one outcome that should not happen — the user cannot tell they were sent somewhere other than where they clicked.
Prime Agent version
main@ 97b994c (verified today; also present in 0.7.2)Environment
Windows 11, reproduced against the checked-out source rather than a terminal session. The single-row limitation is platform-independent.
Additional context
I attempted a fix and closed it as unsound — recording that here so the next person doesn't repeat it.
My approach joined consecutive rows whose painted width equalled the frame width, on the theory that a row filling the terminal must have wrapped. @jonaowen pointed out on that PR that this is wrong, and I confirmed it:
composeFramereceives rows already wrapped by the components, andpaintwrites each at an absolute position, so width equality carries no wrap provenance. It cannot distinguish a soft-wrapped continuation from an explicit logical row, a padded row, or dock/overlay content. Worse, joining can fabricate a target — a full-width row ending in a complete URL followed by a row starting with URL-valid text producedhttps://examhttps://example.com/docsand, which appears nowhere on screen. That is strictly worse than the truncation it was meant to fix.So the constraint for any real fix: width equality cannot authorize navigation. It needs explicit soft-wrap lineage carried from the component that wrapped the URL through frame composition into hit-testing, and joins restricted to rows proven to share one logical line.
selection-metadata.tsalready threadsTableCellSelectionRegionthroughcomposeFrame, so there is an existing precedent for a parallel region channel.A smaller interim that is sound: return
nullwhen a bare-URL match ends exactly at the last column. That removes the wrong-destination navigation with no join and no fabrication risk, at the cost of not opening a complete URL that happens to end on the boundary.Regressions worth having either way: exact-width complete URLs followed immediately by URL characters, explicit newline boundaries, transcript↔dock/overlay boundaries, wide Unicode, and resize.
Happy to implement whichever direction maintainers prefer, or to leave it — posting the analysis so it is not lost either way.
All reactions