From f7e20abe9f075d436774bc367beb3dacb8bb0341 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 27 Jun 2026 23:25:19 -0700 Subject: [PATCH] FN-7168: add Chat to the right dock Adds Chat as an inline right-dock tool with compact dock rendering and pop-out support. - Register Chat as a built-in right-dock overflow view with its toolbar icon and lazy-loaded view. - Add a compact ChatView layout prop so narrow dock hosts use the mobile-style list/detail flow. - Cover Chat dock visibility, storage fallback, lazy-load docs, and compact layout behavior with tests. - Document the updated right-dock tool roster and add a minor changeset. Files changed: .changeset/FN-7168-right-dock-chat.md | 7 ++ docs/dashboard-guide.md | 7 +- .../app/__tests__/lazy-loaded-views-docs.test.ts | 6 +- packages/dashboard/app/components/ChatView.tsx | 9 ++- packages/dashboard/app/components/RightDock.tsx | 2 +- .../__tests__/ChatView.sessions-rooms.test.tsx | 9 +++ .../app/components/__tests__/RightDock.test.tsx | 9 ++- .../__tests__/overflowViewRegistry.chat.test.tsx | 90 ++++++++++++++++++++++ .../__tests__/overflowViewRegistry.test.tsx | 5 +- .../app/components/overflowViewRegistry.tsx | 25 ++++++ 10 files changed, 158 insertions(+), 11 deletions(-) Fusion-Task-Id: FN-7168 Fusion-Task-Lineage: 194c66f6-bdd9-438e-b648-2a79629534a6 Co-authored-by: Fusion (runfusion.ai) --- .changeset/FN-7168-right-dock-chat.md | 7 ++ docs/dashboard-guide.md | 7 +- .../__tests__/lazy-loaded-views-docs.test.ts | 6 +- .../dashboard/app/components/ChatView.tsx | 9 +- .../dashboard/app/components/RightDock.tsx | 2 +- .../ChatView.sessions-rooms.test.tsx | 9 ++ .../components/__tests__/RightDock.test.tsx | 9 +- .../overflowViewRegistry.chat.test.tsx | 90 +++++++++++++++++++ .../__tests__/overflowViewRegistry.test.tsx | 5 +- .../app/components/overflowViewRegistry.tsx | 25 ++++++ 10 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 .changeset/FN-7168-right-dock-chat.md create mode 100644 packages/dashboard/app/components/__tests__/overflowViewRegistry.chat.test.tsx diff --git a/.changeset/FN-7168-right-dock-chat.md b/.changeset/FN-7168-right-dock-chat.md new file mode 100644 index 0000000000..169be94752 --- /dev/null +++ b/.changeset/FN-7168-right-dock-chat.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a Chat tab to the right sidebar so you can chat inline and pop it out. +category: feature +dev: Registers ChatView as the always-visible `chat` overflow-view entry in the right dock. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index a1b54490ad..d28f8df801 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -52,13 +52,14 @@ The **Right Dock Panel** experiment is enabled by default. To disable it, open * When enabled on desktop or tablet project screens, the right dock is a persistent far-right tools sidebar in the project content row. By default it opens as an overlay so the main content does not reflow. Use the dock toolbar pin action to switch into push mode, where the dock becomes an in-flow pane that shrinks the main content beside it; unpinning returns to overlay mode. The selected tool, open/closed state, pinned push-mode state, width, and expanded modal size persist across reloads. -The dock toolbar has built-in inline tool panels for **Activity**, **Activity Log**, **Git Manager**, **Files**, and project tool launchers such as **Import from GitHub** / **Import Tasks** workflow entry points and **Automation** actions when available. **Activity**, **Activity Log**, **Git Manager**, and **Files** render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** 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 for more room. The right-dock **Files** viewer and its expanded pop-out match the Files modal for browser-previewable file types: image, video/movie, audio, and PDF selections render as native browser previews, while editable text files keep the editor and save flow. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar. + +The dock toolbar has built-in inline tool panels for **Files**, **Chat**, **Activity Log**, **Git Manager**, **Dev Server** when enabled, **Secrets**, **Todos** when enabled, and **Pull Requests**. These tools render in embedded mode inside the dock instead of opening fixed popup overlays; **Files** 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 for more room. The right-dock **Files** viewer and its expanded pop-out match the Files modal for browser-previewable file types: image, video/movie, audio, and PDF selections render as native browser previews, while editable text files keep the editor and save flow. Plugin overflow views may add additional right-dock tool tabs, except plugin destinations that explicitly belong in the left sidebar. Use the desktop/tablet right dock this way: 1. Open a project screen with **Right Dock Panel** enabled. Expected outcome: the dock appears on the far right with **Files** selected unless a valid previous dock view is stored. -2. Select **Activity**, **Activity Log**, **Git Manager**, **Files**, or another available tool in the dock toolbar. +2. Select **Chat**, **Activity Log**, **Git Manager**, **Files**, or another available tool in the dock toolbar. Expected outcome: the selected tool renders inline inside the dock body and the toolbar tab becomes active. 3. Drag the dock's left-edge resize handle, or focus the separator and use the arrow keys. Expected outcome: the dock width changes within its min/max bounds and is saved for future reloads. @@ -69,7 +70,7 @@ Use the desktop/tablet right dock this way: 6. Use the Header right-sidebar toggle. Expected outcome: the far-right surface opens or closes without creating duplicate left-sidebar destinations; mobile viewports never render or reserve space for the right dock. -Content views such as Artifacts, Research, Insights, Skills, Memory, Evals, Goals, Dev Server, **Workflows**, **Import Tasks**, and **Automations** live in the left sidebar (or compact mobile navigation) rather than the right dock. On desktop/tablet, GitHub import lives under **Import Tasks**; mobile keeps compact GitHub import entries in the More surfaces. +Content views such as Artifacts, Research, Insights, Skills, Memory, Evals, Goals, **Workflows**, **Import Tasks**, and **Automations** live in the left sidebar (or compact mobile navigation) rather than the right dock. On desktop/tablet, GitHub import lives under **Import Tasks**; mobile keeps compact GitHub import entries in the More surfaces. On mobile viewports, the Right Dock never renders. The compact Header actions and bottom `MobileNavBar` keep their existing mobile behavior even when the experiment is enabled. diff --git a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts index e6b2effb04..43f09401a8 100644 --- a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts +++ b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts @@ -116,7 +116,11 @@ const EXPECTED_EXCLUDED_LAZY = [ }, { file: "../components/overflowViewRegistry.tsx", - symbols: ["DevServerView", "SecretsView", "TodoView", "PullRequestView"], + /* + * FNXC:DashboardLazyViews 2026-06-27-00:00: + * The right-dock chat tab re-imports ChatView through the overflow registry, but ChatView remains counted once as the App-level Chat chunk in the curated AGENTS inventory. + */ + symbols: ["DevServerView", "SecretsView", "TodoView", "PullRequestView", "ChatView"], reason: "right-dock overflow re-imports of App-level chunks already counted once", countedBy: "../App.tsx", }, diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 438ace182f..a01b00c59a 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -65,6 +65,11 @@ export interface ChatViewProps { addToast: (msg: string, type?: "success" | "error" | "warning") => void; experimentalFeatures?: Record; floating?: boolean; + /* + FNXC:RightDockChat 2026-06-27-23:12: + The right dock can host ChatView in a 360px sidebar while the browser viewport remains desktop-sized. Let dock callers force the same narrow list/detail layout used by mobile/resized floating chat without passing floating chrome callbacks. + */ + compactLayout?: boolean; onPopOut?: () => void; onMaximize?: () => void; onMinimize?: () => void; @@ -987,7 +992,7 @@ const ChatMessageItem = memo(function ChatMessageItem({ ); }); -export function ChatView({ projectId, addToast, floating = false, onPopOut, onMaximize, onMinimize, onClose }: ChatViewProps) { +export function ChatView({ projectId, addToast, floating = false, compactLayout = false, onPopOut, onMaximize, onMinimize, onClose }: ChatViewProps) { const { t } = useTranslation("app"); useEffect(() => { recordResumeEvent({ @@ -1188,7 +1193,7 @@ export function ChatView({ projectId, addToast, floating = false, onPopOut, onMa observer.observe(element); return () => observer.disconnect(); }, [floating]); - const isChatMobile = isMobile || floatingNarrow; + const isChatMobile = isMobile || floatingNarrow || compactLayout; useEffect(() => { if (!activeSession?.id) { diff --git a/packages/dashboard/app/components/RightDock.tsx b/packages/dashboard/app/components/RightDock.tsx index a5706bbe05..74feff4b82 100644 --- a/packages/dashboard/app/components/RightDock.tsx +++ b/packages/dashboard/app/components/RightDock.tsx @@ -69,7 +69,7 @@ function isInlineOverflowViewKey(key: string, options: OverflowViewVisibilityOpt return Boolean(entry?.render); } -function readStoredRightDockView(options: OverflowViewVisibilityOptions): OverflowViewKey { +export function readStoredRightDockView(options: OverflowViewVisibilityOptions): OverflowViewKey { if (typeof window === "undefined") return "files"; const stored = window.localStorage.getItem(RIGHT_DOCK_VIEW_STORAGE_KEY); return stored && isOverflowViewKeyVisible(stored, options) && isInlineOverflowViewKey(stored, options) ? stored : "files"; diff --git a/packages/dashboard/app/components/__tests__/ChatView.sessions-rooms.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.sessions-rooms.test.tsx index 18a50ae3df..53be8f71a7 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.sessions-rooms.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.sessions-rooms.test.tsx @@ -877,6 +877,15 @@ describe("Chat pop-out header actions", () => { expect(onClose).toHaveBeenCalledTimes(1); }); + it("forces the narrow one-pane class when hosted in compact right-dock layout", async () => { + setupMockChat({ sessions: [], filteredSessions: [] }); + + await renderWithAct(); + + expect(document.querySelector(".chat-view")).toHaveClass("chat-view--narrow"); + expect(screen.queryByTestId("chat-pop-out")).toBeNull(); + }); + it("defines a modal-width narrow layout that mirrors mobile one-pane behavior", async () => { const css = loadAllAppCss(); diff --git a/packages/dashboard/app/components/__tests__/RightDock.test.tsx b/packages/dashboard/app/components/__tests__/RightDock.test.tsx index bc25d9710b..46bfcd5928 100644 --- a/packages/dashboard/app/components/__tests__/RightDock.test.tsx +++ b/packages/dashboard/app/components/__tests__/RightDock.test.tsx @@ -35,10 +35,11 @@ function TestRightDock(props: Omit & P /* FNXC:Navigation 2026-06-22-16:00: -The right dock is now an all-inline tools rail sourced from STATIC_OVERFLOW_VIEW_ENTRIES in overflowViewRegistry. The roster, in registry order, is files, activity-log, git-manager, devserver (gated on devServerView), secrets, todos (gated on todosEnabled), pull-requests. The earlier usage/github-import/automation launcher actions were removed, so every visible tab is an inline view that switches the dock body and can expand into the modal. +The right dock is now an all-inline tools rail sourced from STATIC_OVERFLOW_VIEW_ENTRIES in overflowViewRegistry. The roster, in registry order, is files, chat, activity-log, git-manager, devserver (gated on devServerView), secrets, todos (gated on todosEnabled), pull-requests. The earlier usage/github-import/automation launcher actions were removed, so every visible tab is an inline view that switches the dock body and can expand into the modal. */ const toolTabIds = [ "right-dock-tab-files", + "right-dock-tab-chat", "right-dock-tab-activity-log", "right-dock-tab-git-manager", "right-dock-tab-devserver", @@ -302,10 +303,11 @@ describe("RightDock", () => { /* FNXC:Navigation 2026-06-22-16:00: - With devServerView and todosEnabled both on, the full seven-entry roster renders in registry order. Files, Activity Log, Git Manager, Dev Server, Secrets, Todos, and Pull Requests are all inline views. + With devServerView and todosEnabled both on, the full eight-entry roster renders in registry order. Files, Chat, Activity Log, Git Manager, Dev Server, Secrets, Todos, and Pull Requests are all inline views. */ expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual(toolTabIds); expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-label", "Files"); + expect(screen.getByTestId("right-dock-tab-chat")).toHaveAttribute("aria-label", "Chat"); expect(screen.getByTestId("right-dock-tab-activity-log")).toHaveAttribute("aria-label", "Activity Log"); expect(screen.getByTestId("right-dock-tab-git-manager")).toHaveAttribute("aria-label", "Git Manager"); expect(screen.getByTestId("right-dock-tab-devserver")).toHaveAttribute("aria-label", "Dev Server"); @@ -320,11 +322,12 @@ describe("RightDock", () => { it("gates devserver and todos tabs behind their visibility flags", () => { /* FNXC:Navigation 2026-06-22-16:00: - devserver is gated on experimentalFeatures.devServerView and todos on todosEnabled. With both unset (default renderProps), the dock renders only the five always-on inline tools. + devserver is gated on experimentalFeatures.devServerView and todos on todosEnabled. With both unset (default renderProps), the dock renders only the six always-on inline tools. */ render(); expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual([ "right-dock-tab-files", + "right-dock-tab-chat", "right-dock-tab-activity-log", "right-dock-tab-git-manager", "right-dock-tab-secrets", diff --git a/packages/dashboard/app/components/__tests__/overflowViewRegistry.chat.test.tsx b/packages/dashboard/app/components/__tests__/overflowViewRegistry.chat.test.tsx new file mode 100644 index 0000000000..8dcfa46c7d --- /dev/null +++ b/packages/dashboard/app/components/__tests__/overflowViewRegistry.chat.test.tsx @@ -0,0 +1,90 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + findOverflowViewEntry, + getVisibleOverflowViewEntries, + isOverflowViewKeyVisible, + type OverflowViewRenderProps, +} from "../overflowViewRegistry"; +import { readStoredRightDockView, RIGHT_DOCK_VIEW_STORAGE_KEY } from "../RightDock"; +import type { ChatViewProps } from "../ChatView"; + +vi.mock("../ChatView", () => ({ + ChatView: ({ projectId, addToast, floating, compactLayout, onPopOut, onMaximize, onMinimize, onClose }: ChatViewProps) => ( +
+ Chat dock view +
+ ), +})); + +const renderProps: OverflowViewRenderProps = { + projectId: "project-chat", + addToast: vi.fn(), +}; + +describe("overflowViewRegistry chat entry", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + afterEach(() => { + cleanup(); + window.localStorage.clear(); + vi.clearAllMocks(); + }); + + it("registers Chat as an always-visible inline right-dock entry", () => { + const chatEntry = getVisibleOverflowViewEntries({}).find((entry) => entry.key === "chat"); + + expect(chatEntry).toBeTruthy(); + expect(chatEntry?.testId).toBe("right-dock-tab-chat"); + expect(chatEntry?.render).toBeTypeOf("function"); + expect(chatEntry?.onActivate).toBeUndefined(); + expect(getVisibleOverflowViewEntries({}).map((entry) => entry.key)).toContain("chat"); + }); + + it("resolves Chat through registry helpers as a renderable visible view", () => { + const chatEntry = findOverflowViewEntry("chat"); + + expect(chatEntry?.key).toBe("chat"); + expect(chatEntry?.render).toBeTypeOf("function"); + expect(chatEntry?.onActivate).toBeUndefined(); + expect(isOverflowViewKeyVisible("chat")).toBe(true); + }); + + it("renders ChatView for both compact dock and expanded pop-out surfaces", async () => { + const chatEntry = findOverflowViewEntry("chat"); + if (!chatEntry?.render) throw new Error("Expected chat registry entry to render inline"); + + const compact = render(<>{chatEntry.render({ ...renderProps, surface: "dock", dockWidth: 360 })}); + const compactChat = await screen.findByTestId("mock-chat-view"); + expect(compactChat).toHaveAttribute("data-project-id", "project-chat"); + expect(compactChat).toHaveAttribute("data-has-toast", "true"); + expect(compactChat).toHaveAttribute("data-compact-layout", "true"); + expect(compactChat).toHaveAttribute("data-has-dock-chrome-props", "false"); + compact.unmount(); + + const wideDock = render(<>{chatEntry.render({ ...renderProps, surface: "dock", dockWidth: 900 })}); + const wideDockChat = await screen.findByTestId("mock-chat-view"); + expect(wideDockChat).toHaveAttribute("data-compact-layout", "false"); + wideDock.unmount(); + + render(<>{chatEntry.render({ ...renderProps, surface: "expand" })}); + const expandedChat = await screen.findByTestId("mock-chat-view"); + expect(expandedChat).toHaveAttribute("data-project-id", "project-chat"); + expect(expandedChat).toHaveAttribute("data-has-toast", "true"); + expect(expandedChat).toHaveAttribute("data-compact-layout", "false"); + expect(expandedChat).toHaveAttribute("data-has-dock-chrome-props", "false"); + }); + + it("keeps Files as the default right-dock view when no selection is persisted", () => { + expect(window.localStorage.getItem(RIGHT_DOCK_VIEW_STORAGE_KEY)).toBeNull(); + expect(readStoredRightDockView({})).toBe("files"); + }); +}); diff --git a/packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx b/packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx index ae074bfe1d..91cbbdce41 100644 --- a/packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx +++ b/packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx @@ -13,6 +13,7 @@ describe("overflowViewRegistry", () => { expect(keys).toEqual([ "files", + "chat", "activity-log", "git-manager", "devserver", @@ -22,6 +23,7 @@ describe("overflowViewRegistry", () => { ]); expect(entries.map((entry) => entry.label)).toEqual([ "Files", + "Chat", "Activity Log", "Git Manager", "Dev Server", @@ -38,7 +40,7 @@ describe("overflowViewRegistry", () => { const keys = getVisibleOverflowViewEntries().map((entry) => entry.key); // devserver requires experimentalFeatures.devServerView; todos requires todosEnabled. - expect(keys).toEqual(["files", "activity-log", "git-manager", "secrets", "pull-requests"]); + expect(keys).toEqual(["files", "chat", "activity-log", "git-manager", "secrets", "pull-requests"]); expect(keys).not.toContain("devserver"); expect(keys).not.toContain("todos"); // Usage moved back to the top header; it is no longer a right-dock key. @@ -107,6 +109,7 @@ describe("overflowViewRegistry", () => { }); expect(entries.map((entry) => entry.key)).toEqual([ "files", + "chat", "activity-log", "git-manager", "devserver", diff --git a/packages/dashboard/app/components/overflowViewRegistry.tsx b/packages/dashboard/app/components/overflowViewRegistry.tsx index 26ac2eddaf..f7d02d3120 100644 --- a/packages/dashboard/app/components/overflowViewRegistry.tsx +++ b/packages/dashboard/app/components/overflowViewRegistry.tsx @@ -6,6 +6,7 @@ import { GitPullRequest, History, Lock, + MessageSquare, Monitor, type LucideProps, } from "lucide-react"; @@ -29,12 +30,14 @@ const DevServerView = lazy(() => import("./DevServerView").then((m) => ({ defaul const SecretsView = lazy(() => import("./SecretsView").then((m) => ({ default: m.SecretsView }))); const TodoView = lazy(() => import("./TodoView").then((m) => ({ default: m.TodoView }))); const PullRequestView = lazy(() => import("./PullRequestView").then((m) => ({ default: m.PullRequestView }))); +const ChatView = lazy(() => import("./ChatView").then((m) => ({ default: m.ChatView }))); export type OverflowViewKey = | "usage" | "activity-log" | "git-manager" | "files" + | "chat" | "devserver" | "secrets" | "todos" @@ -110,6 +113,11 @@ FNXC:RightDockFiles 2026-06-23-00:50: When the dock body is at least this wide there is clearly room for the Files tree|viewer two-pane split, so the dock forces DockFilesView layout="two-pane" deterministically instead of relying on the unreliable @container dock-files query (its root content-box often measured under the breakpoint and kept the view stacked). Matched to the CSS @container dock-files (min-width: 640px) breakpoint; compared against the threaded outer dock width (the dock chrome padding is small relative to 640px of content, so 640 outer width safely implies enough body width for two panes). */ const RIGHT_DOCK_FILES_TWO_PANE_MIN_WIDTH = 640; +/* +FNXC:RightDockChat 2026-06-27-23:12: +ChatView's desktop split pane is unusable in the default 360px right dock, so compact dock hosts force ChatView's narrow list/detail layout until the dock is wider than the tablet/mobile breakpoint. The expanded pop-out keeps the full desktop layout. +*/ +const RIGHT_DOCK_CHAT_COMPACT_MAX_WIDTH = 768; function wrapOverflowView(node: ReactNode): ReactNode { return ( @@ -157,6 +165,23 @@ export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [ />, ), }, + /* + FNXC:Navigation 2026-06-27-00:00: + The right dock hosts the full ChatView as an always-visible inline tool so the compact dock body and the floating expand modal reuse the same conversational surface without adding another navigation destination. + */ + { + key: "chat", + label: "Chat", + icon: MessageSquare, + testId: "right-dock-tab-chat", + render: (props) => wrapOverflowView( + , + ), + }, { key: "activity-log", label: "Activity Log",