kcg-code 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +228 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +83 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { SessionMessage } from "@/types";
3
+ import { baseName, collectReferences, toProjectPath } from "../references";
4
+
5
+ function msg(parts: SessionMessage["parts"]): SessionMessage {
6
+ return { id: "m1", sessionId: "s1", role: "assistant", parts, createdAt: 0 };
7
+ }
8
+
9
+ function tool(name: string, filePath: string) {
10
+ return { type: "tool", tool: name, state: { input: { filePath } } };
11
+ }
12
+
13
+ describe("toProjectPath", () => {
14
+ const cwd = "/home/u/proj";
15
+
16
+ test("relatif diterima apa adanya (./ dibuang)", () => {
17
+ expect(toProjectPath("src/App.tsx", cwd)).toBe("src/App.tsx");
18
+ expect(toProjectPath("./src/App.tsx", cwd)).toBe("src/App.tsx");
19
+ });
20
+
21
+ test("absolut di dalam project -> relatif; di luar project -> null", () => {
22
+ expect(toProjectPath("/home/u/proj/src/a.ts", cwd)).toBe("src/a.ts");
23
+ expect(toProjectPath("/tmp/x.ts", cwd)).toBeNull();
24
+ // Prefix mirip tapi project lain.
25
+ expect(toProjectPath("/home/u/proj-lain/a.ts", cwd)).toBeNull();
26
+ // Tanpa cwd, path absolut tak bisa dipastikan milik project.
27
+ expect(toProjectPath("/home/u/proj/src/a.ts", null)).toBeNull();
28
+ });
29
+
30
+ test("direktori, glob, keluar project, dan kosong -> null", () => {
31
+ expect(toProjectPath("src/", cwd)).toBeNull();
32
+ expect(toProjectPath("src/**/*.ts", cwd)).toBeNull();
33
+ expect(toProjectPath("../rahasia.ts", cwd)).toBeNull();
34
+ expect(toProjectPath(" ", cwd)).toBeNull();
35
+ });
36
+ });
37
+
38
+ describe("collectReferences", () => {
39
+ test("hanya file yang DIBACA; write/edit/patch tidak dihitung", () => {
40
+ const refs = collectReferences([
41
+ msg([
42
+ tool("read", "src/a.ts"),
43
+ tool("write", "src/baru.ts"),
44
+ tool("edit", "src/diubah.ts"),
45
+ tool("patch", "src/patched.ts"),
46
+ tool("multiedit", "src/multi.ts"),
47
+ ]),
48
+ ]);
49
+ expect(refs.map((r) => r.path)).toEqual(["src/a.ts"]);
50
+ });
51
+
52
+ test("file yang dibaca lalu diubah tetap muncul (karena dibaca)", () => {
53
+ const refs = collectReferences([
54
+ msg([tool("read", "a.ts"), tool("edit", "a.ts")]),
55
+ msg([tool("write", "hanya-ditulis.ts")]),
56
+ ]);
57
+ expect(refs.map((r) => r.path)).toEqual(["a.ts"]);
58
+ });
59
+
60
+ test("bash/grep/glob diabaikan", () => {
61
+ const refs = collectReferences([
62
+ msg([
63
+ tool("read", "src/a.ts"),
64
+ { type: "tool", tool: "bash", state: { input: { command: "ls -la" } } },
65
+ { type: "tool", tool: "grep", state: { input: { pattern: "foo" } } },
66
+ { type: "tool", tool: "glob", state: { input: { pattern: "src/**" } } },
67
+ ]),
68
+ ]);
69
+ expect(refs.map((r) => r.path)).toEqual(["src/a.ts"]);
70
+ });
71
+
72
+ test("terbaru di atas & tanpa duplikat", () => {
73
+ const refs = collectReferences([
74
+ msg([tool("read", "a.ts"), tool("read", "b.ts"), tool("read", "c.ts")]),
75
+ ]);
76
+ expect(refs.map((r) => r.path)).toEqual(["c.ts", "b.ts", "a.ts"]);
77
+ });
78
+
79
+ test("file yang dibaca lagi naik kembali ke atas", () => {
80
+ const refs = collectReferences([
81
+ msg([tool("read", "a.ts"), tool("read", "b.ts")]),
82
+ msg([tool("read", "a.ts")]),
83
+ ]);
84
+ expect(refs.map((r) => r.path)).toEqual(["a.ts", "b.ts"]);
85
+ });
86
+
87
+ test("urutan terbaru berlaku lintas pesan", () => {
88
+ const refs = collectReferences([
89
+ msg([tool("read", "lama.ts")]),
90
+ msg([tool("read", "tengah.ts")]),
91
+ msg([tool("read", "baru.ts")]),
92
+ ]);
93
+ expect(refs[0]?.path).toBe("baru.ts");
94
+ });
95
+
96
+ test("echo @file ikut (dibaca); lampiran upload tidak", () => {
97
+ const refs = collectReferences([
98
+ msg([
99
+ { type: "file", filename: "docs/spec.md" },
100
+ { type: "file", filename: "foto.png", attachmentId: "abc", mime: "image/png" },
101
+ ]),
102
+ ]);
103
+ expect(refs).toEqual([{ path: "docs/spec.md" }]);
104
+ });
105
+
106
+ test("path absolut tool dinormalkan dengan cwd", () => {
107
+ const refs = collectReferences([msg([tool("read", "/home/u/proj/src/a.ts")])], "/home/u/proj");
108
+ expect(refs).toEqual([{ path: "src/a.ts" }]);
109
+ });
110
+
111
+ test("tanpa pesan / tanpa pembacaan file -> kosong", () => {
112
+ expect(collectReferences([])).toEqual([]);
113
+ expect(collectReferences([msg([{ type: "text", text: "halo" }])])).toEqual([]);
114
+ expect(collectReferences([msg([tool("write", "x.ts")])])).toEqual([]);
115
+ });
116
+ });
117
+
118
+ test("baseName mengambil nama file terakhir", () => {
119
+ expect(baseName("src/components/App.tsx")).toBe("App.tsx");
120
+ expect(baseName("README.md")).toBe("README.md");
121
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Unit test logika rute murni (src/lib/routes.ts).
3
+ *
4
+ * `bun test` tidak menyediakan DOM, sehingga yang diuji adalah
5
+ * `parseRoute()` dan pembangun path (`projectsPath`, `projectPath`,
6
+ * `sessionPath`) — pola yang sama dengan test `useTheme.ts`. Hook
7
+ * `useRouter` dan `navigate` (History API) diuji manual via dev server.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+ import {
11
+ parseRoute,
12
+ parseSkillsProject,
13
+ projectPath,
14
+ projectsPath,
15
+ sessionPath,
16
+ settingsPath,
17
+ skillsPath,
18
+ } from "../routes";
19
+
20
+ describe("pembangun path", () => {
21
+ test("projectsPath menghasilkan /", () => {
22
+ expect(projectsPath()).toBe("/");
23
+ });
24
+
25
+ test("projectPath menyisipkan id Project", () => {
26
+ expect(projectPath("abc-123")).toBe("/projects/abc-123");
27
+ });
28
+
29
+ test("sessionPath menyisipkan id Project dan id Session", () => {
30
+ expect(sessionPath("p1", "s2")).toBe("/projects/p1/sessions/s2");
31
+ });
32
+ });
33
+
34
+ describe("parseRoute", () => {
35
+ test("pathname kosong / root -> projects", () => {
36
+ expect(parseRoute("/")).toEqual({ name: "projects" });
37
+ expect(parseRoute("")).toEqual({ name: "projects" });
38
+ });
39
+
40
+ test("trailing slash diabaikan", () => {
41
+ expect(parseRoute("/projects/p1/")).toEqual({ name: "project", projectId: "p1" });
42
+ expect(parseRoute("/projects/p1/sessions/s2/")).toEqual({
43
+ name: "session",
44
+ projectId: "p1",
45
+ sessionId: "s2",
46
+ });
47
+ });
48
+
49
+ test("URL daftar Session Project", () => {
50
+ expect(parseRoute("/projects/p1")).toEqual({ name: "project", projectId: "p1" });
51
+ });
52
+
53
+ test("URL Session view", () => {
54
+ expect(parseRoute("/projects/p1/sessions/s2")).toEqual({
55
+ name: "session",
56
+ projectId: "p1",
57
+ sessionId: "s2",
58
+ });
59
+ });
60
+
61
+ test("id UUID tetap opaque (tanpa decoding khusus)", () => {
62
+ const id = "6f0a2b1e-9c4d-4f6a-8e1b-2c3d4e5f6a7b";
63
+ expect(parseRoute(`/projects/${id}`)).toEqual({ name: "project", projectId: id });
64
+ expect(parseRoute(`/projects/${id}/sessions/${id}`)).toEqual({
65
+ name: "session",
66
+ projectId: id,
67
+ sessionId: id,
68
+ });
69
+ });
70
+
71
+ test("path tak dikenal -> not-found", () => {
72
+ expect(parseRoute("/preferences")).toEqual({ name: "not-found" });
73
+ expect(parseRoute("/projects")).toEqual({ name: "not-found" });
74
+ expect(parseRoute("/projects/p1/extra")).toEqual({ name: "not-found" });
75
+ expect(parseRoute("/projects/p1/other/s2")).toEqual({ name: "not-found" });
76
+ // Trailing slash dikonsumsi, jadi ini bukan case "segmen kosong".
77
+ expect(parseRoute("/projects//sessions/s2")).toEqual({ name: "not-found" });
78
+ });
79
+ });
80
+
81
+ describe("rute skills", () => {
82
+ test("skillsPath menghasilkan /skills", () => {
83
+ expect(skillsPath()).toBe("/skills");
84
+ });
85
+
86
+ test("/skills (dengan/tanpa trailing slash) -> skills", () => {
87
+ expect(parseRoute("/skills")).toEqual({ name: "skills" });
88
+ expect(parseRoute("/skills/")).toEqual({ name: "skills" });
89
+ });
90
+
91
+ test("skillsPath dengan Project menambahkan ?project=", () => {
92
+ expect(skillsPath("p1")).toBe("/skills?project=p1");
93
+ expect(skillsPath(null)).toBe("/skills");
94
+ });
95
+
96
+ test("parseSkillsProject membaca ?project=", () => {
97
+ expect(parseSkillsProject("?project=p1")).toBe("p1");
98
+ expect(parseSkillsProject("")).toBeNull();
99
+ expect(parseSkillsProject("?project=")).toBeNull();
100
+ });
101
+
102
+ test("sub-path skills tidak dikenal -> not-found", () => {
103
+ expect(parseRoute("/skills/x")).toEqual({ name: "not-found" });
104
+ });
105
+ });
106
+
107
+ describe("rute settings", () => {
108
+ test("settingsPath & parseRoute", () => {
109
+ expect(settingsPath()).toBe("/settings");
110
+ expect(parseRoute("/settings")).toEqual({ name: "settings", section: "profile" });
111
+ expect(parseRoute("/settings/")).toEqual({ name: "settings", section: "profile" });
112
+ expect(parseRoute("/settings/x")).toEqual({ name: "not-found" });
113
+ expect(parseRoute("/settings/security/x")).toEqual({ name: "not-found" });
114
+ });
115
+
116
+ test("URL lama /settings/devices -> Security, grup devices", () => {
117
+ expect(parseRoute("/settings/devices")).toEqual({
118
+ name: "settings",
119
+ section: "security",
120
+ anchor: "devices",
121
+ });
122
+ // Kunci prototipe bukan alias.
123
+ expect(parseRoute("/settings/constructor")).toEqual({ name: "not-found" });
124
+ expect(parseRoute("/settings/toString")).toEqual({ name: "not-found" });
125
+ });
126
+
127
+ test("sub-halaman settings", () => {
128
+ for (const section of ["profile", "security", "workspace", "remote", "about"] as const) {
129
+ expect(settingsPath(section)).toBe(`/settings/${section}`);
130
+ expect(parseRoute(`/settings/${section}`)).toEqual({ name: "settings", section });
131
+ expect(parseRoute(`/settings/${section}/`)).toEqual({ name: "settings", section });
132
+ }
133
+ });
134
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Unit test ringkasan Session halaman detail Project
3
+ * (src/lib/session-summary.ts) — logika murni, pola sama dengan
4
+ * `routes.test.ts` dan `project-overview.test.ts`.
5
+ */
6
+ import { describe, expect, test } from "bun:test";
7
+ import type { Session, SessionStatus } from "@/types";
8
+ import {
9
+ describeSessionModel,
10
+ describeSessionSummary,
11
+ sortSessions,
12
+ summarizeSessions,
13
+ } from "../session-summary";
14
+
15
+ function session(id: string, status: SessionStatus, updatedAt: number): Session {
16
+ return {
17
+ id,
18
+ projectId: "p1",
19
+ agentType: "opencode",
20
+ cwd: "/sandbox/p1",
21
+ status,
22
+ ocSessionId: null,
23
+ title: null,
24
+ model: null,
25
+ agent: null,
26
+ createdAt: updatedAt,
27
+ updatedAt,
28
+ };
29
+ }
30
+
31
+ describe("sortSessions", () => {
32
+ test("running di atas crashed, crashed di atas stopped", () => {
33
+ const sorted = sortSessions([
34
+ session("stopped", "stopped", 9_000),
35
+ session("crashed", "crashed", 1_000),
36
+ session("running", "running", 500),
37
+ ]);
38
+ expect(sorted.map((s) => s.id)).toEqual(["running", "crashed", "stopped"]);
39
+ });
40
+
41
+ test("dalam status yang sama, terakhir diperbarui di atas", () => {
42
+ const sorted = sortSessions([
43
+ session("lama", "running", 1_000),
44
+ session("baru", "running", 5_000),
45
+ ]);
46
+ expect(sorted.map((s) => s.id)).toEqual(["baru", "lama"]);
47
+ });
48
+
49
+ test("updatedAt sama -> urut berdasarkan id (stabil antar refresh)", () => {
50
+ const sorted = sortSessions([session("b", "running", 1_000), session("a", "running", 1_000)]);
51
+ expect(sorted.map((s) => s.id)).toEqual(["a", "b"]);
52
+ });
53
+
54
+ test("input tidak dimutasi", () => {
55
+ const input = [session("stopped", "stopped", 1), session("running", "running", 2)];
56
+ sortSessions(input);
57
+ expect(input.map((s) => s.id)).toEqual(["stopped", "running"]);
58
+ });
59
+
60
+ test("daftar kosong -> kosong", () => {
61
+ expect(sortSessions([])).toEqual([]);
62
+ });
63
+ });
64
+
65
+ describe("summarizeSessions", () => {
66
+ test("menghitung total dan tiap status", () => {
67
+ const summary = summarizeSessions([
68
+ session("a", "running", 1),
69
+ session("b", "running", 2),
70
+ session("c", "stopped", 3),
71
+ session("d", "crashed", 4),
72
+ ]);
73
+ expect(summary).toEqual({ total: 4, running: 2, stopped: 1, crashed: 1 });
74
+ });
75
+
76
+ test("daftar kosong -> semua nol", () => {
77
+ expect(summarizeSessions([])).toEqual({ total: 0, running: 0, stopped: 0, crashed: 0 });
78
+ });
79
+ });
80
+
81
+ describe("describeSessionSummary", () => {
82
+ test("tanpa Session -> teks pengarah", () => {
83
+ expect(describeSessionSummary({ total: 0, running: 0, stopped: 0, crashed: 0 })).toBe(
84
+ "No sessions yet",
85
+ );
86
+ });
87
+
88
+ test("hanya total bila tidak ada yang berjalan/crash", () => {
89
+ expect(describeSessionSummary({ total: 2, running: 0, stopped: 2, crashed: 0 })).toBe(
90
+ "2 sessions",
91
+ );
92
+ });
93
+
94
+ test("menyebut yang berjalan dan crash", () => {
95
+ expect(describeSessionSummary({ total: 3, running: 1, stopped: 1, crashed: 1 })).toBe(
96
+ "3 sessions · 1 running · 1 crashed",
97
+ );
98
+ });
99
+ });
100
+
101
+ describe("describeSessionModel", () => {
102
+ test("model kosong -> model default", () => {
103
+ expect(describeSessionModel(session("a", "running", 1))).toBe("default model");
104
+ });
105
+
106
+ test("model terpilih -> modelID", () => {
107
+ const s = session("a", "running", 1);
108
+ expect(
109
+ describeSessionModel({
110
+ ...s,
111
+ model: { providerID: "kcgcode", modelID: "kiro/claude-opus-5" },
112
+ }),
113
+ ).toBe("kiro/claude-opus-5");
114
+ });
115
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Unit test helper judul Session (src/lib/session-title.ts).
3
+ */
4
+ import { describe, expect, test } from "bun:test";
5
+ import {
6
+ deriveSessionTitle,
7
+ displaySessionTitle,
8
+ isOpencodeDefaultTitle,
9
+ isPlaceholderTitle,
10
+ } from "../session-title";
11
+
12
+ describe("isOpencodeDefaultTitle", () => {
13
+ test("pola default opencode (parent & child) dikenali", () => {
14
+ expect(isOpencodeDefaultTitle("New session - 2026-09-29T10:59:18.048Z")).toBe(true);
15
+ expect(isOpencodeDefaultTitle("Child session - 2026-09-29T10:59:18.048Z")).toBe(true);
16
+ });
17
+
18
+ test("judul nyata / pola mirip tidak dianggap default", () => {
19
+ expect(isOpencodeDefaultTitle("Cara Kerja Rate Limiting di Express")).toBe(false);
20
+ expect(isOpencodeDefaultTitle("New session")).toBe(false);
21
+ expect(isOpencodeDefaultTitle("New session - kemarin")).toBe(false);
22
+ expect(isOpencodeDefaultTitle("New session - 2026-09-29")).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe("isPlaceholderTitle", () => {
27
+ test("null, kosong, placeholder lama KCG Code, dan default opencode", () => {
28
+ expect(isPlaceholderTitle(null)).toBe(true);
29
+ expect(isPlaceholderTitle(undefined)).toBe(true);
30
+ expect(isPlaceholderTitle(" ")).toBe(true);
31
+ expect(isPlaceholderTitle("KCG Code Session")).toBe(true);
32
+ expect(isPlaceholderTitle("KCG Code Session (resumed)")).toBe(true);
33
+ expect(isPlaceholderTitle("New session - 2026-09-29T10:59:18.048Z")).toBe(true);
34
+ expect(isPlaceholderTitle("Refactor auth module")).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe("displaySessionTitle", () => {
39
+ test("placeholder -> 'New session'; judul nyata dirapikan", () => {
40
+ expect(displaySessionTitle("New session - 2026-09-29T10:59:18.048Z")).toBe("New session");
41
+ expect(displaySessionTitle(null)).toBe("New session");
42
+ expect(displaySessionTitle(" Rate limiting ")).toBe("Rate limiting");
43
+ });
44
+ });
45
+
46
+ describe("deriveSessionTitle", () => {
47
+ test("baris pertama, spasi dirapatkan, dipotong di batas kata", () => {
48
+ expect(deriveSessionTitle(" halo, project apa ini?\nbaris dua")).toBe(
49
+ "halo, project apa ini?",
50
+ );
51
+ expect(deriveSessionTitle("")).toBeNull();
52
+ const long = deriveSessionTitle(
53
+ "tolong jelaskan cara kerja rate limiting di express dan nestjs",
54
+ );
55
+ expect(long?.endsWith("…")).toBe(true);
56
+ expect((long ?? "").length).toBeLessThanOrEqual(51);
57
+ });
58
+ });
@@ -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,134 @@
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
+ status: "running",
21
+ error: null,
22
+ refreshed: null,
23
+ installedPath: null,
24
+ startedAt: 1,
25
+ finishedAt: null,
26
+ lastOutputAt: 1,
27
+ lineCount: 0,
28
+ ...over,
29
+ };
30
+ }
31
+
32
+ function entry(over: Partial<InstallEntry> = {}): InstallEntry {
33
+ return {
34
+ job: job(),
35
+ lines: [],
36
+ next: 0,
37
+ truncated: false,
38
+ dismissed: false,
39
+ expanded: true,
40
+ pollError: null,
41
+ cancelling: false,
42
+ ...over,
43
+ };
44
+ }
45
+
46
+ describe("mergeLog", () => {
47
+ test("menambah baris baru dan memajukan kursor", () => {
48
+ const e = mergeLog(entry(), {
49
+ job: job({ lineCount: 2 }),
50
+ from: 0,
51
+ lines: ["a", "b"],
52
+ truncated: false,
53
+ });
54
+ expect(e.lines).toEqual(["a", "b"]);
55
+ expect(e.next).toBe(2);
56
+ const e2 = mergeLog(e, { job: job({ lineCount: 3 }), from: 2, lines: ["c"], truncated: false });
57
+ expect(e2.lines).toEqual(["a", "b", "c"]);
58
+ expect(e2.next).toBe(3);
59
+ });
60
+
61
+ test("respon tumpang tindih tidak menggandakan baris", () => {
62
+ const e = entry({ lines: ["a", "b"], next: 2 });
63
+ const merged = mergeLog(e, { job: job(), from: 1, lines: ["b", "c"], truncated: false });
64
+ expect(merged.lines).toEqual(["a", "b", "c"]);
65
+ expect(merged.next).toBe(3);
66
+ });
67
+
68
+ test("status job diperbarui & pollError dibersihkan; truncated diteruskan", () => {
69
+ const e = entry({ pollError: "x" });
70
+ const merged = mergeLog(e, {
71
+ job: job({ status: "succeeded" }),
72
+ from: 5,
73
+ lines: ["z"],
74
+ truncated: true,
75
+ });
76
+ expect(merged.job.status).toBe("succeeded");
77
+ expect(merged.pollError).toBeNull();
78
+ expect(merged.truncated).toBe(true);
79
+ expect(merged.next).toBe(6);
80
+ });
81
+ });
82
+
83
+ describe("findRunningInstall", () => {
84
+ test("cocokkan project + source + skill yang masih berjalan", () => {
85
+ const list = [
86
+ entry({ job: job({ id: "done", status: "succeeded" }) }),
87
+ entry({ job: job({ id: "run" }) }),
88
+ ];
89
+ expect(findRunningInstall(list, "p1", "a/b", "c")?.job.id).toBe("run");
90
+ expect(findRunningInstall(list, "p2", "a/b", "c")).toBeUndefined();
91
+ expect(findRunningInstall(list, "p1", "a/b", "d")).toBeUndefined();
92
+ });
93
+ });
94
+
95
+ describe("hydrationEntries (pulih setelah refresh)", () => {
96
+ const running = job({ id: "run" });
97
+ const done = job({ id: "done", status: "succeeded", finishedAt: 5 });
98
+ const failed = job({
99
+ id: "fail",
100
+ status: "failed",
101
+ error: "SKILL_INSTALL_FAILED",
102
+ finishedAt: 6,
103
+ });
104
+
105
+ test("job berjalan & selesai yang belum ditutup dipulihkan, status tampilan ikut", () => {
106
+ const out = hydrationEntries(
107
+ [running, done, failed],
108
+ { dismissed: [], expanded: ["done"] },
109
+ new Set(),
110
+ );
111
+ expect(out.map((e) => e.job.id)).toEqual(["run", "done", "fail"]);
112
+ expect(out.find((e) => e.job.id === "done")?.expanded).toBe(true);
113
+ expect(out.find((e) => e.job.id === "run")?.expanded).toBe(false);
114
+ expect(out.every((e) => e.lines.length === 0 && e.next === 0)).toBe(true);
115
+ });
116
+
117
+ test("job selesai yang ditutup tidak kembali; job berjalan yang disembunyikan tetap tersembunyi", () => {
118
+ const out = hydrationEntries(
119
+ [running, done],
120
+ { dismissed: ["run", "done"], expanded: [] },
121
+ new Set(),
122
+ );
123
+ expect(out.map((e) => [e.job.id, e.dismissed])).toEqual([["run", true]]);
124
+ });
125
+
126
+ test("entry yang sudah ada di store dilewati", () => {
127
+ const out = hydrationEntries(
128
+ [running, done],
129
+ { dismissed: [], expanded: [] },
130
+ new Set(["run"]),
131
+ );
132
+ expect(out.map((e) => e.job.id)).toEqual(["done"]);
133
+ });
134
+ });