Skip to content

feat(notifications): mentions, inbox, and table watches - #1243

Draft
joepio wants to merge 51 commits into
developfrom
cursor/notifications-design-0fcd
Draft

feat(notifications): mentions, inbox, and table watches#1243
joepio wants to merge 51 commits into
developfrom
cursor/notifications-design-0fcd

Conversation

@joepio

@joepio joepio commented Aug 1, 2026

Copy link
Copy Markdown
Member

#77 #1157

Adds in-app notifications for @mentions, watched tables/collections, direct messages, and access requests.

When someone mentions you, a watched query changes, a collaborator DMs you, or someone asks for access, the event lands as a NotificationItem on your Personal drive — not the current workspace. The App-menu bell opens /app/notifications. Read and dismissed state syncs with that drive.

This is the product slice of #77 (mentions, subscriptions, inbox — not feeds) and the mention-picker work in #1157 (search agents outside the current drive).

Mentions

  • TipTap and chat @ write a mentions edge on the authored resource (actor-side, not inbox spam).
  • NotificationEngine reverse-queries those edges and materializes inbox rows for the mentioned agent.
  • The mention search can pick agents that are not in the current drive.

Watches

  • Watch toggle on tables and collections.
  • Settings lists the agent's watches (enable / mute / remove).
  • Incoming UPDATE/DESTROY is matched with the same constraintMatches the Collection view already uses. Membership and content kinds are both supported.

Direct messages and access requests

  • DirectMessage and AccessRequest classes in the notifications ontology.
  • Inbox actions: send a message to a drive collaborator, grant an access request.

Where it lives

Inbox, watches, preferences, and DevicePushTokens sit under {personalDrive}/notifications/ (getOrCreateNotificationsFolder).

/app/dev-drive now matches onboarding: a private Personal drive (personal: true) first, then a Dev workspace (personal: false). The agent secret's initialDrive is Personal so inbox writes and queries agree, while the UI lands on Dev.

Delivery

  • In-app center, unread badge, and toasts.
  • Desktop / web OS notifications (tauri-plugin-notification + Notification API).
  • Hub DevicePushToken lookup stub (wake-to-sync; payload still comes from the store).

Tests

  • @tomic/lib vitest covering engine, watches, and helpers.
  • Playwright notifications.spec.ts for inbox UI, mention/watch paths, and engine materialization.

Related Issues

#77 #1157

Checklist

  • Add changelog entry linking to issue, describe API changes
  • Add or update tests if needed
  • Update docs if needed
Open in Web Open in Cursor 

@cursor cursor Bot changed the title docs(planning): notifications design — mentions + watch subscriptions feat(notifications): mentions, inbox, and table watches Aug 1, 2026
@gitguardian

gitguardian Bot commented Aug 14, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@cursor
cursor Bot force-pushed the cursor/notifications-design-0fcd branch from 9c48bec to d280d93 Compare August 15, 2026 12:25
cursoragent and others added 27 commits August 19, 2026 15:33
…ions

Propose actor-side mention edges, personal NotificationItems, client-side
watch matching for collections/tables, and a phased delivery path from
in-app center through Tauri/OS notifications to later push-as-wake.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Lock personal-drive sync for read/dismissed, and document the local
notification vs APNs/FCM split for Tauri mobile (wake-then-sync payload).

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Lock App menu order (User Settings → Notifications → Settings) and split
inbox route from App Settings preference section.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add the notifications ontology, write actor-side `mentions` from TipTap
and chat, materialize personal NotificationItems via NotificationEngine,
and ship the sidebar inbox plus a Watch toggle on tables.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add Playwright specs for the notifications sidebar/inbox, unread badge,
mark-all-read, and table Watch toggle. Fix WatchToggle busy-state handling
to avoid bare try/finally for React Compiler.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Subscribe inbox rows to LocalChange via useValue, notify the store after
mark/dismiss saves, and fall back to direct store writes if the engine
is still starting. Parent the notifications ontology under
https://atomicdata.dev/ for rights walks. All four notifications
Playwright specs are green; populate unit asserts the ontology seeds.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Show Web Notification API / tauri-plugin-notification banners when the
tab or window is unfocused; toast when focused. Permission is requested
from Watch enable and App Settings, never on cold start. Mark-read and
dismiss cancel matching OS notifications. Wire the Tauri plugin +
capabilities; document remaining Phase 5 push work.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
- WatchToggle on Collection views; Settings watches list (mute/remove)
- E2E: watch + simulated other-agent child materializes NotificationItem
- DevicePushToken ontology + registerDevicePushToken helper
- Hub push_wake helpers (wake-only payload) + commit_monitor hook note
- Cold-start push tap queue; flushPendingWatches for tests

