The webview saves the file perfectly well and mentions it to nobody,
which makes a download indistinguishable from a click that did nothing.
Each one is now announced in the corner as it saves, and offers to show
the finished file in the Finder. The folder is settable and defaults to
the system's Downloads.
Two gaps in the API shape this. There is no progress - DownloadEvent
reports a start and a finish and nothing between - so the destination
file is polled as it grows and the bytes written are shown; the indicator
spins rather than fills, because there is no total to divide by. And on
macOS the finish always reports no path at all, so the destination
assigned at request time is remembered against the URL and read back at
the end.
A name already taken gets "(2)" appended. Silently overwriting is the
last thing anyone wants from a download they were not told about.
The window's position and size survive a restart, and so does the app
you were last in.
Saving a password was only half of it - a saved password you cannot get
back out is not saved in any useful sense. Right-click now offers "Fill
saved password" wherever a page has a password field. Only on an explicit
click, only for the host on screen, and only into that page's own fields,
never on load. The value is set through the native setter and followed by
input and change events, because assigning .value is invisible to React
and Angular and the site would submit an empty field.
The title bar is painted the nav's colour rather than its own grey, so
the two read as one surface in either theme. Doing that revealed that the
window's content had always run the full height underneath it - the
opaque bar was hiding it - so the shell is now inset by the measured
title bar height, or the nav lands on the traffic lights.
The rail's unread badge is a fixed circle inside the button's bounds.
Padding around a number made a pill at two digits, and hanging it off the
corner put it under the rail's clip; past nine it reads "9+", with the
figure itself in the expanded nav.
Version pinned to 0.0.1 and staying on 0.x while this is in beta.
`npm run ship` now exports twice from one build: /Applications/Work.app
to test here, and ~/Desktop/Work-<version>.zip to send someone. One
build, so a tester runs byte for byte what was verified rather than a
second build that drifted. Packed with ditto rather than zip - a plain
zip mangles the bundle's symlinks and it will not open on the far end.
Settings: groups move up under apps, and the hidden-element list moves
into its own window behind a one-line summary. It had grown to 21
selectors of framework class names and was most of the page, in front of
the things people actually open Settings for.
Reset puts apps, groups, hidden elements and zoom back to defaults, and
leaves sessions alone - those live in WebKit's own store, and throwing
them away would mean signing back into every tool to undo a change to
the nav.
Submitting a form containing a password now offers to remember it.
It goes into the macOS Keychain, through the Security framework rather
than the `security` binary - a password passed as a command-line argument
is visible in `ps` to anyone on the machine, however briefly. Never
apps.json, never a log.
The value travels as little as it can: the injected script hands it
straight to Rust, which holds it in memory and tells the shell only which
host and which username, since that is all the shell needs to ask the
question. It is written on Save and dropped on anything else.
Autofill is deliberately not built. Reading a password back out and
injecting it into a page is a materially larger surface than offering to
store one, and deserves its own decision.
Right-click now offers "Empty cache and reload". Caches only - never
cookies or local storage: emptying a cache is what you do when a page is
serving something stale, and signing you out while doing it would be a
different and much less welcome feature. The reload waits for the removal
to finish, or it would refill from what was being thrown away.
Google Chat could play its own alert sound and still reach none of this,
because some apps never construct a Notification - they hand it to their
service worker registration instead. ServiceWorkerRegistration.prototype
.showNotification now routes to the same place. A worker calling it from
its own context is still out of reach; a page calling it is not.
Also shims the badge API, which is the precise form of an unread count -
a number an app states outright rather than one scraped from its title.
WKWebView does not implement it, so an app calling it was talking to
nobody. Chat's title carries no count at all, which is why no counter
ever appeared for it.
The background-apps probe now reports all three signals per app, so which
one an app actually uses is a reading rather than a guess.
A child webview is positioned against the window frame while the shell
measures the hole it left from inside the content view. Borderless, those
origins coincided and it never showed. With a title bar back they are a
title bar apart, so every app was drawn that much too high - painting
over the right-hand part of the bar, which read as the bar being tinted
by whichever site was open, and leaving a strip of the same height along
the bottom.
Tauri cannot report that height. Both inner_position/outer_position and
inner_size/outer_size come back identical on macOS - measured, both
differences were zero against a window whose content is plainly a title
bar shorter than its frame. NSWindow.contentLayoutRect knows, so it is
asked once and cached.
Also hides the window title, and centres the navigation row in its own
strip rather than letting it crowd the bar above it.
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 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.
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.
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.