FN-7210: add right-dock Tasks tab

Add a first-class Tasks panel to the right dock while preserving Files as the default dock view.

- Register a Tasks overflow view with a compact dock task list and empty state.
- Anchor in-dock task detail to the Tasks tab so tab switches preserve the last-viewed task snapshot.
- Update right-dock controller wiring, tests, docs, and changesets for the new dock task surface.

Files changed:
 ...210-recover-completed-incomplete-steps-guard.md |   2 +-
 .changeset/fn-7210-right-dock-tasks-tab.md         |   7 ++
 docs/dashboard-guide.md                            |   7 +-
 packages/dashboard/app/components/DockTaskList.css |  36 +++++++
 packages/dashboard/app/components/DockTaskList.tsx |  58 +++++++++++
 packages/dashboard/app/components/RightDock.tsx    |  26 ++++-
 .../app/components/__tests__/DockTaskList.test.tsx |  48 ++++++++++
 .../app/components/__tests__/RightDock.test.tsx    | 106 +++++++++++++++++----
 .../__tests__/overflowViewRegistry.test.tsx        |   5 +-
 .../app/components/overflowViewRegistry.tsx        |  26 ++++-
 .../app/components/useRightDockController.tsx      |   7 +-
 11 files changed, 298 insertions(+), 30 deletions(-)

Fusion-Task-Id: FN-7210
Fusion-Task-Lineage: 27468cc2-6ce9-46cc-a27e-cf5f49e93c40
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-28 20:11:43 -07:00
parent d87db1490e
commit f3d9bfb27b
11 changed files with 298 additions and 30 deletions

View File

@@ -2,6 +2,6 @@
"@runfusion/fusion": patch
---
summary: Fix tasks stuck in review after a code-review revision by stopping the merge-retry loop from starving the executor's fix pass.
summary: Fix review tasks stuck when merge retries starve the executor's code-review revision pass.
category: fix
dev: recoverCompletedTask now refuses workflow-graph re-entry when the live task has incomplete steps or a remediation bounce (sendTaskBackForFix → scheduleWorkflowRerun) is already scheduled, so a pre-merge optional/advisory REVISE that reopens plan steps lets the executor finish them instead of re-passing the advisory step (budget exhausted) and looping on the "task has incomplete steps" merge gate. Regression: restart.integration.test.ts.

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a Tasks tab to the right sidebar that shows the last-viewed task or a clickable task list.
category: feature
dev: New `tasks` overflow-view registry entry + `DockTaskList` empty state. The FN-7169 dock-task overlay is re-anchored to the Tasks tab; the task snapshot now persists across tab switches and clears on back/close or surface teardown. Default dock view stays `files`.

View File

@@ -55,14 +55,15 @@ When enabled on desktop or tablet project screens, the right dock is a persisten
If **Settings → Appearance → Open tasks in the right sidebar** is enabled, board task-card clicks open task detail inside this right dock and keep the board visible. The setting is default off; mobile or hidden/inactive dock states automatically fall back to the existing full-panel task detail, and non-board task-open paths keep their existing behavior.
<!-- 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.
<!-- FNXC:RightDockTasks 2026-06-28-19:55: The dock task-detail overlay is now anchored to the first-class Tasks tool tab. Document that Tasks is a dock-only auxiliary surface with a last-viewed detail/list fallback, not a new primary navigation destination. -->
The dock toolbar has built-in inline tool panels for **Tasks**, **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. The **Tasks** tab shows the last task opened in the dock; when no dock task is active, it shows a compact clickable task list, and the task-detail back button returns to that list. 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 **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.
2. Select **Tasks**, **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; **Tasks** either restores the last-viewed dock task or shows the compact task list.
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.
4. Select the dock expand action.

View File

@@ -0,0 +1,36 @@
.dock-task-list {
display: flex;
flex-direction: column;
gap: var(--space-sm);
min-height: 0;
height: 100%;
overflow-y: auto;
padding: var(--space-sm);
}
.dock-task-list__row {
min-width: 0;
}
.dock-task-list--empty {
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
padding: var(--space-lg);
}
.dock-task-list__empty-title,
.dock-task-list__empty-copy {
margin: 0;
}
.dock-task-list__empty-title {
color: var(--text-primary);
font-weight: var(--font-weight-semibold);
}
.dock-task-list__empty-copy {
max-width: calc(var(--space-xl) * 12);
font-size: var(--font-size-sm);
}

