* feat(desktop): dock unread badge + focus-gated inbox notifications
Wire two OS-level integrations for inbox activity. Both degrade cleanly on
web and unsupported platforms.
- Unread badge on the macOS dock / Linux Unity launcher. Derived from the
same inbox list the UI renders, deduplicated per issue, capped as "99+"
on macOS via `app.dock.setBadge` (setBadgeCount truncates at 99). New
`useInboxUnreadCount` hook (core/inbox) + `useDesktopUnreadBadge`
(views/platform) keep renderer and main in sync via a `badge:set` IPC.
- Native OS notification on `inbox:new`, fired from the renderer only when
`document.hasFocus()` is false — in-focus feedback is the existing inbox
sidebar's unread styling, so we don't fight macOS's deliberate foreground
suppression. Clicking the banner focuses the main window and navigates
to `/inbox?issue=<key>` via the shared `multica:navigate` bus.
Refactors `inbox-page.tsx` to read the unread count through the new hook
(was a per-render inline filter).
* fix(desktop): pin notification routing to source workspace + mark read on URL select
Two bugs GPT-Boy caught on PR #1445:
1. A notification from workspace A used `getCurrentSlug()` at click time,
so if the user switched to workspace B before clicking the banner (macOS
Notification Center persists banners), routing landed on `/B/inbox?issue=<A key>`
and 404'd. Fix: round-trip the emit-time `slug` through the IPC payload
and use it in the click handler.
2. Notification-click navigation set the URL param but never fired the
mark-read mutation (only InboxPage's click-handler did). The row stayed
unread and the dock badge didn't decrement. Fix: move the mark-read
logic from handleSelect into a useEffect keyed on the selected item —
it now covers both click-to-select and URL-param-select.
IPC payload gains `slug` and `itemId`; preload types + main handler + the
desktop bridge are updated to match.
* fix(inbox): don't archive after deleting an issue
Deleting an issue from the Inbox page was calling the archive API on the
inbox item right after deleteIssue succeeded. Because the inbox_item row
has ON DELETE CASCADE on issue_id, it was already gone by then and the
archive call 404'd with "inbox item not found", surfacing a "Failed to
archive" toast.
Drop the redundant archive call and invalidate the inbox cache through
the issue:deleted WS handler so every tab stays in sync without an extra
round trip.
* fix(inbox): keep stale selection on /inbox instead of the deleted issue
When another tab deletes the selected inbox issue, onInboxIssueDeleted
prunes the cache and `selected` becomes null. The existing fallback then
redirected to the issue detail page — which is also gone, so the user
landed on a "This issue does not exist..." screen instead of back in the
inbox list.
Track the last key that actually resolved against the inbox list. If it
used to be in the list and just disappeared, clear the selection and
stay on /inbox. Only shared links that were never in the user's inbox
continue to fall back to the issue detail page.
Also add ws-updaters tests covering onInboxIssueDeleted and
onInboxIssueStatusChanged.