Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
29b1464
docs(planning): design notifications for mentions and watch subscript…
cursoragent Aug 1, 2026
1a6216a
docs(planning): sync notification read state; detail iOS/Android push
cursoragent Aug 1, 2026
a2318ae
docs(planning): put notifications below user settings in sidebar
cursoragent Aug 1, 2026
fa928f7
feat(notifications): mentions, inbox, and table watches
cursoragent Aug 1, 2026
1119fed
test(e2e): cover notifications inbox, mark-read, and table watch
cursoragent Aug 1, 2026
f5a9e48
fix(notifications): make mark-read reactive and cover in e2e
cursoragent Aug 1, 2026
ed27fbc
feat(notifications): Phase 4 local OS banners (browser + Tauri)
cursoragent Aug 1, 2026
de2806d
feat(notifications): collection watch, settings list, Phase 5 scaffold
cursoragent Aug 1, 2026
a3805f5
test(notifications): harden e2e selectors and watch→item path
cursoragent Aug 1, 2026
e91295d
feat(notifications): mention engine e2e, multi-device read sync, push…
cursoragent Aug 1, 2026
ff2a2d2
test(notifications): fix mention and multi-device e2e flakes
cursoragent Aug 1, 2026
37dca66
feat(notifications): invite A→B mention e2e + DriveChanged reconcile
cursoragent Aug 1, 2026
4c2a9d7
test(notifications): fix invite A→B e2e via drive context-menu share
cursoragent Aug 1, 2026
6c387da
feat(notifications): fix useCanWrite + push wake sync path
cursoragent Aug 2, 2026
a49040d
feat(notifications): choose push plugin, cold-start drain, token lookup
cursoragent Aug 2, 2026
1c1c6f7
feat(notifications): PushSender + hub watch wake matching
cursoragent Aug 2, 2026
e1f1a9e
feat(notifications): env FCM/APNs PushSender + mobile-push feature
cursoragent Aug 2, 2026
8ed430b
chore: update Cargo.lock for tauri-plugin-push-notifications
cursoragent Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 326 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions TESTING_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,37 @@ Both matter because `iroh_transport` holds the router and node identity in

---

## Notifications

| Flow | Layer | Where |
|---|---|---|
| Extract agent mentions from TipTap JSON / chat text | glue | `browser/lib/src/mentions.test.ts` |
| Populate includes NotificationItem / watches ontology | unit | `populate::notifications_ontology_is_populated` |
| Sidebar Notifications + empty inbox | flow | `browser/e2e/tests/notifications.spec.ts` |
| Seeded NotificationItem appears with unread badge | flow | `notifications.spec.ts` |
| Mark all read clears unread styling | flow | `notifications.spec.ts` |
| Table Watch toggle → Watching | flow | `notifications.spec.ts` |
| Collection Watch toggle | flow | same `WatchToggle` on `CollectionPage` |
| Watch → simulated other-agent row → inbox | flow | `notifications.spec.ts` (engine via `__notificationEngine`) |
| Settings watches list (mute / remove) | flow | `WatchesList` in App Settings |
| OS notification id hash + surface heuristics | unit | `osNotifications.test.ts` |
| Local OS banner (browser / Tauri) | flow | manual / desktop smoke — Phase 4 wired |
| DevicePushToken wake payload contract | unit | `devicePushToken.test.ts` + `push_wake` Rust tests |
| Push wake sync→suppress-if-read | unit | `handlePushWake` in `devicePushToken.test.ts` |
| Cold-start push tap / receive queues | unit | `pushWakeTap.test.ts` |
| Remote push payload → receive queue | unit | `tauriPushBridge.test.ts` |
| Hub mention → wake candidates | unit | `push_wake::mention_wakes_for_resource` |
| Hub watch → wake candidates | unit | `push_wake::watch_owner_agent` / mute / enabled |
| Hub DevicePushToken lookup + PushSender | unit | `push_wake` enqueue path (`LoggingPushSender`) |
| Env FCM/APNs sender config | unit | `push_provider::tests` (no network) |
| Mention → NotificationItem materialization (engine) | flow | `notifications.spec.ts` (fake other-agent actor) |
| A mentions B → B sees unread (two agents / invite) | flow | `notifications.spec.ts` (invite + reconcile backlog) |
| Mark read on A clears badge on B | flow | `notifications.spec.ts` (same agent, two contexts) |
| Reverse query mentions ∋ agent | flow | same invite e2e (`reconcileMentionBacklog`) |
| OS / APNs / FCM delivery | flow | **gap** — Phase 5 transport (ontology + wake helpers + tap queue wired) |

---

## Blind spots

Ordered by how much they would hurt.
Expand Down
2 changes: 2 additions & 0 deletions browser/data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@tanstack/react-router": "^1.170.7",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-barcode-scanner": "^2.4.5",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tiptap/core": "^3.23.6",
"@tiptap/extension-drag-handle": "^3.23.6",
"@tiptap/extension-drag-handle-react": "^3.23.6",
Expand Down Expand Up @@ -91,6 +92,7 @@
"remark-math": "^6.0.0",
"styled-components": "^6.4.2",
"stylis": "4.4.0",
"tauri-plugin-push-notifications": "0.1.0",
"trystero": "0.25.3",
"unist-util-visit": "^5.1.0",
"uqr": "^0.1.3",
Expand Down
9 changes: 6 additions & 3 deletions browser/data-browser/src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { LazyMCPProvider } from '@components/AI/MCP/LazyMCPProvider';
import { CustomViewProvider } from '@components/CustomViewProvider';
import { LazyAIChangesProvider } from '@components/AI/AIChanges/LazyAIChangesProvider';
import { FollowProvider } from '@components/Presence/FollowContext';
import { NotificationEngineProvider } from './hooks/useNotificationEngine';

// Setup bugsnag for error handling, but only if there's an API key
const ErrBoundary = window.bugsnagApiKey
Expand Down Expand Up @@ -91,9 +92,11 @@ export const Providers: React.FC<React.PropsWithChildren> = ({ children }) => {
<SkipNav />
<SearchOverlayContextProvider>
<FollowProvider>
<NavWrapper>
{children}
</NavWrapper>
<NotificationEngineProvider>
<NavWrapper>
{children}
</NavWrapper>
</NotificationEngineProvider>
</FollowProvider>
</SearchOverlayContextProvider>
</NewResourceUIProvider>
Expand Down
24 changes: 21 additions & 3 deletions browser/data-browser/src/chunks/RTE/CollaborativeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
ResourceNodeInline,
} from './ResourceExtension/ResourceNode';
import DragHandle from '@tiptap/extension-drag-handle-react';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { TiptapContextProvider } from './TiptapContext';
import { SlashCommands, buildSuggestion } from './SlashMenu/CommandsExtension';
import {
Expand All @@ -30,6 +30,7 @@ import {
import type { LoroDoc } from 'loro-crdt';
import {
dataBrowser,
syncDocumentMentions,
useCanWrite,
useDebouncedSave,
useResource,
Expand Down Expand Up @@ -84,7 +85,7 @@ export default function CollaborativeEditor({
}: CollaborativeEditorProps): React.JSX.Element {
const store = useStore();
const showNewResourceUI = useNewResourceUI();
const [save] = useDebouncedSave(resource, 500);
const [debouncedSave] = useDebouncedSave(resource, 500);
const { agent, drive } = useSettings();
// Same deterministic per-agent color as the presence avatars, so a
// user's cursor and their avatar match everywhere.
Expand Down Expand Up @@ -383,6 +384,22 @@ export default function CollaborativeEditor({
}
}, [editor, editorReady, isComparing, comparisonBaseline]);

// Keep the actor-side `mentions` property in sync with Agent subjects
// embedded via `@` before the debounced Loro body save.
const save = useCallback(() => {
void (async () => {
if (editor) {
try {
await syncDocumentMentions(resource, editor.getJSON());
} catch {
// Mentions are best-effort; body save still proceeds.
}
}

debouncedSave();
})();
}, [editor, resource, debouncedSave]);

return (
<IsInRTEContex value={true}>
<TiptapContextProvider editor={editor}>
Expand All @@ -392,7 +409,8 @@ export default function CollaborativeEditor({
</DragHandle>
<EditorContent key='rich-editor' editor={editor}>
<FloatingHint editor={editor}>
Type &apos;/&apos; for options or &apos;@&apos; for resources
Type &apos;/&apos; for options or &apos;@&apos; for people and
resources
</FloatingHint>
<FullBubbleMenu />
<EditorEvents
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension, type Editor, type Range } from '@tiptap/react';
import { Suggestion, type SuggestionOptions } from '@tiptap/suggestion';
import type { Store } from '@tomic/react';
import { core, isAgentSubject, type Store } from '@tomic/react';
import type { SuggestionItem } from '../types';
import { getIconForClass } from '@helpers/iconMap';
import { PluginKey } from '@tiptap/pm/state';
Expand Down Expand Up @@ -47,7 +47,25 @@ export const buildResourceSuggestion = (

const resources = await Promise.all(results.map(x => store.getResource(x)));

return resources.map(r => ({
// Prefer Agents so `@` feels like a people mention first; resource embeds
// still appear after. Mentions of agents write the `mentions` property on
// save (planning/notifications.md).
const ranked = [...resources].sort((a, b) => {
const aAgent =
isAgentSubject(a.subject) ||
a.getClasses().includes(core.classes.agent);
const bAgent =
isAgentSubject(b.subject) ||
b.getClasses().includes(core.classes.agent);

if (aAgent === bAgent) {
return 0;
}

return aAgent ? -1 : 1;
});

return ranked.map(r => ({
title: r.title,
id: r.subject,
icon: getIconForClass(r.getClasses()[0]),
Expand Down
4 changes: 3 additions & 1 deletion browser/data-browser/src/chunks/TablePage/TablePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TableExportDialog } from './TableExportDialog';
import { TableResource } from './TableResource';
import { useCustomContextItems } from '@components/ResourceContextMenu/CustomContextItemsContext';
import { DIVIDER } from '@components/Dropdown';
import { WatchToggle } from '@components/WatchToggle';

export function TablePage({ resource }: ResourcePageProps): JSX.Element {
const titleId = useId();
Expand Down Expand Up @@ -52,13 +53,14 @@ export function TablePage({ resource }: ResourcePageProps): JSX.Element {
<ResourceCoverImage resource={resource} />
<BoundedHeightContainer>
<Column>
<FlexRow justify='space-between'>
<FlexRow justify='space-between' center>
<EditableTitle
resource={resource}
id={titleId}
onCommit={focusTable}
withDecorations
/>
<WatchToggle resource={resource} />
</FlexRow>
<TableResource resource={resource} />
</Column>
Expand Down
3 changes: 3 additions & 0 deletions browser/data-browser/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { RightPanelProvider } from './RightPanel/RightPanelContext';
import { CommentsPanelContainer } from './CommentsPanel/CommentsPanelContainer';
import { FollowSessionPanelContainer } from './Presence/FollowSessionPanelContainer';
import { MeetingMessageToaster } from './Presence/MeetingMessageToaster';
import { NotificationOsPresenter } from './NotificationOsPresenter';
import { ResourceContextMenuHost } from './ResourceContextMenu';
import { HideInPrint } from './HideInPrint';
import { MAIN_CONTAINER } from '@helpers/containers';
Expand Down Expand Up @@ -73,6 +74,8 @@ export function NavWrapper({ children }: NavWrapperProps): JSX.Element {
<ResourceContextMenuHost />
{/* Toasts new meeting messages when the meeting panel isn't open. */}
{!hideGlobalChrome && <MeetingMessageToaster />}
{/* In-app toast / OS banner for NotificationItems. */}
{!hideGlobalChrome && <NotificationOsPresenter />}
{!hideGlobalChrome && (
<TopBar subject={contextualSubject} top={navbarTop} />
)}
Expand Down
Loading
Loading