diff --git a/docs/assets/fn-8248-chat-above-task.png b/docs/assets/fn-8248-chat-above-task.png new file mode 100644 index 0000000000..9bd3a70f6d Binary files /dev/null and b/docs/assets/fn-8248-chat-above-task.png differ diff --git a/docs/assets/fn-8248-mobile-create-room.png b/docs/assets/fn-8248-mobile-create-room.png new file mode 100644 index 0000000000..8aaff8527b Binary files /dev/null and b/docs/assets/fn-8248-mobile-create-room.png differ diff --git a/docs/assets/fn-8248-task-above-chat.png b/docs/assets/fn-8248-task-above-chat.png new file mode 100644 index 0000000000..a192cf4085 Binary files /dev/null and b/docs/assets/fn-8248-task-above-chat.png differ diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 7f223e24b5..91cf775a41 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -127,9 +127,9 @@ If **Settings → Appearance → Open tasks in the right sidebar** is enabled, b -**Settings → Appearance → Open tasks as popups** changes ordinary board task-card clicks, List row/card opens, and right-dock Tasks-list clicks across desktop, tablet, and mobile viewports. When enabled, those clicks use the existing task popup/FloatingWindow surface on the board/task-detail layer instead of the full-panel task detail, List split-detail/docked detail, or right-dock task detail, keeping the board, List view, or dock list visible in the background while utility windows keep their higher global stacking. On desktop and tablet, task popups restore the last saved popup size and position between tasks; on mobile, task popups stay full-screen sheets. Deep `changes`/`retries`/`workflow` opens, List context-menu/refine actions, task-detail links, plugin/graph opens, and explicit pop-out actions keep their existing paths. +**Settings → Appearance → Open tasks as popups** changes ordinary board task-card clicks, List row/card opens, and right-dock Tasks-list clicks across desktop, tablet, and mobile viewports. When enabled, those clicks use the existing task popup/FloatingWindow surface on the board/task-detail layer instead of the full-panel task detail, List split-detail/docked detail, or right-dock task detail, keeping the board, List view, or dock list visible in the background. Overlapping Quick Chat and task popups interleave by the most-recent pointer or focus interaction; other utility windows retain their higher global stacking. On desktop and tablet, task popups restore the last saved popup size and position between tasks; on mobile, task popups stay full-screen sheets. Deep `changes`/`retries`/`workflow` opens, List context-menu/refine actions, task-detail links, plugin/graph opens, and explicit pop-out actions keep their existing paths. **Settings → Appearance → Keep task popups on the view where they were opened** is enabled by default. Every task-detail popup is attached to its exact originating view, including Planning, Agents, Command Center, Documents, Missions, and plugin views: navigating elsewhere hides it without closing it, and returning re-shows it in the same saved position. You can open the same task independently in more than one view; closing or pressing Escape on one popup does not affect the other. Disable this setting only to restore legacy globally shared popups. Legacy saved popups without an origin remain visible everywhere for compatibility. @@ -669,6 +669,7 @@ Quick Chat is an optional fast, project-scoped assistant surface for conversatio - On first open for a project, Quick Chat restores the last opened non-archived session from per-project local storage; if that saved session is missing, it falls back to the most recently touched non-archived session by latest activity (`max(lastMessageAt, updatedAt)`), and only falls back to the first agent / configured default model when no prior session exists. - Closing and reopening Quick Chat keeps the active conversation warm in memory, so messages stay visible without a conversation reload or "Loading conversation…" flash. - Clicking outside the desktop Quick Chat floating window closes it by default; disable **Settings → General → Close Quick Chat on outside click** to keep it open until you explicitly close/minimize/maximize it. Model, thinking-level, agent, dependency, node, and priority dropdowns that open from Quick Chat are treated as part of the panel even when they render in a page-level portal, so selecting from them does not close Quick Chat. Task pop-out floating windows remain persistent on page clicks. +- Quick Chat and task popups use an interaction-driven peer stack: clicking or focusing either overlapping surface raises it above the other. Blocking dialogs opened from Quick Chat, including **Create Room**, claim a fresh top layer on every open so they remain above both the desktop floating panel and the mobile full-screen sheet. - Queued follow-up messages entered while a Quick Chat response is still streaming now persist per session, so closing/reopening the panel restores the queued stack and flushes the messages one at a time in FIFO order as active responses complete. - 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 diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index e5abd27a1b..5dfd2474d6 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1838,6 +1838,13 @@ function AppInner() { hideHeader dragHandleSelector=".chat-view--floating .view-header" className="floating-window--chat" + layer="task-detail" + /* + FNXC:ChatModal 2026-07-17-15:55: + Quick Chat and task-detail popups must share the task popup interaction stack: either + overlapping surface claims the front on pointer/focus. Other utility FloatingWindows keep + their higher utility band, so this scoped opt-in cannot change Terminal, Files, or New Task. + */ /* FNXC:ModalGeometryPersistence 2026-07-15-19:30: Chat is a full-screen sheet at ≤768px, so preserve its desktop location and size instead of restoring or overwriting them there. */ suspendGeometryPersistenceOnMobile persistGeometryKey="kb-dashboard-chat-floating-window" @@ -1874,8 +1881,10 @@ function AppInner() { FNXC:TaskPopupGeometry 2026-07-03-00:00: Every task-detail FloatingWindow keeps its per-task windowKey for DOM identity, dedupe, cascade fallback, and z-index independence, but all task-detail popups share one persisted geometry key so operators do not resize or reposition the popup between tasks. - FNXC:TaskPopupLayer 2026-07-04-18:36: - Ordinary task-detail popups belong to the board/task-detail layer, not the global floating-utility stack. Pass the task-detail layer so board/right-dock task opens preserve the visible board context while utility windows keep the higher app-wide raise/focus contract. + FNXC:TaskPopupLayer 2026-07-17-15:55: + Task-detail popups and Quick Chat share the task-detail interaction stack, so pointer/focus + moves either overlapping surface above the other. Other utility windows retain their separate, + higher utility band and cannot be reordered by task-popup interaction. FNXC:TaskPopupViewGating 2026-07-15-15:20: Rendering uses only the active view's scoped entries; state keeps hidden snapshots so returning remounts them with shared geometry. Each FloatingWindow key includes its origin so identical task ids never collide across views. diff --git a/packages/dashboard/app/components/CreateRoomModal.tsx b/packages/dashboard/app/components/CreateRoomModal.tsx index a3d1eaa9a3..c239d056c7 100644 --- a/packages/dashboard/app/components/CreateRoomModal.tsx +++ b/packages/dashboard/app/components/CreateRoomModal.tsx @@ -1,10 +1,11 @@ -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { createPortal } from "react-dom"; import { fetchAgents } from "../api"; import type { Agent } from "@fusion/core"; import { AgentAvatar } from "./AgentAvatar"; import { LoadingSpinner } from "./LoadingSpinner"; +import { nextFloatingZ } from "./floatingWindowStack"; import "./CreateRoomModal.css"; export interface RoomDraft { @@ -50,6 +51,16 @@ export function CreateRoomModal({ isOpen, onClose, onCreate, projectId, existing const [isSubmitting, setIsSubmitting] = useState(false); const nameInputRef = useRef(null); const previousFocusRef = useRef(null); + /* + FNXC:ChatRoomModal 2026-07-17-15:56: + Create Room is a blocking dialog launched from Quick Chat's non-blocking FloatingWindow. Because + both surfaces portal to body, claim a fresh shared top-layer z-index on every open so the dialog + stays above its parent on desktop and the mobile full-screen Chat sheet, including after reopen. + */ + const [overlayZ, setOverlayZ] = useState(undefined); + useLayoutEffect(() => { + if (isOpen) setOverlayZ(nextFloatingZ()); + }, [isOpen]); useEffect(() => { if (!isOpen) return; @@ -140,7 +151,7 @@ export function CreateRoomModal({ isOpen, onClose, onCreate, projectId, existing }; return createPortal( -
event.target === event.currentTarget && onClose()}> +
event.target === event.currentTarget && onClose()} style={overlayZ ? { zIndex: overlayZ } : undefined}>
event.stopPropagation()}>

{t("createRoom.title", "Create room")}

diff --git a/packages/dashboard/app/components/FloatingWindow.tsx b/packages/dashboard/app/components/FloatingWindow.tsx index e7dc706b54..b399f7bea9 100644 --- a/packages/dashboard/app/components/FloatingWindow.tsx +++ b/packages/dashboard/app/components/FloatingWindow.tsx @@ -58,7 +58,10 @@ export interface FloatingWindowProps { * Persistent task/terminal pop-outs must omit this so page clicks do not close them. */ closeOnOutsidePointerDown?: boolean; - /** Layer band for z-index claiming. Task details stay with the board/task-detail surface; utilities use the global floating stack. */ + /** + * Layer band for z-index claiming. Task-detail peers (including Quick Chat) interleave by + * interaction; unrelated utilities use the global floating stack. + */ layer?: "utility" | "task-detail"; // FNXC:FloatingWindow 2026-07-11-11:30: accessible name for the dialog overlay so headerless windows (e.g. artifact viewers with their own header chrome) stay queryable/announcable by label. ariaLabel?: string; @@ -213,8 +216,10 @@ export function FloatingWindow({ const claimFrontZ = useCallback(() => (layer === "task-detail" ? nextTaskDetailFloatingZ() : nextFloatingZ()), [layer]); const readCurrentZ = useCallback(() => (layer === "task-detail" ? currentTaskDetailFloatingZ() : currentFloatingZ()), [layer]); /* - FNXC:TaskPopupLayer 2026-07-04-18:36: - Task-detail popups intentionally claim the lower board/task-detail band, while utility FloatingWindow callers keep the higher global stack. This preserves raise/focus among multiple task popups without making ordinary board popups cover utility surfaces like Terminal or Quick Chat. + FNXC:TaskPopupLayer 2026-07-17-15:55: + Task-detail popups and Quick Chat intentionally claim the same board/task-detail interaction + band, so either may rise above the other on pointer/focus. Other utility FloatingWindow callers + retain the higher global stack; only Chat opts into this task-popup peer contract. */ const [zIndex, setZIndex] = useState(() => claimFrontZ()); const panelRef = useRef(null); diff --git a/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx b/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx index 73850eb0fd..da6bcb6995 100644 --- a/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi, beforeEach } from "vitest"; import { userEvent } from "@testing-library/user-event"; import { CreateRoomModal, validateRoomName } from "../CreateRoomModal"; +import { FloatingWindow } from "../FloatingWindow"; import * as apiModule from "../../api"; vi.mock("../../api", () => ({ @@ -72,6 +73,77 @@ describe("CreateRoomModal", () => { expect(onCreate).toHaveBeenCalledWith({ name: "engineering", displayName: "#engineering", memberAgentIds: ["agent-1"] }); }); + it("claims a fresh top layer above floating Chat on open and reopen", async () => { + const { rerender } = render( + <> + {}} layer="task-detail" className="floating-window--chat"> +
floating chat representative
+
+ + , + ); + + const chatPanel = screen.getByTestId("floating-window-chat-modal"); + const firstOverlay = document.querySelector(".create-room-modal")?.parentElement as HTMLElement; + expect(Number(firstOverlay.style.zIndex)).toBeGreaterThan(Number(chatPanel.style.zIndex)); + await screen.findByRole("button", { name: /Alpha/i }); + + // Another Chat interaction can claim its peer stack while the dialog is closed. + rerender( + <> + {}} layer="task-detail" className="floating-window--chat"> +
floating chat representative
+
+ + , + ); + fireEvent.pointerDown(chatPanel); + + rerender( + <> + {}} layer="task-detail" className="floating-window--chat"> +
floating chat representative
+
+ + , + ); + + const reopenedOverlay = document.querySelector(".create-room-modal")?.parentElement as HTMLElement; + expect(Number(reopenedOverlay.style.zIndex)).toBeGreaterThan(Number(chatPanel.style.zIndex)); + expect(Number(reopenedOverlay.style.zIndex)).toBeGreaterThan(Number(firstOverlay.style.zIndex)); + }); + + it("stays above floating Chat while agent data is loading or empty", async () => { + mockFetchAgents.mockImplementation(() => new Promise(() => {})); + const loading = render( + <> + {}} layer="task-detail" className="floating-window--chat"> +
floating chat representative
+
+ + , + ); + + const loadingOverlay = document.querySelector(".create-room-modal")?.parentElement as HTMLElement; + expect(screen.getByRole("status")).toHaveTextContent("Loading agents..."); + expect(Number(loadingOverlay.style.zIndex)).toBeGreaterThan(Number(screen.getByTestId("floating-window-chat-loading").style.zIndex)); + loading.unmount(); + + mockFetchAgents.mockResolvedValueOnce([]); + render( + <> + {}} layer="task-detail" className="floating-window--chat"> +
floating chat representative
+
+ + , + ); + + const emptyOverlay = document.querySelector(".create-room-modal")?.parentElement as HTMLElement; + expect(await screen.findByText("No agents in this project yet.")).toBeInTheDocument(); + expect(Number(emptyOverlay.style.zIndex)).toBeGreaterThan(Number(screen.getByTestId("floating-window-chat-empty").style.zIndex)); + }); + it("closes on escape and overlay click", async () => { const onClose = vi.fn(); render(); diff --git a/packages/dashboard/app/components/__tests__/FloatingWindowStack.cross-type.test.tsx b/packages/dashboard/app/components/__tests__/FloatingWindowStack.cross-type.test.tsx index 859df36c64..6fbf6b8fa3 100644 --- a/packages/dashboard/app/components/__tests__/FloatingWindowStack.cross-type.test.tsx +++ b/packages/dashboard/app/components/__tests__/FloatingWindowStack.cross-type.test.tsx @@ -8,8 +8,9 @@ import { currentFloatingZ, currentTaskDetailFloatingZ, nextFloatingZ, nextTaskDe FNXC:FloatingWindow 2026-06-22-21:30: Cross-type shared-stack contract. Utility floating modal types (utility FloatingWindow, the right-dock pop-out, the floating terminal, the floating New Task dialog) draw their z-index from the SINGLE module-level utility counter so tapping ANY utility raises it above ALL other utilities REGARDLESS of type. RightDockExpandModal stands in for the three non-FloatingWindow floating modals (terminal + New Task wire the identical claim-on-mount + bring-to-front-on-pointerdown pattern; they are heavier to mount in JSDOM and assert the same inline-zIndex contract). -FNXC:TaskPopupLayer 2026-07-04-18:36: -Task-detail FloatingWindow callers are excluded from the global utility stack. They use a lower board/task-detail counter so ordinary board/right-dock task popups can raise among themselves without becoming topmost utility overlays. +FNXC:TaskPopupLayer 2026-07-17-15:55: +Quick Chat opts into the task-detail counter so task popups and Chat interleave by interaction. +Unrelated utility windows remain excluded and keep their independent, higher utility stack. */ const renderProps = { addToast: () => {}, projectId: "project-1" } as const; @@ -59,26 +60,40 @@ describe("floatingWindowStack (cross-type)", () => { expect(Number(dockPanel.style.zIndex)).toBeGreaterThan(Number(fwPanel.style.zIndex)); }); - it("keeps task-detail FloatingWindow popups out of the global utility band", () => { + it("interleaves Quick Chat with one or multiple task popups while utility windows stay higher", () => { render( <> - {}} layer="task-detail" className="floating-window--task-detail"> -
task body
+ {}} layer="task-detail" className="floating-window--chat"> +
chat body
+
+ {}} layer="task-detail" className="floating-window--task-detail"> +
task a body
+
+ {}} layer="task-detail" className="floating-window--task-detail"> +
task b body
{}} /> , ); - const taskPanel = screen.getByTestId("floating-window-task"); - const taskOverlay = screen.getByTestId("floating-window-overlay-task"); + const chatPanel = screen.getByTestId("floating-window-chat-modal"); + const taskA = screen.getByTestId("floating-window-task-a"); + const taskB = screen.getByTestId("floating-window-task-b"); const dockPanel = screen .getByTestId("right-dock-expand-modal") .querySelector(".right-dock-expand-modal--floating") as HTMLElement; - expect(Number(taskPanel.style.zIndex)).toBeLessThan(Number(dockPanel.style.zIndex)); - expect(Number(taskOverlay.style.zIndex)).toBeLessThan(Number(dockPanel.style.zIndex)); + // Later task mounts above Chat; utility surfaces still retain their higher independent band. + expect(Number(taskB.style.zIndex)).toBeGreaterThan(Number(chatPanel.style.zIndex)); + expect(Number(dockPanel.style.zIndex)).toBeGreaterThan(Number(taskB.style.zIndex)); - fireEvent.pointerDown(taskPanel); - expect(Number(taskPanel.style.zIndex)).toBeLessThan(Number(dockPanel.style.zIndex)); + // Task and Chat both claim the same peer counter in either interaction direction. + fireEvent.pointerDown(chatPanel); + expect(Number(chatPanel.style.zIndex)).toBeGreaterThan(Number(taskB.style.zIndex)); + fireEvent.focus(taskA); + expect(Number(taskA.style.zIndex)).toBeGreaterThan(Number(chatPanel.style.zIndex)); + fireEvent.focus(chatPanel); + expect(Number(chatPanel.style.zIndex)).toBeGreaterThan(Number(taskA.style.zIndex)); + expect(Number(chatPanel.style.zIndex)).toBeLessThan(Number(dockPanel.style.zIndex)); }); }); diff --git a/packages/dashboard/app/components/floatingWindowStack.ts b/packages/dashboard/app/components/floatingWindowStack.ts index 01e78631e6..02fdadc9ba 100644 --- a/packages/dashboard/app/components/floatingWindowStack.ts +++ b/packages/dashboard/app/components/floatingWindowStack.ts @@ -5,8 +5,10 @@ SHARED floating-utility z-index stack. This is the ONE source of z-index for uti FNXC:FloatingWindow 2026-06-22-22:30: Base band sits at 10100+ — ABOVE the page overlay/popover band (log viewer, workflow-editor modal, selection popover, static fullscreen fallbacks at z 10000-10001) so a utility floating window the user is dragging is never painted over by those. Transient top-right toasts are bumped to 10500 (styles.css) so system feedback still shows above a dragged utility window. The workflow prompt fullscreen overlay is itself a floating utility surface and claims `nextFloatingZ()` when opened, because a static z 10000 fallback is hidden by the workflow editor's full-screen mobile FloatingWindow sheet. The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them. CRITICAL: every floating modal must be portaled to document.body so this shared z is compared in ONE root stacking context (an inline panel cannot beat siblings outside its own context no matter its z). -FNXC:TaskPopupLayer 2026-07-04-18:36: -Task-detail popups are ordinary board/task-detail surfaces, not global utilities. Keep their focus stack in a lower board-layer band so task popups can raise among themselves without covering terminal, right-dock expand, Quick Chat, file browser, workflow editor, or other utility windows that intentionally use the 10100+ band. +FNXC:TaskPopupLayer 2026-07-17-15:55: +Task-detail popups and Quick Chat are interaction-stack peers in this lower board-layer band: the +most recently mounted or pointer/focus-interacted peer is on top. Terminal, right-dock expand, +Files, New Task, and other utility surfaces continue to use the separate 10100+ utility band. */ let topZ = 10100; let taskDetailTopZ = 220; @@ -21,12 +23,12 @@ export function currentFloatingZ(): number { return topZ; } -/** Claim the front of the board/task-detail popup stack. Monotonic, session-length. */ +/** Claim the front of the task-popup peer stack (task details and Quick Chat). Monotonic, session-length. */ export function nextTaskDetailFloatingZ(): number { return ++taskDetailTopZ; } -/** Current top of the board/task-detail popup stack (read-only). */ +/** Current top of the task-popup peer stack (read-only). */ export function currentTaskDetailFloatingZ(): number { return taskDetailTopZ; }