Light the bell, and make a click mean something

Fixkamo-internal
Shipped
September 7, 2026 at 11:15 PM UTC
Author
Kamo
Commit
ebfe26f

Three defects behind one report: the browser tab alerted for an unread notification while the bell stayed dark, and clicking rows on /notifications went to the homepage. THE BELL COULD NEVER LIGHT. MemberListProvider does not merely wrap its children — it renders <NavTop> as a SIBLING of them. NotificationProvider was written inside it, so NotificationBell called useNotifications() from outside its own provider. That hook fails soft by design, because NavTop also renders on pre-auth surfaces where a missing bell must not take the page down, so it answered unreadCount: 0. The badge was invisible, .kamo-bell-ring was never applied and the icon never filled — structurally, for every member, with no error anywhere. What hid it is that TabAttention reads the SAME hook from inside the provider, so the browser tab worked perfectly the whole time. A member sees their tab flash and their bell sit still, and every instinct says the bell is broken rather than that it is in the wrong place in the tree. This is the second time this shape has cost real behaviour — the tool dock above UnreadProvider was the first — so it is guarded now rather than commented: the guard also checks its own premise, so it cannot start passing vacuously if NavTop ever moves. A CLICK PUSHED A URL FOR THINGS THAT ARE NOT PLACES. deepLink is a URL, and a producer with nowhere to point writes the closest thing the field accepts: CHAT_MESSAGE /?openChat={guid}&senderId={id} the homepage GROWTH_HUB / the homepage NEW_EMAIL /messages right app, not the message LEAD_CALLBACK /leads/view/{id} correct The chat shape was invented for EMAIL links, where a cold arrival from outside has nothing but a URL to work with — UnreadChatNotifier says so where it builds it: "Chat has no route to point at — the window is a tool." Reused in-app it navigates you home to open a window that could have opened where you stood, and DeepLinkToolBootstrap latches dispatchedRef for the life of a mount that lives in the persistent shell, so only the FIRST such click per page load opened anything at all. Growth Hub stored "/" because widgetRegistry records that it has no screen of its own: the widget IS the feature. So the browser resolves an INTENT now, not a URL. A chat opens its tool window in place with no navigation; a Growth Hub review goes home and the launchpad points at the pane; a real path navigates; and "/" resolves to nowhere — which also takes away the "OPEN" chip that was promising a destination the row could not deliver. AND A CLICK NO LONGER DECIDES ANYTHING. It used to mark read and navigate in one motion — two decisions taken from one ambiguous gesture, and for the two kinds a member actually receives most, the visible result was "my notification vanished and I am somewhere else now". A click opens NotificationDetailDialog instead: it names what acting would do, and leaves both choices to the member. Closing it is explicitly the changed-my-mind path. Using the destination does mark it read, because acting on a notification is the strongest evidence there is that it was seen. Also: GROWTH_HUB was missing from the browser's NotificationKind union while the server was already writing it, so those rows had no filter chip and no mute switch — for a kind whose whole reason to exist is being mutable on its own. And EmojiEvents/ReportProblem, the two icons GrowthHubService actually sends, were absent from ICONS and fell back silently. Verified: 42 tests across the three new suites, the full repo typecheck clean, and every guard green including the new one. Design in docs/superpowers/specs/.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing