fix(webui): keep new chat during session refresh

This commit is contained in:
hanyuanling 2026-05-21 10:59:15 +08:00 committed by Xubin Ren
parent 3d3ebf1110
commit de0a8f5e41
2 changed files with 62 additions and 1 deletions

View File

@ -27,13 +27,25 @@ export function useSessions(): {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const tokenRef = useRef(token);
const optimisticKeysRef = useRef<Set<string>>(new Set());
tokenRef.current = token;
const refresh = useCallback(async () => {
try {
setLoading(true);
const rows = await listSessions(tokenRef.current);
setSessions(rows);
const serverKeys = new Set(rows.map((row) => row.key));
setSessions((prev) => [
...rows,
...prev.filter(
(session) =>
optimisticKeysRef.current.has(session.key) &&
!serverKeys.has(session.key),
),
]);
for (const key of Array.from(optimisticKeysRef.current)) {
if (serverKeys.has(key)) optimisticKeysRef.current.delete(key);
}
setError(null);
} catch (e) {
const msg =
@ -57,6 +69,7 @@ export function useSessions(): {
const createChat = useCallback(async (): Promise<string> => {
const chatId = await client.newChat();
const key = `websocket:${chatId}`;
optimisticKeysRef.current.add(key);
// Optimistic insert; a subsequent refresh will replace it with the
// authoritative row once the server persists the session.
setSessions((prev) => [
@ -77,6 +90,7 @@ export function useSessions(): {
const deleteChat = useCallback(
async (key: string) => {
await apiDeleteSession(tokenRef.current, key);
optimisticKeysRef.current.delete(key);
setSessions((prev) => prev.filter((s) => s.key !== key));
},
[],

View File

@ -157,6 +157,53 @@ describe("useSessions", () => {
expect(api.listSessions).toHaveBeenCalledTimes(2);
});
it("keeps a newly created chat visible until the server session list catches up", async () => {
vi.mocked(api.listSessions)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
key: "websocket:chat-new",
channel: "websocket",
chatId: "chat-new",
createdAt: "2026-05-20T10:00:00Z",
updatedAt: "2026-05-20T10:01:00Z",
title: "Generated title",
preview: "First message",
},
]);
const client = fakeClient();
client.newChat.mockResolvedValue("chat-new");
const { result } = renderHook(() => useSessions(), {
wrapper: wrap(client),
});
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.sessions).toEqual([]);
await act(async () => {
await result.current.createChat();
});
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-new"]);
await act(async () => {
await result.current.refresh();
});
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-new"]);
expect(result.current.sessions[0]?.preview).toBe("");
await act(async () => {
await result.current.refresh();
});
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-new"]);
expect(result.current.sessions[0]?.preview).toBe("First message");
expect(result.current.sessions[0]?.title).toBe("Generated title");
});
it("passes through WebUI transcript user media as images and media", async () => {
vi.mocked(api.fetchWebuiThread).mockResolvedValue({
schemaVersion: 3,