- Shipped
- September 6, 2026 at 6:24 PM UTC
- Author
- Kamo
- Commit
- 297c21f
ToolDock is mounted in ToolProviders ABOVE AuthedChrome, deliberately, so it never unmounts across a navigation -- the single fact that keeps a member's typing alive in every tool window. UnreadProvider lives inside AuthedChrome, which makes it a DESCENDANT of the dock. So useUnread() inside a tool window does not throw. UnreadContext is built with a complete default -- getUnread: () => 0, bySurface.sms: 0 -- and everything built on it renders, looks correct, and reports zero forever. Two live consequences: - The softphone's new TEXTS badge would never once have appeared. - SmsTool's markSmsRead call, added specifically so the SMS badge could go DOWN as well as up, resolved to the context default: an already-resolved promise that did nothing. The fix has been shipped and inert. New smsUnreadStore + SmsUnreadSync are the same bridge ToolWindowUnreadSync and HexHeadUnreadSync already build for the tab strip and the hexheads: a provider-free module store read with useSyncExternalStore, fed by a component mounted inside AuthenticatedToolWrapper where the provider is real. Mark-read travels the other way as a CustomEvent, which is how every dock-to-provider message already works. UnreadContext gains `smsSessions` because a reader that has to ENUMERATE unread conversations cannot do it through getUnread, which answers one key at a time and needs the id it is being asked about. The SmsTool test was pinning the broken mechanism -- it mocked useUnread and asserted markSmsRead was called, which passes whether or not the provider is reachable at runtime. It now drives the store and asserts the outbound event, so it fails if anything reaches back for the dead context.