Skip to content

Add notification inbox and tab stacks#16

Merged
TraderSamwise merged 3 commits into
masterfrom
feat/gui-notifications-next
May 24, 2026
Merged

Add notification inbox and tab stacks#16
TraderSamwise merged 3 commits into
masterfrom
feat/gui-notifications-next

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented May 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • add daemon-backed notification feed polling and browser notification delivery
  • add a notification inbox with unread counts and read/clear actions
  • move main app navigation under hidden Expo tabs so each bottom-nav tab keeps its own stack

Verification

  • yarn --cwd app typecheck
  • yarn --cwd app test lib/api.test.ts lib/notification-policy.test.ts stores/settings.test.ts
  • yarn --cwd app lint
  • root pre-push: yarn typecheck && yarn lint && yarn test

Summary by CodeRabbit

Release Notes

  • New Features
    • Added notifications inbox with real-time unread message tracking and polling
    • Added notification bell icon in the top bar displaying unread count badge
    • Ability to mark notifications as read and clear them individually or in bulk
    • Restructured app navigation with new tab-based layout for organized access to dashboard, inbox, threads, and settings

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR transforms the app's navigation from explicit route groups to a tab-based structure, introduces a durable, polled notification feed with daemon-record evaluation, and adds notification UI surfaces (bell button, inbox screen, unread badges) throughout the shell. Core changes: Expo Router Tabs with four main tab layouts, project-scoped notification state atoms with polling in MainLayout, notification API functions with relay routing, daemon record evaluation via evaluateNotificationRecord(), and refactored MobileTabBar/ProjectSidebar for tab navigation with unread count display.

Changes

Notification system and tab-based navigation

Layer / File(s) Summary
Tab-based navigation architecture
app/app/(main)/(tabs)/_layout.tsx, app/app/(main)/(tabs)/(dashboard)/_layout.tsx, app/app/(main)/(tabs)/(inbox)/_layout.tsx, app/app/(main)/(tabs)/(settings)/_layout.tsx, app/app/(main)/(tabs)/(threads)/_layout.tsx, app/app/(main)/_layout.tsx, app/app/index.tsx
Introduces Expo Router Tabs navigator with four main tabs (dashboard, inbox, threads, settings), each with hidden-header Stack layouts. Root authenticated redirect changed from "/(main)" to "/", and MainLayout restructured to mount single (tabs) screen instead of enumerating route screens.
Tab routing helpers
app/lib/main-tabs.ts
Exports MainTabId union type, MAIN_TAB_ROUTES mapping with href/internalHref/screen metadata, mainTabForPath() pathname-to-tab inference (defaulting to dashboard), and useMainTabNavigation() hook for consistent tab navigation.
Notification feed state
app/stores/notifications.ts
Defines ProjectNotificationFeed interface and project-keyed Jotai atoms: notificationFeedFamily for feed data, notificationFeedErrorFamily for errors, notificationFeedRefreshNonceAtom for polling triggers, and notificationUnreadCountFamily derived atom.
Notification API layer
app/lib/api.ts, app/lib/api.test.ts
Adds NotificationRecord and NotificationsResponse types; exports listNotifications() querying relay-routed /notifications endpoint with optional unread/sessionId filters, markNotificationsRead() and clearNotifications() POSTing to relay-routed endpoints. Test coverage verifies relay routing and request bodies.
Notification record evaluation
app/lib/notification-policy.ts, app/lib/notification-policy.test.ts
Introduces evaluateNotificationRecord() transforming daemon records to ClientNotificationEvents: filters unread/uncleared records, maps kinds (needs_input, blocked, error, completed, activity), checks settings enablement, builds title/body/routing. Includes mapNotificationRecordKind() helper; test coverage verifies kind mapping and settings filtering.
MainLayout notification polling
app/app/(main)/_layout.tsx
Adds polling useEffect that periodically calls listNotifications when endpoint and project available, stores feed/error in project-keyed atoms, clears state when endpoint missing, respects notificationFeedRefreshNonce trigger atom.
NotificationProvider feed integration
app/components/NotificationProvider.tsx
Extends provider to derive notificationFeed from atoms, baseline per-project seen record IDs, deduplicate incoming records, evaluate via evaluateNotificationRecord(), conditionally show browser notifications. Retains fallback to desktopState when feed unavailable.
Notifications screen
app/app/(main)/(tabs)/(inbox)/notifications.tsx
Implements full NotificationsScreen: reads project/endpoint/feed from atoms, provides refresh and mutate callbacks for API calls, openNotification for navigation, renders NotificationRow cards with Open/Read/Clear actions, displays error/loading/empty states and header bulk-action controls.
Notification UI components and mobile tab bar
app/components/NotificationBellButton.tsx, app/components/MobileTabBar.tsx, app/components/TopBar.tsx, app/components/AppShell.tsx, app/components/AuthMenu.tsx
Adds NotificationBellButton to TopBar with unread badge and conditional navigation; refactors MobileTabBar from Expo Router pathname-based to React Navigation BottomTabBarProps-driven with project-scoped inbox unread badge; removes MobileTabBar from AppShell; wires AuthMenu settings navigation via useMainTabNavigation().
ProjectSidebar tab navigation
app/components/ProjectSidebar.tsx
Updates bottom navigation from route-based to tab-id-based using mainTabForPath() and useMainTabNavigation(), adds unread badge via notificationUnreadCountFamily, introduces EMPTY_PROJECT_PATH fallback, adjusts session/service selection routes to remove /(main) prefix.
Route path simplification
app/app/(main)/(tabs)/(dashboard)/agent/[sessionId]/chat.tsx, app/app/(main)/(tabs)/(dashboard)/index.tsx, app/app/(main)/(tabs)/(dashboard)/service/[serviceId].tsx
Removes /(main) route-group prefix from navigation targets: dashboard now routes to /agent/[sessionId]/chat and /service/[serviceId], chat plan button routes to /plans/[sessionId], service detail fallback routes to /.
Notification system documentation
docs/notification-system.md
Documents polling-based delivery model: queries /notifications?unread=1, baselines seen record IDs per project, treats daemon records as canonical, retains /desktop-state as temporary fallback, defines browser notification eligibility via deduplication, unread/cleared state, and document visibility.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TraderSamwise/aimux#10: Adds relay-routing configuration that determines how the new listNotifications, markNotificationsRead, and clearNotifications API functions route through the relay proxy.

  • TraderSamwise/aimux#15: Introduces notification settings and policy evaluation foundation that evaluateNotificationRecord() in this PR builds upon for daemon record transformation.

