Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Luke The Dev
2026-03-23 11:44:25 -05:00
committed by GitHub
parent c2cbdeec44
commit 5e7812c352
30 changed files with 2213 additions and 128 deletions
+9 -24
View File
@@ -6,7 +6,6 @@ export type HQSidebarTab =
| "inbox"
| "history"
| "playbooks"
| "marketplace"
| "analytics";
type HQSidebarProps = {
@@ -15,10 +14,10 @@ type HQSidebarProps = {
inboxCount: number;
onToggle: () => void;
onTabChange: (tab: HQSidebarTab) => void;
onOpenMarketplace: () => void;
inboxPanel: ReactNode;
historyPanel: ReactNode;
playbooksPanel: ReactNode;
marketplacePanel: ReactNode;
analyticsPanel: ReactNode;
};
@@ -26,7 +25,6 @@ const TAB_LABELS: Record<HQSidebarTab, string> = {
inbox: "Inbox",
history: "History",
playbooks: "Playbooks",
marketplace: "Marketplace",
analytics: "Analytics",
};
@@ -38,15 +36,14 @@ export function HQSidebar({
inboxCount,
onToggle,
onTabChange,
onOpenMarketplace,
inboxPanel,
historyPanel,
playbooksPanel,
marketplacePanel,
analyticsPanel,
}: HQSidebarProps) {
const analyticsOnly = activeTab === "analytics";
const marketplaceOnly = activeTab === "marketplace";
const railOnly = analyticsOnly || marketplaceOnly;
const railOnly = analyticsOnly;
const activePanel =
activeTab === "inbox"
? inboxPanel
@@ -54,9 +51,7 @@ export function HQSidebar({
? historyPanel
: activeTab === "playbooks"
? playbooksPanel
: activeTab === "marketplace"
? marketplacePanel
: analyticsPanel;
: analyticsPanel;
return (
<aside className="pointer-events-none fixed inset-y-0 right-0 z-20 flex justify-end">
@@ -76,18 +71,10 @@ export function HQSidebar({
<button
type="button"
onClick={() => {
onTabChange("marketplace");
if (!open) {
onToggle();
}
onOpenMarketplace();
}}
className={`rounded-l-md border border-r-0 px-1.5 py-2.5 font-mono text-[10px] font-semibold tracking-[0.2em] shadow-xl backdrop-blur transition-colors ${
marketplaceOnly
? "border-fuchsia-400/50 bg-[#16081b]/95 text-fuchsia-100"
: "border-fuchsia-500/25 bg-[#100611]/90 text-fuchsia-300/80 hover:border-fuchsia-400/45 hover:text-fuchsia-100"
}`}
aria-pressed={marketplaceOnly}
aria-label="Open marketplace sidebar"
className="rounded-l-md border border-r-0 border-fuchsia-500/25 bg-[#100611]/90 px-1.5 py-2.5 font-mono text-[10px] font-semibold tracking-[0.2em] text-fuchsia-300/80 shadow-xl backdrop-blur transition-colors hover:border-fuchsia-400/45 hover:text-fuchsia-100"
aria-label="Open marketplace"
>
<span className="block leading-none [writing-mode:vertical-rl]">
MARKETPLACE
@@ -120,14 +107,12 @@ export function HQSidebar({
<div className="pointer-events-auto flex h-full w-56 flex-col border-l border-cyan-500/20 bg-black/85 shadow-2xl backdrop-blur">
<div className="border-b border-cyan-500/15 px-4 py-3">
<div className="font-mono text-[10px] font-semibold tracking-[0.32em] text-cyan-300/80">
{analyticsOnly ? "ANALYTICS" : marketplaceOnly ? "MARKETPLACE" : "HEADQUARTERS"}
{analyticsOnly ? "ANALYTICS" : "HEADQUARTERS"}
</div>
<div className="mt-1 font-mono text-[11px] text-white/45">
{analyticsOnly
? "Cost, budgets, and performance intelligence."
: marketplaceOnly
? "Discover, install, and enable new skills."
: "Monitor outputs, runs, and schedules."}
: "Monitor outputs, runs, and schedules."}
</div>
{railOnly ? (
<button
@@ -0,0 +1,88 @@
"use client";
import { useEffect } from "react";
import { X } from "lucide-react";
import type { OfficeSkillsMarketplaceController } from "@/features/office/hooks/useOfficeSkillsMarketplace";
import { SkillsMarketplacePanel } from "./SkillsMarketplacePanel";
type SkillsMarketplaceModalProps = {
open: boolean;
marketplace: OfficeSkillsMarketplaceController;
onClose: () => void;
onSelectAgent: (agentId: string) => void;
onOpenAgentSettings: (agentId: string) => void;
};
export function SkillsMarketplaceModal({
open,
marketplace,
onClose,
onSelectAgent,
onOpenAgentSettings,
}: SkillsMarketplaceModalProps) {
useEffect(() => {
if (!open) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") {
return;
}
event.preventDefault();
onClose();
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onClose, open]);
if (!open) {
return null;
}
return (
<div
className="fixed inset-0 z-[125] flex items-center justify-center bg-black/80 p-4 backdrop-blur-sm"
role="dialog"
aria-modal="true"
aria-label="Skills marketplace"
onClick={onClose}
>
<div
className="flex h-[min(90vh,960px)] w-full max-w-6xl flex-col overflow-hidden rounded-xl border border-cyan-500/20 bg-[#050607]/95 shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-4 border-b border-cyan-500/10 px-5 py-4">
<div>
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-cyan-300/80">
Skills Marketplace
</div>
<div className="mt-1 font-mono text-[11px] text-white/45">
Discover, install, and enable gateway skills in a wider workspace.
</div>
</div>
<button
type="button"
onClick={onClose}
className="inline-flex items-center gap-1 rounded border border-white/10 bg-white/5 px-2 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-white/75 transition-colors hover:bg-white/10"
>
<X className="h-3.5 w-3.5" />
Close
</button>
</div>
<div className="min-h-0 flex-1">
<SkillsMarketplacePanel
marketplace={marketplace}
onSelectAgent={onSelectAgent}
onOpenAgentSettings={onOpenAgentSettings}
/>
</div>
</div>
</div>
);
}
@@ -22,6 +22,7 @@ import { buildAgentSkillsAllowlistSet, deriveAgentSkillsAccessMode } from "@/lib
type MarketplaceFilter = "all" | SkillMarketplaceCollectionId;
const FILTER_LABELS: Record<MarketplaceFilter, string> = {
claw3d: "Claw3D",
all: "All",
featured: "Featured",
installed: "Installed",
@@ -99,10 +100,13 @@ export function SkillsMarketplacePanel({
onOpenAgentSettings: (agentId: string) => void;
}) {
const [query, setQuery] = useState("");
const [activeFilter, setActiveFilter] = useState<MarketplaceFilter>("all");
const [activeFilter, setActiveFilter] = useState<MarketplaceFilter>("claw3d");
const [detailSkillKey, setDetailSkillKey] = useState<string | null>(null);
const entries = useMemo(() => marketplace.skillsReport?.skills ?? [], [marketplace.skillsReport]);
const entries = useMemo(
() => marketplace.marketplaceSkills ?? marketplace.skillsReport?.skills ?? [],
[marketplace.marketplaceSkills, marketplace.skillsReport]
);
const collections = useMemo(() => buildSkillMarketplaceCollections(entries), [entries]);
const accessMode = useMemo(
() => deriveAgentSkillsAccessMode(marketplace.skillsAllowlist),
@@ -117,7 +121,7 @@ export function SkillsMarketplacePanel({
const normalizedQuery = query.trim().toLowerCase();
const visibleCollectionIds: SkillMarketplaceCollectionId[] =
activeFilter === "all"
? ["built-in", "installed", "workspace", "extra", "other"]
? ["claw3d", "built-in", "installed", "workspace", "extra", "other"]
: [activeFilter];
return collections
.filter((collection) => visibleCollectionIds.includes(collection.id))
@@ -151,6 +155,7 @@ export function SkillsMarketplacePanel({
const filterCounts = useMemo(() => {
const counts: Record<MarketplaceFilter, number> = {
claw3d: 0,
all: entries.length,
featured: 0,
installed: 0,
@@ -196,8 +201,8 @@ export function SkillsMarketplacePanel({
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
<div className="rounded border border-amber-500/20 bg-amber-500/10 px-3 py-2 font-mono text-[10px] text-amber-100">
Install and global setup actions affect the whole gateway. Agent access controls below apply only
to the selected agent.
Packaged skill installs target the selected agent workspace. Global setup actions still affect
the whole gateway. Agent access controls below apply only to the selected agent.
</div>
<div className="mt-3 rounded border border-cyan-500/15 bg-white/[0.03] px-3 py-3">
@@ -291,6 +296,11 @@ export function SkillsMarketplacePanel({
}`}
>
{marketplace.message.text}
{marketplace.message.kind === "success" ? (
<div className="mt-1 font-mono text-[10px] text-emerald-100/80">
Check the `CLAW3D` filter below to find the installed skill quickly.
</div>
) : null}
</div>
) : null}
@@ -327,14 +337,32 @@ export function SkillsMarketplacePanel({
{entry.metadata.editorBadge ?? "Featured"}
</div>
</div>
<div className="mt-3 flex items-center gap-3 font-mono text-[10px] text-white/55">
<span className="inline-flex items-center gap-1">
<Star className="h-3 w-3 text-amber-300" />
{formatRating(entry.metadata.rating)}
</span>
<span>{formatInstalls(entry.metadata.installs)} installs</span>
<div className="mt-3 flex flex-wrap items-center gap-3 font-mono text-[10px] text-white/55">
{!entry.metadata.hideStats ? (
<>
<span className="inline-flex items-center gap-1">
<Star className="h-3 w-3 text-amber-300" />
{formatRating(entry.metadata.rating)}
</span>
<span>{formatInstalls(entry.metadata.installs)} installs</span>
</>
) : null}
<span>{entry.metadata.category}</span>
</div>
{entry.metadata.poweredByName && entry.metadata.poweredByUrl ? (
<div className="mt-2 font-mono text-[10px] text-white/55">
Powered by{" "}
<a
href={entry.metadata.poweredByUrl}
target="_blank"
rel="noreferrer"
className="text-cyan-200 underline decoration-cyan-500/40 underline-offset-2 transition-colors hover:text-cyan-100"
onClick={(event) => event.stopPropagation()}
>
{entry.metadata.poweredByName}
</a>
</div>
) : null}
</button>
))}
</div>
@@ -355,9 +383,17 @@ export function SkillsMarketplacePanel({
</div>
<div className="flex flex-col gap-2">
{collection.entries.map((entry) => {
const packagedSkill = marketplace.packagedSkillsByKey.get(entry.skill.skillKey);
const packageOnly = Boolean(packagedSkill && !entry.skill.baseDir.trim());
const isEnabledForAgent = getAgentSkillEnabled(entry.skill.name, accessMode, allowlistSet);
const primaryAction =
entry.readiness === "needs-setup" && entry.installable
packageOnly
? {
label: "Install skill",
run: () => void marketplace.handleInstallPackagedSkill(entry.skill.skillKey),
icon: Download,
}
: entry.readiness === "needs-setup" && entry.installable
? {
label: "Install deps",
run: () => void marketplace.handleInstallSkill(entry.skill),
@@ -411,13 +447,30 @@ export function SkillsMarketplacePanel({
<Shield className="h-3 w-3 text-cyan-300" />
{entry.metadata.trustLabel}
</span>
<span className="inline-flex items-center gap-1">
<Star className="h-3 w-3 text-amber-300" />
{formatRating(entry.metadata.rating)}
</span>
<span>{formatInstalls(entry.metadata.installs)} installs</span>
{!entry.metadata.hideStats ? (
<>
<span className="inline-flex items-center gap-1">
<Star className="h-3 w-3 text-amber-300" />
{formatRating(entry.metadata.rating)}
</span>
<span>{formatInstalls(entry.metadata.installs)} installs</span>
</>
) : null}
<span>{entry.skill.source}</span>
</div>
{entry.metadata.poweredByName && entry.metadata.poweredByUrl ? (
<div className="mt-2 font-mono text-[10px] text-white/55">
Powered by{" "}
<a
href={entry.metadata.poweredByUrl}
target="_blank"
rel="noreferrer"
className="text-cyan-200 underline decoration-cyan-500/40 underline-offset-2 transition-colors hover:text-cyan-100"
>
{entry.metadata.poweredByName}
</a>
</div>
) : null}
{entry.missingDetails.length > 0 ? (
<div className="mt-2 font-mono text-[10px] text-amber-100/85">
{entry.missingDetails[0]}
@@ -430,6 +483,7 @@ export function SkillsMarketplacePanel({
type="button"
onClick={() => void marketplace.handleSetSkillEnabled(entry.skill.name, !isEnabledForAgent)}
disabled={
packageOnly ||
entry.readiness === "unavailable" ||
!marketplace.selectedAgentId ||
marketplace.busySkillKey === entry.skill.skillKey
@@ -440,7 +494,7 @@ export function SkillsMarketplacePanel({
: "border-white/10 bg-white/5 text-white/75 hover:bg-white/10"
}`}
>
{isEnabledForAgent ? "Enabled for agent" : "Enable for agent"}
{isEnabledForAgent ? "Disable for agent" : "Enable for agent"}
</button>
<div className="flex flex-wrap justify-end gap-2">
@@ -464,7 +518,7 @@ export function SkillsMarketplacePanel({
className="inline-flex items-center gap-1 rounded border border-rose-500/25 bg-rose-500/10 px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-rose-100 transition-colors hover:border-rose-400/40 disabled:cursor-not-allowed disabled:opacity-45"
>
<Trash2 className="h-3.5 w-3.5" />
Remove
Remove for all agents
</button>
) : null}
@@ -478,6 +532,16 @@ export function SkillsMarketplacePanel({
</div>
</div>
</div>
<div className="mt-2 flex flex-wrap items-center justify-between gap-2 font-mono text-[10px] text-white/35">
<div>
{isEnabledForAgent
? "This skill is currently enabled for the selected agent."
: "This skill is currently disabled for the selected agent."}
</div>
{entry.removable ? (
<div>Removing from the gateway deletes the installed skill for every agent.</div>
) : null}
</div>
</div>
);
})}
@@ -523,15 +587,36 @@ export function SkillsMarketplacePanel({
</span>
</div>
<div className="mt-3 font-mono text-[11px] text-white/75">{detailEntry.metadata.tagline}</div>
<div className="mt-3 grid grid-cols-3 gap-2 font-mono text-[10px] text-white/55">
<div className="rounded border border-white/8 bg-black/30 px-2 py-2">
<div className="text-white/35">Rating</div>
<div className="mt-1 text-white/90">{formatRating(detailEntry.metadata.rating)}</div>
</div>
<div className="rounded border border-white/8 bg-black/30 px-2 py-2">
<div className="text-white/35">Installs</div>
<div className="mt-1 text-white/90">{formatInstalls(detailEntry.metadata.installs)}</div>
{detailEntry.metadata.poweredByName && detailEntry.metadata.poweredByUrl ? (
<div className="mt-3 font-mono text-[10px] text-white/60">
Powered by{" "}
<a
href={detailEntry.metadata.poweredByUrl}
target="_blank"
rel="noreferrer"
className="text-cyan-200 underline decoration-cyan-500/40 underline-offset-2 transition-colors hover:text-cyan-100"
>
{detailEntry.metadata.poweredByName}
</a>
</div>
) : null}
<div
className={`mt-3 grid gap-2 font-mono text-[10px] text-white/55 ${
detailEntry.metadata.hideStats ? "grid-cols-1" : "grid-cols-3"
}`}
>
{!detailEntry.metadata.hideStats ? (
<>
<div className="rounded border border-white/8 bg-black/30 px-2 py-2">
<div className="text-white/35">Rating</div>
<div className="mt-1 text-white/90">{formatRating(detailEntry.metadata.rating)}</div>
</div>
<div className="rounded border border-white/8 bg-black/30 px-2 py-2">
<div className="text-white/35">Installs</div>
<div className="mt-1 text-white/90">{formatInstalls(detailEntry.metadata.installs)}</div>
</div>
</>
) : null}
<div className="rounded border border-white/8 bg-black/30 px-2 py-2">
<div className="text-white/35">Source</div>
<div className="mt-1 text-white/90">{detailEntry.skill.source}</div>
@@ -574,11 +659,23 @@ export function SkillsMarketplacePanel({
) : null}
<div className="mt-4 rounded border border-cyan-500/15 bg-cyan-500/10 px-3 py-3 font-mono text-[10px] text-cyan-100">
Gateway setup changes apply to every agent. Agent enablement still depends on the selected
agent&apos;s allowlist.
Packaged installs land in the selected workspace. Gateway setup changes still apply to every
agent, and agent enablement depends on the selected agent&apos;s allowlist.
</div>
<div className="mt-4 flex flex-wrap gap-2">
{marketplace.packagedSkillsByKey.get(detailEntry.skill.skillKey) &&
!detailEntry.skill.baseDir.trim() ? (
<button
type="button"
onClick={() => void marketplace.handleInstallPackagedSkill(detailEntry.skill.skillKey)}
disabled={marketplace.busySkillKey === detailEntry.skill.skillKey}
className="inline-flex items-center gap-1 rounded border border-cyan-500/25 bg-cyan-500/10 px-2 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-cyan-100 transition-colors hover:border-cyan-400/40 disabled:cursor-not-allowed disabled:opacity-45"
>
<Download className="h-3.5 w-3.5" />
Install skill
</button>
) : null}
{detailEntry.readiness === "needs-setup" && detailEntry.installable ? (
<button
type="button"
@@ -628,6 +725,10 @@ export function SkillsMarketplacePanel({
</a>
) : null}
</div>
<div className="mt-4 rounded border border-white/8 bg-white/[0.03] px-3 py-3 font-mono text-[10px] text-white/60">
`Enable/Disable for agent` only changes access for the selected agent. `Remove for all agents`
deletes the installed skill from the gateway workspace.
</div>
</div>
</div>
) : null}