feat: /plugins page — agent plugin marketplace showcase#39
Conversation
- New /plugins page showcasing all 4 Recoup plugins with install commands - Plugin cards with capabilities, skill counts, audience, GitHub links - Copy-to-clipboard install commands for Claude Code/Cowork/Codex/Cursor - How it works section (Install → Connect → Go) - CTA to docs and pricing - Added Plugins to header nav and footer product section - Copy extracted to lib/copy/plugins.ts (single source of truth pattern)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a new "/plugins" page and layout to the app, introduces a copy data module ( ChangesPlugins Page Feature
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant PluginCard
participant CopyButton
participant Clipboard
User->>PluginCard: view plugin install command
User->>CopyButton: click copy
CopyButton->>Clipboard: writeText(installCommand)
CopyButton-->>User: show copied state
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
app/plugins/page.tsx (2)
215-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHardcoded brand URL for the API-key CTA.
https://developers.recoupable.comis hardcoded here; if this domain is already available viasiteConfig(as used elsewhere, e.g.siteConfig.appUrlin the header), prefer importing it fromlib/config.tsfor a single source of truth.As per coding guidelines,
**/*.{ts,tsx,js,jsx}: "Never hardcode brand values; import fromlib/config.ts".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/plugins/page.tsx` around lines 215 - 222, The API-key CTA in page.tsx is hardcoding the brand URL instead of using the shared config source. Update the anchor in the component that renders “Get API Key” to pull the developers URL from siteConfig or another exported value from lib/config.ts, matching how siteConfig.appUrl is used elsewhere, so the CTA stays consistent and centrally managed.Source: Coding guidelines
1-1: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftEntire page marked
"use client"— unnecessarily large RSC payload.Only
useReveal(IntersectionObserver) andCopyButton(clipboard) require client-side interactivity. The hero, plugin grid text, and CTA content are static and could be rendered server-side, keeping the page.tsx as a server component and isolating client behavior in small leaf components. Per the guideline's priority list, "minimal RSC payload" is called out explicitly — bundling the entire static page into client JS works against that.As per coding guidelines,
{app,components}/**/*.{tsx,ts,jsx,js}: "follow Vercel React best practices with priorities: no barrel imports, no async waterfalls, minimal RSC payload, safe localStorage usage."Also applies to: 146-234
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/plugins/page.tsx` at line 1, The entire page is marked as a client component, but only the interactive pieces need client-side behavior. Remove the top-level "use client" from page.tsx so the page can stay a server component, and extract the interactive logic in useReveal and CopyButton into small client-only leaf components. Keep the hero, plugin grid, and CTA content server-rendered to minimize the RSC payload and follow the app/components best-practice guidance.Source: Coding guidelines
lib/copy/plugins.ts (1)
17-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winData shape and content look solid; minor brand-hardcoding concern.
The
pluginsCopystructure is well-typed (satisfies Plugin[]) and centralizes copy as intended. However, repo URLs point togithub.com/recoupable/...and the step description referencesdevelopers.recoupable.comdirectly. Iflib/config.tsexposes brand/org identifiers (e.g., a GitHub org or docs domain), these should be composed from that config rather than hardcoded strings, so a future brand/domain change doesn't require hunting through copy files.As per coding guidelines,
**/*.{ts,tsx,js,jsx}: "Never hardcode brand values; import fromlib/config.ts".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/copy/plugins.ts` around lines 17 - 125, The `pluginsCopy` object hardcodes brand and destination URLs in the plugin entries and onboarding steps. Update `pluginsCopy` to source the GitHub org/repo base and docs domain from `lib/config.ts` (or other existing config symbols) instead of embedding `recoupable` and `developers.recoupable.com` directly. Keep the same `pluginsCopy` structure and replace the string literals in the `installCommand`, `repo`, and step copy with values composed from the shared config.Source: Coding guidelines
app/plugins/layout.tsx (1)
4-9: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winTitle is well below the recommended SEO length.
"Agent Plugins for Music | Recoup" is ~32 characters, under the 50-60 char guidance for page titles; the description (~154 chars) is within range. Consider padding the title with more descriptive keywords (e.g., naming the plugins or "Claude, Cursor, Codex" compatibility) to hit the target length and improve SEO/CTR.
As per coding guidelines,
{app/**/page.{tsx,jsx},content/posts/*.mdx}: "Every page must have a unique title (50-60 chars) and description (120-160 chars) for SEO."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/plugins/layout.tsx` around lines 4 - 9, The page metadata title in the exported metadata object is too short for the SEO requirement and should be expanded to the 50–60 character range. Update the title in buildPageMetadata for the plugins page to include more descriptive, relevant keywords such as the supported tools or plugin capabilities, while keeping it unique and aligned with the existing description.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/plugins/page.tsx`:
- Around line 1-234: The page file currently mixes five separate
responsibilities (`useReveal`, `CopyButton`, `PluginCard`, `StepCard`, and
`PluginsPage`), which violates the one-function-per-file rule. Split each symbol
into its own module, then import them back into the page component so
`PluginsPage` only composes the UI. Keep the hook and each card/button component
in dedicated files to preserve single responsibility and make them easier to
reuse and test.
- Around line 37-56: The CopyButton component is optimistically setting copied
state without handling clipboard failures, and the icon-only button relies on
title instead of an accessible label. Update the onClick handler in CopyButton
to await navigator.clipboard.writeText and only set copied when it succeeds,
with failure handling that avoids false success state. Also add an aria-label to
the button so screen readers can announce the action clearly, while keeping the
existing visual icon behavior.
---
Nitpick comments:
In `@app/plugins/layout.tsx`:
- Around line 4-9: The page metadata title in the exported metadata object is
too short for the SEO requirement and should be expanded to the 50–60 character
range. Update the title in buildPageMetadata for the plugins page to include
more descriptive, relevant keywords such as the supported tools or plugin
capabilities, while keeping it unique and aligned with the existing description.
In `@app/plugins/page.tsx`:
- Around line 215-222: The API-key CTA in page.tsx is hardcoding the brand URL
instead of using the shared config source. Update the anchor in the component
that renders “Get API Key” to pull the developers URL from siteConfig or another
exported value from lib/config.ts, matching how siteConfig.appUrl is used
elsewhere, so the CTA stays consistent and centrally managed.
- Line 1: The entire page is marked as a client component, but only the
interactive pieces need client-side behavior. Remove the top-level "use client"
from page.tsx so the page can stay a server component, and extract the
interactive logic in useReveal and CopyButton into small client-only leaf
components. Keep the hero, plugin grid, and CTA content server-rendered to
minimize the RSC payload and follow the app/components best-practice guidance.
In `@lib/copy/plugins.ts`:
- Around line 17-125: The `pluginsCopy` object hardcodes brand and destination
URLs in the plugin entries and onboarding steps. Update `pluginsCopy` to source
the GitHub org/repo base and docs domain from `lib/config.ts` (or other existing
config symbols) instead of embedding `recoupable` and
`developers.recoupable.com` directly. Keep the same `pluginsCopy` structure and
replace the string literals in the `installCommand`, `repo`, and step copy with
values composed from the shared config.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bf90432e-8219-404e-8a69-855f9a8b66c9
📒 Files selected for processing (4)
app/plugins/layout.tsxapp/plugins/page.tsxlib/copy/plugins.tslib/nav.ts
| "use client"; | ||
|
|
||
| import { useState, useEffect, useRef } from "react"; | ||
| import Link from "next/link"; | ||
| import { ArrowRight, Copy, Check, Terminal } from "lucide-react"; | ||
| import { pluginsCopy } from "@/lib/copy/plugins"; | ||
| import type { Plugin } from "@/lib/copy/plugins"; | ||
|
|
||
| /* ── reveal-on-scroll ── */ | ||
| function useReveal() { | ||
| const ref = useRef<HTMLDivElement>(null); | ||
| const [v, setV] = useState(false); | ||
| useEffect(() => { | ||
| const el = ref.current; | ||
| if (!el) return; | ||
| const io = new IntersectionObserver( | ||
| ([e]) => { | ||
| if (e.isIntersecting) { | ||
| setV(true); | ||
| io.disconnect(); | ||
| } | ||
| }, | ||
| { threshold: 0.08 }, | ||
| ); | ||
| io.observe(el); | ||
| return () => io.disconnect(); | ||
| }, []); | ||
| return { | ||
| ref, | ||
| cls: `transition-all duration-[900ms] ease-[cubic-bezier(.16,1,.3,1)] ${ | ||
| v ? "opacity-100 translate-y-0" : "opacity-0 translate-y-10" | ||
| }`, | ||
| }; | ||
| } | ||
|
|
||
| /* ── copy button ── */ | ||
| function CopyButton({ text }: { text: string }) { | ||
| const [copied, setCopied] = useState(false); | ||
| return ( | ||
| <button | ||
| onClick={() => { | ||
| navigator.clipboard.writeText(text); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| }} | ||
| className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0" | ||
| title="Copy install command" | ||
| > | ||
| {copied ? ( | ||
| <Check className="w-3.5 h-3.5 text-green-400" /> | ||
| ) : ( | ||
| <Copy className="w-3.5 h-3.5 text-neutral-500" /> | ||
| )} | ||
| </button> | ||
| ); | ||
| } | ||
|
|
||
| /* ── plugin card ── */ | ||
| function PluginCard({ plugin, index }: { plugin: Plugin; index: number }) { | ||
| const { ref, cls } = useReveal(); | ||
| return ( | ||
| <div | ||
| ref={ref} | ||
| className={cls} | ||
| style={{ transitionDelay: `${index * 120}ms` }} | ||
| > | ||
| <div className="h-full rounded-xl bg-neutral-900/50 shadow-[0_0_0_1px_rgba(255,255,255,0.06)] p-6 sm:p-8 flex flex-col"> | ||
| {/* Header */} | ||
| <div className="flex items-start justify-between mb-4"> | ||
| <div> | ||
| <h3 className="text-xl font-semibold text-white">{plugin.name}</h3> | ||
| <span className="text-xs text-neutral-500 mt-1 block"> | ||
| {plugin.skillCount} skill{plugin.skillCount !== 1 ? "s" : ""} | ||
| {" · "} | ||
| {plugin.audience} | ||
| </span> | ||
| </div> | ||
| <a | ||
| href={plugin.repo} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="text-xs text-neutral-500 hover:text-neutral-300 transition-colors" | ||
| > | ||
| GitHub → | ||
| </a> | ||
| </div> | ||
|
|
||
| {/* Description */} | ||
| <p className="text-sm text-neutral-400 leading-relaxed mb-5"> | ||
| {plugin.description} | ||
| </p> | ||
|
|
||
| {/* Capabilities */} | ||
| <ul className="space-y-1.5 mb-6 flex-1"> | ||
| {plugin.capabilities.map((cap) => ( | ||
| <li key={cap} className="flex items-start gap-2 text-sm"> | ||
| <Check className="w-3.5 h-3.5 text-green-400 mt-0.5 shrink-0" /> | ||
| <span className="text-neutral-300">{cap}</span> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
|
|
||
| {/* Install command */} | ||
| <div className="flex items-center gap-2 rounded-lg bg-black/60 px-3 py-2.5 font-mono text-xs text-neutral-400 overflow-hidden"> | ||
| <Terminal className="w-3.5 h-3.5 shrink-0 text-neutral-600" /> | ||
| <code className="truncate flex-1">{plugin.installCommand}</code> | ||
| <CopyButton text={plugin.installCommand} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| /* ── step card ── */ | ||
| function StepCard({ | ||
| step, | ||
| index, | ||
| }: { | ||
| step: (typeof pluginsCopy.steps)[number]; | ||
| index: number; | ||
| }) { | ||
| const { ref, cls } = useReveal(); | ||
| return ( | ||
| <div | ||
| ref={ref} | ||
| className={cls} | ||
| style={{ transitionDelay: `${index * 120}ms` }} | ||
| > | ||
| <div className="text-center"> | ||
| <div | ||
| className="text-4xl font-bold text-white/10 mb-3" | ||
| style={{ fontFamily: "var(--font-bitmap), monospace" }} | ||
| > | ||
| {step.number} | ||
| </div> | ||
| <h3 className="text-lg font-semibold text-white mb-2">{step.title}</h3> | ||
| <p className="text-sm text-neutral-400 leading-relaxed"> | ||
| {step.description} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| /* ── page ── */ | ||
| export default function PluginsPage() { | ||
| const c = pluginsCopy; | ||
|
|
||
| return ( | ||
| <div className="max-w-5xl mx-auto px-6 py-20"> | ||
| {/* Hero */} | ||
| <header className="text-center mb-20"> | ||
| <div className="inline-block rounded-full border border-white/10 px-4 py-1.5 text-xs font-medium tracking-wide uppercase mb-6 text-neutral-400"> | ||
| Plugin Marketplace | ||
| </div> | ||
| <h1 | ||
| className="text-[clamp(2rem,5vw,3.5rem)] font-bold tracking-tight leading-[1.1] mb-4 text-white" | ||
| style={{ fontFamily: "var(--font-bitmap), monospace" }} | ||
| > | ||
| {c.title} | ||
| </h1> | ||
| <p className="text-[15px] text-neutral-400 max-w-2xl mx-auto leading-relaxed"> | ||
| {c.subtitle} | ||
| </p> | ||
|
|
||
| {/* Compatible-with badges */} | ||
| <div className="flex items-center justify-center gap-3 mt-8 flex-wrap"> | ||
| {c.compatibleWith.map((tool) => ( | ||
| <span | ||
| key={tool} | ||
| className="rounded-full bg-white/5 px-3 py-1 text-xs text-neutral-400 border border-white/5" | ||
| > | ||
| {tool} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </header> | ||
|
|
||
| {/* Plugin grid */} | ||
| <section className="grid gap-6 sm:grid-cols-2 mb-24"> | ||
| {c.plugins.map((plugin, i) => ( | ||
| <PluginCard key={plugin.id} plugin={plugin} index={i} /> | ||
| ))} | ||
| </section> | ||
|
|
||
| {/* How it works */} | ||
| <section className="mb-24"> | ||
| <h2 | ||
| className="text-2xl font-bold text-white text-center mb-12" | ||
| style={{ fontFamily: "var(--font-bitmap), monospace" }} | ||
| > | ||
| How it works | ||
| </h2> | ||
| <div className="grid gap-12 sm:grid-cols-3"> | ||
| {c.steps.map((step, i) => ( | ||
| <StepCard key={step.number} step={step} index={i} /> | ||
| ))} | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* CTA */} | ||
| <section className="text-center"> | ||
| <div className="rounded-xl bg-neutral-900/50 shadow-[0_0_0_1px_rgba(255,255,255,0.06)] px-8 py-12"> | ||
| <h2 | ||
| className="text-2xl font-bold text-white mb-3" | ||
| style={{ fontFamily: "var(--font-bitmap), monospace" }} | ||
| > | ||
| Ready to plug in? | ||
| </h2> | ||
| <p className="text-sm text-neutral-400 mb-8 max-w-md mx-auto"> | ||
| Get your API key, install a plugin, and let your AI agent run your | ||
| music business. | ||
| </p> | ||
| <div className="flex items-center justify-center gap-4 flex-wrap"> | ||
| <a | ||
| href="https://developers.recoupable.com" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-2 bg-white text-black px-5 py-2.5 rounded-md text-sm font-medium hover:bg-neutral-200 transition-colors" | ||
| > | ||
| Get API Key <ArrowRight className="w-4 h-4" /> | ||
| </a> | ||
| <Link | ||
| href="/pricing" | ||
| className="inline-flex items-center gap-2 border border-white/10 text-white px-5 py-2.5 rounded-md text-sm font-medium hover:bg-white/5 transition-colors" | ||
| > | ||
| View Pricing | ||
| </Link> | ||
| </div> | ||
| </div> | ||
| </section> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Multiple functions in a single file violates the one-function-per-file rule.
This file defines useReveal, CopyButton, PluginCard, StepCard, and PluginsPage — five distinct responsibilities in one file. Extracting each into its own module (e.g., a useReveal hook file, CopyButton.tsx, PluginCard.tsx, StepCard.tsx) would align with the project's SRP convention and make each piece independently testable/reusable.
As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Implement Single Responsibility Principle: one function per file."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/plugins/page.tsx` around lines 1 - 234, The page file currently mixes
five separate responsibilities (`useReveal`, `CopyButton`, `PluginCard`,
`StepCard`, and `PluginsPage`), which violates the one-function-per-file rule.
Split each symbol into its own module, then import them back into the page
component so `PluginsPage` only composes the UI. Keep the hook and each
card/button component in dedicated files to preserve single responsibility and
make them easier to reuse and test.
Source: Coding guidelines
| function CopyButton({ text }: { text: string }) { | ||
| const [copied, setCopied] = useState(false); | ||
| return ( | ||
| <button | ||
| onClick={() => { | ||
| navigator.clipboard.writeText(text); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| }} | ||
| className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0" | ||
| title="Copy install command" | ||
| > | ||
| {copied ? ( | ||
| <Check className="w-3.5 h-3.5 text-green-400" /> | ||
| ) : ( | ||
| <Copy className="w-3.5 h-3.5 text-neutral-500" /> | ||
| )} | ||
| </button> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
CopyButton: unhandled clipboard rejection and weak a11y label.
navigator.clipboard.writeText returns a promise that can reject (e.g., insecure context, permission denial) — currently unhandled, so copied state is set optimistically even on failure. Also, title alone isn't reliably announced by screen readers; consider aria-label for the icon-only button.
🛡️ Proposed fix
<button
onClick={() => {
- navigator.clipboard.writeText(text);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ navigator.clipboard
+ .writeText(text)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(() => {});
}}
className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0"
title="Copy install command"
+ aria-label="Copy install command"
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function CopyButton({ text }: { text: string }) { | |
| const [copied, setCopied] = useState(false); | |
| return ( | |
| <button | |
| onClick={() => { | |
| navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }} | |
| className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0" | |
| title="Copy install command" | |
| > | |
| {copied ? ( | |
| <Check className="w-3.5 h-3.5 text-green-400" /> | |
| ) : ( | |
| <Copy className="w-3.5 h-3.5 text-neutral-500" /> | |
| )} | |
| </button> | |
| ); | |
| } | |
| function CopyButton({ text }: { text: string }) { | |
| const [copied, setCopied] = useState(false); | |
| return ( | |
| <button | |
| onClick={() => { | |
| navigator.clipboard | |
| .writeText(text) | |
| .then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }) | |
| .catch(() => {}); | |
| }} | |
| className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0" | |
| title="Copy install command" | |
| aria-label="Copy install command" | |
| > | |
| {copied ? ( | |
| <Check className="w-3.5 h-3.5 text-green-400" /> | |
| ) : ( | |
| <Copy className="w-3.5 h-3.5 text-neutral-500" /> | |
| )} | |
| </button> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/plugins/page.tsx` around lines 37 - 56, The CopyButton component is
optimistically setting copied state without handling clipboard failures, and the
icon-only button relies on title instead of an accessible label. Update the
onClick handler in CopyButton to await navigator.clipboard.writeText and only
set copied when it succeeds, with failure handling that avoids false success
state. Also add an aria-label to the button so screen readers can announce the
action clearly, while keeping the existing visual icon behavior.
There was a problem hiding this comment.
6 issues found across 4 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/plugins/page.tsx">
<violation number="1" location="app/plugins/page.tsx:10">
P2: The `useReveal` hook is duplicated from `app/results/page.tsx` (same IntersectionObserver logic, same animation class string, same cubic-bezier timing). Since this is used by at least two pages now, consider extracting it into a shared hook at `components/hooks/useReveal.ts` (or similar) and importing it in both places to keep animations consistent and avoid drift.</violation>
<violation number="2" location="app/plugins/page.tsx:42">
P2: Wrap `navigator.clipboard.writeText` in a try/catch or check `navigator.clipboard?.writeText` — the call throws in insecure contexts (HTTP pages, some iframe sandbox modes) and older browsers, silently breaking the copy action with no user feedback.</violation>
<violation number="3" location="app/plugins/page.tsx:44">
P3: Clean up the 2000ms timeout when `CopyButton` unmounts. If the component unmounts while the "copied" feedback is shown, the stale timeout calls `setCopied(false)` on an unmounted component.</violation>
<violation number="4" location="app/plugins/page.tsx:46">
P3: Add `aria-label` to the copy button — the `title` attribute alone isn't reliably exposed by all screen readers as an accessible name. An `aria-label="Copy install command"` ensures assistive tech users can identify the button target.</violation>
</file>
<file name="lib/copy/plugins.ts">
<violation number="1" location="lib/copy/plugins.ts:64">
P1: Wrong `skillCount` for the Catalog Deals plugin: declared as 17, but the `capabilities` array only has 8 entries. Visitors will see "17 skills" on the card and then scroll through 8 listed items — this creates a trust issue and suggests the data is stale or the count was aspirational. Either update the capabilities list to match the true count, or correct skillCount to match the displayed list.</violation>
<violation number="2" location="lib/copy/plugins.ts:85">
P1: Wrong `skillCount` for the Platform plugin: declared as 2, but its `capabilities` list has 4 items (Guided onboarding flow, API key validation, Plugin discovery & recommendations, Health check & diagnostics). This value is shown directly on the card as "4 skills" — so the page will display "2 skills" while listing 4 capabilities, which looks inconsistent. Same issue exists for Catalog Deals (declared 17, listed 8).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| name: "Catalog Deals", | ||
| description: | ||
| "Turn a messy seller data room into a source-cited deal package. Royalty normalization, rights review, valuation analysis, financing underwriting, and IC memos — all agent-driven.", | ||
| skillCount: 17, |
There was a problem hiding this comment.
P1: Wrong skillCount for the Catalog Deals plugin: declared as 17, but the capabilities array only has 8 entries. Visitors will see "17 skills" on the card and then scroll through 8 listed items — this creates a trust issue and suggests the data is stale or the count was aspirational. Either update the capabilities list to match the true count, or correct skillCount to match the displayed list.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/copy/plugins.ts, line 64:
<comment>Wrong `skillCount` for the Catalog Deals plugin: declared as 17, but the `capabilities` array only has 8 entries. Visitors will see "17 skills" on the card and then scroll through 8 listed items — this creates a trust issue and suggests the data is stale or the count was aspirational. Either update the capabilities list to match the true count, or correct skillCount to match the displayed list.</comment>
<file context>
@@ -0,0 +1,125 @@
+ name: "Catalog Deals",
+ description:
+ "Turn a messy seller data room into a source-cited deal package. Royalty normalization, rights review, valuation analysis, financing underwriting, and IC memos — all agent-driven.",
+ skillCount: 17,
+ capabilities: [
+ "Data room ingestion & normalization",
</file context>
| name: "Platform", | ||
| description: | ||
| "Onboarding and diagnostics. Your agent's starting point — validates credentials, discovers capabilities, and runs health checks.", | ||
| skillCount: 2, |
There was a problem hiding this comment.
P1: Wrong skillCount for the Platform plugin: declared as 2, but its capabilities list has 4 items (Guided onboarding flow, API key validation, Plugin discovery & recommendations, Health check & diagnostics). This value is shown directly on the card as "4 skills" — so the page will display "2 skills" while listing 4 capabilities, which looks inconsistent. Same issue exists for Catalog Deals (declared 17, listed 8).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/copy/plugins.ts, line 85:
<comment>Wrong `skillCount` for the Platform plugin: declared as 2, but its `capabilities` list has 4 items (Guided onboarding flow, API key validation, Plugin discovery & recommendations, Health check & diagnostics). This value is shown directly on the card as "4 skills" — so the page will display "2 skills" while listing 4 capabilities, which looks inconsistent. Same issue exists for Catalog Deals (declared 17, listed 8).</comment>
<file context>
@@ -0,0 +1,125 @@
+ name: "Platform",
+ description:
+ "Onboarding and diagnostics. Your agent's starting point — validates credentials, discovers capabilities, and runs health checks.",
+ skillCount: 2,
+ capabilities: [
+ "Guided onboarding flow",
</file context>
| return ( | ||
| <button | ||
| onClick={() => { | ||
| navigator.clipboard.writeText(text); |
There was a problem hiding this comment.
P2: Wrap navigator.clipboard.writeText in a try/catch or check navigator.clipboard?.writeText — the call throws in insecure contexts (HTTP pages, some iframe sandbox modes) and older browsers, silently breaking the copy action with no user feedback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/plugins/page.tsx, line 42:
<comment>Wrap `navigator.clipboard.writeText` in a try/catch or check `navigator.clipboard?.writeText` — the call throws in insecure contexts (HTTP pages, some iframe sandbox modes) and older browsers, silently breaking the copy action with no user feedback.</comment>
<file context>
@@ -0,0 +1,234 @@
+ return (
+ <button
+ onClick={() => {
+ navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
</file context>
| import type { Plugin } from "@/lib/copy/plugins"; | ||
|
|
||
| /* ── reveal-on-scroll ── */ | ||
| function useReveal() { |
There was a problem hiding this comment.
P2: The useReveal hook is duplicated from app/results/page.tsx (same IntersectionObserver logic, same animation class string, same cubic-bezier timing). Since this is used by at least two pages now, consider extracting it into a shared hook at components/hooks/useReveal.ts (or similar) and importing it in both places to keep animations consistent and avoid drift.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/plugins/page.tsx, line 10:
<comment>The `useReveal` hook is duplicated from `app/results/page.tsx` (same IntersectionObserver logic, same animation class string, same cubic-bezier timing). Since this is used by at least two pages now, consider extracting it into a shared hook at `components/hooks/useReveal.ts` (or similar) and importing it in both places to keep animations consistent and avoid drift.</comment>
<file context>
@@ -0,0 +1,234 @@
+import type { Plugin } from "@/lib/copy/plugins";
+
+/* ── reveal-on-scroll ── */
+function useReveal() {
+ const ref = useRef<HTMLDivElement>(null);
+ const [v, setV] = useState(false);
</file context>
| onClick={() => { | ||
| navigator.clipboard.writeText(text); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); |
There was a problem hiding this comment.
P3: Clean up the 2000ms timeout when CopyButton unmounts. If the component unmounts while the "copied" feedback is shown, the stale timeout calls setCopied(false) on an unmounted component.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/plugins/page.tsx, line 44:
<comment>Clean up the 2000ms timeout when `CopyButton` unmounts. If the component unmounts while the "copied" feedback is shown, the stale timeout calls `setCopied(false)` on an unmounted component.</comment>
<file context>
@@ -0,0 +1,234 @@
+ onClick={() => {
+ navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }}
+ className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0"
</file context>
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); | ||
| }} | ||
| className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0" |
There was a problem hiding this comment.
P3: Add aria-label to the copy button — the title attribute alone isn't reliably exposed by all screen readers as an accessible name. An aria-label="Copy install command" ensures assistive tech users can identify the button target.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/plugins/page.tsx, line 46:
<comment>Add `aria-label` to the copy button — the `title` attribute alone isn't reliably exposed by all screen readers as an accessible name. An `aria-label="Copy install command"` ensures assistive tech users can identify the button target.</comment>
<file context>
@@ -0,0 +1,234 @@
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }}
+ className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0"
+ title="Copy install command"
+ >
</file context>
What
New
/pluginspage showcasing all 4 Recoup plugins with one-click install commands.Why
Recoup's BYOA strategy means plugins ARE the product — but the marketing site had no page showcasing them. Customers discovering Recoup had no clear path to 'install this into your AI tool.' This fills that gap.
What's on the page
Nav changes
Technical
lib/copy/plugins.ts(single source of truth pattern)buildPageMetadataBuilt by Cosmo 🌀 — Nightly Build July 5, 2026
Summary by cubic
Added a new
/pluginspage that showcases all four Recoup agent plugins with one-command installs and clear setup steps. Also adds “Plugins” to the site nav for easier discovery.New Features
/pluginsmarketplace with hero and compatibility badges (Claude Code, Cowork, Codex, Cursor).Refactors
lib/copy/plugins.ts; SEO metadata viabuildPageMetadata.Written for commit b62ede1. Summary will update on new commits.
Summary by CodeRabbit