mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-16 14:49:09 +02:00
State management - Pending task / live timeline are now Query-cache single source; Zustand mirror removed (fixes duplicate assistant render caused by the invalidate→refetch race window) - WS subscriptions moved from ChatWindow to global useRealtimeSync so pending state survives minimize and refresh - New GET /chat/sessions/:id/pending-task to recover live state on mount - Drafts persisted per-session (was per-workspace) Unread tracking - Migration 040: chat_session.unread_since (event-driven; old chats stay clean — no mass backfill) - POST /chat/sessions/:id/read clears unread; broadcasts chat:session_read so other devices sync - New GET /chat/pending-tasks aggregate for the FAB - ChatFab: brand-color impulse animation while running, brand-dot badge of unread session count - ChatWindow auto-marks read when user is viewing the session Header redesign - Two independent dropdowns: agent (avatar + name + My/Others grouping) at the input bottom-left; session (title + agent avatar) in the header - ⊕ new-chat button replaces the old + and history buttons - Session dropdown lists all sessions across agents with avatars - Empty state: 3 clickable starter prompts that send immediately - Mention link renderer falls through to default span on null — fixes @member/@agent/@all silently disappearing app-wide - User messages render through Markdown - Enter submits in chat input only (with IME guard + codeBlock skip); bubble menu hidden in chat Misc - Partial index on agent_task_queue for fast pending-task lookup - 2 new storage keys added to clearWorkspaceStorage - useMarkChatSessionRead has onError rollback - chat.* namespace logs across store, mutations, components, realtime Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
4.3 KiB
TypeScript
114 lines
4.3 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { api } from "../api";
|
|
import { useWorkspaceId } from "../hooks";
|
|
import { chatKeys } from "./queries";
|
|
import { createLogger } from "../logger";
|
|
import type { ChatSession } from "../types";
|
|
|
|
const logger = createLogger("chat.mut");
|
|
|
|
export function useCreateChatSession() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceId();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { agent_id: string; title?: string }) => {
|
|
logger.info("createChatSession.start", { agent_id: data.agent_id, titleLength: data.title?.length ?? 0 });
|
|
return api.createChatSession(data);
|
|
},
|
|
onSuccess: (session) => {
|
|
logger.info("createChatSession.success", { sessionId: session.id, agentId: session.agent_id });
|
|
},
|
|
onError: (err) => {
|
|
logger.error("createChatSession.error", err);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clears the session's unread state server-side. Optimistically flips
|
|
* has_unread to false in the cached lists so the FAB badge drops
|
|
* immediately. The server broadcasts chat:session_read so other devices
|
|
* also sync.
|
|
*/
|
|
export function useMarkChatSessionRead() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceId();
|
|
|
|
return useMutation({
|
|
mutationFn: (sessionId: string) => {
|
|
logger.info("markChatSessionRead.start", { sessionId });
|
|
return api.markChatSessionRead(sessionId);
|
|
},
|
|
onMutate: async (sessionId) => {
|
|
await qc.cancelQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
await qc.cancelQueries({ queryKey: chatKeys.allSessions(wsId) });
|
|
|
|
const prevSessions = qc.getQueryData<ChatSession[]>(chatKeys.sessions(wsId));
|
|
const prevAll = qc.getQueryData<ChatSession[]>(chatKeys.allSessions(wsId));
|
|
|
|
const clear = (old?: ChatSession[]) =>
|
|
old?.map((s) => (s.id === sessionId ? { ...s, has_unread: false } : s));
|
|
qc.setQueryData<ChatSession[]>(chatKeys.sessions(wsId), clear);
|
|
qc.setQueryData<ChatSession[]>(chatKeys.allSessions(wsId), clear);
|
|
|
|
return { prevSessions, prevAll };
|
|
},
|
|
onError: (err, sessionId, ctx) => {
|
|
logger.error("markChatSessionRead.error.rollback", { sessionId, err });
|
|
if (ctx?.prevSessions) qc.setQueryData(chatKeys.sessions(wsId), ctx.prevSessions);
|
|
if (ctx?.prevAll) qc.setQueryData(chatKeys.allSessions(wsId), ctx.prevAll);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useArchiveChatSession() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceId();
|
|
|
|
return useMutation({
|
|
mutationFn: (sessionId: string) => {
|
|
logger.info("archiveChatSession.start", { sessionId });
|
|
return api.archiveChatSession(sessionId);
|
|
},
|
|
onMutate: async (sessionId) => {
|
|
await qc.cancelQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
await qc.cancelQueries({ queryKey: chatKeys.allSessions(wsId) });
|
|
|
|
const prevSessions = qc.getQueryData<ChatSession[]>(chatKeys.sessions(wsId));
|
|
const prevAll = qc.getQueryData<ChatSession[]>(chatKeys.allSessions(wsId));
|
|
|
|
// Optimistic: remove from active, mark as archived in allSessions
|
|
qc.setQueryData<ChatSession[]>(chatKeys.sessions(wsId), (old) =>
|
|
old ? old.filter((s) => s.id !== sessionId) : old,
|
|
);
|
|
qc.setQueryData<ChatSession[]>(chatKeys.allSessions(wsId), (old) =>
|
|
old?.map((s) =>
|
|
s.id === sessionId ? { ...s, status: "archived" as const } : s,
|
|
),
|
|
);
|
|
|
|
logger.debug("archiveChatSession.optimistic", { sessionId });
|
|
return { prevSessions, prevAll };
|
|
},
|
|
onError: (err, sessionId, ctx) => {
|
|
logger.error("archiveChatSession.error.rollback", { sessionId, err });
|
|
if (ctx?.prevSessions) qc.setQueryData(chatKeys.sessions(wsId), ctx.prevSessions);
|
|
if (ctx?.prevAll) qc.setQueryData(chatKeys.allSessions(wsId), ctx.prevAll);
|
|
},
|
|
onSettled: (_data, _err, sessionId) => {
|
|
logger.debug("archiveChatSession.settled", { sessionId });
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
qc.invalidateQueries({ queryKey: chatKeys.allSessions(wsId) });
|
|
},
|
|
});
|
|
}
|