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:
Fusion
2026-05-15 09:39:30 -07:00
committed by gsxdsm
parent 4d99a72c94
commit ea9ec50a1d
5 changed files with 340 additions and 24 deletions

View File

@@ -26,6 +26,8 @@ import { FileMentionPopup } from "./FileMentionPopup";
import { useFileMention } from "../hooks/useFileMention";
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
import { useViewportMode } from "../hooks/useViewportMode";
import { useAppSettings } from "../hooks/useAppSettings";
import { useChatRooms } from "../hooks/useChatRooms";
interface PendingAttachment {
file: File;
@@ -893,6 +895,7 @@ export function QuickChatFAB({
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
const [newSessionChooserOpen, setNewSessionChooserOpen] = useState(false);
const [sessionMenuOpen, setSessionMenuOpen] = useState(false);
const [newSessionMode, setNewSessionMode] = useState<"agent" | "model">("model");
const [newSessionAgentId, setNewSessionAgentId] = useState<string>("");
const [newSessionModel, setNewSessionModel] = useState<string>("");
@@ -985,11 +988,15 @@ export function QuickChatFAB({
refreshSessions,
skipNextSessionInitRef,
} = useQuickChat(projectId, addToast);
const { experimentalFeatures } = useAppSettings();
const chatRoomsEnabled = experimentalFeatures?.chatRooms === true;
const roomsState = useChatRooms(projectId, addToast);
const panelRef = useRef<HTMLDivElement | null>(null);
const fabRef = useRef<HTMLButtonElement | null>(null);
const messagesRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLTextAreaElement | null>(null);
const sessionMenuRef = useRef<HTMLDivElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
const shouldAutoFocusComposerRef = useRef(false);
@@ -1648,6 +1655,60 @@ export function QuickChatFAB({
});
}, [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(() => {
if (chatMode === "agent") {
if (selectedAgent) {
@@ -1679,8 +1740,14 @@ export function QuickChatFAB({
}
void selectSession(selectedSession);
setSessionMenuOpen(false);
}, [selectSession, sessions]);
const handleRoomSwitch = useCallback((roomId: string) => {
roomsState.selectRoom(roomId);
setSessionMenuOpen(false);
}, [roomsState]);
const handleCreateFreshSession = useCallback(async () => {
if (sessionsLoading) return;
@@ -2405,18 +2472,65 @@ export function QuickChatFAB({
</div>
<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>
<select
id="quick-chat-session-select"
value={activeSession?.id ?? ""}
onChange={(event) => handleSessionSwitch(event.target.value)}
data-testid="quick-chat-session-dropdown"
>
<option value="" disabled>{sessionsLoading ? "Loading sessions…" : "Select a session"}</option>
{sessionOptions.map((sessionOption) => (
<option key={sessionOption.id} value={sessionOption.id}>{sessionOption.label}</option>
))}
</select>
<div className="quick-chat-session-menu" ref={sessionMenuRef}>
<label htmlFor="quick-chat-session-dropdown-trigger" className="visually-hidden">Select session</label>
<input type="hidden" data-testid="quick-chat-session-dropdown" value={activeSession?.id ?? ""} readOnly />
<button
id="quick-chat-session-dropdown-trigger"
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)}
>
{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) => (
<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>
))}
{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>
{newSessionChooserOpen && (