Information Architecture + project/agent visual refresh (experimental) (#7543)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board UI is the control surface for issues, projects, agents, goals, workspaces, and operator settings. > - The existing navigation and list surfaces make several high-frequency workflows feel harder to scan than they should, especially around projects and agents. > - The product direction is to improve those surfaces without breaking the existing route model or forcing a new IA on every operator at once. > - This pull request now keeps the dependent IA, project identity, and agent-list visual refresh work together while the Issue-to-Task copy migration is split into #7651. > - The benefit is a clearer left nav, better project identity, denser agent/project list rows, and brand-aligned status treatment while preserving the classic default experience behind a flag. ## Linked Issues or Issue Description Refs #7645 Refs #7651 Internal planning/work references: PAP-53, PAP-56, PAP-58, PAP-59, PAP-60, PAP-61, PAP-68, PAP-69, PAP-70, PAP-71, PAP-72, PAP-75, PAP-76, PAP-80, PAP-85, PAP-86, PAP-87, PAP-88, PAP-89. ## What Changed - Adds `enableStreamlinedLeftNavigation`, defaulting off, and gates sidebar presentation so classic navigation remains the default. - Adds project icon persistence, validation, portability, picker UI, and `ProjectTile` rendering while defaulting new projects to neutral gray. - Adds projects-list task-count and budget summary data with focused server/shared/UI coverage. - Refreshes agent list rows, row actions, active/recent sidebar behavior, and status capsule/chip styling for the approved brand state system. - Removes the placeholder Conference room and Artifacts nav/routes from the finalized experimental nav direction. - Removes `pnpm-lock.yaml` and the Issue-to-Task copy migration from this PR diff; the copy migration now lives in #7651. ## Verification - Existing branch verification from the authored commits: UI typecheck, targeted unit tests, and light/dark visual checks for `/agents`, agent detail, and design-guide status states. - Maintainer cleanup verification on `75e34e5`: `git diff --check origin/master...HEAD` passed, the `design/` diff is empty, and the PR diff is 61 files, below Greptile's 100-file review limit. - `pnpm --filter @paperclipai/ui build` passed. - `NODE_ENV=test pnpm exec vitest run ui/src/components/Sidebar.test.tsx` passed: 1 file, 8 tests. - CI and Greptile should rerun on the latest push. ## Risks - Broad UI surface area: the experimental flag keeps the classic nav default, but changed shared components such as `EntityRow`, `ProjectTile`, and agent status badges could affect multiple pages. - Database migration: `projects.icon` is additive and nullable, but migration ordering and portability import/export must stay aligned. - The Issue-to-Task copy migration is now separated into #7651, so reviewers should evaluate this PR as IA/project/agent presentation work only. - Visual regressions are possible across smaller widths because the PR intentionally changes dense list-row layouts. ## Model Used Claude Opus 4.8 assisted the original feature commits. Paperclip-Paperclip agents assisted some planning/design commits. Codex / GPT-5-class coding agent with shell, GitHub CLI, and repository access performed this PR-readiness cleanup and split. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [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 <noreply@paperclip.ing> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Dotta <bippadotta@protonmail.com>
This commit is contained in:
@@ -46,6 +46,7 @@ function makeProject(id: string, name: string): Project {
|
||||
leadAgentId: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
env: null,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
|
||||
@@ -82,6 +82,7 @@ describe("issueDetailBreadcrumb", () => {
|
||||
leadAgentId: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
env: null,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
|
||||
@@ -594,6 +594,7 @@ describe("optimistic issue comments", () => {
|
||||
leadAgentId: null,
|
||||
targetDate: null,
|
||||
color: null,
|
||||
icon: null,
|
||||
env: null,
|
||||
pauseReason: null,
|
||||
pausedAt: null,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatProjectBudget } from "./utils";
|
||||
|
||||
describe("formatProjectBudget", () => {
|
||||
it("renders a /mo suffix for monthly budgets", () => {
|
||||
expect(formatProjectBudget({ amountCents: 120_000, windowKind: "calendar_month_utc" })).toBe("$1,200.00/mo");
|
||||
});
|
||||
|
||||
it("renders the bare amount for lifetime budgets", () => {
|
||||
expect(formatProjectBudget({ amountCents: 50_000, windowKind: "lifetime" })).toBe("$500.00");
|
||||
});
|
||||
|
||||
it("formats sub-dollar amounts with cents", () => {
|
||||
expect(formatProjectBudget({ amountCents: 150, windowKind: "lifetime" })).toBe("$1.50");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Atom,
|
||||
Box,
|
||||
Boxes,
|
||||
Brain,
|
||||
Briefcase,
|
||||
Bug,
|
||||
Code,
|
||||
Cog,
|
||||
Compass,
|
||||
Cpu,
|
||||
Crown,
|
||||
Database,
|
||||
FileCode,
|
||||
Flame,
|
||||
Folder,
|
||||
Gem,
|
||||
GitBranch,
|
||||
Globe,
|
||||
Hammer,
|
||||
Heart,
|
||||
Hexagon,
|
||||
Layers,
|
||||
Lightbulb,
|
||||
Lock,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
Package,
|
||||
Puzzle,
|
||||
Radar,
|
||||
Rocket,
|
||||
Search,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Star,
|
||||
Target,
|
||||
Telescope,
|
||||
Terminal,
|
||||
Wrench,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { PROJECT_ICON_NAMES, type ProjectIconName } from "@paperclipai/shared";
|
||||
|
||||
export const PROJECT_ICONS: Record<ProjectIconName, LucideIcon> = {
|
||||
folder: Folder,
|
||||
rocket: Rocket,
|
||||
code: Code,
|
||||
terminal: Terminal,
|
||||
database: Database,
|
||||
globe: Globe,
|
||||
package: Package,
|
||||
boxes: Boxes,
|
||||
box: Box,
|
||||
layers: Layers,
|
||||
briefcase: Briefcase,
|
||||
compass: Compass,
|
||||
target: Target,
|
||||
flame: Flame,
|
||||
zap: Zap,
|
||||
star: Star,
|
||||
bug: Bug,
|
||||
wrench: Wrench,
|
||||
hammer: Hammer,
|
||||
lightbulb: Lightbulb,
|
||||
sparkles: Sparkles,
|
||||
shield: Shield,
|
||||
lock: Lock,
|
||||
search: Search,
|
||||
cog: Cog,
|
||||
brain: Brain,
|
||||
cpu: Cpu,
|
||||
"git-branch": GitBranch,
|
||||
"file-code": FileCode,
|
||||
puzzle: Puzzle,
|
||||
gem: Gem,
|
||||
atom: Atom,
|
||||
heart: Heart,
|
||||
mail: Mail,
|
||||
"message-square": MessageSquare,
|
||||
crown: Crown,
|
||||
radar: Radar,
|
||||
telescope: Telescope,
|
||||
hexagon: Hexagon,
|
||||
};
|
||||
|
||||
const DEFAULT_ICON: ProjectIconName = "folder";
|
||||
|
||||
export function getProjectIcon(iconName: string | null | undefined): LucideIcon {
|
||||
if (iconName && PROJECT_ICON_NAMES.includes(iconName as ProjectIconName)) {
|
||||
return PROJECT_ICONS[iconName as ProjectIconName];
|
||||
}
|
||||
return PROJECT_ICONS[DEFAULT_ICON];
|
||||
}
|
||||
+40
-10
@@ -83,20 +83,50 @@ export const statusBadge: Record<string, string> = {
|
||||
export const statusBadgeDefault = "bg-muted text-muted-foreground";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent status dot — solid background for small indicator dots
|
||||
// Agent status — brand state system (PAP-75)
|
||||
//
|
||||
// The agents section shows four states only. Each maps to a brand colour name;
|
||||
// the indicator is a brand heartbeat capsule (8×16, r4) and the row chip is a
|
||||
// brand `.task-chip` (1px border, light/dark variants). `active` is dead code
|
||||
// that folds into idle; `running` pulses and `error` blinks. `terminated` and
|
||||
// `pending_approval` are excluded from the list but may still render on detail
|
||||
// pages, where they fall through to the gray default.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const agentStatusDot: Record<string, string> = {
|
||||
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 type AgentBadgeColor = "gray" | "blue" | "amber" | "red";
|
||||
|
||||
/** Agent status → brand colour name. `active` aliases idle (never assigned). */
|
||||
export const agentStatusColor: Record<string, AgentBadgeColor> = {
|
||||
idle: "gray",
|
||||
active: "gray",
|
||||
running: "blue",
|
||||
paused: "amber",
|
||||
error: "red",
|
||||
};
|
||||
|
||||
export const agentStatusDotDefault = "bg-neutral-400";
|
||||
export const agentStatusColorDefault: AgentBadgeColor = "gray";
|
||||
|
||||
/** Brand `.task-chip` styles (1px border) per colour name — light + dark. */
|
||||
export const agentStatusBadge: Record<AgentBadgeColor, string> = {
|
||||
gray: "bg-[#F5F3F0] text-[#52585D] border-[#A8AEB2] dark:bg-[#6e696024] dark:text-[#9A958A] dark:border-[#9e958a73]",
|
||||
blue: "bg-[#DBEAFE] text-[#1D4ED8] border-[#2563EB] dark:bg-[#2563eb2e] dark:text-[#2563EB] dark:border-[#2563eb73]",
|
||||
amber: "bg-[#FEF3C7] text-[#B45309] border-[#F59E0B] dark:bg-[#f59e0b24] dark:text-[#F59E0B] dark:border-[#f59e0b73]",
|
||||
red: "bg-[#FEE2E2] text-[#991B1B] border-[#DC2626] dark:bg-[#dc26262e] dark:text-[#DC2626] dark:border-[#dc262673]",
|
||||
};
|
||||
|
||||
/** Heartbeat-capsule fill (solid) per colour name. gray darkens in dark mode. */
|
||||
export const agentStatusCapsule: Record<AgentBadgeColor, string> = {
|
||||
gray: "bg-[#A8AEB2] dark:bg-[#6E6960]",
|
||||
blue: "bg-[#2563EB]",
|
||||
amber: "bg-[#F59E0B]",
|
||||
red: "bg-[#DC2626]",
|
||||
};
|
||||
|
||||
/** Per-status capsule motion (running pulses, error blinks). Honors reduced-motion. */
|
||||
export const agentStatusMotion: Record<string, string> = {
|
||||
running: "hb-pulse",
|
||||
error: "hb-blink",
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Priority colors
|
||||
|
||||
@@ -29,6 +29,15 @@ export function formatNumber(n: number): string {
|
||||
return n.toLocaleString("en-US");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a project's budget for the projects list view (IA Phase 4 — PAP-60).
|
||||
* Monthly budgets render a `/mo` suffix; lifetime budgets show the bare amount.
|
||||
*/
|
||||
export function formatProjectBudget(budget: { amountCents: number; windowKind: string }): string {
|
||||
const amount = formatCents(budget.amountCents);
|
||||
return budget.windowKind === "calendar_month_utc" ? `${amount}/mo` : amount;
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | string): string {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
|
||||
Reference in New Issue
Block a user