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.
Files changed (307) hide show
  1. package/README.md +230 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +84 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/daemon.test.ts +84 -0
  13. package/src/cli/__tests__/dashboard.test.ts +224 -0
  14. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  15. package/src/cli/__tests__/port.test.ts +86 -0
  16. package/src/cli/args.ts +186 -0
  17. package/src/cli/attach.ts +154 -0
  18. package/src/cli/commands.ts +327 -0
  19. package/src/cli/daemon.ts +275 -0
  20. package/src/cli/dashboard.tsx +1060 -0
  21. package/src/cli/index.ts +47 -0
  22. package/src/cli/keep-awake.ts +148 -0
  23. package/src/cli/port.ts +92 -0
  24. package/src/cli/theme.ts +100 -0
  25. package/src/components/auth/AuthGate.tsx +182 -0
  26. package/src/components/auth/AvatarEditor.tsx +236 -0
  27. package/src/components/auth/LockScreen.tsx +714 -0
  28. package/src/components/auth/ProfileAvatar.tsx +60 -0
  29. package/src/components/auth/WelcomeScreen.tsx +672 -0
  30. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  31. package/src/components/auth/avatar-icons.tsx +114 -0
  32. package/src/components/layout/AppShell.tsx +147 -0
  33. package/src/components/layout/AppSidebar.tsx +419 -0
  34. package/src/components/layout/SearchDialog.tsx +189 -0
  35. package/src/components/layout/SidebarAccount.tsx +111 -0
  36. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  37. package/src/components/layout/ThemeToggle.tsx +67 -0
  38. package/src/components/projects/FolderBrowser.tsx +142 -0
  39. package/src/components/projects/HomeChat.tsx +287 -0
  40. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  41. package/src/components/projects/NewProjectDialog.tsx +249 -0
  42. package/src/components/projects/ProjectInstructions.tsx +218 -0
  43. package/src/components/projects/ProjectRow.tsx +58 -0
  44. package/src/components/projects/ProjectSidePanel.tsx +514 -0
  45. package/src/components/sessions/AgentLogo.tsx +42 -0
  46. package/src/components/sessions/AgentModeList.tsx +265 -0
  47. package/src/components/sessions/AgentPicker.tsx +132 -0
  48. package/src/components/sessions/AssistantTurn.tsx +145 -0
  49. package/src/components/sessions/AttachmentTile.tsx +161 -0
  50. package/src/components/sessions/CodeBlock.tsx +107 -0
  51. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  52. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  53. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
  54. package/src/components/sessions/ContextPanel.tsx +661 -0
  55. package/src/components/sessions/DropPlate.tsx +165 -0
  56. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  57. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  58. package/src/components/sessions/ImageLightbox.tsx +173 -0
  59. package/src/components/sessions/MarkdownContent.tsx +187 -0
  60. package/src/components/sessions/Mascot.tsx +47 -0
  61. package/src/components/sessions/MessageActions.tsx +60 -0
  62. package/src/components/sessions/MessageRail.tsx +121 -0
  63. package/src/components/sessions/ModelPicker.tsx +247 -0
  64. package/src/components/sessions/ModelSearchList.tsx +231 -0
  65. package/src/components/sessions/NewSessionDialog.tsx +172 -0
  66. package/src/components/sessions/PendingAttachments.tsx +127 -0
  67. package/src/components/sessions/PromptCard.tsx +1085 -0
  68. package/src/components/sessions/PromptPanel.tsx +112 -0
  69. package/src/components/sessions/SessionComposer.tsx +641 -0
  70. package/src/components/sessions/SessionHeader.tsx +176 -0
  71. package/src/components/sessions/SessionList.tsx +503 -0
  72. package/src/components/sessions/SessionRow.tsx +237 -0
  73. package/src/components/sessions/SessionTimeline.tsx +275 -0
  74. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  75. package/src/components/sessions/SessionView.tsx +240 -0
  76. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  77. package/src/components/sessions/UserMessage.tsx +177 -0
  78. package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
  79. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  80. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  81. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  82. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  83. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  84. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  85. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  86. package/src/components/settings/AboutGroup.tsx +170 -0
  87. package/src/components/settings/AccountGroup.tsx +122 -0
  88. package/src/components/settings/DevicesGroup.tsx +151 -0
  89. package/src/components/settings/LhrRemoteAccess.tsx +257 -0
  90. package/src/components/settings/PasskeysGroup.tsx +551 -0
  91. package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
  92. package/src/components/settings/SecurityGroup.tsx +566 -0
  93. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  94. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  95. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  96. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  97. package/src/components/settings/prefs.tsx +333 -0
  98. package/src/components/skills/InstallNotifications.tsx +326 -0
  99. package/src/components/skills/InstallSkillDialog.tsx +390 -0
  100. package/src/components/skills/SkillCards.tsx +254 -0
  101. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  102. package/src/components/ui/adw-dialog.tsx +114 -0
  103. package/src/components/ui/adw-toggle-group.tsx +75 -0
  104. package/src/components/ui/alert-dialog.tsx +179 -0
  105. package/src/components/ui/alert.tsx +60 -0
  106. package/src/components/ui/badge.tsx +46 -0
  107. package/src/components/ui/bubble.tsx +125 -0
  108. package/src/components/ui/button.tsx +62 -0
  109. package/src/components/ui/card.tsx +75 -0
  110. package/src/components/ui/collapsible.tsx +19 -0
  111. package/src/components/ui/dialog.tsx +142 -0
  112. package/src/components/ui/dropdown-menu.tsx +225 -0
  113. package/src/components/ui/empty.tsx +94 -0
  114. package/src/components/ui/field.tsx +234 -0
  115. package/src/components/ui/input-group.tsx +161 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/message-scroller.tsx +131 -0
  119. package/src/components/ui/message.tsx +85 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/search-entry.tsx +70 -0
  122. package/src/components/ui/select.tsx +172 -0
  123. package/src/components/ui/separator.tsx +28 -0
  124. package/src/components/ui/sheet.tsx +138 -0
  125. package/src/components/ui/sidebar.tsx +699 -0
  126. package/src/components/ui/skeleton.tsx +15 -0
  127. package/src/components/ui/sonner.tsx +45 -0
  128. package/src/components/ui/spinner.tsx +16 -0
  129. package/src/components/ui/switch.tsx +32 -0
  130. package/src/components/ui/textarea.tsx +18 -0
  131. package/src/components/ui/tooltip.tsx +51 -0
  132. package/src/config.ts +114 -0
  133. package/src/db.ts +146 -0
  134. package/src/frontend.tsx +51 -0
  135. package/src/hooks/__tests__/useMention.test.ts +79 -0
  136. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  137. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  138. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  139. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  140. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  141. package/src/hooks/use-mobile.ts +16 -0
  142. package/src/hooks/useAgentPicker.ts +101 -0
  143. package/src/hooks/useMediaQuery.ts +49 -0
  144. package/src/hooks/useMention.ts +236 -0
  145. package/src/hooks/useProjects.ts +88 -0
  146. package/src/hooks/useResyncOnReconnect.ts +28 -0
  147. package/src/hooks/useRouter.ts +92 -0
  148. package/src/hooks/useSessionChat.ts +427 -0
  149. package/src/hooks/useSessionContext.ts +106 -0
  150. package/src/hooks/useSessionList.ts +327 -0
  151. package/src/hooks/useSidebarData.ts +108 -0
  152. package/src/hooks/useSmoothText.ts +122 -0
  153. package/src/hooks/useTheme.ts +164 -0
  154. package/src/hooks/useWebSocket.ts +196 -0
  155. package/src/index.css +27 -0
  156. package/src/index.html +14 -0
  157. package/src/index.ts +49 -0
  158. package/src/lib/__tests__/attachments.test.ts +58 -0
  159. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  160. package/src/lib/__tests__/collapsible.test.ts +101 -0
  161. package/src/lib/__tests__/composer.test.ts +118 -0
  162. package/src/lib/__tests__/highlight.test.ts +42 -0
  163. package/src/lib/__tests__/home-chat.test.ts +64 -0
  164. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  165. package/src/lib/__tests__/model-picker.test.ts +205 -0
  166. package/src/lib/__tests__/onboarding.test.ts +41 -0
  167. package/src/lib/__tests__/permission-info.test.ts +61 -0
  168. package/src/lib/__tests__/project-extensions.test.ts +83 -0
  169. package/src/lib/__tests__/project-overview.test.ts +188 -0
  170. package/src/lib/__tests__/references.test.ts +121 -0
  171. package/src/lib/__tests__/routes.test.ts +134 -0
  172. package/src/lib/__tests__/session-selection.test.ts +41 -0
  173. package/src/lib/__tests__/session-summary.test.ts +115 -0
  174. package/src/lib/__tests__/session-title.test.ts +58 -0
  175. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  176. package/src/lib/__tests__/skill-installs.test.ts +135 -0
  177. package/src/lib/__tests__/skills-catalog.test.ts +124 -0
  178. package/src/lib/__tests__/tone.test.ts +37 -0
  179. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  180. package/src/lib/agent-types.ts +49 -0
  181. package/src/lib/api.ts +306 -0
  182. package/src/lib/attachments.ts +203 -0
  183. package/src/lib/auth.ts +92 -0
  184. package/src/lib/avatar-presets.ts +31 -0
  185. package/src/lib/collapsible.ts +75 -0
  186. package/src/lib/composer.ts +89 -0
  187. package/src/lib/data-events.ts +17 -0
  188. package/src/lib/headlines.ts +30 -0
  189. package/src/lib/highlight.ts +186 -0
  190. package/src/lib/layout.ts +20 -0
  191. package/src/lib/lock-screen.ts +54 -0
  192. package/src/lib/model-picker.ts +133 -0
  193. package/src/lib/onboarding.ts +38 -0
  194. package/src/lib/passkeys.ts +113 -0
  195. package/src/lib/pending-prompt.ts +53 -0
  196. package/src/lib/permission-info.ts +101 -0
  197. package/src/lib/project-extensions.ts +136 -0
  198. package/src/lib/project-overview.ts +123 -0
  199. package/src/lib/prompts.ts +87 -0
  200. package/src/lib/references.ts +106 -0
  201. package/src/lib/routes.ts +124 -0
  202. package/src/lib/session-handoff.ts +25 -0
  203. package/src/lib/session-selection.ts +40 -0
  204. package/src/lib/session-status.ts +45 -0
  205. package/src/lib/session-summary.ts +68 -0
  206. package/src/lib/session-title.ts +63 -0
  207. package/src/lib/sidebar-groups.ts +78 -0
  208. package/src/lib/skill-installs.ts +362 -0
  209. package/src/lib/skills-catalog.ts +138 -0
  210. package/src/lib/time.ts +22 -0
  211. package/src/lib/tone.ts +80 -0
  212. package/src/lib/turns.ts +320 -0
  213. package/src/lib/utils.ts +6 -0
  214. package/src/logo.svg +31 -0
  215. package/src/pages/projects/ProjectDetailPage.tsx +131 -0
  216. package/src/pages/projects/ProjectsPage.tsx +97 -0
  217. package/src/pages/sessions/SessionPage.tsx +122 -0
  218. package/src/pages/settings/SettingsPage.tsx +366 -0
  219. package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
  220. package/src/pages/skills/SkillsPage.tsx +745 -0
  221. package/src/paths.ts +121 -0
  222. package/src/runtime.ts +24 -0
  223. package/src/server/__tests__/app.test.ts +1044 -0
  224. package/src/server/__tests__/auth.test.ts +370 -0
  225. package/src/server/__tests__/config.test.ts +101 -0
  226. package/src/server/__tests__/db.test.ts +485 -0
  227. package/src/server/__tests__/host.test.ts +14 -0
  228. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  229. package/src/server/app.ts +427 -0
  230. package/src/server/db/auth.ts +311 -0
  231. package/src/server/db/messages.ts +50 -0
  232. package/src/server/db/projects.ts +106 -0
  233. package/src/server/db/prompts.ts +78 -0
  234. package/src/server/db/rows.ts +150 -0
  235. package/src/server/db/schema.ts +221 -0
  236. package/src/server/db/sessions.ts +239 -0
  237. package/src/server/db/tunnel.ts +124 -0
  238. package/src/server/host.ts +12 -0
  239. package/src/server/result.ts +15 -0
  240. package/src/server/routes/auth-guard.ts +156 -0
  241. package/src/server/routes/auth.routes.ts +287 -0
  242. package/src/server/routes/helpers.ts +165 -0
  243. package/src/server/routes/meta.routes.ts +24 -0
  244. package/src/server/routes/passkeys.routes.ts +161 -0
  245. package/src/server/routes/projects.routes.ts +167 -0
  246. package/src/server/routes/sessions.routes.ts +197 -0
  247. package/src/server/routes/skills.routes.ts +136 -0
  248. package/src/server/routes/tunnel.routes.ts +132 -0
  249. package/src/server/routes/types.ts +35 -0
  250. package/src/server/routes/uploads.routes.ts +54 -0
  251. package/src/server/routes/workspace.routes.ts +127 -0
  252. package/src/server/services/__tests__/app-info.test.ts +18 -0
  253. package/src/server/services/__tests__/attachments.test.ts +159 -0
  254. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  255. package/src/server/services/__tests__/auth.test.ts +341 -0
  256. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  257. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  258. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  259. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  260. package/src/server/services/__tests__/git-info.test.ts +74 -0
  261. package/src/server/services/__tests__/opencode.test.ts +320 -0
  262. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  263. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  264. package/src/server/services/__tests__/session-manager.test.ts +3509 -0
  265. package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
  266. package/src/server/services/__tests__/skills-registry.test.ts +365 -0
  267. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  268. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  269. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  270. package/src/server/services/__tests__/workspace.test.ts +249 -0
  271. package/src/server/services/app-info.ts +29 -0
  272. package/src/server/services/attachments.ts +193 -0
  273. package/src/server/services/auth-passkeys.ts +459 -0
  274. package/src/server/services/auth.ts +633 -0
  275. package/src/server/services/claude-code-client.ts +1093 -0
  276. package/src/server/services/claude-code-translate.ts +370 -0
  277. package/src/server/services/claude-mcp.ts +122 -0
  278. package/src/server/services/claude-skills.ts +102 -0
  279. package/src/server/services/frpc-installer.ts +183 -0
  280. package/src/server/services/git-info.ts +87 -0
  281. package/src/server/services/opencode-client.ts +714 -0
  282. package/src/server/services/opencode-server.ts +319 -0
  283. package/src/server/services/opencode-sse.ts +82 -0
  284. package/src/server/services/project-manager.ts +147 -0
  285. package/src/server/services/sandbox.ts +83 -0
  286. package/src/server/services/session-events.ts +256 -0
  287. package/src/server/services/session-manager.ts +1439 -0
  288. package/src/server/services/skill-install-jobs.ts +275 -0
  289. package/src/server/services/skills-registry.ts +480 -0
  290. package/src/server/services/tunnel-api.ts +207 -0
  291. package/src/server/services/tunnel-config.ts +45 -0
  292. package/src/server/services/tunnel-lhr.ts +387 -0
  293. package/src/server/services/tunnel.ts +633 -0
  294. package/src/server/services/turn-stream.ts +339 -0
  295. package/src/server/services/websocket-gateway.ts +374 -0
  296. package/src/server/services/workspace.ts +223 -0
  297. package/src/server/services/ws-transport.ts +121 -0
  298. package/src/types/context.ts +69 -0
  299. package/src/types/index.ts +13 -0
  300. package/src/types/message.ts +58 -0
  301. package/src/types/project.ts +16 -0
  302. package/src/types/prompt.ts +118 -0
  303. package/src/types/session.ts +52 -0
  304. package/src/types/turns.ts +47 -0
  305. package/src/ws-protocol.ts +93 -0
  306. package/styles/globals.css +311 -0
  307. 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
+ }