Co-authored-by: Joep Meindertsma <joep@ontola.io>
- data-watching / mark-all-read / notifications-empty test ids
- flushPendingWatches after brief wait for async ResourceUpdated
- Avoid brittle lingui-scrambled accessible names in assertions

Co-authored-by: Joep Meindertsma <joep@ontola.io>
… tap wire

- E2E: mention ResourceUpdated (other actor) materializes inbox item
- E2E: mark read on A clears badge on B (same agent, two contexts)
- Wire pushWakeTap → navigate in NotificationOsPresenter
- useDevicePushRegistration on launch; suppress-if-read helpers

Co-authored-by: Joep Meindertsma <joep@ontola.io>
- Wait for engine; in-app nav after mention materialization
- Second-device sign-in without Continue; wait for agent then open inbox

Co-authored-by: Joep Meindertsma <joep@ontola.io>
- NotificationEngine re-runs mention backlog on DriveChanged
- E2E: A invites B, mentions B while B's engine stopped, B reconciles
- Closes reverse-query + two-agent invite gaps in planning/coverage

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Top-bar Share on a freshly UI-created document can leave useCanWrite stuck
false so Create Invite never appears. Use the same drive context-menu share
path as the authorization invite e2e instead.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Fix Share Create Invite stuck read-only on new DID docs by resetting
useCanWrite optimistically on subject/newness and re-checking when
resource.new flips after genesis.

Phase 5: handlePushWake/processPushWake (fetch → reconcile →
suppress-if-read), receive queue wired in NotificationOsPresenter, and
commit_monitor mention → wake stub (provider send still TODO).

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Pick tauri-plugin-push-notifications for Phase 5 (documented; Cargo gated
until Firebase/APNs project files exist). Add tauriPushBridge to drain
local notification cold-start taps and optionally fetch a device token.
Hub enqueue_push_wakes looks up DevicePushToken rows per wake agent.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add PushSender trait with LoggingPushSender default (ready for FCM/APNs),
watch subscription wake matching (target/parent, mute/enabled, owner),
and route commit_monitor through wakes_for_committed_resource.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add EnvPushSender (ATOMIC_FCM_* / ATOMIC_APNS_* bearers) installed at
serve boot, falling back to log-only without secrets. Gate the Tauri
push plugin behind --features mobile-push and document enablement in
planning/notifications.md.

Co-authored-by: Joep Meindertsma <joep@ontola.io>
Co-authored-by: Joep Meindertsma <joep@ontola.io>
Add DirectMessage and AccessRequest ontology classes, actor-side
mentions so the existing engine materializes inbox items, a Send
message dialog for drive collaborators, Request access on Share and
Unauthorized, and Grant from the inbox / request page.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The notifications page queried WASM once on mount. A second browser
signing in as the same agent could miss NotificationItems that arrived
via drive sync after that query, so mark-read never had a row to sync.

Scan the in-memory store (and re-query on ResourceUpdated / sync
status), wait for the seeded item to drain before opening device B,
and keep the index query as a backlog supplement.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
pendingDirtyCount can stay non-zero for unrelated in-flight saves.
Wait until the NotificationItem is fetchable over HTTP so device B
has something to sync.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The App menu label is often the agent name, so waiting for a
User Settings link races sign-in. Agent subject is enough before
opening the inbox.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Drive sync can finish before nested NotificationItems are in the
parent index, so a second device's WASM collection stays empty.
Ask /query for isA=NotificationItem on the personal drive and
hydrate those subjects into the store.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Device B's drive sync can omit nested NotificationItems. Fetch the
item A already posted so the inbox and mark-read path can run.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Restricting the listener to NotificationItem classes missed items
whose isA had not materialized yet at notify time. Debounced rescan
of the in-memory store on every ResourceUpdated.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
bootstrap() no-ops on an already-seeded store, so --initialize never
added NotificationItem and commits 500'd while fetching the class from
atomicdata.dev. Re-run populate on initialize when SHORTNAME exists.

Also wait for the seeded inbox item to drain before opening device B.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Silent wakes never reach a killed phone. The hub now sends a generic
lock-screen alert (FCM notification + APNs aps.alert) plus the existing
about/type data bag, and mints FCM/APNs JWTs from a service-account JSON
or .p8 on disk.

