kcg-code 0.0.0-stage → 0.1.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 (286) hide show
  1. package/README.md +228 -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 +83 -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__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Panel samping halaman Project — gaya Claude Projects: Instructions, MCP
3
+ * server, dan Skills dalam satu kolom di kanan daftar Session.
4
+ *
5
+ * Tiap seksi bisa dilipat (status diingat). Daftar berupa "grouped list"
6
+ * (kotak berdivider) dengan padding simetris — tanpa margin negatif.
7
+ * - MCP: `GET /api/projects/:id/mcp` — ikon, nama, pill status; error
8
+ * `failed` tampil inline. Yang bermasalah diurutkan paling atas.
9
+ * - Skills: `GET /api/projects/:id/skills` — pencarian (bila banyak) + chip
10
+ * asal (All / This project / Global / Built-in); deskripsi 1 baris, klik
11
+ * baris untuk membuka deskripsi lengkap.
12
+ * Tiap seksi memuat datanya sendiri sehingga seksi yang lambat tidak menahan
13
+ * seksi lain. Tombol reload memuat ulang keduanya.
14
+ */
15
+ import {
16
+ ChevronDownIcon,
17
+ PlugIcon,
18
+ RefreshCwIcon,
19
+ SearchIcon,
20
+ SparklesIcon,
21
+ XIcon,
22
+ } from "lucide-react";
23
+ import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
24
+ import { PanelBody, PanelHint, PanelList, PanelSection } from "@/components/projects/PanelSection";
25
+ import { ProjectInstructions } from "@/components/projects/ProjectInstructions";
26
+ import { Button } from "@/components/ui/button";
27
+ import { Skeleton } from "@/components/ui/skeleton";
28
+ import { ApiError, apiFetch } from "@/lib/api";
29
+ import {
30
+ describeMcpSummary,
31
+ filterSkills,
32
+ groupSkills,
33
+ MCP_STATUS_LABEL,
34
+ SKILL_SOURCE_LABEL,
35
+ } from "@/lib/project-extensions";
36
+ import { cn } from "@/lib/utils";
37
+ import type { McpServerInfo, SkillInfo, SkillSource } from "@/server/services/opencode-client";
38
+ import type { Project } from "@/types";
39
+
40
+ /** Skill ditampilkan terbatas dulu; sisanya lewat "Show more". */
41
+ const SKILL_PREVIEW = 5;
42
+ /** Kotak cari muncul hanya bila skill cukup banyak. */
43
+ const SKILL_SEARCH_THRESHOLD = 8;
44
+
45
+ type Load<T> =
46
+ | { phase: "loading" }
47
+ | { phase: "error"; message: string }
48
+ | { phase: "ready"; data: T };
49
+
50
+ /** Muat satu endpoint list; `reloadKey` berubah -> muat ulang. */
51
+ function useProjectList<T>(url: string, key: string, reloadKey: number): Load<T> {
52
+ const [state, setState] = useState<Load<T>>({ phase: "loading" });
53
+ useEffect(() => {
54
+ // `reloadKey` hanya pemicu muat ulang.
55
+ void reloadKey;
56
+ let cancelled = false;
57
+ setState({ phase: "loading" });
58
+ apiFetch(url)
59
+ .then(async (res) => {
60
+ const body = (await res.json()) as Record<string, T>;
61
+ if (!cancelled) setState({ phase: "ready", data: body[key] as T });
62
+ })
63
+ .catch((e: unknown) => {
64
+ if (cancelled) return;
65
+ setState({ phase: "error", message: e instanceof ApiError ? e.message : "Failed to load" });
66
+ });
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [url, key, reloadKey]);
71
+ return state;
72
+ }
73
+
74
+ export interface ProjectSidePanelProps {
75
+ project: Project;
76
+ onProjectChange: (project: Project) => void;
77
+ }
78
+
79
+ export function ProjectSidePanel({ project, onProjectChange }: ProjectSidePanelProps) {
80
+ const [reloadKey, setReloadKey] = useState(0);
81
+ const reload = useCallback(() => setReloadKey((k) => k + 1), []);
82
+ const mcp = useProjectList<McpServerInfo[]>(`/api/projects/${project.id}/mcp`, "mcp", reloadKey);
83
+ const skills = useProjectList<SkillInfo[]>(
84
+ `/api/projects/${project.id}/skills`,
85
+ "skills",
86
+ reloadKey,
87
+ );
88
+
89
+ return (
90
+ <div className="flex flex-col gap-3">
91
+ <ProjectInstructions project={project} onSaved={onProjectChange} />
92
+ <McpSection state={mcp} onReload={reload} />
93
+ <SkillsSection state={skills} />
94
+ </div>
95
+ );
96
+ }
97
+
98
+ function ListSkeleton({ rows = 3 }: { rows?: number }) {
99
+ return (
100
+ <PanelList>
101
+ {Array.from({ length: rows }, (_, i) => (
102
+ // biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
103
+ <li key={i} className="flex items-center gap-3 px-3.5 py-3" aria-hidden>
104
+ <Skeleton className="size-7 shrink-0 rounded-md" />
105
+ <div className="flex flex-1 flex-col gap-1.5">
106
+ <Skeleton className="h-3.5" style={{ width: `${75 - i * 15}%` }} />
107
+ <Skeleton className="h-3 w-1/3" />
108
+ </div>
109
+ </li>
110
+ ))}
111
+ </PanelList>
112
+ );
113
+ }
114
+
115
+ function ErrorBox({ message, onRetry }: { message: string; onRetry?: () => void }) {
116
+ return (
117
+ <div className="flex items-center justify-between gap-3 bg-destructive/5 px-3.5 py-3">
118
+ <p className="min-w-0 text-xs leading-relaxed text-destructive">{message}</p>
119
+ {onRetry && (
120
+ <Button type="button" variant="outline" size="xs" onClick={onRetry} className="shrink-0">
121
+ Retry
122
+ </Button>
123
+ )}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ /** Ikon kotak kecil di awal baris daftar. */
129
+ function RowIcon({ children, className }: { children: ReactNode; className?: string }) {
130
+ return (
131
+ <span
132
+ className={cn(
133
+ "flex size-7 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&_svg]:size-3.5",
134
+ className,
135
+ )}
136
+ aria-hidden
137
+ >
138
+ {children}
139
+ </span>
140
+ );
141
+ }
142
+
143
+ // ---------------------------------------------------------------- MCP ----
144
+
145
+ /** Yang butuh perhatian di atas: failed -> needs_* -> connected -> sisanya. */
146
+ const MCP_ORDER: Record<McpServerInfo["status"], number> = {
147
+ failed: 0,
148
+ needs_auth: 1,
149
+ needs_client_registration: 1,
150
+ connected: 2,
151
+ unknown: 3,
152
+ disabled: 4,
153
+ };
154
+
155
+ const MCP_PILL: Record<McpServerInfo["status"], string> = {
156
+ connected: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
157
+ failed: "bg-destructive/10 text-destructive",
158
+ needs_auth: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
159
+ needs_client_registration: "bg-amber-500/10 text-amber-700 dark:text-amber-400",
160
+ disabled: "bg-muted text-muted-foreground",
161
+ unknown: "bg-muted text-muted-foreground",
162
+ };
163
+
164
+ function McpSection({ state, onReload }: { state: Load<McpServerInfo[]>; onReload: () => void }) {
165
+ const sorted = useMemo(
166
+ () =>
167
+ state.phase === "ready"
168
+ ? [...state.data].sort(
169
+ (a, b) => MCP_ORDER[a.status] - MCP_ORDER[b.status] || a.name.localeCompare(b.name),
170
+ )
171
+ : [],
172
+ [state],
173
+ );
174
+
175
+ return (
176
+ <PanelSection
177
+ id="mcp"
178
+ title="MCP servers"
179
+ icon={<PlugIcon />}
180
+ meta={state.phase === "ready" ? describeMcpSummary(state.data) : undefined}
181
+ action={
182
+ <Button
183
+ type="button"
184
+ variant="ghost"
185
+ size="icon-xs"
186
+ onClick={onReload}
187
+ disabled={state.phase === "loading"}
188
+ aria-label="Reload MCP servers and skills"
189
+ title="Reload"
190
+ className="size-8 text-muted-foreground"
191
+ >
192
+ <RefreshCwIcon className={cn(state.phase === "loading" && "animate-spin")} />
193
+ </Button>
194
+ }
195
+ >
196
+ {state.phase === "loading" ? (
197
+ <ListSkeleton rows={2} />
198
+ ) : state.phase === "error" ? (
199
+ <ErrorBox message={state.message} onRetry={onReload} />
200
+ ) : sorted.length === 0 ? (
201
+ <PanelHint>
202
+ No MCP servers yet. Add them under <code className="font-mono">mcp</code> in
203
+ opencode.json, then reload.
204
+ </PanelHint>
205
+ ) : (
206
+ <PanelList>
207
+ {sorted.map((server) => (
208
+ <li key={server.name} className="flex items-start gap-3 px-3.5 py-3">
209
+ <RowIcon
210
+ className={cn(
211
+ server.status === "connected" &&
212
+ "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
213
+ server.status === "failed" && "bg-destructive/10 text-destructive",
214
+ )}
215
+ >
216
+ <PlugIcon />
217
+ </RowIcon>
218
+ <div className="flex min-w-0 flex-1 flex-col gap-1 pt-0.5">
219
+ <div className="flex min-w-0 items-center justify-between gap-2">
220
+ <span className="truncate text-sm font-medium leading-5">{server.name}</span>
221
+ <span
222
+ className={cn(
223
+ "shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium leading-4",
224
+ MCP_PILL[server.status],
225
+ )}
226
+ >
227
+ {MCP_STATUS_LABEL[server.status]}
228
+ </span>
229
+ </div>
230
+ {server.error && (
231
+ <p
232
+ className="line-clamp-2 font-mono text-[11px] leading-relaxed break-all text-destructive/90"
233
+ title={server.error}
234
+ >
235
+ {server.error}
236
+ </p>
237
+ )}
238
+ </div>
239
+ </li>
240
+ ))}
241
+ </PanelList>
242
+ )}
243
+ </PanelSection>
244
+ );
245
+ }
246
+
247
+ // ------------------------------------------------------------- Skills ----
248
+
249
+ type SkillFilter = "all" | SkillSource;
250
+
251
+ function SkillsSection({ state }: { state: Load<SkillInfo[]> }) {
252
+ const [query, setQuery] = useState("");
253
+ const [filter, setFilter] = useState<SkillFilter>("all");
254
+ const [expanded, setExpanded] = useState(false);
255
+
256
+ const all = state.phase === "ready" ? state.data : [];
257
+ // Urutan tampil: Project -> global -> bawaan (paling relevan dulu).
258
+ const ordered = useMemo(() => groupSkills(all).flatMap((g) => g.skills), [all]);
259
+ const sources = useMemo(() => groupSkills(all).map((g) => g.source), [all]);
260
+ const bySource = useMemo(
261
+ () => (filter === "all" ? ordered : ordered.filter((s) => s.source === filter)),
262
+ [ordered, filter],
263
+ );
264
+ const filtered = useMemo(() => filterSkills(bySource, query), [bySource, query]);
265
+ const searching = query.trim() !== "";
266
+ // Saat mencari, tampilkan semua hasil; selain itu batasi hingga "Show more".
267
+ const visible = searching || expanded ? filtered : filtered.slice(0, SKILL_PREVIEW);
268
+ const hidden = filtered.length - visible.length;
269
+
270
+ return (
271
+ <PanelSection
272
+ id="skills"
273
+ title="Skills"
274
+ icon={<SparklesIcon />}
275
+ meta={
276
+ state.phase === "ready"
277
+ ? all.length === 0
278
+ ? "None found"
279
+ : `${all.length} available`
280
+ : undefined
281
+ }
282
+ >
283
+ {state.phase === "loading" ? (
284
+ <ListSkeleton rows={3} />
285
+ ) : state.phase === "error" ? (
286
+ <ErrorBox message={state.message} />
287
+ ) : all.length === 0 ? (
288
+ <PanelHint>
289
+ No skills found. Add a <code className="font-mono">SKILL.md</code> under{" "}
290
+ <code className="font-mono">.opencode/skills/</code>.
291
+ </PanelHint>
292
+ ) : (
293
+ <div className="flex flex-col">
294
+ {(all.length >= SKILL_SEARCH_THRESHOLD || sources.length > 1) && (
295
+ <PanelBody className="flex flex-col gap-2.5 border-b">
296
+ {all.length >= SKILL_SEARCH_THRESHOLD && (
297
+ <div className="relative">
298
+ <SearchIcon
299
+ className="pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2 text-muted-foreground"
300
+ aria-hidden
301
+ />
302
+ <input
303
+ type="search"
304
+ value={query}
305
+ onChange={(e) => setQuery(e.target.value)}
306
+ onKeyDown={(e) => e.key === "Escape" && setQuery("")}
307
+ placeholder={`Search ${all.length} skills`}
308
+ aria-label="Search skills"
309
+ className="h-9 w-full rounded-lg border bg-background pr-8 pl-8.5 text-sm outline-none transition-shadow placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&::-webkit-search-cancel-button]:hidden"
310
+ />
311
+ {query && (
312
+ <button
313
+ type="button"
314
+ onClick={() => setQuery("")}
315
+ aria-label="Clear search"
316
+ className="absolute top-1/2 right-2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:text-foreground"
317
+ >
318
+ <XIcon className="size-3.5" />
319
+ </button>
320
+ )}
321
+ </div>
322
+ )}
323
+
324
+ {sources.length > 1 && (
325
+ <fieldset
326
+ aria-label="Filter skills by source"
327
+ className="m-0 flex flex-wrap gap-1 border-0 p-0"
328
+ >
329
+ {(["all", ...sources] as SkillFilter[]).map((f) => {
330
+ const count =
331
+ f === "all" ? all.length : all.filter((s) => s.source === f).length;
332
+ const active = filter === f;
333
+ return (
334
+ <button
335
+ key={f}
336
+ type="button"
337
+ aria-pressed={active}
338
+ onClick={() => {
339
+ setFilter(f);
340
+ setExpanded(false);
341
+ }}
342
+ className={cn(
343
+ "inline-flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none",
344
+ active
345
+ ? "bg-foreground text-background"
346
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
347
+ )}
348
+ >
349
+ {f === "all" ? "All" : SKILL_SOURCE_LABEL[f]}
350
+ <span className={cn("tabular-nums", active ? "opacity-70" : "opacity-60")}>
351
+ {count}
352
+ </span>
353
+ </button>
354
+ );
355
+ })}
356
+ </fieldset>
357
+ )}
358
+ </PanelBody>
359
+ )}
360
+
361
+ {visible.length === 0 ? (
362
+ <PanelHint>No skills match "{query.trim()}".</PanelHint>
363
+ ) : (
364
+ <PanelList>
365
+ {visible.map((s) => (
366
+ <SkillRow key={`${s.source}:${s.name}`} skill={s} showSource={filter === "all"} />
367
+ ))}
368
+ {!searching && filtered.length > SKILL_PREVIEW && (
369
+ <li>
370
+ <button
371
+ type="button"
372
+ onClick={() => setExpanded((v) => !v)}
373
+ aria-expanded={expanded}
374
+ className="flex w-full items-center justify-center gap-1.5 px-3.5 py-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:bg-muted/50 focus-visible:outline-none"
375
+ >
376
+ {expanded ? "Show less" : `Show ${hidden} more`}
377
+ <ChevronDownIcon
378
+ className={cn("size-3.5 transition-transform", expanded && "rotate-180")}
379
+ aria-hidden
380
+ />
381
+ </button>
382
+ </li>
383
+ )}
384
+ </PanelList>
385
+ )}
386
+ </div>
387
+ )}
388
+ </PanelSection>
389
+ );
390
+ }
391
+
392
+ /**
393
+ * Satu skill: ikon, nama (+ label asal), deskripsi 1 baris. Klik baris untuk
394
+ * membuka deskripsi lengkap dan path SKILL.md.
395
+ */
396
+ function SkillRow({ skill, showSource }: { skill: SkillInfo; showSource: boolean }) {
397
+ const [open, setOpen] = useState(false);
398
+ const expandable = Boolean(skill.description || skill.location);
399
+ return (
400
+ <li>
401
+ <button
402
+ type="button"
403
+ onClick={() => expandable && setOpen((v) => !v)}
404
+ aria-expanded={expandable ? open : undefined}
405
+ disabled={!expandable}
406
+ className="flex w-full items-start gap-3 px-3.5 py-3 text-left transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
407
+ >
408
+ <RowIcon className={cn(skill.source === "project" && "bg-primary/10 text-primary")}>
409
+ <SparklesIcon />
410
+ </RowIcon>
411
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5 pt-0.5">
412
+ <span className="flex min-w-0 items-center gap-1.5">
413
+ <span className="truncate text-sm font-medium leading-5">{skill.name}</span>
414
+ {showSource && skill.source !== "global" && (
415
+ <span className="shrink-0 rounded bg-muted px-1.5 text-[10px] font-medium leading-4 text-muted-foreground">
416
+ {skill.source === "project" ? "Project" : "Built-in"}
417
+ </span>
418
+ )}
419
+ </span>
420
+ {skill.description && (
421
+ <span
422
+ className={cn(
423
+ "text-xs leading-relaxed text-muted-foreground",
424
+ open ? "whitespace-pre-line" : "line-clamp-1",
425
+ )}
426
+ >
427
+ {skill.description}
428
+ </span>
429
+ )}
430
+ {open && skill.location && (
431
+ <span className="mt-1 truncate font-mono text-[11px] text-muted-foreground/80">
432
+ {skill.location}
433
+ </span>
434
+ )}
435
+ </span>
436
+ {expandable && (
437
+ <ChevronDownIcon
438
+ className={cn(
439
+ "mt-1.5 size-3.5 shrink-0 text-muted-foreground/60 transition-transform",
440
+ open && "rotate-180",
441
+ )}
442
+ aria-hidden
443
+ />
444
+ )}
445
+ </button>
446
+ </li>
447
+ );
448
+ }