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 <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-02 19:14:20 -06:00
1 parent 5955dd4eb4
commit 8145b01e2d
4 files changed
+32 -6

No files matched your search

@@ -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
+17 -2
View File
@@ -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<HTMLButtonElement>('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 }];
+8 -1
View File
@@ -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 = <Button type="button" variant="ghost" className="design-sidebar-new-task rounded-lg" aria-label="New task" onClick={() => openNewIssue()}>
<SquarePen aria-hidden />
<span className={rail ? SIDEBAR_RAIL_HIDDEN_LABEL : "truncate"}>New task</span>
</Button>;
return <FlyoutAppearanceProvider><TruncatedTextMarqueeProvider><aside className="coordinator-sidebar design-sidebar w-full">
<div className="design-sidebar-brand"><SidebarCompanyMenu showChevron={false} /></div>
<nav className="design-sidebar-navigation" aria-label="Workspace navigation">
<div className="design-sidebar-primary">
{rail ? <Tooltip><TooltipTrigger asChild>{newTaskButton}</TooltipTrigger><TooltipContent side="right">New task</TooltipContent></Tooltip> : newTaskButton}
<SidebarNavItem className="design-sidebar-item" to="/chats" label="Chat" icon={MessageCircle} badge={unreadChatCount} badgeLabel="unread chats" />
<div className="group/coordinator-new relative">
<SidebarNavItem to="/issues" label="Tasks" icon={CircleCheck} className={cn("design-sidebar-item", !rail && "pr-9")} />
@@ -3,6 +3,7 @@ import { CoordinatorSidebar } from "@/components/CoordinatorSidebar";
import { CoordinatorTasks } from "@/pages/CoordinatorTasks";
import { DesignPageShell } from "@/components/DesignPageShell";
import { SidebarAccountMenu } from "@/components/SidebarAccountMenu";
import { NewIssueDialog } from "@/components/NewIssueDialog";
import { initialItems } from "../../support/design-proposals/task-page-data";
import { DesignProposalAccess } from "../../support/design-proposals/DesignProposalAccess";
import { prepareFixtureApi } from "../../support/capability-gaps/api";
@@ -11,7 +12,7 @@ import { storybookIssueLabels } from "../../fixtures/paperclipData";
function CoordinatorTasksPreview() {
return <DesignProposalAccess><div className="coordinator-workspace"><DesignPageShell title="Tasks"
sidebar={<><CoordinatorSidebar /><div className="design-sidebar-account"><SidebarAccountMenu forceExpanded /></div></>}
sidebarStorageKey="paperclip.storybook.coordinator-sidebar-width"><CoordinatorTasks /></DesignPageShell></div></DesignProposalAccess>;
sidebarStorageKey="paperclip.storybook.coordinator-sidebar-width"><CoordinatorTasks /></DesignPageShell><NewIssueDialog /></div></DesignProposalAccess>;
}
const meta = {
@@ -21,11 +22,11 @@ const meta = {
parameters: {
layout: "fullscreen", initialEntries: ["/PAP/issues"], designProposalTheme: false,
paperclip: {
capabilities: ["tasks/organize", "navigation-preferences/navigation", "inbox-search/decisions"],
capabilities: ["tasks/organize", "tasks/create", "navigation-preferences/navigation", "inbox-search/decisions"],
status: "proposed", home: "design-proposals",
exception: "Coordinator experimental workspace in an isolated worktree, awaiting review.",
implementation: "production", boundary: "fixtures",
notes: "Mounts the actual CoordinatorSidebar and CoordinatorTasks with the shared proposed task table, toolbar and sidebar styles. Keeps Chat, Tasks, Needs You, More and Recents. Needs attention uses the canonical pending-decision feed and hydrates its task IDs; it does not infer attention from task status. DesignPageShell supplies fixture-only desktop chrome; the real Layout owns responsive shell and account placement. API results are synthetic and writes are unsupported; task-detail, New Task, agent execution and cross-surface resolution require the running app. No global proposal theme is enabled, matching the narrowly scoped production opt-in.",
notes: "Mounts the actual CoordinatorSidebar, CoordinatorTasks and NewIssueDialog with the shared proposed task table, toolbar and sidebar styles. New task above Chat opens the task form. Keeps Tasks, Needs You, More and Recents. Needs attention uses the canonical pending-decision feed and hydrates its task IDs; it does not infer attention from task status. DesignPageShell supplies fixture-only desktop chrome; the real Layout owns responsive shell and account placement. API results are synthetic and writes are unsupported; task submission, task-detail, agent execution and cross-surface resolution require the running app. No global proposal theme is enabled, matching the narrowly scoped production opt-in.",
},
},
beforeEach: () => prepareFixtureApi([