feat: add useChatHistory hook with tests (extracted from ChatUI)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-19 17:56:33 -07:00
parent 3565059d32
commit f3c6915d61
3 changed files with 913 additions and 0 deletions

View File

@ -0,0 +1,506 @@
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useChatHistory } from "./useChatHistory";
describe("useChatHistory", () => {
beforeEach(() => {
sessionStorage.clear();
});
describe("updateTextUI", () => {
it("should create a new assistant message when chat is empty", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
expect(result.current.chatHistory).toEqual([
{ role: "assistant", content: "Hello", model: "gpt-4" },
]);
});
it("should append to the last assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
act(() => {
result.current.updateTextUI("assistant", " world");
});
expect(result.current.chatHistory).toEqual([
{ role: "assistant", content: "Hello world", model: "gpt-4" },
]);
});
it("should not overwrite model on subsequent chunks", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
act(() => {
result.current.updateTextUI("assistant", " world", "gpt-3.5");
});
expect(result.current.chatHistory[0].model).toBe("gpt-4");
});
it("should create a new message when role changes", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("user", "Hi");
});
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
expect(result.current.chatHistory).toHaveLength(2);
expect(result.current.chatHistory[0].role).toBe("user");
expect(result.current.chatHistory[1].role).toBe("assistant");
});
it("should not append to image messages", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateImageUI("http://img.png", "dall-e");
});
act(() => {
result.current.updateTextUI("assistant", "description", "gpt-4");
});
expect(result.current.chatHistory).toHaveLength(2);
});
it("should not append to audio messages", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateAudioUI("http://audio.mp3", "tts-1");
});
act(() => {
result.current.updateTextUI("assistant", "text", "gpt-4");
});
expect(result.current.chatHistory).toHaveLength(2);
});
});
describe("updateReasoningContent", () => {
it("should add reasoning content to existing assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Answer", "gpt-4");
});
act(() => {
result.current.updateReasoningContent("thinking...");
});
expect(result.current.chatHistory[0].reasoningContent).toBe("thinking...");
});
it("should append reasoning content across chunks", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "", "gpt-4");
});
act(() => {
result.current.updateReasoningContent("step 1");
});
act(() => {
result.current.updateReasoningContent(" step 2");
});
expect(result.current.chatHistory[0].reasoningContent).toBe("step 1 step 2");
});
it("should create assistant message with reasoning when last message is user", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.setChatHistory([{ role: "user", content: "question" }]);
});
act(() => {
result.current.updateReasoningContent("thinking...");
});
expect(result.current.chatHistory).toHaveLength(2);
expect(result.current.chatHistory[1]).toEqual({
role: "assistant",
content: "",
reasoningContent: "thinking...",
});
});
it("should not update when chat is empty", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateReasoningContent("thinking...");
});
expect(result.current.chatHistory).toHaveLength(0);
});
});
describe("updateTimingData", () => {
it("should add timeToFirstToken to existing assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
act(() => {
result.current.updateTimingData(150);
});
expect(result.current.chatHistory[0].timeToFirstToken).toBe(150);
});
it("should create assistant message when last is user", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.setChatHistory([{ role: "user", content: "hi" }]);
});
act(() => {
result.current.updateTimingData(200);
});
expect(result.current.chatHistory).toHaveLength(2);
expect(result.current.chatHistory[1].timeToFirstToken).toBe(200);
});
});
describe("updateUsageData", () => {
it("should add usage data to assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
const usage = { completionTokens: 10, promptTokens: 5, totalTokens: 15 };
act(() => {
result.current.updateUsageData(usage);
});
expect(result.current.chatHistory[0].usage).toEqual(usage);
});
it("should add toolName when provided", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
const usage = { completionTokens: 10, promptTokens: 5, totalTokens: 15 };
act(() => {
result.current.updateUsageData(usage, "search_tool");
});
expect(result.current.chatHistory[0].toolName).toBe("search_tool");
});
});
describe("updateTotalLatency", () => {
it("should add totalLatency to assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
act(() => {
result.current.updateTotalLatency(500);
});
expect(result.current.chatHistory[0].totalLatency).toBe(500);
});
});
describe("updateA2AMetadata", () => {
it("should add A2A metadata to assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
const metadata = { taskId: "task-1", contextId: "ctx-1" };
act(() => {
result.current.updateA2AMetadata(metadata);
});
expect(result.current.chatHistory[0].a2aMetadata).toEqual(metadata);
});
});
describe("updateSearchResults", () => {
it("should add search results to assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
});
const searchResults = [{ object: "search", search_query: "test", data: [] }];
act(() => {
result.current.updateSearchResults(searchResults);
});
expect(result.current.chatHistory[0].searchResults).toEqual(searchResults);
});
});
describe("updateImageUI", () => {
it("should add image message to history", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateImageUI("http://img.png", "dall-e-3");
});
expect(result.current.chatHistory).toEqual([
{ role: "assistant", content: "http://img.png", model: "dall-e-3", isImage: true },
]);
});
});
describe("updateEmbeddingsUI", () => {
it("should add truncated embeddings message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateEmbeddingsUI("[0.1, 0.2, 0.3]", "text-embedding-ada");
});
expect(result.current.chatHistory[0].isEmbeddings).toBe(true);
expect(result.current.chatHistory[0].model).toBe("text-embedding-ada");
});
});
describe("updateAudioUI", () => {
it("should add audio message to history", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateAudioUI("http://audio.mp3", "tts-1");
});
expect(result.current.chatHistory).toEqual([
{ role: "assistant", content: "http://audio.mp3", model: "tts-1", isAudio: true },
]);
});
});
describe("updateChatImageUI", () => {
it("should add image to existing assistant message", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Here is the image", "gpt-4");
});
act(() => {
result.current.updateChatImageUI("http://img.png", "gpt-4");
});
expect(result.current.chatHistory[0].image).toEqual({
url: "http://img.png",
detail: "auto",
});
});
it("should create new assistant message with image when no assistant message exists", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateChatImageUI("http://img.png", "gpt-4");
});
expect(result.current.chatHistory[0]).toEqual({
role: "assistant",
content: "",
model: "gpt-4",
image: { url: "http://img.png", detail: "auto" },
});
});
});
describe("handleMCPEvent", () => {
it("should add MCP event", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleMCPEvent({ type: "tool_call", item_id: "1" });
});
expect(result.current.mcpEvents).toHaveLength(1);
});
it("should deduplicate events by item_id and type", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
const event = { type: "tool_call", item_id: "1" };
act(() => {
result.current.handleMCPEvent(event);
});
act(() => {
result.current.handleMCPEvent(event);
});
expect(result.current.mcpEvents).toHaveLength(1);
});
it("should allow events without item_id (no dedup)", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleMCPEvent({ type: "tool_call" });
});
act(() => {
result.current.handleMCPEvent({ type: "tool_call" });
});
expect(result.current.mcpEvents).toHaveLength(2);
});
it("should allow events with same item_id/type but different sequence_number", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleMCPEvent({ type: "tool_call", item_id: "1", sequence_number: 1 });
});
act(() => {
result.current.handleMCPEvent({ type: "tool_call", item_id: "1", sequence_number: 2 });
});
expect(result.current.mcpEvents).toHaveLength(2);
});
});
describe("clearMCPEvents", () => {
it("should clear MCP events without affecting chat history", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
result.current.handleMCPEvent({ type: "tool_call", item_id: "1" });
});
act(() => {
result.current.clearMCPEvents();
});
expect(result.current.mcpEvents).toEqual([]);
expect(result.current.chatHistory).toHaveLength(1);
});
});
describe("clearChatHistory", () => {
it("should clear all state", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateTextUI("assistant", "Hello", "gpt-4");
result.current.handleMCPEvent({ type: "tool_call", item_id: "1" });
});
act(() => {
result.current.clearChatHistory();
});
expect(result.current.chatHistory).toEqual([]);
expect(result.current.mcpEvents).toEqual([]);
expect(result.current.messageTraceId).toBeNull();
expect(result.current.responsesSessionId).toBeNull();
});
it("should revoke audio object URLs when clearing", () => {
const revokeSpy = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {});
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.updateAudioUI("blob:http://localhost/audio-1", "tts-1");
});
act(() => {
result.current.clearChatHistory();
});
expect(revokeSpy).toHaveBeenCalledWith("blob:http://localhost/audio-1");
revokeSpy.mockRestore();
});
it("should clear sessionStorage when not simplified", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
sessionStorage.setItem("chatHistory", "[]");
sessionStorage.setItem("messageTraceId", "trace-1");
sessionStorage.setItem("responsesSessionId", "resp-1");
act(() => {
result.current.clearChatHistory();
});
expect(sessionStorage.getItem("chatHistory")).toBeNull();
expect(sessionStorage.getItem("messageTraceId")).toBeNull();
expect(sessionStorage.getItem("responsesSessionId")).toBeNull();
});
it("should NOT clear sessionStorage when simplified", () => {
sessionStorage.setItem("chatHistory", '[{"role":"user","content":"hi"}]');
const { result } = renderHook(() => useChatHistory({ simplified: true }));
act(() => {
result.current.clearChatHistory();
});
// simplified mode should not touch sessionStorage
expect(sessionStorage.getItem("chatHistory")).toBe('[{"role":"user","content":"hi"}]');
});
});
describe("session management", () => {
it("handleResponseId should set responsesSessionId when useApiSessionManagement is true", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleResponseId("resp-123");
});
expect(result.current.responsesSessionId).toBe("resp-123");
});
it("handleResponseId should NOT set responsesSessionId when useApiSessionManagement is false", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleToggleSessionManagement(false);
});
act(() => {
result.current.handleResponseId("resp-123");
});
expect(result.current.responsesSessionId).toBeNull();
});
it("handleToggleSessionManagement should clear session when switching to UI mode", () => {
const { result } = renderHook(() => useChatHistory({ simplified: false }));
act(() => {
result.current.handleResponseId("resp-123");
});
act(() => {
result.current.handleToggleSessionManagement(false);
});
expect(result.current.useApiSessionManagement).toBe(false);
expect(result.current.responsesSessionId).toBeNull();
});
});
});