View File

@@ -0,0 +1,58 @@
import { useCallback } from "react";
import type { Task, TaskDetail } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { TaskCard } from "./TaskCard";
import "./DockTaskList.css";
export interface DockTaskListProps {
tasks: Array<Task | TaskDetail>;
projectId?: string;
onOpenTask?: (task: Task | TaskDetail) => void;
addToast?: (message: string, type?: ToastType) => void;
prAuthAvailable?: boolean;
autoMergeEnabled?: boolean;
}
/*
FNXC:RightDockTasks 2026-06-28-16:50:
The Tasks tab empty state is a real compact task list, not a blank placeholder. TaskCard's own open callback is routed directly to `onOpenTask` so clicking the card opens the dock Tasks detail with the back button; no wrapper click handler competes with TaskCard or the full-panel detail modal.
*/
export function DockTaskList({
tasks,
projectId,
onOpenTask,
addToast = () => {},
prAuthAvailable = false,
autoMergeEnabled = false,
}: DockTaskListProps) {
const handleOpenTask = useCallback((task: Task | TaskDetail) => {
onOpenTask?.(task);
}, [onOpenTask]);
if (tasks.length === 0) {
return (
<div className="dock-task-list dock-task-list--empty" data-testid="dock-task-list">
<p className="dock-task-list__empty-title">No tasks yet</p>
<p className="dock-task-list__empty-copy">Tasks you create or import will appear here for quick right-sidebar review.</p>
</div>
);
}
return (
<div className="dock-task-list" data-testid="dock-task-list">
{tasks.map((task) => (
<div key={task.id} className="dock-task-list__row" data-testid={`dock-task-list-row-${task.id}`}>
<TaskCard
task={task as Task}
projectId={projectId}
onOpenDetail={handleOpenTask}
addToast={addToast}
disableDrag={true}
prAuthAvailable={prAuthAvailable}
autoMergeEnabled={autoMergeEnabled}
/>
</div>
))}
</div>
);
}

View File

