From dff8524d3d212f09465eb8b2d00f706cdafbafac Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 22:10:01 -0300 Subject: [PATCH 1/4] refactor(agent-skills): rewrite dashboard page as server wrapper + client --- .../dashboard/agent-skills/page.tsx | 173 +----------------- 1 file changed, 3 insertions(+), 170 deletions(-) diff --git a/src/app/(dashboard)/dashboard/agent-skills/page.tsx b/src/app/(dashboard)/dashboard/agent-skills/page.tsx index af5de938a3..5dc69cd1e0 100644 --- a/src/app/(dashboard)/dashboard/agent-skills/page.tsx +++ b/src/app/(dashboard)/dashboard/agent-skills/page.tsx @@ -1,172 +1,5 @@ -"use client"; +import { AgentSkillsPageClient } from "./AgentSkillsPageClient"; -import { useTranslations } from "next-intl"; -import { - AGENT_SKILLS, - AGENT_SKILLS_REPO_URL, - getAgentSkillRawUrl, - getAgentSkillBlobUrl, - type AgentSkill, -} from "@/shared/constants/agentSkills"; -import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; - -function CopyButton({ url }: { url: string }) { - const t = useTranslations("agents"); - const { copied, copy } = useCopyToClipboard(); - const isCopied = copied === url; - - return ( - - ); -} - -function SkillRow({ skill }: { skill: AgentSkill }) { - const t = useTranslations("agents"); - const rawUrl = getAgentSkillRawUrl(skill.id); - const blobUrl = getAgentSkillBlobUrl(skill.id); - - return ( -
-
- {skill.icon} -
- -
-
- {skill.name} - {skill.isEntry && ( - - {t("startHere")} - - )} - {skill.isNew && ( - - {t("badgeNew")} - - )} - {skill.endpoint && ( - - {skill.endpoint} - - )} -
-

{skill.description}

-
- -
- - open_in_new - - -
-
- ); -} - -function SkillSection({ - title, - subtitle, - icon, - skills, -}: { - title: string; - subtitle: string; - icon: string; - skills: AgentSkill[]; -}) { - return ( -
-
- {icon} -
-

{title}

-

{subtitle}

-
-
-
- {skills.map((skill) => ( - - ))} -
-
- ); -} - -export default function AgentSkillsPage() { - const t = useTranslations("agents"); - const apiSkills = AGENT_SKILLS.filter((s) => s.category === "api"); - const cliSkills = AGENT_SKILLS.filter((s) => s.category === "cli"); - - return ( -
- {/* How to use — full width */} -
-
- info - {t("howToUse")} -
-
    -
  1. - 1.{" "} - {t.rich("howToUseStep1", { - copyUrl: t("copyUrl"), - bold: (chunks) => {chunks}, - })} -
  2. -
  3. - 2. {t("howToUseStep2")} -
    - - {t("howToUseStep2Code")} - -
  4. -
  5. 3. {t("howToUseStep3")}
  6. -
- - open_in_new - {t("browseAllSkillsOnGithub")} - -
- - {/* Two-column grid: API Skills | CLI Skills */} -
- - -
-
- ); +export default function Page() { + return ; } From 1aaf43d89efb0e3c351a4c8db9c438475aa61d0e Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 27 May 2026 22:10:08 -0300 Subject: [PATCH 2/4] feat(agent-skills): add SkillCard, SkillPreviewPane, CoverageBar, McpA2aLinksBar --- .../agent-skills/components/CoverageBar.tsx | 77 +++++++++ .../components/McpA2aLinksBar.tsx | 93 +++++++++++ .../agent-skills/components/SkillCard.tsx | 105 +++++++++++++ .../components/SkillPreviewPane.tsx | 147 ++++++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx create mode 100644 src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx create mode 100644 src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx create mode 100644 src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx new file mode 100644 index 0000000000..07846e8240 --- /dev/null +++ b/src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { SkillCoverage } from "@/lib/agentSkills/types"; + +interface CoverageBarProps { + coverage: SkillCoverage; +} + +function barColor(have: number, total: number): string { + const pct = total > 0 ? have / total : 0; + if (pct >= 1) return "bg-emerald-500"; + if (pct >= 0.75) return "bg-amber-400"; + return "bg-red-500"; +} + +function trackColor(have: number, total: number): string { + const pct = total > 0 ? have / total : 0; + if (pct >= 1) return "bg-emerald-500/20"; + if (pct >= 0.75) return "bg-amber-400/20"; + return "bg-red-500/20"; +} + +export function CoverageBar({ coverage }: CoverageBarProps): JSX.Element { + const t = useTranslations("agentSkills"); + const { api, cli } = coverage; + + return ( +
+
+ + {t("categoryApi")} {api.have}/{api.total} + +
+
0 ? (api.have / api.total) * 100 : 0}%` }} + /> +
+ + {api.total > 0 ? Math.round((api.have / api.total) * 100) : 0}% + +
+ +
+ + {t("categoryCli")} {cli.have}/{cli.total} + +
+
0 ? (cli.have / cli.total) * 100 : 0}%` }} + /> +
+ + {cli.total > 0 ? Math.round((cli.have / cli.total) * 100) : 0}% + +
+
+ ); +} + +export default CoverageBar; diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx new file mode 100644 index 0000000000..cddb9cb6dd --- /dev/null +++ b/src/app/(dashboard)/dashboard/agent-skills/components/McpA2aLinksBar.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useCallback, useState, useSyncExternalStore } from "react"; +import { useTranslations } from "next-intl"; + +// SSR-safe origin via useSyncExternalStore. +// Server snapshot returns "" (avoids hydration mismatch). +function useOrigin(): string { + return useSyncExternalStore( + () => () => {}, // no external subscription needed + () => (typeof window !== "undefined" ? window.location.origin : ""), + () => "", // server snapshot + ); +} + +interface LinkCardProps { + label: string; + url: string; + icon: string; + prompt: string; +} + +function LinkCard({ label, url, icon, prompt }: LinkCardProps): JSX.Element { + const t = useTranslations("agentSkills"); + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(url); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + // clipboard not available — silently ignore + } + }, [url]); + + return ( +
+
+ {icon} +
+
+
+ {label} + +
+ {url} +

