From d84c5eae7a2b658b31424455088a1b8d549fc585 Mon Sep 17 00:00:00 2001 From: scotttong Date: Thu, 6 Aug 2026 16:19:22 -0700 Subject: [PATCH] feat(ui): hide task priority from the UI (keep data model) (#11024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Tasks and issues carry a `priority` field that renders across many product surfaces: the detail header, the Triage properties panel, Kanban and thread cards, the New Task composer, list Sort/Group/Filter menus, search filters, and the dashboard chart. > - Product feedback found the priority level adds visual noise and decision cost without clear value in day-to-day task flow. > - We want to remove priority from the interface, but keep the data model, API, validation, and search DSL fully intact so the choice is reversible with no migration. > - This pull request hides every priority indicator and control behind one compile-time flag, `SHOW_TASK_PRIORITY_UI`, set to `false`. > - The benefit is a calmer, simpler UI now, with a single-boolean revive path and zero data loss. ## Linked Issues or Issue Description **Subsystem affected** The web UI (`ui/src`): issue detail, properties panel, Kanban/thread cards, New Task dialog, issues list Sort/Group/Filter menus, search filter bar/sheet, dashboard charts, and the design-guide showcase. **Problem or motivation** The task/issue priority level appears across many surfaces and adds visual clutter and decision overhead without pulling its weight in normal task flow. We want it gone from the interface without discarding the underlying data or breaking anything that depends on it. **Proposed solution** Add a single compile-time UI flag, `SHOW_TASK_PRIORITY_UI` (default `false`), and gate every priority indicator and control behind it. Leave the data model, API params, Zod validation (including the `"medium"` default), and the search filter DSL untouched. Reviving priority is a one-line flip of the flag back to `true`. **Alternatives considered** Deleting the priority code and schema outright. Rejected: it is irreversible, needs a data migration, and throws away a field the API and search still support. A gated flag keeps the change reversible and low risk. **Roadmap alignment** UI simplification. This is a presentation-only change; it does not alter core agent or data behavior. ## What Changed - Added `ui/src/lib/ui-flags.ts` exporting `SHOW_TASK_PRIORITY_UI: boolean = false` (typed `boolean` so gated branches are not flagged as dead code). - Gated the priority row in the Triage properties panel and the editable priority control in the issue detail header (plus its skeleton seed). - Gated the per-card priority icon in `KanbanBoard` and in `IssueThreadInteractionCard`. - Hid the priority chip and the mobile "more" menu priority section in the New Task dialog. The submit path still sends the `"medium"` default. - Removed the Priority options from the issues list Sort and Group-by menus; the comparator and grouping logic stay dormant. - Hid the Priority sections in the issue filters popover and in the search filter bar and sheet. The `priority:` search DSL and filter state stay functional at the data layer. - Suppressed active-filter priority pills for consistency. - Gated the "Tasks by Priority" dashboard chart and the design-guide priority showcase subsection. - Left activity-feed "changed priority" history text intact as a historical record. - Updated call-site tests to assert priority UI is absent while the flag is off, added focused hidden-surface tests, and added a test that proves creating a task still persists `priority: "medium"`. ## Verification - `pnpm check:token-gates` — all 3 gates clean. - `pnpm --filter @paperclipai/ui typecheck` — clean. - `pnpm --filter @paperclipai/ui exec vitest run` on the touched surfaces (IssueProperties, IssueFiltersPopover, IssuesList, NewIssueDialog, IssueDetail, PriorityIcon and its interaction test) — all green under `TZ=UTC`. - Manual: with the flag off, priority does not appear in the detail header, Triage panel, New Task composer, Sort/Group/Filter menus, or the dashboard chart. Creating a task still persists `priority: "medium"`, and the `priority:` search token still filters at the data layer. ## Risks Low risk. The change is presentation-only and additive: no data model, API, validation, or search-DSL changes. The priority code paths remain compiled and tested; flipping `SHOW_TASK_PRIORITY_UI` to `true` restores the full UI. Visual snapshot baselines are intentionally not updated per the `doc/design/DECISION-SHEET.md` entry "Per-change snapshot verification demoted to dormant (Jul 13 2026)". ## Model Used Claude Opus 4.8 (Anthropic), model id `claude-opus-4-8`, extended thinking enabled, with tool use / code execution in an agentic coding harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip Co-authored-by: Claude Opus 4.8 --- .../components/IssueFiltersPopover.test.tsx | 25 ++++++++++ ui/src/components/IssueFiltersPopover.tsx | 4 ++ ui/src/components/IssueProperties.test.tsx | 19 ++++++++ .../components/IssueThreadInteractionCard.tsx | 4 +- ui/src/components/IssuesList.test.tsx | 48 +++++++++++++++++++ ui/src/components/IssuesList.tsx | 11 ++++- ui/src/components/KanbanBoard.tsx | 4 +- ui/src/components/NewIssueDialog.test.tsx | 39 ++++++++++++--- ui/src/components/NewIssueDialog.tsx | 8 +++- .../issue-properties/IssueProperties.tsx | 18 ++++--- ui/src/components/search/SearchFilterBar.tsx | 4 ++ .../components/search/SearchFilterSheet.tsx | 4 ++ ui/src/lib/search-filters.ts | 5 ++ ui/src/lib/ui-flags.ts | 22 +++++++++ ui/src/pages/AgentDetail.tsx | 10 ++-- ui/src/pages/Dashboard.tsx | 10 ++-- ui/src/pages/DesignGuide.tsx | 39 ++++++++++----- ui/src/pages/IssueDetail.test.tsx | 17 ++++--- ui/src/pages/IssueDetail.tsx | 15 ++++-- 19 files changed, 255 insertions(+), 51 deletions(-) create mode 100644 ui/src/lib/ui-flags.ts diff --git a/ui/src/components/IssueFiltersPopover.test.tsx b/ui/src/components/IssueFiltersPopover.test.tsx index 4fe6a11c12..718041bd66 100644 --- a/ui/src/components/IssueFiltersPopover.test.tsx +++ b/ui/src/components/IssueFiltersPopover.test.tsx @@ -81,4 +81,29 @@ describe("IssueFiltersPopover", () => { expect(layoutGrid?.className).toContain("grid-cols-1"); expect(popoverContent?.textContent).toContain("Live runs only"); }); + + it("hides the Priority filter section while priority UI is off (PAP-411)", () => { + const root = createRoot(container); + + act(() => { + root.render( + , + ); + }); + + const popoverContent = container.querySelector("[data-testid='popover-content']"); + expect(popoverContent).not.toBeNull(); + // Status section still renders, Priority section is gated off (PAP-411). + expect(popoverContent?.textContent).toContain("Status"); + expect(popoverContent?.textContent).not.toContain("Priority"); + }); }); diff --git a/ui/src/components/IssueFiltersPopover.tsx b/ui/src/components/IssueFiltersPopover.tsx index d702bad7b5..d491c74c1e 100644 --- a/ui/src/components/IssueFiltersPopover.tsx +++ b/ui/src/components/IssueFiltersPopover.tsx @@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Bot, Filter, HardDrive, Search, User, X } from "lucide-react"; import { PriorityIcon } from "./PriorityIcon"; +import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { StatusIcon } from "./StatusIcon"; import { defaultIssueFilterState, @@ -191,6 +192,8 @@ export function IssueFiltersPopover({ + {/* PAP-411: Priority filter section hidden behind SHOW_TASK_PRIORITY_UI (filter state stays intact). */} + {SHOW_TASK_PRIORITY_UI && (
Priority
@@ -206,6 +209,7 @@ export function IssueFiltersPopover({ ))}
+ )}
diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 5d795de7ce..40d8a2be4d 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -515,6 +515,25 @@ describe("IssueProperties", () => { act(() => root.unmount()); }); + it("hides the Priority property row while priority UI is off (PAP-411)", async () => { + const root = renderProperties(container, { + issue: createIssue({ priority: "high" }), + childIssues: [], + onUpdate: vi.fn(), + inline: true, + }); + await flush(); + + await waitForAssertion(() => { + // The Triage section still renders the Status row... + expect(container.querySelector('[data-property-label="Status"]')).not.toBeNull(); + // ...but the Priority row is gated behind SHOW_TASK_PRIORITY_UI (off). + expect(container.querySelector('[data-property-label="Priority"]')).toBeNull(); + }); + + act(() => root.unmount()); + }); + it("shows assignee and originating without responsible wording", async () => { mockAgentsApi.list.mockResolvedValue([{ id: "agent-1", name: "CodexCoder", status: "active", adapterType: "codex_local" }]); const root = renderProperties(container, { diff --git a/ui/src/components/IssueThreadInteractionCard.tsx b/ui/src/components/IssueThreadInteractionCard.tsx index 633f2f650b..d705484268 100644 --- a/ui/src/components/IssueThreadInteractionCard.tsx +++ b/ui/src/components/IssueThreadInteractionCard.tsx @@ -32,6 +32,7 @@ import { Button } from "./ui/button"; import { Checkbox } from "./ui/checkbox"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible"; import { PriorityIcon } from "./PriorityIcon"; +import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { Textarea } from "./ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; import { Badge } from "@/components/ui/badge"; @@ -512,7 +513,8 @@ function TaskTreeNode({ ) : null}
- {node.task.priority ? ( + {/* PAP-411: priority UI hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && node.task.priority ? ( { }); }); + it("hides the Priority option from the Sort and Group menus while priority UI is off (PAP-411)", async () => { + const { root } = renderWithQueryClient( + undefined} + />, + container, + ); + + await waitForAssertion(() => { + expect(container.querySelectorAll('[data-testid="issue-row"]').length).toBeGreaterThan(0); + }); + + const sortButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.getAttribute("title") === "Sort", + ); + expect(sortButton).toBeTruthy(); + act(() => { + sortButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await waitForAssertion(() => { + const labels = Array.from(document.body.querySelectorAll("button")).map((b) => b.textContent ?? ""); + // Status sort option renders, but the Priority option is gated off (PAP-411). + expect(labels.some((text) => text.includes("Status"))).toBe(true); + expect(labels.some((text) => text.includes("Priority"))).toBe(false); + }); + + const groupButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.getAttribute("title") === "Group", + ); + expect(groupButton).toBeTruthy(); + act(() => { + groupButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await waitForAssertion(() => { + const labels = Array.from(document.body.querySelectorAll("button")).map((b) => b.textContent ?? ""); + expect(labels.some((text) => text.includes("Status"))).toBe(true); + expect(labels.some((text) => text.includes("Priority"))).toBe(false); + }); + + act(() => { + root.unmount(); + }); + }); + it("hides the workflow blocker chip when a sub-issue is blocked only by its previous sibling", async () => { const firstChild = createIssue({ id: "issue-first-child", diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 2aeb017322..d463691c55 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -46,6 +46,7 @@ import { type InboxIssueColumn, } from "../lib/inbox"; import { cn, formatDurationMs, formatTokens } from "../lib/utils"; +import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { collectSubtreeLiveCounts } from "../lib/liveIssueIds"; import { InboxIssueMetaLeading, @@ -1827,6 +1828,7 @@ export function IssuesList({
+ {/* PAP-411: "priority" sort option hidden behind SHOW_TASK_PRIORITY_UI (comparator stays dormant). */} {([ ["workflow", "Workflow"], ["status", "Status"], @@ -1834,7 +1836,9 @@ export function IssuesList({ ["title", "Title"], ["created", "Created"], ["updated", "Updated"], - ] as const).map(([field, label]) => ( + ] as const) + .filter(([field]) => SHOW_TASK_PRIORITY_UI || field !== "priority") + .map(([field, label]) => ( + {/* PAP-411: mobile priority section hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && (
Priority @@ -2211,6 +2216,7 @@ export function NewIssueDialog() { ))}
+ )}
+ {/* PAP-411: PriorityIcon showcase gated behind SHOW_TASK_PRIORITY_UI per board decision. */} + {SHOW_TASK_PRIORITY_UI && (
{["critical", "high", "medium", "low"].map((p) => ( @@ -651,6 +657,7 @@ export function DesignGuide() { Click the icon to change (current: {priority})
+ )}
@@ -1123,7 +1130,8 @@ export function DesignGuide() { leading={ <> - + {/* PAP-411: PriorityIcon hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && } } identifier="PAP-001" @@ -1136,7 +1144,7 @@ export function DesignGuide() { leading={ <> - + {SHOW_TASK_PRIORITY_UI && } } identifier="PAP-002" @@ -1149,7 +1157,7 @@ export function DesignGuide() { leading={ <> - + {SHOW_TASK_PRIORITY_UI && } } identifier="PAP-003" @@ -1161,7 +1169,7 @@ export function DesignGuide() { leading={ <> - + {SHOW_TASK_PRIORITY_UI && } } identifier="PAP-004" @@ -1249,7 +1257,10 @@ export function DesignGuide() { onClick={() => setFilters([ { key: "status", label: "Status", value: "Active" }, - { key: "priority", label: "Priority", value: "High" }, + // PAP-411: priority filter demo row suppressed while SHOW_TASK_PRIORITY_UI is off. + ...(SHOW_TASK_PRIORITY_UI + ? [{ key: "priority", label: "Priority", value: "High" } as FilterValue] + : []), ]) } > @@ -1429,10 +1440,13 @@ export function DesignGuide() { Status
-
- Priority - -
+ {/* PAP-411: priority metadata row hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && ( +
+ Priority + +
+ )}
Responsible
@@ -1500,14 +1514,15 @@ export function DesignGuide() { 2
+ {/* PAP-411: leading PriorityIcon hidden behind SHOW_TASK_PRIORITY_UI. */} } + leading={SHOW_TASK_PRIORITY_UI ? : undefined} identifier="PAP-101" title="Build agent heartbeat system" onClick={() => {}} /> } + leading={SHOW_TASK_PRIORITY_UI ? : undefined} identifier="PAP-102" title="Add cost tracking dashboard" onClick={() => {}} diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index fa0d1b5c4a..8ef40448e7 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -1108,7 +1108,7 @@ describe("IssueDetail", () => { expect(mockDecisionsApi.list).not.toHaveBeenCalled(); }); - it("updates status and priority from the task header controls", async () => { + it("updates status from the task header control and hides the priority control (PAP-411)", async () => { const issue = createIssue({ status: "todo", priority: "medium" }); mockIssuesApi.get.mockResolvedValue(issue); mockIssuesApi.update.mockImplementation(async (_issueId: string, data: Record) => ({ @@ -1133,7 +1133,9 @@ describe("IssueDetail", () => { 'button[aria-label="Change priority (current: medium)"]', ); expect(statusButton).not.toBeNull(); - expect(priorityButton).not.toBeNull(); + // PAP-411: priority UI is hidden behind SHOW_TASK_PRIORITY_UI (off), so the header + // priority control must not render. + expect(priorityButton).toBeNull(); await act(async () => { statusButton!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); @@ -1141,13 +1143,10 @@ describe("IssueDetail", () => { await waitForAssertion(() => { expect(mockIssuesApi.update).toHaveBeenCalledWith(issue.identifier, { status: "done" }); }); - - await act(async () => { - priorityButton!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); - }); - await waitForAssertion(() => { - expect(mockIssuesApi.update).toHaveBeenCalledWith(issue.identifier, { priority: "high" }); - }); + expect(mockIssuesApi.update).not.toHaveBeenCalledWith( + issue.identifier, + expect.objectContaining({ priority: expect.anything() }), + ); mockIssuesApi.update.mockReset(); }); diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 483fb2860d..1c632986e4 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -135,6 +135,7 @@ import { ArtifactFileChip } from "../components/ArtifactFileChip"; import { ScrollToBottom } from "../components/ScrollToBottom"; import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; +import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags"; import { ProductivityReviewBadge } from "../components/ProductivityReviewBadge"; import { Identity } from "../components/Identity"; import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots"; @@ -706,7 +707,8 @@ function IssueDetailLoadingState({ {headerSeed ? ( <> - + {/* PAP-411: priority UI hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && } {identifier ? ( {identifier} ) : null} @@ -4195,10 +4197,13 @@ export function IssueDetail() { blockerAttention={issue.blockerAttention} onChange={(status) => updateIssue.mutate({ status })} /> - updateIssue.mutate({ priority })} - /> + {/* PAP-411: priority UI hidden behind SHOW_TASK_PRIORITY_UI. */} + {SHOW_TASK_PRIORITY_UI && ( + updateIssue.mutate({ priority })} + /> + )} {issue.identifier ?? issue.id.slice(0, 8)} {hasLiveRuns && (