View File

@ -0,0 +1,402 @@
import { useState, useEffect } from "react";
import { MessageType, A2ATaskMetadata } from "./types";
import { TokenUsage } from "./ResponseMetrics";
import { MCPEvent } from "../../mcp_tools/types";
import { truncateString } from "../../../utils/textUtils";
export interface UseChatHistoryReturn {
// State
chatHistory: MessageType[];
setChatHistory: React.Dispatch<React.SetStateAction<MessageType[]>>;
mcpEvents: MCPEvent[];
setMCPEvents: React.Dispatch<React.SetStateAction<MCPEvent[]>>;
messageTraceId: string | null;
setMessageTraceId: React.Dispatch<React.SetStateAction<string | null>>;
responsesSessionId: string | null;
setResponsesSessionId: React.Dispatch<React.SetStateAction<string | null>>;
useApiSessionManagement: boolean;
setUseApiSessionManagement: React.Dispatch<React.SetStateAction<boolean>>;
// Actions
updateTextUI: (role: string, chunk: string, model?: string) => void;
updateReasoningContent: (chunk: string) => void;
updateTimingData: (timeToFirstToken: number) => void;
updateUsageData: (usage: TokenUsage, toolName?: string) => void;
updateA2AMetadata: (a2aMetadata: A2ATaskMetadata) => void;
updateTotalLatency: (totalLatency: number) => void;
updateSearchResults: (searchResults: any[]) => void;
handleResponseId: (responseId: string) => void;
handleToggleSessionManagement: (useApi: boolean) => void;
handleMCPEvent: (event: MCPEvent) => void;
updateImageUI: (imageUrl: string, model: string) => void;
updateEmbeddingsUI: (embeddings: string, model?: string) => void;
updateAudioUI: (audioUrl: string, model: string) => void;
updateChatImageUI: (imageUrl: string, model?: string) => void;
clearChatHistory: () => void;
clearMCPEvents: () => void;
}
export function useChatHistory({ simplified }: { simplified: boolean }): UseChatHistoryReturn {
const [chatHistory, setChatHistory] = useState<MessageType[]>(() => {
if (simplified) return [];
try {
const saved = sessionStorage.getItem("chatHistory");
return saved ? JSON.parse(saved) : [];
} catch (error) {
console.error("Error parsing chatHistory from sessionStorage", error);
return [];
}
});
const [mcpEvents, setMCPEvents] = useState<MCPEvent[]>([]);
const [messageTraceId, setMessageTraceId] = useState<string | null>(
() => sessionStorage.getItem("messageTraceId") || null,
);
const [responsesSessionId, setResponsesSessionId] = useState<string | null>(
() => sessionStorage.getItem("responsesSessionId") || null,
);
const [useApiSessionManagement, setUseApiSessionManagement] = useState<boolean>(() => {
const saved = sessionStorage.getItem("useApiSessionManagement");
return saved ? JSON.parse(saved) : true; // Default to API session management
});
// Debounced chatHistory persistence
useEffect(() => {
if (simplified) return; // Do not persist chat history in simplified (embedded) mode
const handler = setTimeout(() => {
sessionStorage.setItem("chatHistory", JSON.stringify(chatHistory));
}, 500); // Debounce by 500ms
return () => {
clearTimeout(handler);
};
}, [chatHistory, simplified]);
// messageTraceId/responsesSessionId/useApiSessionManagement persistence
useEffect(() => {
if (messageTraceId) {
sessionStorage.setItem("messageTraceId", messageTraceId);
} else {
sessionStorage.removeItem("messageTraceId");
}
if (responsesSessionId) {
sessionStorage.setItem("responsesSessionId", responsesSessionId);
} else {
sessionStorage.removeItem("responsesSessionId");
}
sessionStorage.setItem("useApiSessionManagement", JSON.stringify(useApiSessionManagement));
}, [messageTraceId, responsesSessionId, useApiSessionManagement]);
const updateTextUI = (role: string, chunk: string, model?: string) => {
console.log("updateTextUI called with:", role, chunk, model);
setChatHistory((prev) => {
const last = prev[prev.length - 1];
// if the last message is already from this same role, append
if (last && last.role === role && !last.isImage && !last.isAudio) {
// build a new object, but only set `model` if it wasn't there already
const updated: MessageType = {
...last,
content: last.content + chunk,
model: last.model ?? model, // ← only use the passedin model on the first chunk
};
return [...prev.slice(0, -1), updated];
} else {
// otherwise start a brand new assistant bubble
return [
...prev,
{
role,
content: chunk,
model, // model set exactly once here
},
];
}
});
};
const updateReasoningContent = (chunk: string) => {
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant" && !lastMessage.isImage && !lastMessage.isAudio) {
return [
...prevHistory.slice(0, prevHistory.length - 1),
{
...lastMessage,
reasoningContent: (lastMessage.reasoningContent || "") + chunk,
},
];
} else {
// If there's no assistant message yet, we'll create one with empty content
// but with reasoning content
if (prevHistory.length > 0 && prevHistory[prevHistory.length - 1].role === "user") {
return [
...prevHistory,
{
role: "assistant",
content: "",
reasoningContent: chunk,
},
];
}
return prevHistory;
}
});
};
const updateTimingData = (timeToFirstToken: number) => {
console.log("updateTimingData called with:", timeToFirstToken);
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
console.log("Current last message:", lastMessage);
if (lastMessage && lastMessage.role === "assistant") {
console.log("Updating assistant message with timeToFirstToken:", timeToFirstToken);
const updatedHistory = [
...prevHistory.slice(0, prevHistory.length - 1),
{
...lastMessage,
timeToFirstToken,
},
];
console.log("Updated chat history:", updatedHistory);
return updatedHistory;
}
// If the last message is a user message and no assistant message exists yet,
// create a new assistant message with empty content
else if (lastMessage && lastMessage.role === "user") {
console.log("Creating new assistant message with timeToFirstToken:", timeToFirstToken);
return [
...prevHistory,
{
role: "assistant",
content: "",
timeToFirstToken,
},
];
}
console.log("No appropriate message found to update timing");
return prevHistory;
});
};
const updateUsageData = (usage: TokenUsage, toolName?: string) => {
console.log("Received usage data:", usage);
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
console.log("Updating message with usage data:", usage);
const updatedMessage = {
...lastMessage,
usage,
toolName,
};
console.log("Updated message:", updatedMessage);
return [...prevHistory.slice(0, prevHistory.length - 1), updatedMessage];
}
return prevHistory;
});
};
const updateA2AMetadata = (a2aMetadata: A2ATaskMetadata) => {
console.log("Received A2A metadata:", a2aMetadata);
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
const updatedMessage = {
...lastMessage,
a2aMetadata,
};
return [...prevHistory.slice(0, prevHistory.length - 1), updatedMessage];
}
return prevHistory;
});
};
const updateTotalLatency = (totalLatency: number) => {
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
return [
...prevHistory.slice(0, prevHistory.length - 1),
{
...lastMessage,
totalLatency,
},
];
}
return prevHistory;
});
};
const updateSearchResults = (searchResults: any[]) => {
console.log("Received search results:", searchResults);
setChatHistory((prevHistory) => {
const lastMessage = prevHistory[prevHistory.length - 1];
if (lastMessage && lastMessage.role === "assistant") {
console.log("Updating message with search results");
const updatedMessage = {
...lastMessage,
searchResults,
};
return [...prevHistory.slice(0, prevHistory.length - 1), updatedMessage];
}
return prevHistory;
});
};
const handleResponseId = (responseId: string) => {
console.log("Received response ID for session management:", responseId);
if (useApiSessionManagement) {
setResponsesSessionId(responseId);
}
};
const handleToggleSessionManagement = (useApi: boolean) => {
setUseApiSessionManagement(useApi);
if (!useApi) {
// Clear API session when switching to UI mode
setResponsesSessionId(null);
}
};
const handleMCPEvent = (event: MCPEvent) => {
console.log("ChatUI: Received MCP event:", event);
setMCPEvents((prev) => {
// Check if this is a duplicate event (same item_id and type)
// Only check for duplicates if item_id is defined (for mcp_list_tools, item_id is "mcp_list_tools")
const isDuplicate = event.item_id
? prev.some(
(existingEvent) =>
existingEvent.item_id === event.item_id &&
existingEvent.type === event.type &&
(existingEvent.sequence_number === event.sequence_number ||
(existingEvent.sequence_number === undefined && event.sequence_number === undefined)),
)
: false;
if (isDuplicate) {
console.log("ChatUI: Duplicate MCP event, skipping");
return prev;
}
const newEvents = [...prev, event];
console.log("ChatUI: Updated MCP events:", newEvents);
return newEvents;
});
};
const updateImageUI = (imageUrl: string, model: string) => {
setChatHistory((prevHistory) => [...prevHistory, { role: "assistant", content: imageUrl, model, isImage: true }]);
};
const updateEmbeddingsUI = (embeddings: string, model?: string) => {
setChatHistory((prevHistory) => [
...prevHistory,
{ role: "assistant", content: truncateString(embeddings, 100), model, isEmbeddings: true },
]);
};
const updateAudioUI = (audioUrl: string, model: string) => {
setChatHistory((prevHistory) => [...prevHistory, { role: "assistant", content: audioUrl, model, isAudio: true }]);
};
const updateChatImageUI = (imageUrl: string, model?: string) => {
setChatHistory((prev) => {
const last = prev[prev.length - 1];
// If the last message is from assistant and has content, add image to it
if (last && last.role === "assistant" && !last.isImage && !last.isAudio) {
const updated = {
...last,
image: {
url: imageUrl,
detail: "auto",
},
model: last.model ?? model,
};
return [...prev.slice(0, -1), updated];
} else {
// Otherwise create a new assistant message with just the image
return [
...prev,
{
role: "assistant",
content: "",
model,
image: {
url: imageUrl,
detail: "auto",
},
},
];
}
});
};
const clearChatHistory = () => {
// Clean up audio object URLs before clearing history
chatHistory.forEach((message) => {
if (message.isAudio && typeof message.content === "string") {
URL.revokeObjectURL(message.content);
}
});
setChatHistory([]);
setMessageTraceId(null);
setResponsesSessionId(null); // Clear responses session ID
setMCPEvents([]); // Clear MCP events
if (!simplified) {
sessionStorage.removeItem("chatHistory");
sessionStorage.removeItem("messageTraceId");
sessionStorage.removeItem("responsesSessionId");
}
};
const clearMCPEvents = () => {
setMCPEvents([]);
};
return {
chatHistory,
setChatHistory,
mcpEvents,
setMCPEvents,
messageTraceId,
setMessageTraceId,
responsesSessionId,
setResponsesSessionId,
useApiSessionManagement,
setUseApiSessionManagement,
updateTextUI,
updateReasoningContent,
updateTimingData,
updateUsageData,
updateA2AMetadata,
updateTotalLatency,
updateSearchResults,
handleResponseId,
handleToggleSessionManagement,
handleMCPEvent,
updateImageUI,
updateEmbeddingsUI,
updateAudioUI,
updateChatImageUI,
clearChatHistory,
clearMCPEvents,
};
}

View File

@ -100,6 +100,11 @@ if (!document.getAnimations) {
document.getAnimations = () => [];
}
// Stub URL.revokeObjectURL so vi.spyOn can intercept it in tests
if (!URL.revokeObjectURL) {
URL.revokeObjectURL = () => {};
}
// Mock ResizeObserver for components that use it (e.g., Tremor UI components)
// This prevents "ResizeObserver is not defined" errors in JSDOM
global.ResizeObserver = class ResizeObserver {