diff --git a/packages/shared/src/types/attention.ts b/packages/shared/src/types/attention.ts index c9c505a24b2..6e01956439c 100644 --- a/packages/shared/src/types/attention.ts +++ b/packages/shared/src/types/attention.ts @@ -45,6 +45,8 @@ export interface AttentionProjectRef { id: string; name: string; urlKey: string; + color: string | null; + icon: string | null; } export interface AttentionWorkspaceRef { diff --git a/server/src/__tests__/attention-service.test.ts b/server/src/__tests__/attention-service.test.ts index 32d8352e07f..1c1454c905e 100644 --- a/server/src/__tests__/attention-service.test.ts +++ b/server/src/__tests__/attention-service.test.ts @@ -622,6 +622,8 @@ describeEmbeddedPostgres("attention service", () => { companyId, name: "Attention Project", status: "in_progress", + color: "#0f766e", + icon: "rocket", }); await db.insert(projectWorkspaces).values({ id: workspaceId, @@ -752,7 +754,12 @@ describeEmbeddedPostgres("attention service", () => { const planItem = detailsByKind.get("plan_approval"); expect(planItem?.subject.title).toBe("Plan approval - Approve launch plan"); expect(planItem?.subject.metadata).toMatchObject({ isPlanTarget: true, targetDocumentKey: "plan" }); - expect(planItem?.project).toMatchObject({ id: projectId, name: "Attention Project" }); + expect(planItem?.project).toMatchObject({ + id: projectId, + name: "Attention Project", + color: "#0f766e", + icon: "rocket", + }); expect(planItem?.project?.urlKey).toEqual(expect.any(String)); expect(planItem?.workspace).toEqual({ id: workspaceId, name: "Preview workspace" }); expect(planItem?.detail).toMatchObject({ diff --git a/server/src/services/attention.ts b/server/src/services/attention.ts index 618cb7d4180..0dd562697ba 100644 --- a/server/src/services/attention.ts +++ b/server/src/services/attention.ts @@ -438,6 +438,8 @@ async function issueSummaryMap(db: Db, companyId: string, issueIds: Array { expect(links.some((a) => a.textContent?.includes("Hire agent: Research Analyst"))).toBe(false); }); + it("renders project identity once without a filter button", () => { + render( + , + ); + + const projectChip = container?.querySelector('[data-testid="attention-project-chip"]'); + expect(projectChip?.textContent).toBe("Alpha"); + expect(projectChip?.querySelector("button")).toBeNull(); + expect(container?.querySelector('button[title="Filter by Alpha"]')).toBeNull(); + expect(container?.textContent?.match(/Alpha/g)).toHaveLength(1); + }); + it("renders collapsed inline decision verbs in the right-side action area with semantic variants", () => { render( void; /** Restore a snoozed/dismissed row (curtain variant only). */ onRestore?: (item: AttentionItem) => void; - /** Click-to-filter on the project chip (wires into the toolbar filter state). */ - onFilterProject?: (project: AttentionProjectRef) => void; /** "active" renders the live queue row; "hidden" renders a curtain row. */ variant?: "active" | "hidden"; agentMap?: Map; @@ -87,7 +86,6 @@ export function AttentionQueueRow({ onDismiss, onSnooze, onRestore, - onFilterProject, variant = "active", agentMap, currentUserId, @@ -214,20 +212,7 @@ export function AttentionQueueRow({ {(item.project || workspaceLabel) && (
- {item.project && ( - { - e.stopPropagation(); - onFilterProject(item.project!); - } - : undefined - } - /> - )} + {item.project && } {workspaceLabel && }
)} @@ -332,34 +317,34 @@ function decisionVerbVariant(verb: AttentionItem["decisionVerbs"][number]): "def return "outline"; } -/** Small pill used for the project / workspace chips. Clickable when `onClick`. */ +/** Small passive pill used for the project identity. */ +function ProjectChip({ project }: { project: NonNullable }) { + return ( + + + {project.name} + + ); +} + +/** Small passive pill used for workspace context. */ function Chip({ icon: Icon, label, - onClick, }: { - icon: typeof Folder; + icon: LucideIcon; label: string; - onClick?: (e: MouseEvent) => void; }) { - const className = cn( - "inline-flex max-w-(--sz-12rem) items-center gap-1 rounded-sm border border-border/70 bg-muted/40 px-1.5 py-0.5 text-(length:--text-nano) text-muted-foreground", - onClick && "transition-colors hover:border-border hover:bg-accent hover:text-foreground", - ); - const content = ( - <> + return ( + {label} - + ); - if (onClick) { - return ( - - ); - } - return {content}; } /** Square screenshot thumbnails at the right of the description (plan §10). */ diff --git a/ui/src/lib/attention.test.ts b/ui/src/lib/attention.test.ts index d5ea99039e9..a5f5db5c484 100644 --- a/ui/src/lib/attention.test.ts +++ b/ui/src/lib/attention.test.ts @@ -312,7 +312,7 @@ describe("groupAttentionItems", () => { it("groups by project, keeping a 'No project' bucket for unassigned rows", () => { const items = [ - buildItem({ id: "p1", activityAt: "2026-07-10T10:00:00Z", project: { id: "proj-1", name: "Alpha", urlKey: "alpha" } }), + buildItem({ id: "p1", activityAt: "2026-07-10T10:00:00Z", project: { id: "proj-1", name: "Alpha", urlKey: "alpha", color: null, icon: null } }), buildItem({ id: "none", activityAt: "2026-07-10T11:00:00Z", project: null }), ]; const groups = groupAttentionItems(items, "project"); @@ -350,7 +350,7 @@ describe("groupAttentionItems", () => { }); describe("filterAttentionItems", () => { - const approval = buildItem({ id: "ap", sourceKind: "approval", severity: "high", project: { id: "p1", name: "Alpha", urlKey: "a" } }); + const approval = buildItem({ id: "ap", sourceKind: "approval", severity: "high", project: { id: "p1", name: "Alpha", urlKey: "a", color: null, icon: null } }); const join = buildItem({ id: "jn", sourceKind: "join_request", severity: "low", project: null }); const items = [approval, join]; @@ -389,7 +389,7 @@ describe("filterAttentionItems", () => { describe("buildAttentionFilterOptions", () => { it("collects the distinct dimensions present in the feed", () => { const items = [ - buildItem({ sourceKind: "approval", severity: "high", project: { id: "p1", name: "Alpha", urlKey: "a" }, workspace: { id: "w1", name: "WS" } }), + buildItem({ sourceKind: "approval", severity: "high", project: { id: "p1", name: "Alpha", urlKey: "a", color: null, icon: null }, workspace: { id: "w1", name: "WS" } }), buildItem({ sourceKind: "join_request", severity: "low", project: null, workspace: null }), ]; const options = buildAttentionFilterOptions(items); diff --git a/ui/src/pages/WhatNeedsMe.tsx b/ui/src/pages/WhatNeedsMe.tsx index 4e5fdd369eb..84b574fc1d7 100644 --- a/ui/src/pages/WhatNeedsMe.tsx +++ b/ui/src/pages/WhatNeedsMe.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useQuery } from "@tanstack/react-query"; import { ArrowUpDown, Check, CheckCircle2, Inbox, Layers, ListFilter } from "lucide-react"; -import type { Agent, AttentionItem, AttentionProjectRef } from "@paperclipai/shared"; +import type { Agent, AttentionItem } from "@paperclipai/shared"; import { attentionApi } from "../api/attention"; import { agentsApi } from "../api/agents"; import { authApi } from "../api/auth"; @@ -217,11 +217,6 @@ export function WhatNeedsMe() { setPendingRestore((prev) => new Set(prev).add(item.id)); restore(item.dismissalKey); }; - const handleFilterProject = (project: AttentionProjectRef) => { - if (filters.projectIds.includes(project.id)) return; - updateFilters({ ...filters, projectIds: [...filters.projectIds, project.id] }); - }; - const activeFilterCount = countActiveAttentionFilters(filters); if (!selectedCompanyId) { @@ -371,7 +366,6 @@ export function WhatNeedsMe() { onToggleExpand={() => setExpandedId((prev) => (prev === item.id ? null : item.id))} onDismiss={handleDismiss} onSnooze={handleSnooze} - onFilterProject={handleFilterProject} agentMap={agentMap} currentUserId={currentUserId} /> diff --git a/ui/storybook/stories/what-needs-me.stories.tsx b/ui/storybook/stories/what-needs-me.stories.tsx index 5fc41fde7f0..37246117218 100644 --- a/ui/storybook/stories/what-needs-me.stories.tsx +++ b/ui/storybook/stories/what-needs-me.stories.tsx @@ -188,9 +188,9 @@ const ACTIVITY_OFFSETS: Record = { "budget-1": NOW - 40 * DAY, }; const PROJECTS: Record = { - "appr-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, - "intx-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, - "review-1": { id: "proj-beta", name: "Beta", urlKey: "beta" }, + "appr-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha", color: "#0f766e", icon: "rocket" }, + "intx-1": { id: "proj-alpha", name: "Alpha", urlKey: "alpha", color: "#0f766e", icon: "rocket" }, + "review-1": { id: "proj-beta", name: "Beta", urlKey: "beta", color: "#7c3aed", icon: "layers" }, }; const DETAILS: Record = { "recov-1": { kind: "generic", summaryExcerpt: "Agent has not produced output in 40 minutes.", images: [] }, @@ -224,7 +224,7 @@ const SHOWCASE: AttentionItem[] = [ { id: "approve", label: "Approve plan", description: null }, { id: "request_changes", label: "Request changes", description: null }, ], - project: { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, + project: { id: "proj-alpha", name: "Alpha", urlKey: "alpha", color: "#0f766e", icon: "rocket" }, }), activityAt: new Date(NOW - 20 * 60 * 1000).toISOString(), detail: { kind: "plan_approval", issueTitle: "Attention home", planTitle: "Row/card redesign — 8 sections", summaryExcerpt: null, images: [IMAGES[1]] }, @@ -238,7 +238,7 @@ const SHOWCASE: AttentionItem[] = [ { id: "approve", label: "Approve", description: null }, { id: "reject", label: "Reject", description: null }, ], - project: { id: "proj-beta", name: "Beta", urlKey: "beta" }, + project: { id: "proj-beta", name: "Beta", urlKey: "beta", color: "#7c3aed", icon: "layers" }, }), activityAt: new Date(NOW - 40 * 60 * 1000).toISOString(), detail: { kind: "confirmation", promptExcerpt: "Ship v2026.707.0 changelog to the public page?", isPlanTarget: false, images: [] }, @@ -248,7 +248,7 @@ const SHOWCASE: AttentionItem[] = [ inlineResolvable: true, subject: { kind: "interaction", id: "intx-qs", companyId, title: "Answer 2 questions on rollout", identifier: null, status: "pending", href: "/PAP/issues/PAP-1000#qs", metadata: { kind: "ask_user_questions", issueId: "issue-1000" } }, decisionVerbs: [{ id: "respond", label: "Answer", description: null }], - project: { id: "proj-alpha", name: "Alpha", urlKey: "alpha" }, + project: PROJECTS["intx-1"], }), activityAt: new Date(NOW - 90 * 60 * 1000).toISOString(), detail: { kind: "questions", questionCount: 2, firstQuestionText: "Which auth provider should we standardize on?", images: [IMAGES[0], IMAGES[2]] }, @@ -383,7 +383,6 @@ function Queue({ onToggleExpand={() => setExpandedId((p) => (p === it.id ? null : it.id))} onDismiss={(d) => setCleared((prev) => new Set(prev).add(d.id))} onSnooze={(d) => setCleared((prev) => new Set(prev).add(d.id))} - onFilterProject={() => {}} /> ))}