Skip to content

feat: /plugins page — agent plugin marketplace showcase#39

Open
sidneyswift wants to merge 1 commit into
mainfrom
feat/plugins-page
Open

feat: /plugins page — agent plugin marketplace showcase#39
sidneyswift wants to merge 1 commit into
mainfrom
feat/plugins-page

Conversation

@sidneyswift

@sidneyswift sidneyswift commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

What

New /plugins page 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

  • Hero with compatibility badges (Claude Code, Cowork, Codex, Cursor)
  • 4 plugin cards — Research (7 skills), Content (3), Catalog Deals (17), Platform (2)
  • Each card: description, capabilities list, audience, GitHub link, copyable install command
  • How it works — 3-step flow (Install → Connect → Go)
  • CTA — links to docs (API key) and pricing

Nav changes

  • Added 'Plugins' as first item in header nav
  • Added 'Plugins' to footer product section

Technical

  • Copy in lib/copy/plugins.ts (single source of truth pattern)
  • Layout with SEO metadata via buildPageMetadata
  • Client component with reveal-on-scroll animations (same pattern as /results)
  • Compiles clean (pre-existing TS error in demos page is unrelated)

Built by Cosmo 🌀 — Nightly Build July 5, 2026


Summary by cubic

Added a new /plugins page 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

    • /plugins marketplace with hero and compatibility badges (Claude Code, Cowork, Codex, Cursor).
    • Four plugin cards (Research, Content, Catalog Deals, Platform) with capabilities, audience, GitHub link, and copy-to-clipboard install command.
    • “How it works” (Install → Connect → Go) and CTAs to docs (API key) and pricing; added “Plugins” to header and footer nav.
  • Refactors

    • Centralized copy in lib/copy/plugins.ts; SEO metadata via buildPageMetadata.
    • Client component with reveal-on-scroll animations; no API changes.

Written for commit b62ede1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a new Plugins page with a marketplace-style layout, plugin cards, setup steps, and calls to action.
    • Added clipboard copy support for plugin install commands with a brief success state.
    • Added a new layout and page metadata for the Plugins section.
  • Navigation
    • Added Plugins links to the main header and footer navigation.
  • Content
    • Added structured page copy for plugin details, compatibility info, and install workflow.

- 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)
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marketing Ready Ready Preview Jul 6, 2026 12:56am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new "/plugins" page and layout to the app, introduces a copy data module (Plugin interface and pluginsCopy) defining plugin listings and workflow steps, and updates navigation (header and footer) to include links to the new Plugins page.

Changes

Plugins Page Feature

Layer / File(s) Summary
Plugin copy content
lib/copy/plugins.ts
Defines Plugin interface and pluginsCopy constant with page copy, plugin entries, compatibility targets, and install workflow steps.
Page layout and metadata
app/plugins/layout.tsx
Adds metadata via buildPageMetadata and a default PluginsLayout component rendering children.
Plugins page UI
app/plugins/page.tsx
Adds useReveal scroll animation hook, CopyButton clipboard component, PluginCard, StepCard, and the composed PluginsPage with hero, plugin grid, steps, and CTA sections.
Navigation links
lib/nav.ts
Adds Plugins entries to header nav and footerNav.product.items, both pointing to /plugins.

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
Loading

Possibly related PRs

  • recoupable/marketing#1: Both PRs modify the shared navigation configuration (lib/nav.ts), adding entries to the same nav structure.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the new /plugins plugin marketplace page introduced by the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugins-page
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/plugins-page

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
app/plugins/page.tsx (2)

215-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded brand URL for the API-key CTA.

https://developers.recoupable.com is hardcoded here; if this domain is already available via siteConfig (as used elsewhere, e.g. siteConfig.appUrl in the header), prefer importing it from lib/config.ts for a single source of truth.

As per coding guidelines, **/*.{ts,tsx,js,jsx}: "Never hardcode brand values; import from lib/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 lift

Entire page marked "use client" — unnecessarily large RSC payload.

Only useReveal (IntersectionObserver) and CopyButton (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 win

Data shape and content look solid; minor brand-hardcoding concern.

The pluginsCopy structure is well-typed (satisfies Plugin[]) and centralizes copy as intended. However, repo URLs point to github.com/recoupable/... and the step description references developers.recoupable.com directly. If lib/config.ts exposes 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 from lib/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 win

Title 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

📥 Commits

Reviewing files that changed from the base of the PR and between f9862ee and b62ede1.

📒 Files selected for processing (4)
  • app/plugins/layout.tsx
  • app/plugins/page.tsx
  • lib/copy/plugins.ts
  • lib/nav.ts

Comment thread app/plugins/page.tsx
Comment on lines +1 to +234
"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>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Comment thread app/plugins/page.tsx
Comment on lines +37 to +56
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>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread lib/copy/plugins.ts
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread lib/copy/plugins.ts
name: "Platform",
description:
"Onboarding and diagnostics. Your agent's starting point — validates credentials, discovers capabilities, and runs health checks.",
skillCount: 2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread app/plugins/page.tsx
return (
<button
onClick={() => {
navigator.clipboard.writeText(text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread app/plugins/page.tsx
import type { Plugin } from "@/lib/copy/plugins";

/* ── reveal-on-scroll ── */
function useReveal() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread app/plugins/page.tsx
onClick={() => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread app/plugins/page.tsx
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
className="ml-2 p-1 rounded hover:bg-white/10 transition-colors shrink-0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant