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,100 @@
1
+ /**
2
+ * Unit test pengelompokan Session per Project untuk sidebar
3
+ * (src/lib/sidebar-groups.ts) — logika murni.
4
+ */
5
+ import { describe, expect, test } from "bun:test";
6
+ import type { Project, Session, SessionStatus } from "@/types";
7
+ import { filterSidebarGroups, groupSessionsByProject } from "../sidebar-groups";
8
+
9
+ function project(id: string, name: string, createdAt: number): Project {
10
+ return { id, name, path: `/sandbox/${name}`, createdAt };
11
+ }
12
+
13
+ function session(
14
+ id: string,
15
+ projectId: string,
16
+ status: SessionStatus,
17
+ updatedAt: number,
18
+ title: string | null = null,
19
+ ): Session {
20
+ return {
21
+ id,
22
+ projectId,
23
+ agentType: "opencode",
24
+ cwd: "/sandbox",
25
+ status,
26
+ ocSessionId: null,
27
+ title,
28
+ model: null,
29
+ agent: null,
30
+ createdAt: updatedAt,
31
+ updatedAt,
32
+ };
33
+ }
34
+
35
+ const titleOf = (s: Session) => s.title ?? "New session";
36
+
37
+ describe("groupSessionsByProject", () => {
38
+ test("mengelompokkan Session per Project dan menghitung yang running", () => {
39
+ const groups = groupSessionsByProject(
40
+ [project("p1", "alpha", 1), project("p2", "beta", 2)],
41
+ [
42
+ session("s1", "p1", "running", 10),
43
+ session("s2", "p1", "stopped", 20),
44
+ session("s3", "p2", "running", 5),
45
+ ],
46
+ );
47
+ const alpha = groups.find((g) => g.project.id === "p1");
48
+ expect(alpha?.sessions.map((s) => s.id)).toEqual(["s1", "s2"]);
49
+ expect(alpha?.runningCount).toBe(1);
50
+ expect(groups.find((g) => g.project.id === "p2")?.sessions).toHaveLength(1);
51
+ });
52
+
53
+ test("Project dengan aktivitas terbaru di atas; Project kosong tetap tampil", () => {
54
+ const groups = groupSessionsByProject(
55
+ [project("p1", "alpha", 1), project("p2", "beta", 2), project("p3", "gamma", 100)],
56
+ [session("s1", "p1", "stopped", 50), session("s2", "p2", "stopped", 80)],
57
+ );
58
+ expect(groups.map((g) => g.project.id)).toEqual(["p3", "p2", "p1"]);
59
+ expect(groups[0]?.sessions).toEqual([]);
60
+ });
61
+
62
+ test("Session dengan projectId tak dikenal diabaikan", () => {
63
+ const groups = groupSessionsByProject(
64
+ [project("p1", "alpha", 1)],
65
+ [session("s1", "ghost", "running", 10)],
66
+ );
67
+ expect(groups).toHaveLength(1);
68
+ expect(groups[0]?.sessions).toEqual([]);
69
+ });
70
+ });
71
+
72
+ describe("filterSidebarGroups", () => {
73
+ const groups = groupSessionsByProject(
74
+ [project("p1", "alpha", 1), project("p2", "beta", 2)],
75
+ [
76
+ session("s1", "p1", "running", 10, "Fix login bug"),
77
+ session("s2", "p1", "stopped", 20, "Add sidebar"),
78
+ session("s3", "p2", "running", 5, "Refactor login"),
79
+ ],
80
+ );
81
+
82
+ test("query kosong mengembalikan semua grup", () => {
83
+ expect(filterSidebarGroups(groups, " ", titleOf)).toHaveLength(2);
84
+ });
85
+
86
+ test("nama Project cocok -> grup utuh", () => {
87
+ const out = filterSidebarGroups(groups, "ALPHA", titleOf);
88
+ expect(out).toHaveLength(1);
89
+ expect(out[0]?.sessions).toHaveLength(2);
90
+ });
91
+
92
+ test("judul Session cocok -> hanya Session tersebut, grup kosong dibuang", () => {
93
+ const out = filterSidebarGroups(groups, "login", titleOf);
94
+ expect(out.map((g) => g.sessions.map((s) => s.id))).toEqual([["s1"], ["s3"]]);
95
+ expect(filterSidebarGroups(groups, "sidebar", titleOf).map((g) => g.project.id)).toEqual([
96
+ "p1",
97
+ ]);
98
+ expect(filterSidebarGroups(groups, "zzz", titleOf)).toEqual([]);
99
+ });
100
+ });
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Unit test store instalasi skill (src/lib/skill-installs.ts) — bagian murni.
3
+ */
4
+ import { describe, expect, test } from "bun:test";
5
+ import type { InstallJob } from "@/server/services/skill-install-jobs";
6
+ import {
7
+ findRunningInstall,
8
+ hydrationEntries,
9
+ type InstallEntry,
10
+ mergeLog,
11
+ } from "../skill-installs";
12
+
13
+ function job(over: Partial<InstallJob> = {}): InstallJob {
14
+ return {
15
+ id: "j1",
16
+ projectId: "p1",
17
+ projectName: "Proj",
18
+ source: "a/b",
19
+ skillId: "c",
20
+ agentTypes: ["opencode"],
21
+ status: "running",
22
+ error: null,
23
+ refreshed: null,
24
+ installedPath: null,
25
+ startedAt: 1,
26
+ finishedAt: null,
27
+ lastOutputAt: 1,
28
+ lineCount: 0,
29
+ ...over,
30
+ };
31
+ }
32
+
33
+ function entry(over: Partial<InstallEntry> = {}): InstallEntry {
34
+ return {
35
+ job: job(),
36
+ lines: [],
37
+ next: 0,
38
+ truncated: false,
39
+ dismissed: false,
40
+ expanded: true,
41
+ pollError: null,
42
+ cancelling: false,
43
+ ...over,
44
+ };
45
+ }
46
+
47
+ describe("mergeLog", () => {
48
+ test("menambah baris baru dan memajukan kursor", () => {
49
+ const e = mergeLog(entry(), {
50
+ job: job({ lineCount: 2 }),
51
+ from: 0,
52
+ lines: ["a", "b"],
53
+ truncated: false,
54
+ });
55
+ expect(e.lines).toEqual(["a", "b"]);
56
+ expect(e.next).toBe(2);
57
+ const e2 = mergeLog(e, { job: job({ lineCount: 3 }), from: 2, lines: ["c"], truncated: false });
58
+ expect(e2.lines).toEqual(["a", "b", "c"]);
59
+ expect(e2.next).toBe(3);
60
+ });
61
+
62
+ test("respon tumpang tindih tidak menggandakan baris", () => {
63
+ const e = entry({ lines: ["a", "b"], next: 2 });
64
+ const merged = mergeLog(e, { job: job(), from: 1, lines: ["b", "c"], truncated: false });
65
+ expect(merged.lines).toEqual(["a", "b", "c"]);
66
+ expect(merged.next).toBe(3);
67
+ });
68
+
69
+ test("status job diperbarui & pollError dibersihkan; truncated diteruskan", () => {
70
+ const e = entry({ pollError: "x" });
71
+ const merged = mergeLog(e, {
72
+ job: job({ status: "succeeded" }),
73
+ from: 5,
74
+ lines: ["z"],
75
+ truncated: true,
76
+ });
77
+ expect(merged.job.status).toBe("succeeded");
78
+ expect(merged.pollError).toBeNull();
79
+ expect(merged.truncated).toBe(true);
80
+ expect(merged.next).toBe(6);
81
+ });
82
+ });
83
+
84
+ describe("findRunningInstall", () => {
85
+ test("cocokkan project + source + skill yang masih berjalan", () => {
86
+ const list = [
87
+ entry({ job: job({ id: "done", status: "succeeded" }) }),
88
+ entry({ job: job({ id: "run" }) }),
89
+ ];
90
+ expect(findRunningInstall(list, "p1", "a/b", "c")?.job.id).toBe("run");
91
+ expect(findRunningInstall(list, "p2", "a/b", "c")).toBeUndefined();
92
+ expect(findRunningInstall(list, "p1", "a/b", "d")).toBeUndefined();
93
+ });
94
+ });
95
+
96
+ describe("hydrationEntries (pulih setelah refresh)", () => {
97
+ const running = job({ id: "run" });
98
+ const done = job({ id: "done", status: "succeeded", finishedAt: 5 });
99
+ const failed = job({
100
+ id: "fail",
101
+ status: "failed",
102
+ error: "SKILL_INSTALL_FAILED",
103
+ finishedAt: 6,
104
+ });
105
+
106
+ test("job berjalan & selesai yang belum ditutup dipulihkan, status tampilan ikut", () => {
107
+ const out = hydrationEntries(
108
+ [running, done, failed],
109
+ { dismissed: [], expanded: ["done"] },
110
+ new Set(),
111
+ );
112
+ expect(out.map((e) => e.job.id)).toEqual(["run", "done", "fail"]);
113
+ expect(out.find((e) => e.job.id === "done")?.expanded).toBe(true);
114
+ expect(out.find((e) => e.job.id === "run")?.expanded).toBe(false);
115
+ expect(out.every((e) => e.lines.length === 0 && e.next === 0)).toBe(true);
116
+ });
117
+
118
+ test("job selesai yang ditutup tidak kembali; job berjalan yang disembunyikan tetap tersembunyi", () => {
119
+ const out = hydrationEntries(
120
+ [running, done],
121
+ { dismissed: ["run", "done"], expanded: [] },
122
+ new Set(),
123
+ );
124
+ expect(out.map((e) => [e.job.id, e.dismissed])).toEqual([["run", true]]);
125
+ });
126
+
127
+ test("entry yang sudah ada di store dilewati", () => {
128
+ const out = hydrationEntries(
129
+ [running, done],
130
+ { dismissed: [], expanded: [] },
131
+ new Set(["run"]),
132
+ );
133
+ expect(out.map((e) => e.job.id)).toEqual(["done"]);
134
+ });
135
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Unit test helper katalog skill (src/lib/skills-catalog.ts).
3
+ */
4
+ import { describe, expect, test } from "bun:test";
5
+ import type { SkillInfo } from "@/server/services/opencode-client";
6
+ import type { RegistrySkill } from "@/server/services/skills-registry";
7
+ import type { Project } from "@/types";
8
+ import {
9
+ formatInstalls,
10
+ githubOwner,
11
+ installedAgentsFor,
12
+ mergeInstalledSkills,
13
+ resolveSkillsProject,
14
+ visibleInstalledSkills,
15
+ } from "../skills-catalog";
16
+
17
+ describe("formatInstalls", () => {
18
+ test("ringkas per ribuan / jutaan", () => {
19
+ expect(formatInstalls(0)).toBe("0");
20
+ expect(formatInstalls(950)).toBe("950");
21
+ expect(formatInstalls(1000)).toBe("1K");
22
+ expect(formatInstalls(1234)).toBe("1.2K");
23
+ expect(formatInstalls(752_553)).toBe("753K");
24
+ expect(formatInstalls(1_300_000)).toBe("1.3M");
25
+ });
26
+ });
27
+
28
+ describe("deteksi terpasang", () => {
29
+ const skills: SkillInfo[] = [
30
+ {
31
+ name: "find-skills",
32
+ description: null,
33
+ location: "/p/.agents/skills/find-skills/SKILL.md",
34
+ source: "project",
35
+ },
36
+ { name: "Global-One", description: null, location: "/home/u/.config/x", source: "global" },
37
+ { name: "builtin", description: null, location: null, source: "builtin" },
38
+ ];
39
+ const reg = (skillId: string, name = skillId): RegistrySkill => ({
40
+ id: `a/b/${skillId}`,
41
+ name,
42
+ source: "a/b",
43
+ skillId,
44
+ installs: 1,
45
+ url: "",
46
+ });
47
+
48
+ test("mergeInstalledSkills: gabung per source + nama, catat agent", () => {
49
+ const merged = mergeInstalledSkills({
50
+ opencode: skills,
51
+ "claude-code": [
52
+ {
53
+ name: "Find-Skills",
54
+ description: "From CC",
55
+ location: "/p/.claude/x",
56
+ source: "project",
57
+ },
58
+ { name: "cc-only", description: null, location: "/p/.claude/y", source: "project" },
59
+ ],
60
+ });
61
+ expect(merged.map((s) => [s.name, s.source, s.agents])).toEqual([
62
+ ["builtin", "builtin", ["opencode"]],
63
+ ["cc-only", "project", ["claude-code"]],
64
+ ["find-skills", "project", ["opencode", "claude-code"]],
65
+ ["Global-One", "global", ["opencode"]],
66
+ ]);
67
+ // Deskripsi kosong diisi dari agent lain.
68
+ expect(merged.find((s) => s.name === "find-skills")?.description).toBe("From CC");
69
+ });
70
+
71
+ test("installedAgentsFor: hanya sumber project, case-insensitive", () => {
72
+ const merged = mergeInstalledSkills({
73
+ opencode: skills,
74
+ "claude-code": [skills[0] as SkillInfo],
75
+ });
76
+ expect(installedAgentsFor(reg("FIND-SKILLS"), merged)).toEqual(["opencode", "claude-code"]);
77
+ expect(installedAgentsFor(reg("other", "find-skills"), merged)).toEqual([
78
+ "opencode",
79
+ "claude-code",
80
+ ]);
81
+ expect(installedAgentsFor(reg("global-one"), merged)).toEqual([]);
82
+ });
83
+ });
84
+
85
+ describe("resolveSkillsProject", () => {
86
+ const p = (id: string): Project => ({ id, name: id, path: `/${id}`, createdAt: 1 });
87
+ const projects = [p("a"), p("b")];
88
+
89
+ test("URL menang, lalu tersimpan; id tak dikenal dilewati", () => {
90
+ expect(resolveSkillsProject(projects, "b", "a")?.id).toBe("b");
91
+ expect(resolveSkillsProject(projects, "gone", "a")?.id).toBe("a");
92
+ expect(resolveSkillsProject(projects, null, "gone")).toBeNull();
93
+ expect(resolveSkillsProject(projects, null, null)).toBeNull();
94
+ });
95
+
96
+ test("satu-satunya Project dipilih otomatis", () => {
97
+ expect(resolveSkillsProject([p("only")], null, null)?.id).toBe("only");
98
+ expect(resolveSkillsProject([], null, null)).toBeNull();
99
+ });
100
+ });
101
+
102
+ describe("githubOwner", () => {
103
+ test("owner/repo -> owner; domain / bentuk lain -> null", () => {
104
+ expect(githubOwner("vercel-labs/skills")).toBe("vercel-labs");
105
+ expect(githubOwner("mintlify.com")).toBeNull();
106
+ expect(githubOwner("a/b/c")).toBeNull();
107
+ });
108
+ });
109
+
110
+ describe("visibleInstalledSkills", () => {
111
+ const s = (name: string, source: SkillInfo["source"], description: string | null = null) => ({
112
+ name,
113
+ description,
114
+ location: null,
115
+ source,
116
+ });
117
+ const all = [s("g1", "global"), s("builtin", "builtin"), s("p1", "project", "React helper")];
118
+
119
+ test("tanpa bawaan, project dulu, filter nama/deskripsi", () => {
120
+ expect(visibleInstalledSkills(all, "").map((x) => x.name)).toEqual(["p1", "g1"]);
121
+ expect(visibleInstalledSkills(all, "react").map((x) => x.name)).toEqual(["p1"]);
122
+ expect(visibleInstalledSkills(all, "G1").map((x) => x.name)).toEqual(["g1"]);
123
+ });
124
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BASE_FREQUENCY, scaleFrequency } from "../tone";
3
+
4
+ describe("scaleFrequency", () => {
5
+ test("penanda pertama = nada dasar, makin ke bawah makin tinggi", () => {
6
+ expect(scaleFrequency(0)).toBeCloseTo(BASE_FREQUENCY, 2);
7
+ const freqs = [0, 1, 2, 3, 4, 5, 6, 7].map((i) => scaleFrequency(i));
8
+ for (let i = 1; i < freqs.length; i++) {
9
+ expect(freqs[i]).toBeGreaterThan(freqs[i - 1] as number);
10
+ }
11
+ });
12
+
13
+ test("nada mayor: langkah ke-3 (fa) = 5 semiton, oktaf penuh = 2x dasar", () => {
14
+ expect(scaleFrequency(3)).toBeCloseTo(BASE_FREQUENCY * 2 ** (5 / 12), 2);
15
+ expect(scaleFrequency(7)).toBeCloseTo(BASE_FREQUENCY * 2, 2);
16
+ });
17
+
18
+ test("ditahan di dua oktaf: tidak melengking & tidak turun lagi", () => {
19
+ // Derajat ke-14 = do dua oktaf di atas dasar; di atas itu nadanya sama.
20
+ expect(scaleFrequency(14)).toBeCloseTo(BASE_FREQUENCY * 4, 2);
21
+ expect(scaleFrequency(20)).toBeCloseTo(scaleFrequency(14), 2);
22
+ expect(scaleFrequency(1000)).toBeCloseTo(scaleFrequency(14), 2);
23
+ // Tidak pernah menurun (rail panjang tetap terasa menaik).
24
+ let prev = 0;
25
+ for (let i = 0; i < 40; i++) {
26
+ const f = scaleFrequency(i);
27
+ expect(f).toBeGreaterThanOrEqual(prev);
28
+ expect(f).toBeLessThanOrEqual(BASE_FREQUENCY * 4 + 0.01);
29
+ prev = f;
30
+ }
31
+ });
32
+
33
+ test("index negatif / pecahan tetap aman", () => {
34
+ expect(scaleFrequency(-5)).toBeCloseTo(BASE_FREQUENCY, 2);
35
+ expect(scaleFrequency(1.9)).toBeCloseTo(scaleFrequency(1), 2);
36
+ });
37
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Unit test penerapan delta streaming di sisi Client (`appendMessagePartDelta`)
3
+ * — logika murni, meniru urutan event opencode 1.x: part dibuat dengan teks
4
+ * kosong (`message_part`), lalu teks bertambah lewat `message_part_delta`.
5
+ */
6
+ import { describe, expect, test } from "bun:test";
7
+ import type { SessionMessage } from "@/types";
8
+ import { appendMessagePartDelta, upsertMessagePart } from "../turns";
9
+
10
+ function base(): SessionMessage[] {
11
+ return upsertMessagePart([], "s1", "msg_a1", { type: "text", id: "prt_t1", text: "" });
12
+ }
13
+
14
+ describe("appendMessagePartDelta", () => {
15
+ test("menambahkan delta berurutan ke field part", () => {
16
+ let msgs = base();
17
+ for (const d of ["Ku", "cing ", "lucu."]) {
18
+ msgs = appendMessagePartDelta(msgs, "msg_a1", "prt_t1", "text", d);
19
+ }
20
+ expect(msgs[0]?.parts[0]?.text).toBe("Kucing lucu.");
21
+ expect(msgs[0]?.streaming).toBe(true);
22
+ });
23
+
24
+ test("field belum ada diperlakukan sebagai string kosong", () => {
25
+ const msgs = upsertMessagePart([], "s1", "msg_a1", { type: "reasoning", id: "prt_r1" });
26
+ const next = appendMessagePartDelta(msgs, "msg_a1", "prt_r1", "text", "pikir");
27
+ expect(next[0]?.parts[0]?.text).toBe("pikir");
28
+ });
29
+
30
+ test("pesan/part tak dikenal atau delta kosong -> array yang sama", () => {
31
+ const msgs = base();
32
+ expect(appendMessagePartDelta(msgs, "msg_x", "prt_t1", "text", "a")).toBe(msgs);
33
+ expect(appendMessagePartDelta(msgs, "msg_a1", "prt_x", "text", "a")).toBe(msgs);
34
+ expect(appendMessagePartDelta(msgs, "msg_a1", "prt_t1", "text", "")).toBe(msgs);
35
+ });
36
+
37
+ test("tidak memutasi state sebelumnya", () => {
38
+ const msgs = base();
39
+ const before = JSON.stringify(msgs);
40
+ appendMessagePartDelta(msgs, "msg_a1", "prt_t1", "text", "hai");
41
+ expect(JSON.stringify(msgs)).toBe(before);
42
+ });
43
+
44
+ test("snapshot final (message_part) menggantikan hasil akumulasi", () => {
45
+ let msgs = base();
46
+ msgs = appendMessagePartDelta(msgs, "msg_a1", "prt_t1", "text", "Kuc");
47
+ msgs = upsertMessagePart(msgs, "s1", "msg_a1", {
48
+ type: "text",
49
+ id: "prt_t1",
50
+ text: "Kucing lucu.",
51
+ });
52
+ expect(msgs[0]?.parts).toHaveLength(1);
53
+ expect(msgs[0]?.parts[0]?.text).toBe("Kucing lucu.");
54
+ });
55
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tipe CLI_Agent yang bisa dipilih di UI (backend Session).
3
+ *
4
+ * - `opencode` : server headless `opencode serve`.
5
+ * - `claude-code` : Claude Code lewat Claude Agent SDK (ikut settings user).
6
+ */
7
+ import type { AgentType } from "@/types";
8
+
9
+ export const AGENT_TYPES: readonly AgentType[] = ["opencode", "claude-code"];
10
+
11
+ export const AGENT_TYPE_LABELS: Record<AgentType, string> = {
12
+ opencode: "OpenCode",
13
+ "claude-code": "Claude Code",
14
+ };
15
+
16
+ /**
17
+ * Agent terakhir yang dipakai untuk membuat Session (composer homepage &
18
+ * dialog New Session), diingat per perangkat. Kunci lama dipertahankan agar
19
+ * pilihan yang sudah tersimpan tetap terbaca.
20
+ */
21
+ const PREFERRED_AGENT_KEY = "kcg-home-agent-type";
22
+
23
+ /** Agent pilihan terakhir; default `opencode` (juga saat storage tak tersedia). */
24
+ export function readPreferredAgentType(): AgentType {
25
+ try {
26
+ const saved =
27
+ typeof localStorage === "undefined" ? null : localStorage.getItem(PREFERRED_AGENT_KEY);
28
+ return saved === "claude-code" ? "claude-code" : "opencode";
29
+ } catch {
30
+ return "opencode";
31
+ }
32
+ }
33
+
34
+ /** Simpan agent pilihan; diam saja bila storage tak tersedia (mode privat). */
35
+ export function storePreferredAgentType(agentType: AgentType): void {
36
+ try {
37
+ localStorage.setItem(PREFERRED_AGENT_KEY, agentType);
38
+ } catch {
39
+ // Pilihan tetap berlaku untuk kunjungan ini.
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Query string `?agent=` untuk daftar model/agent Project. opencode tanpa
45
+ * query (default server) agar URL lama tidak berubah.
46
+ */
47
+ export function agentQuery(agentType: AgentType): string {
48
+ return agentType === "claude-code" ? "?agent=claude-code" : "";
49
+ }