Repository navigation
Fix mobile preview rendering, discovery, and terminal controls - #467
Merged
Merged
Conversation
Discovery scales its probe budget with the listener count, so a pane full of idle sockets can no longer hide the dev server, and e2e fixtures answer text/html so they are discovered at all. One predicate in the protocol package now decides what a web page is: discovery, the navigation guard and the rewriter accept application/xhtml+xml instead of only text/html. Node protocol goes 25 to 26 for the titled port rows, with the version floor and the agent's own version raised in the same commit. The devices trigger announces the count its badge paints, and the Turbopack shim leaves an href alone when a page blanks its chunk base. Co-Authored-By: Claude <noreply@anthropic.com>
The rewriter goes back to text/html only: the compat shim it injects is HTML markup, and raw < and && inside it make a document parsed as strict XML fail outright, so an XHTML page now previews byte-for-byte as its author wrote it while still being discovered and navigated. A refused navigation keeps the cookies that came with it, a page title takes the normalizeLabel path at the wire boundary, and the alternate-screen bar sends Home/End in the spelling the app asked for. The plane-side lane cap and the stricter discovery status gate are documented as deliberate, not as twins. Co-Authored-By: Claude <noreply@anthropic.com>
The two probes per port now run together instead of queueing, so a pane holding many listeners that accept and never answer still lists its dev server, and each loopback family gets its own window so a stalled IPv4 dial cannot hide an IPv6-only site. A navigation whose response names no type is painted rather than refused, since a browser renders that as text and a dev server that forgets the header still serves a page someone asked for. PreviewLabel's cache-wide subscription stays: query-core 5.101.4 gives no public per-query listener, so aiming it means non-public access or an observer that subscribes to the cache itself. Co-Authored-By: Claude <noreply@anthropic.com>
The refusal exists because iOS hands a frame's file to a system viewer with no way back; a desktop browser downloads in its own UI, so those bytes go through now, and the phone's refusal page also announces itself as a dashboard toast, believed only from the frame that posted it. An XHTML page is rewritten after all, its shim inside a CDATA section so the XML parser stays happy. Discovery asks both loopback families before dropping a port, a HEAD refusal carries no body, the Turbopack probe reads the raw src before mapping, the CSS bump matches rel as written and set later, a revoked disclosure stops being explained mid-dialog, tab labels follow their own query instead of the whole cache, and the compat verifier judges each leg on its own. Co-Authored-By: Claude <noreply@anthropic.com>
…stable Both inline scripts a rewritten document gets now go through one helper that wraps them in CDATA for a page the browser parses as strict XML, escapes nothing the XML parser would choke on, and skips the fallback injection that would have landed markup before the prolog. A port whose server accepted and is still compiling its first route is now offered untitled instead of hidden, discovery asks both loopback families before dropping a port, and an over-long page title is capped rather than voiding the pane's whole discovery answer. The stylesheet cache-bust also fires when rel arrives after href, matches rel as written, and the socket mapper returns a ws: URL, so HMR that reduced its own address to a pathname still connects. The navigation guard moved to its own module with unit tests, which brings http.ts back under the file-size rule. Co-Authored-By: Claude <noreply@anthropic.com>
The socket rewrite was applied to EventSource too, which needs an http(s) URL by spec, so every server-sent stream inside a preview would have died; it now applies to WebSocket alone. And adding rel to the mapped attributes let the URL mapper chew up rel="noopener" and friends, turning a security token into a proxied path; rel now returns exactly what the app wrote, with the stylesheet test reading it as the token list it is. A page title is now trimmed in UTF-16 units, the way the route's own schema counts them, without ever splitting a surrogate pair, so an emoji-heavy title can't 500 the read that promises never to error. An untitled page on one loopback family no longer hides a titled site on the other. The shim's two rules are asserted in a real Chromium through spec 48 (each bug was confirmed red against the new checks in isolation) and pinned as source invariants in the unit suite. Co-Authored-By: Claude <noreply@anthropic.com>
The guard refused anything that was not a document, including a dev server's plain-text log page or a PNG someone tapped, neither of which leaves the frame on iOS. It now refuses what a browser cannot present itself: an attachment, whatever its type claims, or an unreadable binary. The probe budget was drained from the same leading port every poll, so a pane whose stalled sockets sat ahead of its dev server hid that server permanently rather than transiently; each scan now starts where the last one gave up, and twice as many ports are asked at once. A verdict the first loopback family already gave survives the scan's budget expiring during the second. Discovery identifies itself as a probe instead of sending no client at all, so an internal tool that ignores non-browser clients still reaches the picker. Also: one map() per script src instead of two, the shim escapes its own closing tag so a caller cannot forget to, and the title parser's JSDoc is back above the function I had slid it under. Co-Authored-By: Claude <noreply@anthropic.com>
JSON, XML and their feed dialects render as text in the frame, so the guard now paints them and refuses only what actually leaves the screen: a file the server hands over, or a binary the view has no reader for. Accepted with that, the committed expectation that a preview's own JSON endpoint gets an error page is updated, per the operator's call. A chunk marker in a script's query (a page served through a proxy route) no longer publishes a base no chunk matches; the pathname alone may name one. The stylesheet twin is now found by the same token-list read as the element being bumped, so rel="preload stylesheet" gets its revision. Accept entries with q=0 are a refusal, not an offer. The EventSource leg of the browser check held its stream open and separated a thrown constructor from a transport error, so it can no longer settle on whichever of open and error raced first; it was confirmed red against the leak it exists to catch. The changeset had grown into one sentence per round and is rewritten. Co-Authored-By: Claude <noreply@anthropic.com>
A port is offered when something on it ANSWERED with a document, and not offered when it merely accepted a connection. Round 7 asked for the cold dev server and I implemented it by holding a silence as a verdict, which also advertised every wedged database, language server and leftover bind as "Web preview", against the feature's own promise. The window per loopback family is now generous enough for a first compile (a late answer is listed), and the scan's deadline starts before the /proc walk with room for the port in flight, so the pass stays inside the plane's command timeout instead of answering nothing. The guard's text family is a list, not a wildcard: text/csv, text/calendar and text/vcard are how a dev server names an export, and iOS takes those to Quick Look just as it takes a PDF. A stylesheet's revision now waits for the link to be connected instead of firing twice, the refusal channel tolerates a hex case difference without going dead, and the refusal page stops advising a new tab that would only be refused again. XHTML now has the proof it lacked: a real Chromium loads the fixture's application/xhtml+xml document, asserts no parse error, and asserts the shim executed; removing the CDATA wrapper was confirmed to turn that red. Co-Authored-By: Claude <noreply@anthropic.com>
The stylesheet revision is now idempotent, which is what actually stops the double bump: last round I guarded one of the two paths that reach it, and the observer's append pass still rewrote href a second time, costing a second token and a second fetch of the same sheet. The probe window is now measured from when the probes start, with a floor, so a /proc pass that runs long on a loaded host can no longer spend the entire budget before the first port is tried and answer with an empty picker. Concurrent readers of discovery join one scan instead of each firing a sweep at the node, and a revocation drops the in-flight entry with the cache. The compat verifier asserted the accumulated failures of the very leg whose transients its own comment says to tolerate; the check now runs before that leg and the arrays are cleared at its boundary. The crowded-scan test drops its wall-clock bound, which could only fail on a busy runner rather than on a real regression, and the two cookie extractions in the proxy are one function. Co-Authored-By: Claude <noreply@anthropic.com>
…rest Discovery and forwarding had two opinions of loopback: a port whose IPv4 socket accepts and stalls was listed off its IPv6 page, and the lane (which only retries ::1 after a refusal) hung on the stall. The probe now holds to what the lane can dial. CDP discovery and the screencast bridge ask both families, so a browser bound to ::1 is watched and not merely listed. The scan's probe floor can no longer outrun the plane's command timeout, a ports sweep dropped by a pane or node close cannot write itself back into the cache, the MCP views mirror takes the wire row type so a title can't go missing again, and two doc/dead-code leftovers from earlier rounds are fixed.
…pane In the alternate buffer a press is only ever a key, so a view grantee or a copy-mode pane got silence from all four buttons. They now stand down with the reason at the control, on the same disabled-reason tooltip the rest of the bar uses, and stay live whenever the app can be driven. The terminal publishes the buffer switch through onAltScreenChange and callers clear it on dispose.
@tanstack/router-core declares ^1.6.2, and the lock had 1.6.3, which every one of the five advisories (three high) names as vulnerable. bun audit still matches the original version, so the gate went red on the advisory DB rather than on any change here; CI runs the same gate. Origin/main carries the same pin. Verify:changed's audit tier blocked on this, so the in-range update is what the script itself prescribes rather than a new ignore entry.
Round 12's two MINORs, both in last round's family policy. An IPv4 that accepts and answers something else (an API's JSON on the port a dev server serves on ::1) still handed the row its title and label, though the viewer would see the API: now an IPv4 answer settles the port alone, and IPv6 carries the offer only after a refused connect, which is exactly when forward.ts goes there too. The screencast bridge asked its two families with no bound on the first, so a wedged IPv4 listener spent the whole 3 s handshake and the follow-up fetch died on the abort; each family now gets its own slice of the budget. Also folds a doubled comment about the scan clock, and says in the changeset that the seroval raise rides in this PR.
Round 13's two MINORs. probeFamily called every failed fetch 'unreachable', so a tool that accepts on 127.0.0.1 and resets (or answers in gRPC rather than HTTP) let the IPv6 dev server carry the row, though the lane completes its TCP connect there and never walks to ::1. Only a connection that never landed now counts as absent, judged from the error codes Bun's fetch actually sets (ECONNREFUSED and friends, measured against real sockets rather than guessed). And the scan's deadline never reached the devtools probe, so a worker picking up a port at the window's end could run 800 ms past it and push the command into the plane's timeout; both probes per port are now bounded by one window. The changeset no longer claims a dev server is unhidden by a family that merely answers.
Round 14's MINOR: the ladder helper was tested with fetches that refused or answered at once, so nothing pinned the one setting that lets an IPv6-only devtools endpoint survive a stalled IPv4 listener. Two tests now do, one at the helper and one at the discovery call site, both verified red by removing the slice (the probe then sits out the caller's whole deadline). The NIT: the changeset said a dev server is no longer hidden by 'a first family that never answers' while the next sentence uses that same phrase for the case that still hides it; the first now says 'an IPv4 that is not listening at all'.
Round 15's NIT, which cost one stub to close: both slice tests let the family window REPLACE the caller's signal without anything going red, because in each case some timer still ends the wait. This one aborts the caller at 30 ms with a 3 s family slice, so only the composition can answer in time; dropping it fails at 3001 ms. Verified by mutation, source restored.
PR #468 landed the same raise with its own note in tidy-seroval-security.md, so this branch saying it also delivers the fix would be a duplicate claim in the release body. The merge of origin/main takes the lockfile entry too, leaving only this PR's own two lock changes: the node agent's 3.1.0 (which versions.ts requires for protocol 26) and the entities dependency discovery's title decode now needs.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Fix mobile previews that lose styles or open internal service ports as downloads, and make preview labels and terminal controls usable on phones.
Validation: API (4,908), runtime (575), and web (3,151) tests passed, along with focused regressions, affected type checks, lint, and the web build. Chromium and WebKit checks covered rendering, repeated Next.js and Vite CSS hot reload, filtering, titles, download protection, terminal navigation input, and mobile header interactions. Native CUA testing was unavailable because the host has no graphical display or accessibility bus.
Includes server and node patch changesets. Not deployed.
bun run verify:changedpassed its build, type, lint, unit-test, and installer checks, then stopped at a CLI update scenario that deliberately refuses hosts with an existing per-user Subshell Server service definition. That scenario needs CI or a clean host; the installed service was left untouched.