From 53d6297f75de400fcdd7d6bd6beabb1f71031926 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:14:29 -0500 Subject: [PATCH] fix(ui): simplify external object labels and star controls (#9929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source control plane people use to manage AI agents and their work > - Operators rely on task detail properties and header actions to scan linked resources and make quick decisions > - External GitHub objects used a long pull-request label and repeated mention counts that added visual noise without adding state > - The task-detail star action also rendered as a labelled outline button, unlike the compact star controls used elsewhere > - These inconsistencies made dense task-detail surfaces slower to scan and broke Paperclip's content-first visual language > - This pull request shortens the GitHub pull-request label, removes duplicate mention-count decoration, and aligns the detail star action with the icon-only control pattern > - The benefit is a calmer, more consistent task-detail experience with accessible labels preserved for assistive technology ## Linked Issues or Issue Description No matching public GitHub issue or open pull request was found. **What happened?** On the task detail surface, linked GitHub pull requests were labelled `Github Pull Request` and could show a repeated `×N` mention count. The detail-header star control used a labelled outline button rather than the compact icon-only star pattern. **Expected behavior** Linked pull requests should use the concise `Github PR` label without duplicate mention-count decoration, and the detail star action should render as an accessible icon-only ghost button consistent with neighboring controls. **Steps to reproduce** 1. Open a task with a linked GitHub pull request mentioned more than once. 2. Inspect the external-object property label and value row. 3. Inspect the star action in the task detail header. **Paperclip version or commit:** `230126d80b` (`master` at preparation time) **Deployment / installation:** Local development, built from source. **Scope:** Core UI; not adapter-specific, database-related, or configuration-related. ## What Changed - Render GitHub pull-request property labels as `Github PR`. - Remove repeated external-object mention-count decoration from property values. - Render detail-header star controls as icon-only ghost buttons while preserving `aria-label`, pressed, busy, error, and tooltip states. - Expand component coverage for concise labels, duplicate-count suppression, visual variants, and accessible star actions. ## Verification - `pnpm --filter @paperclipai/ui exec vitest run src/components/IssueProperties.test.tsx src/components/StarToggle.test.tsx` — 54 tests passed. - `pnpm --filter @paperclipai/ui typecheck` — passed. - Visual review (before/after plus normal, starred, pending, and error states): https://htmlpreview.github.io/?https://gist.githubusercontent.com/cryppadotta/9688862a8826c1134aa2b2e8c16509d8/raw/45cb4921851f8968b21e7490e0d705161a9e165d/star-toggle-review.html - `pnpm check:token-gates` — reports five existing `#9627` violations in files unchanged by this PR; the same values are present on `master`. ## Risks - Low risk: changes are limited to task-detail presentation and tests. - The star action remains fully accessible through its existing ARIA label and tooltip, but it no longer displays visible text. - External-object mention counts remain available in data; only the redundant property-row decoration is removed. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5.3 Codex, tool-enabled coding agent with repository and shell access. ## 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 (e.g. `docs/...`, `fix/...`) 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 - [x] 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 --- ui/src/components/IssueProperties.test.tsx | 12 ++++++---- ui/src/components/StarToggle.test.tsx | 23 +++++++++++++++---- ui/src/components/StarToggle.tsx | 8 +++---- .../issue-properties/external-object-rows.tsx | 7 ++---- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/ui/src/components/IssueProperties.test.tsx b/ui/src/components/IssueProperties.test.tsx index 4e1f2c46ef..892f94f546 100644 --- a/ui/src/components/IssueProperties.test.tsx +++ b/ui/src/components/IssueProperties.test.tsx @@ -2394,7 +2394,7 @@ describe("IssueProperties", () => { inline: true, externalObjects: [ { - mentionCount: 1, + mentionCount: 2, sourceLabels: ["Description"], pill: { providerKey: "github", @@ -2411,7 +2411,7 @@ describe("IssueProperties", () => { group: { object: null, mentions: [], - mentionCount: 1, + mentionCount: 2, sourceLabels: ["Description"], }, }, @@ -2463,7 +2463,9 @@ describe("IssueProperties", () => { }); await flush(); - expect(container.textContent).toContain("Github Pull Request"); + expect(container.textContent).toContain("Github PR"); + expect(container.textContent).not.toContain("Github Pull Request"); + expect(container.textContent).not.toContain("×2"); expect(container.textContent).toContain("Github Issue"); expect(container.textContent).toContain("URL"); expect(container.textContent).not.toContain("URL link"); @@ -2472,13 +2474,13 @@ describe("IssueProperties", () => { expect(container.textContent).toContain("Open"); expect(container.textContent).not.toContain("External objects"); const label = Array.from(container.querySelectorAll("span")) - .find((span) => span.textContent === "Github Pull Request"); + .find((span) => span.textContent === "Github PR"); expect(label?.querySelector("svg")).toBeTruthy(); const pullRequestLink = Array.from(container.querySelectorAll("a")) .find((anchor) => anchor.getAttribute("href") === "https://github.com/acme/web/pull/241"); expect(pullRequestLink?.textContent).toContain("PR 241 - Merged"); expect(pullRequestLink?.textContent).not.toContain("acme/web#241"); - expect(pullRequestLink?.textContent).not.toContain("Github Pull Request"); + expect(pullRequestLink?.textContent).not.toContain("Github PR"); expect(pullRequestLink?.querySelectorAll("svg")).toHaveLength(1); expect(pullRequestLink?.className).not.toContain("paperclip-mention-chip"); expect(pullRequestLink?.className).not.toContain("rounded-full"); diff --git a/ui/src/components/StarToggle.test.tsx b/ui/src/components/StarToggle.test.tsx index bc3c0a863d..2f0dcd1f48 100644 --- a/ui/src/components/StarToggle.test.tsx +++ b/ui/src/components/StarToggle.test.tsx @@ -92,15 +92,30 @@ describe("StarToggle", () => { expect(onToggle).not.toHaveBeenCalled(); }); - it("surfaces a retry affordance on error for the button variant", async () => { + it("surfaces an icon-only retry affordance on error for the detail variant", async () => { await render( {}} />); const btn = button(); - expect(btn?.textContent).toContain("Retry star"); + expect(btn?.textContent).toBe(""); + expect(btn?.getAttribute("data-variant")).toBe("ghost"); expect(btn?.getAttribute("title")).toBe("Couldn't save — retry"); }); - it("renders the labelled Star/Starred button variant", async () => { + it("renders the unstarred detail variant as an unfilled icon without an outline", async () => { + await render( {}} />); + const btn = button(); + expect(btn?.textContent).toBe(""); + expect(btn?.getAttribute("data-variant")).toBe("ghost"); + expect(btn?.getAttribute("aria-label")).toBe("Star Alpha"); + expect(btn?.querySelector("svg")?.getAttribute("class")).not.toContain("fill-amber-500"); + }); + + it("renders the detail variant as a filled, icon-only star without an outline", async () => { await render( {}} />); - expect(button()?.textContent).toContain("Starred"); + const btn = button(); + expect(btn?.textContent).toBe(""); + expect(btn?.getAttribute("data-variant")).toBe("ghost"); + expect(btn?.getAttribute("data-size")).toBe("icon-sm"); + expect(btn?.getAttribute("aria-label")).toBe("Unstar Alpha"); + expect(btn?.querySelector("svg")?.getAttribute("class")).toContain("fill-amber-500"); }); }); diff --git a/ui/src/components/StarToggle.tsx b/ui/src/components/StarToggle.tsx index 72c1d9b428..6f67b2c854 100644 --- a/ui/src/components/StarToggle.tsx +++ b/ui/src/components/StarToggle.tsx @@ -14,7 +14,7 @@ export interface StarToggleProps { error?: boolean; /** * "row" — quiet icon-only control for sidebar and browse-list rows. - * "button" — labelled Star/Starred button for detail headers. + * "button" — always-visible icon-only control for detail headers. */ size?: "row" | "button"; /** Called with the desired next starred value. */ @@ -63,12 +63,11 @@ export function StarToggle({ } if (size === "button") { - const label = pending ? "Saving..." : error ? "Retry star" : starred ? "Starred" : "Star"; return ( ); } diff --git a/ui/src/components/issue-properties/external-object-rows.tsx b/ui/src/components/issue-properties/external-object-rows.tsx index e449fda289..37382746f4 100644 --- a/ui/src/components/issue-properties/external-object-rows.tsx +++ b/ui/src/components/issue-properties/external-object-rows.tsx @@ -32,7 +32,7 @@ function externalObjectRowDisplayKey(group: IssueExternalObjectGroup): string { const displayKey = pill.displayKey?.trim(); if (displayKey) return displayKey; if (pill.providerKey === "github") { - if (pill.objectType === "pull_request") return "Github Pull Request"; + if (pill.objectType === "pull_request") return "Github PR"; if (pill.objectType === "issue") return "Github Issue"; } return externalObjectDisplayLabel(pill.providerKey, pill.objectType); @@ -101,7 +101,7 @@ function externalObjectPropertyTitle(group: IssueExternalObjectGroup): string { } function ExternalObjectPropertyValue({ group }: { group: IssueExternalObjectGroup }) { - const { pill, mentionCount } = group; + const { pill } = group; const statusLabel = externalObjectPropertyStatusLabel(group); const providerLabel = externalObjectProviderLabel(pill.providerKey); const typeLabel = externalObjectTypeLabel(pill.objectType); @@ -116,9 +116,6 @@ function ExternalObjectPropertyValue({ group }: { group: IssueExternalObjectGrou label={`${providerLabel}: ${statusLabel}`} /> {value} - {mentionCount > 1 ? ( - ×{mentionCount} - ) : null} ); const className = cn(