diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 0b094674..d8dd3cd7 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -34,6 +34,7 @@ export type IssueViewState = { statuses: string[]; priorities: string[]; assignees: string[]; + labels: string[]; sortField: "status" | "priority" | "title" | "created" | "updated"; sortDir: "asc" | "desc"; groupBy: "status" | "priority" | "assignee" | "none"; @@ -44,6 +45,7 @@ const defaultViewState: IssueViewState = { statuses: ["todo", "in_progress", "in_review", "blocked"], priorities: [], assignees: [], + labels: [], sortField: "status", sortDir: "asc", groupBy: "status", @@ -85,6 +87,7 @@ function applyFilters(issues: Issue[], state: IssueViewState): Issue[] { if (state.statuses.length > 0) result = result.filter((i) => state.statuses.includes(i.status)); if (state.priorities.length > 0) result = result.filter((i) => state.priorities.includes(i.priority)); if (state.assignees.length > 0) result = result.filter((i) => i.assigneeAgentId != null && state.assignees.includes(i.assigneeAgentId)); + if (state.labels.length > 0) result = result.filter((i) => (i.labelIds ?? []).some((id) => state.labels.includes(id))); return result; } @@ -115,6 +118,7 @@ function countActiveFilters(state: IssueViewState): number { if (state.statuses.length > 0) count++; if (state.priorities.length > 0) count++; if (state.assignees.length > 0) count++; + if (state.labels.length > 0) count++; return count; } @@ -148,6 +152,7 @@ export function IssuesList({ initialAssignees, onUpdateIssue, }: IssuesListProps) { + const { selectedCompanyId } = useCompany(); const { openNewIssue } = useDialog(); const [viewState, setViewState] = useState(() => { @@ -174,6 +179,12 @@ export function IssuesList({ return sortIssues(applyFilters(issues, viewState), viewState); }, [issues, viewState]); + const { data: labels } = useQuery({ + queryKey: queryKeys.issues.labels(selectedCompanyId!), + queryFn: () => issuesApi.listLabels(selectedCompanyId!), + enabled: !!selectedCompanyId, + }); + const activeFilterCount = countActiveFilters(viewState); const groupedContent = useMemo(() => { @@ -254,7 +265,7 @@ export function IssuesList({ className="h-3 w-3 ml-1 hidden sm:block" onClick={(e) => { e.stopPropagation(); - updateView({ statuses: [], priorities: [], assignees: [] }); + updateView({ statuses: [], priorities: [], assignees: [], labels: [] }); }} /> )} @@ -267,7 +278,7 @@ export function IssuesList({ {activeFilterCount > 0 && ( @@ -354,6 +365,24 @@ export function IssuesList({ )} + + {labels && labels.length > 0 && ( +
+ Labels +
+ {labels.map((label) => ( + + ))} +
+
+ )} @@ -494,10 +523,30 @@ export function IssuesList({ onChange={(s) => onUpdateIssue(issue.id, { status: s })} /> - + {issue.identifier ?? issue.id.slice(0, 8)} {issue.title} + {(issue.labels ?? []).length > 0 && ( +
+ {(issue.labels ?? []).slice(0, 3).map((label) => ( + + {label.name} + + ))} + {(issue.labels ?? []).length > 3 && ( + +{(issue.labels ?? []).length - 3} + )} +
+ )}
{liveIssueIds?.has(issue.id) && ( diff --git a/ui/src/components/MetricCard.tsx b/ui/src/components/MetricCard.tsx index 913cc926..d53b286f 100644 --- a/ui/src/components/MetricCard.tsx +++ b/ui/src/components/MetricCard.tsx @@ -23,11 +23,11 @@ export function MetricCard({ icon: Icon, value, label, description, to, onClick

{value}

-

+

{label}

{description && ( -
{description}
+
{description}
)}
diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 123802cd..92256e30 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -33,6 +33,7 @@ import { Calendar, } from "lucide-react"; import { cn } from "../lib/utils"; +import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors"; import { MarkdownEditor, type MarkdownEditorRef } from "./MarkdownEditor"; import { AgentIcon } from "./AgentIconPicker"; import type { Project, Agent } from "@paperclip/shared"; @@ -68,18 +69,18 @@ function clearDraft() { } const statuses = [ - { value: "backlog", label: "Backlog", color: "text-muted-foreground" }, - { value: "todo", label: "Todo", color: "text-blue-400" }, - { value: "in_progress", label: "In Progress", color: "text-yellow-400" }, - { value: "in_review", label: "In Review", color: "text-violet-400" }, - { value: "done", label: "Done", color: "text-green-400" }, + { value: "backlog", label: "Backlog", color: issueStatusText.backlog ?? issueStatusTextDefault }, + { value: "todo", label: "Todo", color: issueStatusText.todo ?? issueStatusTextDefault }, + { value: "in_progress", label: "In Progress", color: issueStatusText.in_progress ?? issueStatusTextDefault }, + { value: "in_review", label: "In Review", color: issueStatusText.in_review ?? issueStatusTextDefault }, + { value: "done", label: "Done", color: issueStatusText.done ?? issueStatusTextDefault }, ]; const priorities = [ - { value: "critical", label: "Critical", icon: AlertTriangle, color: "text-red-400" }, - { value: "high", label: "High", icon: ArrowUp, color: "text-orange-400" }, - { value: "medium", label: "Medium", icon: Minus, color: "text-yellow-400" }, - { value: "low", label: "Low", icon: ArrowDown, color: "text-blue-400" }, + { value: "critical", label: "Critical", icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault }, + { value: "high", label: "High", icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault }, + { value: "medium", label: "Medium", icon: Minus, color: priorityColor.medium ?? priorityColorDefault }, + { value: "low", label: "Low", icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault }, ]; export function NewIssueDialog() { diff --git a/ui/src/components/PriorityIcon.tsx b/ui/src/components/PriorityIcon.tsx index d63eeaf0..fe5e7ce8 100644 --- a/ui/src/components/PriorityIcon.tsx +++ b/ui/src/components/PriorityIcon.tsx @@ -1,14 +1,15 @@ import { useState } from "react"; import { ArrowUp, ArrowDown, Minus, AlertTriangle } from "lucide-react"; import { cn } from "../lib/utils"; +import { priorityColor, priorityColorDefault } from "../lib/status-colors"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; const priorityConfig: Record = { - critical: { icon: AlertTriangle, color: "text-red-400", label: "Critical" }, - high: { icon: ArrowUp, color: "text-orange-400", label: "High" }, - medium: { icon: Minus, color: "text-yellow-400", label: "Medium" }, - low: { icon: ArrowDown, color: "text-blue-400", label: "Low" }, + critical: { icon: AlertTriangle, color: priorityColor.critical ?? priorityColorDefault, label: "Critical" }, + high: { icon: ArrowUp, color: priorityColor.high ?? priorityColorDefault, label: "High" }, + medium: { icon: Minus, color: priorityColor.medium ?? priorityColorDefault, label: "Medium" }, + low: { icon: ArrowDown, color: priorityColor.low ?? priorityColorDefault, label: "Low" }, }; const allPriorities = ["critical", "high", "medium", "low"]; diff --git a/ui/src/components/StatusBadge.tsx b/ui/src/components/StatusBadge.tsx index 702a8e16..1fb15612 100644 --- a/ui/src/components/StatusBadge.tsx +++ b/ui/src/components/StatusBadge.tsx @@ -1,39 +1,12 @@ import { cn } from "../lib/utils"; - -const statusColors: Record = { - active: "bg-green-900/50 text-green-300", - running: "bg-cyan-900/50 text-cyan-300", - paused: "bg-orange-900/50 text-orange-300", - idle: "bg-yellow-900/50 text-yellow-300", - archived: "bg-neutral-800 text-neutral-400", - planned: "bg-neutral-800 text-neutral-400", - achieved: "bg-green-900/50 text-green-300", - completed: "bg-green-900/50 text-green-300", - failed: "bg-red-900/50 text-red-300", - timed_out: "bg-orange-900/50 text-orange-300", - succeeded: "bg-green-900/50 text-green-300", - error: "bg-red-900/50 text-red-300", - pending_approval: "bg-amber-900/50 text-amber-300", - backlog: "bg-neutral-800 text-neutral-400", - todo: "bg-blue-900/50 text-blue-300", - in_progress: "bg-indigo-900/50 text-indigo-300", - in_review: "bg-violet-900/50 text-violet-300", - blocked: "bg-amber-900/50 text-amber-300", - done: "bg-green-900/50 text-green-300", - terminated: "bg-red-900/50 text-red-300", - cancelled: "bg-neutral-800 text-neutral-500", - pending: "bg-yellow-900/50 text-yellow-300", - revision_requested: "bg-amber-900/50 text-amber-300", - approved: "bg-green-900/50 text-green-300", - rejected: "bg-red-900/50 text-red-300", -}; +import { statusBadge, statusBadgeDefault } from "../lib/status-colors"; export function StatusBadge({ status }: { status: string }) { return ( {status.replace("_", " ")} diff --git a/ui/src/components/StatusIcon.tsx b/ui/src/components/StatusIcon.tsx index 3467ecf6..bafbb35b 100644 --- a/ui/src/components/StatusIcon.tsx +++ b/ui/src/components/StatusIcon.tsx @@ -1,18 +1,9 @@ import { useState } from "react"; import { cn } from "../lib/utils"; +import { issueStatusIcon, issueStatusIconDefault } from "../lib/status-colors"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; -const statusColors: Record = { - backlog: "text-muted-foreground border-muted-foreground", - todo: "text-blue-400 border-blue-400", - in_progress: "text-yellow-400 border-yellow-400", - in_review: "text-violet-400 border-violet-400", - done: "text-green-400 border-green-400", - cancelled: "text-neutral-500 border-neutral-500", - blocked: "text-red-400 border-red-400", -}; - const allStatuses = ["backlog", "todo", "in_progress", "in_review", "done", "cancelled", "blocked"]; function statusLabel(status: string): string { @@ -28,7 +19,7 @@ interface StatusIconProps { export function StatusIcon({ status, onChange, className, showLabel }: StatusIconProps) { const [open, setOpen] = useState(false); - const colorClass = statusColors[status] ?? "text-muted-foreground border-muted-foreground"; + const colorClass = issueStatusIcon[status] ?? issueStatusIconDefault; const isDone = status === "done"; const circle = ( diff --git a/ui/src/lib/status-colors.ts b/ui/src/lib/status-colors.ts new file mode 100644 index 00000000..2f3b9c75 --- /dev/null +++ b/ui/src/lib/status-colors.ts @@ -0,0 +1,108 @@ +/** + * Canonical status & priority color definitions. + * + * Every component that renders a status indicator (StatusIcon, StatusBadge, + * agent status dots, etc.) should import from here so colors stay consistent. + */ + +// --------------------------------------------------------------------------- +// Issue status colors +// --------------------------------------------------------------------------- + +/** StatusIcon circle: text + border classes */ +export const issueStatusIcon: Record = { + backlog: "text-muted-foreground border-muted-foreground", + todo: "text-blue-400 border-blue-400", + in_progress: "text-yellow-400 border-yellow-400", + in_review: "text-violet-400 border-violet-400", + done: "text-green-400 border-green-400", + cancelled: "text-neutral-500 border-neutral-500", + blocked: "text-red-400 border-red-400", +}; + +export const issueStatusIconDefault = "text-muted-foreground border-muted-foreground"; + +/** Text-only color for issue statuses (dropdowns, labels) */ +export const issueStatusText: Record = { + backlog: "text-muted-foreground", + todo: "text-blue-400", + in_progress: "text-yellow-400", + in_review: "text-violet-400", + done: "text-green-400", + cancelled: "text-neutral-500", + blocked: "text-red-400", +}; + +export const issueStatusTextDefault = "text-muted-foreground"; + +// --------------------------------------------------------------------------- +// Badge colors — used by StatusBadge for all entity types +// --------------------------------------------------------------------------- + +export const statusBadge: Record = { + // Agent statuses + active: "bg-green-900/50 text-green-300", + running: "bg-cyan-900/50 text-cyan-300", + paused: "bg-orange-900/50 text-orange-300", + idle: "bg-yellow-900/50 text-yellow-300", + archived: "bg-neutral-800 text-neutral-400", + + // Goal statuses + planned: "bg-neutral-800 text-neutral-400", + achieved: "bg-green-900/50 text-green-300", + completed: "bg-green-900/50 text-green-300", + + // Run statuses + failed: "bg-red-900/50 text-red-300", + timed_out: "bg-orange-900/50 text-orange-300", + succeeded: "bg-green-900/50 text-green-300", + error: "bg-red-900/50 text-red-300", + terminated: "bg-red-900/50 text-red-300", + pending: "bg-yellow-900/50 text-yellow-300", + + // Approval statuses + pending_approval: "bg-amber-900/50 text-amber-300", + revision_requested: "bg-amber-900/50 text-amber-300", + approved: "bg-green-900/50 text-green-300", + rejected: "bg-red-900/50 text-red-300", + + // Issue statuses — consistent hues with issueStatusIcon above + backlog: "bg-neutral-800 text-neutral-400", + todo: "bg-blue-900/50 text-blue-300", + in_progress: "bg-yellow-900/50 text-yellow-300", + in_review: "bg-violet-900/50 text-violet-300", + blocked: "bg-red-900/50 text-red-300", + done: "bg-green-900/50 text-green-300", + cancelled: "bg-neutral-800 text-neutral-500", +}; + +export const statusBadgeDefault = "bg-neutral-800 text-neutral-400"; + +// --------------------------------------------------------------------------- +// Agent status dot — solid background for small indicator dots +// --------------------------------------------------------------------------- + +export const agentStatusDot: Record = { + running: "bg-cyan-400 animate-pulse", + active: "bg-green-400", + paused: "bg-yellow-400", + idle: "bg-yellow-400", + pending_approval: "bg-amber-400", + error: "bg-red-400", + archived: "bg-neutral-400", +}; + +export const agentStatusDotDefault = "bg-neutral-400"; + +// --------------------------------------------------------------------------- +// Priority colors +// --------------------------------------------------------------------------- + +export const priorityColor: Record = { + critical: "text-red-400", + high: "text-orange-400", + medium: "text-yellow-400", + low: "text-blue-400", +}; + +export const priorityColorDefault = "text-yellow-400"; diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index b16b56e9..90cf2d99 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -16,6 +16,7 @@ import { adapterLabels, roleLabels } from "../components/agent-config-primitives import { getUIAdapter, buildTranscript } from "../adapters"; import type { TranscriptEntry } from "../adapters"; import { StatusBadge } from "../components/StatusBadge"; +import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors"; import { MarkdownBody } from "../components/MarkdownBody"; import { CopyText } from "../components/CopyText"; import { EntityRow } from "../components/EntityRow"; @@ -1103,15 +1104,7 @@ function ConfigSummary({ className="flex items-center gap-2 text-sm text-blue-400 hover:underline" > - + {r.name} ({roleLabels[r.role] ?? r.role}) diff --git a/ui/src/pages/Agents.tsx b/ui/src/pages/Agents.tsx index 492a77c5..410330df 100644 --- a/ui/src/pages/Agents.tsx +++ b/ui/src/pages/Agents.tsx @@ -9,6 +9,7 @@ import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useSidebar } from "../context/SidebarContext"; import { queryKeys } from "../lib/queryKeys"; import { StatusBadge } from "../components/StatusBadge"; +import { agentStatusDot, agentStatusDotDefault } from "../lib/status-colors"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; import { relativeTime, cn } from "../lib/utils"; @@ -227,19 +228,7 @@ export function Agents() { leading={ } @@ -325,18 +314,7 @@ function OrgTreeNode({ }) { const agent = agentMap.get(node.id); - const statusColor = - node.status === "running" - ? "bg-cyan-400 animate-pulse" - : node.status === "active" - ? "bg-green-400" - : node.status === "paused" - ? "bg-yellow-400" - : node.status === "pending_approval" - ? "bg-amber-400" - : node.status === "error" - ? "bg-red-400" - : "bg-neutral-400"; + const statusColor = agentStatusDot[node.status] ?? agentStatusDotDefault; return (
diff --git a/ui/src/pages/DesignGuide.tsx b/ui/src/pages/DesignGuide.tsx index 2df81b96..420ed2f1 100644 --- a/ui/src/pages/DesignGuide.tsx +++ b/ui/src/pages/DesignGuide.tsx @@ -69,6 +69,11 @@ import { DropdownMenuCheckboxItem, DropdownMenuShortcut, } from "@/components/ui/dropdown-menu"; +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover"; import { Sheet, SheetTrigger, @@ -110,6 +115,7 @@ import { import { StatusBadge } from "@/components/StatusBadge"; import { StatusIcon } from "@/components/StatusIcon"; import { PriorityIcon } from "@/components/PriorityIcon"; +import { agentStatusDot, agentStatusDotDefault } from "@/lib/status-colors"; import { EntityRow } from "@/components/EntityRow"; import { EmptyState } from "@/components/EmptyState"; import { MetricCard } from "@/components/MetricCard"; @@ -287,8 +293,8 @@ export function DesignGuide() {

Tiny label — text-xs text-muted-foreground

-

- Mono identifier — text-xs font-mono text-muted-foreground +

+ Mono identifier — text-sm font-mono text-muted-foreground

Large stat — text-2xl font-bold

Log/code text — font-mono text-xs

@@ -435,16 +441,10 @@ export function DesignGuide() {
- {[ - ["running", "bg-cyan-400 animate-pulse"], - ["active", "bg-green-400"], - ["paused", "bg-yellow-400"], - ["error", "bg-red-400"], - ["offline", "bg-neutral-400"], - ].map(([label, color]) => ( + {(["running", "active", "paused", "error", "archived"] as const).map((label) => (
- + {label}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 8f10468d..b04122ef 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -412,7 +412,7 @@ export function IssueDetail() { priority={issue.priority} onChange={(priority) => updateIssue.mutate({ priority })} /> - {issue.identifier ?? issue.id.slice(0, 8)} + {issue.identifier ?? issue.id.slice(0, 8)} {issue.projectId ? (