feat(FN-3906): add Chat Rooms experimental feature with Settings toggle
Added an experimental Chat Rooms feature gated behind a settings toggle in the dashboard. The ChatView rooms UI is now conditionally rendered based on the experimental features setting, controlled via the SettingsModal. Included documentation updates and corresponding test coverage. Fusion-Task-Id: FN-3906
This commit is contained in:
5
.changeset/fn-3906-chat-rooms-experimental.md
Normal file
5
.changeset/fn-3906-chat-rooms-experimental.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Hide Chat Rooms behind the `chatRooms` experimental flag. By default, Chat now shows direct-chat-only UI; re-enable rooms via **Settings → Experimental Features → Chat Rooms**.
|
||||||
@@ -99,6 +99,7 @@ Chat view provides project-scoped conversations with agents.
|
|||||||
|
|
||||||
Chat Rooms are project-scoped group conversations for multiple agents. They are separate from one-on-one direct chat sessions.
|
Chat Rooms are project-scoped group conversations for multiple agents. They are separate from one-on-one direct chat sessions.
|
||||||
|
|
||||||
|
- Chat Rooms are currently gated behind the `chatRooms` experimental feature flag. Enable it in **Settings → Experimental Features → Chat Rooms**.
|
||||||
- Use the **Direct / Rooms** toggle in the Chat sidebar to switch scopes. The selected scope is saved and restored the next time you open Chat.
|
- Use the **Direct / Rooms** toggle in the Chat sidebar to switch scopes. The selected scope is saved and restored the next time you open Chat.
|
||||||
- In **Rooms**, click **Create room** to open the room-creation modal.
|
- In **Rooms**, click **Create room** to open the room-creation modal.
|
||||||
- Room names follow strict validation: a leading `#` is removed automatically, names must be lowercase, up to 80 characters, use only `a-z`, `0-9`, `-`, or `_`, cannot start or end with `-`/`_`, and must be unique in the current project.
|
- Room names follow strict validation: a leading `#` is removed automatically, names must be lowercase, up to 80 characters, use only `a-z`, `0-9`, `-`, or `_`, cannot start or end with `-`/`_`, and must be unique in the current project.
|
||||||
|
|||||||
@@ -1220,7 +1220,11 @@ function AppInner() {
|
|||||||
return (
|
return (
|
||||||
<PageErrorBoundary>
|
<PageErrorBoundary>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ChatView addToast={addToast} projectId={currentProject?.id} />
|
<ChatView
|
||||||
|
addToast={addToast}
|
||||||
|
projectId={currentProject?.id}
|
||||||
|
experimentalFeatures={experimentalFeatures}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</PageErrorBoundary>
|
</PageErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { matchesAgentMentionFilter } from "./mentionMatching";
|
|||||||
export interface ChatViewProps {
|
export interface ChatViewProps {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast: (msg: string, type?: "success" | "error" | "warning") => void;
|
addToast: (msg: string, type?: "success" | "error" | "warning") => void;
|
||||||
|
experimentalFeatures?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeTime(dateStr: string): string {
|
function formatRelativeTime(dateStr: string): string {
|
||||||
@@ -709,7 +710,7 @@ const ChatMessageItem = memo(function ChatMessageItem({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatView({ projectId, addToast }: ChatViewProps) {
|
export function ChatView({ projectId, addToast, experimentalFeatures }: ChatViewProps) {
|
||||||
const {
|
const {
|
||||||
activeSession,
|
activeSession,
|
||||||
sessionsLoading,
|
sessionsLoading,
|
||||||
@@ -741,6 +742,9 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
const [sidebarWidth, setSidebarWidth] = useState(CHAT_SIDEBAR_DEFAULT_WIDTH);
|
const [sidebarWidth, setSidebarWidth] = useState(CHAT_SIDEBAR_DEFAULT_WIDTH);
|
||||||
const [chatScope, setChatScope] = useState<"direct" | "rooms">("direct");
|
const [chatScope, setChatScope] = useState<"direct" | "rooms">("direct");
|
||||||
const [createRoomOpen, setCreateRoomOpen] = useState(false);
|
const [createRoomOpen, setCreateRoomOpen] = useState(false);
|
||||||
|
const chatRoomsEnabled = experimentalFeatures?.chatRooms === true;
|
||||||
|
// Keep this hook unconditional to preserve hook ordering and test stability.
|
||||||
|
// Rooms UI and interactions are fully gated by `chatRoomsEnabled`.
|
||||||
const rooms = useChatRooms(projectId, addToast);
|
const rooms = useChatRooms(projectId, addToast);
|
||||||
const [agentsMap, setAgentsMap] = useState<Map<string, Agent>>(new Map());
|
const [agentsMap, setAgentsMap] = useState<Map<string, Agent>>(new Map());
|
||||||
const [discoveredSkills, setDiscoveredSkills] = useState<DiscoveredSkill[]>([]);
|
const [discoveredSkills, setDiscoveredSkills] = useState<DiscoveredSkill[]>([]);
|
||||||
@@ -813,21 +817,29 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const persistedScope = localStorage.getItem(CHAT_SCOPE_STORAGE_KEY);
|
const persistedScope = localStorage.getItem(CHAT_SCOPE_STORAGE_KEY);
|
||||||
if (persistedScope === "direct" || persistedScope === "rooms") {
|
if (persistedScope === "direct") {
|
||||||
setChatScope(persistedScope);
|
setChatScope("direct");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (persistedScope === "rooms" && chatRoomsEnabled) {
|
||||||
|
setChatScope("rooms");
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore storage errors.
|
// Ignore storage errors.
|
||||||
}
|
}
|
||||||
}, []);
|
}, [chatRoomsEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!chatRoomsEnabled && chatScope === "rooms") {
|
||||||
|
setChatScope("direct");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(CHAT_SCOPE_STORAGE_KEY, chatScope);
|
localStorage.setItem(CHAT_SCOPE_STORAGE_KEY, chatScope);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore storage errors.
|
// Ignore storage errors.
|
||||||
}
|
}
|
||||||
}, [chatScope]);
|
}, [chatRoomsEnabled, chatScope]);
|
||||||
|
|
||||||
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
|
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({
|
||||||
enabled: isMobile && !!activeSession,
|
enabled: isMobile && !!activeSession,
|
||||||
@@ -1229,7 +1241,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chatScope === "rooms") {
|
if (chatRoomsEnabled && chatScope === "rooms") {
|
||||||
if (!rooms.activeRoom) {
|
if (!rooms.activeRoom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1239,7 +1251,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSend();
|
handleSend();
|
||||||
}, [messageInput, chatScope, rooms, handleSend]);
|
}, [messageInput, chatRoomsEnabled, chatScope, rooms, handleSend]);
|
||||||
|
|
||||||
const handleSkillSelect = useCallback(
|
const handleSkillSelect = useCallback(
|
||||||
(skill: DiscoveredSkill) => {
|
(skill: DiscoveredSkill) => {
|
||||||
@@ -1759,6 +1771,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
className={`chat-sidebar${!sidebarVisible ? " chat-sidebar--hidden" : ""}`}
|
className={`chat-sidebar${!sidebarVisible ? " chat-sidebar--hidden" : ""}`}
|
||||||
style={isMobile ? undefined : { width: `${sidebarWidth}px` }}
|
style={isMobile ? undefined : { width: `${sidebarWidth}px` }}
|
||||||
>
|
>
|
||||||
|
{chatRoomsEnabled && (
|
||||||
<div className="chat-sidebar-scope-toggle" role="tablist" data-testid="chat-sidebar-scope-toggle">
|
<div className="chat-sidebar-scope-toggle" role="tablist" data-testid="chat-sidebar-scope-toggle">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1781,7 +1794,8 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
Rooms
|
Rooms
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{chatScope === "direct" ? (
|
)}
|
||||||
|
{!chatRoomsEnabled || chatScope === "direct" ? (
|
||||||
<>
|
<>
|
||||||
{/* Search section */}
|
{/* Search section */}
|
||||||
<div className="chat-sidebar-search-container">
|
<div className="chat-sidebar-search-container">
|
||||||
@@ -1999,7 +2013,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{confirmDeleteRoomId && (
|
{chatRoomsEnabled && confirmDeleteRoomId && (
|
||||||
<div className="chat-new-dialog-backdrop chat-view-dialog-backdrop" onClick={() => setConfirmDeleteRoomId(null)}>
|
<div className="chat-new-dialog-backdrop chat-view-dialog-backdrop" onClick={() => setConfirmDeleteRoomId(null)}>
|
||||||
<div className="chat-new-dialog chat-view-dialog" onClick={(e) => e.stopPropagation()}>
|
<div className="chat-new-dialog chat-view-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3>Delete Room?</h3>
|
<h3>Delete Room?</h3>
|
||||||
@@ -2030,7 +2044,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Thread */}
|
{/* Thread */}
|
||||||
{chatScope === "rooms" ? (
|
{chatRoomsEnabled && chatScope === "rooms" ? (
|
||||||
<div className="chat-thread">
|
<div className="chat-thread">
|
||||||
{rooms.activeRoom ? (
|
{rooms.activeRoom ? (
|
||||||
<>
|
<>
|
||||||
@@ -2460,6 +2474,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{chatRoomsEnabled && (
|
||||||
<CreateRoomModal
|
<CreateRoomModal
|
||||||
isOpen={createRoomOpen}
|
isOpen={createRoomOpen}
|
||||||
onClose={() => setCreateRoomOpen(false)}
|
onClose={() => setCreateRoomOpen(false)}
|
||||||
@@ -2470,6 +2485,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
|||||||
setCreateRoomOpen(false);
|
setCreateRoomOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* New Chat Dialog (rendered at root level) */}
|
{/* New Chat Dialog (rendered at root level) */}
|
||||||
{showNewDialog && (
|
{showNewDialog && (
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
|
|||||||
todoView: "Todo List",
|
todoView: "Todo List",
|
||||||
researchView: "Research View",
|
researchView: "Research View",
|
||||||
evalsView: "Evals View",
|
evalsView: "Evals View",
|
||||||
|
chatRooms: "Chat Rooms",
|
||||||
agentOnboarding: "Planning-style Agent Onboarding",
|
agentOnboarding: "Planning-style Agent Onboarding",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
|
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ describe("ChatView rooms", () => {
|
|||||||
const roomsMock = buildRoomsMock({ rooms: [] });
|
const roomsMock = buildRoomsMock({ rooms: [] });
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
|
await userEvent.click(screen.getByTestId("chat-create-room-btn"));
|
||||||
await userEvent.type(screen.getByLabelText("Room name"), "product");
|
await userEvent.type(screen.getByLabelText("Room name"), "product");
|
||||||
@@ -100,7 +100,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
|
await userEvent.click(screen.getByTestId("chat-room-item-engineering"));
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.type(screen.getByTestId("chat-input"), "hello room");
|
await userEvent.type(screen.getByTestId("chat-input"), "hello room");
|
||||||
await userEvent.click(screen.getByTestId("chat-send-btn"));
|
await userEvent.click(screen.getByTestId("chat-send-btn"));
|
||||||
@@ -128,7 +128,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.type(screen.getByTestId("chat-input"), " hello room from enter ");
|
await userEvent.type(screen.getByTestId("chat-input"), " hello room from enter ");
|
||||||
await userEvent.keyboard("{Enter}");
|
await userEvent.keyboard("{Enter}");
|
||||||
@@ -144,7 +144,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
|
await userEvent.click(screen.getByRole("button", { name: "Delete" }));
|
||||||
@@ -173,7 +173,7 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockReturnValue(roomsMock);
|
mockUseChatRooms.mockReturnValue(roomsMock);
|
||||||
|
|
||||||
render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
await userEvent.click(screen.getByTestId("chat-room-delete-engineering"));
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
@@ -189,11 +189,11 @@ describe("ChatView rooms", () => {
|
|||||||
});
|
});
|
||||||
mockUseChatRooms.mockImplementation(() => state);
|
mockUseChatRooms.mockImplementation(() => state);
|
||||||
|
|
||||||
const { rerender } = render(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
const { rerender } = render(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
|
||||||
state.messages = [{ id: "msg-2", roomId: "room-1", role: "assistant", content: "reply", thinkingOutput: null, metadata: null, senderAgentId: "agent-1", mentions: [], createdAt: "2026-05-09T00:00:00.000Z" }];
|
state.messages = [{ id: "msg-2", roomId: "room-1", role: "assistant", content: "reply", thinkingOutput: null, metadata: null, senderAgentId: "agent-1", mentions: [], createdAt: "2026-05-09T00:00:00.000Z" }];
|
||||||
rerender(<ChatView addToast={vi.fn()} projectId="proj-1" />);
|
rerender(<ChatView addToast={vi.fn()} projectId="proj-1" experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
expect(screen.getByText("reply")).toBeInTheDocument();
|
expect(screen.getByText("reply")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2436,13 +2436,22 @@ describe("Direct/Rooms scope toggle", () => {
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("hides rooms UI when chatRooms experimental flag is off", () => {
|
||||||
|
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||||
|
|
||||||
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{}} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("chat-sidebar-scope-rooms")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("chat-sidebar-rooms")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("defaults to Direct with sidebar list visible", () => {
|
it("defaults to Direct with sidebar list visible", () => {
|
||||||
setupMockChat({
|
setupMockChat({
|
||||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
expect(screen.getByTestId("chat-sidebar-scope-direct")).toHaveAttribute("aria-selected", "true");
|
expect(screen.getByTestId("chat-sidebar-scope-direct")).toHaveAttribute("aria-selected", "true");
|
||||||
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toHaveAttribute("aria-selected", "false");
|
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toHaveAttribute("aria-selected", "false");
|
||||||
@@ -2450,13 +2459,23 @@ describe("Direct/Rooms scope toggle", () => {
|
|||||||
expect(screen.queryByTestId("chat-sidebar-rooms-empty")).toBeNull();
|
expect(screen.queryByTestId("chat-sidebar-rooms-empty")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows rooms UI when chatRooms experimental flag is on", async () => {
|
||||||
|
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||||
|
|
||||||
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
expect(screen.getByTestId("chat-sidebar-rooms")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows rooms placeholder and hides direct search/list in Rooms scope", async () => {
|
it("shows rooms placeholder and hides direct search/list in Rooms scope", async () => {
|
||||||
setupMockChat({
|
setupMockChat({
|
||||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
|
|
||||||
@@ -2473,7 +2492,7 @@ describe("Direct/Rooms scope toggle", () => {
|
|||||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-direct"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-direct"));
|
||||||
@@ -2483,17 +2502,27 @@ describe("Direct/Rooms scope toggle", () => {
|
|||||||
expect(screen.getByTestId("chat-session-session-001")).toHaveClass("chat-session-item--active");
|
expect(screen.getByTestId("chat-session-session-001")).toHaveClass("chat-session-item--active");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forces direct scope when localStorage persisted rooms but chatRooms is off", () => {
|
||||||
|
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||||
|
localStorage.setItem("fusion:chat-scope", "rooms");
|
||||||
|
|
||||||
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{}} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("chat-sidebar-scope-rooms")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("chat-search-input")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("persists scope in localStorage and restores Rooms on next mount", async () => {
|
it("persists scope in localStorage and restores Rooms on next mount", async () => {
|
||||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||||
|
|
||||||
const { unmount } = render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
const { unmount } = render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
await userEvent.click(screen.getByTestId("chat-sidebar-scope-rooms"));
|
||||||
expect(localStorage.getItem("fusion:chat-scope")).toBe("rooms");
|
expect(localStorage.getItem("fusion:chat-scope")).toBe("rooms");
|
||||||
|
|
||||||
unmount();
|
unmount();
|
||||||
|
|
||||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
render(<ChatView projectId="proj-123" addToast={vi.fn()} experimentalFeatures={{ chatRooms: true }} />);
|
||||||
|
|
||||||
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toHaveAttribute("aria-selected", "true");
|
expect(screen.getByTestId("chat-sidebar-scope-rooms")).toHaveAttribute("aria-selected", "true");
|
||||||
expect(screen.getByTestId("chat-sidebar-rooms-empty")).toBeInTheDocument();
|
expect(screen.getByTestId("chat-sidebar-rooms-empty")).toBeInTheDocument();
|
||||||
@@ -3206,7 +3235,9 @@ describe("ChatView mobile behavior", () => {
|
|||||||
|
|
||||||
await userEvent.click(screen.getByTestId("chat-jump-to-latest"));
|
await userEvent.click(screen.getByTestId("chat-jump-to-latest"));
|
||||||
expect(scrollTopValue).toBe(1000);
|
expect(scrollTopValue).toBe(1000);
|
||||||
|
await waitFor(() => {
|
||||||
expect(screen.queryByTestId("chat-jump-to-latest")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("chat-jump-to-latest")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
restoreMatchMedia.mockRestore();
|
restoreMatchMedia.mockRestore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1933,6 +1933,14 @@ describe("SettingsModal", () => {
|
|||||||
expect(screen.getByLabelText("Evals View")).toBeInTheDocument();
|
expect(screen.getByLabelText("Evals View")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows chatRooms in the Experimental Features list", async () => {
|
||||||
|
renderModal();
|
||||||
|
|
||||||
|
await openExperimentalFeaturesSection();
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("Chat Rooms")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows agentOnboarding in the Experimental Features list", async () => {
|
it("shows agentOnboarding in the Experimental Features list", async () => {
|
||||||
renderModal();
|
renderModal();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user