Inspiration

Logitech MX devices are great for Premiere, Lightroom - apps that stay put. But my actual day is 15 browser tabs: GitHub, Notion, Gmail, Figma. The device had no idea which site I was on. Same layout whether I was reviewing a pull request or triaging email. I'd glance at it, remember it wasn't going to help, and go back to the keyboard. It became furniture. The hardware wasn't the problem - there was just no bridge between what the device knows (which app is focused) and what I actually care about (which website I'm on).

What it does

When you switch browser tabs, your device layout switches with it. Open GitHub, it shows PR actions. Switch to Gmail - archive, compose, snooze. You can also record workflows: a sequence of clicks and inputs you do repeatedly, replayed from a single button press. Everything runs locally. Nothing leaves your machine.

How we built it

Five components across three languages: a C# plugin that watches a signal file and switches profiles when the active domain changes, a Chrome/Brave/Edge extension (Manifest V3) that tracks tabs and handles all in-browser interaction, a Swift native agent running as a launchd daemon that owns the SQLite database and bridges browser ↔ plugin over XPC, a connector binary translating Chrome's native messaging stdio protocol into XPC calls, and a SwiftUI app for managing everything.

Challenges we ran into

SPA compatibility in the content script was a real fight. Dispatching a standard click does nothing in React or Vue. We access native property descriptors directly to set input values and fire the full pointer + mouse event chain for clicks. YouTube validates isTrusted on keyboard events, which means synthetic keyboard events from a content script are silently ignored. We moved that handling into the AddaSwift agent directly - it bypasses the browser event system entirely and controls playback at a level YouTube can't filter.

Accomplishments that we're proud of

Tab change in Chrome to profile switch on the physical device in under a second, all local, no server. The workflow recorder working transparently on React-heavy SPAs - all the compatibility complexity invisible to the user.

What we learned

A few things stuck with us. PluginDynamicCommand with parameterized actions is exactly the right primitive for dynamic web content - one class, runtime-registered actions, arbitrary parameters. We kept reaching for more complex patterns and kept finding the SDK already handled it.

On the browser side: Chrome's service worker lifecycle is more aggressive than the docs imply. The native messaging connection drops silently when the worker suspends, with no error surfaced to the extension. You only find out when the next message fails. The heartbeat alarm was a late addition, and it fixed a class of bugs that had been intermittent and hard to reproduce.

Building across three languages with hard IPC boundaries forced every interface to be explicit - there's no shared memory, no implicit coupling. That turned out to make debugging easier, not harder. When something broke, the boundary it broke at told you immediately which layer to look at.

What's next for AddaSwift

WebMCP feels like the right long-term direction - sites declare their available actions as structured tools, and AddaSwift picks them up automatically with no configuration. But it's still early. The protocol is in preview, only a handful of sites support it today, and access is largely limited to development environments. We've built toward it, but for now keyboard shortcuts and DOM mapping carry the weight. WebMCP is the bet on where the web is going.

Share this project:

Updates