Poem

A tab bar dances, freshly born,
Notifications leap from dawn to morn,
Badges gleam with unread tale—
No more /(main), the routes all sail!
🐰📬✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add notification inbox and tab stacks' directly summarizes the main architectural changes: introducing a notification inbox feature and refactoring navigation under tab stacks.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gui-notifications-next

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/app/(main)/(tabs)/(dashboard)/_layout.tsx

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

app/app/(main)/(tabs)/(dashboard)/agent/[sessionId]/chat.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

app/app/(main)/(tabs)/(dashboard)/index.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 20 others

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
app/components/NotificationBellButton.tsx (1)

25-26: ⚡ Quick win

Use shared tab inference for active state instead of a hardcoded pathname prefix.

Line 25 should derive active state from the same tab mapping used for navigation, so route alias/path changes don’t silently desync the bell highlight behavior.

Proposed change
-import { useMainTabNavigation } from "`@/lib/main-tabs`";
+import { mainTabForPath, useMainTabNavigation } from "`@/lib/main-tabs`";
@@
-  const active = pathname.startsWith("/notifications");
+  const active = mainTabForPath(pathname) === "inbox";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/NotificationBellButton.tsx` around lines 25 - 26, Replace the
hardcoded startsWith check that sets active (const active =
pathname.startsWith("/notifications")) with the shared tab inference used by
navigation: import and call the shared function/enum (e.g., getActiveTab,
inferTabFromPath, or the Tabs enum used elsewhere) with pathname and set active
= getActiveTab(pathname) === 'notifications' (or === Tabs.Notifications) so the
NotificationBellButton uses the same tab mapping as navigation and won’t desync
when routes/aliases change.
app/components/NotificationProvider.tsx (1)

31-57: ⚡ Quick win

Bound per-project seen-ID cache growth.

seenNotificationIdsRef only grows, so long-running sessions can retain every historical notification ID for a project. Prune stale IDs (or cap size) before processing new records.

Proposed adjustment
   const currentIds = new Set(notificationFeed.notifications.map((record) => record.id));
   const seenIds = seenNotificationIdsRef.current.get(projectScope) ?? new Set<string>();
+  for (const id of seenIds) {
+    if (!currentIds.has(id)) seenIds.delete(id);
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/NotificationProvider.tsx` around lines 31 - 57,
seenNotificationIdsRef currently only grows per project; before processing in
the effect (where projectScope, currentIds, seenIds, and
notificationFeed.notifications are used) prune or cap the stored seen IDs to
avoid unbounded memory growth: for example, intersect seenIds with currentIds to
drop IDs no longer relevant and/or enforce a max size (e.g., keep the most
recent N IDs by converting seenIds to an ordered array and slicing) then
continue with the existing logic that iterates notificationFeed.notifications,
calls evaluateNotificationRecord, and possibly showBrowserNotification; update
seenNotificationIdsRef.current.set(projectScope, seenIds) after pruning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@app/components/NotificationBellButton.tsx`:
- Around line 25-26: Replace the hardcoded startsWith check that sets active
(const active = pathname.startsWith("/notifications")) with the shared tab
inference used by navigation: import and call the shared function/enum (e.g.,
getActiveTab, inferTabFromPath, or the Tabs enum used elsewhere) with pathname
and set active = getActiveTab(pathname) === 'notifications' (or ===
Tabs.Notifications) so the NotificationBellButton uses the same tab mapping as
navigation and won’t desync when routes/aliases change.

In `@app/components/NotificationProvider.tsx`:
- Around line 31-57: seenNotificationIdsRef currently only grows per project;
before processing in the effect (where projectScope, currentIds, seenIds, and
notificationFeed.notifications are used) prune or cap the stored seen IDs to
avoid unbounded memory growth: for example, intersect seenIds with currentIds to
drop IDs no longer relevant and/or enforce a max size (e.g., keep the most
recent N IDs by converting seenIds to an ordered array and slicing) then
continue with the existing logic that iterates notificationFeed.notifications,
calls evaluateNotificationRecord, and possibly showBrowserNotification; update
seenNotificationIdsRef.current.set(projectScope, seenIds) after pruning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab094c1a-1879-47ea-b348-8d40a69aa6b4

📥 Commits

Reviewing files that changed from the base of the PR and between b68d4ea and 043080c.

📒 Files selected for processing (29)
  • app/app/(main)/(tabs)/(dashboard)/_layout.tsx
  • app/app/(main)/(tabs)/(dashboard)/agent/[sessionId]/chat.tsx
  • app/app/(main)/(tabs)/(dashboard)/graveyard.tsx
  • app/app/(main)/(tabs)/(dashboard)/index.tsx
  • app/app/(main)/(tabs)/(dashboard)/plans/[sessionId].tsx
  • app/app/(main)/(tabs)/(dashboard)/service/[serviceId].tsx
  • app/app/(main)/(tabs)/(inbox)/_layout.tsx
  • app/app/(main)/(tabs)/(inbox)/notifications.tsx
  • app/app/(main)/(tabs)/(settings)/_layout.tsx
  • app/app/(main)/(tabs)/(settings)/settings.tsx
  • app/app/(main)/(tabs)/(threads)/_layout.tsx
  • app/app/(main)/(tabs)/(threads)/threads.tsx
  • app/app/(main)/(tabs)/_layout.tsx
  • app/app/(main)/_layout.tsx
  • app/app/index.tsx
  • app/components/AppShell.tsx
  • app/components/AuthMenu.tsx
  • app/components/MobileTabBar.tsx
  • app/components/NotificationBellButton.tsx
  • app/components/NotificationProvider.tsx
  • app/components/ProjectSidebar.tsx
  • app/components/TopBar.tsx
  • app/lib/api.test.ts
  • app/lib/api.ts
  • app/lib/main-tabs.ts
  • app/lib/notification-policy.test.ts
  • app/lib/notification-policy.ts
  • app/stores/notifications.ts
  • docs/notification-system.md
💤 Files with no reviewable changes (1)
  • app/components/AppShell.tsx

@TraderSamwise
TraderSamwise merged commit 6619b5e into master May 24, 2026
1 check passed
@TraderSamwise
TraderSamwise deleted the feat/gui-notifications-next branch May 24, 2026 02:14
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.

1 participant