kcg-code 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +84 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/daemon.test.ts +84 -0
- package/src/cli/__tests__/dashboard.test.ts +224 -0
- package/src/cli/__tests__/keep-awake.test.ts +44 -0
- package/src/cli/__tests__/port.test.ts +86 -0
- package/src/cli/args.ts +186 -0
- package/src/cli/attach.ts +154 -0
- package/src/cli/commands.ts +327 -0
- package/src/cli/daemon.ts +275 -0
- package/src/cli/dashboard.tsx +1060 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/keep-awake.ts +148 -0
- package/src/cli/port.ts +92 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +147 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +287 -0
- package/src/components/projects/HomeRecentSessions.tsx +90 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/ProjectInstructions.tsx +218 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +514 -0
- package/src/components/sessions/AgentLogo.tsx +42 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +132 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
- package/src/components/sessions/ContextPanel.tsx +661 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +247 -0
- package/src/components/sessions/ModelSearchList.tsx +231 -0
- package/src/components/sessions/NewSessionDialog.tsx +172 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +641 -0
- package/src/components/sessions/SessionHeader.tsx +176 -0
- package/src/components/sessions/SessionList.tsx +503 -0
- package/src/components/sessions/SessionRow.tsx +237 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +240 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +257 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +333 -0
- package/src/components/skills/InstallNotifications.tsx +326 -0
- package/src/components/skills/InstallSkillDialog.tsx +390 -0
- package/src/components/skills/SkillCards.tsx +254 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/adw-dialog.tsx +114 -0
- package/src/components/ui/adw-toggle-group.tsx +75 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/search-entry.tsx +70 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +114 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +101 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +327 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +83 -0
- package/src/lib/__tests__/project-overview.test.ts +188 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-selection.test.ts +41 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +135 -0
- package/src/lib/__tests__/skills-catalog.test.ts +124 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/agent-types.ts +49 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +136 -0
- package/src/lib/project-overview.ts +123 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +124 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-selection.ts +40 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +362 -0
- package/src/lib/skills-catalog.ts +138 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +131 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +366 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
- package/src/pages/skills/SkillsPage.tsx +745 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1044 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +427 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +167 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +136 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/claude-code.test.ts +609 -0
- package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
- package/src/server/services/__tests__/claude-skills.test.ts +63 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3509 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
- package/src/server/services/__tests__/skills-registry.test.ts +365 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/claude-code-client.ts +1093 -0
- package/src/server/services/claude-code-translate.ts +370 -0
- package/src/server/services/claude-mcp.ts +122 -0
- package/src/server/services/claude-skills.ts +102 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1439 -0
- package/src/server/services/skill-install-jobs.ts +275 -0
- package/src/server/services/skills-registry.ts +480 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- package/tsconfig.json +37 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialog konfirmasi instalasi skill dari skills.sh.
|
|
3
|
+
*
|
|
4
|
+
* Susunan (atas -> bawah), dari yang paling sering diputuskan:
|
|
5
|
+
* 1. Identitas skill: avatar owner, nama, `source · installs · skills.sh ↗`.
|
|
6
|
+
* 2. Agent tujuan: dua tile berdampingan (OpenCode / Claude Code, bisa
|
|
7
|
+
* keduanya). Agent yang belum memuat skill ini terpilih secara default;
|
|
8
|
+
* yang sudah memuatnya ditandai "Installed" (memilihnya = pasang ulang).
|
|
9
|
+
* 3. Audit keamanan (`GET /api/skills/audit`) dalam satu baris ringkas;
|
|
10
|
+
* rincian per partner dilipat dan otomatis terbuka bila ada temuan
|
|
11
|
+
* (`warn`/`fail`). Skill `fail` tetap boleh dipasang, tombolnya destruktif.
|
|
12
|
+
*
|
|
13
|
+
* Label tombol menyebut agent tujuan ("Install for Claude Code"). Konfirmasi
|
|
14
|
+
* hanya MEMULAI job instalasi; progres + output CLI tampil di notifikasi
|
|
15
|
+
* instalasi (`InstallNotifications`).
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
CheckIcon,
|
|
19
|
+
ChevronDownIcon,
|
|
20
|
+
ExternalLinkIcon,
|
|
21
|
+
ShieldAlertIcon,
|
|
22
|
+
ShieldCheckIcon,
|
|
23
|
+
ShieldQuestionIcon,
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import { useEffect, useState } from "react";
|
|
26
|
+
import { AgentLogo } from "@/components/sessions/AgentLogo";
|
|
27
|
+
import { SourceAvatar } from "@/components/skills/SkillCards";
|
|
28
|
+
import {
|
|
29
|
+
AlertDialog,
|
|
30
|
+
AlertDialogAction,
|
|
31
|
+
AlertDialogCancel,
|
|
32
|
+
AlertDialogContent,
|
|
33
|
+
AlertDialogDescription,
|
|
34
|
+
AlertDialogFooter,
|
|
35
|
+
AlertDialogHeader,
|
|
36
|
+
AlertDialogTitle,
|
|
37
|
+
} from "@/components/ui/alert-dialog";
|
|
38
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
39
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
40
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
41
|
+
import { AGENT_TYPE_LABELS, AGENT_TYPES } from "@/lib/agent-types";
|
|
42
|
+
import { ApiError, apiFetch } from "@/lib/api";
|
|
43
|
+
import { AUDIT_BADGE_CLASS, AUDIT_LABEL, formatInstalls } from "@/lib/skills-catalog";
|
|
44
|
+
import { cn } from "@/lib/utils";
|
|
45
|
+
import type { RegistrySkill, SkillAudit } from "@/server/services/skills-registry";
|
|
46
|
+
import type { AgentType } from "@/types";
|
|
47
|
+
|
|
48
|
+
type AuditState =
|
|
49
|
+
| { phase: "loading" }
|
|
50
|
+
| { phase: "error"; message: string }
|
|
51
|
+
| { phase: "ready"; audit: SkillAudit };
|
|
52
|
+
|
|
53
|
+
export interface InstallSkillDialogProps {
|
|
54
|
+
/** Skill yang akan dipasang; `null` = dialog tertutup. */
|
|
55
|
+
skill: RegistrySkill | null;
|
|
56
|
+
projectName: string;
|
|
57
|
+
/** Agent yang sudah memuat skill ini dari folder Project. */
|
|
58
|
+
installedAgents: readonly AgentType[];
|
|
59
|
+
/** Permintaan memulai instalasi sedang dikirim. */
|
|
60
|
+
starting: boolean;
|
|
61
|
+
/** Pesan bila server menolak memulai (mis. instalasi lain sedang berjalan). */
|
|
62
|
+
startError: string | null;
|
|
63
|
+
onOpenChange: (open: boolean) => void;
|
|
64
|
+
onConfirm: (skill: RegistrySkill, agents: AgentType[]) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Folder skill di Project per agent (petunjuk di tile). */
|
|
68
|
+
const AGENT_SKILLS_DIR: Record<AgentType, string> = {
|
|
69
|
+
opencode: ".agents/skills",
|
|
70
|
+
"claude-code": ".claude/skills",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Label tombol utama sesuai agent terpilih. */
|
|
74
|
+
function installLabel(agents: readonly AgentType[], flagged: boolean): string {
|
|
75
|
+
const verb = flagged ? "Install anyway" : "Install";
|
|
76
|
+
if (agents.length === 0) return verb;
|
|
77
|
+
if (agents.length === AGENT_TYPES.length) return `${verb} for both`;
|
|
78
|
+
return `${verb} for ${agents.map((a) => AGENT_TYPE_LABELS[a]).join(" + ")}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function InstallSkillDialog({
|
|
82
|
+
skill,
|
|
83
|
+
projectName,
|
|
84
|
+
installedAgents,
|
|
85
|
+
starting,
|
|
86
|
+
startError,
|
|
87
|
+
onOpenChange,
|
|
88
|
+
onConfirm,
|
|
89
|
+
}: InstallSkillDialogProps) {
|
|
90
|
+
const [audit, setAudit] = useState<AuditState>({ phase: "loading" });
|
|
91
|
+
const [agents, setAgents] = useState<AgentType[]>([]);
|
|
92
|
+
|
|
93
|
+
// Dialog dibuka untuk skill baru -> pilih agent yang belum memuatnya.
|
|
94
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: hanya saat skill berganti
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!skill) return;
|
|
97
|
+
const missing = AGENT_TYPES.filter((a) => !installedAgents.includes(a));
|
|
98
|
+
setAgents(missing.length > 0 ? missing : [...AGENT_TYPES]);
|
|
99
|
+
}, [skill]);
|
|
100
|
+
|
|
101
|
+
const toggleAgent = (agent: AgentType, on: boolean) =>
|
|
102
|
+
setAgents((prev) => AGENT_TYPES.filter((a) => (a === agent ? on : prev.includes(a))));
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!skill) return;
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
setAudit({ phase: "loading" });
|
|
108
|
+
const params = new URLSearchParams({ source: skill.source, skill: skill.skillId });
|
|
109
|
+
apiFetch(`/api/skills/audit?${params}`)
|
|
110
|
+
.then((res) => res.json() as Promise<{ audit: SkillAudit }>)
|
|
111
|
+
.then(({ audit }) => {
|
|
112
|
+
if (!cancelled) setAudit({ phase: "ready", audit });
|
|
113
|
+
})
|
|
114
|
+
.catch((e: unknown) => {
|
|
115
|
+
if (!cancelled) {
|
|
116
|
+
setAudit({
|
|
117
|
+
phase: "error",
|
|
118
|
+
message: e instanceof ApiError ? e.message : "Failed to load the security audit.",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [skill]);
|
|
126
|
+
|
|
127
|
+
const flagged = audit.phase === "ready" && audit.audit.overall === "fail";
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<AlertDialog
|
|
131
|
+
open={skill !== null}
|
|
132
|
+
onOpenChange={(next) => {
|
|
133
|
+
if (!starting) onOpenChange(next);
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{/* Tinggi dibatasi agar dialog (audit terbuka) tetap muat di layar HP. */}
|
|
137
|
+
<AlertDialogContent className="max-h-[calc(100dvh-2rem)] gap-5 overflow-y-auto sm:max-w-md">
|
|
138
|
+
<AlertDialogHeader className="text-left">
|
|
139
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
140
|
+
{skill && <SourceAvatar source={skill.source} />}
|
|
141
|
+
<div className="flex min-w-0 flex-col gap-0.5">
|
|
142
|
+
<AlertDialogTitle className="truncate" title={skill?.name}>
|
|
143
|
+
Install {skill?.name}
|
|
144
|
+
</AlertDialogTitle>
|
|
145
|
+
{skill && (
|
|
146
|
+
<p className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
147
|
+
<span className="truncate font-mono" title={skill.source}>
|
|
148
|
+
{skill.source}
|
|
149
|
+
</span>
|
|
150
|
+
<span aria-hidden>·</span>
|
|
151
|
+
<span className="shrink-0 tabular-nums">
|
|
152
|
+
{formatInstalls(skill.installs)} installs
|
|
153
|
+
</span>
|
|
154
|
+
<span aria-hidden>·</span>
|
|
155
|
+
<a
|
|
156
|
+
href={skill.url}
|
|
157
|
+
target="_blank"
|
|
158
|
+
rel="noopener noreferrer"
|
|
159
|
+
className="inline-flex shrink-0 items-center gap-0.5 font-medium text-foreground underline-offset-4 hover:underline"
|
|
160
|
+
>
|
|
161
|
+
skills.sh
|
|
162
|
+
<ExternalLinkIcon className="size-3" aria-hidden />
|
|
163
|
+
<span className="sr-only">(opens in a new tab)</span>
|
|
164
|
+
</a>
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<AlertDialogDescription className="sr-only">
|
|
170
|
+
Choose which agents in {projectName} get this skill, then review its security audit.
|
|
171
|
+
</AlertDialogDescription>
|
|
172
|
+
</AlertDialogHeader>
|
|
173
|
+
|
|
174
|
+
{skill && (
|
|
175
|
+
<div className="flex min-w-0 flex-col gap-4 text-sm">
|
|
176
|
+
<fieldset className="flex flex-col gap-2" disabled={starting}>
|
|
177
|
+
<legend className="mb-2 text-xs text-muted-foreground">
|
|
178
|
+
Add to <span className="font-medium text-foreground">{projectName}</span> for
|
|
179
|
+
</legend>
|
|
180
|
+
<div className="grid grid-cols-2 gap-2">
|
|
181
|
+
{AGENT_TYPES.map((a) => (
|
|
182
|
+
<AgentTile
|
|
183
|
+
key={a}
|
|
184
|
+
agent={a}
|
|
185
|
+
checked={agents.includes(a)}
|
|
186
|
+
installed={installedAgents.includes(a)}
|
|
187
|
+
onChange={(on) => toggleAgent(a, on)}
|
|
188
|
+
/>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
{agents.length === 0 && (
|
|
192
|
+
<p className="text-xs text-muted-foreground">Pick at least one agent.</p>
|
|
193
|
+
)}
|
|
194
|
+
</fieldset>
|
|
195
|
+
|
|
196
|
+
<AuditPanel state={audit} />
|
|
197
|
+
|
|
198
|
+
{startError && (
|
|
199
|
+
<p role="alert" className="text-xs text-destructive">
|
|
200
|
+
{startError}
|
|
201
|
+
</p>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<div className="flex flex-col gap-3">
|
|
207
|
+
<p className="text-xs text-muted-foreground">
|
|
208
|
+
Skills run with the agent's full permissions. Installation continues in the background —
|
|
209
|
+
follow it in the notification.
|
|
210
|
+
</p>
|
|
211
|
+
<AlertDialogFooter>
|
|
212
|
+
<AlertDialogCancel disabled={starting}>Cancel</AlertDialogCancel>
|
|
213
|
+
<AlertDialogAction
|
|
214
|
+
variant={flagged ? "destructive" : "default"}
|
|
215
|
+
disabled={starting || !skill || agents.length === 0}
|
|
216
|
+
onClick={(e) => {
|
|
217
|
+
// Dialog tetap terbuka sampai job berhasil dibuat.
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
if (skill && agents.length > 0) onConfirm(skill, agents);
|
|
220
|
+
}}
|
|
221
|
+
>
|
|
222
|
+
{starting ? (
|
|
223
|
+
<>
|
|
224
|
+
<Spinner data-icon="inline-start" />
|
|
225
|
+
Starting…
|
|
226
|
+
</>
|
|
227
|
+
) : (
|
|
228
|
+
installLabel(agents, flagged)
|
|
229
|
+
)}
|
|
230
|
+
</AlertDialogAction>
|
|
231
|
+
</AlertDialogFooter>
|
|
232
|
+
</div>
|
|
233
|
+
</AlertDialogContent>
|
|
234
|
+
</AlertDialog>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Tile agent yang bisa dipilih: logo, nama, folder tujuan, indikator centang
|
|
240
|
+
* di pojok. Checkbox asli disembunyikan (`sr-only`) tapi tetap jadi kontrol
|
|
241
|
+
* yang dibaca screen reader & menerima fokus keyboard (Spasi).
|
|
242
|
+
*/
|
|
243
|
+
function AgentTile({
|
|
244
|
+
agent,
|
|
245
|
+
checked,
|
|
246
|
+
installed,
|
|
247
|
+
onChange,
|
|
248
|
+
}: {
|
|
249
|
+
agent: AgentType;
|
|
250
|
+
checked: boolean;
|
|
251
|
+
installed: boolean;
|
|
252
|
+
onChange: (checked: boolean) => void;
|
|
253
|
+
}) {
|
|
254
|
+
return (
|
|
255
|
+
<label
|
|
256
|
+
className={cn(
|
|
257
|
+
"relative flex cursor-pointer flex-col gap-2.5 rounded-xl border p-3 transition-colors select-none",
|
|
258
|
+
"has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50 has-disabled:cursor-not-allowed has-disabled:opacity-60",
|
|
259
|
+
checked ? "border-primary bg-primary/5" : "hover:border-foreground/20 hover:bg-accent/30",
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<input
|
|
263
|
+
type="checkbox"
|
|
264
|
+
checked={checked}
|
|
265
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
266
|
+
className="peer sr-only"
|
|
267
|
+
/>
|
|
268
|
+
<span className="flex items-start justify-between">
|
|
269
|
+
<span className="flex size-8 items-center justify-center rounded-lg bg-muted text-foreground">
|
|
270
|
+
<AgentLogo agentType={agent} className="size-4" />
|
|
271
|
+
</span>
|
|
272
|
+
<span
|
|
273
|
+
aria-hidden
|
|
274
|
+
className={cn(
|
|
275
|
+
"flex size-5 items-center justify-center rounded-full border transition-colors",
|
|
276
|
+
checked ? "border-primary bg-primary text-primary-foreground" : "bg-background",
|
|
277
|
+
)}
|
|
278
|
+
>
|
|
279
|
+
{checked && <CheckIcon className="size-3" strokeWidth={3} />}
|
|
280
|
+
</span>
|
|
281
|
+
</span>
|
|
282
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
283
|
+
<span className="text-sm font-medium leading-5">{AGENT_TYPE_LABELS[agent]}</span>
|
|
284
|
+
<span className="truncate font-mono text-[11px] text-muted-foreground">
|
|
285
|
+
{installed ? (checked ? "Installed · reinstall" : "Installed") : AGENT_SKILLS_DIR[agent]}
|
|
286
|
+
</span>
|
|
287
|
+
</span>
|
|
288
|
+
</label>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Audit keamanan sebagai satu baris ringkas (ikon, status, jumlah partner
|
|
294
|
+
* yang lolos). Rincian per partner bisa dibuka; terbuka otomatis bila ada
|
|
295
|
+
* temuan agar tidak terlewat.
|
|
296
|
+
*/
|
|
297
|
+
function AuditPanel({ state }: { state: AuditState }) {
|
|
298
|
+
const shell = "rounded-xl border bg-muted/30";
|
|
299
|
+
|
|
300
|
+
if (state.phase === "loading") {
|
|
301
|
+
return (
|
|
302
|
+
<div role="status" aria-busy="true" className={cn(shell, "flex items-center gap-3 p-3")}>
|
|
303
|
+
<span className="sr-only">Loading security audit…</span>
|
|
304
|
+
<Skeleton className="size-4 rounded-full" />
|
|
305
|
+
<Skeleton className="h-4 w-40" />
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (state.phase === "error") {
|
|
310
|
+
return (
|
|
311
|
+
<p className={cn(shell, "flex items-center gap-2 p-3 text-xs text-muted-foreground")}>
|
|
312
|
+
<ShieldQuestionIcon className="size-4 shrink-0" aria-hidden />
|
|
313
|
+
{state.message}
|
|
314
|
+
</p>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const { audits, overall } = state.audit;
|
|
319
|
+
if (audits === null || overall === null) {
|
|
320
|
+
return (
|
|
321
|
+
<p className={cn(shell, "flex items-start gap-2 p-3 text-xs text-muted-foreground")}>
|
|
322
|
+
<ShieldQuestionIcon className="size-4 shrink-0" aria-hidden />
|
|
323
|
+
<span>
|
|
324
|
+
<span className="font-medium text-foreground">Not audited yet.</span> Review the skill on
|
|
325
|
+
skills.sh before installing.
|
|
326
|
+
</span>
|
|
327
|
+
</p>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const passed = audits.filter((a) => a.status === "pass").length;
|
|
332
|
+
const Icon = overall === "pass" ? ShieldCheckIcon : ShieldAlertIcon;
|
|
333
|
+
return (
|
|
334
|
+
<Collapsible defaultOpen={overall !== "pass"} className={shell}>
|
|
335
|
+
<CollapsibleTrigger className="group flex w-full items-center gap-2 rounded-xl p-3 text-left text-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
|
|
336
|
+
<Icon
|
|
337
|
+
className={cn(
|
|
338
|
+
"size-4 shrink-0",
|
|
339
|
+
overall === "pass"
|
|
340
|
+
? "text-emerald-600 dark:text-emerald-400"
|
|
341
|
+
: overall === "warn"
|
|
342
|
+
? "text-amber-600 dark:text-amber-400"
|
|
343
|
+
: "text-destructive",
|
|
344
|
+
)}
|
|
345
|
+
aria-hidden
|
|
346
|
+
/>
|
|
347
|
+
<span className="font-medium">Security audit</span>
|
|
348
|
+
<span
|
|
349
|
+
className={cn(
|
|
350
|
+
"rounded px-1.5 text-[11px] leading-5 font-medium",
|
|
351
|
+
AUDIT_BADGE_CLASS[overall],
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
{AUDIT_LABEL[overall]}
|
|
355
|
+
</span>
|
|
356
|
+
<span className="ml-auto text-muted-foreground tabular-nums">
|
|
357
|
+
{passed}/{audits.length} passed
|
|
358
|
+
</span>
|
|
359
|
+
<ChevronDownIcon
|
|
360
|
+
className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-180"
|
|
361
|
+
aria-hidden
|
|
362
|
+
/>
|
|
363
|
+
</CollapsibleTrigger>
|
|
364
|
+
<CollapsibleContent>
|
|
365
|
+
<ul className="flex flex-col gap-2 border-t px-3 py-3">
|
|
366
|
+
{audits.map((a) => (
|
|
367
|
+
<li key={a.provider} className="flex items-start justify-between gap-3 text-xs">
|
|
368
|
+
<span className="flex min-w-0 flex-col">
|
|
369
|
+
<span className="font-medium">{a.provider}</span>
|
|
370
|
+
{a.summary && (
|
|
371
|
+
<span className="line-clamp-2 text-muted-foreground" title={a.summary}>
|
|
372
|
+
{a.summary}
|
|
373
|
+
</span>
|
|
374
|
+
)}
|
|
375
|
+
</span>
|
|
376
|
+
<span
|
|
377
|
+
className={cn(
|
|
378
|
+
"shrink-0 rounded px-1.5 text-[11px] leading-5 font-medium",
|
|
379
|
+
AUDIT_BADGE_CLASS[a.status],
|
|
380
|
+
)}
|
|
381
|
+
>
|
|
382
|
+
{AUDIT_LABEL[a.status]}
|
|
383
|
+
</span>
|
|
384
|
+
</li>
|
|
385
|
+
))}
|
|
386
|
+
</ul>
|
|
387
|
+
</CollapsibleContent>
|
|
388
|
+
</Collapsible>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kartu skill untuk grid 2 kolom di halaman Skills.
|
|
3
|
+
*
|
|
4
|
+
* - `RegistrySkillCard`: hasil skills.sh — nama (tautan), owner/repo, jumlah
|
|
5
|
+
* install, tombol Install / "Installing…" (klik = lihat progres) /
|
|
6
|
+
* status "Installed" (bila sudah terpasang untuk semua agent).
|
|
7
|
+
* - `InstalledSkillCard`: skill terpasang — nama, label asal, deskripsi.
|
|
8
|
+
* - Kedua kartu menampilkan logo agent tempat skill terpasang (`AgentBadges`)
|
|
9
|
+
* di pojok kanan atas.
|
|
10
|
+
* - `SkillCardSkeleton`: placeholder saat memuat.
|
|
11
|
+
* Semua kartu setinggi baris grid (`h-full`) agar grid rapi.
|
|
12
|
+
*/
|
|
13
|
+
import { CheckIcon, DownloadIcon, ExternalLinkIcon, PlusIcon, SparklesIcon } from "lucide-react";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
import { AgentLogo } from "@/components/sessions/AgentLogo";
|
|
16
|
+
import { Button } from "@/components/ui/button";
|
|
17
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
18
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
19
|
+
import { AGENT_TYPE_LABELS, AGENT_TYPES } from "@/lib/agent-types";
|
|
20
|
+
import { SKILL_SOURCE_LABEL } from "@/lib/project-extensions";
|
|
21
|
+
import { formatInstalls, githubOwner, type InstalledSkill } from "@/lib/skills-catalog";
|
|
22
|
+
import { cn } from "@/lib/utils";
|
|
23
|
+
import type { RegistrySkill } from "@/server/services/skills-registry";
|
|
24
|
+
import type { AgentType } from "@/types";
|
|
25
|
+
|
|
26
|
+
/** Grid kartu: 1 kolom di HP, 2 kolom mulai `md`. */
|
|
27
|
+
export function SkillGrid({ label, children }: { label: string; children: ReactNode }) {
|
|
28
|
+
return (
|
|
29
|
+
<ul aria-label={label} className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
30
|
+
{children}
|
|
31
|
+
</ul>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardShell({ className, children }: { className?: string; children: ReactNode }) {
|
|
36
|
+
return (
|
|
37
|
+
<li
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex h-full min-w-0 flex-col gap-3 rounded-xl border bg-card p-4 shadow-xs transition-colors",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</li>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Avatar owner GitHub; jatuh ke ikon bila bukan sumber GitHub / gagal dimuat. */
|
|
49
|
+
export function SourceAvatar({ source }: { source: string }) {
|
|
50
|
+
const owner = githubOwner(source);
|
|
51
|
+
return (
|
|
52
|
+
<span className="relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-muted-foreground">
|
|
53
|
+
<SparklesIcon className="size-4" aria-hidden />
|
|
54
|
+
{owner && (
|
|
55
|
+
<img
|
|
56
|
+
src={`https://github.com/${encodeURIComponent(owner)}.png?size=80`}
|
|
57
|
+
alt=""
|
|
58
|
+
loading="lazy"
|
|
59
|
+
referrerPolicy="no-referrer"
|
|
60
|
+
className="absolute inset-0 size-full object-cover"
|
|
61
|
+
onError={(e) => {
|
|
62
|
+
e.currentTarget.style.display = "none";
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Logo agent tempat skill terpasang; kosong -> tidak dirender.
|
|
72
|
+
* `sm` untuk baris daftar yang rapat (panel samping Project).
|
|
73
|
+
*/
|
|
74
|
+
export function AgentBadges({
|
|
75
|
+
agents,
|
|
76
|
+
size = "md",
|
|
77
|
+
}: {
|
|
78
|
+
agents: readonly AgentType[];
|
|
79
|
+
size?: "sm" | "md";
|
|
80
|
+
}) {
|
|
81
|
+
if (agents.length === 0) return null;
|
|
82
|
+
const label = `Installed for ${agents.map((a) => AGENT_TYPE_LABELS[a]).join(" and ")}`;
|
|
83
|
+
return (
|
|
84
|
+
<span
|
|
85
|
+
role="img"
|
|
86
|
+
aria-label={label}
|
|
87
|
+
title={label}
|
|
88
|
+
className={cn("flex shrink-0 items-center", size === "sm" ? "gap-0.5" : "gap-1")}
|
|
89
|
+
>
|
|
90
|
+
{agents.map((a) => (
|
|
91
|
+
<span
|
|
92
|
+
key={a}
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex items-center justify-center border bg-background text-foreground",
|
|
95
|
+
size === "sm" ? "size-5 rounded" : "size-6 rounded-md",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<AgentLogo agentType={a} className={size === "sm" ? "size-3" : "size-3.5"} />
|
|
99
|
+
</span>
|
|
100
|
+
))}
|
|
101
|
+
</span>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function RegistrySkillCard({
|
|
106
|
+
skill,
|
|
107
|
+
installedAgents,
|
|
108
|
+
installing,
|
|
109
|
+
onInstall,
|
|
110
|
+
onShowProgress,
|
|
111
|
+
}: {
|
|
112
|
+
skill: RegistrySkill;
|
|
113
|
+
/** Agent yang sudah memuat skill ini dari folder Project. */
|
|
114
|
+
installedAgents: readonly AgentType[];
|
|
115
|
+
/** Ada job instalasi berjalan untuk skill ini di Project terpilih. */
|
|
116
|
+
installing: boolean;
|
|
117
|
+
onInstall: (skill: RegistrySkill) => void;
|
|
118
|
+
/** Tampilkan lagi notifikasi progres instalasinya. */
|
|
119
|
+
onShowProgress: () => void;
|
|
120
|
+
}) {
|
|
121
|
+
// Masih bisa dipasang selama ada agent yang belum memuatnya.
|
|
122
|
+
const installedEverywhere = installedAgents.length === AGENT_TYPES.length;
|
|
123
|
+
return (
|
|
124
|
+
<CardShell className="hover:border-foreground/20">
|
|
125
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
126
|
+
<SourceAvatar source={skill.source} />
|
|
127
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
128
|
+
<a
|
|
129
|
+
href={skill.url}
|
|
130
|
+
target="_blank"
|
|
131
|
+
rel="noopener noreferrer"
|
|
132
|
+
title={skill.name}
|
|
133
|
+
className="group/link flex min-w-0 items-center gap-1 text-sm font-semibold leading-5 hover:underline"
|
|
134
|
+
>
|
|
135
|
+
<span className="truncate">{skill.name}</span>
|
|
136
|
+
<ExternalLinkIcon
|
|
137
|
+
className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover/link:opacity-100 group-focus-visible/link:opacity-100"
|
|
138
|
+
aria-hidden
|
|
139
|
+
/>
|
|
140
|
+
<span className="sr-only"> on skills.sh (opens in a new tab)</span>
|
|
141
|
+
</a>
|
|
142
|
+
<span className="truncate font-mono text-xs text-muted-foreground" title={skill.source}>
|
|
143
|
+
{skill.source}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
<AgentBadges agents={installedAgents} />
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div className="mt-auto flex items-center justify-between gap-2">
|
|
150
|
+
<span className="flex items-center gap-1 text-xs tabular-nums text-muted-foreground">
|
|
151
|
+
<DownloadIcon className="size-3.5" aria-hidden />
|
|
152
|
+
<span className="sr-only">Installs:</span>
|
|
153
|
+
{formatInstalls(skill.installs)}
|
|
154
|
+
<span aria-hidden> installs</span>
|
|
155
|
+
</span>
|
|
156
|
+
{installing ? (
|
|
157
|
+
<Button
|
|
158
|
+
type="button"
|
|
159
|
+
variant="secondary"
|
|
160
|
+
size="sm"
|
|
161
|
+
onClick={onShowProgress}
|
|
162
|
+
aria-label={`Installing ${skill.name}. Show progress`}
|
|
163
|
+
>
|
|
164
|
+
<Spinner data-icon="inline-start" />
|
|
165
|
+
Installing…
|
|
166
|
+
</Button>
|
|
167
|
+
) : installedEverywhere ? (
|
|
168
|
+
<span className="flex h-8 items-center gap-1 rounded-md bg-emerald-500/10 px-2.5 text-xs font-medium text-emerald-700 dark:text-emerald-400">
|
|
169
|
+
<CheckIcon className="size-3.5" aria-hidden />
|
|
170
|
+
Installed
|
|
171
|
+
</span>
|
|
172
|
+
) : (
|
|
173
|
+
<Button
|
|
174
|
+
type="button"
|
|
175
|
+
variant="outline"
|
|
176
|
+
size="sm"
|
|
177
|
+
onClick={() => onInstall(skill)}
|
|
178
|
+
aria-label={`Install ${skill.name}`}
|
|
179
|
+
>
|
|
180
|
+
<PlusIcon data-icon="inline-start" />
|
|
181
|
+
Install
|
|
182
|
+
</Button>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</CardShell>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function InstalledSkillCard({ skill }: { skill: InstalledSkill }) {
|
|
190
|
+
const isProject = skill.source === "project";
|
|
191
|
+
return (
|
|
192
|
+
<CardShell>
|
|
193
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
194
|
+
<span
|
|
195
|
+
className={cn(
|
|
196
|
+
"flex size-10 shrink-0 items-center justify-center rounded-lg",
|
|
197
|
+
isProject ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground",
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
<SparklesIcon className="size-4" aria-hidden />
|
|
201
|
+
</span>
|
|
202
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
203
|
+
<span className="flex min-w-0 items-center gap-1.5">
|
|
204
|
+
<span className="truncate text-sm font-semibold leading-5" title={skill.name}>
|
|
205
|
+
{skill.name}
|
|
206
|
+
</span>
|
|
207
|
+
<span
|
|
208
|
+
className={cn(
|
|
209
|
+
"shrink-0 rounded px-1.5 text-[10px] leading-4 font-medium",
|
|
210
|
+
isProject ? "bg-primary/10 text-primary" : "bg-muted text-muted-foreground",
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{SKILL_SOURCE_LABEL[skill.source]}
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
<p
|
|
217
|
+
className={cn(
|
|
218
|
+
"line-clamp-3 text-xs leading-relaxed",
|
|
219
|
+
skill.description ? "text-muted-foreground" : "text-muted-foreground/60 italic",
|
|
220
|
+
)}
|
|
221
|
+
title={skill.description ?? undefined}
|
|
222
|
+
>
|
|
223
|
+
{skill.description ?? "No description"}
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
<AgentBadges agents={skill.agents} />
|
|
227
|
+
</div>
|
|
228
|
+
</CardShell>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function SkillCardSkeleton({ count = 4 }: { count?: number }) {
|
|
233
|
+
return (
|
|
234
|
+
<div role="status" aria-busy="true" className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
235
|
+
<span className="sr-only">Loading skills…</span>
|
|
236
|
+
{Array.from({ length: count }, (_, i) => (
|
|
237
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
|
|
238
|
+
<div key={i} className="flex flex-col gap-3 rounded-xl border bg-card p-4" aria-hidden>
|
|
239
|
+
<div className="flex items-start gap-3">
|
|
240
|
+
<Skeleton className="size-10 shrink-0 rounded-lg" />
|
|
241
|
+
<div className="flex flex-1 flex-col gap-1.5">
|
|
242
|
+
<Skeleton className="h-4" style={{ width: `${75 - (i % 3) * 15}%` }} />
|
|
243
|
+
<Skeleton className="h-3 w-1/2" />
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center justify-between">
|
|
247
|
+
<Skeleton className="h-3 w-20" />
|
|
248
|
+
<Skeleton className="h-8 w-20 rounded-md" />
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|