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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/FN-7168-right-dock-chat.md
Normal file
7
.changeset/FN-7168-right-dock-chat.md
Normal file
@@ -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.
|
||||
@@ -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.
|
||||
<!-- FNXC:DashboardNavigationDocs 2026-06-27-00:00: The right dock now hosts Chat as an inline tool panel; keep this user-facing roster aligned with STATIC_OVERFLOW_VIEW_ENTRIES so users know Chat can also pop out from the dock. -->
|
||||
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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -65,6 +65,11 @@ export interface ChatViewProps {
|
||||
addToast: (msg: string, type?: "success" | "error" | "warning") => void;
|
||||
experimentalFeatures?: Record<string, boolean>;
|
||||
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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(<ChatView projectId="proj-123" addToast={vi.fn()} compactLayout />);
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -35,10 +35,11 @@ function TestRightDock(props: Omit<RightDockProps, "pinned" | "onTogglePin"> & 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(<TestRightDock open={true} renderProps={renderProps} />);
|
||||
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",
|
||||
|
||||
@@ -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) => (
|
||||
<div
|
||||
data-testid="mock-chat-view"
|
||||
data-project-id={projectId}
|
||||
data-has-toast={String(typeof addToast === "function")}
|
||||
data-compact-layout={String(compactLayout === true)}
|
||||
data-has-dock-chrome-props={String(Boolean(floating || onPopOut || onMaximize || onMinimize || onClose))}
|
||||
>
|
||||
Chat dock view
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
<ChatView
|
||||
projectId={props.projectId}
|
||||
addToast={props.addToast}
|
||||
compactLayout={props.surface === "dock" && (props.dockWidth ?? RIGHT_DOCK_CHAT_COMPACT_MAX_WIDTH) <= RIGHT_DOCK_CHAT_COMPACT_MAX_WIDTH}
|
||||
/>,
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "activity-log",
|
||||
label: "Activity Log",
|
||||
|
||||
Reference in New Issue
Block a user