{prompt}

+
+
+ ); +} + +export function McpA2aLinksBar(): JSX.Element { + const t = useTranslations("agentSkills"); + const origin = useOrigin(); + + const mcpUrl = origin ? `${origin}/api/mcp/sse` : "/api/mcp/sse"; + const a2aUrl = origin ? `${origin}/.well-known/agent.json` : "/.well-known/agent.json"; + + return ( +
+ + +
+ ); +} + +export default McpA2aLinksBar; diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx new file mode 100644 index 0000000000..aab1ac205f --- /dev/null +++ b/src/app/(dashboard)/dashboard/agent-skills/components/SkillCard.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useCallback } from "react"; +import { useTranslations } from "next-intl"; +import type { AgentSkill } from "@/lib/agentSkills/types"; + +interface SkillCardProps { + skill: AgentSkill; + selected: boolean; + onClick: () => void; +} + +export function SkillCard({ skill, selected, onClick }: SkillCardProps): JSX.Element { + const t = useTranslations("agentSkills"); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + }, + [onClick], + ); + + const previewItems: string[] = + skill.category === "api" + ? (skill.endpoints ?? []).slice(0, 2) + : (skill.cliCommands ?? []).slice(0, 2); + + return ( +
+
+ + {skill.icon ?? "article"} + +
+ +
+
+ {skill.name} + + + {skill.category === "api" ? t("categoryApi") : t("categoryCli")} + + + {skill.isEntry && ( + + start + + )} + + {skill.isNew && ( + + new + + )} +
+ +

{skill.description}

+ + {previewItems.length > 0 && ( +
+ {previewItems.map((item) => ( + + {item} + + ))} +
+ )} +
+
+ ); +} + +export default SkillCard; diff --git a/src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx b/src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx new file mode 100644 index 0000000000..e73ff67157 --- /dev/null +++ b/src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { useCallback } from "react"; +import dynamic from "next/dynamic"; +import { useTranslations } from "next-intl"; + +// Lazy-load react-markdown to reduce initial bundle size. +const ReactMarkdown = dynamic(() => import("react-markdown"), { + loading: () => , +}); + +// remark-gfm loaded lazily alongside ReactMarkdown via remarkPlugins prop. +// We avoid rehype-raw to prevent XSS (Hard Rule #7 context). + +interface SkillPreviewPaneProps { + skillId: string | null; + markdown: string | null; + loading: boolean; + onRefresh?: () => void; +} + +function SkeletonLines({ lines }: { lines: number }): JSX.Element { + return ( +