feat(desktop): add macOS dock badge for chats with unseen activity
Show a count of chats (root sessions) with unseen activity on the macOS dock icon. The count is computed in the existing tray snapshot (full cross-project list, not the capped tray view; a subtask's unseen rolls up to its root only when subtask notifications are enabled) and pushed to the main process over the existing desktop_tray_update IPC, which calls app.setBadgeCount (0 clears it). The badge clears as sessions are marked seen on window focus. Add a Dock badge toggle in Appearance settings (default on, persisted, darwin desktop only), localized across all dictionaries, with a matching settings-search entry whose availability mirrors the render guard exactly.
This commit is contained in:
@@ -19,6 +19,7 @@ import { QUOTA_PROVIDERS, formatWindowLabel, formatQuotaValueLabel } from '@/lib
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { resolveProjectForSessionDirectory, normalizeProjectPath } from '@/lib/projectResolution';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
import type { WorktreeMetadata } from '@/types/worktree';
|
||||
@@ -80,6 +81,9 @@ type TraySnapshot = {
|
||||
// dropdown (same "configured to show" rule as the header/mobile). Empty
|
||||
// groups → the tray omits the Usage submenu entirely.
|
||||
usage: TrayUsage;
|
||||
// Number of chats (root sessions) with unseen activity, for the macOS dock
|
||||
// badge. 0 when the user disabled the badge — the main process clears it.
|
||||
dockBadgeCount: number;
|
||||
};
|
||||
|
||||
// focus-session / new-session are routed natively by the main process through
|
||||
@@ -389,7 +393,26 @@ const buildSnapshot = (instanceName: string): TraySnapshot => {
|
||||
|
||||
const approvals = live.approvals.map((a) => ({ ...a, sessionTitle: titleById.get(a.sessionId) || '' }));
|
||||
|
||||
return { sessions, approvals, instanceName, usage: buildUsage() };
|
||||
// Dock badge: count chats (root sessions) with unseen activity over the FULL
|
||||
// cross-project list — not the MAX_SESSIONS-capped `sessions` above — so the
|
||||
// number is accurate even with many projects. A subtask's unseen rolls up to
|
||||
// its root only when the user opted into subtask notifications, matching the
|
||||
// sidebar's needs-attention rule.
|
||||
const ui = useUIStore.getState();
|
||||
let dockBadgeCount = 0;
|
||||
if (ui.dockBadgeEnabled) {
|
||||
for (const session of allSessions) {
|
||||
if (!session?.id || session.parentID) continue; // roots only
|
||||
let familyUnseen = notif.unseenCount[session.id] ?? 0;
|
||||
if (familyUnseen === 0 && ui.notifyOnSubtasks) {
|
||||
familyUnseen = collectDescendants(session.id)
|
||||
.reduce((sum, id) => sum + (notif.unseenCount[id] ?? 0), 0);
|
||||
}
|
||||
if (familyUnseen > 0) dockBadgeCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { sessions, approvals, instanceName, usage: buildUsage(), dockBadgeCount };
|
||||
};
|
||||
|
||||
export const useTraySync = (): void => {
|
||||
@@ -491,6 +514,9 @@ export const useTraySync = (): void => {
|
||||
const unsubscribeProjects = useProjectsStore.subscribe(() => scheduleFlush());
|
||||
const unsubscribeWorktrees = useSessionUIStore.subscribe(() => scheduleFlush());
|
||||
const unsubscribeGit = useGitStore.subscribe(() => scheduleFlush());
|
||||
// The dock-badge toggle and subtask-notification preference live here; a
|
||||
// change must re-push the snapshot so the badge appears/clears immediately.
|
||||
const unsubscribeUI = useUIStore.subscribe(() => scheduleFlush());
|
||||
// Cross-project status map: fed live by the sync dispatcher from the global
|
||||
// event stream, and seeded/reconciled by the poll below.
|
||||
const unsubscribeGlobalStatus = useGlobalSessionStatusStore.subscribe(() => scheduleFlush());
|
||||
@@ -541,6 +567,7 @@ export const useTraySync = (): void => {
|
||||
unsubscribeProjects();
|
||||
unsubscribeWorktrees();
|
||||
unsubscribeGit();
|
||||
unsubscribeUI();
|
||||
unsubscribeGlobalStatus();
|
||||
unsubscribeQuota();
|
||||
unsubscribeRegistry?.();
|
||||
|
||||
Reference in New Issue
Block a user