Ovvi

Browser

A browser pane: a web view with Web Inspector in a placeholder herdr pane, on This Mac and on remote machines

herdr cannot draw a web page. So a herdr pane holds the place in the layout, and the app draws a web view in its rectangle. The browser is a pane kind in the plugin registry, built into the app with a manifest written in Swift (The browser). What is not built yet is in Future work. Research: browser.md.

The label

A pane is a browser pane when its herdr label is ovvi:browser#<iid> <url>, for example ovvi:browser#m4p8sa http://localhost:3000/dashboard (Labels and instance ids). The app knows the pane from the label alone: a pane made by the app, by an agent or in herdr's TUI is the same.

  • iid (the instance id): exactly 6 characters of [a-z0-9], not used by another pane of the session. It names the pane's state file and never changes.
  • url: http:// or https://, at most 1024 characters, no control characters. A bare host:port gets http://. file:// opens only on This Mac; a remote machine serves its files through a server. A new pane has no url yet: ovvi:browser#<iid>.
  • The address is as the machine sees it: localhost means the pane's machine (Remote pages). herdr keeps it in its session file and shows it to every device, so an address must not hold a secret (for example a ?token= URL).
  • The label is the only pane field that has no length limit and survives moves and cold restarts (herdr 0.9.5). Metadata tokens are cut at 80 characters and lost on restart.
  • Rename Pane works on a browser pane. A name that breaks the label turns the card back into a terminal. Reset Pane Name is disabled.

Create

A browser pane comes from the chooser's Browser (New pane), Open Browser Here, or an agent (Agents open browser panes).

  1. The app picks a free iid and makes the pane in one call: layout.apply {workspace_id, focus:false, root:{type:"pane", cwd, label, command}} in a new tab, with the old pane's live folder, the label ovvi:browser#<iid> and the placeholder as its command (Create).
  2. pane.move puts it beside the old pane, and pane.close closes the old pane, as Replace Pane does. If the move fails, the new pane closes. The new pane stays hidden by its label in every tab until the old one is gone, so the temporary tab never shows.
  3. The card shows "Opening Browser…" until the snapshot that puts the new pane in the old one's place, so the old pane's shell never shows. Then it shows an empty address field that has the keyboard.
  4. The user types an address. Return loads it, approves its origin (When a page loads by itself), removes the saved state, and sets the label with pane.rename {label: "ovvi:browser#<iid> <url>"}.

In herdr's TUI

The placeholder, about 25 lines of sh that herdr runs as the pane's command, prints "Browser", the address without its scheme (example.com/docs, or /Users/me/report.html for a file: address; nothing for none) and "Shown in Ovvi" in the middle of the pane. It follows the page: when the label changes (a typed address, a link, an agent's ovvi open --replace), the app sends the placeholder SIGWINCH, and it reads its label from herdr (herdr pane get) and draws again. A resize draws it again the same way. Keys typed into the pane do nothing. The TUI's border shows the current label. After a cold restart herdr brings the pane back as a plain shell with its label, and the app types the placeholder into it again (The placeholder).

The card

