Vincent f057268103 Prove cookie injection lands, rather than assuming it
setCookie is fire-and-forget, so the import's count was what WebKit was
handed, not what it kept. A probe from inside the page reads back the
other end: pairing against Arc gave `names=tz,cids,frontend_lang` for
aputure.odoo.com, and `tz` exists only in Arc's store — so the import
demonstrably landed.

The sites still ask for sign-in. That is the far end refusing the
session, not a broken import, and the two are now distinguishable
instead of being guessed at.
2026-09-01 12:21:41 +02:00

Work

A desktop browser for one thing only: the web tools you work in. A left nav lists them, clicking one shows it, and links between them navigate inside the app. Everything else opens in your real browser.

Tauri 2 · Rust · React 19 · Tailwind CSS 4 · macOS

There is no address bar, no tab strip, and no way to reach a site that is not on the list. That is the point.

How it works

Every app is its own webview. Not an iframe — Google, Microsoft and most SaaS send X-Frame-Options: DENY, so an iframe-based version of this app cannot exist. Each tool gets a real child webview (Window::add_child), and they all stay loaded, so switching keeps your scroll position, your half-typed draft, and anything counting down.

The shell measures, Rust positions. A child webview is a native view that takes no part in CSS layout. The React shell leaves an empty <div id="stage">, measures it with a ResizeObserver, and reports the rect; Rust sizes the active webview to it and hides the rest. It also means a native view paints over anything the shell draws, which is why opening Settings hides the stage first.

Links are routed by intent, not by URL alone. An injected script catches genuine user clicks and target=_blank, and only those. A URL outside the app's own hosts is handed to Rust over a made-up workapp-route: scheme — deliberately not Tauri IPC, which would mean granting google.com the ability to call into this app. Rust then decides:

Target What happens
The current app's hosts Nothing — ordinary navigation
A known identity provider Stays inside, so SSO can complete
Another app on your list Switches to that app and navigates it
Anything else Opens in your default browser

Redirects are never blocked. on_navigation returns true for everything that is not the sentinel, because a strict navigation filter breaks every OAuth chain the moment it bounces through accounts.google.com.

Sessions persist. Each webview keeps its cookies across restarts, so you log into a tool once. Every app also claims a real Chrome user agent by default, because Google refuses logins from anything it identifies as an embedded webview.

Pairing imports a browser's cookies. Settings lists the browsers installed, most recently used first, and pairing decrypts that browser's cookie store — PBKDF2-HMAC-SHA1 against its Keychain key, then AES-128-CBC — and injects the result into WebKit's shared jar. Only cookies for the apps on your list and their sign-in hosts are read; everything else is dropped before anything is written. macOS asks for Keychain permission the first time, which is the consent gate and cannot be skipped.

Google and Microsoft increasingly tie a session to the browser that created it, so those may still ask you to sign in once. After that the persistent jar keeps it.

Notifications work, with one gap. WKWebView defines window.Notification but it is inert: constructing one throws nothing and shows nothing, so a page believes it notified you and you never hear about it. It is replaced with a shim that forwards to a real macOS notification carrying the app's name. Service-worker push in the background is not covered — only notifications a page raises while it is open.

Anything on a page can be hidden. Right-click it and choose Hide this element, or use the eye button in the nav to point at one (arrow-up widens the selection to the parent, Escape cancels). Selectors are stored per app, listed in Settings, editable, and reversible. The rule is re-asserted on every navigation and re-added if a single-page app rewrites <head>, and cached in the page's own storage so a reload hides it before the first paint rather than after it has flashed on screen.

Apps and groups

An app owns the exact host of its URL — mail.google.com, not google.com — or Gmail and Drive would each swallow the other's links. Where two scopes match, the longest wins. Extra hosts can be added per app.

Groups are for the nav only. Deleting one keeps its apps, ungrouped: deleting a folder should never be a way to lose the things inside it.

The nav collapses to a 72px icon rail that is still clickable, so switching apps never requires expanding it first. It is that wide because the macOS traffic lights have to fit inside it: there is no title bar and no toolbar, so the nav carries the drag strip.

Back, forward, reload and hide-an-element sit next to the cog at the top of the nav. There is no toolbar — two-finger swipe goes back and forward.

Running it

npm install && npm run tauri dev

Shipping it

Build, replace the copy in /Applications, and relaunch — one command:

npm run ship

No disk image; nothing here is being distributed.

Configuration

apps.json, under ~/Library/Application Support/com.vincent.workapp/. Hand-editing it is supported — missing fields fall back to their defaults, and an app with no scope gets its URL's host.

Tests

cd src-tauri && cargo test

Routing and config are pure and carry real tests: scope matching and its precedence, identity-provider passthrough, the routing decisions, sentinel round-trips, and config round-trips. Webview orchestration and bounds sync have no seam a unit test can reach and are verified by running the app.

S
Description
No description provided
Readme
1,001 KiB
Languages
Rust 51.9%
TypeScript 31%
JavaScript 12.7%
HTML 2.6%
CSS 0.9%
Other 0.9%