Adom Bridge (macOS)
Public Made by Adomby adom
Adom Bridge for macOS: the menu-bar daemon that connects AI tools to your Mac.
name: adom-desktop-notify
description: >-
Send native macOS Notification Center notifications from a cloud AI or a bridge via Adom Bridge's notify_user verb, and GET THE USER'S ANSWER BACK. Two separate axes: (1) get the reply to you (wait:true blocks and returns their choice, or poll notify_events/notify_response; there is no server-push to a turn-based AI). (2) reveal a window on their screen with focus, a local desktop action, including focus:{host:true} to reveal Adom Hydrogen when Bridge is embedded, and focus:{app} for any app. Use to alert the user (build done, export ready), point them at a window that needs them, or ask for a decision and read it back. Covers the notification types, the exact verb calls, how the AI polls or waits for responses, and what a click actually does.
Notifications: notify_user (macOS Notification Center)
notify_user fires a native macOS notification under Adom Bridge's own
identity (UNUserNotificationCenter via the notification plugin, with an
osascript fallback if the plugin is unavailable). It lands as a banner and in
Notification Center like any first-class app.
Reachable three ways, all identical shape:
- CLI / cloud:
adom-desktop notify_user '{"title":"...","body":"..."}' - Direct API (sibling app):
POST /command {"app":"desktop","command":"notify_user","args":{...}} - From a bridge: the same
notify_userverb via the relay.
Returns { "status":"ok", "action":"displayed", "id":"...", "clickBehavior":"...", "_hint":"...", "_next":[...] }
plus _couldHaveDoneBetter and _capabilities when the call left something on
the table. Read those; they are how you learn what this verb can do without
reading this file.
TWO axes: do not confuse them
A notification can do two separate things. Almost all confusion comes from mixing them up:
| Axis | Field(s) | What it does | Reply to you? |
|---|---|---|---|
| 1: GET THE USER'S ANSWER BACK | wait:true (block-and-return), or poll notify_events / notify_response |
delivers the user's click/choice to you, the AI | YES, this is the one you usually want |
| 2: REVEAL A WINDOW | focus:{...} |
raises a window on the user's SCREEN (Hydrogen, a browser, a dialog) | NO, pure local desktop action |
Axis 1: how you get the answer back (there is NO server-push; you poll or you wait)
You are turn-based; Bridge cannot inject an event into your conversation. So there are exactly two ways, fastest first:
# FAST PATH: wait:true. The call BLOCKS and returns the user's choice inline.
adom-desktop notify_user '{"title":"Ship v2 to prod?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}],"wait":true,"timeoutSec":120}'
# -> { "pending": false, "action": "Ship it" }
# FIRE-AND-CONTINUE: do not block; drain events on a later turn.
adom-desktop notify_user '{"id":"deploy-42","title":"Ship v2?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}]}'
adom-desktop notify_events '{"sinceSeq":0}' # every click since a cursor (recommended: no per-id polling)
adom-desktop notify_response '{"id":"deploy-42"}' # or poll one id
The click is recorded durably (a cross-process file), so both paths work even when the click is handled by a transient process. Details: sections 5 / 5b below.
Axis 2: focus (reveal a window) is NOT a reply
focus is covered next. It raises a window on screen; it does not tell you
anything by itself. A notification can do BOTH at once (a reminder with
buttons plus focus:{host:true} reveals Hydrogen on click AND reports the
click back via wait / notify_events).
Start here: make the click DO something (focus)
Most notifications are about something else on screen: a browser waiting on a login, a dialog, an app that needs a click. If you send a bare notification, clicking it opens the Adom Bridge window, which is almost never what the user wanted. They clicked because they wanted to get to the thing.
Pass focus and one click takes them straight there:
adom-desktop notify_user '{
"title": "Sign-in needed",
"body": "Google wants you to confirm before I can continue.",
"focus": { "titleContains": "Sign in" }
}'
Ways to name the window, in order of preference:
| selector | when | note |
|---|---|---|
{"hwnd": 459604} |
best: you already have the handle | exact. On macOS this is the CGWindowList window id; get it from desktop_list_windows, or from whatever verb opened the window. |
{"cacheKey": "my-session-id"} |
you drove the window through a verb that cached it under a key | survives title churn, which a title match does not. |
{"app": "adom-hydrogen"} |
you know the owning APP, not the exact window | process/app name match, case-insensitive. Survives PID churn (a process that restarts keeps its name), so it beats a stale window id for a long-lived app that respawns. |
{"host": true} |
the notification is a prompt/approval Bridge is showing on behalf of Adom Hydrogen | "the app Bridge is embedded under." Bridge resolves Hydrogen's window itself; you do not need its id or title. A click reveals Hydrogen, not Bridge. Falls back to opening Bridge when standalone. |
{"titleContains": "Sign in"} |
you only know roughly what it says | substring, case-insensitive. Breaks the moment the title changes, and page titles change constantly. |
Bridge resolves the target when you SEND and tells you in the response whether the click will land:
"clickBehavior": "focus-window",
"focus": { "resolved": true, "hwnd": 1313916, "title": "Sign in", "resolvedBy": "title" }
resolved:false means no matching window exists right now. The notification
still shows, and Bridge re-resolves at click time using the selectors you
passed, so this is fine if the window has not been created yet, and a bug you
should fix if it should already exist. If it still cannot find the window on
click, it falls back to opening Adom Bridge rather than doing nothing.
Rule of thumb: if your notification mentions a window, a browser, a dialog,
a tab, or asks the user to go click something, it should carry focus.
Fields
| field | type | notes |
|---|---|---|
title |
string | bold top line. Optional (missing/null renders as empty). |
body |
string | message text (use \n for a second line). Optional. |
level |
string | info (default), success, warning, error, emergency. macOS owns the notification chrome; level is carried for logging/urgency and does not restyle the banner. |
durationLong |
bool | request a longer display duration. macOS banners are transient by default; how long a banner stays is ultimately a macOS setting (banner vs alert style). |
scenario |
string | reminder / alarm / incomingCall mark the notification as needing acknowledgement. On macOS a banner still auto-dismisses unless the user has set Adom Bridge's notification style to Alerts; pair with buttons and wait/notify_events so you learn the outcome either way. |
buttons |
array | [{ "label": "...", "arguments": "<optional uri>" }], action buttons. Omit arguments and Bridge synthesizes a round-trip URI so a click is reported via notify_response. Buttons only render if the user has ALLOWED notifications for Adom Bridge (System Settings > Notifications). |
actions |
array | back-compat: ["Label1","Label2"], simple label-only buttons (same round-trip as buttons without arguments). |
progress |
object | { "title":"...", "value":0.0-1.0, "status":"...", "valueOverride":"6 / 10" }. Accepted cross-platform; macOS Notification Center has no progress-bar chrome, so treat this as status text on macOS. |
inputs |
array | [{ "id":"reply", "type":"text", "placeholder":"..." }]. Display only; see Limitations. For a real choice round-trip use buttons. |
id |
string | correlation id. Supply it so you can poll notify_response {id} for the clicked button. Auto-generated if omitted (and returned to you as id, so you can still poll it). |
focus |
object | {hwnd} | {cacheKey} | {titleContains} | {app} | {host:true}: the window this notification is ABOUT. A body click brings THAT window to the front instead of opening Bridge. See the section above. |
wait |
bool | block the verb until the user clicks (or timeoutSec elapses) and return their choice, instead of polling notify_response. Needs buttons (or a focus click) to be meaningful. |
timeoutSec |
number | how long wait blocks, 1-600. Default 60. |
The types: copy/paste
1. Basic (transient banner)
adom-desktop notify_user '{"title":"Export done","body":"gerbers.zip is ready."}'
2. Level (carried in the log/urgency)
adom-desktop notify_user '{"title":"DRC failed","body":"3 clearance violations.","level":"error"}'
3. Acknowledge-me reminder (pair with buttons and read the answer back)
adom-desktop notify_user '{"title":"Review needed","body":"The board is ready for your sign-off.","scenario":"reminder","buttons":[{"label":"Acknowledge"}]}'
4. Progress updates: re-fire with the SAME id as the job advances.
adom-desktop notify_user '{"id":"export-1","title":"Exporting gerbers","progress":{"title":"InstaPCB","value":0.6,"status":"Working...","valueOverride":"6 / 10 layers"}}'
5. Buttons that round-trip: learn which one the user clicked
# Fire with a correlation id you choose:
adom-desktop notify_user '{"id":"approve-42","title":"Deploy v2?","body":"Ship to prod?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}]}'
# Then poll until the user acts (pending:true until they click):
adom-desktop notify_response '{"id":"approve-42"}'
# pending -> { "pending": true, "action": null }
# clicked -> { "pending": false, "action": "Ship it" }
Poll every 1-2s; give up after a sensible timeout (the user may dismiss it).
The click is recorded durably (a cross-process file), so notify_response
works even when the activation is handled by a transient process, not just
when Bridge's window is open.
5b. Get told without per-id polling: notify_events
# One call returns EVERY click since your cursor (the relay has no true
# server-push, so this is the "tell me immediately" path):
adom-desktop notify_events '{"sinceSeq":0}'
# -> { "events": [ { "event":"notify_clicked", "id":"approve-42", "action":"Ship it", "at":"..." } ],
# "nextSeq": 1, "count": 1 }
# Pass the returned nextSeq as sinceSeq next time to get only new clicks.
5c. Retract a delivered notification: notify_dismiss
# Clear a notification from Notification Center once you have handled it,
# by the same correlation id you fired it with (best-effort):
adom-desktop notify_dismiss '{"id":"approve-42"}' # -> { "dismissed": true }
6. Explicit button URI: drive an arbitrary Bridge action (advanced)
# A button can carry any adom-desktop:// URI; clicking dispatches it.
adom-desktop notify_user '{"title":"...","buttons":[{"label":"Approve 1 hr","arguments":"adom-desktop://shell-approve?dur=3600"}]}'
7. Point the user at the window that needs them (the most useful type)
# The whole notification is "go look at this", so make the click go there.
HWND=$(adom-desktop desktop_list_windows | jq '.windows[]|select(.title|test("Sign in"))|.hwnd')
adom-desktop notify_user '{"title":"Sign-in needed","body":"Confirm in the browser and I will continue.","focus":{"hwnd":'"$HWND"'}}'
Add scenario:"reminder" plus a button if you also need to know they dealt
with it. A focus click is itself recorded as an acknowledgement
(action:"focus"), so notify_response / notify_events tell you they
engaged even with no buttons at all.
Click behaviour: what actually happens
Clicking the notification body or any button dispatches Bridge's
adom-desktop: URI protocol through the single-instance handler.
What a body click does depends on you:
| you sent | body click does |
|---|---|
focus: {...} |
brings that window to the front. Falls back to opening Bridge if the window is gone. Also records action:"focus" under your id. |
| nothing | opens the Adom Bridge window. Rarely what the user wanted. |
A button click does what its arguments URI says, defaulting to a round-trip
that reports the label back through notify_response. Buttons do not
foreground Bridge; a notification exists to reach someone WITHOUT interrupting
them, so acknowledging one must not yank them out of what they were typing.
The exceptions are deliberate: a body click (the conventional "show me the
app" gesture) and a shell-approval grant (you should see what you just
authorized).
What a click can and cannot be wired to
focus-window is exposed as a click action; arbitrary verbs are not, and will
not be. The reason is that you write both the button label and its URI, so a
button could always be mislabelled. Only actions whose worst case is tolerable
under ANY label belong on a notification: raising a window is non-destructive,
immediately visible, and undone by one Cmd-Tab. Anything that executes,
writes, or grants is not; that is what the permission model is for.
The response teaches you
Every notify_user response carries what a click will do and what to do next:
{
"status": "ok", "action": "displayed", "id": "t-42",
"clickBehavior": "focus-window",
"focus": { "resolved": true, "hwnd": 1313916, "title": "...", "resolvedBy": "hwnd" },
"_hint": "Shown. Clicking the notification brings \"...\" to the front ...",
"_next": [ "notify_response {\"id\":\"t-42\"}", "notify_events {\"sinceSeq\":N}", "desktop_list_windows" ]
}
When the call left something on the table you also get
_couldHaveDoneBetter (specific, e.g. "no focus was passed, so clicking
this just opens Adom Bridge") and _capabilities (the full menu of what
notify_user can do). These appear ONLY when there is something to fix, so a
well-formed call gets a short response.
Current coaching triggers:
- no
focus(with an extra nudge when the wording sounds like it is asking the user to go do something) focusthat resolves to no window right now- buttons without
scenario:"reminder"(a transient banner the user may never see) wait:truewith nothing to click
Limitations (macOS Notification Center rules, not Bridge bugs)
- Notifications must be allowed. The first notification triggers the macOS permission prompt; if the user declined, nothing shows. Fix in System Settings > Notifications > Adom Bridge.
- Banners are transient by default. macOS decides banner-vs-alert style
per app; only the Alerts style keeps a notification on screen until acted
on. A
scenario:"reminder"cannot force persistence past the user's banner-style setting, and Focus / Do Not Disturb can suppress banners entirely (they still land in Notification Center). - Buttons require notification permission and appear on hover/expand of the banner, per macOS convention.
- Typed
inputsare display-only. For a real choice round-trip, use buttons (each choice is a button) and readnotify_response. - A notification cannot truly block. For a hard, must-click-to-proceed prompt, that is a real dialog window (the Bridge approval dialog), not a notification.
Troubleshooting: "I got status:ok but saw no notification"
notify_user returning status:ok means only that Bridge accepted the
request and dispatched the notification; it does not confirm it displayed. If
nothing appeared:
- Check Notification Center (click the clock in the menu bar). Focus / Do Not Disturb routes banners straight there. That is a macOS setting, not a Bridge bug; the user can allow Adom Bridge notifications during Focus in System Settings > Focus.
- Check notification permission: System Settings > Notifications > Adom Bridge must be allowed.
- Check you are actually reaching that Bridge. From a cloud container,
notify_usertraverses the relay the Bridge is connected to; a--target <name>aimed at a relay that is not running silently fails, so confirm withadom-desktop targetsfirst. (The direct API does carry notify; a bridge or sibling app on the same machine canPOST /command {"command":"notify_user",...}with no relay.) - When you must be SURE the human saw it, do not rely on the notification alone; also surface the message in your chat/response.
When to use which
- Awareness ("export done", "build green"): basic, or
level:"success". - "Go look at this" (a login, a dialog, an app waiting on them):
focus, so the click lands on that window. This is the single highest-value field in the verb and the one most often forgotten. - Need the user to choose:
buttonspluswait:true(or pollnotify_response/notify_events). - Long-running job: re-fire with the same
idas status advances. - Genuine emergency:
level:"emergency", and consider also summoning a window viafocusso it cannot be missed.
---
name: adom-desktop-notify
description: >-
Send native macOS Notification Center notifications from a cloud AI or a bridge via Adom Bridge's notify_user verb, and GET THE USER'S ANSWER BACK. Two separate axes: (1) get the reply to you (wait:true blocks and returns their choice, or poll notify_events/notify_response; there is no server-push to a turn-based AI). (2) reveal a window on their screen with `focus`, a local desktop action, including focus:{host:true} to reveal Adom Hydrogen when Bridge is embedded, and focus:{app} for any app. Use to alert the user (build done, export ready), point them at a window that needs them, or ask for a decision and read it back. Covers the notification types, the exact verb calls, how the AI polls or waits for responses, and what a click actually does.
---
# Notifications: `notify_user` (macOS Notification Center)
`notify_user` fires a native macOS notification under Adom Bridge's own
identity (UNUserNotificationCenter via the notification plugin, with an
osascript fallback if the plugin is unavailable). It lands as a banner and in
Notification Center like any first-class app.
Reachable three ways, all identical shape:
- **CLI / cloud:** `adom-desktop notify_user '{"title":"...","body":"..."}'`
- **Direct API (sibling app):** `POST /command {"app":"desktop","command":"notify_user","args":{...}}`
- **From a bridge:** the same `notify_user` verb via the relay.
Returns `{ "status":"ok", "action":"displayed", "id":"...", "clickBehavior":"...", "_hint":"...", "_next":[...] }`
plus `_couldHaveDoneBetter` and `_capabilities` when the call left something on
the table. Read those; they are how you learn what this verb can do without
reading this file.
## TWO axes: do not confuse them
A notification can do two separate things. Almost all confusion comes from
mixing them up:
| Axis | Field(s) | What it does | Reply to you? |
|---|---|---|---|
| 1: GET THE USER'S ANSWER BACK | `wait:true` (block-and-return), or poll `notify_events` / `notify_response` | delivers the user's click/choice to you, the AI | YES, this is the one you usually want |
| 2: REVEAL A WINDOW | `focus:{...}` | raises a window on the user's SCREEN (Hydrogen, a browser, a dialog) | NO, pure local desktop action |
### Axis 1: how you get the answer back (there is NO server-push; you poll or you wait)
You are turn-based; Bridge cannot inject an event into your conversation. So
there are exactly two ways, fastest first:
```bash
# FAST PATH: wait:true. The call BLOCKS and returns the user's choice inline.
adom-desktop notify_user '{"title":"Ship v2 to prod?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}],"wait":true,"timeoutSec":120}'
# -> { "pending": false, "action": "Ship it" }
# FIRE-AND-CONTINUE: do not block; drain events on a later turn.
adom-desktop notify_user '{"id":"deploy-42","title":"Ship v2?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}]}'
adom-desktop notify_events '{"sinceSeq":0}' # every click since a cursor (recommended: no per-id polling)
adom-desktop notify_response '{"id":"deploy-42"}' # or poll one id
```
The click is recorded durably (a cross-process file), so both paths work even
when the click is handled by a transient process. Details: sections 5 / 5b
below.
### Axis 2: `focus` (reveal a window) is NOT a reply
`focus` is covered next. It raises a window on screen; it does not tell you
anything by itself. A notification can do BOTH at once (a reminder with
buttons plus `focus:{host:true}` reveals Hydrogen on click AND reports the
click back via `wait` / `notify_events`).
## Start here: make the click DO something (`focus`)
Most notifications are about something else on screen: a browser waiting on a
login, a dialog, an app that needs a click. If you send a bare notification,
clicking it opens the Adom Bridge window, which is almost never what the user
wanted. They clicked because they wanted to get to the thing.
Pass `focus` and one click takes them straight there:
```bash
adom-desktop notify_user '{
"title": "Sign-in needed",
"body": "Google wants you to confirm before I can continue.",
"focus": { "titleContains": "Sign in" }
}'
```
Ways to name the window, in order of preference:
| selector | when | note |
|---|---|---|
| `{"hwnd": 459604}` | best: you already have the handle | exact. On macOS this is the CGWindowList window id; get it from `desktop_list_windows`, or from whatever verb opened the window. |
| `{"cacheKey": "my-session-id"}` | you drove the window through a verb that cached it under a key | survives title churn, which a title match does not. |
| `{"app": "adom-hydrogen"}` | you know the owning APP, not the exact window | process/app name match, case-insensitive. Survives PID churn (a process that restarts keeps its name), so it beats a stale window id for a long-lived app that respawns. |
| `{"host": true}` | the notification is a prompt/approval Bridge is showing on behalf of Adom Hydrogen | "the app Bridge is embedded under." Bridge resolves Hydrogen's window itself; you do not need its id or title. A click reveals Hydrogen, not Bridge. Falls back to opening Bridge when standalone. |
| `{"titleContains": "Sign in"}` | you only know roughly what it says | substring, case-insensitive. Breaks the moment the title changes, and page titles change constantly. |
Bridge resolves the target when you SEND and tells you in the response whether
the click will land:
```json
"clickBehavior": "focus-window",
"focus": { "resolved": true, "hwnd": 1313916, "title": "Sign in", "resolvedBy": "title" }
```
`resolved:false` means no matching window exists right now. The notification
still shows, and Bridge re-resolves at click time using the selectors you
passed, so this is fine if the window has not been created yet, and a bug you
should fix if it should already exist. If it still cannot find the window on
click, it falls back to opening Adom Bridge rather than doing nothing.
**Rule of thumb: if your notification mentions a window, a browser, a dialog,
a tab, or asks the user to go click something, it should carry `focus`.**
## Fields
| field | type | notes |
|---|---|---|
| `title` | string | bold top line. Optional (missing/null renders as empty). |
| `body` | string | message text (use `\n` for a second line). Optional. |
| `level` | string | `info` (default), `success`, `warning`, `error`, `emergency`. macOS owns the notification chrome; `level` is carried for logging/urgency and does not restyle the banner. |
| `durationLong` | bool | request a longer display duration. macOS banners are transient by default; how long a banner stays is ultimately a macOS setting (banner vs alert style). |
| `scenario` | string | `reminder` / `alarm` / `incomingCall` mark the notification as needing acknowledgement. On macOS a banner still auto-dismisses unless the user has set Adom Bridge's notification style to Alerts; pair with buttons and `wait`/`notify_events` so you learn the outcome either way. |
| `buttons` | array | `[{ "label": "...", "arguments": "<optional uri>" }]`, action buttons. Omit `arguments` and Bridge synthesizes a round-trip URI so a click is reported via `notify_response`. Buttons only render if the user has ALLOWED notifications for Adom Bridge (System Settings > Notifications). |
| `actions` | array | back-compat: `["Label1","Label2"]`, simple label-only buttons (same round-trip as `buttons` without `arguments`). |
| `progress` | object | `{ "title":"...", "value":0.0-1.0, "status":"...", "valueOverride":"6 / 10" }`. Accepted cross-platform; macOS Notification Center has no progress-bar chrome, so treat this as status text on macOS. |
| `inputs` | array | `[{ "id":"reply", "type":"text", "placeholder":"..." }]`. Display only; see Limitations. For a real choice round-trip use `buttons`. |
| `id` | string | correlation id. Supply it so you can poll `notify_response {id}` for the clicked button. Auto-generated if omitted (and returned to you as `id`, so you can still poll it). |
| `focus` | object | `{hwnd}` \| `{cacheKey}` \| `{titleContains}` \| `{app}` \| `{host:true}`: the window this notification is ABOUT. A body click brings THAT window to the front instead of opening Bridge. See the section above. |
| `wait` | bool | block the verb until the user clicks (or `timeoutSec` elapses) and return their choice, instead of polling `notify_response`. Needs buttons (or a `focus` click) to be meaningful. |
| `timeoutSec` | number | how long `wait` blocks, 1-600. Default 60. |
## The types: copy/paste
**1. Basic** (transient banner)
```bash
adom-desktop notify_user '{"title":"Export done","body":"gerbers.zip is ready."}'
```
**2. Level** (carried in the log/urgency)
```bash
adom-desktop notify_user '{"title":"DRC failed","body":"3 clearance violations.","level":"error"}'
```
**3. Acknowledge-me reminder** (pair with buttons and read the answer back)
```bash
adom-desktop notify_user '{"title":"Review needed","body":"The board is ready for your sign-off.","scenario":"reminder","buttons":[{"label":"Acknowledge"}]}'
```
**4. Progress updates**: re-fire with the SAME `id` as the job advances.
```bash
adom-desktop notify_user '{"id":"export-1","title":"Exporting gerbers","progress":{"title":"InstaPCB","value":0.6,"status":"Working...","valueOverride":"6 / 10 layers"}}'
```
**5. Buttons that round-trip**: learn which one the user clicked
```bash
# Fire with a correlation id you choose:
adom-desktop notify_user '{"id":"approve-42","title":"Deploy v2?","body":"Ship to prod?","scenario":"reminder","buttons":[{"label":"Ship it"},{"label":"Hold"}]}'
# Then poll until the user acts (pending:true until they click):
adom-desktop notify_response '{"id":"approve-42"}'
# pending -> { "pending": true, "action": null }
# clicked -> { "pending": false, "action": "Ship it" }
```
Poll every 1-2s; give up after a sensible timeout (the user may dismiss it).
The click is recorded durably (a cross-process file), so `notify_response`
works even when the activation is handled by a transient process, not just
when Bridge's window is open.
**5b. Get told without per-id polling: `notify_events`**
```bash
# One call returns EVERY click since your cursor (the relay has no true
# server-push, so this is the "tell me immediately" path):
adom-desktop notify_events '{"sinceSeq":0}'
# -> { "events": [ { "event":"notify_clicked", "id":"approve-42", "action":"Ship it", "at":"..." } ],
# "nextSeq": 1, "count": 1 }
# Pass the returned nextSeq as sinceSeq next time to get only new clicks.
```
**5c. Retract a delivered notification: `notify_dismiss`**
```bash
# Clear a notification from Notification Center once you have handled it,
# by the same correlation id you fired it with (best-effort):
adom-desktop notify_dismiss '{"id":"approve-42"}' # -> { "dismissed": true }
```
**6. Explicit button URI**: drive an arbitrary Bridge action (advanced)
```bash
# A button can carry any adom-desktop:// URI; clicking dispatches it.
adom-desktop notify_user '{"title":"...","buttons":[{"label":"Approve 1 hr","arguments":"adom-desktop://shell-approve?dur=3600"}]}'
```
**7. Point the user at the window that needs them** (the most useful type)
```bash
# The whole notification is "go look at this", so make the click go there.
HWND=$(adom-desktop desktop_list_windows | jq '.windows[]|select(.title|test("Sign in"))|.hwnd')
adom-desktop notify_user '{"title":"Sign-in needed","body":"Confirm in the browser and I will continue.","focus":{"hwnd":'"$HWND"'}}'
```
Add `scenario:"reminder"` plus a button if you also need to know they dealt
with it. A focus click is itself recorded as an acknowledgement
(`action:"focus"`), so `notify_response` / `notify_events` tell you they
engaged even with no buttons at all.
## Click behaviour: what actually happens
Clicking the notification body or any button dispatches Bridge's
`adom-desktop:` URI protocol through the single-instance handler.
What a body click does depends on you:
| you sent | body click does |
|---|---|
| `focus: {...}` | brings that window to the front. Falls back to opening Bridge if the window is gone. Also records `action:"focus"` under your `id`. |
| nothing | opens the Adom Bridge window. Rarely what the user wanted. |
A button click does what its `arguments` URI says, defaulting to a round-trip
that reports the label back through `notify_response`. Buttons do not
foreground Bridge; a notification exists to reach someone WITHOUT interrupting
them, so acknowledging one must not yank them out of what they were typing.
The exceptions are deliberate: a body click (the conventional "show me the
app" gesture) and a shell-approval grant (you should see what you just
authorized).
### What a click can and cannot be wired to
`focus-window` is exposed as a click action; arbitrary verbs are not, and will
not be. The reason is that you write both the button label and its URI, so a
button could always be mislabelled. Only actions whose worst case is tolerable
under ANY label belong on a notification: raising a window is non-destructive,
immediately visible, and undone by one Cmd-Tab. Anything that executes,
writes, or grants is not; that is what the permission model is for.
## The response teaches you
Every `notify_user` response carries what a click will do and what to do next:
```json
{
"status": "ok", "action": "displayed", "id": "t-42",
"clickBehavior": "focus-window",
"focus": { "resolved": true, "hwnd": 1313916, "title": "...", "resolvedBy": "hwnd" },
"_hint": "Shown. Clicking the notification brings \"...\" to the front ...",
"_next": [ "notify_response {\"id\":\"t-42\"}", "notify_events {\"sinceSeq\":N}", "desktop_list_windows" ]
}
```
When the call left something on the table you also get
`_couldHaveDoneBetter` (specific, e.g. "no `focus` was passed, so clicking
this just opens Adom Bridge") and `_capabilities` (the full menu of what
`notify_user` can do). These appear ONLY when there is something to fix, so a
well-formed call gets a short response.
Current coaching triggers:
- no `focus` (with an extra nudge when the wording sounds like it is asking
the user to go do something)
- `focus` that resolves to no window right now
- buttons without `scenario:"reminder"` (a transient banner the user may
never see)
- `wait:true` with nothing to click
## Limitations (macOS Notification Center rules, not Bridge bugs)
- **Notifications must be allowed.** The first notification triggers the
macOS permission prompt; if the user declined, nothing shows. Fix in
System Settings > Notifications > Adom Bridge.
- **Banners are transient by default.** macOS decides banner-vs-alert style
per app; only the Alerts style keeps a notification on screen until acted
on. A `scenario:"reminder"` cannot force persistence past the user's
banner-style setting, and Focus / Do Not Disturb can suppress banners
entirely (they still land in Notification Center).
- **Buttons require notification permission** and appear on hover/expand of
the banner, per macOS convention.
- **Typed `inputs` are display-only.** For a real choice round-trip, use
buttons (each choice is a button) and read `notify_response`.
- **A notification cannot truly block.** For a hard, must-click-to-proceed
prompt, that is a real dialog window (the Bridge approval dialog), not a
notification.
## Troubleshooting: "I got `status:ok` but saw no notification"
`notify_user` returning `status:ok` means only that Bridge accepted the
request and dispatched the notification; it does not confirm it displayed. If
nothing appeared:
1. **Check Notification Center** (click the clock in the menu bar). Focus /
Do Not Disturb routes banners straight there. That is a macOS setting, not
a Bridge bug; the user can allow Adom Bridge notifications during Focus in
System Settings > Focus.
2. **Check notification permission**: System Settings > Notifications >
Adom Bridge must be allowed.
3. **Check you are actually reaching that Bridge.** From a cloud container,
`notify_user` traverses the relay the Bridge is connected to; a
`--target <name>` aimed at a relay that is not running silently fails, so
confirm with `adom-desktop targets` first. (The direct API does carry
notify; a bridge or sibling app on the same machine can
`POST /command {"command":"notify_user",...}` with no relay.)
4. When you must be SURE the human saw it, do not rely on the notification
alone; also surface the message in your chat/response.
## When to use which
- **Awareness** ("export done", "build green"): basic, or `level:"success"`.
- **"Go look at this"** (a login, a dialog, an app waiting on them):
`focus`, so the click lands on that window. This is the single
highest-value field in the verb and the one most often forgotten.
- **Need the user to choose**: `buttons` plus `wait:true` (or poll
`notify_response` / `notify_events`).
- **Long-running job**: re-fire with the same `id` as status advances.
- **Genuine emergency**: `level:"emergency"`, and consider also summoning a
window via `focus` so it cannot be missed.