The card sits in herdr's split like any card, for example beside the dev server's terminal.

  • Header. Two rows, as on an agent card (Pane card). Row 1 (36 pt): the page's icon (16 pt) and its title, else its host, else its address, else "New Tab". Then the card buttons, on the keyboard card and on a hovered card. Row 2 (32 pt) is the toolbar: Back, Forward and Reload (Stop while a page loads), 22 × 22 pt buttons with 11 pt glyphs, then the address field, which takes the free width, then the Web Inspector button (hammer). Back, Forward and Reload touch; 6 pt between them, the address field and the Web Inspector button. The toolbar's buttons have the card buttons' color and dim as any disabled card button when they cannot act. Row 2 ends where row 1 does, 8 pt in.

  • Swipes. A two-finger sideways swipe over the page goes back or forward, as in Safari (Swiping).

  • Page icon. WebKit has no favicon API. After each load, one script in the page reads its <link rel~=icon> (else /favicon.ico) and fetches it, at most 1 MiB in 5 s, and gives the image back in base64. The page's own fetch goes through its proxy and its loopback rule (Remote localhost), so CORS keeps a few cross-origin icons out. The globe shows while there is no icon, and over a failed load.

  • Address field. A capsule 26 pt tall, at least 120 pt wide, filled with the primary color at 0.06 under a 0.5 pt edge at 0.08, text 12.5 pt: the host and port in full, the rest in secondary. Empty, it says "Ask Google or type a URL". On a remote machine the machine's name sits in a chip at its end (11 pt, in the needs-you color on a 0.18 fill of it). A click on the chip switches the card to This Mac's network: the chip then says "This Mac" in secondary text on a 0.08 fill of the primary color (This Mac's network). Under the pointer the field shows the text cursor (the chip shows the arrow) and its fill goes to 0.1. A click or ⌘L selects the whole address under a 1.5 pt accent edge. Return loads it and gives the keyboard to the page. Text that is not an address goes to a Google search (https://www.google.com/search?q=…): text with a space, or text with no scheme (https://), no dot, and a host that is not localhost or an IPv6 address ([::1]). So localhost:3000 and example.com load, and swift actors, ovvi and devbox:3000 search. Esc puts back the current address and gives the keyboard to the page.

  • Empty card. Until its first page, the card shows a 44 pt globe in tertiary. The web view stays hidden until a page commits, because an empty web view draws white.

  • A click off the page. The hidden web view takes no click. So a click anywhere on the empty card, the loader, an error or "Open this page?" makes the card the keyboard card, as a click on a page does, also a click on its buttons. The page takes the keyboard, or the address field when the card has no address.

  • Loading. A 2 pt progress line in the accent runs under the header while a page loads.

  • Loader. Only while a card first opens or reads its saved state: the globe at 32 pt, and under it a spinner left of "Opening Browser…" in 13 pt secondary. It fades out over 0.15 s (at once with Reduce Motion). A typed address shows the progress line alone. A card made again after it was dropped (What stays in memory) shows its last picture in place of the loader, with no fade, until its page draws. A page draws once its document is parsed and painted (DOMContentLoaded and one frame), not when every image and script finished: one that never answers would keep the loader over the page.

  • Error. A page that fails shows, in the middle of the card, a warning symbol, the app's words in 15 pt semibold, a line in secondary text for a refused local port, and Reload:

    CaseText
    nothing listens on a localhost port"Nothing answers on localhost:3001" (on a remote machine "… on ‹machine›"), then "Is the dev server running?"
    a remote machine cannot reach a name"‹machine› cannot reach ‹host:port›"
    the remote session is not live"‹machine› is not connected"
    an unknown name"Cannot find ‹host›"
    the proxy did not start"Ovvi could not start the proxy to ‹machine›"
    anything elseWebKit's own text

    The address field keeps the address that failed. WebKit goes back to the last page's address, but the field does not follow it. Reload loads the failed address again.

  • The page is inset 6 pt from the card's sides and bottom, with a 6 pt radius, and starts 6 pt under the toolbar, so Back has 11 pt under it. Cards are never glass.

  • Resize. While the card's size changes (a divider or a window drag, the sidebar), the page keeps its size, clipped, or with the card's fill at its new edge. 150 ms after the last change, or when a window drag ends, the page takes the new size: one page layout per drag, as a terminal keeps its grid (States). A split's motion gives the page its new size once, as the motion starts. A page laid out at every frame makes the split's motion stutter.

  • The tab's title is "Browser", then the page's title (else its host) in secondary text, as a terminal tab's "zsh ovvi" ("Browser | Hacker News" where a title is plain text: Tab bar). Its program icon shows globe. Its overview tile shows the page's last picture (Overview).

  • The window's PaneViewRegistry keeps each web view by terminal id, so a card scrolled away or moved keeps its page, up to the budget in What stays in memory.

  • A floated browser pane (Floating pane) keeps the toolbar under the floating window's title row, 4 pt down from the card's top edge. The title row shows "Browser" and the page's title in secondary text, with no place.

Pictures and switches

A kept tab's page (Tab page) keeps its web views in the window, hidden, and a switch to it shows them. A tab whose page was not kept puts its kept web views back into the window as they are: recorded at 60 fps, WebKit draws no blank frame on such a switch, so nothing covers the page. Each web card (a browser's or a plugin's) keeps a picture of its page, for its overview tile and for a card made again:

  • Taken as the card leaves view, and for the cards in view as the overview opens (takeSnapshot, about 8 ms in WebKit's process, also out of a window). Every picture is taken at 1x: 6 MB for a full-window card on a Retina screen, not 24. A tile is scaled down; a dropped card made again shows the picture until its page loaded.
  • The cap. One cap over every picture in the app (web cards' and chats'): 200 MB, the least recently taken out first. A card with no picture shows its loader. Pictures are not purgeable: macOS takes volatile surfaces back within seconds.
  • A web card is made as its tab first shows, not ahead: a switch to a tab whose browser is made then takes 62 ms to its first frame, and one made ahead 61 ms.
  • When a new host adopts a web view, the view keeps its size: a view set to the host's zero size lays the page out again and draws it blank.

What stays in memory

  • The web cards of the shown tab and of every floating window stay, plus the 25 most recently shown other web cards, browsers and plugins together, over all windows (spareWebCards, D46). Each kept card costs about 28 MB, mostly its WebKit process, and no CPU at rest.
  • An older one is dropped: its web view and its process go, and its last picture stays for its tile and its next card. When it shows again, the card is made from its label, under that picture. A browser comes back with its saved history (State on the machine). A plugin card comes back at the place in its label, with its state from its state file (State), and with no Back and Forward.
  • A floated card, a plugin card with unsaved edits and a browser with a popup open (a sign-in window) are never dropped.
  • On critical memory pressure from macOS (DispatchSource.makeMemoryPressureSource), the kept cards out of view go down to half the bound, and so do the kept terminals. A warning does nothing: macOS can send one every 30 minutes while memory is free, and a dropped card costs its next show 100–200 ms.
  • The two warm web views (Other rules) cost about 10 MB each, their page processes with no page.
  • spareWebCards and spareTerminals (defaults, or -spareWebCards N at launch) set the two bounds.
  • New tabs. ⌘-click on a link, a link with target=_blank, and Open Link in New Tab in the page's context menu open the link in a new browser pane, in a new herdr tab right after the card's tab. The new tab stays in the background, as in Safari. The menu's Open Image, Open Frame and Open Video in New Tab do the same. These items replace WebKit's "in New Window" items.
    • The user picked the link, so its origin is approved (When a page loads by itself) and the new card loads it with no question.
    • Only http, https and file links open a tab. A target=_blank form and a blob: or data: page load in the card.
    • WebKit does not tell rel=opener apart, so such a link opens a tab too.
  • To the side. ⌥-click on a link (WebKit's download gesture) and Open Link to the Side, under Open Link in New Tab, open the link in a new browser pane beside the card, in the background, its origin approved. Links open as everywhere in the app (Opening links and files).
  • The page's context menu has no download items: the app saves no downloads.
  • A page's window.open (an OAuth login, for example) opens over the card, 24 pt in, on the floating glass, with its host and a ×. It gets the keyboard. The popup keeps its opener. window.close(), the × and Esc close it, Esc also while the popup's own page has the keyboard. The page gets the keyboard back if the popup had it.
  • A mailto: or app link that the user clicks opens its app. The page stays.
  • A video's full-screen button and a page's requestFullscreen (a site's own player) show it full screen in a space of its own. Esc or the player's button brings it back to the card.
  • Page alerts and confirms show as a sheet on the window, titled with the origin. A file input opens the system's open panel as a sheet.

Web Inspector

The Web Inspector button or ⌥⌘I (Safari's key) shows WebKit's inspector, docked at the bottom of the card. Its own control moves it to a window.

  • Showing it has no public API: the app calls WebKit's _WKInspector show (cmux ships the same). WebKit keeps the last docked or window choice in the app's defaults (__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached), so the app removes that key before each show, and it docks every time.
  • Every browser card sets isInspectable, so Safari's Develop menu (Develop › ‹this Mac› › Ovvi) reaches each page too, and the page's menu (a right-click) has Inspect Element. Plugin cards have neither: a right-click on empty space in their page shows only Reload.
  • The button and ⌥⌘I are disabled until the card has a page.

State on the machine

The page and its back and forward list are saved on the pane's machine, in the instance state file (State): $HOME/.local/state/ovvi/<session>/browser/<iid>.json.

  • It holds {"url": …, "history": …}. url is as the machine sees it. history is WebKit's interactionState (the back and forward list) in base64, left out at 128 KiB and over.
  • Written 500 ms after a navigation ends or the address changes (a single-page app's route). Last writer wins. Nothing is written after the pane closed.
  • Read when the card is built: a relaunch, a session restart, another device. The card opens the saved page with its history, else the label's address. Both follow the load rule below, and so does the history's current page: one from another origin that is not approved stops and asks.
  • Typing an address removes the file. The device that closes the pane removes it, and the 30-day cleanup on connect takes the rest.
  • An open card follows its label. When the label gets another address from elsewhere (an agent's rename), the card loads it as a new entry of its history, by the load rule. It ignores the address that its own rename replaced, so an older snapshot does not undo a typed address.
  • Simpler choice: the scroll position is not saved.

When a page loads by itself

Any process on a machine (a prompt-injected agent, a script) can rename a pane or write its state file. So:

  • A page the user types loads at once and approves its origin for that machine; so do Open and a link opened in a new tab. A page that only finishes loading approves nothing: a link the user clicks loads in the card, but after a relaunch the card asks before it opens a saved page from an origin not approved.
  • A page from the machine (the label, the state file, a rename) loads by itself only if its origin is approved for that machine or on the safe list. Otherwise the card asks Open this page?, under a globe, with the address (host in full, path dimmed, cut in the middle) and two buttons: Not Now and Open. Return opens and Esc is Not Now while the card has the keyboard. Open approves the origin. Not Now leaves the page the card had, or the empty card.
  • The safe list: the machine's own localhost, 127.0.0.1, [::1] and *.localhost on any port, This Mac's file:// pages, and github.com, gitlab.com, bitbucket.org, developer.apple.com, developer.mozilla.org, docs.github.com, vercel.com, *.vercel.app and *.netlify.app. On This Mac's network the loopback names are This Mac's, so they ask like any other origin.
  • Approvals are per device and per machine, the newest 200 web origins, in the app's defaults (browserApprovals.<machine id>, Data). Removing a machine removes them. Settings ▸ Privacy lists, adds and removes them (Privacy).

Profiles

Each machine has its own WKWebsiteDataStore(forIdentifier:), with the machine's id as its identifier (This Mac has a fixed one).

  • The store keeps cookies, local storage, IndexedDB, the cache and service workers on disk, under ~/Library/WebKit/<bundle id>/WebsiteDataStore/<id>/, across relaunches. Logins persist, and machines stay apart.
  • Stores are per device: another Mac has its own logins.
  • Removing a machine removes its store (WKWebsiteDataStore.remove(forIdentifier:)). If a window still uses the store, it stays on disk.
  • WebKit uses no keychain item for cookies or storage, so no keychain prompt appears.

Remote localhost

On a remote machine, localhost:3000 means that machine, and private names resolve as the machine resolves them. Every page of the machine's cards goes through one SOCKS proxy on the Mac, carried by the machine's existing SSH connection. Nothing is installed on the machine.

  • The front. A SOCKS5 listener of the app's own (HerdrKit's SOCKSFront) on a free loopback port is the store's proxy. It passes each connection to ssh's own SOCKS forward on the machine's ControlMaster (The SSH connection):
    ssh -F /dev/null -S <ctl> -O forward -D 127.0.0.1:<P> <target>
    ssh's -D closes a connection it cannot open with no SOCKS reply, and WebKit then waits 60 s. The front answers "refused" at once instead.
  • <machine>.localhost. WebKit never sends localhost, 127.0.0.1 or [::1] through a proxy. So a remote card loads http://<machine>.localhost:3000 where the machine sees http://localhost:3000, and the front asks the machine for localhost wherever a page asked for <anything>.localhost. The machine needs no *.localhost resolver. The address field and the label show localhost, with the machine's chip.
  • <machine> is the machine's name in lowercase words joined by dashes ("machine" when none is left). It is not made unique: each machine has its own store and proxy, so a clash mixes nothing. After a rename, pages keep working, but their origin changes and their logins go.
  • This Mac's loopback is never reached from a remote card. Loads and a restored back and forward list wait for the proxy. A main-frame link, redirect or form post to localhost is sent as <machine>.localhost. A content rule blocks every other loopback request: images, fetches, websockets, frames and popups, and spellings such as 127.1, localhost. and [::ffff:7f…]. So a remote page whose script calls http://localhost:8000 is blocked: serve that API on the page's own origin.
  • Public sites in a remote card also load through the machine.
  • The proxy starts with the machine's first browser card and ends with its last (ssh -O cancel). The -D ends with its master, and the next page connection forwards again on the new master. When nothing answers on the -D port, the front asks for a new forward once and tries again. A front whose listener ends by itself listens again on the same port, every 2 s until it can. A card whose proxy did not start tries again on Reload. So a page that failed loads again on Reload, without closing the window.
  • No page waits for ever. ssh answers a page's connection once the machine opened it. With no answer in 5 s, the app sends a command over the relay. If that command gets no answer in 3 s (longer when the relay must start), the master or its link hangs: a stopped master, or a link left half open by a sleep. The app ends the master if it still answers -O exit, starts a new one, and tries the page once on it. If the relay answers, the machine is only slow, and the page waits 30 s more. A page that still gets no answer shows its error and Reload. A connection the front already carries (a dev server's live-reload websocket) lives on after the last card until WebKit drops it. Two machines with one SSH target share one proxy.
  • The forward uses no SSH session. It needs AllowTcpForwarding yes on the server (the default). A server that refuses shows as "cannot reach".
  • This Mac's store uses no proxy.

This Mac's network

The machine chip in a remote card's address field is a switch. A click loads the card through This Mac's network, and a second click loads it through the machine again.

  • On This Mac's network the card uses This Mac's store, with no proxy. So localhost is This Mac's, and logins are This Mac's. The chip says "This Mac", plain.
  • Each switch makes a new web view, because a store's network cannot change. The card loads the address in its field again. Its back and forward list starts again, because its entries belong to the other network.
  • file:// pages stay closed to a remote card on either network.
  • A page that the machine puts in the label or the state file loads by itself only from an approved origin. This Mac's localhost is not safe then. A switch approves the address it loads.
  • A link opened in a new tab keeps the card's network.
  • A new card starts on the network that Settings ▸ Plugins ▸ Browser names: "Web pages on remote machines load through" The Machine (the default) or This Mac. The setting is one value for every remote machine, kept in settings.json as "plugins": {"browser": {"network": "mac"}} (Plugin settings in settings.json). It changes no open card: the chip switches a card.
  • The choice is kept per card on this Mac, through relaunches: in the app's defaults, under browserViaMac.<machine id>, a list of <session>/<iid>, the newest 200. Another Mac has its own choice. Removing a machine removes its list.

Commands on a browser card

KeysCommandMenu
⌘RReload Page (enabled once there is a page or an error, not while the card asks)View
⌘LOpen Location: the address field, all selectedEdit
⌥⌘IShow Web InspectorEdit
⌘+ ⌘− ⌘0zoom the page, 50 to 300 % in 10 % steps, while the card has the keyboard (in a floating window, its own card's); ⌘0 also undoes a pinchView
⇧⌘RReplace Pane: a fresh shell with the New pane chooser, with no questionFile
  • A trackpad pinch magnifies the page around the fingers, and a two-finger double tap zooms to the block under them and back, as in Safari (allowsMagnification). The page keeps its layout: it grows as a picture does, and scrolls inside the card; ⌘+ and ⌘− reflow it instead. Smart zoom needs the trackpad's Smart zoom setting. Overview tiles show pictures, so a pinch over one does nothing. Plugin cards and window.open popups do not magnify.
  • Open Browser Here is hidden. Open Terminal Here and Start Agent Here replace the pane with no question while it runs only its placeholder or a plain shell, and closing it asks nothing then either. Any other program asks, as in any pane: any process can label a pane.
  • Find (⌘F), Insert Path, Go to Directory and Reset Pane Name are disabled.
  • A floating window offers Reload Page, Open Location and Show Web Inspector too.

Agents open browser panes

Any agent in a herdr pane opens a browser card next to itself, on any machine, with the ovvi command (CLI):

ovvi open browser http://localhost:3000             # beside the agent's pane
ovvi open browser http://localhost:3000 --tab       # in a new tab, with a notice that offers Show
ovvi open browser http://localhost:3000/x --replace w1:p7    # the card at w1:p7 goes to a new address
ovvi open browser ./report.html                     # a file, on This Mac only
  • Ovvi makes the pane as Open Browser Here does, with the label ovvi:browser#<iid> <url> and its placeholder, and answers with the pane, the label and whether the card shows.
  • --replace renames the card's label with its own iid. The open card loads the new address as a new history entry, by the load rule, and Back returns. Ovvi removes the card's saved state first, so a card made later loads the new address, not the saved one.
  • A card with the same address already in the session is the answer, unless --new.
  • An address from an agent that is not approved or safe waits for Open (When a page loads by itself).

The label is how it works underneath: any process that labels a pane by hand gets the same card.

  1. herdr pane split "$HERDR_PANE_ID" --direction right --no-focus makes a pane beside its own.
  2. herdr pane rename <pane> "ovvi:browser#<iid> <url>" makes it a browser pane, with a fresh iid.
  3. herdr pane report-metadata <pane> --source <name> --token ovvi-label=1 --ttl-ms 1000 on that pane. herdr sends no event for a rename, so this token's pane.updated event makes the app read the snapshot at once. Without it the card shows at the next event or the 30 s health check.
  4. The app types the placeholder into the labeled pane's shell (The placeholder).

Security

  • The page gets no bridge: no window.ovvi and no script message handlers, so a page cannot run commands.
  • A page that a machine puts in a label or state file loads only by the rules in When a page loads by itself.
  • Plugin pages get the ovvi object, under the rules in Security and trust.
  • A file:// page can show any file that the user can read, as in Safari: a page's image, video or script can come from another folder (../clips/intro.mp4), and a link to a page in another folder opens in the card. The page's own rules stay WebKit's: a file page is an origin of its own, so its scripts cannot fetch or read other files. Plugin pages never load files this way: they come through the ovvi-plugin: scheme.

Other rules

  • The web view's user agent ends with Version/‹macOS major.minor› Safari/605.1.15 (applicationNameForUserAgent), as Safari's does, so sites that sniff for Safari work.
  • When the system ends the page's process (webViewWebContentProcessDidTerminate), the card loads the page again by itself, once per load, and WebKit brings back its scroll. A page whose process ends again before it loaded shows "The page stopped working" with Reload, so the card never stays blank, also in a card out of view. A load counts until it finished: a page whose load never ends (an image that never answers) shows the failure on its next ended process.
  • WebKit runs helper processes for web cards: one GPU process and one network process, which all cards share, and one page process for each card. It starts the network process only after the first page process runs. On a busy Mac, the first start of these processes can take many seconds, and a new web view's page process takes 0.1 s to a few seconds. So the app keeps two warm web views out of any window: one for This Mac's browser cards and one for plugin cards. Each runs one script with no page, so its page process starts (and with it the GPU and network processes), and it keeps no history. A new card takes the warm view of its kind, and the app starts the next one. So a card that opens beside another (a ⌥-click on a link) draws its page with no wait for a page process. Both start 3 s after launch, after the restored cards. A remote machine's browser card that loads through the machine makes its own web view, with its own page process. The app never waits for these processes: a card keeps its loader or its empty look until its page draws.
  • When WebKit's GPU process stops answering (a busy Mac), WebKit gives up on it and starts another, but it cannot end the old one, and every page keeps drawing through it: each web card, browser and plugin, stays blank, also after Reload. The app ends that process within a few seconds. The pages connect to the new one and draw again where they were, with no reload; a card out of view does so when it shows. WebKit has no public API for its GPU process: the app reads its process id with _gpuProcessIdentifier.
  • Plain http needs NSAllowsArbitraryLoadsInWebContent and NSAllowsLocalNetworking in Info.plist.

Risks

RiskEffectPlan
Labels or state files written by a hostile machinea page opens by itselfan origin outside the approvals and the safe list needs a click, also for the saved history's current page
A hostile machine serves a page on its own localhost (safe list)that page loads by itself, and its links, redirects and requests go anywhereaccepted: the rule stops a label or state file alone from opening an unapproved origin, not a page the machine serves; a link it leads to approves nothing
The SOCKS port is open to other accounts on the Macanother account can reach the machine's networkaccepted for a single-user Mac; a random port, open only while a card needs it
A server with AllowTcpForwarding noremote pages cannot loadthe card says "cannot reach"; Tailscale or LAN addresses still load
_WKInspector is privatethe Web Inspector button can break on a macOS updateSafari's Develop menu still reaches every browser card
Web view memorymemory grows with many browser cardsat most 25 web cards kept out of view, half on critical memory pressure (What stays in memory)

Choices

  • WKWebView with Web Inspector. No size cost, the same engine as plugin cards and iOS, and a proxy per store. Considered: Chromium through CEF, for Chrome DevTools. It adds 330 MB to the app, uses 195–281 MB for the first page, needs an engine update about every 4 weeks, and its shared keychain item holds every request behind a password prompt. Rejected: real Chrome driven over its DevTools protocol; a Chromium build of our own.
  • A built-in pane kind with one store per machine, and the load rule above. A label or state file alone never opens an origin that is not approved or safe, and machines stay apart. A page that loads (the machine's own localhost among them) can still lead anywhere: see Risks. Rejected: loading every label by itself; tracking which device created or edited a label.
  • Remote pages through one SOCKS proxy per machine, with the app's front before ssh's -D. One forward covers every port and name on the machine, refusals come at once, and nothing is installed there. Rejected: a forward per port (ports clash between machines and with the Mac, and the origin changes); a server helper of our own.
  • A file page can show the whole disk, its scripts stay in their origin. A page and its media often sit in sibling folders, and Safari loads them. WebKit already gives read access to the whole disk to a file page opened from a link or the history. Rejected: only the page's folder (a sibling's media is blocked); file access for scripts (allowFileAccessFromFileURLs), which lets any downloaded page read the user's files.
  • The typed address in the label, the current one in the state file. Coming back to a browser card, on any device or after a restart, shows the page where the user left it. Rejected: rewriting the label on every navigation (a rename per click, seen by every TUI); state kept per device in the app.

On this page