@@ -148,6 +148,16 @@ export function RightDock({
}
}, [selectedKey, visibilityOptions]);
useEffect(() => {
if (!dockTask) return;
/*
FNXC:RightDockTasks 2026-06-28-16:55:
Programmatic dock-task opens (for example board-card clicks) land on the dedicated Tasks tab without lifting selectedKey to the controller. Persisting `tasks` restores the same first-class dock surface while keeping the no-storage default as Files.
*/
setSelectedKey("tasks");
persistRightDockView("tasks");
}, [dockTask]);
const selectedEntry = (findOverflowViewEntry(selectedKey, visibilityOptions)?.render
? findOverflowViewEntry(selectedKey, visibilityOptions)
: findOverflowViewEntry("files", visibilityOptions)) ?? entries.find((entry) => entry.render);
@@ -160,13 +170,12 @@ export function RightDock({
}
if (!entry?.render) return;
/*
FNXC:OpenTasksInRightSidebar 2026-06-28-00:00:
Selecting any normal right-dock tab leaves the task-detail overlay surface and restores the last overflow-view body. This avoids stacking task detail over Files/Goals and prevents orphaned task headers after the user intentionally switches dock context.
FNXC:RightDockTasks 2026-06-28-16:58:
Tab switches no longer clear the dock-task snapshot. Detail is anchored to the Tasks tab, so selecting Files/Chat hides the detail while preserving the last-viewed task for when the user returns to Tasks.
*/
onCloseDockTask?.();
setSelectedKey(key);
persistRightDockView(key);
}, [onCloseDockTask, renderProps, visibilityOptions]);
}, [renderProps, visibilityOptions]);
const handleResizeStart = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
event.preventDefault();
@@ -236,7 +245,11 @@ export function RightDock({
}
const SelectedIcon = selectedEntry.icon;
const showingDockTask = Boolean(dockTask && dockTaskContent);
/*
FNXC:RightDockTasks 2026-06-28-17:00:
Task detail is visible only on the Tasks tab; other tabs render their own registry bodies while the task snapshot persists in the controller. Back/close clears the snapshot and leaves the selected Tasks body to render the list.
*/
const showingDockTask = Boolean(dockTask && dockTaskContent && selectedKey === "tasks");
const dockWidth = `${width}px`;
const expandSelectedViewLabel = t("rightDock.expandView", "Expand {{label}}", { label: selectedEntry.label });
const closeDockTaskLabel = t("rightDock.closeTaskDetail", "Back to right dock views");
@@ -340,6 +353,9 @@ export function RightDock({
{/*
FNXC:RightDockFiles 2026-06-23-00:50:
Thread the live dock width down to registry render functions as `dockWidth` (alongside surface="dock") so a view can deterministically choose its wide layout from the actual dock size. The Files entry uses this to force two-pane when the dock is wide enough, sidestepping the @container query that never reliably fired in the narrow-vs-wide dock body.
FNXC:RightDockTasks 2026-06-28-17:02:
The Tasks tab without an active snapshot falls through to its registry render, which is the compact DockTaskList. Only a selected Tasks tab with live dockTaskContent replaces this body with TaskDetailContent.
*/}
{showingDockTask ? dockTaskContent : selectedEntry.render?.({ ...renderProps, surface: "dock", dockWidth: width })}
</div>

View File

@@ -0,0 +1,48 @@
import { fireEvent, render, screen } from "@testing-library/react";
import type { Task, TaskDetail } from "@fusion/core";
import { describe, expect, it, vi } from "vitest";
import { DockTaskList } from "../DockTaskList";
vi.mock("../TaskCard", () => ({
TaskCard: ({ task, onOpenDetail, disableDrag }: { task: Task | TaskDetail; onOpenDetail: (task: Task | TaskDetail) => void; disableDrag?: boolean }) => (
<button
type="button"
data-testid={`mock-task-card-${task.id}`}
data-disable-drag={String(disableDrag)}
onClick={() => onOpenDetail(task)}
>
{task.title ?? task.id}
</button>
),
}));
/*
FNXC:RightDockTasks 2026-06-28-17:15:
DockTaskList must route TaskCard's own open action to the dock snapshot setter. This explicitly guards against a nested row/card handler split where the card opens the full detail modal while the wrapper also opens the dock detail.
*/
describe("DockTaskList", () => {
it("renders populated task rows and routes TaskCard opens to onOpenTask", () => {
const first = { id: "FN-1", title: "First task", column: "todo" } as Task;
const second = { id: "FN-2", title: "Second task", column: "in-progress" } as Task;
const onOpenTask = vi.fn();
render(<DockTaskList tasks={[first, second]} onOpenTask={onOpenTask} addToast={vi.fn()} />);
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
expect(screen.getByTestId("dock-task-list-row-FN-1")).toBeInTheDocument();
expect(screen.getByTestId("dock-task-list-row-FN-2")).toBeInTheDocument();
expect(screen.getByTestId("mock-task-card-FN-1")).toHaveAttribute("data-disable-drag", "true");
fireEvent.click(screen.getByTestId("mock-task-card-FN-2"));
expect(onOpenTask).toHaveBeenCalledTimes(1);
expect(onOpenTask).toHaveBeenCalledWith(second);
});
it("renders a friendly empty message and no task rows when there are no tasks", () => {
render(<DockTaskList tasks={[]} onOpenTask={vi.fn()} addToast={vi.fn()} />);
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
expect(screen.getByText("No tasks yet")).toBeInTheDocument();
expect(screen.queryByTestId(/dock-task-list-row-/)).toBeNull();
});
});

View File

@@ -20,6 +20,18 @@ vi.mock("../TaskDetailModal", () => ({
),
}));
vi.mock("../DockTaskList", () => ({
DockTaskList: ({ tasks = [], onOpenTask }: { tasks?: Array<{ id: string; title?: string }>; onOpenTask?: (task: { id: string; title?: string }) => void }) => (
<div data-testid="dock-task-list">
{tasks.length === 0 ? <span>No tasks yet</span> : tasks.map((task) => (
<button key={task.id} type="button" data-testid={`dock-task-list-row-${task.id}`} onClick={() => onOpenTask?.(task)}>
{task.title ?? task.id}
</button>
))}
</div>
),
}));
vi.mock("../../api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api")>();
return {
@@ -41,9 +53,10 @@ 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, 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.
The right dock is now an all-inline tools rail sourced from STATIC_OVERFLOW_VIEW_ENTRIES in overflowViewRegistry. The roster, in registry order, is tasks, 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-tasks",
"right-dock-tab-files",
"right-dock-tab-chat",
"right-dock-tab-activity-log",
@@ -136,6 +149,18 @@ describe("RightDock", () => {
expect(screen.getByTestId("right-dock-files-view")).toHaveAttribute("data-layout", "two-pane");
});
it("renders the Tasks tab list at both narrow and wide dock widths", () => {
const { unmount } = render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} />);
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
unmount();
window.localStorage.setItem(RIGHT_DOCK_WIDTH_STORAGE_KEY, "900");
render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} />);
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
});
it("falls back to Files when storage points at a removed right-dock view", () => {
window.localStorage.setItem(RIGHT_DOCK_VIEW_STORAGE_KEY, "documents");
render(<TestRightDock open={true} renderProps={renderProps} />);
@@ -159,49 +184,59 @@ describe("RightDock", () => {
expect(screen.queryByTestId("right-dock-collapse-toggle")).toBeNull();
});
it("renders dock task detail in the body and returns to overflow views from the close affordance", () => {
it("anchors dock task detail to Tasks and returns to the task list from the close affordance", () => {
const onCloseDockTask = vi.fn();
const { rerender } = render(
<TestRightDock
open={true}
renderProps={renderProps}
renderProps={{ ...renderProps, tasks: [] }}
dockTask={{ id: "FN-7169", title: "Sidebar task" } as never}
dockTaskContent={<div data-testid="dock-task-detail">Sidebar task</div>}
onCloseDockTask={onCloseDockTask}
/>,
);
expect(screen.getByTestId("right-dock-tab-tasks")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("right-dock-body")).toHaveTextContent("Sidebar task");
expect(screen.queryByTestId("right-dock-files-view")).toBeNull();
fireEvent.click(screen.getByTestId("right-dock-close-task"));
expect(onCloseDockTask).toHaveBeenCalledTimes(1);
rerender(<TestRightDock open={true} renderProps={renderProps} dockTask={null} dockTaskContent={null} onCloseDockTask={onCloseDockTask} />);
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
rerender(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} dockTask={null} dockTaskContent={null} onCloseDockTask={onCloseDockTask} />);
expect(screen.getByTestId("right-dock-tab-tasks")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
expect(screen.queryByTestId("right-dock-files-view")).toBeNull();
});
it("clears dock task detail when a normal right-dock tab is selected", () => {
it("preserves dock task detail across normal right-dock tab switches", () => {
const onCloseDockTask = vi.fn();
render(
<TestRightDock
open={true}
renderProps={renderProps}
renderProps={{ ...renderProps, tasks: [] }}
dockTask={{ id: "FN-7169", title: "Sidebar task" } as never}
dockTaskContent={<div data-testid="dock-task-detail">Sidebar task</div>}
onCloseDockTask={onCloseDockTask}
/>,
);
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("Sidebar task");
fireEvent.click(screen.getByTestId("right-dock-tab-git-manager"));
expect(onCloseDockTask).toHaveBeenCalledTimes(1);
expect(onCloseDockTask).not.toHaveBeenCalled();
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
expect(screen.getByText("Git Manager")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("Sidebar task");
});
it("controller dock task opens, replaces, and clears on inactive teardown", () => {
it("controller dock task opens, replaces, persists across tabs, and clears on close or inactive teardown", () => {
const firstTask = { id: "FN-1", title: "First task", column: "todo" };
const secondTask = { id: "FN-2", title: "Second task", column: "todo" };
const openDetailTask = vi.fn();
const controllerInput = {
active: true,
projectId: "project-1",
@@ -211,7 +246,7 @@ describe("RightDock", () => {
tasks: [firstTask, secondTask],
workflowSteps: [],
subscribePluginEvents: () => () => {},
openDetailTask: vi.fn(),
openDetailTask,
openFileInBrowser: vi.fn(),
onMoveTask: vi.fn(),
onDeleteTask: vi.fn(),
@@ -227,8 +262,8 @@ describe("RightDock", () => {
footerVisible: false,
} as unknown as RightDockControllerInput;
function Harness({ active }: { active: boolean }) {
const controller = useRightDockController({ ...controllerInput, active });
function Harness({ active, tasks = [firstTask, secondTask] }: { active: boolean; tasks?: Array<typeof firstTask> }) {
const controller = useRightDockController({ ...controllerInput, active, tasks });
return (
<>
<button type="button" data-testid="open-first" onClick={() => controller.openTaskInDock(firstTask as never)}>open first</button>
@@ -242,16 +277,28 @@ describe("RightDock", () => {
const { rerender } = render(<Harness active={true} />);
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("open-first"));
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("dock-task-list-row-FN-1"));
expect(openDetailTask).not.toHaveBeenCalled();
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("First task");
fireEvent.click(screen.getByTestId("right-dock-tab-files"));
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("First task");
fireEvent.click(screen.getByTestId("open-second"));
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("Second task");
expect(screen.queryByText("First task")).toBeNull();
rerender(<Harness active={true} tasks={[]} />);
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("Second task");
fireEvent.click(screen.getByTestId("close-dock-task"));
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("open-first"));
expect(screen.getByTestId("dock-task-detail")).toHaveTextContent("First task");
@@ -259,7 +306,7 @@ describe("RightDock", () => {
expect(screen.queryByTestId("right-dock")).toBeNull();
rerender(<Harness active={true} />);
expect(screen.queryByTestId("dock-task-detail")).toBeNull();
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
});
it("renders the pin affordance for both states and delegates the toggle", () => {
@@ -412,9 +459,10 @@ describe("RightDock", () => {
/*
FNXC:Navigation 2026-06-22-16:00:
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.
With devServerView and todosEnabled both on, the full nine-entry roster renders in registry order. Tasks, 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-tasks")).toHaveAttribute("aria-label", "Tasks");
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");
@@ -431,10 +479,11 @@ 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 six always-on inline tools.
devserver is gated on experimentalFeatures.devServerView and todos on todosEnabled. With both unset (default renderProps), the dock renders only the seven always-on inline tools.
*/
render(<TestRightDock open={true} renderProps={renderProps} />);
expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("data-testid"))).toEqual([
"right-dock-tab-tasks",
"right-dock-tab-files",
"right-dock-tab-chat",
"right-dock-tab-activity-log",
@@ -451,11 +500,15 @@ describe("RightDock", () => {
FNXC:Navigation 2026-06-22-16:00:
The right dock no longer hosts launcher-action tabs that fire Header handlers; every tab is an inline view. Clicking a non-Files tab selects it (aria-selected flips, Files deselects) and replaces the body, and the Files tab restores the inline Files view.
*/
render(<TestRightDock open={true} renderProps={renderProps} />);
render(<TestRightDock open={true} renderProps={{ ...renderProps, tasks: [] }} />);
expect(screen.getByTestId("right-dock-tab-files")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("right-dock-tab-tasks"));
expect(screen.getByTestId("right-dock-tab-tasks")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
for (const tabId of ["right-dock-tab-activity-log", "right-dock-tab-git-manager", "right-dock-tab-secrets"]) {
fireEvent.click(screen.getByTestId(tabId));
expect(screen.getByTestId(tabId)).toHaveAttribute("aria-selected", "true");
@@ -552,6 +605,23 @@ describe("RightDock", () => {
focusButton.remove();
});
it("renders the Tasks list in the expanded modal and routes row clicks back to the dock", () => {
const onOpenTaskInDock = vi.fn();
const task = { id: "FN-EXPAND", title: "Expanded task", column: "todo" };
render(
<RightDockExpandModal
viewKey="tasks"
renderProps={{ ...renderProps, tasks: [task], onOpenTaskInDock }}
onClose={vi.fn()}
/>,
);
expect(screen.getByTestId("right-dock-expand-modal")).toHaveAttribute("aria-label", "Tasks expanded");
expect(screen.getByTestId("dock-task-list")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("dock-task-list-row-FN-EXPAND"));
expect(onOpenTaskInDock).toHaveBeenCalledWith(task);
});
it("does not render the expanded modal for action entries", () => {
render(
<RightDockExpandModal

View File

@@ -12,6 +12,7 @@ describe("overflowViewRegistry", () => {
const keys = entries.map((entry) => entry.key);
expect(keys).toEqual([
"tasks",
"files",
"chat",
"activity-log",
@@ -22,6 +23,7 @@ describe("overflowViewRegistry", () => {
"pull-requests",
]);
expect(entries.map((entry) => entry.label)).toEqual([
"Tasks",
"Files",
"Chat",
"Activity Log",
@@ -40,7 +42,7 @@ describe("overflowViewRegistry", () => {
const keys = getVisibleOverflowViewEntries().map((entry) => entry.key);
// devserver requires experimentalFeatures.devServerView; todos requires todosEnabled.
expect(keys).toEqual(["files", "chat", "activity-log", "git-manager", "secrets", "pull-requests"]);
expect(keys).toEqual(["tasks", "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.
@@ -108,6 +110,7 @@ describe("overflowViewRegistry", () => {
pluginDashboardViews,
});
expect(entries.map((entry) => entry.key)).toEqual([
"tasks",
"files",
"chat",
"activity-log",

View File

@@ -2,6 +2,7 @@ import { Suspense, lazy, type ComponentType, type ReactNode } from "react";
import {
CheckSquare,
Folder,
ListTodo,
GitBranch,
GitPullRequest,
History,
@@ -21,6 +22,7 @@ import { PageErrorBoundary } from "./ErrorBoundary";
import { getPluginNavIcon } from "./pluginNavIcon";
import { ActivityLogModal } from "./ActivityLogModal";
import { GitManagerModal } from "./GitManagerModal";
import { DockTaskList } from "./DockTaskList";
/*
FNXC:Navigation 2026-06-22-00:40:
@@ -36,6 +38,7 @@ export type OverflowViewKey =
| "usage"
| "activity-log"
| "git-manager"
| "tasks"
| "files"
| "chat"
| "devserver"
@@ -75,6 +78,7 @@ export interface OverflowViewRenderProps {
pluginContext?: PluginDashboardViewContext;
onOpenSettings?: (section?: string) => void;
onOpenTaskDetail?: (taskId: string) => void;
onOpenTaskInDock?: (task: Task | TaskDetail) => void;
onOpenDetail?: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
onSendSelectionToTask?: (description: string) => void;
onCreateTaskFromInsight?: (payload: { insightId: string; title: string; description: string }) => Promise<void> | void;
@@ -139,7 +143,27 @@ FNXC:Navigation 2026-06-22-00:00:
Right-dock tools render INLINE inside the dock container, not as popup modals: usage, activity-log, and git-manager use each modal's `presentation="embedded"` mode instead of launching an overlay. (github-import and automation remain launcher actions here only until their left-sidebar/main destinations land, then they leave the dock.)
*/
export const STATIC_OVERFLOW_VIEW_ENTRIES: readonly OverflowViewEntry[] = [
/* FNXC:Navigation 2026-06-22-00:20: Files is the first/default right-dock tool. */
/*
FNXC:RightDockTasks 2026-06-28-16:45:
Tasks is the leading right-dock inline view, but the persisted/default selection remains Files. It hosts the compact task list on both dock and expand surfaces; the dock-only detail surface is selected by RightDock when a task snapshot exists.
*/
{
key: "tasks",
label: "Tasks",
icon: ListTodo,
testId: "right-dock-tab-tasks",
render: (props) => wrapOverflowView(
<DockTaskList
tasks={props.tasks ?? []}
projectId={props.projectId}
onOpenTask={props.onOpenTaskInDock}
addToast={props.addToast}
prAuthAvailable={false}
autoMergeEnabled={false}
/>,
),
},
/* FNXC:Navigation 2026-06-22-00:20: Files remains the default right-dock tool when no valid stored view exists. */
{
key: "files",
label: "Files",

View File

@@ -186,6 +186,11 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
.then((task) => input.openDetailTask(task as TaskDetail))
.catch((error) => input.addToast(error instanceof Error ? error.message : "Failed to open task detail", "error"));
},
/*
FNXC:RightDockTasks 2026-06-28-17:05:
DockTaskList rows must open the in-dock Tasks detail, not the canonical full task modal. Thread the existing dock snapshot setter into registry render props so both compact and expanded Tasks lists route TaskCard's internal open action to the Tasks tab.
*/
onOpenTaskInDock: openTaskInDock,
onOpenDetail: input.openDetailTask,
onSendSelectionToTask: input.onSendSelectionToTask,
onCreateTaskFromInsight: input.onCreateTaskFromInsight,
@@ -195,7 +200,7 @@ export function useRightDockController(input: RightDockControllerInput): RightDo
renderTaskCard,
subscribePluginEvents: input.subscribePluginEvents,
openFile: input.openFileInBrowser,
}), [input, renderTaskCard]);
}), [input, openTaskInDock, renderTaskCard]);
const dockTaskContent = resolvedDockTask ? (
/*