The cookie import is gone. It worked mechanically - 43 cookies decrypted from Arc and verifiably visible to the page - but Google, Microsoft and Odoo all refused the imported sessions, because each binds a session to the browser that created it. Signing in once inside the app is simpler and actually works, so the whole path is deleted rather than kept as a feature that mostly fails. That takes rusqlite, aes, cbc, pbkdf2, hmac, sha1 and sha2 out of the build with it. Notifications are now raised through mac-notification-sys rather than Tauri's notification plugin, because the plugin cannot report that one was clicked. A click switches to the app that raised it and then runs the page's own click handler - the only thing that knows which message the notification was about. Zoom is per app, on a fixed ladder so Cmd+0 returns to exactly 100%. The shortcuts are menu-bar accelerators rather than a key listener, since the keystroke has to work while a remote page has focus. The hidden-element count is off the nav rows.
119 lines
5.3 KiB
Markdown
119 lines
5.3 KiB
Markdown
# 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 and it sticks. Every app also claims a real Chrome user agent by default, because Google
|
|
refuses logins from anything it identifies as an embedded webview.
|
|
|
|
|
|
**Notifications work, and clicking one lands on the message.** 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.
|
|
|
|
Clicking the banner switches to that app and then runs **the page's own click handler**,
|
|
which is the only thing that knows which message it was about — Gmail opens the thread,
|
|
Chat opens the conversation. This is why notifications are raised directly through
|
|
`mac-notification-sys` rather than Tauri's notification plugin: the plugin has no way to
|
|
report that a notification was clicked.
|
|
|
|
Service-worker push in the background is not covered — only notifications a page raises
|
|
while it is open.
|
|
|
|
**Zoom is per app.** ⌘+ and ⌘− step a ladder that always returns to exactly 100% with ⌘0,
|
|
and each app remembers its own size. The shortcuts are menu-bar accelerators rather than a
|
|
key listener, because they have to work while a remote page has focus.
|
|
|
|
**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
|
|
|
|
```bash
|
|
npm install && npm run tauri dev
|
|
```
|
|
|
|
## Shipping it
|
|
|
|
Build, replace the copy in `/Applications`, and relaunch — one command:
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.
|