diff --git a/.changeset/fn-7120-mobile-new-chat.md b/.changeset/fn-7120-mobile-new-chat.md
new file mode 100644
index 0000000000..c14e197ee9
--- /dev/null
+++ b/.changeset/fn-7120-mobile-new-chat.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: Add a New Chat button to the mobile chat quick-switch dropdown.
+category: feature
+dev: New `chat-mobile-session-new` menuitem in ChatView's mobile session switcher reuses the existing setShowNewDialog/NewChatDialog path; Direct-scope only.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 012c3bac78..2657ccbf84 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -349,7 +349,7 @@ Chat view provides project-scoped conversations with agents.
- If you queue a follow-up user message while the assistant is still streaming, Chat persists that queued text per session, shows the queued preview above the input box with a divider, and restores/sends it once the active response finishes if you leave and return.
- Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail.
- On mobile direct-chat threads, entering a thread and restoring Chat after tab/page visibility returns re-anchors to the newest message (`scrollTop = scrollHeight`) so the view always opens at the live tail.
-- On mobile direct-chat threads, tapping the active title/identity in the thread header opens a lightweight conversation dropdown so you can switch to another direct session without backing out to the sidebar list first; long conversation titles now stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
+- On mobile direct-chat threads, tapping the active title/identity in the thread header opens a lightweight conversation dropdown so you can switch to another direct session or start a New Chat without backing out to the sidebar list first; long conversation titles now stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
- Direct chat sessions can be renamed from the desktop conversation context menu and from the mobile session switcher; blank rename submissions clear the custom title so the default session label is shown again.
- On mobile (`max-width: 768px`), chat bubbles are slightly wider in full Chat for improved readability while preserving header/composer gutters. On tablet-width main Chat containers, assistant/agent, streaming, and failure bubbles use a wider reading measure for long responses while user bubbles and narrow floating Quick Chat hosts stay bounded.
diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css
index dfa29353f6..38dca12c68 100644
--- a/packages/dashboard/app/components/ChatView.css
+++ b/packages/dashboard/app/components/ChatView.css
@@ -700,6 +700,37 @@ Mobile chat session switching needs a dedicated rename tap target beside each se
background: color-mix(in srgb, var(--todo) 12%, transparent);
}
+.chat-mobile-session-new {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ width: 100%;
+ margin-top: var(--space-xs);
+ padding: var(--space-sm) var(--space-md);
+ border: none;
+ border-top: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--text);
+ cursor: pointer;
+ text-align: left;
+ line-height: normal;
+}
+
+.chat-mobile-session-new svg {
+ flex-shrink: 0;
+ color: var(--text-muted);
+}
+
+.chat-mobile-session-new:hover {
+ background: var(--card-hover);
+}
+
+.chat-mobile-session-new:focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring-strong);
+}
+
.chat-mobile-session-option-title {
width: 100%;
display: block;
diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx
index c579778cf4..55b66b0a88 100644
--- a/packages/dashboard/app/components/ChatView.tsx
+++ b/packages/dashboard/app/components/ChatView.tsx
@@ -3992,6 +3992,23 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa
))}
+ {/*
+ FNXC:Chat 2026-06-27-00:00:
+ Mobile Direct-scope quick session switching must let users start a new chat without leaving the open thread. Route this affordance through the same setShowNewDialog(true) / NewChatDialog path as the header and sidebar-footer controls.
+ */}
+
)}
diff --git a/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx
index e320fc349d..f76088d051 100644
--- a/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx
+++ b/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx
@@ -177,15 +177,6 @@ describe("ChatView mobile behavior", () => {
return { listeners, mockVV };
}
- function ensureMatchMedia() {
- if (!window.matchMedia) {
- Object.defineProperty(window, "matchMedia", {
- writable: true,
- value: vi.fn(),
- });
- }
- }
-
function mockMobileViewport() {
ensureMatchMedia();
Object.defineProperty(window, "innerWidth", { value: 375, configurable: true });
@@ -342,6 +333,7 @@ describe("ChatView mobile behavior", () => {
await renderWithAct();
expect(screen.queryByTestId("chat-mobile-session-trigger")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("chat-mobile-session-new")).not.toBeInTheDocument();
expect(screen.getByText("#backend")).toBeInTheDocument();
} finally {
localStorage.setItem("fusion:chat-scope", "direct");
@@ -349,6 +341,111 @@ describe("ChatView mobile behavior", () => {
}
});
+ it("mobile mode: quick session switcher New Chat opens the dialog and closes the menu", async () => {
+ const restoreMatchMedia = mockMobileViewport();
+ try {
+ setupMockChat({
+ sessions: [activeSessionFixture],
+ filteredSessions: [activeSessionFixture],
+ activeSession: activeSessionFixture,
+ });
+
+ await renderWithAct();
+
+ await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
+ const dropdown = screen.getByTestId("chat-mobile-session-dropdown");
+ const newChatItem = within(dropdown).getByRole("menuitem", { name: /new chat/i });
+
+ expect(newChatItem).toBe(screen.getByTestId("chat-mobile-session-new"));
+ await userEvent.click(newChatItem);
+
+ expect(screen.queryByTestId("chat-mobile-session-dropdown")).not.toBeInTheDocument();
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ } finally {
+ restoreMatchMedia.mockRestore();
+ }
+ });
+
+ it("mobile mode: quick session switcher New Chat renders for single and multiple session lists", async () => {
+ const restoreMatchMedia = mockMobileViewport();
+ try {
+ setupMockChat({
+ sessions: [activeSessionFixture],
+ filteredSessions: [activeSessionFixture],
+ activeSession: activeSessionFixture,
+ });
+ const singleRender = await renderWithAct();
+
+ await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
+ expect(screen.getByTestId("chat-mobile-session-new")).toBeInTheDocument();
+ expect(screen.getByRole("menuitem", { name: /new chat/i })).toBeInTheDocument();
+
+ singleRender.unmount();
+
+ const anotherSession = {
+ id: "session-002",
+ agentId: "agent-002",
+ status: "active" as const,
+ title: "Another Chat",
+ createdAt: "2026-04-07T00:00:00.000Z",
+ updatedAt: "2026-04-07T00:00:00.000Z",
+ };
+ setupMockChat({
+ sessions: [activeSessionFixture, anotherSession],
+ filteredSessions: [activeSessionFixture, anotherSession],
+ activeSession: activeSessionFixture,
+ });
+ await renderWithAct();
+
+ await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
+ expect(screen.getByTestId("chat-mobile-session-new")).toBeInTheDocument();
+ expect(screen.getAllByRole("menuitem")).toHaveLength(3);
+ } finally {
+ restoreMatchMedia.mockRestore();
+ }
+ });
+
+ it("floating narrow mode: quick session switcher includes New Chat", async () => {
+ const restoreMatchMedia = mockDesktopViewport();
+ const originalResizeObserver = globalThis.ResizeObserver;
+ const rectSpy = vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
+ x: 0,
+ y: 0,
+ width: 560,
+ height: 640,
+ top: 0,
+ right: 560,
+ bottom: 640,
+ left: 0,
+ toJSON: () => ({}),
+ });
+
+ class MockResizeObserver implements ResizeObserver {
+ readonly observe = vi.fn();
+ readonly unobserve = vi.fn();
+ readonly disconnect = vi.fn();
+ constructor(_callback: ResizeObserverCallback) {}
+ }
+
+ globalThis.ResizeObserver = MockResizeObserver;
+ try {
+ setupMockChat({
+ sessions: [activeSessionFixture],
+ filteredSessions: [activeSessionFixture],
+ activeSession: activeSessionFixture,
+ });
+
+ await renderWithAct();
+
+ await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
+ expect(screen.getByTestId("chat-mobile-session-new")).toBeInTheDocument();
+ } finally {
+ globalThis.ResizeObserver = originalResizeObserver;
+ rectSpy.mockRestore();
+ restoreMatchMedia.mockRestore();
+ }
+ });
+
it("mobile mode: iOS first tap focuses direct composer without blocking native focus, then sends", async () => {
const restoreMatchMedia = mockMobileViewport();
const isIOSSpy = vi.spyOn(mobileScrollLock, "isIOS").mockReturnValue(true);