Tauri Android gets POST_NOTIFICATIONS and an optional google-services
hook; Flutter registers firebase_messaging after sign-in onto the same
DevicePushToken ontology.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
cursoragent and others added 21 commits August 19, 2026 15:34
/app/dev-drive created a single personal workspace, so the Notifications
folder appeared as a child of Dev drive. Provision a private Personal
drive for inbox/watches and a non-personal Dev drive workspace, then
land on the workspace.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Creating a Personal drive plus Dev drive made every before() slower and
turned newDrive()'s waitForURL(/did:ad:/) into a no-op (already on a
workspace). Skip the unused Personal ontology, wait for the new drive
title, and put the workspace in the agent secret's initialDrive so
second-context specs still open Dev drive subjects.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
/app/dev-drive now provisions Personal + Dev drive. Under suite-wide
load that setup ate the old 60s budget before the spec body ran.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Callers like the user-drives spec open the helper from User Settings,
which has no resource subject. Treat the previous subject as optional.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Keep the /app/dev-drive secret's initialDrive on Personal so inbox writes
and queries use the same drive. Remount the notification engine when the
Agent resource gains personalDrive (invite persist), and recover from a
failed first start instead of leaving __notificationEngine unset.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
reloadWatches no longer replaces the watch map with an empty collection
index after a just-saved subscription. The unread badge always hits
server /query, and invite e2e opens /app/invite on FRONTEND_URL so the
invitee uses the same bundle as the rest of the suite.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Re-fetch notification items over the websocket so device B sees mark-read
commits. After invite persist, rebuild the Agent with Personal as
initialDrive so the engine remounts on the private inbox drive.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Always HTTP-refetch inbox rows so mark-read is not stuck on a cached
unread copy. The second context now signs in through signIn() and waits
for Connected before opening the inbox.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
visibleNotificationItems no longer skips `new` rows, so a watch or
mention upsert appears in the inbox before the collection index
catches up.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
cargo fmt --check and pnpm lint (oxlint + oxfmt) are the first CI
gates. Format the notifications/push Rust and TS that were blocking
the pipeline before e2e could run.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
CI fail-fast passed rustfmt/oxlint; flutter analyze then exited 1 on
unused_import / unnecessary_import in the push notification files.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The bin re-declares modules instead of using the lib. push_provider
imports crate::push_credentials, which compiled in the lib but not
in atomic-server's binary until the module is listed in bin.rs.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
CI linked a stale rustc incremental binary (old simple.rs line numbers,
10 tests instead of 12) and the suite shares a process-global OnceLock
DB. Touch sources so cargo rebuilds, run --test-threads=1, and pin
active_drive from setup's returned subject.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Touching only the bridge sources rebuilt simple.rs against a cached
atomic_lib rlib that predated DEVICE_PUSH_* url constants (E0425).
Clean both packages so cargo rebuilds lib + the bridge together.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Notifications ontology adds access-request (and others) that sort
before agent. Assert nested members are sorted by shortname and that
Agent is still in the collection.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Dev-drive agents are named Dev User, so signIn's 10s wait for a
User Settings row never resolved (and swallowed the timeout). Second
contexts then raced presence, meetings, and cold-load. Wait for
store.getAgent() instead.

Open the inbox via a scrolled, force-clicked sidebar-notifications
testid — the App menu sits below two drives. Ignore NotificationEngine
isA/mentions /query frames in the refresh-storm regression.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Dev-drive secrets pin initialDrive to Personal. A second context that
only opens ?subject=<Dev drive> still has Personal as store.getDrive(),
so presence, meetings, and cold-load children miss the workspace.
Click Set as current drive after that navigation.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
The Drive page's Set as current drive control often appears after the
previous 5s visibility check, so second contexts stayed on Personal.
Call store.setDrive and wait until the session drive matches.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Same-agent extra contexts sign in with Personal as initialDrive, then
open a workspace canvas/document. Live WS stays on Personal unless we
setDrive from the resource's stamped drive property.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Share getOrCreateFolderByLocalId with Drafts/Forks, list inbox subjects
once for the route and badge, match watches via Collection.constraintMatches,
and drive e2e folder/drive setup through production helpers.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
createdBy lives on server.properties, not core. Passing undefined into
Resource.set crashed Loro in the engine-path notification specs.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
@cursor
cursor Bot force-pushed the cursor/notifications-design-0fcd branch from f0c9c34 to 7490b98 Compare August 19, 2026 15:36
cursoragent and others added 3 commits August 19, 2026 17:53
e2e format-check failed on an 80-col import. InvitePage no longer assigns
agent.initialDrive on the value from useSettings (react/immutability);
new-agent invites rebuild from the secret instead.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Production Vite builds look up chrome text by catalog id. Mentions, watches,
DMs, and access-request copy was never extracted, so CI rendered empty
headings (the inbox h1 had no accessible name).

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Use testids for the notifications heading and send-message dialog so CI
does not depend on translated accessible names. Fall back to the personal
drive when the invite host is unreadable, and accept either Dev drive or
the invitee's home title after accept.

Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants