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,514 @@
1
+ /**
2
+ * Panel samping halaman Project — Instructions, MCP server, dan Skills dalam
3
+ * satu kolom di kanan daftar Session.
4
+ *
5
+ * Gaya libadwaita (GNOME Settings), memakai primitif `settings/prefs`:
6
+ * tiap seksi = AdwPreferencesGroup (judul + deskripsi di luar kotak, aksi
7
+ * datar di kanan judul) berisi "boxed list". Baris = AdwActionRow /
8
+ * AdwExpanderRow; kolom cari & filter (GtkSearchEntry, AdwToggleGroup) ada di
9
+ * antara judul dan kotak.
10
+ *
11
+ * MCP & Skills dibaca dari KEDUA agent (OpenCode & Claude Code) lalu
12
+ * digabung per nama, sama seperti halaman Skills:
13
+ * - MCP: `GET /api/projects/:id/mcp?agent=` — status per agent (logo +
14
+ * label) di subjudul; yang bermasalah paling atas, error tampil inline.
15
+ * Server Claude Code dibaca dari config (`.mcp.json`, `~/.claude.json`);
16
+ * statusnya "Not checked" sampai ada Session Claude Code yang berjalan.
17
+ * - Skills: `GET /api/projects/:id/skills?agent=` — cari (bila banyak) +
18
+ * filter asal; logo agent yang memuat skill; baris dibuka untuk deskripsi
19
+ * lengkap & path. Tombol "+" membuka halaman Skills untuk Project ini.
20
+ * Satu agent gagal dibaca tidak menggagalkan yang lain (baris catatan).
21
+ * Tiap seksi memuat datanya sendiri; tombol reload memuat ulang keduanya.
22
+ */
23
+ import { PlugIcon, PlusIcon, RefreshCwIcon, SparklesIcon, TriangleAlertIcon } from "lucide-react";
24
+ import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
25
+ import { ProjectInstructions } from "@/components/projects/ProjectInstructions";
26
+ import { AgentLogo } from "@/components/sessions/AgentLogo";
27
+ import { ActionRow, ExpanderRow, PrefsGroup, RowBadge } from "@/components/settings/prefs";
28
+ import { AgentBadges } from "@/components/skills/SkillCards";
29
+ import { AdwToggleGroup } from "@/components/ui/adw-toggle-group";
30
+ import { Button } from "@/components/ui/button";
31
+ import { SearchEntry } from "@/components/ui/search-entry";
32
+ import { Skeleton } from "@/components/ui/skeleton";
33
+ import { useRouter } from "@/hooks/useRouter";
34
+ import { AGENT_TYPE_LABELS, AGENT_TYPES, agentQuery } from "@/lib/agent-types";
35
+ import { ApiError, apiFetch } from "@/lib/api";
36
+ import {
37
+ describeMcpEntries,
38
+ filterSkills,
39
+ groupSkills,
40
+ MCP_STATUS_LABEL,
41
+ type McpServerEntry,
42
+ mergeMcpServers,
43
+ } from "@/lib/project-extensions";
44
+ import { skillsPath } from "@/lib/routes";
45
+ import { type InstalledSkill, mergeInstalledSkills } from "@/lib/skills-catalog";
46
+ import { cn } from "@/lib/utils";
47
+ import type { McpServerInfo, SkillInfo, SkillSource } from "@/server/services/opencode-client";
48
+ import type { AgentType, McpServerState, Project } from "@/types";
49
+
50
+ /** Skill ditampilkan terbatas dulu; sisanya lewat "Show more". */
51
+ const SKILL_PREVIEW = 5;
52
+ /** Kolom cari muncul hanya bila skill cukup banyak. */
53
+ const SKILL_SEARCH_THRESHOLD = 8;
54
+
55
+ /** Hasil per agent; `failed` = agent yang gagal dibaca (yang lain tetap tampil). */
56
+ interface AgentLists<T> {
57
+ byAgent: Partial<Record<AgentType, T[]>>;
58
+ failed: { agent: AgentType; message: string }[];
59
+ }
60
+
61
+ type Load<T> =
62
+ | { phase: "loading" }
63
+ | { phase: "error"; message: string }
64
+ | { phase: "ready"; data: T };
65
+
66
+ /**
67
+ * Muat satu endpoint list untuk setiap agent (`?agent=`) bersamaan.
68
+ * Error hanya bila SEMUA agent gagal. `reloadKey` berubah -> muat ulang.
69
+ */
70
+ function useAgentLists<T>(path: string, key: string, reloadKey: number): Load<AgentLists<T>> {
71
+ const [state, setState] = useState<Load<AgentLists<T>>>({ phase: "loading" });
72
+ useEffect(() => {
73
+ // `reloadKey` hanya pemicu muat ulang.
74
+ void reloadKey;
75
+ let cancelled = false;
76
+ setState({ phase: "loading" });
77
+ void Promise.allSettled(
78
+ AGENT_TYPES.map((a) =>
79
+ apiFetch(`${path}${agentQuery(a)}`).then(async (res) => {
80
+ const body = (await res.json()) as Record<string, T[]>;
81
+ return body[key] ?? [];
82
+ }),
83
+ ),
84
+ ).then((results) => {
85
+ if (cancelled) return;
86
+ const data: AgentLists<T> = { byAgent: {}, failed: [] };
87
+ results.forEach((r, i) => {
88
+ const agent = AGENT_TYPES[i] as AgentType;
89
+ if (r.status === "fulfilled") data.byAgent[agent] = r.value;
90
+ else {
91
+ const message = r.reason instanceof ApiError ? r.reason.message : "Failed to load";
92
+ data.failed.push({ agent, message });
93
+ }
94
+ });
95
+ if (data.failed.length === AGENT_TYPES.length) {
96
+ setState({ phase: "error", message: data.failed[0]?.message ?? "Failed to load" });
97
+ } else {
98
+ setState({ phase: "ready", data });
99
+ }
100
+ });
101
+ return () => {
102
+ cancelled = true;
103
+ };
104
+ }, [path, key, reloadKey]);
105
+ return state;
106
+ }
107
+
108
+ export interface ProjectSidePanelProps {
109
+ project: Project;
110
+ onProjectChange: (project: Project) => void;
111
+ }
112
+
113
+ export function ProjectSidePanel({ project, onProjectChange }: ProjectSidePanelProps) {
114
+ const [reloadKey, setReloadKey] = useState(0);
115
+ const reload = useCallback(() => setReloadKey((k) => k + 1), []);
116
+ const mcp = useAgentLists<McpServerInfo>(`/api/projects/${project.id}/mcp`, "mcp", reloadKey);
117
+ const skills = useAgentLists<SkillInfo>(
118
+ `/api/projects/${project.id}/skills`,
119
+ "skills",
120
+ reloadKey,
121
+ );
122
+
123
+ return (
124
+ <div className="flex flex-col gap-6">
125
+ <ProjectInstructions project={project} onSaved={onProjectChange} />
126
+ <McpGroup state={mcp} onReload={reload} />
127
+ <SkillsGroup state={skills} projectId={project.id} />
128
+ </div>
129
+ );
130
+ }
131
+
132
+ // ------------------------------------------------------------- shared ----
133
+
134
+ /** Tombol datar bundar di kanan judul grup (aksi header AdwPreferencesGroup). */
135
+ function FlatIconButton({
136
+ label,
137
+ onClick,
138
+ disabled,
139
+ children,
140
+ }: {
141
+ label: string;
142
+ onClick: () => void;
143
+ disabled?: boolean;
144
+ children: ReactNode;
145
+ }) {
146
+ return (
147
+ <Button
148
+ type="button"
149
+ variant="ghost"
150
+ size="icon"
151
+ onClick={onClick}
152
+ disabled={disabled}
153
+ aria-label={label}
154
+ title={label}
155
+ className="size-8 rounded-full text-foreground/80"
156
+ >
157
+ {children}
158
+ </Button>
159
+ );
160
+ }
161
+
162
+ /** Baris placeholder saat memuat — tinggi sama dengan ActionRow. */
163
+ function SkeletonRows({ rows }: { rows: number }) {
164
+ return Array.from({ length: rows }, (_, i) => (
165
+ // biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
166
+ <li key={i} className="flex min-h-14 flex-col justify-center gap-1.5 px-4 py-2.5" aria-hidden>
167
+ <Skeleton className="h-4" style={{ width: `${60 - i * 12}%` }} />
168
+ <Skeleton className="h-3 w-1/3" />
169
+ </li>
170
+ ));
171
+ }
172
+
173
+ /** Baris error dengan tombol coba lagi. */
174
+ function ErrorRow({ message, onRetry }: { message: string; onRetry?: () => void }) {
175
+ return (
176
+ <ActionRow
177
+ prefix={<TriangleAlertIcon className="size-[18px] text-destructive" aria-hidden />}
178
+ title="Couldn't load"
179
+ subtitle={message}
180
+ suffix={
181
+ onRetry && (
182
+ <Button type="button" variant="outline" size="sm" onClick={onRetry}>
183
+ Retry
184
+ </Button>
185
+ )
186
+ }
187
+ />
188
+ );
189
+ }
190
+
191
+ /** Baris catatan bila sebagian agent gagal dibaca. */
192
+ function PartialFailureRow({ failed }: { failed: AgentLists<unknown>["failed"] }) {
193
+ if (failed.length === 0) return null;
194
+ return (
195
+ <ActionRow
196
+ prefix={
197
+ <TriangleAlertIcon className="size-[18px] text-amber-600 dark:text-amber-400" aria-hidden />
198
+ }
199
+ title={`Couldn't read ${failed.map((f) => AGENT_TYPE_LABELS[f.agent]).join(", ")}`}
200
+ subtitle={failed[0]?.message}
201
+ />
202
+ );
203
+ }
204
+
205
+ // ---------------------------------------------------------------- MCP ----
206
+
207
+ /** Warna teks status MCP per agent. */
208
+ const MCP_TEXT: Record<McpServerState["status"], string> = {
209
+ connected: "text-emerald-700 dark:text-emerald-400",
210
+ failed: "text-destructive",
211
+ needs_auth: "text-amber-700 dark:text-amber-400",
212
+ needs_client_registration: "text-amber-700 dark:text-amber-400",
213
+ disabled: "text-muted-foreground",
214
+ unknown: "text-muted-foreground",
215
+ };
216
+
217
+ /** Warna ikon baris = status paling bermasalah. */
218
+ const MCP_ICON: Record<McpServerState["status"], string> = {
219
+ connected: "text-emerald-600 dark:text-emerald-400",
220
+ failed: "text-destructive",
221
+ needs_auth: "text-amber-600 dark:text-amber-400",
222
+ needs_client_registration: "text-amber-600 dark:text-amber-400",
223
+ disabled: "text-muted-foreground/60",
224
+ unknown: "text-muted-foreground",
225
+ };
226
+
227
+ /**
228
+ * Label status per agent. `unknown` di Claude Code berarti status belum
229
+ * pernah dilaporkan proses `claude` (server dibaca dari config).
230
+ */
231
+ function mcpAgentLabel(agent: AgentType, status: McpServerState["status"]): string {
232
+ if (agent === "claude-code" && status === "unknown") return "Not checked";
233
+ return MCP_STATUS_LABEL[status];
234
+ }
235
+
236
+ function McpGroup({
237
+ state,
238
+ onReload,
239
+ }: {
240
+ state: Load<AgentLists<McpServerInfo>>;
241
+ onReload: () => void;
242
+ }) {
243
+ const entries = useMemo(
244
+ () => (state.phase === "ready" ? mergeMcpServers(state.data.byAgent) : []),
245
+ [state],
246
+ );
247
+
248
+ return (
249
+ <PrefsGroup
250
+ title="MCP servers"
251
+ description={state.phase === "ready" ? describeMcpEntries(entries) : "Loading…"}
252
+ suffix={
253
+ <FlatIconButton
254
+ label="Reload MCP servers and skills"
255
+ onClick={onReload}
256
+ disabled={state.phase === "loading"}
257
+ >
258
+ <RefreshCwIcon className={cn(state.phase === "loading" && "animate-spin")} />
259
+ </FlatIconButton>
260
+ }
261
+ >
262
+ {state.phase === "loading" ? (
263
+ <SkeletonRows rows={2} />
264
+ ) : state.phase === "error" ? (
265
+ <ErrorRow message={state.message} onRetry={onReload} />
266
+ ) : (
267
+ <>
268
+ {entries.length === 0 ? (
269
+ <ActionRow
270
+ title="No MCP servers"
271
+ subtitle={
272
+ <>
273
+ Add them under <code className="font-mono">mcp</code> in opencode.json, or in{" "}
274
+ <code className="font-mono">.mcp.json</code> for Claude Code, then reload.
275
+ </>
276
+ }
277
+ />
278
+ ) : (
279
+ entries.map((entry) => <McpRow key={entry.name} entry={entry} />)
280
+ )}
281
+ <PartialFailureRow failed={state.data.failed} />
282
+ </>
283
+ )}
284
+ </PrefsGroup>
285
+ );
286
+ }
287
+
288
+ /**
289
+ * Satu MCP server (AdwActionRow): ikon simbolik berwarna status, nama, lalu
290
+ * status per agent di subjudul — "logo Label" untuk tiap agent pemakai.
291
+ * Error tampil di bawahnya per agent.
292
+ */
293
+ function McpRow({ entry }: { entry: McpServerEntry }) {
294
+ const errors = entry.agents
295
+ .map((a) => ({ agent: a, error: entry.byAgent[a]?.error ?? null }))
296
+ .filter((e): e is { agent: AgentType; error: string } => e.error !== null);
297
+ return (
298
+ <ActionRow
299
+ prefix={<PlugIcon className={cn("size-[18px]", MCP_ICON[entry.status])} aria-hidden />}
300
+ title={<span className="block truncate">{entry.name}</span>}
301
+ subtitle={
302
+ <span className="flex flex-col gap-1">
303
+ <span className="flex flex-wrap items-center gap-x-3 gap-y-0.5">
304
+ {entry.agents.map((a) => {
305
+ const status = entry.byAgent[a]?.status ?? "unknown";
306
+ const label = mcpAgentLabel(a, status);
307
+ return (
308
+ <span
309
+ key={a}
310
+ className={cn("inline-flex items-center gap-1.5", MCP_TEXT[status])}
311
+ title={
312
+ a === "claude-code" && status === "unknown"
313
+ ? "Configured for Claude Code. Status appears once a Claude Code session runs in this project."
314
+ : `${AGENT_TYPE_LABELS[a]}: ${label}`
315
+ }
316
+ >
317
+ <AgentLogo agentType={a} className="size-3" />
318
+ <span className="sr-only">{AGENT_TYPE_LABELS[a]}:</span>
319
+ {label}
320
+ </span>
321
+ );
322
+ })}
323
+ </span>
324
+ {errors.map((e) => (
325
+ <span
326
+ key={e.agent}
327
+ className="line-clamp-2 font-mono text-[11.5px] leading-relaxed break-all text-destructive/90"
328
+ title={e.error}
329
+ >
330
+ {entry.agents.length > 1 && `${AGENT_TYPE_LABELS[e.agent]}: `}
331
+ {e.error}
332
+ </span>
333
+ ))}
334
+ </span>
335
+ }
336
+ />
337
+ );
338
+ }
339
+
340
+ // ------------------------------------------------------------- Skills ----
341
+
342
+ type SkillFilter = "all" | SkillSource;
343
+
344
+ const SKILL_FILTER_LABEL: Record<SkillFilter, string> = {
345
+ all: "All",
346
+ project: "Project",
347
+ global: "Global",
348
+ builtin: "Built-in",
349
+ };
350
+
351
+ function SkillsGroup({
352
+ state,
353
+ projectId,
354
+ }: {
355
+ state: Load<AgentLists<SkillInfo>>;
356
+ projectId: string;
357
+ }) {
358
+ const { navigate } = useRouter();
359
+ const [query, setQuery] = useState("");
360
+ const [filter, setFilter] = useState<SkillFilter>("all");
361
+ const [expanded, setExpanded] = useState(false);
362
+
363
+ // Skill yang sama di kedua agent jadi satu baris dengan dua logo.
364
+ const all = useMemo(
365
+ () => (state.phase === "ready" ? mergeInstalledSkills(state.data.byAgent) : []),
366
+ [state],
367
+ );
368
+ // Urutan tampil: Project -> global -> bawaan (paling relevan dulu).
369
+ const groups = useMemo(() => groupSkills(all), [all]);
370
+ const ordered = useMemo(() => groups.flatMap((g) => g.skills), [groups]);
371
+ const sources = useMemo(() => groups.map((g) => g.source), [groups]);
372
+ const bySource = useMemo(
373
+ () => (filter === "all" ? ordered : ordered.filter((s) => s.source === filter)),
374
+ [ordered, filter],
375
+ );
376
+ const filtered = useMemo(() => filterSkills(bySource, query), [bySource, query]);
377
+ const searching = query.trim() !== "";
378
+ // Saat mencari, tampilkan semua hasil; selain itu batasi hingga "Show more".
379
+ const visible = searching || expanded ? filtered : filtered.slice(0, SKILL_PREVIEW);
380
+ const hidden = filtered.length - visible.length;
381
+ const browse = () => navigate(skillsPath(projectId));
382
+
383
+ const showSearch = all.length >= SKILL_SEARCH_THRESHOLD;
384
+ const showFilter = sources.length > 1;
385
+ const toolbar =
386
+ state.phase === "ready" && (showSearch || showFilter) ? (
387
+ <div className="flex flex-col gap-2">
388
+ {showSearch && (
389
+ <SearchEntry
390
+ value={query}
391
+ onValueChange={setQuery}
392
+ placeholder={`Search ${all.length} skills`}
393
+ aria-label="Search skills"
394
+ />
395
+ )}
396
+ {showFilter && (
397
+ <AdwToggleGroup
398
+ label="Filter skills by source"
399
+ value={filter}
400
+ onChange={(f) => {
401
+ setFilter(f);
402
+ setExpanded(false);
403
+ }}
404
+ options={(["all", ...sources] as SkillFilter[]).map((f) => {
405
+ const count = f === "all" ? all.length : all.filter((s) => s.source === f).length;
406
+ return {
407
+ value: f,
408
+ label: SKILL_FILTER_LABEL[f],
409
+ title: `${SKILL_FILTER_LABEL[f]}: ${count}`,
410
+ ariaLabel: `${SKILL_FILTER_LABEL[f]}, ${count} skills`,
411
+ };
412
+ })}
413
+ className="[&_button]:text-[13px]"
414
+ />
415
+ )}
416
+ </div>
417
+ ) : undefined;
418
+
419
+ return (
420
+ <PrefsGroup
421
+ title="Skills"
422
+ description={
423
+ state.phase === "ready"
424
+ ? all.length === 0
425
+ ? "None installed"
426
+ : `${all.length} available${filter !== "all" || searching ? ` · ${filtered.length} shown` : ""}`
427
+ : "Loading…"
428
+ }
429
+ suffix={
430
+ <FlatIconButton label="Browse and install skills" onClick={browse}>
431
+ <PlusIcon />
432
+ </FlatIconButton>
433
+ }
434
+ toolbar={toolbar}
435
+ >
436
+ {state.phase === "loading" ? (
437
+ <SkeletonRows rows={3} />
438
+ ) : state.phase === "error" ? (
439
+ <ErrorRow message={state.message} />
440
+ ) : (
441
+ <>
442
+ {all.length === 0 ? (
443
+ <ActionRow
444
+ prefix={<SparklesIcon className="size-[18px] text-muted-foreground" aria-hidden />}
445
+ title="Browse skills"
446
+ subtitle="Install one from skills.sh for OpenCode, Claude Code, or both"
447
+ onActivate={browse}
448
+ />
449
+ ) : visible.length === 0 ? (
450
+ <li className="px-4 py-4 text-center text-[13px] text-muted-foreground">
451
+ No skills match “{query.trim()}”.
452
+ </li>
453
+ ) : (
454
+ visible.map((s) => (
455
+ <SkillRow key={`${s.source}:${s.name}`} skill={s} showSource={filter === "all"} />
456
+ ))
457
+ )}
458
+ {!searching && filtered.length > SKILL_PREVIEW && (
459
+ <li>
460
+ <button
461
+ type="button"
462
+ onClick={() => setExpanded((v) => !v)}
463
+ aria-expanded={expanded}
464
+ className="flex min-h-12 w-full items-center justify-center px-4 text-[15px] transition-colors outline-none hover:bg-muted/60 focus-visible:bg-muted/60 focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:ring-inset active:bg-muted"
465
+ >
466
+ {expanded ? "Show less" : `Show ${hidden} more`}
467
+ </button>
468
+ </li>
469
+ )}
470
+ <PartialFailureRow failed={state.data.failed} />
471
+ </>
472
+ )}
473
+ </PrefsGroup>
474
+ );
475
+ }
476
+
477
+ /**
478
+ * Satu skill (AdwExpanderRow): nama (+ label asal), deskripsi 1 baris, logo
479
+ * agent pemakai. Dibuka -> deskripsi lengkap & path SKILL.md.
480
+ */
481
+ function SkillRow({ skill, showSource }: { skill: InstalledSkill; showSource: boolean }) {
482
+ const details =
483
+ skill.description || skill.location ? (
484
+ <div className="flex flex-col gap-2">
485
+ {skill.description && (
486
+ <p className="text-[13px] leading-relaxed whitespace-pre-line">{skill.description}</p>
487
+ )}
488
+ {skill.location && (
489
+ <p
490
+ className="truncate font-mono text-[11.5px] text-muted-foreground"
491
+ title={skill.location}
492
+ >
493
+ {skill.location}
494
+ </p>
495
+ )}
496
+ </div>
497
+ ) : undefined;
498
+ return (
499
+ <ExpanderRow
500
+ title={
501
+ <>
502
+ <span className="truncate">{skill.name}</span>
503
+ {showSource && skill.source !== "global" && (
504
+ <RowBadge>{skill.source === "project" ? "Project" : "Built-in"}</RowBadge>
505
+ )}
506
+ </>
507
+ }
508
+ subtitle={skill.description ?? undefined}
509
+ suffix={<AgentBadges agents={skill.agents} size="sm" />}
510
+ >
511
+ {details}
512
+ </ExpanderRow>
513
+ );
514
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Logo backend Session (OpenCode / Claude Code) untuk pemilih agent.
3
+ *
4
+ * - OpenCode : tanda "frame" dari favicon resmi opencode.ai, digambar ulang
5
+ * dengan `currentColor` agar mengikuti tema terang/gelap.
6
+ * - Claude : tanda Claude (simple-icons) dengan warna brand #D97757.
7
+ */
8
+ import { cn } from "@/lib/utils";
9
+ import type { AgentType } from "@/types";
10
+
11
+ const CLAUDE_PATH =
12
+ "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z";
13
+
14
+ export function AgentLogo({ agentType, className }: { agentType: AgentType; className?: string }) {
15
+ if (agentType === "claude-code") {
16
+ return (
17
+ <svg
18
+ viewBox="0 0 24 24"
19
+ aria-hidden="true"
20
+ className={cn("size-4 shrink-0", className)}
21
+ fill="#D97757"
22
+ >
23
+ <path d={CLAUDE_PATH} />
24
+ </svg>
25
+ );
26
+ }
27
+ return (
28
+ <svg
29
+ viewBox="96 96 320 320"
30
+ aria-hidden="true"
31
+ className={cn("size-4 shrink-0", className)}
32
+ fill="currentColor"
33
+ >
34
+ <path d="M320 224V352H192V224H320Z" opacity={0.4} />
35
+ <path
36
+ fillRule="evenodd"
37
+ clipRule="evenodd"
38
+ d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z"
39
+ />
40
+ </svg>
41
+ );
42
+ }