mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-30 16:20:35 +02:00
The chat window used to fire two parallel session queries (active subset + full list) and surfaced them through two UI entry points (the title dropdown + a History icon panel). The two caches drifted during the WS-invalidate window — visible as "completed → reload → ghost row" flickers — and the History toggle was a redundant entry into the same underlying data. Collapse to one cache (full list, ?status=all) and one entry point (dropdown). The dropdown groups locally into Active / Archived; the archived group is collapsed by default with a count, and per-row delete moves into the dropdown via hover-revealed trash + confirm dialog. Backend stays untouched: old desktop builds still hit GET /chat-sessions without ?status and continue receiving the active subset, so installed clients are unaffected. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
3.6 KiB
TypeScript
103 lines
3.6 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) });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clears the session's unread state server-side. Optimistically flips
|
|
* has_unread to false in the cached list 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) });
|
|
|
|
const prevSessions = qc.getQueryData<ChatSession[]>(chatKeys.sessions(wsId));
|
|
|
|
const clear = (old?: ChatSession[]) =>
|
|
old?.map((s) => (s.id === sessionId ? { ...s, has_unread: false } : s));
|
|
qc.setQueryData<ChatSession[]>(chatKeys.sessions(wsId), clear);
|
|
|
|
return { prevSessions };
|
|
},
|
|
onError: (err, sessionId, ctx) => {
|
|
logger.error("markChatSessionRead.error.rollback", { sessionId, err });
|
|
if (ctx?.prevSessions) qc.setQueryData(chatKeys.sessions(wsId), ctx.prevSessions);
|
|
},
|
|
onSettled: () => {
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hard-deletes a chat session. Optimistically removes the row from the
|
|
* sessions list so the dropdown updates instantly; rolls back on error.
|
|
* The matching `chat:session_deleted` WS event keeps other tabs/devices
|
|
* in sync — see use-realtime-sync.ts.
|
|
*/
|
|
export function useDeleteChatSession() {
|
|
const qc = useQueryClient();
|
|
const wsId = useWorkspaceId();
|
|
|
|
return useMutation({
|
|
mutationFn: (sessionId: string) => {
|
|
logger.info("deleteChatSession.start", { sessionId });
|
|
return api.deleteChatSession(sessionId);
|
|
},
|
|
onMutate: async (sessionId) => {
|
|
await qc.cancelQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
|
|
const prevSessions = qc.getQueryData<ChatSession[]>(chatKeys.sessions(wsId));
|
|
|
|
const drop = (old?: ChatSession[]) => old?.filter((s) => s.id !== sessionId);
|
|
qc.setQueryData<ChatSession[]>(chatKeys.sessions(wsId), drop);
|
|
|
|
logger.debug("deleteChatSession.optimistic", { sessionId });
|
|
return { prevSessions };
|
|
},
|
|
onError: (err, sessionId, ctx) => {
|
|
logger.error("deleteChatSession.error.rollback", { sessionId, err });
|
|
if (ctx?.prevSessions) qc.setQueryData(chatKeys.sessions(wsId), ctx.prevSessions);
|
|
},
|
|
onSettled: (_data, _err, sessionId) => {
|
|
logger.debug("deleteChatSession.settled", { sessionId });
|
|
qc.invalidateQueries({ queryKey: chatKeys.sessions(wsId) });
|
|
},
|
|
});
|
|
}
|