diff --git a/.changeset/fn-6518-quick-chat-focus.md b/.changeset/fn-6518-quick-chat-focus.md
new file mode 100644
index 0000000000..778ab6126a
--- /dev/null
+++ b/.changeset/fn-6518-quick-chat-focus.md
@@ -0,0 +1,5 @@
+---
+"@runfusion/fusion": patch
+---
+
+Bringing up Quick Chat now focuses the composer input on desktop (matching existing mobile behavior).
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 4404c7c2c0..7d0a42eb7a 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -302,7 +302,7 @@ Quick Chat is an optional floating panel for fast, project-scoped assistant conv
- Resume lookups still use targeted session queries instead of loading the full active-session list first
- Tool-call summaries in the floating quick-chat panel are intentionally condensed into a single-line header row (especially on small screens) so tool name + status stay scannable without multi-line wrapping
- Question tool calls use the same shared response card as full Chat, with compact spacing in the floating panel and read-only answered history so Quick Chat can continue agent clarification loops without exposing raw tool JSON.
-- On mobile viewports, opening Quick Chat auto-focuses the composer as soon as it is ready so the keyboard opens immediately
+- Opening Quick Chat auto-focuses the composer as soon as it is ready on desktop and mobile viewports; mobile additionally uses the stealth-input handoff so the soft keyboard opens immediately
- FAB dragging uses pointer events with document-level move/up tracking and a 5px drag threshold so Android touch drags reposition reliably while short taps still open Quick Chat
- Quick Chat now mirrors full Chat tail behavior: if you scroll up, live updates stop auto-following and a **Latest** jump control appears until you jump back down.
- On mobile, Quick Chat re-anchors to the newest message whenever the panel is opened/reopened and when page visibility is restored, while still preserving the near-bottom gate so intentional scroll-away keeps **Latest** jump behavior.
diff --git a/packages/dashboard/app/components/QuickChatFAB.tsx b/packages/dashboard/app/components/QuickChatFAB.tsx
index e3eaa9d026..dd1c917bfa 100644
--- a/packages/dashboard/app/components/QuickChatFAB.tsx
+++ b/packages/dashboard/app/components/QuickChatFAB.tsx
@@ -1616,7 +1616,11 @@ export function QuickChatFAB({
return;
}
- shouldAutoFocusComposerRef.current = window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT;
+ /*
+ FNXC:QuickChat 2026-06-17-02:50:
+ Bringing up Quick Chat must focus the composer on every viewport so typing can start immediately. Mobile still claims the iOS keyboard through the stealth input first; the ready-state focus effect keeps that synchronous handoff while desktop reaches its requestAnimationFrame focus path.
+ */
+ shouldAutoFocusComposerRef.current = true;
}, [isOpen]);
useEffect(() => {
diff --git a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx
index 8a85ba9cee..e13c1b24d2 100644
--- a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx
+++ b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx
@@ -161,6 +161,32 @@ function setQuickChatVisualViewportSample(
});
}
+function mockRequestAnimationFrames() {
+ const originalRaf = window.requestAnimationFrame;
+ const originalCancelRaf = window.cancelAnimationFrame;
+ const rafQueue: FrameRequestCallback[] = [];
+ window.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => {
+ rafQueue.push(cb);
+ return rafQueue.length;
+ });
+ window.cancelAnimationFrame = vi.fn();
+
+ return {
+ async drain() {
+ await act(async () => {
+ while (rafQueue.length > 0) {
+ const cb = rafQueue.shift();
+ cb?.(performance.now());
+ }
+ });
+ },
+ restore() {
+ window.requestAnimationFrame = originalRaf;
+ window.cancelAnimationFrame = originalCancelRaf;
+ },
+ };
+}
+
describe("QuickChatFAB session-first UX", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -1056,6 +1082,82 @@ describe("QuickChatFAB session-first UX", () => {
expect(screen.getByTestId("quick-chat-session-option-session-model")).toBeInTheDocument();
});
+ it("FN-6518: desktop opening Quick Chat focuses the enabled composer", async () => {
+ const raf = mockRequestAnimationFrames();
+
+ try {
+ render();
+ fireEvent.click(screen.getByTestId("quick-chat-fab"));
+
+ const input = await screen.findByTestId("quick-chat-input") as HTMLTextAreaElement;
+ await waitFor(() => expect(input).not.toBeDisabled());
+ await raf.drain();
+
+ expect(document.activeElement).toBe(input);
+ } finally {
+ raf.restore();
+ }
+ });
+
+ it("FN-6518: desktop composer focuses after the session becomes ready post-open", async () => {
+ const raf = mockRequestAnimationFrames();
+ const deferredSessions = createDeferredPromise<{ sessions: ChatSession[] }>();
+ mockFetchChatSessions.mockImplementationOnce(() => deferredSessions.promise);
+
+ try {
+ render();
+ fireEvent.click(screen.getByTestId("quick-chat-fab"));
+
+ const input = await screen.findByTestId("quick-chat-input") as HTMLTextAreaElement;
+ expect(input).toBeDisabled();
+ deferredSessions.resolve({ sessions: [modelSession, agentSession] });
+ await waitFor(() => expect(input).not.toBeDisabled());
+ await raf.drain();
+
+ expect(document.activeElement).toBe(input);
+ } finally {
+ raf.restore();
+ }
+ });
+
+ it("FN-6518: mobile opening Quick Chat hands focus from stealth input to composer", async () => {
+ Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
+ window.dispatchEvent(new Event("resize"));
+ mockUseViewportMode.mockReturnValue("mobile");
+
+ render();
+ fireEvent.click(screen.getByTestId("quick-chat-fab"));
+
+ const input = await screen.findByTestId("quick-chat-input") as HTMLTextAreaElement;
+ await waitFor(() => expect(input).not.toBeDisabled());
+
+ expect(document.activeElement).toBe(input);
+ });
+
+ it("FN-6518: auto-focus does not steal focus from an external control", async () => {
+ const raf = mockRequestAnimationFrames();
+ const externalFocusTarget = document.createElement("button");
+ externalFocusTarget.type = "button";
+ externalFocusTarget.textContent = "External focus target";
+ document.body.appendChild(externalFocusTarget);
+
+ try {
+ const { rerender } = render();
+ externalFocusTarget.focus();
+ expect(document.activeElement).toBe(externalFocusTarget);
+
+ rerender();
+ const input = await screen.findByTestId("quick-chat-input") as HTMLTextAreaElement;
+ await waitFor(() => expect(input).not.toBeDisabled());
+ await raf.drain();
+
+ expect(document.activeElement).toBe(externalFocusTarget);
+ } finally {
+ externalFocusTarget.remove();
+ raf.restore();
+ }
+ });
+
it("FN-6301: iOS first tap focuses composer without canceling native focus, then sends", async () => {
Object.defineProperty(window, "innerWidth", { configurable: true, value: 390 });
window.dispatchEvent(new Event("resize"));