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