Simple Icons publishes each brand's own hex next to its glyph, and the
build was throwing it away. Tiles now carry it - Gmail red, Drive blue,
Chat green, Gemini violet - and the glyph is drawn black or white by the
tile's perceived brightness, since brand colours are picked to look right
rather than to carry a white mark. GitHub and Notion are near-black,
Snapchat is pure yellow, and a fixed white glyph loses one end of that.
An app's right corners are now rounded on its own layer. The container's
rounded-xl could never have clipped them: an app is a native view sitting
on top, not something the shell lays out, so it kept square corners and
overhung the curve. Only the right pair - the left edge butts against the
nav, and rounding it would notch the middle of the window.
The traffic lights move to (26, 24) and the drag strip deepens to match,
so they sit inside the window's margin instead of against its corner.
Adds Gemini, Claude, ChatGPT, Linear, ClickUp, HubSpot, Shopify and
Cloudflare to the host table.
The favicon service was wrong as often as it was right: a sign-in page's
icon for anything behind a login, nothing at all for a private host, and
both answers cached past any way of asking again. Refreshing could not
fix it, because the staleness was not local. Now every mark is.
Each app is its brand glyph in white on a round tile, coloured from
Tailwind's 500s by hashing the host - you find things by their colour, so
one that moved every launch would be worse than none. A host with no
glyph gets its initial in the same tile.
Matching tries the registrable name first, since a self-hosted tool is
nearly always on a subdomain of its vendor - aputure.odoo.com is Odoo,
not Aputure. A short table covers what a domain cannot answer, which is
most of Google.
The build reduces Simple Icons' 15MB of SVGs to one 4.5MB map in public/,
fetched once rather than parsed into the bundle at every start; the
bundle stays at 233KB. It is generated on every build, so never committed
and never stale.
Traffic lights are offset to sit inside the window margin rather than
crowding its edge.
Each rise in an app's unread count while you are elsewhere adds to a
tally beside its name, and a dot on its icon in the rail. Looking at the
app is the only thing that clears it.
The window has no frame of its own, so the shell now keeps a 6px margin
in a normal window - the only part of it that is not a web page, and so
the only place left to grab. Full screen gives the room back.
Dialogs get the app blurred behind them. An app's webview paints above
the shell, so it has to be moved aside before a dialog can be seen at
all, and once moved there is nothing left to blur - a still taken on the
way out is the only way to keep the background there. It runs on a
blocking worker: its completion handler is on the main thread, and
waiting there deadlocks until the timeout and returns nothing.
Settings is much larger, the cog moved to the foot of the nav, group
names read as names rather than shouted headings, and "Refresh icons"
bumps a version every favicon URL carries, for the ones that cache wrong.
The report was that a backgrounded site dies slowly and stops fetching.
It did, for three separate reasons, and the first two fixes each traded
one failure for another:
Hiding inactive views lets WebKit suspend them, so nothing is hidden any
more - every view keeps its size and stays in the window, and the active
one is ordered on top.
Telling a background page it was hidden then made Gmail throttle its own
syncing; its unread count sat unchanged for two and a half minutes. That
spoof is gone.
Neither was enough on its own: a background page keeps its timers but
loses the connection its updates arrive on, and WKWebView has no
equivalent of Electron's backgroundThrottling. Background apps are now
poked every 45s with the events a page uses to catch up after you return
to a tab - it fetches without losing a half-written reply.
Notifications now come from the unread count in the title rather than
the site's notification code, which Gmail will not run while it believes
you are looking at it. Measured: Gmail behind Odoo went 155 -> 157
untouched and raised "Gmail / 1 new".
Also adds a background-app probe to Settings, which is what turned this
from guesswork into measurement, and drops the nav's blocked-element
count and the hide-element button.
Three separate faults, all mine:
set_application was never called, so mac-notification-sys looked up an
app named "use_default", failed, and posted every notification as
com.apple.Finder - wearing Finder's alert style rather than this app's.
NSUserNotificationCenter suppresses the banner whenever the posting app
is frontmost unless the delegate implements shouldPresentNotification:.
The crate's delegate implements only delivery and activation, so the
method is added to its class at runtime. Without it, Gmail notifying
while you sit in Odoo - same window, still frontmost - is never seen,
which is the case the whole design exists for.
send_notification only waits for a response when the options ask it to.
Passing None returned instantly with NotificationResponse::None, so the
click branch was unreachable. Waiting parks a thread and a notification
left unread never resolves, so waiters are capped at 32.
The diagnostic now records the raise before blocking, since with
wait_for_click a notification sitting on screen otherwise read as
"none yet".
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.
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.
Pairing decrypts a Chromium browser's cookie store (PBKDF2-HMAC-SHA1
against its Keychain key, then AES-128-CBC) and injects the result into
WKHTTPCookieStore. Only the configured apps' hosts and their sign-in
hosts survive the filter. Browsers are offered most-recently-used first,
since the first entry becomes the default and someone with four
Chromium browsers installed wants the one they actually browse in.
WKWebView defines window.Notification but it does nothing: constructing
one throws no error and shows no banner, so a page believes it notified
you. Measured on the machine as `api=function shim=no` before the shim
was made unconditional; `from page: Odoo - Test notification -> raised`
after.
Anything on a page can be right-clicked away. The rule is re-asserted on
every navigation, because the injected script only carries a snapshot
from when the view was built and a selector added since would otherwise
come back on reload.
The top bar is gone. Navigation lives beside the cog, the nav carries
the traffic lights, and two-finger swipe goes back and forward.
The seed is now the real app list, scoped to exact hosts so a Drive link
inside Gmail switches rather than being swallowed.
INPUT carried w-full, which lost the specificity coin-toss against the
group select's w-[110px] and starved the name and URL fields to stubs.
Width now belongs to the call site.
Settings lists apps in the nav's order rather than by position alone,
so the two never disagree.
scripts/ship.sh builds, replaces /Applications/Work.app and relaunches.
A Tauri 2 shell with one child webview per configured tool. Nav on the left
with groups and a collapsible icon rail; links between configured apps switch
tabs, everything else leaves for the real browser.
Design spec in docs/superpowers/specs/2026-09-01-work-app-design.md.