fix(webui): derive temporary chats from session policy

This commit is contained in:
chengyongru
2026-08-07 17:08:22 +08:00
parent 36253685bd
commit f971d7e895
13 changed files with 647 additions and 185 deletions
+58 -39
View File
@@ -64,8 +64,6 @@ import {
import { projectNameFromPath, scopeWithAccessMode } from "@/lib/workspace";
import {
createTemporaryChatSession,
isTemporaryChatId,
temporaryChatIdFromSessionKey,
} from "@/lib/temporary-chat";
type BootState =
@@ -101,6 +99,7 @@ type ShellRoute = {
view: ShellView;
activeKey: string | null;
settingsSection: SettingsSectionKey;
temporary?: boolean;
};
const loadSettingsView = () => import("@/components/settings/SettingsView");
const SettingsView = lazy(async () => {
@@ -235,11 +234,12 @@ function readShellRoute(): ShellRoute {
const encoded = path.slice("/temporary/".length);
try {
const chatId = decodeURIComponent(encoded).trim();
return isTemporaryChatId(chatId)
return chatId
? {
view: "chat",
activeKey: `websocket:${chatId}`,
settingsSection: "overview",
temporary: true,
}
: defaultShellRoute();
} catch {
@@ -262,8 +262,10 @@ function readShellRoute(): ShellRoute {
function shellRouteHash(route: ShellRoute): string {
if (route.view === "chat") {
const temporaryChatId = temporaryChatIdFromSessionKey(route.activeKey);
if (temporaryChatId) return `#/temporary/${encodeURIComponent(temporaryChatId)}`;
if (route.temporary && route.activeKey?.startsWith("websocket:")) {
const chatId = route.activeKey.slice("websocket:".length);
return `#/temporary/${encodeURIComponent(chatId)}`;
}
return route.activeKey
? `#/chat/${encodeURIComponent(route.activeKey)}`
: "#/new";
@@ -1034,7 +1036,8 @@ function Shell({
settingsSnapshot?.surface ?? settingsSnapshot?.runtime_surface ?? runtimeSurface;
const showHostChrome = effectiveRuntimeSurface === "native";
const showMainSidebar = view !== "settings";
const temporaryChatId = temporaryChatIdFromSessionKey(activeKey);
const activeTemporarySession = activeKey ? temporarySessions[activeKey] ?? null : null;
const temporaryChatId = activeTemporarySession?.chatId ?? null;
const temporaryChatActive = view === "chat" && temporaryChatId !== null;
const temporaryChatRequested = temporaryChatActive || temporaryChatEnabled;
const temporarySessionList = useMemo(
@@ -1173,9 +1176,7 @@ function Shell({
const activeSession = useMemo<ChatSummary | null>(() => {
if (!activeKey) return null;
if (temporaryChatIdFromSessionKey(activeKey)) {
return temporarySessions[activeKey] ?? null;
}
if (temporarySessions[activeKey]) return temporarySessions[activeKey];
return sessions.find((s) => s.key === activeKey) ?? null;
}, [sessions, activeKey, temporarySessions]);
const runningChatIdList = useMemo(() => Array.from(runningChatIds), [runningChatIds]);
@@ -1249,7 +1250,8 @@ function Shell({
pendingCreatedSessionKeyRef.current = null;
}
if (!activeKey) return;
if (temporaryChatIdFromSessionKey(activeKey)) {
const currentRoute = readShellRoute();
if (currentRoute.temporary) {
if (temporarySessions[activeKey]) return;
navigate(defaultShellRoute(), { replace: true });
return;
@@ -1258,7 +1260,6 @@ function Shell({
// WebKit can commit the route before useSessions' optimistic insert.
// Keep that just-created destination valid until the session list catches up.
if (pendingCreatedKey === activeKey) return;
const currentRoute = readShellRoute();
navigate(
currentRoute.view === "chat"
? defaultShellRoute()
@@ -1478,31 +1479,38 @@ function Shell({
workspaceScope?: WorkspaceScopePayload | null,
initialMessage?: string,
) => {
const session = createTemporaryChatSession();
const restrictedScope = workspaceScope
? normalizeWorkspaceScope(scopeWithAccessMode(workspaceScope, "restricted"))
: null;
const nextSession: ChatSummary = {
...session,
preview: initialMessage ?? "",
...(restrictedScope ? { workspaceScope: restrictedScope } : {}),
};
setTemporarySessions((current) => ({
...current,
[nextSession.key]: nextSession,
}));
setTemporaryChatEnabled(false);
setWorkspaceError(null);
setSessionSearchOpen(false);
navigate({
view: "chat",
activeKey: nextSession.key,
settingsSection: "overview",
});
setMobileSidebarOpen(false);
return nextSession.chatId;
try {
const chatId = await client.newTemporaryChat();
const session = createTemporaryChatSession(chatId);
const restrictedScope = workspaceScope
? normalizeWorkspaceScope(scopeWithAccessMode(workspaceScope, "restricted"))
: null;
const nextSession: ChatSummary = {
...session,
preview: initialMessage ?? "",
...(restrictedScope ? { workspaceScope: restrictedScope } : {}),
};
setTemporarySessions((current) => ({
...current,
[nextSession.key]: nextSession,
}));
setTemporaryChatEnabled(false);
setWorkspaceError(null);
setSessionSearchOpen(false);
navigate({
view: "chat",
activeKey: nextSession.key,
settingsSection: "overview",
temporary: true,
});
setMobileSidebarOpen(false);
return nextSession.chatId;
} catch (error) {
console.error("Failed to create temporary chat", error);
return null;
}
},
[navigate],
[client, navigate],
);
const onForkChat = useCallback(async (
@@ -1572,7 +1580,8 @@ function Shell({
const onSelectChat = useCallback(
(key: string) => {
const selected = temporarySessionsRef.current[key]
const selectedTemporary = temporarySessionsRef.current[key];
const selected = selectedTemporary
?? sessions.find((session) => session.key === key);
const selectedChatId = selected?.chatId;
if (selectedChatId) {
@@ -1589,7 +1598,12 @@ function Shell({
setDraftWorkspaceScope(null);
}
setWorkspaceError(null);
navigate({ view: "chat", activeKey: key, settingsSection: "overview" });
navigate({
view: "chat",
activeKey: key,
settingsSection: "overview",
...(selectedTemporary ? { temporary: true } : {}),
});
setMobileSidebarOpen(false);
},
[navigate, sessions],
@@ -1610,6 +1624,7 @@ function Shell({
view: "chat",
activeKey: remaining[0]?.key ?? null,
settingsSection: "overview",
...(remaining[0] ? { temporary: true } : {}),
}, { replace: true });
}
setMobileSidebarOpen(false);
@@ -1897,7 +1912,11 @@ function Shell({
nextRunning.delete(chatId);
runningChatIdsRef.current = nextRunning;
setRunningChatIds(nextRunning);
if (isTemporaryChatId(chatId)) return;
if (
Object.values(temporarySessionsRef.current).some(
(session) => session.chatId === chatId,
)
) return;
setUpdatedChatIds((current) => {
const next = new Set(current);
if (activeChatIdRef.current === chatId) {
@@ -1922,7 +1941,7 @@ function Shell({
if (Object.keys(temporarySessionsRef.current).length === 0) return;
temporarySessionsRef.current = {};
setTemporarySessions({});
if (temporaryChatIdFromSessionKey(readShellRoute().activeKey)) {
if (readShellRoute().temporary) {
navigate(defaultShellRoute(), { replace: true });
}
});
@@ -85,7 +85,6 @@ import { useMediaQuery } from "@/hooks/useMediaQuery";
import type { SendAttachment, SendOptions } from "@/hooks/useNanobotStream";
import { usePageVisibility } from "@/hooks/usePageVisibility";
import { useVoiceRecorder, type VoiceRecorderErrorKey } from "@/hooks/useVoiceRecorder";
import { isTemporaryChatId } from "@/lib/temporary-chat";
import type {
CliAppInfo,
ChatSummary,
@@ -444,9 +443,7 @@ function storeSlashRecents(commands: string[]): void {
function queuedPromptsStorageKey(key?: string | null): string | null {
const clean = key?.trim();
return clean && !isTemporaryChatId(clean)
? `${QUEUED_PROMPTS_STORAGE_PREFIX}${clean}`
: null;
return clean ? `${QUEUED_PROMPTS_STORAGE_PREFIX}${clean}` : null;
}
function normalizeQueuedSessionMentions(value: unknown): SessionMention[] {
+6 -4
View File
@@ -33,7 +33,6 @@ import {
} from "@/lib/mcp-preset-events";
import type { CanonicalRunSnapshot, StreamError } from "@/lib/nanobot-client";
import { inferProviderFromModelName, providerDisplayLabel } from "@/lib/provider-brand";
import { isTemporaryChatId } from "@/lib/temporary-chat";
import type {
ChatSummary,
SettingsPayload,
@@ -677,6 +676,7 @@ export function ThreadShell({
const viewportRef = useRef<ThreadViewportHandle | null>(null);
const activeViewportTurnByChatIdRef = useRef<Map<string, string>>(new Map());
const messageCacheRef = useRef<Map<string, UIMessage[]>>(new Map());
const knownTemporaryChatIdsRef = useRef(new Set<string>());
/** Last chatId we associated with the in-memory thread (for cache-on-switch). */
const prevChatIdForCacheRef = useRef<string | null>(null);
/** Skip one message-cache write right after chatId changes (messages may not match yet). */
@@ -753,10 +753,12 @@ export function ThreadShell({
useEffect(() => {
const retained = new Set(temporaryChatIds);
for (const cachedChatId of messageCacheRef.current.keys()) {
if (isTemporaryChatId(cachedChatId) && !retained.has(cachedChatId)) {
for (const chatId of retained) knownTemporaryChatIdsRef.current.add(chatId);
for (const cachedChatId of knownTemporaryChatIdsRef.current) {
if (!retained.has(cachedChatId)) {
messageCacheRef.current.delete(cachedChatId);
activeViewportTurnByChatIdRef.current.delete(cachedChatId);
knownTemporaryChatIdsRef.current.delete(cachedChatId);
}
}
}, [temporaryChatIds]);
@@ -1435,7 +1437,7 @@ export function ThreadShell({
workspaceScopeDisabled={workspaceScopeDisabled}
workspaceError={workspaceError}
onWorkspaceScopeChange={onWorkspaceScopeChange}
pendingQueueKey={chatId}
pendingQueueKey={temporary ? null : chatId}
transcriptionProvider={settingsSnapshot?.transcription?.provider}
ingressLimits={ingressLimits}
quotedContext={quotedContext}
+35 -14
View File
@@ -11,7 +11,6 @@ import type {
WorkspaceScopePayload,
} from "./types";
import { createHostWebSocket } from "./runtime";
import { isTemporaryChatId } from "./temporary-chat";
/** WebSocket readyState constants, referenced by value to stay portable
* across runtimes that don't expose a global ``WebSocket`` (tests, SSR). */
@@ -109,6 +108,10 @@ interface PendingRequest<T> {
timer: ReturnType<typeof setTimeout>;
}
interface PendingChatRequest extends PendingRequest<string> {
temporary: boolean;
}
const SYSTEM_COMMAND_TURN_PREFIX = "webui-system:";
const TURN_REJECTION_DETAILS = new Set([
"access_denied",
@@ -197,7 +200,7 @@ export class NanobotClient {
private static readonly COMPLETED_TURN_FENCE_MAX = 256;
/** Latest ``goal_state`` snapshot per ``chat_id`` (multi-session isolation). */
private goalStateByChatId = new Map<string, GoalStateWsPayload>();
private pendingNewChat: PendingRequest<string> | null = null;
private pendingNewChat: PendingChatRequest | null = null;
private pendingTranscriptions = new Map<string, PendingRequest<string>>();
private pendingSystemCommands = new Map<string, PendingRequest<void>>();
// Frames queued while the socket is not yet OPEN
@@ -743,7 +746,7 @@ export class NanobotClient {
}
discardTemporaryChat(chatId: string): void {
if (!isTemporaryChatId(chatId)) return;
if (!this.temporaryChatIds.has(chatId)) return;
if (this.socket?.readyState === WS_OPEN) {
this.rawSend({ type: "discard_temporary_chat", chat_id: chatId });
}
@@ -760,7 +763,7 @@ export class NanobotClient {
this.pendingNewChat = null;
reject(new Error("newChat timed out"));
}, timeoutMs);
this.pendingNewChat = { resolve, reject, timer };
this.pendingNewChat = { resolve, reject, timer, temporary: false };
this.queueSend({
type: "new_chat",
...(workspaceScope ? { workspace_scope: workspaceScope } : {}),
@@ -768,6 +771,21 @@ export class NanobotClient {
});
}
/** Ask the WebUI gateway to create a connection-owned non-persistent chat. */
newTemporaryChat(timeoutMs: number = 5_000): Promise<string> {
if (this.pendingNewChat) {
return Promise.reject(new Error("newChat already in flight"));
}
return new Promise<string>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingNewChat = null;
reject(new Error("newTemporaryChat timed out"));
}, timeoutMs);
this.pendingNewChat = { resolve, reject, timer, temporary: true };
this.queueSend({ type: "new_temporary_chat" });
});
}
transcribeAudio(
dataUrl: string,
options?: { durationMs?: number; timeoutMs?: number },
@@ -804,7 +822,7 @@ export class NanobotClient {
this.pendingNewChat = null;
reject(new Error("forkChat timed out"));
}, timeoutMs);
this.pendingNewChat = { resolve, reject, timer };
this.pendingNewChat = { resolve, reject, timer, temporary: false };
this.queueSend({
type: "fork_chat",
source_chat_id: sourceChatId,
@@ -815,10 +833,7 @@ export class NanobotClient {
}
attach(chatId: string): void {
if (isTemporaryChatId(chatId)) {
this.temporaryChatIds.add(chatId);
return;
}
if (this.temporaryChatIds.has(chatId)) return;
this.knownChats.add(chatId);
if (this.socket?.readyState === WS_OPEN) {
this.queueSend({ type: "attach", chat_id: chatId });
@@ -840,8 +855,7 @@ export class NanobotClient {
startsNewRun?: boolean;
},
): void {
const temporary = isTemporaryChatId(chatId);
if (temporary) this.temporaryChatIds.add(chatId);
const temporary = this.temporaryChatIds.has(chatId);
if (!temporary) this.knownChats.add(chatId);
const frame: Outbound = {
type: "message",
@@ -891,7 +905,7 @@ export class NanobotClient {
}
setWorkspaceScope(chatId: string, workspaceScope: WorkspaceScopePayload): void {
if (isTemporaryChatId(chatId)) return;
if (this.temporaryChatIds.has(chatId)) return;
this.knownChats.add(chatId);
this.queueSend({
type: "set_workspace_scope",
@@ -1016,8 +1030,15 @@ export class NanobotClient {
}
if (parsed.event === "attached") {
this.knownChats.add(parsed.chat_id);
if (this.pendingNewChat) {
if (parsed.temporary === true) {
this.temporaryChatIds.add(parsed.chat_id);
} else {
this.knownChats.add(parsed.chat_id);
}
if (
this.pendingNewChat
&& this.pendingNewChat.temporary === (parsed.temporary === true)
) {
clearTimeout(this.pendingNewChat.timer);
this.pendingNewChat.resolve(parsed.chat_id);
this.pendingNewChat = null;
+1 -13
View File
@@ -1,20 +1,8 @@
import type { ChatSummary } from "./types";
export const TEMPORARY_CHAT_ID_PREFIX = "temporary-";
const WEBSOCKET_SESSION_KEY_PREFIX = "websocket:";
export function isTemporaryChatId(value: string): boolean {
return value.startsWith(TEMPORARY_CHAT_ID_PREFIX);
}
export function temporaryChatIdFromSessionKey(value: string | null): string | null {
if (!value?.startsWith(WEBSOCKET_SESSION_KEY_PREFIX)) return null;
const chatId = value.slice(WEBSOCKET_SESSION_KEY_PREFIX.length);
return isTemporaryChatId(chatId) ? chatId : null;
}
export function createTemporaryChatSession(): ChatSummary {
const chatId = `${TEMPORARY_CHAT_ID_PREFIX}${crypto.randomUUID()}`;
export function createTemporaryChatSession(chatId: string): ChatSummary {
const now = new Date().toISOString();
return {
key: `${WEBSOCKET_SESSION_KEY_PREFIX}${chatId}`,
+2 -1
View File
@@ -1162,7 +1162,7 @@ export interface InboundTurnMetadata {
export type InboundEvent =
| { event: "ready"; chat_id: string; client_id: string }
| { event: "attached"; chat_id: string }
| { event: "attached"; chat_id: string; temporary?: boolean }
| { event: "message_accepted"; chat_id: string; turn_id: string }
| ({
event: "message";
@@ -1338,6 +1338,7 @@ export interface FilePreviewPayload {
export type Outbound =
| { type: "new_chat"; workspace_scope?: WorkspaceScopePayload }
| { type: "new_temporary_chat" }
| { type: "fork_chat"; source_chat_id: string; before_user_index: number; title?: string }
| { type: "attach"; chat_id: string }
| { type: "set_sidebar_state"; state: SidebarStatePayload }
+15 -9
View File
@@ -20,6 +20,7 @@ const updateUrlSpy = vi.fn();
const attachSpy = vi.fn();
const setSidebarStateSpy = vi.fn();
const discardTemporaryChatSpy = vi.fn();
const newTemporaryChatSpy = vi.fn<() => Promise<string>>();
const sendMessageSpy = vi.fn();
const statusHandlers = new Set<(status: ConnectionStatus) => void>();
const runStatusHandlers = new Set<(chatId: string, startedAt: number | null) => void>();
@@ -238,6 +239,7 @@ vi.mock("@/lib/nanobot-client", async (importOriginal) => {
getGoalState = () => undefined;
sendMessage = sendMessageSpy;
newChat = vi.fn();
newTemporaryChat = newTemporaryChatSpy;
attach = attachSpy;
setSidebarState = setSidebarStateSpy;
discardTemporaryChat = discardTemporaryChatSpy;
@@ -270,6 +272,10 @@ describe("App layout", () => {
attachSpy.mockReset();
setSidebarStateSpy.mockReset();
discardTemporaryChatSpy.mockReset();
let temporaryChatCounter = 0;
newTemporaryChatSpy.mockImplementation(async () => (
`00000000-0000-4000-8000-${String(++temporaryChatCounter).padStart(12, "0")}`
));
sendMessageSpy.mockReset();
statusHandlers.clear();
runStatusHandlers.clear();
@@ -425,9 +431,9 @@ describe("App layout", () => {
});
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
const firstHash = window.location.hash;
expect(firstHash).toMatch(/^#\/temporary\/temporary-/);
expect(firstHash).toMatch(/^#\/temporary\/[0-9a-f-]+$/);
expect(screen.queryByRole("button", { name: "Temporary chat" })).not.toBeInTheDocument();
expect(createChatSpy).not.toHaveBeenCalled();
@@ -441,9 +447,9 @@ describe("App layout", () => {
target: { value: "second private message" },
});
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
const secondHash = window.location.hash;
expect(secondHash).toMatch(/^#\/temporary\/temporary-/);
expect(secondHash).toMatch(/^#\/temporary\/[0-9a-f-]+$/);
expect(secondHash).not.toBe(firstHash);
expect(screen.queryByRole("button", { name: "Temporary chat" })).not.toBeInTheDocument();
expect(discardTemporaryChatSpy).not.toHaveBeenCalled();
@@ -481,8 +487,8 @@ describe("App layout", () => {
const discardedChatIds = discardTemporaryChatSpy.mock.calls.map(([chatId]) => chatId);
expect(new Set(discardedChatIds).size).toBe(2);
expect(discardedChatIds).toEqual([
expect.stringMatching(/^temporary-/),
expect.stringMatching(/^temporary-/),
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002",
]);
});
@@ -544,7 +550,7 @@ describe("App layout", () => {
target: { value: "start temporary chat" },
});
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
expect(screen.queryByText("Not saved")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Clear temporary chat" })).not.toBeInTheDocument();
@@ -560,7 +566,7 @@ describe("App layout", () => {
target: { value: "do not lose this" },
});
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
const beforeUnload = new Event("beforeunload", { cancelable: true });
act(() => window.dispatchEvent(beforeUnload));
@@ -579,7 +585,7 @@ describe("App layout", () => {
target: { value: "connection-sensitive message" },
});
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
act(() => {
statusHandlers.forEach((handler) => handler("reconnecting"));
+55 -8
View File
@@ -71,19 +71,25 @@ afterEach(() => {
});
describe("NanobotClient", () => {
it("keeps temporary chats out of attachment and reconnect state", () => {
it("keeps temporary chats out of attachment and reconnect state", async () => {
const client = new NanobotClient({
url: "ws://test",
reconnect: false,
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
});
const chatId = "temporary-test";
const chatId = "temp-server-id";
client.connect();
lastSocket().fakeOpen();
const creation = client.newTemporaryChat();
expect(JSON.parse(lastSocket().sent.at(-1) as string)).toEqual({
type: "new_temporary_chat",
});
lastSocket().fakeMessage({ event: "attached", chat_id: chatId, temporary: true });
await expect(creation).resolves.toBe(chatId);
lastSocket().sent = [];
client.onChat(chatId, vi.fn());
client.sendMessage(chatId, "hello", undefined, { turnId: "turn-1" });
lastSocket().fakeOpen();
expect(lastSocket().sent.map((raw) => JSON.parse(raw))).toEqual([
{
type: "message",
@@ -101,6 +107,30 @@ describe("NanobotClient", () => {
});
});
it("waits for the temporary attachment when creating a temporary chat", async () => {
const client = new NanobotClient({
url: "ws://test",
reconnect: false,
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
});
client.connect();
lastSocket().fakeOpen();
const creation = client.newTemporaryChat();
let resolved = false;
void creation.then(() => { resolved = true; });
lastSocket().fakeMessage({ event: "attached", chat_id: "ordinary-chat" });
await Promise.resolve();
expect(resolved).toBe(false);
lastSocket().fakeMessage({
event: "attached",
chat_id: "server-temporary-chat",
temporary: true,
});
await expect(creation).resolves.toBe("server-temporary-chat");
});
it("forgets every temporary chat when the socket drops", async () => {
const client = new NanobotClient({
url: "ws://test",
@@ -112,20 +142,37 @@ describe("NanobotClient", () => {
const secondHandler = vi.fn();
client.connect();
lastSocket().fakeOpen();
client.onChat("temporary-drop-a", firstHandler);
client.onChat("temporary-drop-b", secondHandler);
const firstCreation = client.newTemporaryChat();
lastSocket().fakeMessage({
event: "attached",
chat_id: "temp-drop-a",
temporary: true,
});
await firstCreation;
const secondCreation = client.newTemporaryChat();
lastSocket().fakeMessage({
event: "attached",
chat_id: "temp-drop-b",
temporary: true,
});
await secondCreation;
lastSocket().sent = [];
client.onChat("temp-drop-a", firstHandler);
client.onChat("temp-drop-b", secondHandler);
firstHandler.mockClear();
secondHandler.mockClear();
lastSocket().close();
await vi.advanceTimersByTimeAsync(1);
lastSocket().fakeOpen();
lastSocket().fakeMessage({
event: "message",
chat_id: "temporary-drop-a",
chat_id: "temp-drop-a",
text: "stale first chat",
});
lastSocket().fakeMessage({
event: "message",
chat_id: "temporary-drop-b",
chat_id: "temp-drop-b",
text: "stale second chat",
});
+2 -2
View File
@@ -2944,7 +2944,7 @@ describe("ThreadComposer", () => {
onSend={onSend}
onStop={vi.fn()}
isStreaming
pendingQueueKey="temporary-private"
pendingQueueKey={null}
placeholder="Type your message..."
/>,
);
@@ -2966,7 +2966,7 @@ describe("ThreadComposer", () => {
onSend={onSend}
onStop={vi.fn()}
isStreaming
pendingQueueKey="temporary-private"
pendingQueueKey={null}
placeholder="Type your message..."
/>,
);