From 8145b01e2d6555fdc6034c35e4f4c735d261f6fc Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:14:20 -0600 Subject: [PATCH] feat(ui): add New task above Chat in Coordinator sidebar (#18703) Add a visible New task button above Chat. Open the existing task form in expanded and collapsed sidebar states. Update the Storybook fixture and navigation map. Add tests for action order and dialog dispatch. Co-Authored-By: Paperclip --- maps/feature-map/navigation-preferences.md | 3 +++ ui/src/components/CoordinatorSidebar.test.tsx | 19 +++++++++++++++++-- ui/src/components/CoordinatorSidebar.tsx | 9 ++++++++- .../coordinator-tasks.stories.tsx | 7 ++++--- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/maps/feature-map/navigation-preferences.md b/maps/feature-map/navigation-preferences.md index 07111b37560..a0c867737ac 100644 --- a/maps/feature-map/navigation-preferences.md +++ b/maps/feature-map/navigation-preferences.md @@ -112,6 +112,9 @@ to the existing Agent Chat control. Hidden or Cloud-managed controls are not presented as editable. Disabled Chat routes link back to Experimental settings. Enabling the personal workspace never changes the instance capability. +The Coordinator sidebar has a visible **New task** button above **Chat**. It opens +the existing task form and retains the shortcut beside **Tasks**. + Automated: [preference hook](../../ui/src/hooks/useCoordinatorWorkspaceEnabled.test.tsx), [prerequisite settings card](../../ui/src/components/CoordinatorWorkspaceSetting.test.tsx), [authorization](../../server/src/__tests__/workspace-preferences-routes.test.ts), and diff --git a/ui/src/components/CoordinatorSidebar.test.tsx b/ui/src/components/CoordinatorSidebar.test.tsx index 93cefa92685..fdf7adfc922 100644 --- a/ui/src/components/CoordinatorSidebar.test.tsx +++ b/ui/src/components/CoordinatorSidebar.test.tsx @@ -10,12 +10,12 @@ import { coordinatorAttentionQueryKey, coordinatorAttentionSummaryQueryKey } fro import { queryKeys } from "@/lib/queryKeys"; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; -const fixture = vi.hoisted(() => ({ attention: vi.fn(), summary: vi.fn(), pathname: "/COO/issues", collapsed: false, userId: "reviewer", chats: [] as Array<{ isUnreadForMe: boolean }> })); +const fixture = vi.hoisted(() => ({ attention: vi.fn(), summary: vi.fn(), openNewIssue: vi.fn(), pathname: "/COO/issues", collapsed: false, userId: "reviewer", chats: [] as Array<{ isUnreadForMe: boolean }> })); vi.mock("@/hooks/useAgentChatNavigation", () => ({ useAgentChatNavigation: () => ({ companyId: "company", userId: fixture.userId, agents: { data: [] }, chats: { data: fixture.chats }, session: { isSuccess: true }, }) })); vi.mock("@/hooks/useRecentTasks", () => ({ useRecentTasks: () => ({ entries: [] }) })); -vi.mock("@/context/DialogContext", () => ({ useDialogActions: () => ({ openNewIssue: vi.fn() }) })); +vi.mock("@/context/DialogContext", () => ({ useDialogActions: () => ({ openNewIssue: fixture.openNewIssue }) })); vi.mock("@/context/SidebarContext", () => ({ useSidebar: () => ({ collapsed: fixture.collapsed, peeking: false, isMobile: false, setSidebarOpen: vi.fn() }) })); vi.mock("@/api/attention", () => ({ attentionApi: { list: fixture.attention, summary: fixture.summary } })); vi.mock("@/api/projects", () => ({ projectsApi: { list: async () => [] } })); @@ -49,6 +49,21 @@ beforeEach(() => { client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); }); +describe("Coordinator task creation", () => { + it.each([false, true])("opens the task form from New task above Chat when collapsed is %s", async (collapsed) => { + fixture.collapsed = collapsed; + await renderSidebar(); + const button = container.querySelector('button[aria-label="New task"]'); + const chat = container.querySelector('a[href="/chats"]'); + expect(button).not.toBeNull(); + expect(chat).not.toBeNull(); + expect(button!.compareDocumentPosition(chat!) & Node.DOCUMENT_POSITION_FOLLOWING).not.toBe(0); + expect(fixture.openNewIssue).not.toHaveBeenCalled(); + await act(async () => button!.click()); + expect(fixture.openNewIssue).toHaveBeenCalledOnce(); + }); +}); + describe("Coordinator pinned pages", () => { it("announces unread chat totals in expanded and collapsed navigation", async () => { fixture.chats = [{ isUnreadForMe: true }, { isUnreadForMe: false }, { isUnreadForMe: true }]; diff --git a/ui/src/components/CoordinatorSidebar.tsx b/ui/src/components/CoordinatorSidebar.tsx index 96f5cd0dced..4b538b824f4 100644 --- a/ui/src/components/CoordinatorSidebar.tsx +++ b/ui/src/components/CoordinatorSidebar.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; -import { Inbox, Bell, Boxes, ChevronRight, CircleCheck, FolderOpen, History, LayoutDashboard, MessageCircle, MoreHorizontal, Network, Pin, PinOff, Plus, Repeat, Search, Settings, Unplug } from "lucide-react"; +import { Inbox, Bell, Boxes, ChevronRight, CircleCheck, FolderOpen, History, LayoutDashboard, MessageCircle, MoreHorizontal, Network, Pin, PinOff, Plus, Repeat, Search, Settings, SquarePen, Unplug } from "lucide-react"; import type { Project } from "@paperclipai/shared"; import { useCoordinatorAttentionCount } from "@/hooks/useCoordinatorAttentionCount"; import { issuesApi } from "@/api/issues"; @@ -19,6 +19,8 @@ import { SidebarSection } from "./SidebarSection"; import { TruncatedTextMarqueeProvider } from "./TruncatedText"; import { useScrollbarWhileScrolling } from "./side-panel/use-scrollbar-while-scrolling"; import { Flyout, FlyoutContent, FlyoutTrigger, FlyoutAppearanceProvider } from "./ui/flyout"; +import { Button } from "./ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { StatusIcon } from "./StatusIcon"; const moreDestinations = [ @@ -96,10 +98,15 @@ export function CoordinatorSidebar({ children }: { children?: ReactNode }) { ? [{ kind: "project" as const, id: project.id, time: projectVisits.entries[project.id]!, project }] : []); return [...taskRows, ...chatRows, ...projectRows].sort((a, b) => b.time - a.time).slice(0, 12); }, [agents.data, chats.data, recentTasks, projects, projectVisits, scope]); + const newTaskButton = ; return