FN-7934: fix chat brain popup clipping in narrow floating windows

Fixes the in-chat model/thinking (Brain) popup being cut off inside a narrow floating Chat window or compact dock on a wide desktop viewport.

- Key the popover's viewport-fitting inset layout on ChatView's .chat-view--narrow class (chat surface width) instead of only the @media (max-width: 768px) browser-viewport query, so narrow floating/docked chat surfaces get the fitted layout too.
- Add narrow-surface CSS rules for .chat-thinking-level-root, .chat-thinking-popover, .chat-thinking-agent-list, and .chat-thinking-popover-list to constrain position/width/max-height to the chat surface.
- Add a CSS-contract regression test asserting both the desktop popover sizing and the new narrow-surface rules stay in sync.
- Update docs/dashboard-guide.md to describe the popup staying fitted to the chat surface for narrow floating Chat windows/compact docks, not just mobile/tablet viewports.
- Add a patch changeset for @runfusion/fusion documenting the fix.

Files changed:
 .changeset/fn-7934-chat-narrow-model-popup.md      |  7 +++++
 docs/dashboard-guide.md                            |  3 +-
 packages/dashboard/app/components/ChatView.css     | 22 +++++++++++++++
 .../__tests__/ChatThinkingLevelControl.test.tsx    | 32 ++++++++++++++++++++++
 4 files changed, 63 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7934

Fusion-Task-Lineage: 30c461c5-a153-4005-8a8c-24f02916a934

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-13 00:48:02 -07:00
parent 9ba8a2e575
commit 29560021d3
4 changed files with 63 additions and 1 deletions

View File

@@ -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.

View File

@@ -523,7 +523,8 @@ Chat view provides project-scoped conversations with agents.
<!-- FNXC:Chat-ThinkingLevel 2026-07-12-20:13: FN-7905: chat thinking-level Default entries display the resolved project/global default in both the New Chat picker and in-chat Brain popup; choosing Default still clears the per-session override. --> <!-- FNXC:Chat-ThinkingLevel 2026-07-12-20:13: FN-7905: chat thinking-level Default entries display the resolved project/global default in both the New Chat picker and in-chat Brain popup; choosing Default still clears the per-session override. -->
<!-- FNXC:Chat-ModelSwitch 2026-07-12-00:00: FN-7908 extends that same Brain popup rather than adding another composer button, so active non-CLI Direct chats can retarget to a model pair or real agent mid-conversation and the next send resolves the updated session target. --> <!-- FNXC:Chat-ModelSwitch 2026-07-12-00:00: FN-7908 extends that same Brain popup rather than adding another composer button, so active non-CLI Direct chats can retarget to a model pair or real agent mid-conversation and the next send resolves the updated session target. -->
<!-- FNXC:Chat-ModelSwitch 2026-07-12-22:44: FN-7916 keeps the Brain popup usable on mobile/tablet touch devices: the portaled shared model picker is treated as part of the popup for selection, and the popup is viewport-fitted instead of anchored off-screen. --> <!-- FNXC:Chat-ModelSwitch 2026-07-12-22:44: FN-7916 keeps the Brain popup usable on mobile/tablet touch devices: the portaled shared model picker is treated as part of the popup for selection, and the popup is viewport-fitted instead of anchored off-screen. -->
- 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. <!-- FNXC:Chat-ModelSwitch 2026-07-13-00:00: FN-7934 applies that fitted Brain-popup layout to narrow chat surfaces, including floating Chat windows and compact docks on wide desktop viewports, because the browser viewport alone does not describe the popover's clipping container. -->
- 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) - 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)
<!-- FNXC:ChatEmptyMessage 2026-07-10-00:00: Empty final assistant responses can be legitimate provider output (for example a Grok CLI run ending without text). Document the shared Chat/Planner Chat behavior so operators see "No message" instead of interpreting a blank bubble as a rendering failure. --> <!-- FNXC:ChatEmptyMessage 2026-07-10-00:00: Empty final assistant responses can be legitimate provider output (for example a Grok CLI run ending without text). Document the shared Chat/Planner Chat behavior so operators see "No message" instead of interpreting a blank bubble as a rendering failure. -->
- 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. - 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.

View File

@@ -2046,6 +2046,28 @@ FN-7908 keeps model/agent retargeting inside the same brain popup. The widened p
z-index: 50; 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-target-section,
.chat-thinking-level-section { .chat-thinking-level-section {
padding: var(--space-sm); padding: var(--space-sm);

View File

@@ -1,3 +1,5 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { THINKING_LEVELS } from "@fusion/core"; import { THINKING_LEVELS } from "@fusion/core";
@@ -28,6 +30,13 @@ const agents = [
{ id: "agent-002", name: "Beta", role: "reviewer" }, { 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", () => { describe("ChatThinkingLevelControl", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -222,3 +231,26 @@ describe("ChatThinkingLevelControl", () => {
expect(screen.queryByRole("listbox")).toBeNull(); 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);");
});
});