Open an explicit OSC 8 link on a plain click under mouse reporting - #6
Merged
Merged
Conversation
… mouse reporting A full-screen agent takes the mouse the moment it starts — Claude Code enables 1000/1002/1003/1006 in its first frame — so `_handleTapUp` handed every click to the program and returned before resolving a hyperlink. The cell still painted as a link, because the render path marks `hasHyperlink` unconditionally. Shift+click reached it, but nothing on screen said so. Take the same trade the touch tap already takes: an OSC 8 link is the program declaring these exact cells ARE a link, which is a more specific statement of intent than its claim on the mouse, so the collision is confined to programs contradicting their own markup. A primary press on such a cell arms the URI and withholds the whole press-motion-release span; everything else — bare-URL matches, ordinary cells, other buttons — still reaches the program, and Shift keeps its meaning. Hover follows the click rather than the bypass, so the pointer stops advertising a link the program is about to swallow.
bharathm03
added a commit
that referenced
this pull request
Aug 27, 2026
PR #6 put `_explicitHyperlinkAt` in the `onHover` guard so a link under a full-screen agent would paint as reachable. That call resolves a cell through `_positionForOffset`, which asked `controller.snapshot` whether the buffer was empty — and reading the snapshot settles the styled formatter: three full-buffer passes plus a re-parse of the styled output. Under `GhosttyTerminalRendererMode.renderState` the painter never reads that snapshot (`snapshotOf` is a lazy getter behind `_paintsTranscript`), so nothing else was paying for it and the hover paid the whole cost alone. Any output since the last read marks it stale, which in an agent session is every frame — so moving the mouse over a live terminal cost a full transcript rebuild per hover event. Measured at ~17ms per hover on a 1800-line scrollback, on the surface where the pointer spends all its time. Selection drags resolve cells the same way and paid it too. Ask the engine instead. `_scrollableLineCount` already prefers the native scrollback total and falls back to the formatter only where there is no engine geometry to ask — web, and before the terminal exists, which is where the formatter is what renders anyway. It was already being called a few lines below for `maxRow`, so this also drops a duplicate read. The guard is timed against the same loop with the pointer held still: both take the same output and repaint the same frames, so what separates them is the hover alone, and a ratio survives a machine an absolute bound would not. 862ms vs 179ms with the old guard; even under the bound with it gone.
bharathm03
added a commit
that referenced
this pull request
Aug 27, 2026
* feat(vte_flutter): report the hovered OSC 8 URI so a host can show where a link goes The view paints an underline on every OSC 8 cell and swaps the cursor on hover, but the URI itself never leaves it -- so a host has no way to disclose a destination before opening it, and OSC 8 lets a link's text disagree with its target. Fires only on a real change, so a host may rebuild on it: moving within one link is silent. A controller swap reports for itself, deferred to after the frame because didUpdateWidget runs mid-build and a host rebuilding there would be setting state during build. dispose deliberately does not fire, for the same reason. * perf(vte_flutter): stop hovering from rebuilding the whole transcript PR #6 put `_explicitHyperlinkAt` in the `onHover` guard so a link under a full-screen agent would paint as reachable. That call resolves a cell through `_positionForOffset`, which asked `controller.snapshot` whether the buffer was empty — and reading the snapshot settles the styled formatter: three full-buffer passes plus a re-parse of the styled output. Under `GhosttyTerminalRendererMode.renderState` the painter never reads that snapshot (`snapshotOf` is a lazy getter behind `_paintsTranscript`), so nothing else was paying for it and the hover paid the whole cost alone. Any output since the last read marks it stale, which in an agent session is every frame — so moving the mouse over a live terminal cost a full transcript rebuild per hover event. Measured at ~17ms per hover on a 1800-line scrollback, on the surface where the pointer spends all its time. Selection drags resolve cells the same way and paid it too. Ask the engine instead. `_scrollableLineCount` already prefers the native scrollback total and falls back to the formatter only where there is no engine geometry to ask — web, and before the terminal exists, which is where the formatter is what renders anyway. It was already being called a few lines below for `maxRow`, so this also drops a duplicate read. The guard is timed against the same loop with the pointer held still: both take the same output and repaint the same frames, so what separates them is the hover alone, and a ratio survives a machine an absolute bound would not. 862ms vs 179ms with the old guard; even under the bound with it gone.
bharathm03
added a commit
to antgrid-ai/antgrid
that referenced
this pull request
Aug 27, 2026
…hows where it goes (#25) A full-screen agent takes the mouse before it prints anything -- Claude Code enables 1000/1002/1003/1006 in its first frame, measured on its PTY -- so every click went to the agent and the link stayed dead, in exactly the case the OSC 8 work was written for. The cell still painted as a link. Bumps the fork pin to antgrid-ai/dart_terminal#6, which opens an explicit OSC 8 link on a plain primary click and makes hover mark only what a click will actually open. The desktop path skipped the confirm sheet because the terminal was said to reveal a link's destination on hover. It does not, and never did: the hovered URI only swaps the mouse cursor to a pointer, and nothing in the view or this app ever paints it. What actually discloses on desktop is the browser's address bar -- which is why the sheet still belongs on mobile alone, where a verified App Link can open its own app instead of any browser. Renamed to say the true reason.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#5let a touch tap open an explicit OSC 8 link even when the program has the mouse. The same case on desktop still needed Shift, and nothing on screen said so — so in the one situation the OSC 8 work was written for (a link printed by a full-screen agent) a plain click did nothing while the cell painted as a link.Measured on Claude Code's PTY: it enables mouse modes
1000,1002,1003,1006in its first frame and emits OSC 8 hyperlinks. So_handleTapUpreturned at the mouse-capture guard before hyperlink resolution on every click.What changes
A primary press that lands on a cell carrying an explicit OSC 8 URI arms
_tapHyperlinkUriand withholds the whole press-motion-release span, so the program is never handed half a click;_handleTapUpthen opens it. This is the trade#5already made for touch — OSC 8 is the program declaring these exact cells are a link, a more specific statement of intent than its claim on the mouse — so the collision is confined to programs contradicting their own markup.Unchanged, and tested as such:
Hover follows the click instead of the bypass, so the pointer stops advertising a link the program is about to swallow.
A drag off a link cell now sends the program nothing rather than a release with no press — same trade the Shift bypass already makes, and drag-select is unavailable under mouse reporting either way.
Tests
Six added in
terminal_view_test.dart, each negative-checked against a revertedterminal_view.dart: plain click opens and records neither PRESS nor RELEASE; plain click beside the link records PRESS; right click on the link records PRESS; plain click on a bare URL records PRESS; hover marks an OSC 8 cell; hover does not mark a bare URL.ghostty_vte_flutter:+208 -28, the 28 pre-existing onmaster(baseline re-measured this run).flutter analyzeclean,dart format --set-exit-if-changed .clean across 104 files.