feat(FN-3604): add chat unread indicator to header and mobile nav

Implements a chat unread indicator across the header and mobile nav bar, driven by a new unread-response tracker in the app state, with tests and a docs update for the feature.

Fusion-Task-Id: FN-3604
This commit is contained in:
Fusion
2026-05-06 18:46:45 -07:00
committed by gsxdsm
parent c5ea44a41d
commit d370f305e9
9 changed files with 202 additions and 3 deletions

View File

@@ -115,6 +115,7 @@ function prefetchLazyViews() {
}
const SETUP_WARNING_DISMISSED_KEY = "kb-setup-warning-dismissed";
const ACTIVE_CHAT_SESSION_STORAGE_KEY = "kb-chat-active-session";
function buildRemoteDashboardUrl(serverUrl: string, authToken?: string | null): string {
const url = new URL(serverUrl);
@@ -345,8 +346,9 @@ function AppInner() {
// via useMobileScrollLock — the reference-counted hook handles overlap.
useMobileScrollLock(mobileKeyboardOpen);
// App-level mailbox unread count state (used for header/mobile nav badges)
// App-level mailbox/chat unread state (used for header/mobile nav badges)
const [mailboxUnreadCount, setMailboxUnreadCount] = useState(0);
const [chatHasUnreadResponse, setChatHasUnreadResponse] = useState(false);
const refreshMailboxUnreadCount = useCallback(() => {
fetchUnreadCount(currentProject?.id)
@@ -378,6 +380,39 @@ function AppInner() {
});
}, [currentProject?.id, refreshMailboxUnreadCount]);
useEffect(() => {
if (taskView === "chat") {
setChatHasUnreadResponse(false);
}
}, [taskView]);
useEffect(() => {
const params = new URLSearchParams();
if (currentProject?.id) {
params.set("projectId", currentProject.id);
}
const query = params.size > 0 ? `?${params.toString()}` : "";
return subscribeSse(`/api/events${query}`, {
events: {
"chat:message:added": (event: MessageEvent) => {
try {
const payload = JSON.parse(event.data) as { role?: string; sessionId?: string; projectId?: string | null };
const activeSessionId = getScopedItem(ACTIVE_CHAT_SESSION_STORAGE_KEY, currentProject?.id);
if (!activeSessionId) return;
if (payload.role !== "assistant") return;
if (taskView === "chat") return;
if (payload.sessionId !== activeSessionId) return;
if (payload.projectId && currentProject?.id && payload.projectId !== currentProject.id) return;
setChatHasUnreadResponse(true);
} catch {
// no-op
}
},
},
});
}, [currentProject?.id, taskView]);
// Nodes management is an overlay view (not a modal), so it stays local to App.
const [nodesOpen, setNodesOpen] = useState(false);
const [retryingProjects, setRetryingProjects] = useState(false);
@@ -1193,6 +1228,7 @@ function AppInner() {
onOpenSystemStats={openSystemStatsWithNav}
onOpenMailbox={() => handleTaskViewChange("mailbox")}
mailboxUnreadCount={mailboxUnreadCount}
chatHasUnreadResponse={chatHasUnreadResponse}
onOpenSchedules={openSchedulesWithNav}
onOpenGitManager={openGitManagerWithNav}
onOpenNodes={handleOpenNodesWithNav}
@@ -1314,6 +1350,7 @@ function AppInner() {
onOpenMailbox={() => handleTaskViewChange("mailbox")}
onOpenNodes={handleOpenNodesWithNav}
mailboxUnreadCount={mailboxUnreadCount}
chatHasUnreadResponse={chatHasUnreadResponse}
onOpenGitManager={openGitManagerWithNav}
onOpenWorkflowSteps={openWorkflowStepsWithNav}
onOpenSchedules={openSchedulesWithNav}