bell: notify a chat's page and list row, any chat, any time

Client half of the per-chat notification preference — DetailBar gets a bell
dropdown (every end of turn / only when input is needed / none), a global
useChatNotifications hook holds one connection to /api/chat/notify/ws for
the whole session so a background chat can still ring the bell and fire a
browser Notification, and the session list shows a badge on any row set to
notify. The server side (schema, the notify-delivery socket, the sidecar
signal at turn-end/plan-gate/question-gate) landed already in 5cba1b65,
swept in from a concurrent session sharing this working tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 21:32:06 +00:00
co-authored by Claude Opus 5
parent 5cba1b65a6
commit 73cdc2cb77
7 changed files with 165 additions and 5 deletions
@@ -39,8 +39,12 @@ export type ClaudeSessionSummary = {
model?: string | null;
/** Whether this conversation is in plan mode. Absent on older rows; false is the default everywhere. */
planMode?: boolean;
/** The bell preference — from `chat_notification_prefs`. Absent/`'none'` means no row yet. */
notifyPreference?: NotifyPreference;
};
export type NotifyPreference = 'every_turn' | 'input_needed' | 'none';
// Display-ready message, matching the frontend ChatMessage union (rebuilt from Claude's transcript).
export type ClaudeSessionMessage =
| { role: 'user'; text: string }
@@ -167,6 +171,14 @@ export function useClaudeSessions(cwd?: string | null, options?: { all?: boolean
[client, sessionQ, invalidate],
);
const setNotifyPreference = useCallback(
async (id: string, preference: NotifyPreference) => {
await client.patch(`/chat/sessions/${id}/notify-pref${sessionQ}`, { preference });
invalidate();
},
[client, sessionQ, invalidate],
);
return {
sessions: data?.sessions ?? [],
isLoading,
@@ -177,6 +189,7 @@ export function useClaudeSessions(cwd?: string | null, options?: { all?: boolean
loadSession,
deleteSession,
renameSession,
setNotifyPreference,
invalidate,
};
}