From 9b7bd418334c93d60af33fb97624f149cb3e17cd Mon Sep 17 00:00:00 2001 From: scotttong Date: Fri, 11 Sep 2026 17:02:40 -0700 Subject: [PATCH] feat(ui): refine dashboard cards, charts, and recent lists (#13269) 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. > - Operators use the dashboard and Live runs page to inspect current and recent runs. > - Large transcript cards take space and make it hard to compare run states. > - A compact card must show the agent, linked task, and time, with access to run details. > - This pull request applies the supplied card design to both views. > - Operators can scan more runs and open a run or task for details. ## Linked Issues or Issue Description **What existing behavior does this improve?** The agent run cards on the dashboard and Live runs page, plus dashboard charts and recent lists. **Subsystem affected** `ui/` — React board UI. **Current behavior** Both views show large cards with embedded transcripts. Queued and running cards share a live treatment. The dashboard chart grid leaves an empty column, recent lists use different row sizes, and some activity labels expose raw event names. **Proposed behavior** Both views use the same compact grid. Each card shows the existing initials avatar, the agent name, one linked task row, and a right-aligned timestamp. There is no status chip next to the agent name. The timestamp uses small Inter text and the same muted gray as dashboard metric descriptions. Recovery details remain in the task view. Run status remains in the header tooltip and accessible label, and run transcripts remain in the run detail view. The shared in-progress task icon is now an animated circular spinner across the app. **Reason and benefit** Operators can scan task states without scrolling through embedded output. The in-progress spinner keeps the shared task icon sizes, circular shape, and stroke width. It honors reduced-motion preferences. **Breaking changes** The dashboard and Live runs page no longer show transcripts inside cards. Open the agent header to inspect a run. There are no API or database changes. **Additional context** Related public work found during the duplicate search: - Refs #4317 — related run-state display concerns. This change does not change server state reconciliation. - Refs #11394 — related live-run cache updates. This change retains the existing data flow. - Refs #2118 — related navigation from agent cards. The card header here opens the specific run. ## What Changed - Apply compact cards to the dashboard and Live runs page, with status icons and theme tokens. - Use 16 × 16px status icons in the shared cards, including the missing-task clock. - Keep the existing circular initials avatar. Remove the status chip beside the agent name while preserving the run label for tooltips and accessibility. - Replace the shared in-progress task glyph with an animated open circle. Use the same 10-unit radius and 2-unit stroke as the other task glyphs, with a reduced-motion guard across all task status surfaces. This is the requested workflow-status indicator across the app, including between runs; live indicators separately report active execution. - Use the same Tailwind blue tokens as nav dots and Live labels for the progress icon: blue-400 in dark mode and blue-600 in light mode. Covered-blocked icons follow the same blue token. - Match the Tasks by Status chart's In Progress bar and legend to the spinner's theme-aware color token. - Distribute the three visible dashboard charts across equal-width desktop columns, preserving existing gaps, card padding, and page margins. Retain four columns when the optional priority chart is enabled. - Use the done task icon's `--status-task-icon-done` token for green bars in Run Activity, Tasks by Status, and Success Rate, including their legends. - Use the blocked task icon's `--status-task-icon-blocked` token for red bars and legends in Run Activity, Tasks by Status, Success Rate, and priority charts. - Align Recent Tasks live counts to the sidebar's right edge. Reveal the ellipsis over a fading row surface on hover, keyboard focus, and while its menu is open, without moving the label or count. - Keep the ellipsis backdrop synchronized with the row background during fade-out, using the actual sidebar surface in both themes to prevent a darker flash. - On coarse-pointer devices, reserve space for the always-visible ellipsis and disable its fade so the live count remains readable beside it. - Order desktop dashboard Recent Tasks as status icon, task title, agent avatar/name, 11px monospace task ID, then timestamp. In narrow cards, keep the ID right-aligned beside the truncated title and place the agent and timestamp on the second line. - Give both dashboard lists the same 48px desktop rows, 24px leading slots, 80px desktop task ID columns, and 64px right-aligned timestamp columns. Use an 8px icon-to-content gap matching the task detail heading. Narrow layouts use matching 76px rows with intrinsic-width IDs on the title line; activity timestamps stay below. - Order dashboard Recent Activity as actor avatar, actor name, verb, task title, 11px monospace task ID, then timestamp. Use direct verbs such as “Board read …” instead of “issue read marked”. Truncate titles to preserve IDs and timestamps; keep full names and titles in tooltips. - Use 12px task status icons in the top breadcrumb and Properties status row. Keep the main task title icon at 20px. - Reduce monospace task IDs to the 11px micro type token in breadcrumbs and shared run cards, aligning them to the adjacent task titles' text baseline while keeping status icons centered. - Show the task title and identifier in one bordered row. Keep missing-task links usable and show lookup failures. - Remove the recovery chip and place the timestamp below the task row. - Keep the link to all runs available when the dashboard has four or fewer runs. - Avoid transcript polling for compact cards. - Fix bundled Inter font URLs and update the design guide and Storybook fixtures. - Cover run navigation, task status, failed lookups, shared icon sizes and stroke, and reduced-motion-safe animation in tests. ## Verification - Latest review fixes: targeted SidebarRecentTasks, SidebarNavItem, StatusGlyph, StatusIcon, ActiveAgentsPanel, activity-format, ui-font-assets, and Dashboard suites — 65 tests pass, including read/unread verbs and reduced-motion-safe task animation. - `pnpm --filter @paperclipai/ui typecheck` — passes. - `pnpm check:token-gates` — all gates pass. - `git diff --check` — passes. - Related component suites passed during development: run cards, status glyphs, breadcrumbs, issue properties, charts, sidebar navigation, recent-task actions, and settings sidebar. - Full repository `pnpm -r typecheck` and `pnpm build` pass on the final commit using the temporary Rust toolchain. Storybook build passed earlier in preparation. - All CI gates pass on `c85e949b12b471d425d216caa609aee41dc58bde`: all general/workspace and serialized server test shards, all three browser shards, build/runner verification, typecheck/release registry, canary dry run, policy, and security checks. The duplicate local `pnpm test:run` was stopped after CI completed the same test suites; it is not claimed as a completed local pass. The two Storybook jobs are skipped by the configured draft-PR workflow. - Greptile reviewed the final commit at 5/5 with no actionable findings and zero unresolved review threads. - Browser measurements at 1654px, 860px, and 390px confirm matching 48px desktop and 76px narrow rows (plus 1px dividers), right-aligned IDs on the title line, 64px timestamp slots, and no horizontal overflow. Narrow Recent Tasks rows retain readable agent names below the title. Activity reads as actor, verb, title, ID, time. Additional reference details can expand an activity row. - Verified three equal-width desktop charts with 16px gaps and aligned outer edges. Chart blues, greens, and reds resolve to the matching task-icon tokens. - Verified real queued, running, succeeded, failed, cancelled, and timed-out runs in the dev dashboard. Checked compact cards, task/run links, timestamps, light/dark themes, and narrow layout. - To inspect: open Dashboard, then Live runs. Both pages use the same small cards. Open an agent header for run output, or the task row for task details. - Verified the updated spinner on the real dashboard cards, Recent tasks, and task list. Checked actual SVG circle radius, rendered size, and stroke width. Confirmed the spinner, nav dots, and Live labels resolve to the identical blue-400 color in the dark-mode preview; light mode uses their shared blue-600 token. - Verified 12px computed width and height in the task breadcrumb and Properties pane, the restored 20px main task icon, and 11px task IDs with baseline alignment in the breadcrumb and shared dashboard/Live runs cards. - Verified the chart bar and legend resolve to the spinner color; Recent Tasks counts align with Dashboard's count. Checked the fading ellipsis overlay with keyboard focus and its open-menu state, then dismissed the menu. ## Risks - Operators must open run details to read output that was previously embedded. - Run outcomes are available in the header tooltip and run details instead of a visible status chip. An in-progress task icon reflects task status, independently of an individual run's outcome. - Long names and task titles truncate within the compact layout. Full labels remain available through links and tooltips. - Font loading changes affect all UI text that uses the bundled Inter font. The files remain served from the existing public fonts directory. - No schema, API, adapter, or execution-policy changes. ## Model Used OpenAI Codex, GPT-6 Astra (`gpt-6-astra`), with extended reasoning, repository tools, code execution, and browser verification. The session does not expose the context-window size. ## 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: Scott Tong Co-authored-by: Paperclip --- ui/src/components/ActiveAgentsPanel.test.tsx | 72 ++++++- ui/src/components/ActiveAgentsPanel.tsx | 191 +++++++++--------- ui/src/components/ActivityCharts.tsx | 19 +- ui/src/components/ActivityRow.tsx | 45 +++-- ui/src/components/BreadcrumbBar.tsx | 18 +- .../CompanySettingsSidebar.test.tsx | 3 +- ui/src/components/Sidebar.test.tsx | 3 +- ui/src/components/SidebarNavItem.tsx | 2 +- ui/src/components/SidebarRecentTasks.tsx | 6 +- ui/src/components/StatusGlyph.test.tsx | 29 ++- ui/src/components/StatusGlyph.tsx | 18 +- .../issue-properties/IssueProperties.tsx | 2 +- ui/src/components/primary-sidebar-styles.ts | 2 +- ui/src/index.css | 75 ++++++- ui/src/lib/activity-format.test.ts | 5 + ui/src/lib/activity-format.ts | 2 + ui/src/lib/ui-font-assets.test.ts | 2 +- ui/src/pages/Dashboard.tsx | 47 +++-- ui/src/pages/DashboardLive.tsx | 2 - ui/src/pages/DesignGuide.tsx | 18 ++ ui/src/pages/IssueDetail.tsx | 2 +- .../stories/agent-management.stories.tsx | 5 +- 22 files changed, 375 insertions(+), 193 deletions(-) diff --git a/ui/src/components/ActiveAgentsPanel.test.tsx b/ui/src/components/ActiveAgentsPanel.test.tsx index 8918ae1579..8871a05e24 100644 --- a/ui/src/components/ActiveAgentsPanel.test.tsx +++ b/ui/src/components/ActiveAgentsPanel.test.tsx @@ -4,7 +4,7 @@ import { act, type ReactNode } from "react"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ActiveAgentsPanel } from "./ActiveAgentsPanel"; +import { ActiveAgentsPanel, AgentRunCard } from "./ActiveAgentsPanel"; const mockHeartbeatsApi = vi.hoisted(() => ({ liveRunsForCompany: vi.fn(), @@ -30,10 +30,6 @@ vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi, })); -vi.mock("./Identity", () => ({ - Identity: ({ name }: { name: string }) => {name}, -})); - vi.mock("./RunChatSurface", () => ({ RunChatSurface: () =>
Run output
, })); @@ -156,6 +152,7 @@ describe("ActiveAgentsPanel", () => { anchor.textContent?.includes("more active/recent"), ); expect(moreLink?.getAttribute("href")).toBe("/dashboard/live"); + expect(container.textContent).not.toContain("Run output"); await act(async () => { root.unmount(); @@ -189,6 +186,7 @@ describe("ActiveAgentsPanel", () => { limit: 50, }); expect(container.textContent).not.toContain("more active/recent"); + expect(container.textContent).not.toContain("Run output"); await act(async () => { root.unmount(); @@ -224,7 +222,8 @@ describe("ActiveAgentsPanel", () => { const issueLink = [...container.querySelectorAll("a")].find((anchor) => anchor.textContent?.includes("Phase 4B"), ); - expect(issueLink?.textContent).toBe("PAP-3562 - Phase 4B: Implement LLM Wiki distillation UI"); + expect(issueLink?.textContent).toContain("Phase 4B: Implement LLM Wiki distillation UI"); + expect(issueLink?.textContent).toContain("PAP-3562"); expect(issueLink?.getAttribute("href")).toBe("/issues/PAP-3562"); }); @@ -232,4 +231,65 @@ describe("ActiveAgentsPanel", () => { root.unmount(); }); }); + + it("keeps run outcomes distinct from the linked task status", async () => { + const root = createRoot(container); + const statuses = ["running", "queued", "succeeded", "failed", "timed_out", "cancelled", "interrupted"]; + await act(async () => { + root.render(<>{statuses.map((status, index) => ( + + ))}); + }); + const headers = [...container.querySelectorAll('a[aria-label$=". View run"]')]; + expect(headers.map((header) => header.getAttribute("aria-label"))).toEqual([ + "Agent 0 — Running. View run", "Agent 1 — Queued. View run", + "Agent 2 — Succeeded. View run", "Agent 3 — Failed. View run", + "Agent 4 — Timed out. View run", "Agent 5 — Cancelled. View run", + "Agent 6 — Interrupted. View run", + ]); + expect(headers.every((header) => header.querySelector("svg") === null)).toBe(true); + expect(container.querySelector(".status-chip")).toBeNull(); + expect(container.querySelectorAll('[aria-label="Task in review"]')).toHaveLength(7); + expect(container.querySelectorAll(".motion-safe\\:animate-spin")).toHaveLength(0); + expect(container.querySelector('a[aria-label="Agent 0 — Running. View run"]')?.getAttribute("href")) + .toBe("/agents/agent-0/runs/run-0"); + await act(async () => root.unmount()); + }); + + it("keeps a failed task lookup navigable and shows a clear error", async () => { + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.textContent).toContain("Task unavailable"); + expect(container.querySelector('a[href="/issues/issue-missing"]')).not.toBeNull(); + await act(async () => root.unmount()); + }); + + it("does not animate running records while execution is reconnecting", async () => { + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('a[aria-label="Agent 0 — Running. View run"]')).not.toBeNull(); + expect(container.querySelector(".status-chip")).toBeNull(); + expect(container.querySelectorAll(".motion-safe\\:animate-spin")).toHaveLength(0); + await act(async () => root.unmount()); + }); }); diff --git a/ui/src/components/ActiveAgentsPanel.tsx b/ui/src/components/ActiveAgentsPanel.tsx index b68d80f49a..25bbb97b18 100644 --- a/ui/src/components/ActiveAgentsPanel.tsx +++ b/ui/src/components/ActiveAgentsPanel.tsx @@ -1,45 +1,18 @@ import { memo, useMemo } from "react"; import { Link } from "@/lib/router"; import { useQueries, useQuery } from "@tanstack/react-query"; -import { requiresExecutionReconciliation, type Issue, type IssueRecoveryAction } from "@paperclipai/shared"; +import type { Issue } from "@paperclipai/shared"; import { heartbeatsApi, type LiveRunForIssue } from "../api/heartbeats"; import type { TranscriptEntry } from "../adapters"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { cn, relativeTime } from "../lib/utils"; -import { - deriveActiveRecoveryDisplayState, - RECOVERY_CHIP_DEFAULT_TONE, -} from "../lib/recovery-display"; -import { ExternalLink } from "lucide-react"; +import { Clock3 } from "lucide-react"; import { Identity } from "./Identity"; +import { StatusGlyph } from "./StatusGlyph"; import { RunChatSurface } from "./RunChatSurface"; import { useLiveRunTranscripts } from "./transcript/useLiveRunTranscripts"; import { usePublishSharedQueryData, useSharedPollingQuery } from "../hooks/useSharedPolling"; -import { Badge } from "@/components/ui/badge"; - -function RunCardRecoveryChip({ action }: { action: IssueRecoveryAction }) { - const state = deriveActiveRecoveryDisplayState(action); - if (!state || requiresExecutionReconciliation(action.cause)) return null; - const tone = RECOVERY_CHIP_DEFAULT_TONE[state]; - const Icon = tone.icon; - return ( - - - {tone.label} - - ); -} const MIN_DASHBOARD_RUNS = 4; const DASHBOARD_RUN_CARD_LIMIT = 4; @@ -47,10 +20,17 @@ const DASHBOARD_LOG_POLL_INTERVAL_MS = 15_000; const DASHBOARD_LOG_READ_LIMIT_BYTES = 64_000; const DASHBOARD_MAX_CHUNKS_PER_RUN = 40; const EMPTY_TRANSCRIPT: TranscriptEntry[] = []; +const EMPTY_RUNS: LiveRunForIssue[] = []; -function isRunActive(run: LiveRunForIssue): boolean { - return run.status === "queued" || run.status === "running"; -} +const runStatusLabels: Record = { + running: "Running", + queued: "Queued", + succeeded: "Succeeded", + failed: "Failed", + timed_out: "Timed out", + cancelled: "Cancelled", + interrupted: "Interrupted", +}; interface ActiveAgentsPanelProps { companyId: string; @@ -63,6 +43,7 @@ interface ActiveAgentsPanelProps { emptyMessage?: string; queryScope?: string; showMoreLink?: boolean; + showTranscripts?: boolean; } export function ActiveAgentsPanel({ @@ -76,6 +57,7 @@ export function ActiveAgentsPanel({ emptyMessage = "No recent agent runs.", queryScope = "dashboard", showMoreLink = true, + showTranscripts = false, }: ActiveAgentsPanelProps) { const liveRunsQueryKey = [...queryKeys.liveRuns(companyId), queryScope, { minRunCount, fetchLimit }] as const; const sharedLiveRuns = useSharedPollingQuery({ @@ -119,7 +101,7 @@ export function ActiveAgentsPanel({ }, [issueQueries]); const { transcriptByRun, hasOutputForRun } = useLiveRunTranscripts({ - runs: visibleRuns, + runs: showTranscripts ? visibleRuns : EMPTY_RUNS, companyId, maxChunksPerRun: DASHBOARD_MAX_CHUNKS_PER_RUN, logPollIntervalMs: DASHBOARD_LOG_POLL_INTERVAL_MS, @@ -137,7 +119,7 @@ export function ActiveAgentsPanel({

{emptyMessage}

) : ( -
+
{visibleRuns.map((run) => ( visibleIssueIds[index] === run.issueId && query.isError)} className={cardClassName} /> ))}
)} - {showMoreLink && hiddenRunCount > 0 && ( + {showMoreLink && runs.length > 0 && (
- {hiddenRunCount} more active/recent run{hiddenRunCount === 1 ? "" : "s"} + {hiddenRunCount > 0 + ? `${hiddenRunCount} more active/recent run${hiddenRunCount === 1 ? "" : "s"}` + : "View all runs"}
)} @@ -163,88 +148,94 @@ export function ActiveAgentsPanel({ ); } -const AgentRunCard = memo(function AgentRunCard({ +export const AgentRunCard = memo(function AgentRunCard({ companyId, run, issue, - transcript, - hasOutput, - isActive, + transcript = EMPTY_TRANSCRIPT, + hasOutput = false, + showTranscript = false, + issueLoadFailed = false, className, }: { companyId: string; run: LiveRunForIssue; - issue?: Issue; - transcript: TranscriptEntry[]; - hasOutput: boolean; - isActive: boolean; + issue?: Pick; + transcript?: TranscriptEntry[]; + hasOutput?: boolean; + showTranscript?: boolean; + issueLoadFailed?: boolean; className?: string; }) { + const statusLabel = runStatusLabels[run.status] ?? run.status.replace(/[_-]/g, " "); + const runUrl = `/agents/${run.agentId}/runs/${run.id}`; + const timestamp = run.finishedAt + ? `Finished ${relativeTime(run.finishedAt)}` + : run.startedAt ? `Started ${relativeTime(run.startedAt)}` : `Queued ${relativeTime(run.createdAt)}`; + const taskTitle = issue?.title ?? (issueLoadFailed ? "Task unavailable" : "Loading task…"); + return (
-
-
-
-
- {isActive && (!run.execution || run.execution.phase === "working") ? ( - - - - - ) : ( - - )} - -
-
- {isActive ? "Working" : run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : `Started ${relativeTime(run.createdAt)}`} -
-
+ )} data-run-status={run.status}> +
+ + + + {run.issueId ? ( - + + + + {taskTitle} + + {issue?.identifier ?? run.issueId.slice(0, 8)} + + + ) : ( + + + {run.invocationSource === "timer" ? "Scheduled heartbeat" : "No linked task"} -
- - {run.issueId && ( -
- - {issue?.identifier ?? run.issueId.slice(0, 8)} - {issue?.title ? ` - ${issue.title}` : ""} - - {issue?.activeRecoveryAction ? ( -
- -
- ) : null} -
)} +
-
- -
+ {showTranscript && ( +
+ +
+ )}
); }); diff --git a/ui/src/components/ActivityCharts.tsx b/ui/src/components/ActivityCharts.tsx index cc72236270..104e94cbea 100644 --- a/ui/src/components/ActivityCharts.tsx +++ b/ui/src/components/ActivityCharts.tsx @@ -20,9 +20,9 @@ function emptyRunDay(date: string): DashboardRunActivityDay { } const runSegmentColors = { - succeeded: "var(--hex-10b981)", + succeeded: "var(--status-task-icon-done)", recovered: "var(--status-task-todo)", - failed: "var(--hex-ef4444)", + failed: "var(--status-task-icon-blocked)", other: "var(--hex-737373)", } as const; @@ -166,7 +166,7 @@ export function RunActivityChart(props: RunChartProps) { } const priorityColors: Record = { - critical: "var(--hex-ef4444)", + critical: "var(--status-task-icon-blocked)", high: "var(--hex-f97316)", medium: "var(--hex-eab308)", low: "var(--hex-6b7280)", @@ -223,14 +223,15 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA // status vocabulary; badge, row, chart, and log agree). Previously an // independent palette (todo blue, in_progress violet, etc.). `backlog` // deliberately keeps --project-none (pre-B5, per user ruling); the -// priority series and success-rate tints below are not status hues and -// are left alone. +// non-red priority series and warning success-rate tints retain their own hues. +// Progress, done, and blocked use the icon hues so bars and legends match +// the task icons in each theme. const statusColors: Record = { todo: "var(--status-task-todo)", - in_progress: "var(--status-task-in_progress)", + in_progress: "var(--status-task-icon-in_progress)", in_review: "var(--status-task-in_review)", - done: "var(--status-task-done)", - blocked: "var(--status-task-blocked)", + done: "var(--status-task-icon-done)", + blocked: "var(--status-task-icon-blocked)", cancelled: "var(--status-task-cancelled)", backlog: "var(--project-none)", }; @@ -309,7 +310,7 @@ export function SuccessRateChart(props: RunChartProps) { // rather than dragging it down as failures. const effectiveSucceeded = entry.succeeded + entry.recovered; const rate = entry.total > 0 ? effectiveSucceeded / entry.total : 0; - const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--hex-10b981)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--hex-ef4444)"; + const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--status-task-icon-done)" : rate >= 0.5 ? "var(--hex-eab308)" : "var(--status-task-icon-blocked)"; return (
0 ? Math.round(rate * 100) : 0}% (${effectiveSucceeded}/${entry.total})`}> {entry.total > 0 ? ( diff --git a/ui/src/components/ActivityRow.tsx b/ui/src/components/ActivityRow.tsx index 13d335e854..f2ca58a21c 100644 --- a/ui/src/components/ActivityRow.tsx +++ b/ui/src/components/ActivityRow.tsx @@ -53,27 +53,44 @@ export function ActivityRow({ event, agentMap, userProfileMap, entityNameMap, en const inner = (
-
-
- - {actorAvatarUrl && } - {deriveInitials(actorName)} - -

- {actorName} - {verb} - {name && {name}} - {entityTitle && — {entityTitle}} -

+
+ +
+
+

+ + {actorName}{" "} + {verb} + + {event.entityType === "issue" ? ( + {entityTitle} + ) : ( + + {name && {name}} + {entityTitle && — {entityTitle}} + + )} +

+ + {event.entityType === "issue" ? name : null} + +
+
+ + {timeAgo(event.createdAt)} + +
- {timeAgo(event.createdAt)}
); const classes = cn( - "px-4 py-2 text-sm", + "dashboard-list-row text-sm", link && "cursor-pointer hover:bg-accent/50 transition-colors", className, ); diff --git a/ui/src/components/BreadcrumbBar.tsx b/ui/src/components/BreadcrumbBar.tsx index e51f942b65..b123da3ea7 100644 --- a/ui/src/components/BreadcrumbBar.tsx +++ b/ui/src/components/BreadcrumbBar.tsx @@ -24,7 +24,7 @@ type GlobalToolbarContext = { companyId: string | null; companyPrefix: string | function CrumbIdentifier({ identifier }: { identifier?: string }) { if (!identifier) return null; return ( - + {identifier} ); @@ -113,9 +113,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?: return (
{menuButton} -

+

{currentCrumb.leading ? ( - {currentCrumb.leading} + {currentCrumb.leading} ) : null} {currentCrumb.label} @@ -137,9 +137,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?: {isLast || !crumb.href ? ( crumb.leading || crumb.identifier ? ( - + {crumb.leading && ( - {crumb.leading} + {crumb.leading} )} {!taskDetailLayout ? : null} {crumb.label} @@ -154,12 +154,12 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?: {crumb.leading && ( - {crumb.leading} + {crumb.leading} )} {!taskDetailLayout ? : null} {crumb.label} @@ -194,9 +194,9 @@ export function BreadcrumbBar({ taskDetailLayout = false }: { taskDetailLayout?: {menuButton}
{breadcrumbs[0].leading || breadcrumbs[0].identifier ? ( -

+

{breadcrumbs[0].leading && ( - {breadcrumbs[0].leading} + {breadcrumbs[0].leading} )} {breadcrumbs[0].label} diff --git a/ui/src/components/CompanySettingsSidebar.test.tsx b/ui/src/components/CompanySettingsSidebar.test.tsx index 111f60690d..7a3418dcc8 100644 --- a/ui/src/components/CompanySettingsSidebar.test.tsx +++ b/ui/src/components/CompanySettingsSidebar.test.tsx @@ -144,8 +144,7 @@ describe("CompanySettingsSidebar", () => { expect(container.textContent).not.toContain("Settings"); expect(container.querySelector('[aria-label="Back from Settings"]')).toBeNull(); const settingsSurface = container.querySelector('[data-contextual-sidebar="settings"]'); - expect(settingsSurface?.classList).toContain("bg-border/50"); - expect(settingsSurface?.classList).toContain("dark:bg-muted"); + expect(settingsSurface?.classList).toContain("primary-sidebar-surface"); expect(container.querySelector('[data-slot="contextual-sidebar-nav"]')?.className).toBe( primarySidebarStyles.nav, ); diff --git a/ui/src/components/Sidebar.test.tsx b/ui/src/components/Sidebar.test.tsx index 970709436f..bd71c36ff9 100644 --- a/ui/src/components/Sidebar.test.tsx +++ b/ui/src/components/Sidebar.test.tsx @@ -173,8 +173,7 @@ describe("Sidebar", () => { const sidebar = container.querySelector("aside"); expect(sidebar?.classList).not.toContain("border-r"); expect(sidebar?.classList).not.toContain("border-border"); - expect(sidebar?.classList).toContain("bg-border/50"); - expect(sidebar?.classList).toContain("dark:bg-muted"); + expect(sidebar?.classList).toContain("primary-sidebar-surface"); flushSync(() => { root.unmount(); diff --git a/ui/src/components/SidebarNavItem.tsx b/ui/src/components/SidebarNavItem.tsx index e918ed3540..efbcfa4e56 100644 --- a/ui/src/components/SidebarNavItem.tsx +++ b/ui/src/components/SidebarNavItem.tsx @@ -182,7 +182,7 @@ export function SidebarNavItem({ )} {!rail && (hasLive || liveAccessory) && ( - + {liveAccessory} {hasLive && ( <> diff --git a/ui/src/components/SidebarRecentTasks.tsx b/ui/src/components/SidebarRecentTasks.tsx index be9797bbf4..3bcaf90380 100644 --- a/ui/src/components/SidebarRecentTasks.tsx +++ b/ui/src/components/SidebarRecentTasks.tsx @@ -250,11 +250,11 @@ function RecentTasksList({ <> {entries.map((entry) => ( -
+
{!rail ? ( @@ -265,7 +265,7 @@ function RecentTasksList({ variant="ghost" size="icon-xs" aria-label={`More actions for ${entry.title}`} - className="absolute right-2 top-(--pct-50) z-10 -translate-y-(--pct-50) text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 pointer-coarse:opacity-100 group-hover/recent-task:opacity-100 group-focus-within/recent-task:opacity-100 data-[state=open]:bg-accent data-[state=open]:text-foreground data-[state=open]:opacity-100" + className="sidebar-action-menu absolute right-2 top-(--pct-50) z-10 -translate-y-(--pct-50) text-muted-foreground pointer-events-none opacity-0 transition-opacity hover:bg-sidebar-accent dark:hover:bg-sidebar-accent hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100 pointer-coarse:before:hidden group-hover/recent-task:pointer-events-auto group-hover/recent-task:opacity-100 group-focus-within/recent-task:pointer-events-auto group-focus-within/recent-task:opacity-100 data-[state=open]:pointer-events-auto data-[state=open]:bg-sidebar-accent data-[state=open]:text-foreground data-[state=open]:opacity-100" >