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,113 @@
1
+ /**
2
+ * Klien passkey (WebAuthn) — membungkus `@simplewebauthn/browser` + API
3
+ * `/api/auth/passkeys/*` agar Settings & lock screen memakai alur yang sama.
4
+ *
5
+ * Pembatalan oleh pengguna (menutup dialog sistem) bukan error: dikembalikan
6
+ * sebagai `{ ok: false, cancelled: true }` supaya UI tidak menampilkan pesan
7
+ * merah untuk tindakan yang disengaja.
8
+ */
9
+ import {
10
+ browserSupportsWebAuthn,
11
+ startAuthentication,
12
+ startRegistration,
13
+ } from "@simplewebauthn/browser";
14
+ import { ApiError, apiFetch } from "@/lib/api";
15
+ import type { PasskeyAvailability, PasskeyInfo } from "@/server/services/auth-passkeys";
16
+
17
+ export type { PasskeyAvailability, PasskeyInfo };
18
+
19
+ export type PasskeyResult<T> =
20
+ | { ok: true; data: T }
21
+ | { ok: false; cancelled: true }
22
+ | { ok: false; cancelled: false; message: string; code?: string };
23
+
24
+ /** Browser ini bisa memakai passkey (API WebAuthn + secure context). */
25
+ export function passkeysSupportedHere(): boolean {
26
+ if (typeof window === "undefined") return false;
27
+ return window.isSecureContext && browserSupportsWebAuthn();
28
+ }
29
+
30
+ /**
31
+ * Error dari `navigator.credentials` -> pesan ramah. `NotAllowedError` =
32
+ * dibatalkan / timeout oleh pengguna (diperlakukan sebagai batal).
33
+ */
34
+ export function describeWebAuthnError(e: unknown): PasskeyResult<never> {
35
+ if (e instanceof ApiError)
36
+ return { ok: false, cancelled: false, message: e.message, code: e.code };
37
+ const name = e instanceof Error ? e.name : "";
38
+ if (name === "NotAllowedError" || name === "AbortError") return { ok: false, cancelled: true };
39
+ if (name === "InvalidStateError") {
40
+ return { ok: false, cancelled: false, message: "This device already has a passkey here." };
41
+ }
42
+ if (name === "SecurityError") {
43
+ return {
44
+ ok: false,
45
+ cancelled: false,
46
+ message: "Passkeys aren't allowed on this address. Use https or localhost.",
47
+ };
48
+ }
49
+ return { ok: false, cancelled: false, message: "Something went wrong with the passkey." };
50
+ }
51
+
52
+ export async function getPasskeyAvailability(): Promise<PasskeyAvailability> {
53
+ const res = await apiFetch("/api/auth/passkeys/availability");
54
+ return (await res.json()) as PasskeyAvailability;
55
+ }
56
+
57
+ export async function listPasskeys(): Promise<PasskeyInfo[]> {
58
+ const res = await apiFetch("/api/auth/passkeys");
59
+ return ((await res.json()) as { passkeys: PasskeyInfo[] }).passkeys;
60
+ }
61
+
62
+ /** Daftarkan passkey baru; `current` = PIN/password saat ini (konfirmasi). */
63
+ export async function addPasskey(current: string): Promise<PasskeyResult<PasskeyInfo>> {
64
+ try {
65
+ const optRes = await apiFetch("/api/auth/passkeys/register/options", {
66
+ method: "POST",
67
+ body: JSON.stringify({ current }),
68
+ });
69
+ const { challengeId, options } = (await optRes.json()) as {
70
+ challengeId: string;
71
+ options: Parameters<typeof startRegistration>[0]["optionsJSON"];
72
+ };
73
+ const response = await startRegistration({ optionsJSON: options });
74
+ const res = await apiFetch("/api/auth/passkeys/register", {
75
+ method: "POST",
76
+ body: JSON.stringify({ challengeId, response }),
77
+ });
78
+ return { ok: true, data: ((await res.json()) as { passkey: PasskeyInfo }).passkey };
79
+ } catch (e) {
80
+ return describeWebAuthnError(e);
81
+ }
82
+ }
83
+
84
+ /** Buka kunci dengan passkey. Sukses = cookie sesi sudah diset server. */
85
+ export async function unlockWithPasskey(): Promise<PasskeyResult<null>> {
86
+ try {
87
+ const optRes = await apiFetch("/api/auth/passkeys/login/options", { method: "POST" });
88
+ const { challengeId, options } = (await optRes.json()) as {
89
+ challengeId: string;
90
+ options: Parameters<typeof startAuthentication>[0]["optionsJSON"];
91
+ };
92
+ const response = await startAuthentication({ optionsJSON: options });
93
+ await apiFetch("/api/auth/passkeys/login", {
94
+ method: "POST",
95
+ body: JSON.stringify({ challengeId, response }),
96
+ });
97
+ return { ok: true, data: null };
98
+ } catch (e) {
99
+ return describeWebAuthnError(e);
100
+ }
101
+ }
102
+
103
+ export async function renamePasskey(id: string, name: string): Promise<PasskeyInfo> {
104
+ const res = await apiFetch(`/api/auth/passkeys/${encodeURIComponent(id)}`, {
105
+ method: "PATCH",
106
+ body: JSON.stringify({ name }),
107
+ });
108
+ return ((await res.json()) as { passkey: PasskeyInfo }).passkey;
109
+ }
110
+
111
+ export async function removePasskey(id: string): Promise<void> {
112
+ await apiFetch(`/api/auth/passkeys/${encodeURIComponent(id)}`, { method: "DELETE" });
113
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Prompt pertama yang diketik di homepage, dititipkan sampai Session
3
+ * barunya ter-attach di Session view (`useSessionChat`) lalu dikirim sekali.
4
+ *
5
+ * Disimpan di `sessionStorage` (per tab) sehingga tetap aman walau halaman
6
+ * Session dimuat ulang sebelum koneksi WS siap; `take` menghapusnya agar
7
+ * prompt tidak terkirim dua kali.
8
+ */
9
+
10
+ const KEY_PREFIX = "kcg-pending-prompt:";
11
+
12
+ /** Storage minimal — diinjeksi saat test (default `sessionStorage`). */
13
+ export interface PendingStore {
14
+ getItem(key: string): string | null;
15
+ setItem(key: string, value: string): void;
16
+ removeItem(key: string): void;
17
+ }
18
+
19
+ function defaultStore(): PendingStore | null {
20
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
21
+ }
22
+
23
+ export function setPendingPrompt(
24
+ sessionId: string,
25
+ text: string,
26
+ store: PendingStore | null = defaultStore(),
27
+ ): void {
28
+ const trimmed = text.trim();
29
+ if (store === null || trimmed === "") return;
30
+ store.setItem(KEY_PREFIX + sessionId, trimmed);
31
+ }
32
+
33
+ /** Baca prompt tertunda tanpa menghapusnya (untuk pesan optimistis di UI). */
34
+ export function peekPendingPrompt(
35
+ sessionId: string,
36
+ store: PendingStore | null = defaultStore(),
37
+ ): string | null {
38
+ if (store === null) return null;
39
+ const value = store.getItem(KEY_PREFIX + sessionId);
40
+ return value !== null && value.trim() !== "" ? value : null;
41
+ }
42
+
43
+ /** Ambil sekaligus hapus prompt tertunda; null bila tidak ada. */
44
+ export function takePendingPrompt(
45
+ sessionId: string,
46
+ store: PendingStore | null = defaultStore(),
47
+ ): string | null {
48
+ if (store === null) return null;
49
+ const key = KEY_PREFIX + sessionId;
50
+ const value = store.getItem(key);
51
+ if (value !== null) store.removeItem(key);
52
+ return value !== null && value.trim() !== "" ? value : null;
53
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Penjelasan izin tool untuk manusia — logika murni (tanpa DOM, diuji).
3
+ *
4
+ * Server menyimpan judul permission sebagai satu string ringkas hasil
5
+ * `describePermission`: `"<permission> — `pola1` — `pola2`"` (mis.
6
+ * ``external_directory — `/home/u/.config/opencode/*` ``). String itu
7
+ * akurat tapi tidak ramah: nama permission internal opencode + pola glob
8
+ * mentah. Helper ini memecahnya menjadi:
9
+ * - `label` : kalimat singkat apa yang diminta agent ("Access a folder
10
+ * outside the project").
11
+ * - `targets`: pola yang terdampak, tanpa backtick.
12
+ * - `risk` : seberapa hati-hati pengguna perlu (menentukan warna kartu).
13
+ *
14
+ * Daftar permission mengikuti skema `PermissionConfig` opencode. Permission
15
+ * yang tidak dikenal (versi opencode lebih baru) tetap tampil dengan nama
16
+ * aslinya — tidak pernah disembunyikan.
17
+ */
18
+
19
+ export type PermissionRisk = "low" | "medium" | "high";
20
+
21
+ export interface PermissionInfo {
22
+ /** Nama permission opencode apa adanya (mis. `external_directory`). */
23
+ permission: string | null;
24
+ /** Kalimat ramah tentang apa yang diminta. */
25
+ label: string;
26
+ /** Pola/path/perintah yang terdampak (tanpa backtick). */
27
+ targets: string[];
28
+ risk: PermissionRisk;
29
+ }
30
+
31
+ interface PermissionMeta {
32
+ label: string;
33
+ risk: PermissionRisk;
34
+ }
35
+
36
+ /**
37
+ * Permission opencode -> label & risiko.
38
+ * - high : bisa mengubah sistem / keluar dari batas project.
39
+ * - medium : mengubah file project / menjalankan agent lain / akses jaringan.
40
+ * - low : hanya membaca.
41
+ */
42
+ const PERMISSIONS: Record<string, PermissionMeta> = {
43
+ bash: { label: "Run a shell command", risk: "high" },
44
+ external_directory: { label: "Access a folder outside the project", risk: "high" },
45
+ doom_loop: { label: "Keep going after repeating the same step", risk: "medium" },
46
+ edit: { label: "Edit files", risk: "medium" },
47
+ task: { label: "Start a sub-agent", risk: "medium" },
48
+ webfetch: { label: "Fetch a web page", risk: "medium" },
49
+ websearch: { label: "Search the web", risk: "medium" },
50
+ skill: { label: "Use a skill", risk: "medium" },
51
+ read: { label: "Read files", risk: "low" },
52
+ list: { label: "List folder contents", risk: "low" },
53
+ glob: { label: "Search for files", risk: "low" },
54
+ grep: { label: "Search inside files", risk: "low" },
55
+ lsp: { label: "Use the language server", risk: "low" },
56
+ todowrite: { label: "Update the task list", risk: "low" },
57
+ question: { label: "Ask you a question", risk: "low" },
58
+ };
59
+
60
+ /** Judul fallback server saat event tidak membawa nama permission. */
61
+ const FALLBACK_TITLES = new Set(["Izin tool", "Tool permission"]);
62
+
63
+ /** Ubah `snake_case` menjadi kalimat ("external_directory" -> "External directory"). */
64
+ function humanize(name: string): string {
65
+ const spaced = name.replace(/[_-]+/g, " ").trim();
66
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
67
+ }
68
+
69
+ /**
70
+ * Pecah judul permission server menjadi info yang bisa dibaca manusia.
71
+ * Format masukan: `"<permission>"`, `"<permission> — `a` — `b`"`, atau
72
+ * judul fallback / null.
73
+ */
74
+ export function parsePermissionTitle(title: string | null): PermissionInfo {
75
+ const raw = (title ?? "").trim();
76
+ if (raw === "" || FALLBACK_TITLES.has(raw)) {
77
+ return { permission: null, label: "Use a tool", targets: [], risk: "medium" };
78
+ }
79
+
80
+ const [head = "", ...rest] = raw.split(" — ");
81
+ const permission = head.trim();
82
+ // Pola dibungkus backtick oleh server; buang backtick-nya. Bagian tanpa
83
+ // backtick tetap diterima (format lama / pihak lain).
84
+ const targets = rest
85
+ .map((part) =>
86
+ part
87
+ .trim()
88
+ .replace(/^`(.*)`$/s, "$1")
89
+ .trim(),
90
+ )
91
+ .filter((t) => t !== "");
92
+
93
+ const meta = PERMISSIONS[permission.toLowerCase()];
94
+ return {
95
+ permission,
96
+ label: meta?.label ?? humanize(permission),
97
+ targets,
98
+ // Permission tak dikenal: anggap sedang — jangan meremehkan risikonya.
99
+ risk: meta?.risk ?? "medium",
100
+ };
101
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Helper tampilan panel samping halaman Project (MCP & skill) — logika murni
3
+ * tanpa DOM, dipisah agar dapat diuji `bun test`.
4
+ */
5
+ import { AGENT_TYPES } from "@/lib/agent-types";
6
+ import type {
7
+ McpServerInfo,
8
+ McpStatus,
9
+ SkillInfo,
10
+ SkillSource,
11
+ } from "@/server/services/opencode-client";
12
+ import type { AgentType } from "@/types";
13
+
14
+ /** Label status MCP untuk UI. */
15
+ export const MCP_STATUS_LABEL: Record<McpStatus, string> = {
16
+ connected: "Connected",
17
+ disabled: "Disabled",
18
+ failed: "Failed",
19
+ needs_auth: "Needs auth",
20
+ needs_client_registration: "Needs registration",
21
+ unknown: "Unknown",
22
+ };
23
+
24
+ /** Warna titik status MCP (sepadan dengan titik status Session). */
25
+ export const MCP_STATUS_DOT: Record<McpStatus, string> = {
26
+ connected: "bg-emerald-500",
27
+ disabled: "bg-muted-foreground/40",
28
+ failed: "bg-destructive",
29
+ needs_auth: "bg-amber-500",
30
+ needs_client_registration: "bg-amber-500",
31
+ unknown: "bg-muted-foreground/40",
32
+ };
33
+
34
+ /** Ringkasan singkat untuk header seksi MCP ("2 of 3 connected"). */
35
+ export function describeMcpSummary(servers: readonly McpServerInfo[]): string {
36
+ if (servers.length === 0) return "None configured";
37
+ const connected = servers.filter((s) => s.status === "connected").length;
38
+ return `${connected} of ${servers.length} connected`;
39
+ }
40
+
41
+ /** Urutan perhatian status MCP: yang bermasalah lebih dulu. */
42
+ const MCP_ATTENTION: Record<McpStatus, number> = {
43
+ failed: 0,
44
+ needs_auth: 1,
45
+ needs_client_registration: 1,
46
+ connected: 2,
47
+ unknown: 3,
48
+ disabled: 4,
49
+ };
50
+
51
+ /** Satu MCP server (per nama) beserta statusnya di tiap agent yang memakainya. */
52
+ export interface McpServerEntry {
53
+ name: string;
54
+ /** Status per agent; hanya agent yang mengonfigurasi server ini. */
55
+ byAgent: Partial<Record<AgentType, McpServerInfo>>;
56
+ /** Agent pemakai, urut `AGENT_TYPES`. */
57
+ agents: AgentType[];
58
+ /** Status paling butuh perhatian di antara agent (untuk ikon & urutan). */
59
+ status: McpStatus;
60
+ }
61
+
62
+ /**
63
+ * Gabungkan daftar MCP per agent menjadi satu daftar per nama server,
64
+ * terurut: bermasalah dulu, lalu nama. Murni — diuji.
65
+ */
66
+ export function mergeMcpServers(
67
+ byAgent: Partial<Record<AgentType, readonly McpServerInfo[]>>,
68
+ ): McpServerEntry[] {
69
+ const merged = new Map<string, McpServerEntry>();
70
+ for (const agent of AGENT_TYPES) {
71
+ for (const server of byAgent[agent] ?? []) {
72
+ const entry = merged.get(server.name) ?? {
73
+ name: server.name,
74
+ byAgent: {},
75
+ agents: [],
76
+ status: server.status,
77
+ };
78
+ entry.byAgent[agent] = server;
79
+ entry.agents.push(agent);
80
+ if (MCP_ATTENTION[server.status] < MCP_ATTENTION[entry.status]) entry.status = server.status;
81
+ merged.set(server.name, entry);
82
+ }
83
+ }
84
+ return [...merged.values()].sort(
85
+ (a, b) => MCP_ATTENTION[a.status] - MCP_ATTENTION[b.status] || a.name.localeCompare(b.name),
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Ringkasan header seksi MCP gabungan: berapa server yang tersambung di
91
+ * setidaknya satu agent ("1 of 2 connected").
92
+ */
93
+ export function describeMcpEntries(entries: readonly McpServerEntry[]): string {
94
+ if (entries.length === 0) return "None configured";
95
+ const connected = entries.filter((e) =>
96
+ Object.values(e.byAgent).some((s) => s?.status === "connected"),
97
+ ).length;
98
+ return `${connected} of ${entries.length} connected`;
99
+ }
100
+
101
+ /** Label asal skill. */
102
+ export const SKILL_SOURCE_LABEL: Record<SkillSource, string> = {
103
+ project: "This project",
104
+ global: "Global",
105
+ builtin: "Built-in",
106
+ };
107
+
108
+ export interface SkillGroup<T extends SkillInfo = SkillInfo> {
109
+ source: SkillSource;
110
+ label: string;
111
+ skills: T[];
112
+ }
113
+
114
+ /**
115
+ * Kelompokkan skill per asal: Project dulu (paling relevan), lalu global,
116
+ * lalu bawaan. Kelompok kosong dibuang; urutan dalam kelompok dipertahankan.
117
+ */
118
+ export function groupSkills<T extends SkillInfo>(skills: readonly T[]): SkillGroup<T>[] {
119
+ const order: SkillSource[] = ["project", "global", "builtin"];
120
+ return order
121
+ .map((source) => ({
122
+ source,
123
+ label: SKILL_SOURCE_LABEL[source],
124
+ skills: skills.filter((s) => s.source === source),
125
+ }))
126
+ .filter((g) => g.skills.length > 0);
127
+ }
128
+
129
+ /** Filter skill berdasarkan kata kunci (nama atau deskripsi, case-insensitive). */
130
+ export function filterSkills<T extends SkillInfo>(skills: readonly T[], query: string): T[] {
131
+ const q = query.trim().toLowerCase();
132
+ if (q === "") return [...skills];
133
+ return skills.filter(
134
+ (s) => s.name.toLowerCase().includes(q) || (s.description ?? "").toLowerCase().includes(q),
135
+ );
136
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Ringkasan Project untuk homepage — logika murni (tanpa DOM / fetch).
3
+ *
4
+ * Homepage (`pages/projects/ProjectsPage.tsx`) menggabungkan `GET /api/projects`
5
+ * dengan `GET /api/sessions` agar setiap Project bisa langsung "dilanjutkan":
6
+ * jumlah Session, Session yang sedang berjalan, dan Session terakhir yang
7
+ * disentuh. Dipisah ke `lib/` supaya bisa diuji `bun test` (pola yang sama
8
+ * dengan `lib/routes.ts`).
9
+ */
10
+
11
+ import type { Project, Session } from "@/types";
12
+
13
+ /** Project + agregat Session miliknya, siap dirender di daftar homepage. */
14
+ export interface ProjectOverview {
15
+ project: Project;
16
+ /** Jumlah Session milik Project ini. */
17
+ sessionCount: number;
18
+ /** Jumlah Session berstatus `running`. */
19
+ runningCount: number;
20
+ /**
21
+ * Session yang paling baru diperbarui (`updatedAt` terbesar) — target tombol
22
+ * "Lanjutkan"; `null` bila Project belum punya Session.
23
+ */
24
+ lastSession: Session | null;
25
+ /**
26
+ * Hingga `RECENT_SESSION_LIMIT` Session terbaru (`updatedAt` menurun) —
27
+ * daftar "lanjutkan" di bawah composer homepage.
28
+ */
29
+ recentSessions: Session[];
30
+ /**
31
+ * Waktu aktivitas terakhir: `updatedAt` Session terbaru, atau `createdAt`
32
+ * Project bila belum ada Session. Dipakai untuk urutan daftar.
33
+ */
34
+ lastActivityAt: number;
35
+ }
36
+
37
+ /** Jumlah Session terbaru yang dibawa tiap `ProjectOverview`. */
38
+ export const RECENT_SESSION_LIMIT = 3;
39
+
40
+ /**
41
+ * Gabungkan Project dengan Session-nya, terurut dari aktivitas terbaru.
42
+ * Session dengan `projectId` yang tidak dikenal diabaikan.
43
+ */
44
+ export function buildProjectOverviews(
45
+ projects: readonly Project[],
46
+ sessions: readonly Session[],
47
+ ): ProjectOverview[] {
48
+ const byProject = new Map<string, Session[]>();
49
+ for (const session of sessions) {
50
+ const bucket = byProject.get(session.projectId);
51
+ if (bucket === undefined) byProject.set(session.projectId, [session]);
52
+ else bucket.push(session);
53
+ }
54
+
55
+ const overviews = projects.map((project) => {
56
+ const own = byProject.get(project.id) ?? [];
57
+ const runningCount = own.filter((s) => s.status === "running").length;
58
+ const recentSessions = [...own]
59
+ .sort((a, b) => b.updatedAt - a.updatedAt)
60
+ .slice(0, RECENT_SESSION_LIMIT);
61
+ const lastSession = recentSessions[0] ?? null;
62
+ return {
63
+ project,
64
+ sessionCount: own.length,
65
+ runningCount,
66
+ lastSession,
67
+ recentSessions,
68
+ lastActivityAt: lastSession?.updatedAt ?? project.createdAt,
69
+ };
70
+ });
71
+
72
+ // Aktivitas terbaru di atas; nama (case-insensitive) sebagai tie-break agar
73
+ // urutan stabil dan tidak berubah-ubah antar refresh.
74
+ return overviews.sort((a, b) => {
75
+ if (b.lastActivityAt !== a.lastActivityAt) return b.lastActivityAt - a.lastActivityAt;
76
+ return a.project.name.localeCompare(b.project.name, undefined, { sensitivity: "base" });
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Filter daftar berdasarkan kata kunci (cocok pada nama atau path, tanpa
82
+ * membedakan huruf besar/kecil). Query kosong/whitespace -> daftar utuh.
83
+ */
84
+ export function filterProjectOverviews(
85
+ overviews: readonly ProjectOverview[],
86
+ query: string,
87
+ ): ProjectOverview[] {
88
+ const needle = query.trim().toLowerCase();
89
+ if (needle === "") return [...overviews];
90
+ return overviews.filter(
91
+ ({ project }) =>
92
+ project.name.toLowerCase().includes(needle) || project.path.toLowerCase().includes(needle),
93
+ );
94
+ }
95
+
96
+ /** Total Session yang sedang berjalan di seluruh Project. */
97
+ export function totalRunningSessions(overviews: readonly ProjectOverview[]): number {
98
+ return overviews.reduce((sum, o) => sum + o.runningCount, 0);
99
+ }
100
+
101
+ /**
102
+ * Short relative label for activity time ("just now", "3 hours ago").
103
+ * `now` diinjeksi agar deterministik saat diuji.
104
+ */
105
+ export function formatRelativeTime(ts: number, now: number = Date.now()): string {
106
+ const diff = now - ts;
107
+ if (diff < 0) return "just now";
108
+
109
+ const minute = 60_000;
110
+ const hour = 60 * minute;
111
+ const day = 24 * hour;
112
+
113
+ if (diff < minute) return "just now";
114
+ if (diff < hour) return `${Math.floor(diff / minute)} minutes ago`;
115
+ if (diff < day) return `${Math.floor(diff / hour)} hours ago`;
116
+ if (diff < 7 * day) return `${Math.floor(diff / day)} days ago`;
117
+
118
+ return new Date(ts).toLocaleDateString(undefined, {
119
+ day: "2-digit",
120
+ month: "short",
121
+ year: "numeric",
122
+ });
123
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Interactive_Prompt — logika murni (tanpa DOM).
3
+ *
4
+ * Dipisah dari `PromptCard.tsx` agar dapat diuji `bun test` (unit test 24.6):
5
+ * - `getPromptActions`: aksi yang dirender untuk sebuah prompt.
6
+ * - `groupPrompts`: beberapa permission identik (kind + title sama) menjadi
7
+ * SATU kartu (bug kartu nyepam).
8
+ */
9
+ import type { InteractivePrompt, PromptQuestion, PromptResponse } from "@/types";
10
+
11
+ export type PromptActionVariant = "default" | "destructive" | "outline";
12
+
13
+ export interface PromptAction {
14
+ key: string;
15
+ label: string;
16
+ response: PromptResponse;
17
+ variant: PromptActionVariant;
18
+ /** Ikon khusus (mis. "shield-check" untuk Always allow). */
19
+ icon?: "shield-check";
20
+ }
21
+
22
+ /**
23
+ * Aksi yang dirender untuk sebuah Interactive_Prompt (murni — diuji di 24.6).
24
+ * Untuk `confirmation`: Approve, Always allow, Deny, dan Cancel; Cancel
25
+ * memakai respon yang sama dengan Deny (Requirement 8.5).
26
+ *
27
+ * Menu MULTI-SELECT tidak memakai aksi per opsi — jawaban dikumpulkan lewat
28
+ * checkbox lalu dikirim sekaligus `{ options: string[] }` (lihat PromptCard).
29
+ */
30
+ export function getPromptActions(prompt: InteractivePrompt): PromptAction[] {
31
+ if (prompt.type === "menu") {
32
+ return (prompt.options ?? []).map((option) => ({
33
+ key: `option-${option.label}`,
34
+ label: option.label,
35
+ response: { option: option.label },
36
+ variant: "outline",
37
+ }));
38
+ }
39
+ return [
40
+ { key: "approve", label: "Approve", response: "approve", variant: "default" },
41
+ {
42
+ key: "always",
43
+ label: "Always allow",
44
+ response: "always",
45
+ variant: "outline",
46
+ icon: "shield-check",
47
+ },
48
+ { key: "deny", label: "Deny", response: "deny", variant: "destructive" },
49
+ // Requirement 8.5: Cancel diperlakukan sebagai respon Deny yang sama.
50
+ { key: "cancel", label: "Cancel", response: "deny", variant: "outline" },
51
+ ];
52
+ }
53
+
54
+ /**
55
+ * Daftar pertanyaan yang dirender kartu — multi-question memakai field
56
+ * `questions`; pertanyaan tunggal diwrap dari field legacy.
57
+ * Murni (diuji tanpa DOM) agar konsisten dengan helper lain di file ini.
58
+ */
59
+ export function getPromptQuestions(prompt: InteractivePrompt): PromptQuestion[] {
60
+ if (prompt.questions && prompt.questions.length > 0) return prompt.questions;
61
+ if (prompt.kind !== "question") return [];
62
+ return [
63
+ {
64
+ question: prompt.title,
65
+ options: prompt.options ?? null,
66
+ multiple: prompt.multiple === true ? true : undefined,
67
+ custom: prompt.custom === true ? true : undefined,
68
+ },
69
+ ];
70
+ }
71
+
72
+ /**
73
+ * Kelompokkan prompt pending yang identik (kind + title sama) menjadi satu
74
+ * kartu. Kunci sama dengan sisi server (`permissionGroupKey`) agar kartu yang
75
+ * dijawab user persis grup yang di-fan-out server. Urutan kemunculan
76
+ * anggota pertama dipertahankan.
77
+ */
78
+ export function groupPrompts(prompts: readonly InteractivePrompt[]): InteractivePrompt[][] {
79
+ const groups = new Map<string, InteractivePrompt[]>();
80
+ for (const p of prompts) {
81
+ const key = `${p.kind}\u0000${p.title ?? ""}`;
82
+ const g = groups.get(key);
83
+ if (g) g.push(p);
84
+ else groups.set(key, [p]);
85
+ }
86
+ return [...groups.values()];
87
+ }