diff --git a/.changeset/fn-7210-recover-completed-incomplete-steps-guard.md b/.changeset/fn-7210-recover-completed-incomplete-steps-guard.md index af3b7ff847..2283b038ca 100644 --- a/.changeset/fn-7210-recover-completed-incomplete-steps-guard.md +++ b/.changeset/fn-7210-recover-completed-incomplete-steps-guard.md @@ -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. diff --git a/.changeset/fn-7210-right-dock-tasks-tab.md b/.changeset/fn-7210-right-dock-tasks-tab.md new file mode 100644 index 0000000000..0f0ccee94a --- /dev/null +++ b/.changeset/fn-7210-right-dock-tasks-tab.md @@ -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`. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 1002d0b7aa..547970ea14 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -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. -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. + +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. diff --git a/packages/dashboard/app/components/DockTaskList.css b/packages/dashboard/app/components/DockTaskList.css new file mode 100644 index 0000000000..77124e3f85 --- /dev/null +++ b/packages/dashboard/app/components/DockTaskList.css @@ -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); +} diff --git a/packages/dashboard/app/components/DockTaskList.tsx b/packages/dashboard/app/components/DockTaskList.tsx new file mode 100644 index 0000000000..92567f1daf --- /dev/null +++ b/packages/dashboard/app/components/DockTaskList.tsx @@ -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; + 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 ( +
+

No tasks yet

+

Tasks you create or import will appear here for quick right-sidebar review.

+
+ ); + } + + return ( +
+ {tasks.map((task) => ( +
+ +
+ ))} +
+ ); +} diff --git a/packages/dashboard/app/components/RightDock.tsx b/packages/dashboard/app/components/RightDock.tsx index 9d99f637d5..3e9664e9a1 100644 --- a/packages/dashboard/app/components/RightDock.tsx +++ b/packages/dashboard/app/components/RightDock.tsx @@ -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) => { 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 })} diff --git a/packages/dashboard/app/components/__tests__/DockTaskList.test.tsx b/packages/dashboard/app/components/__tests__/DockTaskList.test.tsx new file mode 100644 index 0000000000..6cb6543280 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/DockTaskList.test.tsx @@ -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 }) => ( + + ), +})); + +/* +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(); + + 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(); + + expect(screen.getByTestId("dock-task-list")).toBeInTheDocument(); + expect(screen.getByText("No tasks yet")).toBeInTheDocument(); + expect(screen.queryByTestId(/dock-task-list-row-/)).toBeNull(); + }); +}); diff --git a/packages/dashboard/app/components/__tests__/RightDock.test.tsx b/packages/dashboard/app/components/__tests__/RightDock.test.tsx index b88fc280a5..46d5085294 100644 --- a/packages/dashboard/app/components/__tests__/RightDock.test.tsx +++ b/packages/dashboard/app/components/__tests__/RightDock.test.tsx @@ -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 }) => ( +
+ {tasks.length === 0 ? No tasks yet : tasks.map((task) => ( + + ))} +
+ ), +})); + vi.mock("../../api", async (importOriginal) => { const actual = await importOriginal(); return { @@ -41,9 +53,10 @@ 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, 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(); + 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(); + 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(); @@ -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( Sidebar task} 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(); - expect(screen.getByTestId("right-dock-files-view")).toBeInTheDocument(); + rerender(); + 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( Sidebar task} 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 }) { + const controller = useRightDockController({ ...controllerInput, active, tasks }); return ( <> @@ -242,16 +277,28 @@ describe("RightDock", () => { const { rerender } = render(); 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(); + 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(); 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(); 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(); + render(); 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( + , + ); + + 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( { 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", diff --git a/packages/dashboard/app/components/overflowViewRegistry.tsx b/packages/dashboard/app/components/overflowViewRegistry.tsx index f7d02d3120..de961a6957 100644 --- a/packages/dashboard/app/components/overflowViewRegistry.tsx +++ b/packages/dashboard/app/components/overflowViewRegistry.tsx @@ -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; @@ -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( + , + ), + }, + /* FNXC:Navigation 2026-06-22-00:20: Files remains the default right-dock tool when no valid stored view exists. */ { key: "files", label: "Files", diff --git a/packages/dashboard/app/components/useRightDockController.tsx b/packages/dashboard/app/components/useRightDockController.tsx index 156b0d188d..12297b9c5a 100644 --- a/packages/dashboard/app/components/useRightDockController.tsx +++ b/packages/dashboard/app/components/useRightDockController.tsx @@ -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 ? ( /*