diff --git a/.changeset/fn-7934-chat-narrow-model-popup.md b/.changeset/fn-7934-chat-narrow-model-popup.md new file mode 100644 index 0000000000..7552e655cc --- /dev/null +++ b/.changeset/fn-7934-chat-narrow-model-popup.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the in-chat model/thinking popup being cut off inside a narrow floating Chat window. +category: fix +dev: The popover's viewport-fitting inset is now keyed on ChatView's .chat-view--narrow class (surface width, incl. floating window / compact dock) instead of only @media (max-width: 768px) (browser viewport), so a narrow floating Chat window on a wide viewport no longer clips the popup. CustomModelDropdown is unchanged. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 633e696823..c4fda79385 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -523,7 +523,8 @@ Chat view provides project-scoped conversations with agents. -- A small **Brain**-icon button next to the composer's attach button lets you change an already-created direct chat session's target and thinking level mid-conversation, without starting a new chat. Its **Model / Agent** section can switch the session to another model via the shared model picker or to a real agent from the agent list; its **Thinking level** section still lists the six thinking levels plus **Default** (clear/inherit, labeled with the current resolved default such as **Default (medium)**). Each selection persists immediately and applies starting with the session's next send, including on mobile and tablet touch viewports where the popup stays fitted to the screen. This control appears only for non-CLI Direct sessions — it is not shown for CLI-agent-backed sessions or in Chat Rooms, neither of which support this per-session retargeting control. + +- A small **Brain**-icon button next to the composer's attach button lets you change an already-created direct chat session's target and thinking level mid-conversation, without starting a new chat. Its **Model / Agent** section can switch the session to another model via the shared model picker or to a real agent from the agent list; its **Thinking level** section still lists the six thinking levels plus **Default** (clear/inherit, labeled with the current resolved default such as **Default (medium)**). Each selection persists immediately and applies starting with the session's next send, including on mobile/tablet touch viewports and narrow floating Chat windows or compact docks where the popup stays fitted to the chat surface. This control appears only for non-CLI Direct sessions — it is not shown for CLI-agent-backed sessions or in Chat Rooms, neither of which support this per-session retargeting control. - Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately) - Final assistant messages with no text, tool calls, thinking output, attachments, or failure details render a muted **No message** placeholder instead of a blank bubble. In-progress responses still use the existing **Working…** / **Thinking…** streaming state until the run finishes. diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index 0c54080b86..e3c1102c52 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -2046,6 +2046,28 @@ FN-7908 keeps model/agent retargeting inside the same brain popup. The widened p z-index: 50; } +/* +FNXC:Chat-ModelSwitch 2026-07-13-00:00: +FN-7934: The brain popup must fit the containing chat surface, not only the browser viewport. Key the inset layout on .chat-view--narrow because it reflects surface width for floating Chat windows and compact docks; otherwise a narrow floating Chat window on a wide viewport clips the Model / Agent picker on the right. +*/ +.chat-view--narrow .chat-thinking-level-root { + position: static; +} + +.chat-view--narrow .chat-thinking-popover { + left: var(--space-md); + right: var(--space-md); + width: auto; + max-width: none; + max-inline-size: none; + max-height: min(calc(var(--space-xl) * 20), calc(100vh - (var(--space-xl) * 5))); +} + +.chat-view--narrow .chat-thinking-agent-list, +.chat-view--narrow .chat-thinking-popover-list { + max-height: calc(var(--space-xl) * 7); +} + .chat-thinking-target-section, .chat-thinking-level-section { padding: var(--space-sm); diff --git a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx index 9eb0a46fd0..9c6e26cb07 100644 --- a/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatThinkingLevelControl.test.tsx @@ -1,3 +1,5 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { fireEvent, render, screen } from "@testing-library/react"; import { THINKING_LEVELS } from "@fusion/core"; @@ -28,6 +30,13 @@ const agents = [ { id: "agent-002", name: "Beta", role: "reviewer" }, ]; +const chatViewCss = () => readFileSync(resolve(__dirname, "../ChatView.css"), "utf-8"); + +function cssRule(css: string, selector: string) { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+"); + return css.match(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`))?.[1] ?? ""; +} + describe("ChatThinkingLevelControl", () => { beforeEach(() => { vi.clearAllMocks(); @@ -222,3 +231,26 @@ describe("ChatThinkingLevelControl", () => { expect(screen.queryByRole("listbox")).toBeNull(); }); }); + +describe("ChatThinkingLevelControl CSS contract", () => { + it("keeps the popover fit keyed to narrow chat surfaces while preserving desktop sizing", () => { + const css = chatViewCss(); + const desktopPopoverRule = cssRule(css, ".chat-thinking-popover"); + const narrowRootRule = cssRule(css, ".chat-view--narrow .chat-thinking-level-root"); + const narrowPopoverRule = cssRule(css, ".chat-view--narrow .chat-thinking-popover"); + const narrowListRule = cssRule(css, ".chat-view--narrow .chat-thinking-agent-list,\n.chat-view--narrow .chat-thinking-popover-list"); + + expect(desktopPopoverRule).toContain("left: 0;"); + expect(desktopPopoverRule).toContain("width: min(calc(var(--space-xl) * 15), calc(100vw - (var(--space-lg) * 2)));"); + expect(desktopPopoverRule).toContain("max-width: calc(100vw - (var(--space-lg) * 2));"); + + expect(narrowRootRule).toContain("position: static;"); + expect(narrowPopoverRule).toContain("left: var(--space-md);"); + expect(narrowPopoverRule).toContain("right: var(--space-md);"); + expect(narrowPopoverRule).toContain("width: auto;"); + expect(narrowPopoverRule).toContain("max-width: none;"); + expect(narrowPopoverRule).toContain("max-inline-size: none;"); + expect(narrowPopoverRule).toContain("max-height: min(calc(var(--space-xl) * 20), calc(100vh - (var(--space-xl) * 5)));"); + expect(narrowListRule).toContain("max-height: calc(var(--space-xl) * 7);"); + }); +});