feat(notifications): mentions, inbox, and table watches - #1243
Open
joepio wants to merge 55 commits into
Open
Conversation
️✅ 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. 🦉 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
Bot
force-pushed
the
cursor/notifications-design-0fcd
branch
2 times, most recently
from
August 19, 2026 15:36
f0c9c34 to
7490b98
Compare
…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>
/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>
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>
Develop renamed the user-facing personal drive to private drive. Keep the two-drive /app/dev-drive split and update comments/docs to match. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
cursor
Bot
force-pushed
the
cursor/notifications-design-0fcd
branch
from
August 28, 2026 07:41
1dfbdd4 to
ba2d7c2
Compare
joepio
added a commit
that referenced
this pull request
Aug 28, 2026
Settles vocabulary and shape for work that spans three existing docs. Decided in conversation, so written down before it has to be decided again. Users see two words: templates and integrations. "App" is not a category — thirteen of fourteen candidate mini-apps shipped as pure configuration, so an app is how a few templates are built, not something anyone browses for. Offering both as siblings is the mistake already made in the assistant's tools, where two curated sources with no rule for choosing produced a hand-written CRM instead of the CRM template. Automations get a third word when the first one ships, not before. The catalogue is browsed by shape under New and by brand under Import, because nobody thinks "I want a table template" — they think "I use Notion". A tested catalogue starts at about three entries, which reads as supporting nothing, so the search that finds nothing offers to have one written. That is what makes a small catalogue honest rather than embarrassing. For automations: no node editor, because the verdict preview already answers the question a node editor exists to answer, and better. The trigger stays declarative and already is. Outputs split into proposed and immediate, and there is a hole — ctx.http fires during the run that produces the verdict, so an outbound call escapes the review it appears to be under. Also records that #1243 gives the notification contract and not its producer: zero server files means an automation's "notify me" needs a second, server-side writer of the same resource.
joepio
added a commit
that referenced
this pull request
Aug 28, 2026
Both the badge tooltip and the share warning listed notifications among what the private drive holds. It does not: notifications are #1243, still unmerged. What is actually there is the saved-drives list (seeded in `ensurePersonalDrive`), favourites, and AI chats. Warning someone about data that does not exist teaches them to discount the warning, which is the one thing this copy cannot afford — it is the last thing between a private drive and being handed to a colleague.
Chatroom invites grant the child, not the parent drive. Activating that
host DID left the invitee sidebar showing a truncated subject instead of
Dev drive or "{name}'s Drive", which failed the smoke chatroom spec.
Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Chatroom invites grant the destination, not the parent. Activating the host after a name-check still raced: adoptDriveFromDeepLink could overwrite the session with the unread parent (Unauthorized / truncated DID). Only land on the host when the destination is itself a Drive, and do not clobber a did: session drive after the deep-link await. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
Extract inviteSessionDrive so a child invite cannot land on an unread parent. Unit tests pin drive-level vs child-resource behavior. Co-authored-by: joepmeindertsma <joepmeindertsma@gmail.com>
joepio
marked this pull request as ready for review
August 28, 2026 19:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#77 #1157
#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
NotificationItemon your private 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).
Rebased onto latest
develop(private-drive rename, planning index restructure,spaUrl, and related e2e helpers).Mentions
@write amentionsedge on the authored resource (actor-side, not inbox spam).NotificationEnginereverse-queries those edges and materializes inbox rows for the mentioned agent.Watches
UPDATE/DESTROYis matched with the sameconstraintMatchesthe Collection view already uses. Membership and content kinds are both supported.Direct messages and access requests
DirectMessageandAccessRequestclasses in the notifications ontology.Where it lives
Inbox, watches, preferences, and
DevicePushTokens sit under{privateDrive}/notifications/(getOrCreateNotificationsFolder)./app/dev-drivenow matches onboarding: a private drive (personal: true) first, then a Dev workspace (personal: false). The agent secret'sinitialDriveis the private drive so inbox writes and queries agree, while the UI lands on Dev.Accepting a drive invite still makes that drive the session drive. Accepting a child invite (chatroom, document) keeps the session on the invitee's
{name}'s Drive— the grant is the child, not the parent.Delivery
tauri-plugin-notification+ Notification API).DevicePushTokenlookup stub (wake-to-sync; payload still comes from the store).Tests
@tomic/libvitest covering engine, watches, and helpers.notifications.spec.tsfor inbox UI, mention/watch paths, and engine materialization.e2e.spec.tschatroom invite asserts the sidebar stays on{name}'s Drive.Related Issues
#77 #1157
Checklist