Push notifications for bb on your iPhone — and any other device with a Web Push browser. When a thread finishes its turn, fails, or sits waiting on your input, your phone buzzes with a notification that deep-links straight back to the thread.
bb's web UI is already an installable PWA (manifest, standalone display, maskable icons) but ships no service worker, so the browser has nowhere to deliver a push. This plugin supplies one — plus everything around it: smart suppression while you're watching, an app icon badge, collapse keys so an offline phone doesn't get buzz-bombed, and an offline shell for a flaky tunnel.
bb plugin install git:https://github.com/vburojevic/bb-plugin-ios-notifications.git@main
bb push status # confirm it loadediOS only delivers Web Push to apps installed on the Home Screen — a Safari tab has no push API at all.
- Open your bb in Safari on the iPhone (for example through a
bb connecttunnel URL). - Tap Share → Add to Home Screen and confirm.
- Open bb from the new Home Screen icon — push only works inside the installed app.
- Go to Settings → Plugins → iOS Notifications and tap Enable on this device.
- Tap Send a test notification to confirm the loop works.
The same Enable button works in desktop Safari, Chrome, and Edge, and on Android (installed or in the browser). The bb desktop app is the one place Web Push cannot work — Chromium routes push through Google's FCM and Electron ships no connection to it; the settings panel explains this instead of offering a button that cannot work.
| Event | Notification |
|---|---|
| Thread finishes its turn | Thread title + the assistant's last message |
| Thread fails | "Failed — …" + the error, sent with high urgency |
| Thread waits on you | "Waiting for you — …" + the actual question or command needing approval, after a 20s grace period |
The needs-input push is the quiet star: a thread blocked on a permission
prompt or an AskUserQuestion looks exactly like a working thread from your
phone. If the prompt is still unanswered after 20 seconds, you get pushed —
answering at your desk within the grace period never buzzes.
On iOS the platform does this, not the plugin. A device whose browser supports
Declarative Web Push (Safari 18.4+) is sent a payload carrying
"web_push": 8030, and the system shows the notification and navigates to its
navigate URL when tapped — no notificationclick, no service worker. That is
the only mechanism that reliably works in an installed iOS web app, where
client.navigate() and clients.openWindow() are both ignored and every
worker-driven route is therefore a no-op.
Support is feature-detected by the worker and reported per device, never guessed
from a user agent, and the declarative payload needs an absolute URL — so each
device also reports which origin it reaches bb on. bb push status shows both:
57246457006ea18f iPhone (Home Screen) [declarative → https://your.tunnel] (last delivered …)
A device without that support keeps the plugin's own payload and the handoff below.
This is what makes a tap work when bb is backgrounded or fully closed: the platform launches or foregrounds the installed app on the notification's URL itself. Nothing in this plugin has to be running, and nothing it does is allowed to get in the way — the worker's push handler deliberately shows no notification for a declarative payload (showing one would replace the platform's proposal and take the tap back), and the content script's worker bookkeeping is detached from its mount so a cold launch never waits on a network round trip or a worker activation before bb renders.
A tap lands on the thread the notification was about, and the page is what
puts it there. Inside an installed iOS web app the service worker cannot
navigate anything: the system brings the app forward on whatever route it was
last showing, and client.navigate() and clients.openWindow() are ignored —
which is why taps used to reopen bb on the wrong thread.
So the worker records the tap and hands it over two ways. It messages any
running window, which routes itself in place (no reload) and acknowledges; and
it leaves the record behind for the page to find on launch and on every resume,
which is what covers a cold start and a suspended page whose message arrives
late. Both paths are idempotent, expire after a minute, and are claimed by
deleting the record, so with several windows open exactly one moves. Where the
worker can navigate — Android, desktop browsers — client.navigate() and
openWindow() remain as fallbacks for an app bundle too old to answer.
- Viewing suppression — every bb window (including the desktop app)
reports which thread it is on, instantly on navigation and focus changes.
While any window is visible on a thread, that thread's pushes are skipped
everywhere. Needs-input pushes are deferred rather than dropped, so a prompt
that outlives your attention still reaches your phone. Backgrounding the app
lifts suppression immediately (
keepalivereports survive page suspension). - Collapse keys — every thread push carries a Web Push
Topic, so a phone that was offline gets only the newest notification per thread instead of the whole backlog. - Min turn duration — optionally skip pushes for turns shorter than 30s–5m. Never filters failures or questions.
- Child threads — subagent/worker threads are skipped unless you opt in; hidden threads (workflow agents) are always skipped.
- Silent when watched on-device — if the receiving device itself has a visible window on the thread, the notification lands silently in the tray.
The app icon badge shows how many threads are currently waiting on your input — on installed PWAs (iOS 16.4+, Android), and on the bb desktop app, whose dock icon supports the same API even though push does not reach it.
The count follows the server, not the last notification. Every bb window already reports which thread it is on — on navigation, on focus changes and every 15s — and the reply to that report carries the count, so answering a prompt at your desk drops the badge on every other device that has a window open, and reading one of three waiting threads leaves two rather than zero. Tapping a notification does not clear it either: the thread you tapped is still waiting until you answer it.
Two limits worth knowing. While the installed app is fully closed, only an arriving push can change its badge — Web Push has no silent delivery, so there is nothing to send. And a badge is at most 15s stale while a window is open, which is the heartbeat of the report it rides on.
bb ships no service worker, so a dropped connection leaves an installed PWA on
the browser's blank error page. With offlineShell on (the default), the
worker caches the app shell and serves it only when a navigation fails, so
you get bb's own reconnecting UI instead. The handler is deliberately narrow:
navigations only, network-first, never assets or /api, and it refuses to
cache anything that isn't a plain same-origin 200 HTML document containing
bb's mount point — a tunnel's sign-in page can never become the shell.
| Setting | Default | Effect |
|---|---|---|
notifyOnIdle |
true |
Notify when a thread finishes its turn |
notifyOnFailure |
true |
Notify when a thread fails (ignores min turn duration) |
notifyOnNeedsInput |
true |
Notify when a thread waits >20s on a permission or question |
includeChildThreads |
false |
Include subagent/child threads |
minTurnDuration |
off |
Skip pushes for turns shorter than this — idle only |
suppressWhenViewing |
true |
Skip pushes for a thread some visible bb window is on |
openInMobilePanel |
true |
Cold taps open the thread in the Mobile plugin's phone view, when installed |
offlineShell |
true |
Serve the cached app shell when a page load fails |
contact |
https://getbb.app |
VAPID subject; push services require mailto: or https: |
Change them in Settings → Plugins → iOS Notifications or with
bb plugin config ios-notifications set <key> <value>. Settings are read per
event — no reload needed.
bb push status # devices, delivery state, settings, what's being viewed
bb push test [message] # send a test to every registered device
bb push remove <device-id> # unregister a device by id
Devices can also be removed from the settings panel.
An installed iOS app caches both halves of this plugin — the service worker and the app bundle — and can keep running an old pair long after the plugin itself is updated, which looks exactly like a fix that did not work. Two things make that visible instead of a guess.
On the device, Settings → Plugins → iOS Notifications states which bundle and which service worker are actually running. A worker from before 1.0.5 does not answer the version ping, and the panel says so along with the remedy: close bb completely and reopen it.
From the machine, bb push status prints the same thing per reporting window,
plus the last dozen notification taps:
plugin revision: 1.0.6
clients:
iPhone (Home Screen) app 1.0.6 worker 1.0.6 (thr_abc, visible)
Mac (browser) app 1.0.6 worker 1.0.6 (no thread, hidden)
taps:
06:41:02 tap /threads/thr_abc (1.0.6)
06:41:02 routed /threads/thr_abc (1.0.6)
A tap with no routed after it means the worker ran but no page finished the
navigation. No tap at all means the tap never reached the worker. Both are
answers; neither is visible without this.
- Service worker — served by the plugin backend at
/api/v1/plugins/ios-notifications/http/sw.jswithService-Worker-Allowed: /, which lets a script under/api/…claim the root scope the app runs in. Handlespush,notificationclick, badge updates, and the optional offline-shell fetch fallback. - VAPID keypair — generated once on first load, kept in the plugin's kv storage, never rotated (rotation would invalidate every subscription).
- Subscriptions — one kv row per device, keyed by
sha256(endpoint).slice(0, 16). A push service answering 404/410 removes the row; transient 429/5xx errors get one retry. - Needs-input detection — the realtime feed flags threads with a pending interaction; the plugin then reads the actual pending interaction (question prompt, command awaiting approval) to build the notification body.
- Viewing reports — a tiny content script posts
{clientId, threadId, visible}to an unauthenticated plugin route (so it also works through abb connecttunnel, whose origin fails local-auth checks — the tunnel fronts it with its own auth wall). Reports age out after 45s and the map is hard-capped, so the endpoint can't be grown unbounded.
git clone https://github.com/vburojevic/bb-plugin-ios-notifications
cd bb-plugin-ios-notifications
npm install
bb plugin types . # regenerate types/ from your bb's SDK
bb plugin build . # bundle server + app into dist/
bb plugin install . # install as a local path plugin
bb plugin dev . watches sources and hot-reloads the frontend bundle.
MIT