Conversation
Pins tauri to 2.11 (2.11.1 ships the remote-origin ACL and .localhost origin fixes; 2.11.0 adds Webview::eval_with_callback and lets the on_new_window handler run on the main thread without Sync, which the built-in browser's popup routing relies on). tauri-runtime-wry 2.11.4 requires wry 0.55, so wry lands at 0.55.1. The JS side moves in step to @tauri-apps/api 2.11.1 / @tauri-apps/cli 2.11.4.
… decision function Moves the pure parsing helpers out of link-safety.tsx into lib/link-classify.ts (behaviour unchanged, re-exported for existing importers) and adds classifyLinkTarget as the single first step every link entrance runs: local path, OS handler, http(s), or an unsupported scheme that is still refused rather than handed to the OS. Adds lib/resolve-link-action.ts, the synchronous decision behind the built-in browser: classify, then terminal rules (host block rules and the remote-workspace loopback override, neither invertible), then the per-source preference or an explicit choice, then the modifier-key inversion, then placement (file column vs viewer drawer). lib/browser gains the URL helpers (loopback / private-network detection, dedupe normalization) and the per-key localStorage preferences the decision reads; nothing consumes the new modules yet.
Adds the src-tauri/src/browser module: wire types mirrored by the
frontend, the scheme allow-list policy, a tab registry, and two
surfaces — an embedded child webview for macOS / Windows and an owned
window for Linux and fallbacks — plus the first browser_* commands
(capabilities, open, close, bounds, visibility, navigate, reload,
state, list) and the browser://state / browser://closed events.
The embedded surface is built straight through tauri-runtime-wry's
re-exported wry (WebViewBuilder::build_as_child on the owner window)
rather than tauri's Window::add_child. A tauri child flips the owner's
is_webview_window() to false, after which get_webview_window("main")
returns None and every command taking a tauri::WebviewWindow argument
fails; wry-built children are invisible to tauri, so the workspace
window keeps behaving as before and no `unstable` feature is needed.
wry webviews are not Send, so instances live in a main-thread
thread-local behind a cloneable ChildHandle that hops to the main
thread per operation.
Also adds a dev-only puppet (feature browser-smoke) that executes
JSON commands from a control directory through the same _core
functions, used to verify the surface without UI automation.
…option Every embedded tab now gets a helper script in a WebKit content world named "codeg" plus a native message handler (objc2), installed before the first document loads: the webview is built with no URL and navigated only after the channel is in place, which also keeps about:blank out of the tab's history. The helper reports SPA navigation and document titles the engine does not surface, and records user gestures (click / auxclick / keydown with the anchor found along composedPath) for the popup policy. Page scripts cannot see the world, and prototype tampering in the page world does not affect it. The shim also provides world-scoped evaluation, PNG snapshots (takeSnapshotWithConfiguration) and back / forward / stop, wired through the surface enum and new browser_go_back / browser_go_forward / browser_stop commands. Page-initiated new windows go through wry's new_window_req_handler: a request without a user gesture in the last second is denied (popup blocker); everything else is answered with NewWindowResponse::Create using a child webview built from the opener's WKWebViewConfiguration, so window.opener, referrer and postMessage keep working while the host only decides presentation — the popup is adopted as a tab next to its opener and announced on browser://popup. A popup shares its opener's user-content controller, so world installation is idempotent per controller and messages are attributed to tabs by source webview. The helper no longer opens middle-clicked links itself: WebKit already treats a middle-button auxclick on an anchor as a link click and asks for a new window, so doing it in script produced two tabs.
file-tab-id learns a `browser:<backend id>` kind, and the workspace tab type becomes a discriminated union: file-like tabs keep their shape, browser tabs carry only a seed (initial URL, opener) and never a path. Live per-tab state (url, title, loading, history flags) lives in a separate useSyncExternalStore-backed store fed by browser://state, so page activity never churns the fileTabs slice. openBrowserTab opens one tab per URL (fragment ignored) and re-activates an existing one; adoptBrowserTab registers a popup the backend adopted, inserted right after its opener. Closing a browser tab releases its native surface. BrowserEventsBridge, mounted once in the workspace layout, subscribes to browser://state, browser://popup and browser://closed only when the backend reports a browser is available. Also adds the TypeScript mirror of the Rust wire types and the browser_* command wrappers.
…d viewer drawer A browser tab now renders in the file pane: a toolbar (back / forward / reload-stop / address bar / copy / open in system browser, with a loading bar), notice bars for blocked popups and remote egress, and the BrowserSurfaceHost placeholder the native webview is fitted to. The host creates the surface once per tab record, keeps the webview's bounds equal to its rect (ResizeObserver + rAF), and hides it whenever the rect is not truly visible — the file column CSS-hidden in conversation mode, a full-page route covering the workspace, a DOM error page replacing the page — handing focus back to the main webview first. Native views paint above the DOM, so overlays take occlusion leases: Dialog, AlertDialog, Drawer, DropdownMenu and ContextMenu content hold one while mounted (the browser viewer drawer opts out because it hosts a surface itself), and a MutationObserver fallback catches lease-less open dialogs and menus. Any lease hides every surface. The transcript's side panel gains a browser viewer for links opened under a full-page route, backed by the same workspace tab record. Tab strip items show the page's live title and a globe icon. The backend can ask a window to open a URL over browser://open-request (agent tools and deep links later; the dev puppet now). Strings live under the new Browser i18n namespace in all ten locales.
… surfaces The shared Dialog / AlertDialog / DropdownMenu / ContextMenu / Drawer content wrappers stay mounted while their overlay is closed — only the primitive inside unmounts its DOM — so a lease taken on wrapper mount was held by every closed overlay in the tree (30 leases with nothing open) and every browser surface stayed hidden. The lease now lives on the content element's callback ref: acquired when the node attaches, released when it detaches, which is exactly when the overlay is really open. Verified against a live dropdown: opening it hides the active tab's webview, Escape brings it back. BrowserEventsBridge closes any surface the backend still holds when it first mounts: tab records are session-only, so after a document reload those webviews would otherwise stay painted with nothing left to hide them. The dev puppet gains a browser_debug op exposing the native hidden flag and frame next to the registry's view.
…ecision useOpenUrlTarget decides and executes where an http(s) (or mailto/tel) address goes, inside the click's own call stack: resolveLinkAction over a preferences snapshot and the current surface (built-in browser available, file column visible, viewer host present, remote window), then a built-in tab, the transcript's side-panel browser under a full-page route, the system browser, or the OS handler. The first built-in open shows a one-time toast with an "always use the system browser" action. link-safety's http(s)/mailto/tel branch now goes through it (local file paths keep their existing route); the modifier state of the click rides along even though Streamdown's link-safety contract only passes the URL. Web links in the transcript gain a context menu: built-in, system, copy. The terminal's WebLinksAddon gets a real handler, which also fixes the dead link click in the desktop webview (xterm's default is a bare window.open). The open primitives move to lib/link-open.ts, and browserCapabilitiesSnapshot() gives synchronous access to the resolved capabilities; until they resolve, links go to the system browser.
…pability labels A ⌘/Ctrl-click on a plain anchor lets the engine navigate the tab in place; the navigation handler now consumes the matching gesture from the tab's ring (click, button 0, primary modifier, no target/download, same href without fragment) and asks the frontend for a background tab instead, cancelling the in-place load. Also adds a test that no capability window pattern ever covers browser-*/browser-popup-*/codeg-doc-* labels or uses a bare wildcard.
The open request now carries the originating tab; the workspace inserts the new record next to it (and inherits its folder) instead of appending at the end, matching how browsers place ⌘/Ctrl-clicked tabs.
…d clear browsing data Adds the Built-in browser section to General settings: default target per link source, web inspector switch, surface override and a confirmed clear of cookies, caches and site storage (new browser_clear_data command; on macOS straight at the shared WKWebsiteDataStore so it works with no tab open). The inspector and surface preferences existed but nothing read them: the surface host now passes both when it creates a tab, the child builder honours devtools per tab and popups inherit the opener's value. Also: the pop-up notice bar can open the blocked address as a tab next to its opener, and the file header shows a browser tab's live title.
The surface host drove an owned window's visibility from its placeholder rect, and the tab view hides that placeholder — so the window was hidden the moment it was created. Owned windows now follow only the tab-on-screen signals and never receive bounds; showing one also focuses it.
wry 0.55.1 unwraps `WKWebView.URL`, which is nil until a document commits: calling tauri's `WebviewWindow::url()` on a browser surface that never navigated (or whose first navigation failed) panics the main thread and takes the app with it. Child surfaces now read their URL through the macOS shim and owned windows report theirs from the tab state; the dev puppet skips `browser-*` windows. wry also has no navigation-failure callback and reports "load started" at `didCommitNavigation`, so a host that never answers left the tab spinning forever. Every navigation now arms a `load_seq`-guarded watcher that polls `WKWebView.isLoading` and, once the engine gives up without a document, raises `error: failed` with the requested URL. Timing follows the engine (a refused connection ~10s, a black-holed one ~60s); nothing is capped on our side. The wording is the status layer's, so the error page reads in the user's language instead of an English backend string.
`WKWebView.reload()` has nothing to reload when the first navigation failed before committing a document, so the error page's retry button did nothing at all. `reload_core` now re-runs the requested navigation when the surface has no document, which also re-arms the load watcher.
Browser tabs used to live in WebKit's default data store together with the workspace webview, so "clear browsing data" wiped the app's own localStorage along with the pages' cookies. Tabs now get a profile of their own: a persistent `WKWebsiteDataStore` by identifier on macOS 14+, a dedicated WebView2 user-data folder on Windows and a data directory on Linux. Regular tabs are built from a configuration that carries that store; popups keep inheriting their opener's. Clearing works on the profile only (older macOS, where there is no separate store, removes records per origin and spares the app's hosts). That container is also where a proxy lives, so the built-in browser now uses the app's "system proxy" setting — the same process environment the app's own requests and agent processes see. macOS applies it live to the profile store through `proxyConfigurations` (Network.framework resolved at run time; the symbols exist only on macOS 14+), with loopback excluded so local dev servers keep working; Windows freezes it in the environment arguments of the first browser webview (a restart applies a change); Linux passes it per window. `browser_capabilities` reports `isolatedStorage` and the proxy status, and the settings section shows a read-only "Network proxy" row.
The load watcher only raised an error when nothing had ever committed; navigating an existing page to an address that fails just stopped the spinner and left the old page, with no hint of what happened. Any load that ends without the requested page now shows the error page for that address (a failed reload of the page already showing still just stops the spinner), reloads arm the watcher too, and retry re-navigates to the failed address instead of reloading the document underneath.
A browser tab is one URL, so unlike a file tab it is worth bringing back: the dev-server page next to the chat is exactly what a user looks for after relaunching. Records are restored "not loaded" — the native surface of a tab is created when it is first shown — so restoring twenty tabs costs twenty small records and no webviews. - `lib/browser/browser-tab-persistence.ts`: per-window-label localStorage list (url, title, folderId), versioned, sanitised on read so one corrupt entry cannot take the list with it. The stored address is the first web URL among the live one, the requested one and the record's. - `BrowserTabsPersistence`: restores once per document, then debounces writes behind the tab store. Nothing is written before the restore has run, or the empty strip of a fresh document would erase the previous run's tabs. - The surface ledger moves from the host component into the store, so a released tab is "not loaded" again and the same host resumes it. - Optional background unload (off by default): a tab off screen for 30 minutes has its surface released; its record keeps the page it was showing. The tab strip draws unloaded tabs faded. - ⇧⌘T reopens a closed browser tab at the page it was showing.
P1 refused every download because there was no destination policy and no UI. Both now exist: the engine transfers the file, the host decides only where it may land and reports what happened. - `browser/downloads.rs`: destination = the OS downloads folder (never the app's data dir). The name comes from the server (`Content-Disposition`), so only its last component is used, and only after `safe_file_name` strips separators, parent hops, drive letters, control characters and leading dots — a suggested name can only ever name a file directly in that folder. An existing file is never replaced: ` (1)`, ` (2)`… until the path is free. - Records live in managed state and reach the frontend on `browser://download` when a transfer starts and when it ends. macOS reports no path on completion (WebKit API limit), so the destination chosen at request time is remembered and matched back by URL. - Download bar per tab, hydrated from `browser_list_downloads` so a frontend reload does not lose a running transfer. It offers "show in folder" for a completed file and never opens anything. - The settings section says where downloads land.
A navigation that turns into a download never commits a document, so the load watcher saw "the requested address never arrived" and painted the error page over the page the user was still looking at — found by downloading a file from a directory listing in the dev app. The tab now settles instead: the watcher is retired, loading and error are cleared, and the requested address goes back to the document the tab is showing. The download reports itself through its own bar.
The search is WebKit's own (`findString:withConfiguration:`), so the page cannot see it, style it or break it, and the highlight is the engine's — the bar only collects the query and reports whether the last step matched. No match counter: the API answers "did this step find something", and a made-up count would be worse than none. ⌘F reaches the bar from either side of the boundary. While the page has keyboard focus the app's DOM never sees the keystroke, so the injected helper reports it over the isolated-world channel as a `shortcut` message and the host forwards `browser://shortcut`; the set of shortcut names is closed on the host side, so whatever a page claims, only `find` can act. This is the one place the helper cancels an event — the same claim every browser makes on its own shortcut — and it leaves propagation alone. Embedded surfaces only; an owned window is a plain WebviewWindow whose WKWebView the host does not hold.
…and find Independent review of the P2 batch, in three fenced slices. Every fix below came with a concrete triggering sequence; the ones I pushed back on are noted at the end. Tab persistence and surfaces: - `restoreBrowserTabs` merged instead of aborting. A tab opened before the restore ran (deep link, agent request — the capability probe is a round trip) made it skip the whole restore, and the next save then overwrote the stored set with that one tab. Now it appends what is not already open. - Surface claims carry a token. `browser_open_tab` is a round trip: if the tab was closed while it was in flight, `browser_close` reached the backend first, did nothing (no registry entry yet), and the webview that arrived afterwards stayed painted over the workspace with no tab behind it. A holder whose token is stale now closes what it built. - A mounted surface host re-creates its surface if the tab's live state disappears, and `suspendBrowserTab` refuses a tab that is on screen — the background unload could otherwise fire between a host's mount and its effect and leave the pane blank. Downloads: - Two downloads started at once could be handed the same destination: the free-path check cannot reserve on disk (the engines require a destination that does NOT exist), so the paths handed out are now held in memory until the transfer ends. - A symlink in the way counted as "nothing here" (`exists()` follows it), so the engine would have written through it to wherever it pointed. - `CON`, `NUL.txt`, `LPT1` and friends still name devices on Windows. - The history cap could evict a RUNNING download, leaving its completion with nothing to update and the bar saying "downloading" for ever. Only finished records are evictable now, and the frontend store caps itself too. - Dismissing one download's row dismissed every tab's rows. - A download no longer retires the load watcher outright: it marks the load generation, and the watcher settles quietly only for that generation. The old version swallowed the failure of a LATER navigation whose load was in flight when a delayed download callback arrived. Find in page: - ⌘F was dead whenever the caret was inside an iframe: a keystroke only reaches the focused frame, and the handler required the main frame. The tab id comes from the source webview, not the payload, so a subframe still cannot speak for another tab. - Searches are sequenced; a stale answer no longer relabels the current query. - A timeout is an error, not "no match" — WebKit may highlight a match a moment later and the bar would have been saying the opposite of the screen. - Pressing ⌘F with the bar already open re-focuses it. Pushed back, with reasons: `BrowserTabView` is keyed by tab id at both mount sites, so no state crosses tabs; and two downloads of the SAME url can still swap their record labels, because the engine's completion callback carries only the url — the files themselves land correctly.
Re-review of the previous fix found the race it left behind. A backend tab id is reused across generations — a suspended tab is released and, when the user comes back to it, created again under the same id — and both commands are round trips the backend may run in either order. The close issued for generation 1 could therefore arrive after generation 2 had registered and destroy the live surface, leaving a tab that believes it is loaded showing nothing. Lifecycle calls for one id now go through a per-id promise chain, so they reach the backend in the order they were issued. An idle chain still calls straight through (closing a surface should not wait for a microtask), a failed op does not stall what is queued behind it, and the chain is dropped once it drains. The stale-token cleanup only closes when nobody holds the claim: if the id was re-claimed, that owner's create is already queued behind us and a close from here would land after it. Also from the re-review: a download in an owned window left the tab loading for ever. Owned windows have no load watcher — `is_loading` has no handle to answer from — so nothing consumed the generation mark the previous commit introduced. They settle immediately instead. Verified while checking whether the download could be correlated by URL instead: a navigation that 302s to a file reports the FINAL url, not the one navigated to. Matching on the url would therefore put an error page over a perfectly good page for every redirected download, so the generation mark stays, with its known limit written down.
…overlays Site rules (W2.5): a per-site table in Settings → Built-in browser — always the built-in browser, always the system browser, or block — with the most specific pattern winning (hostname, *.suffix, *, optional :port). The link decision consults it; the backend enforces `block` on every navigation a tab attempts, on pop-ups and on the open/navigate commands, showing a block page. An administrator's policy file (`policy.json` in the machine-wide config directory, or CODEG_POLICY_FILE) can fix rules and turn the feature off; its rules show read-only in the settings section and are consulted first. Load errors (W2.4): a wrapper around wry's WKNavigationDelegate reports provisional starts and failures, so a failed load shows the right error page (DNS / TLS / other, with the engine's own wording) the moment WebKit gives up instead of after the load watcher's poll, and page-initiated navigations update the requested address. A load WebKit refuses silently — a restricted port commits an empty document instead of failing — is recognised by the blank commit standing in for the page that was started. Refused top-level navigations (a scheme not allowed in a tab, a blocked host) now show a notice on the tab; a mailto:/tel: the page pointed at can be handed to the OS from it. Frames may hold about:srcdoc, data: and blob: content, which the top-level allow-list used to refuse. Freeze frames (W2.4): hiding a surface under an overlay first captures the page's last frame (JPEG through AppKit, a few milliseconds) and the placeholder paints it until the surface shows again, so a dialog or menu opens over the page rather than over a blank pane. A visibility request that a newer one overtook while capturing is dropped instead of applied late.
…nd visibility Site rules: a host is matched without its trailing dot and with IPv6 literals in the canonical form the URL parser uses, so `example.com.` and `[0:0:0:0:0:0:0:1]` cannot slip past a rule for `example.com` / `[::1]`; brackets in a pattern must hold an IPv6 literal; the frontend lower-cases ASCII only, like the backend, so a Unicode fold cannot make a pattern one side accepts and the other drops. A blocked address typed while another page was still loading now stops that load and retires its watcher, or its commit or failure would have landed on top of the block page. Load state: a server redirect updates the address the tab is heading for (the error page names the redirect target); a load the policy interrupted — a refused redirect target, or a response that became a download — is settled on the page the tab shows instead of being reported by the watcher as an address that never arrived (measured: WebKit re-asks the navigation policy for a 302 target, so a redirect to a blocked host is refused; it then fails the load with WebKitErrorDomain 102, which is not a page failure). A popup id that is already taken is skipped rather than replacing the live surface behind it. Visibility: hide/show requests for a tab now apply one at a time under a per-tab lock in arrival order, so a show can no longer complete between a freeze capture's sequence check and the hide it guarded (measured with 40 interleaved requests: the final state is the last request's). Frontend: a click made in the first moments after launch, before the backend has said what it can do and which hosts the administrator blocks, is held until that answer is in instead of being routed on a guess (routed to the system browser it would have slipped past a managed block); the events bridge subscribes to preference changes before its first await, so a rule written by the settings window during the capabilities round trip reaches the backend, and a failed push is retried once; a stale show's answer no longer wipes the frame a newer hide painted; the frame is dropped when the error page takes the surface's place; the settings editor compares patterns in normalized form and keys rows by position.
The cold-start deferral held every link until the backend had answered, including mailto:, tel: and file paths, which do not depend on the answer at all. Only an http(s) click waits now. The link-safety tests that click on the desktop get an answered backend (no built-in browser), which is the state the app is in whenever a user can click.
…hey match Two spellings of one host (`[::1]` and `[0:0:0:0:0:0:0:1]`) can both sit in the table; among equally specific rules the more restrictive action now wins on both sides, and the settings editor recognises such duplicates by what a pattern matches rather than by its text. A bracketed pattern must be an IPv6 literal on both sides (`[*]` is not a wildcard). The delegate wrapper now tells navigations apart by the WKNavigation object WebKit hands to every callback of one navigation: a redirect, interruption or failure reported for a superseded navigation — a download the previous document started, an earlier load a newer one replaced — no longer settles or fails the load in flight. A visibility request also remembers which incarnation of a tab id it was made for, so a hide still capturing its frame when the tab is closed and reopened under the same id cannot apply to the new tab; a destroyed window's tabs drop their visibility locks with them.
…hanges atomic `str::trim` and `String.prototype.trim` disagree on U+0085 and U+00A0; a pattern one side parsed and the other rejected was a rule the frontend silently ignored. Both sides now trim ASCII whitespace only. A visibility request takes the surface and its stamp in one registry operation and checks the stamp in the same operation that records the change, so a close-and-reopen of the same tab id cannot pair an old surface with the new tab's stamp; a tab's visibility lock is deleted while the tabs lock is still held, in both removal paths, so a tab inserted under the same id in between keeps its own lock.
Rust's is_ascii_whitespace does not include vertical tab; the frontend's trim class now matches it character for character.
clippy's type_complexity on the (kind, length, port, restrictiveness) tuple; behaviour unchanged.
Conflicts and how they were settled: - link-safety.tsx — the branch moved the parsing helpers into `@/lib/link-classify`; main added `canOpenLinkOrFile` next to them. Kept the extraction and re-implemented `canOpenLinkOrFile` on the imported helpers, so the composer's right-click "Open link" row still has its gate. - terminal-view.tsx — both sides landed on the same line: the branch's link-decision refs and main's `ignoreAppZoom` zoom derivation. Both kept. - workspace-context — both sides added tab work at the same import and the same two `describe` blocks got interleaved. Split back apart. - i18n × 10 — main's new canvas keys close the canvas section; the branch's `Browser` section follows it. Integration the merge itself required: main's "a reopened tab goes back to the slot it was closed from" now covers browser tabs too. They share the file strip and the same reopen shortcut, so `ClosedBrowserTab` carries an `index`, the three close paths pass the slot they already hold, and `openBrowserTab` takes an `index` the way `openFilePreview` does. Two tests moved with the code they cover: the composer's "Open link" row lands on the system browser through the branch's link decision (web mode → `window.open`) rather than main's direct `openUrl`, and the controller source test counts four reopen call sites now that browser tabs are one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aring it Third review round of the profile work. - An admission is shared with the engine's completion callback on macOS, so a clear whose caller stops waiting still counts as in flight until WebKit is done; on Windows and Linux the surface API reports no completion, and the admission ends when the call returns (a deletion that follows at once may have to be retried, which the comment states). - Frontend: a record whose surface is being created is not dormant and is left alone when its profile disappears; suspending a tab of a deleted profile drops the record when the default profile already shows that page; the viewer drawer keys its remembered record per body instance, forgets it on unmount, and once resolved shows that record or nothing. The popup-precedence test picks the default-profile opener explicitly.
Fourth review round of the profile work (frontend only). - Suspending a tab of a deleted profile checks for a duplicate in the default profile inside the state updater, against the list as it is when the update applies, and moves the active-tab pointer to the neighbouring survivor the way closing a tab does. - The viewer drawer's body is keyed by URL, so a drawer handed another address starts a fresh body and never shows the record it remembered for the previous one under the new header.
First build of the branch by a Windows compiler. `doc_open_core` left its surface untyped on any platform but macOS (a diverging cfg block), and the imports that only the macOS popup path uses were unconditional; the doc comment of `build_child` had drifted onto the Windows web-context thread-local. The document guest now compiles wherever the child surface does (it stays refused at runtime by `doc_guest::supported`), and `doc_guest`'s `Duration` import follows its unix-only use.
Every embedded-surface call that is not a plain wry one answered "not
implemented on this platform yet" here: no page channel (so no SPA address
sync, no gesture ring, and therefore every page-initiated window denied as
"no-gesture"), no history, no stop, no load state, no snapshots, no find, no
typed load failures, and no user-agent exception. `shim/windows.rs` fills them
in against WebView2, next to the macOS shim and behind the same contract.
The page-to-host channel is a CDP isolated world named `codeg`:
`Page.addScriptToEvaluateOnNewDocument {worldName}` injects the helper and
`Runtime.addBinding {executionContextName}` puts the send primitive in the same
world, so Windows needs no prefix script. Every step is awaited before the
caller navigates, and the message loop is pumped without the surface map held,
because a tab closing on that pump would find it locked. Which frame spoke is
decided from `executionContextCreated.auxData.frameId` against the top-level
frame id -- never from the envelope's own `top` field, which the page's frame
controls, and an unknown context is not the main frame.
An adopted popup's first document is already on its way when the engine hands
the webview over, so the injection cannot reach it and that one document would
have no channel for its whole life. It gets its world from
`Page.createIsolatedWorld` once it has settled; a binding registered by world
name is in it.
Popups are adopted the way they are on macOS -- the engine navigates the
webview it was handed, so `window.opener`, `Referer` and `noopener` are the
page's own -- by building the child in the opener's environment, which is what
`NewWindowResponse::Create` requires.
Load failures are the engine's `NavigationCompleted` status, with one Windows
judgement on top: the status says "connection aborted" both for a load that
broke and for a navigation that was abandoned because it became a download or
was superseded. Chromium commits its own error page for the first and nothing
for the second, so a failure is only reported when the address that failed is
the one now showing. A stop the host asked for is marked and never reported.
Without this the tab reported a perfectly successful load of an error page the
user never asked for, or covered a page they were reading with an error page
because they clicked a download link.
Subframes get their own fence: `NavigationStarting` is main-frame only, so an
iframe was outside the scheme list and the `block` site rules altogether. Every
frame is caught as it is created, frames of frames included, and asked the same
question the main frame's handler answers.
The sign-in identity takes two calls, because they answer different questions:
`put_UserAgent` is what the page sees and does reach the document being
navigated to, but not the request already in flight, and
`NavigationStarting`'s request headers are read-only. What goes on the wire is
written per document request in `WebResourceRequested`, in both directions, so
a navigation away from a sign-in host does not carry the borrowed identity out
with it.
A tab whose isolated-world helper never got installed still opens, navigates and renders — it just quietly stops blocking nothing and opening nothing: pop-ups the page asks for are all refused as `no-gesture`, the address bar stops following in-page navigation, and the find shortcut pressed inside the page goes nowhere. Until now the only trace was a `tracing::warn!` nobody reads. `BrowserTabState` carries the engine's own words for the failure in `channelError`, filled at each of the three installs (tab, document guest, adopted pop-up) and cleared when the helper's `hello` proves the round trip. The notice bar says what the tab lost, once the page has finished loading and the channel has stayed down for a moment — every tab is degraded until that first `hello`, so the wait is what keeps the bar off an ordinary open. The engine's words go in the tooltip, for a bug report rather than for reading. `browser_capabilities` stops answering `degraded` with "page channel not installed yet": it now reports what a new tab will be given, which is the native channel wherever the embedded surface is compiled.
`listeners_close_when_released_and_idle` measured two stretches against one `Instant`, and between them it waits on a port that was just closed. macOS refuses that connection in milliseconds; Windows takes about two seconds, which is more than the one second of margin the last sweep leaves — so the held listener looked one second too young to close and the test failed on Windows every time.
The second download a page starts raises a WebView2 permission request, and until it is answered the engine draws its own bubble over the page and holds `DownloadStarting` back. Inside an embedded tab that bubble belongs to nobody: it is not in codeg's design language, it lands wherever the page happens to be, and codeg never hears about it — so a user who clicked a download link saw nothing happen at all, and the download bar stayed empty. The host answers it now, for that one permission kind only; a camera or a microphone is the user's to grant and the engine's prompt is the right place to grant it. The test is the popup blocker's: a tab that was clicked recently is doing what the user asked, and every file it takes shows up in the download bar. One that was not is refused and says so in the notice bar. The window is longer than the popup's second because a "download all" button fires its requests over the time the server takes to answer each one, and the engine only asks once the second download is already on its way. The answer is deliberately not saved in the profile. The engine would otherwise write the first one down and stop asking, freezing whichever way one page happened to be treated — and what this weighs is the click that came before the request, not the site.
The guest's code path was written for both platforms and had never run on Windows, where it sat on `about:blank`: WebView2 has no custom schemes, so wry serves ours from `http://codeg-doc.<host>/…` — a rewrite it applies to a webview's INITIAL url only, and a guest is built empty and navigated once its grant is bound. The engine was handed a scheme it does not know. `engine_url` rewrites the one address that goes to the engine; everything else — `is_document_url`, the CSP's `'self'`, the handler reverting the request it receives — already read both spellings. With that, everything the guest promises holds there: relative stylesheets, images and `fetch` resolve on its own origin, safe mode runs no script, dynamic mode does, a path that climbs out of the root is not served, and the network is refused by `connect-src 'self'`. Its storage is a private one, apart from the browsing profile. One difference from macOS is worth knowing: two guests alive in the same run share that private storage, because Windows partitions it per environment while macOS gives each guest a data store of its own. It takes dynamic mode on both sides to matter, and the fence around the network and the filesystem is unaffected — but a per-guest document host would settle it on both platforms and is the better fix when someone can test the macOS half.
"Show in folder" did nothing at all when the download folder is a share or a redirected location: the opener plugin resolves the path before handing it to the shell, and on Windows that produces the extended `\?\UNC\…` form, which `ILCreateFromPath` refuses. Nothing was reported either — the click just went nowhere. Falling back in the frontend alone does not reach: opening the containing folder is scoped to `$HOME/**`, and the paths that need the fallback are exactly the ones outside it. So the fallback asks the backend, which reveals the path IT recorded for that download — the caller names a download, not a path, so nothing new becomes reachable through it. Two Windows details are load-bearing. `/select,` wants the path quoted inside its one argument, a shape `Command::arg` cannot produce, hence the raw command line. And Explorer does not follow `/select,` into a UNC path at all — it drops the argument and opens a default folder — so a network location gets its folder opened instead of the file selected, which is the whole errand minus the selection.
A guest may navigate to its own addresses, and on Windows those are spelled as an ordinary http host, so the check accepted anything that began with that prefix — `https://codeg-doc.example.com/` included. Any registrable domain could therefore be a guest address, which is how a document the user allowed scripts for would send what it read to whoever wrote it. It is the one mapped host now. Two smaller ones from the same review: the quote that a hand-built Windows command line cannot take is refused on Windows alone, where a downloaded name cannot contain one anyway, rather than on platforms where a quote is an ordinary character and the argument is passed structurally; and a backslash only ends a path component where no slash does, so a unix file named `report\\2026.pdf` falls back to the folder it is really in. The degraded-channel bar no longer says navigation inside the page is limited — it works, it is the address bar that does not follow it.
…its own The first cut of this fallback took the last separator of either kind, which mistakes a backslash in a unix file name for a folder boundary; the second took backslashes only when the path had no forward slash at all, which loses the last component of a Windows path that mixes them (Windows accepts both). A path that announces itself as Windows — a drive or a UNC share — is now cut at whichever separator comes last, and any other path only at a slash.
Reaping a watch child queued kill + wait on a detached task. The callers who most need the kill are the ones whose runtime is about to go away — a shutdown, a test returning — and a queued task is dropped unpolled there, so the child outlived the reap. It showed up as `sleep 600` processes surviving `cargo test`; because they inherit the runner's stdout, `cargo test | tail` then never saw EOF and looked like a hung suite. The real watch child was covered anyway by kill_on_drop, which is a second net and was doing the first net's job. The signal now goes out on the calling thread and only the `wait()` that keeps a killed child from lingering as a zombie stays on a task, where losing it at shutdown costs nothing. The test sleepers are kill_on_drop too, like the real one, so a test that panics before it removes its entry cannot leave one behind either.
Installing the page channel was injecting the helper first and registering `__codegSend` second, and every step of the install waits for WebView2 by pumping the message loop. An adopted popup's first document is already on its way when the engine hands the webview over, so it can commit inside that pump — and with the injection registered first it commits into a `codeg` world that has nothing to send through. That document then has no channel for the whole of its life: the world exists, so the recovery that builds one for a document the injection did not reach never fires, and the tab sits silently degraded. `Runtime.addBinding` takes a world NAME and does not need the world to exist, which is also how the recovery path already gets its binding. Registered first, a document that commits in the window has no `codeg` world at all — which is exactly the state the recovery knows how to fix.
…commits The identity a page is shown is set as its navigation starts, on the address it is heading for — the only moment early enough to reach the document it commits. A navigation that commits nothing leaves that setting behind on an identity meant for a page that never arrived, and the document still on screen goes on answering `navigator.userAgent` with it and sending it with everything it asks for afterwards: a sign-in host's borrowed identity left over an ordinary site, because the user pressed stop or the response turned out to be a download. Each of the three ways a navigation ends without replacing the page now decides the identity again, from the document that is actually showing. A load replaced while still in flight is told apart by the engine's own navigation id. The newer navigation owns the load flag, the address in flight, and the identity — the superseded one may not take any of them back on its way out, nor report a failure for an address that is no longer the one being loaded.
Whether a message came from the main frame is decided from the execution context the engine reports it in, and that is the one thing about a message the page cannot influence — so the map from context to frame has to be right. CDP hands out context ids per renderer process, each starting over, and the map only ever lost an entry when the engine said so. An entry nobody retired could therefore be answered for by an id that now belongs to some other frame. A frame holds one `codeg` world at a time, so recording a new one retires whatever older entry named that frame: its document was replaced, or the renderer that owed the destruction notice went away without sending it. Either way it is gone. This also settles which context the main frame's world is, where two entries for one frame used to be picked between by hash order.
…t there The recovery that builds a `codeg` world by hand for a document the injection could not reach was gated on `channel_installed`, which `install_world` only sets at the very end — so through every message pump the install waits on, the one thing that could rescue a document arriving in that window was switched off. The gate also sat below the check that drops a superseded navigation's completion, and that check is about the navigation, not about the page: a load replaced while in flight leaves on screen the document it did not replace, which then keeps no channel at all if its own completion was the superseded one. Both are now one question — does the document on screen have a world — asked where the answer can have changed: at the end of the install, and at every completion before anything navigation-specific is decided. What it must not do is build a world for a surface that has simply never navigated, so the top frame's first commit is recorded and a webview still on the document it was created with is left alone. A context is also keyed by the CDP session that reported it. Ids are handed out per renderer and start over in each, so an id alone did not name a context, and a frame in a renderer of its own could hold the number the page's world has — in a map that decides whether a message came from the main frame.
Linux has no embedded surface — a child webview cannot be positioned there — so a tab is an owned top-level window, and until now that was a window with nothing behind it: no page channel, and therefore no gesture ring, no address bar that follows the page, no shortcut the page had focus for; no history, no stop, no find, no snapshot, no freeze frame, no sign-in identity; and one wording for every load failure, because wry forwards no failure at all and WebKitGTK quietly replaces the document with an error page of its own. The window now carries the shim itself. The helper and the primitive it posts through go into a WebKitGTK script world named `codeg`, which is the same arrangement macOS has and spells the send primitive the same way, so both share one prefix script. What differs is how a message is known to be the page's own: a `script-message-received` reports a value and nothing about the frame that sent it, so there is no frame to check against. The helper is therefore injected into the top frame ALONE, and the handler it posts through is reachable from the `codeg` world alone — a subframe has no sender rather than an unverifiable one. The engine webview is taken hold of once, at creation, because tauri hands it out on the main thread only and it cannot cross threads; two callers need it synchronously from that thread, one of them the navigation decision, which is where the sign-in identity has to be set for it to reach the request. Two things the engine does not make possible here, rather than pretend otherwise: it reports no frame for a navigation decision, so the top-level scheme list applies to every frame (a `data:` frame is refused — quietly, since a refusal a frame would have been allowed is not worth a notice, and widening the list would let a page put `data:` in the address bar); and a name that does not resolve is only told apart from any other transport failure when the resolver's own error survives. The branch did not compile for Linux at all before this: the document command's surface block is nothing but a `return` there, and a diverging block tells the compiler nothing about what the rest of the function is holding.
…verywhere else `window.open` did nothing at all on an owned window: nobody answered the engine, so the request was dropped without a word — and with no channel there was no gesture to judge it by anyway. Both are in place now, so the answer is the one the embedded surfaces give: the scheme list, the site rules, then a user gesture the page can point at within the same second, and only then a window of its own, registered as a tab beside the one that asked and reported as adopted or denied with a reason. The window is built RELATED to the opener, which WebKitGTK insists on for a page-initiated window and which is also what carries `window.opener` across — so the engine navigates it itself and `Referer` and `noopener` stay the page's own business. A sign-in flow that hands off to a popup works on Linux for the first time. The gesture window moves to `policy`, where both surfaces can ask it.
…ge be searched `browser_capabilities` described an owned window by what the macOS and Windows fallback can do, and the find bar was closed for one on every platform. On Linux the owned window is the surface the shim is written for: it answers find, history, stop and snapshots, and its page talks to the host. The frontend now asks rather than assumes, and ⌘F in a Linux tab opens a bar that searches the page in the window it is showing in. The sign-in identity's capability follows the same correction: what it needs is a navigation hook, which the owned window has on Linux.
…ad the mark A navigation that turns into a download leaves a mark the failed-load watcher reads, so it does not paint an error over a page the user is still on. Where no watcher runs the mark is never read, and the tab has to be settled on the spot or it spins for ever — which the code decided by asking whether the surface was embedded. That is no longer the same question. An owned window on Linux answers a load state now, so its watcher stays alive and consumes the mark, while the same window on macOS and Windows still cannot. Asking the surface for the load state asks the thing that actually decides.
Review of the Linux surface, and of the Windows fix before it. Six things, the first of which made most of the Linux work unreachable. - A tab installed the page channel only when its surface was embedded, which used to be the same question and no longer is: the owned window carries the channel on Linux. Every ordinary Linux tab was therefore left degraded — no gesture ring, so every `window.open` denied for want of a gesture, no address bar following the page, no shortcut. The surface is now asked whether it can carry a channel, and the same answer feeds the capability the frontend reads. - The sign-in identity is withdrawn on Linux. WebKitGTK's user agent belongs to the whole webview and its navigation decision does not say which frame is asking, so any iframe — one a hostile page adds on purpose, or an ordinary embed — could put the borrowed identity on the top-level page's own requests, or take it off in the middle of a sign-in. There is no main-frame hook to hang it on, so it is not offered rather than offered wrong. - The helper now goes into every frame as well as the top one, posting through a second handler the host reports as not-the-main-frame. The top-frame-only injection was sound but cost what macOS deliberately keeps: a keystroke reaches only the frame that has focus and a click inside an iframe never reaches the top document, so find was dead and every popup an embedded page asked for was denied. `channel.rs` gates the second handler exactly as it gates macOS's subframes. - A failure reported once the document had committed was marked provisional, which paints an error page over the page the user is reading when a connection drops mid-body. - The window's destruction hook is attached before the engine webview is taken hold of, and an entry replaced under a label lets go of its webview's state — that state is keyed by a pointer the allocator can hand out again. - A popup recorded devtools as off however it was built, so its own popups inherited the wrong setting. On the Windows side: recovery is held back until `Runtime.addBinding` has run, because a world built before that has no send primitive in it — the helper gives up, and the empty world it leaves behind is one nothing would ever build again; a look turned away while an attempt is in flight is taken once that attempt fails; an adopted popup is told it is one, since `window.open()` with no address commits `about:blank` and nothing else can tell that from a webview that never navigated; and an event whose CDP session cannot be read is dropped rather than counted as the page's own.
…hich script ran first Both injected copies run in the top frame, and the one meant for subframes kept the privileged primitive only because it happened to be registered second. Which of them the engine runs first is not ours to promise, and if it ever ran the other way the page's own frame would report itself as a subframe: no address bar following it, no hello, a tab degraded for good. It now asks whether it is the top frame.
A recovery answers with the world the engine built when it ran the command, not when the answer arrived — so if the document was replaced in between, that world belongs to a document that is gone. The look turned away while the attempt was in flight then consumed its one retry against a map that still names the dead context: the engine answers commands on one channel and reports a context's death on another, and they need not arrive in that order. That one look no longer trusts "the main frame already has a world". Building a second time asks for a world of the same name, which is the same world, and a helper that has already run is one its own guard turns away — so the cost of being wrong is a round trip, and the cost of being right is a tab that talks.
`[target.'cfg(target_os = "linux")'.dependencies]` is unconditional on
features, so the webkit2gtk / gtk / javascriptcore-rs entries added for the
Linux shim applied to `--no-default-features` too. `src/browser` is gated on
`tauri-runtime` and compiles nothing there, but cargo still ran the gtk-sys,
gdk-sys and atk-sys build scripts, and those need `.pc` files:
The system library `atk` required by crate `atk-sys` was not found.
Neither place that builds codeg-server installs GTK — the CI cell's apt step
is gated on `matrix.mode == 'desktop'`, and the Dockerfile's backend stage
takes only pkg-config and libssl-dev. So this broke the server cell and the
Docker image, on a dependency line for code that build cannot reach.
Optional, and named in `tauri-runtime` beside the other desktop-only
dependencies. Verified by checking the Linux server target against a
pkg-config directory holding openssl and zlib alone: no GTK probe happens.
The comment also claimed these matched "the feature set wry 0.55 uses".
It does not: tauri-runtime-wry turns on wry's `linux-body`, which enables
`webkit2gtk/v2_40`, two steps above the `v2_38` declared here.
…form it is `capabilities_report_the_page_channel_the_surface_brings` still encoded the rule that a page channel travels with the *embedded* surface. That stopped being true when the Linux owned window learned to install the world itself: on Linux `CHILD_SURFACE_COMPILED` is false, so the test took its else branch and demanded `Degraded` from a build that correctly answers `Native`. This is the same stale equivalence review caught in `commands/browser.rs` one round earlier — "is embedded" and "has a channel" used to be one question. The test kept the old one. `capabilities` asked `cfg!(target_os = "linux")` for the same fact that `surface_window::HAS_CHANNEL` already states, and that `owned_window_controls` reports to the frontend three lines below. Both now read the constant, so the knowledge of which surfaces carry a channel lives in one place.
`v2_38` was picked to "match wry" and matched nothing: tauri-runtime-wry enables wry's `linux-body`, which turns on `webkit2gtk/v2_40`. Cargo unifies features, so every build already compiled the shim against 2.40 while the manifest claimed two steps lower. Naming the real level costs nothing today — no distribution that can build codeg at all is excluded, because wry has been asking for 2.40 the whole time. What it buys is a comprehensible failure later: if wry stops enabling `linux-body`, this is a pkg-config version error naming webkit2gtk rather than a missing symbol somewhere in `shim/linux.rs`.
W3.1 of the built-in browser plan: the snapshot engine, as a bundle for the isolated world. Nothing reads it yet — see "what is not here" below. `browser-agent/vendor/playwright/` is Playwright's aria tree at v1.63.0, the nine files that make up the import closure of `injected/ariaSnapshot.ts`, copied byte for byte. `src/index.ts` calls it in `ai` mode — the mode Playwright MCP uses — so an agent reading a codeg tab reads the shape it already knows, and puts `snapshot` / `elementForRef` on `__codegAgent` for world-scoped eval to reach. Not one vendored line is edited, which makes an update a copy and a diff rather than a merge. Three things buy that: esbuild transpiles without typechecking, so Playwright's sources need not answer to our compiler settings; the bundler resolves their `@isomorphic/…` alias instead of us rewriting their imports; and `browser-agent/` is excluded from the app's tsconfig and from eslint's view of the repo. The plan budgeted a second DOM pass here, to promote `cursor: pointer` / `onclick` / `tabindex` elements that carry no ARIA role. It is not needed: `ai` mode refs every element that is visible and receives pointer events, so a roleless clickable div is already namable and already carries `[cursor=pointer]`. The probe asserts exactly that, so the day upstream changes its mind we find out from a failing check. Two things it does that upstream does not: - **Refs survive being wrong.** Playwright's counter lives in the module, so each document starts again at `e1` and two pages name different elements the same. Every world draws a generation and reports it with the snapshot; a ref quoting an older one is refused, not resolved onto whatever is now `e1`. Navigation destroys the world, so this is automatic. - **The tree can be capped.** `maxChars` cuts on a line boundary — a half-written node reads as a real one — and says `truncated` so the caller knows the page did not simply end. Verified in a real engine, not jsdom: under jsdom every box is zero-sized, so `ai` mode yields a tree with no refs at all and proves nothing. `pnpm browser:agent:probe` drives the committed bundle in headless Chrome over CDP and checks nine things, including that the page's own world cannot see `__codegAgent`. The unit tests cover the part that is ours and pure. The bundle is committed so a cargo build never needs node, and CI checks it against its source — nothing else in the build would notice a stale artifact, since the Rust side will happily embed last week's bundle. **What is not here:** any way to reach it. The Rust seam is where authorization lives — `none / read / control`, per tab and origin, no automatic grants — and a page-reading path that predates the grant model is exactly the hole the red lines exist to prevent. The bundle and the grants land together, in the package that adds the `browser_*` tools.
Review found the snapshot's refs outliving a same-document navigation. The
generation only changes with the document, and `pushState`, `replaceState` and
a hash change leave the document, the world and the module exactly where they
were while the page becomes a different page. That is a route change in a
single-page app — most of what a dev server serves — and the nodes a framework
keeps across one, a header and its buttons, are exactly the ones still
`isConnected` afterwards. An agent could act on `e8` from the page it read
while looking at the page it did not.
A snapshot now records the address it was taken at, and a ref is refused once
`location.href` has moved.
That is a floor, not the contract, and the difference is worth stating because
it decides where the rest belongs. An address is not an identity: a route that
goes A → B → A comes back to a string that matches. Nor can this world learn
that it happened — patching `History.prototype` in an isolated world patches
that world's prototype while the page calls a different function object, the
same isolation that keeps `__codegAgent` out of the page's reach. The probe
now measures this rather than asserting it: it patches `pushState` in the
world, has the *page* navigate, and checks the patch never fired while the
address moved anyway.
So `snapshot({ epoch })` takes an opaque host token into the generation an
agent echoes back. The host is the only party that sees these transitions, so
deciding when refs die is the host's, and enforcing it is split: the host
refuses on the spot by comparing the epoch it embedded, this world refuses
from the next snapshot on, because until then it has learned nothing. The
comment says exactly that — an earlier draft claimed the world invalidates on
an epoch change alone, which it cannot.
Also from review, both smaller:
- The probe put `cursor:pointer` and `onclick` on one element, so it could not
show either mattered on its own — and "upstream already names these" is the
whole reason there is no promotion pass here. Three separate roleless divs
now, each asserted namable alone, plus that `[cursor=pointer]` is rendered
on the pointer one and not on the handler one.
- `truncate`'s comment and a test name said the first line is kept whole when
it alone exceeds the cap. The code cuts it, which is right — the cap is the
caller's bound — so the words changed, not the code.
- An empty `epoch` was read as no epoch. It is a value the host chose; only an
absent one is absent.
The snapshot engine has had no consumer since it landed, deliberately: the seam where it would be called is the seam where authorization lives, and a page-reading path that predates the grant model is the hole the rule exists to prevent. This adds the model, and the one read it allows. Nothing is granted automatically. Not by address class — a private address says nothing about whether the page behind it is signed in. Not by which process is listening — a `socat` in front of the real server owns the port just as convincingly. Not by an allow-list — a same-origin substitution is invisible to any re-check. A tab the agent opened itself is no different from one the user opened. Reading needs a grant as much as acting does: the pages worth protecting are the ones a tree would describe in the most detail. The grant lives on the tab rather than in a store of its own, so that the code which learns a tab changed origin is the code that revokes, under the lock it already holds. `revoke_if_departed` is called from both places that write `state.origin`. That is complete because the engine refuses a cross-origin `pushState`: the one class of navigation the host learns about late is the one class that cannot cross the boundary a grant is bound to. `agent_snapshot_core` checks the grant twice. The first check is a refusal before the page is touched at all. The second is the one that decides, because a read is not instantaneous — the user can revoke, the tab can be closed and reopened under the same id, the page can go anywhere — so the tree is handed over only if the grant as it stands now still covers the address the world reports having walked. Not the address the host last heard about, which is the one that can be stale. The bundle is evaluated into a document lazily, and only after the grant check passes, so a tab nobody has shared contains no page-reading code at all. It goes in wrapped in a function body: the shim evaluates an expression, and a hundred kilobytes of generated program is not one. Verified end to end against WKWebView through the smoke puppet: a read refused before sharing; a share bound to `http://127.0.0.1:8790`; a four-ref tree in 6 ms; the page's own `pushState` moving the host epoch and the world refusing the ref from before it; a same-origin navigation keeping the grant while the world's generation changes; `127.0.0.1` to `localhost` on the same server — a different origin — revoking it and refusing the next read; a reopened tab id starting with nothing. The epoch narrows the window on a route that leaves and returns; it does not close it. The host hears about same-document navigation only through the helper's poll. Closing it belongs to the world, which can watch `history.length` and `popstate`, and to the package that acts on refs, where a stale one costs a wrong click rather than a confusing tree.
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.
Links, dev servers and local HTML open in a codeg tab instead of leaving the app. Ten work packages (P1 + P2), three platforms, 65 commits.
Opened as a draft: the Linux half has never been run on a real machine, and neither have the most recent Windows fixes. Opening it mainly to put the branch through the three-platform matrix for the first time — see Verification below.
What a browser tab is
A native webview owned by Rust, not an iframe. Two surface kinds behind one
BrowserSurfaceenum, dispatched by aper_surface!macro:surface_child— a wry child webview embedded in the app window (macOS, Windows).surface_window— an owned top-leveltauri::WebviewWindow. The only surface on Linux, and available as a preference everywhere.The frontend paints a hole and holds an occlusion lease; the surface follows that rectangle and freezes when an overlay covers it. Tabs survive restart, idle ones unload.
The page channel
Each surface installs a script in an isolated world that talks back over one primitive,
__codegSend.channel.rsis the single enforcement point for every platform:helloandnav-stateare accepted only from the main frame, whilegestureandshortcutare deliberately accepted from subframes too, so a click inside an iframe still counts as a user gesture and ⌘F still works there.Three shims implement the same contract against three engines:
WKUserContentControllerworlds,WKWebsiteDataStoreper profile.CallDevToolsProtocolMethod:Runtime.addBinding{executionContextName}plusPage.addScriptToEvaluateOnNewDocument{worldName}, contexts keyed by(CDP session, id).Roughly what is in here
Per-tab profiles and proxy following · typed load errors with a real error page · downloads · find in page · site rules · a
codeg-doc:document guest for local HTML · a port bridge that puts dev servers into web-mode tabs · popup handling with gesture and opener rules · a settings section.Verification — read this part
cargo test --features test-utils3711 passed / 0 failed. Manual checklist run.6eafbb9band the shared fixes after it were written on macOS and have not been run there yet.Both remaining machines have a written checklist waiting.
Two caveats about what green CI would and would not mean:
--no-run. It proves the test binaries link, not that anything works. Windows conclusions come from the physical machine only.doc_open_core); that was found by hand, because the cell that would have caught it was never triggered.The Linux code was written against a local cross-compilation setup (
x86_64-unknown-linux-gnuwith stub.pcfiles,x86_64-pc-windows-gnuwith mingw). That checks compilation and clippy with fabricated pkg-config metadata — the ubuntu cell is the first time it meets real system headers.Boundaries worth a reviewer's eye
browser-*,browser-popup-*andcodeg-doc-*must never appear in the capability list. There is a guard for this.about:blankandblob:on http(s).Deliberately not here
Favicons (the field exists and stays
None) · download progress and cancel (wry gives noWKDownloadhandle) · a match counter on macOS find · background tab preloading · hard reload and a custom context menu · the document view on Linux (an owned window gets no per-webview custom protocol) · P3 (agents) and P4 (remote egress).🤖 Generated with Claude Code