mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The board UI is the main daily navigation surface for agents, projects, and their related resources. > - Operators need a lightweight way to keep frequently used agents and projects close without changing company-wide ordering or ownership. > - Resource memberships already model per-user relationships to projects and agents, so they are the right place to store user-specific starred state. > - This pull request extends that membership contract with a starred timestamp and exposes star controls in list/detail views. > - The sidebar then uses those starred memberships to show compact, user-specific shortcuts. > - The benefit is faster navigation without introducing a separate favorites system or leaking preferences across users. ## Linked Issues or Issue Description No public GitHub issue exists. Feature request: ## Problem or motivation Users cannot pin frequently used agents or projects into the main sidebar. Returning to important resources requires scanning full project/agent lists or navigating through detail pages, which adds friction to repeated daily workflows. ## Proposed solution Store a per-user `starred_at` timestamp on agent and project memberships, expose API actions to set or clear that state, add star toggle controls to list/detail pages, and render starred projects and agents as compact sidebar shortcuts. ## Alternatives considered A separate favorites table would work, but it would duplicate membership scoping and require another resource relationship model. Keeping starred state on memberships preserves existing company/user boundaries and avoids a second source of truth. ## Roadmap alignment Checked `ROADMAP.md`; no overlapping planned core work for starred resource/sidebar navigation was found. ## Additional context The affected subsystems are `packages/db`, `packages/shared`, `server/`, and `ui/`. The migration is idempotent with `IF NOT EXISTS` guards so environments that saw an earlier local migration name can still apply the final ordered migration safely. ## What Changed - Added idempotent migration `0133_resource_membership_stars` for `starred_at` columns and lookup indexes on agent/project memberships. - Extended shared resource membership types and validators with starred metadata and actions. - Updated server resource membership services/routes to read and mutate starred resource state. - Added reusable star toggle UI and resource membership hook support for starred state. - Added starred projects and agents sidebar rendering, plus star controls on list and detail pages. - Added focused shared, server, and UI coverage for starred membership behavior and sidebar rendering. ## Verification - Rebased and force-with-lease pushed current PR head `a086fc965391c9e50a51b5b83b5b44a797b2a6f4` onto current `paperclipai/paperclip:master`; `gh pr view` reports `MERGEABLE` with no merge conflicts. GitHub checks are green for this fresh head. - `pnpm exec vitest run packages/shared/src/resource-memberships.test.ts server/src/__tests__/resource-memberships-routes.test.ts server/src/__tests__/workspace-runtime.test.ts ui/src/components/Sidebar.test.tsx ui/src/components/SidebarAgents.test.tsx ui/src/components/SidebarStarredProjects.test.tsx ui/src/components/StarToggle.test.tsx ui/src/pages/InstanceExperimentalSettings.test.tsx` passed after the rebase: 8 files, 143 tests. - Greptile re-review is 5/5; the remaining screenshot thread was resolved as non-blocking because this task explicitly requested no screenshots/images in the PR. - `pnpm exec vitest run ui/src/components/SidebarStarredProjects.test.tsx` passed after the mobile pending-spinner fix. - `pnpm exec vitest run packages/shared/src/resource-memberships.test.ts server/src/__tests__/resource-memberships-routes.test.ts ui/src/components/Sidebar.test.tsx ui/src/components/SidebarAgents.test.tsx ui/src/components/SidebarStarredProjects.test.tsx ui/src/components/StarToggle.test.tsx ui/src/pages/InstanceExperimentalSettings.test.tsx` passed: 7 files, 68 tests. - `pnpm --filter @paperclipai/db typecheck && pnpm --filter @paperclipai/shared typecheck && pnpm --filter @paperclipai/server typecheck && pnpm --filter @paperclipai/ui typecheck` passed db/shared/server, then failed in pre-existing UI code outside this PR: `src/pages/CompanyEnvironments.tsx` missing `@xterm/*` type declarations and `previous` possibly null. - Checked that the PR diff does not include `pnpm-lock.yaml` or `.github/workflows` changes. - Checked `ROADMAP.md` and found no overlapping planned core work for starred resource/sidebar navigation. - Searched existing GitHub PRs for duplicate starred-resource/sidebar work and found none. ## Risks - Migration touches membership tables. The SQL uses `IF NOT EXISTS` for columns and indexes so environments that saw an earlier local migration name can still apply this safely. - Sidebar ordering and visibility changes could affect users who rely on the previous flat sidebar layout. - Starred state is per-user membership metadata; code paths must continue preserving company/user scoping around memberships. > 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 GPT-5 Codex, tool-enabled coding agent with shell/GitHub access. Context window not disclosed by the runtime. ## 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 <noreply@paperclip.ing>
133 lines
4.3 KiB
TypeScript
133 lines
4.3 KiB
TypeScript
import type { MouseEvent } from "react";
|
|
import { Loader2, Star } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "../lib/utils";
|
|
|
|
export interface StarToggleProps {
|
|
/** Whether the resource is currently starred (post-optimistic value). */
|
|
starred: boolean;
|
|
/** Human-readable resource name, used for the accessible label. */
|
|
resourceName: string;
|
|
/** Optimistic mutation in flight — shows a spinner and blocks input. */
|
|
pending?: boolean;
|
|
/** Last mutation failed — surface a retry affordance (red star). */
|
|
error?: boolean;
|
|
/**
|
|
* "row" — quiet icon-only control for sidebar and browse-list rows.
|
|
* "button" — labelled Star/Starred button for detail headers.
|
|
*/
|
|
size?: "row" | "button";
|
|
/** Called with the desired next starred value. */
|
|
onToggle: (nextStarred: boolean) => void;
|
|
/**
|
|
* Row variant only: keep the control hidden at rest even when starred, so it
|
|
* only appears on hover/focus. Sidebar rows are "intentionally quiet"; browse
|
|
* rows keep the starred control visible.
|
|
*/
|
|
quiet?: boolean;
|
|
/** Extra classes for the control itself. */
|
|
className?: string;
|
|
/**
|
|
* Row variant only: classes that control at-rest visibility when the resource
|
|
* is not starred (e.g. reveal on hover/focus). Ignored when starred (a starred
|
|
* control is always visible) or on the button variant. Defaults to the shared
|
|
* unnamed-`group` reveal used by browse rows; sidebar passes a named-group
|
|
* variant so it reveals with `group/project` / `group/agent`.
|
|
*/
|
|
revealClassName?: string;
|
|
}
|
|
|
|
const DEFAULT_ROW_REVEAL =
|
|
"opacity-100 sm:opacity-0 sm:transition-opacity sm:group-hover:opacity-100 sm:group-focus-within:opacity-100";
|
|
|
|
export function StarToggle({
|
|
starred,
|
|
resourceName,
|
|
pending = false,
|
|
error = false,
|
|
size = "row",
|
|
onToggle,
|
|
quiet = false,
|
|
className,
|
|
revealClassName,
|
|
}: StarToggleProps) {
|
|
const ariaLabel = starred ? `Unstar ${resourceName}` : `Star ${resourceName}`;
|
|
const Icon = pending ? Loader2 : Star;
|
|
|
|
function handleClick(event: MouseEvent<HTMLButtonElement>) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (pending) return;
|
|
// On error, retry the last intent (toggle toward the desired end state).
|
|
onToggle(!starred);
|
|
}
|
|
|
|
if (size === "button") {
|
|
const label = pending ? "Saving..." : error ? "Retry star" : starred ? "Starred" : "Star";
|
|
return (
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
variant="outline"
|
|
aria-label={ariaLabel}
|
|
aria-pressed={starred}
|
|
aria-busy={pending ? "true" : undefined}
|
|
disabled={pending}
|
|
onClick={handleClick}
|
|
title={error ? "Couldn't save — retry" : undefined}
|
|
className={cn(
|
|
error
|
|
? "text-red-500 hover:text-red-500"
|
|
: starred
|
|
? "text-amber-600 dark:text-amber-500"
|
|
: undefined,
|
|
className,
|
|
)}
|
|
>
|
|
<Icon
|
|
className={cn(
|
|
"h-4 w-4",
|
|
pending && "motion-safe:animate-spin",
|
|
!pending && starred && !error && "fill-amber-500 text-amber-500",
|
|
!pending && error && "text-red-500",
|
|
)}
|
|
/>
|
|
<span>{label}</span>
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// Row variant: a starred (or errored) control is always visible; an unstarred
|
|
// one is quiet and revealed on hover/focus so the nav stays calm. When `quiet`
|
|
// (sidebar), even a starred control hides at rest and reveals on hover/focus.
|
|
const visible = error || pending || (starred && !quiet);
|
|
return (
|
|
<Button
|
|
type="button"
|
|
size="icon-xs"
|
|
variant="ghost"
|
|
aria-label={ariaLabel}
|
|
aria-pressed={starred}
|
|
aria-busy={pending ? "true" : undefined}
|
|
disabled={pending}
|
|
onClick={handleClick}
|
|
title={error ? "Couldn't save — retry" : undefined}
|
|
className={cn(
|
|
"h-6 w-6 shrink-0",
|
|
visible ? "opacity-100" : revealClassName ?? DEFAULT_ROW_REVEAL,
|
|
className,
|
|
)}
|
|
>
|
|
<Icon
|
|
className={cn(
|
|
"h-3.5 w-3.5",
|
|
pending && "motion-safe:animate-spin",
|
|
!pending && error && "text-red-500",
|
|
!pending && !error && starred && "fill-amber-500 text-amber-500",
|
|
!pending && !error && !starred && "text-muted-foreground",
|
|
)}
|
|
/>
|
|
</Button>
|
|
);
|
|
}
|