feat(FN-3022): merge fusion/fn-3022

Commits merged:
- feat(FN-3022): complete Step 4 — document mobile quick chat autofocus behavior
- test(FN-3022): complete Step 2 — cover mobile autofocus and delayed enable path
- feat(FN-3022): complete Step 1 — reliable mobile autofocus when composer becomes ready

Files changed:
docs/dashboard-guide.md                            |  1 +
 packages/dashboard/app/components/QuickChatFAB.tsx | 42 +++++++++++++++---
 .../app/components/__tests__/QuickChatFAB.test.tsx | 51 ++++++++++++++++++++++
 3 files changed, 89 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-3022
This commit is contained in:
Fusion
2026-04-30 00:27:50 -07:00
committed by gsxdsm
parent dc96d572cb
commit b359cb8672
3 changed files with 91 additions and 7 deletions

View File

@@ -806,6 +806,7 @@ export function QuickChatFAB({
const inputRef = useRef<HTMLInputElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const pendingAttachmentsRef = useRef<PendingAttachment[]>([]);
const shouldAutoFocusComposerRef = useRef(false);
const resolvedModelSelection = selectedModel || configuredDefaultModelSelection;
@@ -834,6 +835,7 @@ export function QuickChatFAB({
}, [chatMode, parsedModelSelection, selectedAgentId]);
const hasChatTarget = chatMode === "agent" ? Boolean(selectedAgentId) : Boolean(parsedModelSelection);
const inputDisabled = !hasChatTarget || !activeSession;
useEffect(() => {
if (agents.length === 0) {
@@ -968,13 +970,45 @@ export function QuickChatFAB({
}, [pendingAttachments]);
useEffect(() => {
if (!isOpen) return;
const frame = requestAnimationFrame(() => {
inputRef.current?.focus();
});
return () => cancelAnimationFrame(frame);
if (!isOpen) {
shouldAutoFocusComposerRef.current = false;
return;
}
if (typeof window === "undefined") {
return;
}
shouldAutoFocusComposerRef.current = window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT;
}, [isOpen]);
useEffect(() => {
if (!isOpen || inputDisabled || !shouldAutoFocusComposerRef.current) {
return;
}
const input = inputRef.current;
if (!input) {
return;
}
const activeElement = document.activeElement;
const panelContainsFocus = activeElement ? panelRef.current?.contains(activeElement) : false;
const isBodyFocused = activeElement === document.body;
if (!panelContainsFocus && !isBodyFocused) {
shouldAutoFocusComposerRef.current = false;
return;
}
const frame = requestAnimationFrame(() => {
input.focus();
shouldAutoFocusComposerRef.current = false;
});
return () => cancelAnimationFrame(frame);
}, [isOpen, inputDisabled]);
// Attachment object URLs must be revoked when the composer unmounts.
useEffect(() => {
return () => {
@@ -1086,8 +1120,6 @@ export function QuickChatFAB({
return "Select a model to start chatting";
}, [chatMode, selectedAgent, selectedModelTag]);
const inputDisabled = !hasChatTarget || !activeSession;
const pendingPreview = pendingMessage.length > 50
? `${pendingMessage.slice(0, 50)}`
: pendingMessage;