diff --git a/.changeset/fn-6899-click-revealed-right-dock.md b/.changeset/fn-6899-click-revealed-right-dock.md new file mode 100644 index 0000000000..7439e5520a --- /dev/null +++ b/.changeset/fn-6899-click-revealed-right-dock.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": minor +--- + +Make the dashboard right dock persistent by default with an in-dock collapse toggle, and remove duplicate Header right-dock toggle behavior. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index d45019a163..c113bc36b8 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -23,7 +23,7 @@ Task Detail modal opens from onboarding, activity log, and task-to-task navigati **Left Sidebar Navigation** is enabled by default for desktop/tablet project screens, moving project navigation out of the Header and into a persistent left sidebar. To opt out, open **Settings → Experimental Features** and turn **Left Sidebar Navigation** off (`leftSidebarNav: false`). -When enabled on desktop or tablet project screens, the sidebar starts with a centered **New Task** button that opens the existing New Task dialog from any project screen. Expanded mode shows the plus icon and **New Task** label; collapsed rail mode keeps the centered icon-only button accessible through its label/title. Below that action, the sidebar contains the primary destinations (Board, List, Agents, Command Center, Missions, Chat, Artifacts, Mailbox, and plugin primary views), selected auxiliary destinations as regular entries (Research, Insights, Skills, Memory, Stash Recovery, Evals, Goals, Dev Server, and plugin overflow views when their flags/plugins are enabled), and a footer with the collapse toggle directly above the Settings button. Secrets and Todos are intentionally not listed in the left sidebar; use the Right Dock for those auxiliary views on desktop/tablet. The Header retains the Fusion brand and project selector, keeps its non-navigation controls, and hides the view-toggle row and **More views** trigger so there is only one canonical primary navigation surface. +When enabled on desktop or tablet project screens, the sidebar starts with a centered **New Task** button that opens the existing New Task dialog from any project screen. Expanded mode shows the plus icon and **New Task** label; collapsed rail mode keeps the centered icon-only button accessible through its label/title. Below that action, the sidebar contains the primary destinations (Board, List, Agents, Command Center, Missions, Chat, Artifacts, Mailbox, and plugin primary views), selected auxiliary destinations as regular entries (Research, Insights, Skills, Memory, Secrets, Stash Recovery, Evals, Goals, Todos, Dev Server, and plugin overflow views when their flags/plugins are enabled), and a footer with the collapse toggle directly above the Settings button. The Header retains the Fusion brand and project selector, keeps its non-navigation controls, and hides the view-toggle row and **More views** trigger so there is only one canonical primary navigation surface. While the sidebar is active on desktop/tablet project screens, Board and List workflow controls move into the Header slot that replaces the hidden view toggle. Board and List share one workflow dropdown: each workflow row includes an inline edit action, and a persistent **New workflow** action remains at the bottom of the dropdown while the workflow list scrolls. The standalone workflow row above the board/list content is removed in this mode. When the flag is off, outside project screens, or on mobile, workflow controls remain inline with the same consolidated dropdown. @@ -35,9 +35,9 @@ On mobile viewports (`<=768px`), the sidebar is not rendered even when the defau The **Right Dock Panel** experiment is enabled by default. To disable it, open **Settings → Experimental Features** and turn off **Right Dock Panel**. -When enabled on desktop or tablet project screens, the Header right-panel toggle opens a persistent dock on the right side of the project content. If Left Sidebar Navigation is also enabled and the Header view-toggle row is hidden, the same right-dock toggle remains available as a standalone Header icon. +When enabled on desktop or tablet project screens, the dock is visible by default as the persistent far-right sidebar in the project content row. Its in-dock collapse toggle (`right-dock-collapse-toggle`) replaces the former Header right-panel toggle, so there is no duplicate Header control when Left Sidebar Navigation is active or when the Header view-toggle row is visible. -The dock toolbar is a tools rail with exactly six destinations: **Activity**, **Activity Log**, **Import from GitHub**, **Git Manager**, **Files**, and **Automation**. The launcher tools reuse the same handlers as the former desktop Header toolbar buttons; **Files** remains the inline dock view, opens by default, and is the fallback when browser storage points at a removed dock key. Inline dock views have an expand button that opens the same view in a resizable modal, while action-only tools launch directly and are not expandable. Dock width and expanded modal size persist across reloads. +The dock toolbar has exactly six destinations: **Activity**, **Activity Log**, **Import from GitHub**, **Git Manager**, **Files**, and **Automation**. The launcher tools reuse the same handlers as the former desktop Header toolbar buttons; **Files** remains the inline dock view, opens by default, and is the fallback when browser storage points at a removed dock key. Inline dock views have an expand button that opens the same view in a resizable modal, while action-only tools launch directly and are not expandable. Dock collapsed state, width, and expanded modal size persist across reloads. Content views such as Artifacts, Research, Insights, Skills, Memory, Secrets, Evals, Goals, Todos, and Dev Server live in the left sidebar (or compact mobile navigation) rather than the right dock. The six tool buttons are no longer duplicated in the desktop top Header toolbar, and the former desktop overflow trigger is removed when it would otherwise be empty. diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 9e7ab33a95..2c611fbd8c 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1998,7 +1998,6 @@ function AppInner() { projectId={currentProject?.id} mobileNavEnabled={isMobile} leftSidebarNavActive={sidebarActive} - rightDockActive={rightDockActive} rightDockOpen={rightDock.open} onToggleRightDock={rightDock.toggle} // Node switching props availableNodes={nodes} currentNode={currentNode} diff --git a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx index 7638043881..a47931e277 100644 --- a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx +++ b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx @@ -276,36 +276,30 @@ describe("Mobile Feature Access Regression Guard", () => { } }); - it("right dock reroutes desktop and tablet More views without leaving the dropdown or chevron behind", () => { + it("desktop and tablet More views remain a dropdown rather than a Header right-dock toggle", () => { for (const tier of ["desktop", "tablet"] as const) { mockViewport(tier); - const onToggleRightDock = vi.fn(); const { unmount } = render(
, ); const trigger = screen.getByTestId("view-toggle-overflow-trigger"); - expect(trigger.querySelector(".lucide-panel-right")).toBeTruthy(); - expect(trigger.querySelector(".lucide-chevron-down")).toBeNull(); + expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy(); + expect(trigger.querySelector(".lucide-panel-right")).toBeNull(); fireEvent.click(trigger); - expect(onToggleRightDock).toHaveBeenCalledOnce(); - expect(screen.queryByRole("menu", { name: "More views" })).toBeNull(); + expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument(); unmount(); } }); - it("right dock stays reachable as one standalone toggle when left sidebar nav is active", () => { + it("left sidebar nav leaves no duplicate Header right-dock toggle", () => { for (const tier of ["desktop", "tablet"] as const) { mockViewport(tier); - const onToggleRightDock = vi.fn(); const { unmount } = render(
{ mobileNavEnabled={false} showAgentsTab={true} leftSidebarNavActive={true} - rightDockActive={true} - rightDockOpen={true} - onToggleRightDock={onToggleRightDock} />, ); - const triggers = screen.getAllByTestId("view-toggle-overflow-trigger"); - expect(triggers).toHaveLength(1); - expect(triggers[0].querySelector(".lucide-panel-right")).toBeTruthy(); - expect(triggers[0]).toHaveAttribute("aria-pressed", "true"); - fireEvent.click(triggers[0]); - expect(onToggleRightDock).toHaveBeenCalledOnce(); + expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull(); + expect(document.querySelector(".header-right-dock-toggle")).toBeNull(); unmount(); } }); @@ -351,23 +338,18 @@ describe("Mobile Feature Access Regression Guard", () => { it("right dock flag off keeps the desktop and tablet More views chevron dropdown", () => { for (const tier of ["desktop", "tablet"] as const) { mockViewport(tier); - const onToggleRightDock = vi.fn(); const { unmount } = render(
, ); const trigger = screen.getByTestId("view-toggle-overflow-trigger"); expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy(); fireEvent.click(trigger); - expect(onToggleRightDock).not.toHaveBeenCalled(); expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument(); unmount(); } diff --git a/packages/dashboard/app/components/Header.tsx b/packages/dashboard/app/components/Header.tsx index 9991f33e9d..6676b2b3f0 100644 --- a/packages/dashboard/app/components/Header.tsx +++ b/packages/dashboard/app/components/Header.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useCallback, useMemo, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, PanelRight, ChevronDown, ChevronRight } from "lucide-react"; +import { Settings, LayoutGrid, List, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Workflow, Bot, Target, Grid3X3, Mail, MessageSquare, Check, Zap, Sparkles, FileText, Brain, CheckSquare, Lock, Gauge, ChevronDown, ChevronRight } from "lucide-react"; import "./Header.css"; // ProjectSelector styles used by the imported standalone component. import "./ProjectSelector.css"; @@ -96,10 +96,6 @@ export interface HeaderProps { mobileNavEnabled?: boolean; /** When true on non-mobile screens, persistent left sidebar owns primary view navigation. */ leftSidebarNavActive?: boolean; - /** When true on tablet/desktop, the More views overflow trigger toggles the auxiliary right dock instead of a menu. */ - rightDockActive?: boolean; - rightDockOpen?: boolean; - onToggleRightDock?: () => void; /** Available nodes for the node selector */ availableNodes?: NodeConfig[]; /** Currently selected node (null for local) */ @@ -149,9 +145,6 @@ export function Header({ shellHost = { kind: "browser" }, mobileNavEnabled, leftSidebarNavActive = false, - rightDockActive = false, - rightDockOpen = false, - onToggleRightDock, availableNodes = [], currentNode, onSelectNode, @@ -174,7 +167,10 @@ export function Header({ The hidden Header view-toggle location becomes the workflow-control portal slot only when left sidebar navigation is active on tablet/desktop. Mobile and flag-off paths keep workflow controls inline so the board/list chrome remains byte-identical. */ const hideHeaderViewNav = leftSidebarNavActive && !isMobile; - const shouldRouteMoreViewsToRightDock = rightDockActive && !isMobile && typeof onToggleRightDock === "function"; + /* + FNXC:Navigation 2026-06-21-23:40: + The right dock is persistent and owns its own collapse control, so Header must not render a duplicate right-dock toggle or repurpose the More views overflow trigger on tablet/desktop. + */ const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false); const [isNonMobileSearchOpen, setIsNonMobileSearchOpen] = useState(false); // Track when user has explicitly closed the search (used for toggle visibility) @@ -599,25 +595,6 @@ export function Header({ /> )} - {/* - FNXC:Navigation 2026-06-21-00:00: - The default-on right dock changes the tablet/desktop More views affordance into a true panel toggle, so the icon must communicate a right panel and expose pressed/expanded state. When left-sidebar navigation hides Header view tabs, this standalone control keeps the dock reachable without duplicating the hidden overflow trigger; mobile and flag-off paths keep the legacy chevron menu. - */} - {hideHeaderViewNav && shouldRouteMoreViewsToRightDock && ( - - )} - {/** * FNXC:Header 2026-06-21-00:00: * Desktop and tablet header search must render after the workflow portal slot so a populated WorkflowSwitcher appears left of the search icon while preserving the mobile search trigger's existing position and behavior. @@ -759,29 +736,19 @@ export function Header({ <> - {!shouldRouteMoreViewsToRightDock && isViewOverflowOpen && ( + {isViewOverflowOpen && (
{ - persistRightDockOpen(false); - onOpenChange(false); - }, [onOpenChange]); + const toggleCollapsed = useCallback(() => { + const nextOpen = !open; + persistRightDockOpen(nextOpen); + onOpenChange(nextOpen); + }, [onOpenChange, open]); const handleResizeStart = useCallback((event: React.PointerEvent) => { event.preventDefault(); @@ -167,32 +171,35 @@ export function RightDock({ persistRightDockWidth(nextWidth); }, [width]); - if (!open || !selectedEntry) { + if (!selectedEntry) { return null; } const SelectedIcon = selectedEntry.icon; + const dockWidth = open ? `${width}px` : undefined; return ( ); } diff --git a/packages/dashboard/app/components/__tests__/Header.test.tsx b/packages/dashboard/app/components/__tests__/Header.test.tsx index 21ecd740df..0a60887446 100644 --- a/packages/dashboard/app/components/__tests__/Header.test.tsx +++ b/packages/dashboard/app/components/__tests__/Header.test.tsx @@ -269,65 +269,33 @@ describe("Header", () => { expect(screen.queryByTestId("view-overflow-stash-recovery")).toBeNull(); }); - it.each(["desktop", "tablet"] as const)("routes More views to the right dock panel toggle on %s", (tier) => { - const onToggleRightDock = vi.fn(); - const { rerender } = renderHeader({ - onChangeView: noop, - rightDockActive: true, - rightDockOpen: false, - onToggleRightDock, - }, tier); + it.each(["desktop", "tablet"] as const)("keeps More views as a chevron dropdown instead of a right-dock toggle on %s", (tier) => { + renderHeader({ onChangeView: noop, todosEnabled: true }, tier); const trigger = screen.getByTestId("view-toggle-overflow-trigger"); - expect(trigger.querySelector(".lucide-panel-right")).toBeTruthy(); - expect(trigger.querySelector(".lucide-chevron-down")).toBeNull(); - expect(trigger).toHaveAttribute("aria-pressed", "false"); - expect(trigger).not.toHaveAttribute("aria-haspopup"); - + expect(trigger.querySelector(".lucide-chevron-down")).toBeTruthy(); + expect(trigger.querySelector(".lucide-panel-right")).toBeNull(); + expect(trigger).toHaveAttribute("aria-haspopup", "menu"); + expect(trigger).not.toHaveAttribute("aria-pressed"); fireEvent.click(trigger); - expect(onToggleRightDock).toHaveBeenCalledTimes(1); - expect(screen.queryByRole("menu", { name: "More views" })).toBeNull(); - - rerender( -
, - ); - expect(screen.getByTestId("view-toggle-overflow-trigger")).toHaveAttribute("aria-pressed", "true"); + expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument(); }); - it.each(["desktop", "tablet"] as const)("keeps one standalone right dock toggle when left sidebar hides view nav on %s", (tier) => { - const onToggleRightDock = vi.fn(); + it.each(["desktop", "tablet"] as const)("renders no duplicate Header right-dock toggle when left sidebar hides view nav on %s", (tier) => { renderHeader({ onChangeView: noop, leftSidebarNavActive: true, - rightDockActive: true, - rightDockOpen: true, - onToggleRightDock, + todosEnabled: true, }, tier); - const triggers = screen.getAllByTestId("view-toggle-overflow-trigger"); - expect(triggers).toHaveLength(1); - expect(triggers[0].querySelector(".lucide-panel-right")).toBeTruthy(); - expect(triggers[0]).toHaveAttribute("aria-pressed", "true"); - fireEvent.click(triggers[0]); - expect(onToggleRightDock).toHaveBeenCalledTimes(1); - expect(screen.queryByRole("menu", { name: "More views" })).toBeNull(); + expect(screen.queryByTestId("view-toggle-overflow-trigger")).toBeNull(); + expect(document.querySelector(".header-right-dock-toggle")).toBeNull(); }); - it("keeps the legacy chevron dropdown on mobile even when right dock props are present", () => { - const onToggleRightDock = vi.fn(); + it("keeps the legacy chevron dropdown on mobile", () => { renderHeader({ onChangeView: noop, mobileNavEnabled: false, - rightDockActive: true, - rightDockOpen: false, - onToggleRightDock, }, "mobile"); const trigger = screen.getByTestId("view-toggle-overflow-trigger"); @@ -335,7 +303,6 @@ describe("Header", () => { expect(trigger.querySelector(".lucide-panel-right")).toBeNull(); expect(trigger).toHaveAttribute("aria-haspopup", "menu"); fireEvent.click(trigger); - expect(onToggleRightDock).not.toHaveBeenCalled(); expect(screen.getByRole("menu", { name: "More views" })).toBeInTheDocument(); }); diff --git a/packages/dashboard/app/components/__tests__/RightDock.test.tsx b/packages/dashboard/app/components/__tests__/RightDock.test.tsx index 69c51b23fd..b7dc5ec91c 100644 --- a/packages/dashboard/app/components/__tests__/RightDock.test.tsx +++ b/packages/dashboard/app/components/__tests__/RightDock.test.tsx @@ -50,10 +50,7 @@ describe("RightDock", () => { }); it("renders Files by default and restores only persisted inline views", () => { - const onOpenChange = vi.fn(); - const { unmount } = render( - , - ); + const { unmount } = render(); expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-selected", "true"); expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument(); @@ -62,7 +59,7 @@ describe("RightDock", () => { expect(window.localStorage.getItem(RIGHT_DOCK_VIEW_STORAGE_KEY)).toBeNull(); unmount(); - render(); + render(); expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-selected", "true"); }); @@ -148,11 +145,11 @@ describe("RightDock", () => { expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument(); }); - it("closes internally and clamps then persists resize width", () => { + it("collapses internally and clamps then persists resize width", () => { const onOpenChange = vi.fn(); render(); - fireEvent.click(screen.getByTestId("right-dock-close")); + fireEvent.click(screen.getByTestId("right-dock-collapse-toggle")); expect(onOpenChange).toHaveBeenCalledWith(false); expect(window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY)).toBe("false"); @@ -174,6 +171,26 @@ describe("RightDock", () => { expect(screen.getByTestId("right-dock-resize-handle")).toHaveAttribute("aria-valuenow", "400"); }); + it("shows an in-dock collapse toggle and keeps the collapsed rail persistent", () => { + const onOpenChange = vi.fn(); + const { rerender } = render(); + + expect(screen.getByTestId("right-dock-collapse-toggle")).toHaveAttribute("aria-expanded", "true"); + expect(screen.getByTestId("right-dock-body")).toBeInTheDocument(); + expect(screen.getByTestId("right-dock-resize-handle")).toBeInTheDocument(); + expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual(toolTabIds); + + rerender(); + expect(screen.getByTestId("right-dock")).toHaveClass("right-dock--collapsed"); + expect(screen.getByTestId("right-dock-collapse-toggle")).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByTestId("right-dock-body")).toBeNull(); + expect(screen.queryByTestId("right-dock-resize-handle")).toBeNull(); + expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual(toolTabIds); + fireEvent.click(screen.getByTestId("right-dock-collapse-toggle")); + expect(onOpenChange).toHaveBeenLastCalledWith(true); + expect(window.localStorage.getItem(RIGHT_DOCK_OPEN_STORAGE_KEY)).toBe("true"); + }); + it("renders the expanded modal through the same registry and restores focus on close", async () => { const onClose = vi.fn(); const focusButton = document.createElement("button"); diff --git a/packages/dashboard/app/components/useRightDockController.tsx b/packages/dashboard/app/components/useRightDockController.tsx index 3f10d48148..cbae25fc9f 100644 --- a/packages/dashboard/app/components/useRightDockController.tsx +++ b/packages/dashboard/app/components/useRightDockController.tsx @@ -46,8 +46,8 @@ export interface RightDockController { } /* -FNXC:Navigation 2026-06-21-12:20: -App owns whether the default-on right dock is available, but this controller owns the persisted open state and shared view props so App.tsx does not duplicate each overflow destination's dock/modal rendering contract. +FNXC:Navigation 2026-06-21-23:40: +The right dock is visible by default and collapses from inside the dock. Keep the persisted open/collapsed state in this controller so App and Header do not need duplicate right-dock toggle wiring. */ export function useRightDockController(input: RightDockControllerInput): RightDockController { const [open, setOpen] = useState(readStoredRightDockOpen);