diff --git a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
index 82908fd8..2b8a1cb6 100644
--- a/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
+++ b/src/apps/officer-web/Screens/Dashboard/Layout/DashboardLayout.tsx
@@ -1,6 +1,6 @@
import { useMemo, useRef } from 'react';
import { useLocation } from 'react-router';
-import { useDock, usePanelFullscreen } from 'officerdev';
+import { useDock, usePanelFullscreen, useChatNotifications } from 'officerdev';
import { usePermissions } from 'hooks/usePermissions';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { ScreenErrorFallback } from './ScreenErrorFallback';
@@ -33,6 +33,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const isTouch = useIsTouch();
const { pathname } = useLocation();
usePageTitleSync();
+ useChatNotifications();
// A panel can maximize into the content region on its own, but it cannot paint over this header — the
// region below is an `absolute z-2` stacking context and the header is a `fixed z-10` sibling of it. So
// "full screen" is cooperative: the panel asks, and the chrome steps aside.
diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx
index c91db337..2eefff85 100644
--- a/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx
+++ b/src/workspaces/officerdev/src/apps/ChatHistory/ChatDetailPanel.tsx
@@ -3,11 +3,13 @@ import { useLocation, useParams } from 'react-router';
import { Unplug } from 'lucide-react';
import { toast } from '@/components/ui/sonner';
import { EditableTitle } from '@/components/EditableTitle';
+import { NotifyMenu } from './NotifyMenu';
import { useSelectedChatSession } from '../../channels';
import { usePublishPageTitle } from '../../page-title';
import { useAuth } from 'hooks/useAuth';
import { errorText } from 'helpers/error-text';
import { useClaudeSessions } from 'state/useClaudeSessions';
+import type { NotifyPreference } from 'state/useClaudeSessions';
import { useChat, EmbeddableChat } from '../Chat';
import type { ChatMessage } from '../Chat/types';
@@ -32,6 +34,7 @@ export type SelectedSession = {
partCount?: number;
/** Whether this conversation was left in plan mode, so the composer toggle comes back lit. */
planMode?: boolean;
+ notifyPreference?: NotifyPreference;
} | null;
type ChatLocationState = {
@@ -56,9 +59,18 @@ type DetailBarProps = {
isConnected: boolean;
isGenerating: boolean;
onDisconnect?: () => void;
+ notifyPreference?: NotifyPreference;
};
-function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, onDisconnect }: DetailBarProps) {
+function DetailBar({
+ sessionTitle,
+ cwd,
+ partCount,
+ isConnected,
+ isGenerating,
+ onDisconnect,
+ notifyPreference,
+}: DetailBarProps) {
// The id to rename comes from the URL, not from `resumeSessionId`. They are the same for an ordinary
// conversation and not for a merged `/clear` chain, where the resume target is the tail transcript
// while the list, the links and the server all address the chain by its head. Renaming the tail would
@@ -69,7 +81,7 @@ function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, on
// The same rename the list's pencil calls, so both surfaces write one place and the invalidation that
// follows refreshes the row too. A failure toasts rather than reverting silently, matching `SessionList`.
- const { renameSession } = useClaudeSessions(cwd);
+ const { renameSession, setNotifyPreference } = useClaudeSessions(cwd);
const commitRename = useCallback(
async (next: string) => {
if (!sessionId) return;
@@ -82,6 +94,18 @@ function DetailBar({ sessionTitle, cwd, partCount, isConnected, isGenerating, on
[renameSession, sessionId],
);
+ const commitNotifyPreference = useCallback(
+ async (preference: NotifyPreference) => {
+ if (!sessionId) return;
+ try {
+ await setNotifyPreference(sessionId, preference);
+ } catch (err) {
+ toast.error(errorText(err));
+ }
+ },
+ [setNotifyPreference, sessionId],
+ );
+
return (