diff --git a/AgentCompany-New/src/renderer/src/overlays.css b/AgentCompany-New/src/renderer/src/overlays.css index b3cdbcdb..a1ffe4e6 100644 --- a/AgentCompany-New/src/renderer/src/overlays.css +++ b/AgentCompany-New/src/renderer/src/overlays.css @@ -72,6 +72,51 @@ .option-card .opt-title { font-size: 13.5px; font-weight: 540; } .option-card .opt-sub { font-size: 12px; color: var(--text-3); margin-top: 4px; } +.popover-anchor { position: relative; display: inline-flex; } +.popover-panel { + position: absolute; + top: calc(100% + 6px); + z-index: 35; + min-width: 208px; + max-width: 280px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--r-md); + box-shadow: var(--shadow-md); + padding: 6px; + display: flex; + flex-direction: column; + gap: 1px; +} +.popover-panel.end { right: 0; } +.popover-panel.start { left: 0; } +.menu-label { + padding: 6px 8px 4px; + font-size: 11px; + font-weight: 540; + letter-spacing: 0.02em; + color: var(--text-3); +} +.menu-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border: 0; + border-radius: var(--r-sm); + background: transparent; + color: var(--text-2); + font-size: 13px; + text-align: left; + width: 100%; +} +.menu-row:hover { background: var(--hover); color: var(--text-1); } +.menu-row.active { background: var(--panel-3); color: var(--text-1); font-weight: 540; } +.menu-row.danger { color: var(--red); } +.menu-row.danger:hover { background: rgba(210, 59, 59, 0.1); color: var(--red); } +.menu-row .menu-check { margin-left: auto; color: var(--accent); display: inline-flex; } +.menu-divider { height: 1px; background: var(--border-soft); margin: 4px 0; } + ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; } ::-webkit-scrollbar-track { background: transparent; } diff --git a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx index 62329b69..54803b06 100644 --- a/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/WorkPage.tsx @@ -1,12 +1,15 @@ import { useMemo, useState } from "react"; import { Avatar, Button, Icon, IconButton, Pill, Segmented } from "../shared/ui"; +import { MenuDivider, MenuLabel, MenuRow, Popover } from "../shared/Popover"; import { TopbarActions } from "../shared/TopbarActions"; import { + TASK_PRIORITY, TASK_STATUS, type AgentRecord, type GoalRecord, type ProviderRecord, type ProjectRecord, + type TaskPriority, type TaskRecord, type TaskRunRecord, type TaskStatus, @@ -35,8 +38,23 @@ export function WorkPage({ snapshot, actions }: PageProps) { const orgId = activeOrgId(snapshot); const [view, setView] = useState("board"); const [newTaskOpen, setNewTaskOpen] = useState(false); + const [priorityFilter, setPriorityFilter] = useState("all"); + const [assigneeFilter, setAssigneeFilter] = useState("all"); - const tasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]); + const allTasks = useMemo(() => snapshot.tasks.filter((task) => task.orgId === orgId), [snapshot.tasks, orgId]); + const tasks = useMemo( + () => + allTasks.filter((task) => { + if (priorityFilter !== "all" && task.priority !== priorityFilter) return false; + if (assigneeFilter !== "all" && task.assigneeAgentId !== assigneeFilter) return false; + return true; + }), + [allTasks, priorityFilter, assigneeFilter], + ); + const assigneeCount = useMemo( + () => new Set(tasks.map((task) => task.assigneeAgentId).filter((id): id is string => id !== null)).size, + [tasks], + ); const agents = useMemo(() => snapshot.agents.filter((agent) => agent.orgId === orgId), [snapshot.agents, orgId]); const projects = useMemo(() => snapshot.projects.filter((project) => project.orgId === orgId), [snapshot.projects, orgId]); const goals = useMemo(() => snapshot.goals.filter((goal) => goal.orgId === orgId), [snapshot.goals, orgId]); @@ -57,12 +75,18 @@ export function WorkPage({ snapshot, actions }: PageProps) {
- +

Work

- {tasks.length} {tasks.length === 1 ? "task" : "tasks"} · {agents.length} {agents.length === 1 ? "assignee" : "assignees"} + {tasks.length} {tasks.length === 1 ? "task" : "tasks"} · {assigneeCount} {assigneeCount === 1 ? "assignee" : "assignees"}

{view === "board" && ( @@ -87,6 +111,61 @@ export function WorkPage({ snapshot, actions }: PageProps) { ); } +function WorkFilter({ + agents, + priority, + assignee, + onPriority, + onAssignee, +}: { + agents: AgentRecord[]; + priority: TaskPriority | "all"; + assignee: string; + onPriority: (next: TaskPriority | "all") => void; + onAssignee: (next: string) => void; +}) { + const active = priority !== "all" || assignee !== "all"; + const priorities: ReadonlyArray<{ id: TaskPriority | "all"; label: string }> = [ + { id: "all", label: "All priorities" }, + { id: TASK_PRIORITY.HIGH, label: "High" }, + { id: TASK_PRIORITY.NORMAL, label: "Normal" }, + { id: TASK_PRIORITY.LOW, label: "Low" }, + ]; + return ( + ( + + )} + > + {() => ( + <> + Priority + {priorities.map((option) => ( + onPriority(option.id)}> + {option.label} + {priority === option.id && } + + ))} + + Assignee + onAssignee("all")}> + All assignees + {assignee === "all" && } + + {agents.map((agent) => ( + onAssignee(agent.id)}> + {agent.name} + {assignee === agent.id && } + + ))} + + )} + + ); +} + function BoardView({ tasks, agents, diff --git a/AgentCompany-New/src/renderer/src/shared/Popover.tsx b/AgentCompany-New/src/renderer/src/shared/Popover.tsx new file mode 100644 index 00000000..27dba521 --- /dev/null +++ b/AgentCompany-New/src/renderer/src/shared/Popover.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState, type ReactNode } from "react"; + +/** Anchored popover used for filter and overflow menus. Closes on outside + * click or Escape so triggers across the app stay functional and consistent. */ +export function Popover({ + align = "end", + trigger, + children, +}: { + align?: "start" | "end"; + trigger: (state: { open: boolean; toggle: () => void }) => ReactNode; + children: (close: () => void) => ReactNode; +}) { + const [open, setOpen] = useState(false); + const anchorRef = useRef(null); + + useEffect(() => { + if (!open) return; + const onPointerDown = (event: MouseEvent) => { + if (anchorRef.current && !anchorRef.current.contains(event.target as Node)) setOpen(false); + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setOpen(false); + }; + document.addEventListener("mousedown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("mousedown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [open]); + + return ( +
+ {trigger({ open, toggle: () => setOpen((value) => !value) })} + {open && ( +
+ {children(() => setOpen(false))} +
+ )} +
+ ); +} + +export function MenuLabel({ children }: { children: ReactNode }) { + return
{children}
; +} + +export function MenuRow({ + active = false, + danger = false, + onClick, + children, +}: { + active?: boolean; + danger?: boolean; + onClick: () => void; + children: ReactNode; +}) { + return ( + + ); +} + +export function MenuDivider() { + return
; +}