feat(FN-4611): add themed quick chat session dropdown
- Replace the quick chat header mode toggle with a session picker dropdown styled from dashboard theme tokens - Surface chat room entries alongside sessions and keep active room/session switching in the same menu - Add QuickChatFAB and CSS regression coverage for the new dropdown behavior and token-based styling - Add a changeset note for the quick chat session picker update Fusion-Task-Id: FN-4611 Fusion-Task-Id: FN-4634 Fusion-Task-Lineage: 87cc75e7-ba50-4cb5-bb9f-6d86f49268e0
This commit is contained in:
5
.changeset/fn-4611-quick-chat-themed-session-dropdown.md
Normal file
5
.changeset/fn-4611-quick-chat-themed-session-dropdown.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
QuickChat session picker now uses the themed dropdown style and includes chat rooms in the switch list.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { loadAllAppCss } from "../test/cssFixture";
|
||||||
|
|
||||||
|
describe("QuickChat session dropdown CSS", () => {
|
||||||
|
it("defines themed dropdown selectors using tokens", async () => {
|
||||||
|
const css = await loadAllAppCss();
|
||||||
|
|
||||||
|
const triggerBlock = css.match(/\.quick-chat-session-trigger\s*\{[^}]*\}/)?.[0] ?? "";
|
||||||
|
const dropdownBlock = css.match(/\.quick-chat-session-dropdown\s*\{[^}]*\}/)?.[0] ?? "";
|
||||||
|
|
||||||
|
expect(triggerBlock).toContain(".quick-chat-session-trigger");
|
||||||
|
expect(dropdownBlock).toContain(".quick-chat-session-dropdown");
|
||||||
|
|
||||||
|
const combined = `${triggerBlock}\n${dropdownBlock}`;
|
||||||
|
expect(combined).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
|
||||||
|
expect(combined).not.toMatch(/rgba?\(/);
|
||||||
|
expect(combined).not.toMatch(/(?<![\w-])(?:[1-9]\d*|\d+\.\d+)px\b/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -381,6 +381,91 @@
|
|||||||
padding: calc(var(--space-sm) - (var(--space-xs) / 4)) calc(var(--space-md) - (var(--space-xs) / 4));
|
padding: calc(var(--space-sm) - (var(--space-xs) / 4)) calc(var(--space-md) - (var(--space-xs) / 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-menu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-trigger {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
border-color: var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-sm);
|
||||||
|
transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-trigger > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-trigger > svg:last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-trigger:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-trigger:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + var(--space-xs));
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(var(--space-xl) * 10);
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--surface);
|
||||||
|
border: var(--btn-border-width) solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
padding: var(--space-xs);
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-dropdown-group-label {
|
||||||
|
margin: var(--space-xs) var(--space-sm);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--space-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: calc(var(--space-xs) / 8);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-option {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: var(--space-sm);
|
||||||
|
text-align: left;
|
||||||
|
transition: background var(--transition-fast), color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-option:hover {
|
||||||
|
background: var(--card-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-option:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--focus-ring-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-option--active {
|
||||||
|
background: color-mix(in srgb, var(--todo) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.quick-chat-panel-messages {
|
.quick-chat-panel-messages {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -791,6 +876,13 @@
|
|||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-chat-session-dropdown {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: auto;
|
||||||
|
max-height: calc(100vh - var(--header-height) - var(--mobile-nav-height));
|
||||||
|
}
|
||||||
|
|
||||||
.quick-chat-resize-handle {
|
.quick-chat-resize-handle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { FileMentionPopup } from "./FileMentionPopup";
|
|||||||
import { useFileMention } from "../hooks/useFileMention";
|
import { useFileMention } from "../hooks/useFileMention";
|
||||||
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
|
||||||
import { useViewportMode } from "../hooks/useViewportMode";
|
import { useViewportMode } from "../hooks/useViewportMode";
|
||||||
|
import { useAppSettings } from "../hooks/useAppSettings";
|
||||||
|
import { useChatRooms } from "../hooks/useChatRooms";
|
||||||
|
|
||||||
interface PendingAttachment {
|
interface PendingAttachment {
|
||||||
file: File;
|
file: File;
|
||||||
@@ -893,6 +895,7 @@ export function QuickChatFAB({
|
|||||||
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||||
const [newSessionChooserOpen, setNewSessionChooserOpen] = useState(false);
|
const [newSessionChooserOpen, setNewSessionChooserOpen] = useState(false);
|
||||||
|
const [sessionMenuOpen, setSessionMenuOpen] = useState(false);
|
||||||
const [newSessionMode, setNewSessionMode] = useState<"agent" | "model">("model");
|
const [newSessionMode, setNewSessionMode] = useState<"agent" | "model">("model");
|
||||||
const [newSessionAgentId, setNewSessionAgentId] = useState<string>("");
|
const [newSessionAgentId, setNewSessionAgentId] = useState<string>("");
|
||||||
const [newSessionModel, setNewSessionModel] = useState<string>("");
|
const [newSessionModel, setNewSessionModel] = useState<string>("");
|
||||||
@@ -985,11 +988,15 @@ export function QuickChatFAB({
|
|||||||
refreshSessions,
|
refreshSessions,
|
||||||
skipNextSessionInitRef,
|
skipNextSessionInitRef,
|
||||||
} = useQuickChat(projectId, addToast);
|
} = useQuickChat(projectId, addToast);
|
||||||
|
const { experimentalFeatures } = useAppSettings();
|
||||||
|
const chatRoomsEnabled = experimentalFeatures?.chatRooms === true;
|
||||||
|
const roomsState = useChatRooms(projectId, addToast);
|
||||||
|
|
||||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||||
const fabRef = useRef<HTMLButtonElement | null>(null);
|
const fabRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const sessionMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
|
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
|
||||||
const shouldAutoFocusComposerRef = useRef(false);
|
const shouldAutoFocusComposerRef = useRef(false);
|
||||||
@@ -1648,6 +1655,60 @@ export function QuickChatFAB({
|
|||||||
});
|
});
|
||||||
}, [agents, models, sessions]);
|
}, [agents, models, sessions]);
|
||||||
|
|
||||||
|
const roomOptions = useMemo(
|
||||||
|
() => (chatRoomsEnabled ? roomsState.rooms : []),
|
||||||
|
[chatRoomsEnabled, roomsState.rooms],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showRoomGroups = chatRoomsEnabled && roomOptions.length > 0;
|
||||||
|
|
||||||
|
const activeSessionLabel = useMemo(() => {
|
||||||
|
if (showRoomGroups && roomsState.activeRoom) {
|
||||||
|
return `#${roomsState.activeRoom.name}`;
|
||||||
|
}
|
||||||
|
const activeOption = sessionOptions.find((option) => option.id === activeSession?.id);
|
||||||
|
if (activeOption) {
|
||||||
|
return activeOption.label;
|
||||||
|
}
|
||||||
|
if (sessionsLoading) {
|
||||||
|
return "Loading sessions…";
|
||||||
|
}
|
||||||
|
return "Select a session";
|
||||||
|
}, [activeSession?.id, roomsState.activeRoom, sessionOptions, sessionsLoading, showRoomGroups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSessionMenuOpen(false);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionMenuOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSessionMenuOutsideClick = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node;
|
||||||
|
if (sessionMenuRef.current?.contains(target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSessionMenuOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSessionMenuEscape = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
setSessionMenuOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleSessionMenuOutsideClick);
|
||||||
|
document.addEventListener("keydown", handleSessionMenuEscape);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleSessionMenuOutsideClick);
|
||||||
|
document.removeEventListener("keydown", handleSessionMenuEscape);
|
||||||
|
};
|
||||||
|
}, [sessionMenuOpen]);
|
||||||
|
|
||||||
const inputPlaceholder = useMemo(() => {
|
const inputPlaceholder = useMemo(() => {
|
||||||
if (chatMode === "agent") {
|
if (chatMode === "agent") {
|
||||||
if (selectedAgent) {
|
if (selectedAgent) {
|
||||||
@@ -1679,8 +1740,14 @@ export function QuickChatFAB({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void selectSession(selectedSession);
|
void selectSession(selectedSession);
|
||||||
|
setSessionMenuOpen(false);
|
||||||
}, [selectSession, sessions]);
|
}, [selectSession, sessions]);
|
||||||
|
|
||||||
|
const handleRoomSwitch = useCallback((roomId: string) => {
|
||||||
|
roomsState.selectRoom(roomId);
|
||||||
|
setSessionMenuOpen(false);
|
||||||
|
}, [roomsState]);
|
||||||
|
|
||||||
const handleCreateFreshSession = useCallback(async () => {
|
const handleCreateFreshSession = useCallback(async () => {
|
||||||
if (sessionsLoading) return;
|
if (sessionsLoading) return;
|
||||||
|
|
||||||
@@ -2405,18 +2472,65 @@ export function QuickChatFAB({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="quick-chat-panel-agent-select" data-testid="quick-chat-session-select">
|
<div className="quick-chat-panel-agent-select" data-testid="quick-chat-session-select">
|
||||||
<label htmlFor="quick-chat-session-select" className="visually-hidden">Select session</label>
|
<div className="quick-chat-session-menu" ref={sessionMenuRef}>
|
||||||
<select
|
<label htmlFor="quick-chat-session-dropdown-trigger" className="visually-hidden">Select session</label>
|
||||||
id="quick-chat-session-select"
|
<input type="hidden" data-testid="quick-chat-session-dropdown" value={activeSession?.id ?? ""} readOnly />
|
||||||
value={activeSession?.id ?? ""}
|
<button
|
||||||
onChange={(event) => handleSessionSwitch(event.target.value)}
|
id="quick-chat-session-dropdown-trigger"
|
||||||
data-testid="quick-chat-session-dropdown"
|
type="button"
|
||||||
|
className="btn quick-chat-session-trigger"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={sessionMenuOpen}
|
||||||
|
data-testid="quick-chat-session-dropdown-trigger"
|
||||||
|
onClick={() => setSessionMenuOpen((current) => !current)}
|
||||||
>
|
>
|
||||||
<option value="" disabled>{sessionsLoading ? "Loading sessions…" : "Select a session"}</option>
|
{activeSession?.modelProvider ? (
|
||||||
|
<ProviderIcon provider={activeSession.modelProvider} size="sm" />
|
||||||
|
) : (
|
||||||
|
<MessageSquare size={16} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
<span>{activeSessionLabel}</span>
|
||||||
|
<ChevronDown size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{sessionMenuOpen && (
|
||||||
|
<div className="quick-chat-session-dropdown" role="menu" data-testid="quick-chat-session-dropdown-menu">
|
||||||
|
{showRoomGroups && (
|
||||||
|
<div className="quick-chat-session-dropdown-group-label">Sessions</div>
|
||||||
|
)}
|
||||||
{sessionOptions.map((sessionOption) => (
|
{sessionOptions.map((sessionOption) => (
|
||||||
<option key={sessionOption.id} value={sessionOption.id}>{sessionOption.label}</option>
|
<button
|
||||||
|
key={sessionOption.id}
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
data-testid={`quick-chat-session-option-${sessionOption.id}`}
|
||||||
|
className={`quick-chat-session-option${roomsState.activeRoom === null && activeSession?.id === sessionOption.id ? " quick-chat-session-option--active" : ""}`}
|
||||||
|
onClick={() => handleSessionSwitch(sessionOption.id)}
|
||||||
|
>
|
||||||
|
{sessionOption.label}
|
||||||
|
</button>
|
||||||
))}
|
))}
|
||||||
</select>
|
|
||||||
|
{showRoomGroups && (
|
||||||
|
<>
|
||||||
|
<div className="quick-chat-session-dropdown-group-label">Rooms</div>
|
||||||
|
{roomOptions.map((room) => (
|
||||||
|
<button
|
||||||
|
key={room.id}
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
data-testid={`quick-chat-session-option-room-${room.slug}`}
|
||||||
|
className={`quick-chat-session-option${roomsState.activeRoom?.id === room.id ? " quick-chat-session-option--active" : ""}`}
|
||||||
|
onClick={() => handleRoomSwitch(room.id)}
|
||||||
|
>
|
||||||
|
#{room.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{newSessionChooserOpen && (
|
{newSessionChooserOpen && (
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import * as apiModule from "../../api";
|
|||||||
import { useAgents } from "../../hooks/useAgents";
|
import { useAgents } from "../../hooks/useAgents";
|
||||||
import { useViewportMode } from "../../hooks/useViewportMode";
|
import { useViewportMode } from "../../hooks/useViewportMode";
|
||||||
import { useMobileKeyboard } from "../../hooks/useMobileKeyboard";
|
import { useMobileKeyboard } from "../../hooks/useMobileKeyboard";
|
||||||
|
import { useAppSettings } from "../../hooks/useAppSettings";
|
||||||
|
import { useChatRooms } from "../../hooks/useChatRooms";
|
||||||
import { QuickChatFAB } from "../QuickChatFAB";
|
import { QuickChatFAB } from "../QuickChatFAB";
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("../../api", () => ({
|
||||||
@@ -24,6 +26,8 @@ vi.mock("../../api", () => ({
|
|||||||
vi.mock("../../hooks/useAgents", () => ({ useAgents: vi.fn() }));
|
vi.mock("../../hooks/useAgents", () => ({ useAgents: vi.fn() }));
|
||||||
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: vi.fn() }));
|
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: vi.fn() }));
|
||||||
vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: vi.fn() }));
|
vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: vi.fn() }));
|
||||||
|
vi.mock("../../hooks/useAppSettings", () => ({ useAppSettings: vi.fn() }));
|
||||||
|
vi.mock("../../hooks/useChatRooms", () => ({ useChatRooms: vi.fn() }));
|
||||||
|
|
||||||
const mockFetchResumeChatSession = vi.mocked(apiModule.fetchResumeChatSession);
|
const mockFetchResumeChatSession = vi.mocked(apiModule.fetchResumeChatSession);
|
||||||
const mockFetchChatSessions = vi.mocked(apiModule.fetchChatSessions);
|
const mockFetchChatSessions = vi.mocked(apiModule.fetchChatSessions);
|
||||||
@@ -36,6 +40,8 @@ const mockCancelChatResponse = vi.mocked(apiModule.cancelChatResponse);
|
|||||||
const mockUseAgents = vi.mocked(useAgents);
|
const mockUseAgents = vi.mocked(useAgents);
|
||||||
const mockUseViewportMode = vi.mocked(useViewportMode);
|
const mockUseViewportMode = vi.mocked(useViewportMode);
|
||||||
const mockUseMobileKeyboard = vi.mocked(useMobileKeyboard);
|
const mockUseMobileKeyboard = vi.mocked(useMobileKeyboard);
|
||||||
|
const mockUseAppSettings = vi.mocked(useAppSettings);
|
||||||
|
const mockUseChatRooms = vi.mocked(useChatRooms);
|
||||||
|
|
||||||
const agents: Agent[] = [
|
const agents: Agent[] = [
|
||||||
{ id: "agent-001", name: "Agent One", role: "executor", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} },
|
{ id: "agent-001", name: "Agent One", role: "executor", state: "active", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), metadata: {} },
|
||||||
@@ -117,6 +123,23 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
viewportOffsetTop: 0,
|
viewportOffsetTop: 0,
|
||||||
keyboardOpen: false,
|
keyboardOpen: false,
|
||||||
});
|
});
|
||||||
|
mockUseAppSettings.mockReturnValue({
|
||||||
|
experimentalFeatures: {},
|
||||||
|
} as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [],
|
||||||
|
roomsLoading: false,
|
||||||
|
roomsError: null,
|
||||||
|
activeRoom: null,
|
||||||
|
activeRoomMembers: [],
|
||||||
|
messages: [],
|
||||||
|
messagesLoading: false,
|
||||||
|
selectRoom: vi.fn(),
|
||||||
|
createRoom: vi.fn(),
|
||||||
|
deleteRoom: vi.fn(),
|
||||||
|
sendRoomMessage: vi.fn(),
|
||||||
|
refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
mockFetchResumeChatSession.mockImplementation(async ({ agentId, modelProvider, modelId }) => ({
|
mockFetchResumeChatSession.mockImplementation(async ({ agentId, modelProvider, modelId }) => ({
|
||||||
session: resolveResumeSession(agentId, modelProvider, modelId),
|
session: resolveResumeSession(agentId, modelProvider, modelId),
|
||||||
}));
|
}));
|
||||||
@@ -150,8 +173,64 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
|
|
||||||
expect(await screen.findByTestId("quick-chat-session-dropdown")).toBeInTheDocument();
|
expect(await screen.findByTestId("quick-chat-session-dropdown")).toBeInTheDocument();
|
||||||
expect(screen.queryByTestId("quick-chat-mode-toggle")).toBeNull();
|
expect(screen.queryByTestId("quick-chat-mode-toggle")).toBeNull();
|
||||||
expect(screen.getByRole("option", { name: "Model thread — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
expect(screen.getByRole("option", { name: "Session 2 — Agent One" })).toBeInTheDocument();
|
expect(screen.getByTestId("quick-chat-session-option-session-model")).toHaveClass("quick-chat-session-option--active");
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-session-agent")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render room options when chat rooms are disabled", async () => {
|
||||||
|
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"));
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("quick-chat-session-option-room-engineering")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens/closes session menu and handles rooms when enabled", async () => {
|
||||||
|
const selectRoom = vi.fn();
|
||||||
|
mockUseAppSettings.mockReturnValue({ experimentalFeatures: { chatRooms: true } } as ReturnType<typeof useAppSettings>);
|
||||||
|
mockUseChatRooms.mockReturnValue({
|
||||||
|
rooms: [{ id: "room-1", name: "engineering", slug: "engineering", memberCount: 2, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }],
|
||||||
|
roomsLoading: false,
|
||||||
|
roomsError: null,
|
||||||
|
activeRoom: { id: "room-1", name: "engineering", slug: "engineering", memberCount: 2, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||||
|
activeRoomMembers: [],
|
||||||
|
messages: [],
|
||||||
|
messagesLoading: false,
|
||||||
|
selectRoom,
|
||||||
|
createRoom: vi.fn(),
|
||||||
|
deleteRoom: vi.fn(),
|
||||||
|
sendRoomMessage: vi.fn(),
|
||||||
|
refreshRooms: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
|
const trigger = await screen.findByTestId("quick-chat-session-dropdown-trigger");
|
||||||
|
expect(trigger).toHaveTextContent("#engineering");
|
||||||
|
fireEvent.click(trigger);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("quick-chat-session-dropdown-menu")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Sessions")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Rooms")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-session-model")).toHaveClass("quick-chat-session-option");
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-room-engineering")).toHaveClass("quick-chat-session-option--active");
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-room-engineering"));
|
||||||
|
expect(selectRoom).toHaveBeenCalledWith("room-1");
|
||||||
|
|
||||||
|
fireEvent.click(trigger);
|
||||||
|
fireEvent.mouseDown(screen.getByTestId("quick-chat-new-thread"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByTestId("quick-chat-session-dropdown-menu")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(trigger);
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-session-model"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockFetchChatMessages).toHaveBeenCalledWith("session-model", { limit: 50 }, "proj-1");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens inline chooser from new button defaulting to model", async () => {
|
it("opens inline chooser from new button defaulting to model", async () => {
|
||||||
@@ -218,7 +297,8 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message Agent Two");
|
expect(screen.getByTestId("quick-chat-input")).toHaveAttribute("placeholder", "Message Agent Two");
|
||||||
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-latest");
|
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-latest");
|
||||||
});
|
});
|
||||||
expect(screen.getByRole("option", { name: /Claude thread/i })).toBeInTheDocument();
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-archived-newest")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reopen restores the newest active session by max(lastMessageAt, updatedAt)", async () => {
|
it("reopen restores the newest active session by max(lastMessageAt, updatedAt)", async () => {
|
||||||
@@ -247,7 +327,8 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("newer-last-message");
|
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("newer-last-message");
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("quick-chat-session-dropdown"), { target: { value: "older-updated" } });
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-older-updated"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("older-updated");
|
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("older-updated");
|
||||||
});
|
});
|
||||||
@@ -293,7 +374,8 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-latest");
|
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-latest");
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.change(screen.getByTestId("quick-chat-session-dropdown"), { target: { value: "active-older" } });
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-active-older"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-older");
|
expect(screen.getByTestId("quick-chat-session-dropdown")).toHaveValue("active-older");
|
||||||
});
|
});
|
||||||
@@ -372,15 +454,17 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
expect(await screen.findByRole("option", { name: "Session 1 — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
fireEvent.click(await screen.findByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
expect(screen.getByRole("option", { name: "Session 2 — Claude 3.7 Sonnet [anthropic/claude-3-7-sonnet]" })).toBeInTheDocument();
|
expect(screen.getByTestId("quick-chat-session-option-session-openai")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-session-anthropic")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes both title and model descriptor in session label", async () => {
|
it("includes both title and model descriptor in session label", async () => {
|
||||||
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
expect(await screen.findByRole("option", { name: "Model thread — GPT-4o [openai/gpt-4o]" })).toBeInTheDocument();
|
fireEvent.click(await screen.findByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
expect(screen.getByTestId("quick-chat-session-option-session-model")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses icon-only model tag without pill styling when mobile header fallback is active", async () => {
|
it("uses icon-only model tag without pill styling when mobile header fallback is active", async () => {
|
||||||
@@ -577,9 +661,9 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
|
||||||
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
fireEvent.click(screen.getByTestId("quick-chat-fab"));
|
||||||
|
|
||||||
const select = await screen.findByTestId("quick-chat-session-dropdown");
|
await screen.findByTestId("quick-chat-session-dropdown");
|
||||||
await screen.findByRole("option", { name: "Session 2 — Agent One" });
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
fireEvent.change(select, { target: { value: "session-agent" } });
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-session-agent"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockFetchChatMessages).toHaveBeenCalledWith("session-agent", { limit: 50 }, "proj-1");
|
expect(mockFetchChatMessages).toHaveBeenCalledWith("session-agent", { limit: 50 }, "proj-1");
|
||||||
@@ -970,7 +1054,9 @@ describe("QuickChatFAB session-first UX", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.change(await screen.findByTestId("quick-chat-session-dropdown"), { target: { value: "session-agent" } });
|
await screen.findByTestId("quick-chat-session-dropdown");
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-dropdown-trigger"));
|
||||||
|
fireEvent.click(screen.getByTestId("quick-chat-session-option-session-agent"));
|
||||||
scrollHeightValue = 1700;
|
scrollHeightValue = 1700;
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user