feat(FN-4849): complete Step 2 — route room thread reads and sends
Fusion-Task-Id: FN-4849 Fusion-Task-Lineage: 624f86c4-0b0f-4543-90d2-381f890da401
This commit is contained in:
committed by
gsxdsm
parent
2adc9fa0da
commit
b97709a3d4
@@ -1129,7 +1129,19 @@ export function QuickChatFAB({
|
|||||||
}, [chatMode, selectedAgentId, targetModelSelection]);
|
}, [chatMode, selectedAgentId, targetModelSelection]);
|
||||||
|
|
||||||
const hasChatTarget = chatMode === "agent" ? Boolean(selectedAgentId) : Boolean(targetModelSelection);
|
const hasChatTarget = chatMode === "agent" ? Boolean(selectedAgentId) : Boolean(targetModelSelection);
|
||||||
const inputDisabled = !hasChatTarget || !activeSession;
|
const roomThreadActive = chatRoomsEnabled && Boolean(roomsState.activeRoom);
|
||||||
|
const displayedMessages = useMemo<ChatMessageInfo[]>(() => {
|
||||||
|
if (!roomThreadActive) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
return roomsState.messages.map((message) => ({
|
||||||
|
id: message.id,
|
||||||
|
role: message.role,
|
||||||
|
content: message.content,
|
||||||
|
toolCalls: undefined,
|
||||||
|
}));
|
||||||
|
}, [messages, roomThreadActive, roomsState.messages]);
|
||||||
|
const inputDisabled = roomThreadActive ? false : (!hasChatTarget || !activeSession);
|
||||||
const hasPersistedAgentSessionSelection = useMemo(
|
const hasPersistedAgentSessionSelection = useMemo(
|
||||||
() => Boolean(selectedAgentId) && sessions.some((session) => !session.modelProvider && !session.modelId && session.agentId === selectedAgentId),
|
() => Boolean(selectedAgentId) && sessions.some((session) => !session.modelProvider && !session.modelId && session.agentId === selectedAgentId),
|
||||||
[selectedAgentId, sessions],
|
[selectedAgentId, sessions],
|
||||||
@@ -1578,16 +1590,16 @@ export function QuickChatFAB({
|
|||||||
}, [anchorToBottom]);
|
}, [anchorToBottom]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const sessionId = activeSession?.id ?? null;
|
const threadId = roomThreadActive ? (roomsState.activeRoom?.id ?? null) : (activeSession?.id ?? null);
|
||||||
const previousState = previousOpenStateRef.current;
|
const previousState = previousOpenStateRef.current;
|
||||||
previousOpenStateRef.current = { isOpen, sessionId };
|
previousOpenStateRef.current = { isOpen, sessionId: threadId };
|
||||||
|
|
||||||
if (!isOpen || !sessionId) {
|
if (!isOpen || !threadId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const openingNow = !previousState.isOpen && isOpen;
|
const openingNow = !previousState.isOpen && isOpen;
|
||||||
const sessionChangedWhileOpen = previousState.isOpen && previousState.sessionId !== sessionId;
|
const sessionChangedWhileOpen = previousState.isOpen && previousState.sessionId !== threadId;
|
||||||
if (!openingNow && !sessionChangedWhileOpen) {
|
if (!openingNow && !sessionChangedWhileOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1596,7 +1608,7 @@ export function QuickChatFAB({
|
|||||||
if (!messagesEl) return;
|
if (!messagesEl) return;
|
||||||
|
|
||||||
anchorToBottom(messagesEl);
|
anchorToBottom(messagesEl);
|
||||||
}, [isOpen, activeSession?.id, anchorToBottom]);
|
}, [isOpen, activeSession?.id, anchorToBottom, roomThreadActive, roomsState.activeRoom?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile || !isOpen || !activeSession) {
|
if (!isMobile || !isOpen || !activeSession) {
|
||||||
@@ -1633,7 +1645,7 @@ export function QuickChatFAB({
|
|||||||
if (!isUserScrollingRef.current) {
|
if (!isUserScrollingRef.current) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}, [messages, streamingText, streamingThinking, isStreaming, isOpen, scrollToBottom]);
|
}, [displayedMessages, streamingText, streamingThinking, isStreaming, isOpen, roomThreadActive, scrollToBottom]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSession?.id) {
|
if (!activeSession?.id) {
|
||||||
@@ -1716,7 +1728,7 @@ export function QuickChatFAB({
|
|||||||
const showRoomGroups = chatRoomsEnabled && roomOptions.length > 0;
|
const showRoomGroups = chatRoomsEnabled && roomOptions.length > 0;
|
||||||
|
|
||||||
const activeSessionLabel = useMemo(() => {
|
const activeSessionLabel = useMemo(() => {
|
||||||
if (showRoomGroups && roomsState.activeRoom) {
|
if (showRoomGroups && roomThreadActive && roomsState.activeRoom) {
|
||||||
return `#${roomsState.activeRoom.name}`;
|
return `#${roomsState.activeRoom.name}`;
|
||||||
}
|
}
|
||||||
const activeOption = sessionOptions.find((option) => option.id === activeSession?.id);
|
const activeOption = sessionOptions.find((option) => option.id === activeSession?.id);
|
||||||
@@ -1727,7 +1739,7 @@ export function QuickChatFAB({
|
|||||||
return "Loading sessions…";
|
return "Loading sessions…";
|
||||||
}
|
}
|
||||||
return "Select a session";
|
return "Select a session";
|
||||||
}, [activeSession?.id, roomsState.activeRoom, sessionOptions, sessionsLoading, showRoomGroups]);
|
}, [activeSession?.id, roomThreadActive, roomsState.activeRoom, sessionOptions, sessionsLoading, showRoomGroups]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@@ -1763,7 +1775,7 @@ export function QuickChatFAB({
|
|||||||
}, [sessionMenuOpen]);
|
}, [sessionMenuOpen]);
|
||||||
|
|
||||||
const inputPlaceholder = useMemo(() => {
|
const inputPlaceholder = useMemo(() => {
|
||||||
if (chatRoomsEnabled && roomsState.activeRoom) {
|
if (roomThreadActive && roomsState.activeRoom) {
|
||||||
return `Message #${roomsState.activeRoom.name}`;
|
return `Message #${roomsState.activeRoom.name}`;
|
||||||
}
|
}
|
||||||
if (chatMode === "agent") {
|
if (chatMode === "agent") {
|
||||||
@@ -1777,7 +1789,7 @@ export function QuickChatFAB({
|
|||||||
return `Message ${selectedModelTag}`;
|
return `Message ${selectedModelTag}`;
|
||||||
}
|
}
|
||||||
return "Select a model to start chatting";
|
return "Select a model to start chatting";
|
||||||
}, [chatMode, chatRoomsEnabled, roomsState.activeRoom?.name, selectedAgent, selectedModelTag]);
|
}, [chatMode, roomThreadActive, roomsState.activeRoom?.name, selectedAgent, selectedModelTag]);
|
||||||
|
|
||||||
const handleSessionSwitch = useCallback((sessionId: string) => {
|
const handleSessionSwitch = useCallback((sessionId: string) => {
|
||||||
const selectedSession = sessions.find((session) => session.id === sessionId);
|
const selectedSession = sessions.find((session) => session.id === sessionId);
|
||||||
@@ -1785,7 +1797,7 @@ export function QuickChatFAB({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatRoomsEnabled && roomsState.activeRoom) {
|
if (roomThreadActive) {
|
||||||
roomsState.selectRoom(null);
|
roomsState.selectRoom(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1808,7 +1820,7 @@ export function QuickChatFAB({
|
|||||||
|
|
||||||
void selectSession(selectedSession);
|
void selectSession(selectedSession);
|
||||||
setSessionMenuOpen(false);
|
setSessionMenuOpen(false);
|
||||||
}, [chatRoomsEnabled, markRead, roomsState, selectSession, sessions]);
|
}, [markRead, roomThreadActive, roomsState, selectSession, sessions]);
|
||||||
|
|
||||||
const handleRoomSwitch = useCallback((roomId: string) => {
|
const handleRoomSwitch = useCallback((roomId: string) => {
|
||||||
const selectedRoom = roomsState.rooms.find((room) => room.id === roomId);
|
const selectedRoom = roomsState.rooms.find((room) => room.id === roomId);
|
||||||
@@ -1926,9 +1938,14 @@ export function QuickChatFAB({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (roomThreadActive && attachmentsToSend.length > 0) {
|
||||||
|
addToast("Attachments are not supported in chat rooms yet", "warning");
|
||||||
|
focusComposerInput();
|
||||||
|
preserveComposerFocusRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (trimmed === "/clear" || trimmed === "/new") {
|
if (trimmed === "/clear" || trimmed === "/new") {
|
||||||
stopStreaming();
|
|
||||||
clearPendingMessage();
|
|
||||||
attachmentsToSend.forEach((attachment) => {
|
attachmentsToSend.forEach((attachment) => {
|
||||||
if (attachment.previewUrl) {
|
if (attachment.previewUrl) {
|
||||||
URL.revokeObjectURL(attachment.previewUrl);
|
URL.revokeObjectURL(attachment.previewUrl);
|
||||||
@@ -1937,13 +1954,20 @@ export function QuickChatFAB({
|
|||||||
setPendingAttachments((previous) => previous.filter((attachment) => !attachmentsToSend.includes(attachment)));
|
setPendingAttachments((previous) => previous.filter((attachment) => !attachmentsToSend.includes(attachment)));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (chatMode === "model") {
|
if (roomThreadActive && roomsState.activeRoom?.id) {
|
||||||
|
await roomsState.clearRoom(roomsState.activeRoom.id);
|
||||||
|
setHelpMessageVisible(false);
|
||||||
|
} else if (chatMode === "model") {
|
||||||
|
stopStreaming();
|
||||||
|
clearPendingMessage();
|
||||||
const parsed = parseModelSelection(resolvedModelSelection);
|
const parsed = parseModelSelection(resolvedModelSelection);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await startFreshSession(FN_AGENT_ID, parsed.modelProvider, parsed.modelId);
|
await startFreshSession(FN_AGENT_ID, parsed.modelProvider, parsed.modelId);
|
||||||
} else if (selectedAgentId) {
|
} else if (selectedAgentId) {
|
||||||
|
stopStreaming();
|
||||||
|
clearPendingMessage();
|
||||||
await startFreshSession(selectedAgentId);
|
await startFreshSession(selectedAgentId);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1957,7 +1981,11 @@ export function QuickChatFAB({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setHelpMessageVisible(false);
|
setHelpMessageVisible(false);
|
||||||
await sendMessage(trimmed, attachmentsToSend.map((attachment) => attachment.file));
|
if (roomThreadActive) {
|
||||||
|
await roomsState.sendRoomMessage(trimmed);
|
||||||
|
} else {
|
||||||
|
await sendMessage(trimmed, attachmentsToSend.map((attachment) => attachment.file));
|
||||||
|
}
|
||||||
attachmentsToSend.forEach((attachment) => {
|
attachmentsToSend.forEach((attachment) => {
|
||||||
if (attachment.previewUrl) {
|
if (attachment.previewUrl) {
|
||||||
URL.revokeObjectURL(attachment.previewUrl);
|
URL.revokeObjectURL(attachment.previewUrl);
|
||||||
@@ -1978,6 +2006,8 @@ export function QuickChatFAB({
|
|||||||
inputDisabled,
|
inputDisabled,
|
||||||
messageInput,
|
messageInput,
|
||||||
resolvedModelSelection,
|
resolvedModelSelection,
|
||||||
|
roomThreadActive,
|
||||||
|
roomsState,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
startFreshSession,
|
startFreshSession,
|
||||||
@@ -2492,7 +2522,7 @@ export function QuickChatFAB({
|
|||||||
<div className="quick-chat-panel-header">
|
<div className="quick-chat-panel-header">
|
||||||
<div className="quick-chat-panel-title-wrap">
|
<div className="quick-chat-panel-title-wrap">
|
||||||
<h3>Quick Chat</h3>
|
<h3>Quick Chat</h3>
|
||||||
{chatRoomsEnabled && roomsState.activeRoom ? (
|
{roomThreadActive && roomsState.activeRoom ? (
|
||||||
<span className="quick-chat-model-tag" data-testid="quick-chat-room-tag" title={`#${roomsState.activeRoom.name}`}>
|
<span className="quick-chat-model-tag" data-testid="quick-chat-room-tag" title={`#${roomsState.activeRoom.name}`}>
|
||||||
#{roomsState.activeRoom.name}
|
#{roomsState.activeRoom.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -2553,7 +2583,7 @@ export function QuickChatFAB({
|
|||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
data-testid="quick-chat-session-dropdown"
|
data-testid="quick-chat-session-dropdown"
|
||||||
value={showRoomGroups && roomsState.activeRoom ? "" : activeSession?.id ?? ""}
|
value={showRoomGroups && roomThreadActive ? "" : activeSession?.id ?? ""}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -2565,7 +2595,7 @@ export function QuickChatFAB({
|
|||||||
data-testid="quick-chat-session-dropdown-trigger"
|
data-testid="quick-chat-session-dropdown-trigger"
|
||||||
onClick={() => setSessionMenuOpen((current) => !current)}
|
onClick={() => setSessionMenuOpen((current) => !current)}
|
||||||
>
|
>
|
||||||
{chatRoomsEnabled && roomsState.activeRoom ? (
|
{roomThreadActive && roomsState.activeRoom ? (
|
||||||
<Hash size={16} aria-hidden="true" />
|
<Hash size={16} aria-hidden="true" />
|
||||||
) : activeSession?.modelProvider ? (
|
) : activeSession?.modelProvider ? (
|
||||||
<ProviderIcon provider={activeSession.modelProvider} size="sm" />
|
<ProviderIcon provider={activeSession.modelProvider} size="sm" />
|
||||||
@@ -2608,7 +2638,7 @@ export function QuickChatFAB({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{sessionOptions.map((sessionOption) => {
|
{sessionOptions.map((sessionOption) => {
|
||||||
const isActiveSession = roomsState.activeRoom === null && activeSession?.id === sessionOption.id;
|
const isActiveSession = !roomThreadActive && activeSession?.id === sessionOption.id;
|
||||||
const session = sessions.find((item) => item.id === sessionOption.id);
|
const session = sessions.find((item) => item.id === sessionOption.id);
|
||||||
const showUnreadDot = !isActiveSession && isUnread("direct", sessionOption.id, session?.lastMessageAt ?? session?.updatedAt);
|
const showUnreadDot = !isActiveSession && isUnread("direct", sessionOption.id, session?.lastMessageAt ?? session?.updatedAt);
|
||||||
return (
|
return (
|
||||||
@@ -2717,9 +2747,9 @@ export function QuickChatFAB({
|
|||||||
<div className="quick-chat-panel-messages" ref={messagesRef} data-testid="quick-chat-messages" onScroll={updateScrollState}>
|
<div className="quick-chat-panel-messages" ref={messagesRef} data-testid="quick-chat-messages" onScroll={updateScrollState}>
|
||||||
{sessionsLoading ? (
|
{sessionsLoading ? (
|
||||||
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
||||||
) : isStreaming ? (
|
) : !roomThreadActive && isStreaming ? (
|
||||||
<>
|
<>
|
||||||
{messages.map((message: ChatMessageInfo) => (
|
{displayedMessages.map((message: ChatMessageInfo) => (
|
||||||
<QuickChatMessageItem
|
<QuickChatMessageItem
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
@@ -2767,13 +2797,35 @@ export function QuickChatFAB({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : messagesLoading ? (
|
) : roomThreadActive ? roomsState.messagesLoading ? (
|
||||||
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
||||||
) : messages.length === 0 && !streamingText && !streamingThinking && !isStreaming && !helpMessageVisible ? (
|
) : displayedMessages.length === 0 && !helpMessageVisible ? (
|
||||||
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
|
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{messages.map((message: ChatMessageInfo) => (
|
{displayedMessages.map((message: ChatMessageInfo) => (
|
||||||
|
<QuickChatMessageItem
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
forcePlain={message.role !== "user" && plainTextMessageIds.has(message.id)}
|
||||||
|
mentionAgentsByName={mentionAgentsByName}
|
||||||
|
roomContext={roomContext}
|
||||||
|
onToggleRender={toggleMessageRenderMode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{helpMessageVisible && (
|
||||||
|
<div className="quick-chat-panel-message quick-chat-panel-message--received" data-testid="quick-chat-help-message">
|
||||||
|
{renderAssistantMessageContent("Available commands:\n- `/new` or `/clear` — Clear conversation and start fresh\n- `/skill:{name}` — Use a specific skill\n- `/help` — Show this help")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : messagesLoading ? (
|
||||||
|
<div className="quick-chat-panel-empty">Loading conversation…</div>
|
||||||
|
) : displayedMessages.length === 0 && !streamingText && !streamingThinking && !isStreaming && !helpMessageVisible ? (
|
||||||
|
<div className="quick-chat-panel-empty">No messages yet. Start the conversation!</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{displayedMessages.map((message: ChatMessageInfo) => (
|
||||||
<QuickChatMessageItem
|
<QuickChatMessageItem
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
@@ -3018,7 +3070,7 @@ export function QuickChatFAB({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{pendingMessage && (
|
{!roomThreadActive && pendingMessage && (
|
||||||
<div className="chat-pending-message" data-testid="chat-pending-indicator">
|
<div className="chat-pending-message" data-testid="chat-pending-indicator">
|
||||||
<span>{`Queued: ${pendingPreview}`}</span>
|
<span>{`Queued: ${pendingPreview}`}</span>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1444,4 +1444,162 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
|
|
||||||
expect(openFile).toHaveBeenCalledWith("packages/dashboard/app/components/QuickChatFAB.tsx", { line: undefined, col: undefined });
|
expect(openFile).toHaveBeenCalledWith("packages/dashboard/app/components/QuickChatFAB.tsx", { line: undefined, col: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("FN-4849 room switching", () => {
|
||||||
|
const room = {
|
||||||
|
id: "room-1",
|
||||||
|
name: "engineering",
|
||||||
|
slug: "engineering",
|
||||||
|
memberCount: 2,
|
||||||
|
createdAt: "2026-05-16T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-05-16T00:00:03.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("switching to a room renders room messages, not session messages", async () => {
|
||||||
|
const selectRoom = vi.fn();
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room],
|
||||||
|
roomsLoading: false,
|
||||||
|
roomsError: null,
|
||||||
|
activeRoom: room,
|
||||||
|
activeRoomMembers: [],
|
||||||
|
messages: [
|
||||||
|
{ id: "room-msg-1", roomId: room.id, role: "assistant", content: "room msg 1", createdAt: "2026-05-16T00:00:01.000Z" },
|
||||||
|
{ id: "room-msg-2", roomId: room.id, role: "user", content: "room msg 2", createdAt: "2026-05-16T00:00:02.000Z" },
|
||||||
|
],
|
||||||
|
messagesLoading: false,
|
||||||
|
selectRoom,
|
||||||
|
createRoom: vi.fn(),
|
||||||
|
deleteRoom: vi.fn(),
|
||||||
|
sendRoomMessage: vi.fn(),
|
||||||
|
clearRoom: vi.fn(),
|
||||||
|
refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
mockFetchChatMessages.mockResolvedValueOnce({
|
||||||
|
messages: [{ id: "session-msg", sessionId: "session-model", role: "assistant", content: "hello from session", createdAt: "2026-05-16T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
const messages = await screen.findByTestId("quick-chat-messages");
|
||||||
|
expect(messages).toHaveTextContent("room msg 1");
|
||||||
|
expect(messages).toHaveTextContent("room msg 2");
|
||||||
|
expect(messages).not.toHaveTextContent("hello from session");
|
||||||
|
expect(screen.getByTestId("quick-chat-session-dropdown-trigger")).toHaveTextContent("#engineering");
|
||||||
|
expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message #engineering");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sending while in a room routes to sendRoomMessage, not sendMessage", async () => {
|
||||||
|
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
|
||||||
|
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage, clearRoom: vi.fn(), refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
const input = await screen.findByTestId("quick-chat-input");
|
||||||
|
fireEvent.change(input, { target: { value: "room dispatch" } });
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-send"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(sendRoomMessage).toHaveBeenCalledWith("room dispatch");
|
||||||
|
});
|
||||||
|
expect(mockStreamChatResponse).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("/clear while in a room calls clearRoom, not startFreshSession", async () => {
|
||||||
|
const clearRoom = vi.fn().mockResolvedValue(undefined);
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
|
||||||
|
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom, refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
const input = await screen.findByTestId("quick-chat-input");
|
||||||
|
fireEvent.change(input, { target: { value: "/clear" } });
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-send"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(clearRoom).toHaveBeenCalledWith("room-1");
|
||||||
|
});
|
||||||
|
expect(mockCreateChatSession).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("composer is enabled in a room even without an activeSession", async () => {
|
||||||
|
mockFetchResumeChatSession.mockRejectedValueOnce(new Error("resume failed"));
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
|
||||||
|
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
expect(await screen.findByTestId("quick-chat-input")).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switching back from a room to a session restores session messages", async () => {
|
||||||
|
const selectRoom = vi.fn();
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [],
|
||||||
|
messages: [{ id: "room-msg-1", roomId: room.id, role: "assistant", content: "room msg 1", createdAt: "2026-05-16T00:00:01.000Z" }],
|
||||||
|
messagesLoading: false,
|
||||||
|
selectRoom, createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
mockFetchChatMessages.mockResolvedValueOnce({
|
||||||
|
messages: [{ id: "session-msg", sessionId: "session-model", role: "assistant", content: "hello from session", createdAt: "2026-05-16T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const view = render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
fireEvent.click(await screen.findByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-session-model"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(selectRoom).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: null, activeRoomMembers: [],
|
||||||
|
messages: [], messagesLoading: false,
|
||||||
|
selectRoom, createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
view.rerender(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
expect(await screen.findByTestId("quick-chat-messages")).toHaveTextContent("hello from session");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attachment-in-room is blocked with a warning toast", async () => {
|
||||||
|
const addToast = vi.fn();
|
||||||
|
const sendRoomMessage = vi.fn().mockResolvedValue(undefined);
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [room], roomsLoading: false, roomsError: null, activeRoom: room, activeRoomMembers: [], messages: [], messagesLoading: false,
|
||||||
|
selectRoom: vi.fn(), createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage, clearRoom: vi.fn(), refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={addToast} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
const attachmentInput = document.querySelector(".quick-chat-attachment-input") as HTMLInputElement | null;
|
||||||
|
const input = screen.getByTestId("quick-chat-input");
|
||||||
|
const file = new File(["hi"], "note.txt", { type: "text/plain" });
|
||||||
|
expect(attachmentInput).not.toBeNull();
|
||||||
|
fireEvent.change(attachmentInput!, { target: { files: [file] } });
|
||||||
|
fireEvent.change(input, { target: { value: "try send" } });
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-send"));
|
||||||
|
|
||||||
|
expect(addToast).toHaveBeenCalledWith("Attachments are not supported in chat rooms yet", "warning");
|
||||||
|
expect(sendRoomMessage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user