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,45 @@
1
+ /**
2
+ * Toaster shadcn/ui (Sonner). Disesuaikan dari `shadcn add sonner`: tema
3
+ * mengikuti `useTheme` milik proyek (bukan `next-themes`).
4
+ *
5
+ * Pakai: `import { toast } from "sonner"` lalu `toast.success(...)` /
6
+ * `toast.error(...)`. Mount `<Toaster />` sekali di akar aplikasi.
7
+ */
8
+ import {
9
+ CircleCheckIcon,
10
+ InfoIcon,
11
+ Loader2Icon,
12
+ OctagonXIcon,
13
+ TriangleAlertIcon,
14
+ } from "lucide-react";
15
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
16
+ import { useTheme } from "@/hooks/useTheme";
17
+
18
+ const Toaster = ({ ...props }: ToasterProps) => {
19
+ const { theme } = useTheme();
20
+
21
+ return (
22
+ <Sonner
23
+ theme={theme}
24
+ className="toaster group"
25
+ icons={{
26
+ success: <CircleCheckIcon className="size-4" />,
27
+ info: <InfoIcon className="size-4" />,
28
+ warning: <TriangleAlertIcon className="size-4" />,
29
+ error: <OctagonXIcon className="size-4" />,
30
+ loading: <Loader2Icon className="size-4 animate-spin" />,
31
+ }}
32
+ style={
33
+ {
34
+ "--normal-bg": "var(--popover)",
35
+ "--normal-text": "var(--popover-foreground)",
36
+ "--normal-border": "var(--border)",
37
+ "--border-radius": "var(--radius)",
38
+ } as React.CSSProperties
39
+ }
40
+ {...props}
41
+ />
42
+ );
43
+ };
44
+
45
+ export { Toaster };
@@ -0,0 +1,16 @@
1
+ import { Loader2Icon } from "lucide-react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
6
+ return (
7
+ <Loader2Icon
8
+ role="status"
9
+ aria-label="Loading"
10
+ className={cn("size-4 animate-spin", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ export { Spinner };
@@ -0,0 +1,32 @@
1
+ import { Switch as SwitchPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Switch({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<typeof SwitchPrimitive.Root> & {
10
+ size?: "sm" | "default";
11
+ }) {
12
+ return (
13
+ <SwitchPrimitive.Root
14
+ data-slot="switch"
15
+ data-size={size}
16
+ className={cn(
17
+ "peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <SwitchPrimitive.Thumb
23
+ data-slot="switch-thumb"
24
+ className={cn(
25
+ "pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
26
+ )}
27
+ />
28
+ </SwitchPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ export { Switch };
@@ -0,0 +1,18 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Textarea };
@@ -0,0 +1,51 @@
1
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
2
+ import type * as React from "react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delayDuration={delayDuration}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
20
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
21
+ }
22
+
23
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
24
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
25
+ }
26
+
27
+ function TooltipContent({
28
+ className,
29
+ sideOffset = 0,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
33
+ return (
34
+ <TooltipPrimitive.Portal>
35
+ <TooltipPrimitive.Content
36
+ data-slot="tooltip-content"
37
+ sideOffset={sideOffset}
38
+ className={cn(
39
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
40
+ className,
41
+ )}
42
+ {...props}
43
+ >
44
+ {children}
45
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
46
+ </TooltipPrimitive.Content>
47
+ </TooltipPrimitive.Portal>
48
+ );
49
+ }
50
+
51
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
package/src/config.ts ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Config_File & validasi Sandbox_Root saat startup.
3
+ * Sesuai `design.md` — `config.ts`.
4
+ *
5
+ * Skema minimal: `{ "sandboxRoot": "/abs/path" }`.
6
+ * Path default mengikuti mode runtime (`src/runtime.ts`):
7
+ * - **cli** (`kcgcode`): `~/.kcgcode/config.json`
8
+ * - **dev** (`bun dev`): `./kcg-code.config.json` bila ada, else user config
9
+ * `loadConfig()` keluar dengan kode error bila `sandboxRoot` tidak diatur
10
+ * atau direktori yang ditunjuk tidak ditemukan (Requirement 10.1).
11
+ */
12
+ import { readFileSync, realpathSync } from "node:fs";
13
+ import { DEFAULT_CONFIG_PATH, resolveEffectiveConfigPath } from "./paths";
14
+ import type { Result } from "./server/result";
15
+
16
+ export { DEFAULT_CONFIG_PATH };
17
+
18
+ export interface AppConfig {
19
+ /** Path absolut hasil `realpath` dari `sandboxRoot`. */
20
+ sandboxRoot: string;
21
+ /** Path berkas konfigurasi yang dibaca. */
22
+ configPath: string;
23
+ }
24
+
25
+ /**
26
+ * Membaca dan memvalidasi Config_File tanpa menghentikan proses.
27
+ * Dipisah dari `loadConfig()` agar dapat diuji secara unit (task 2.2).
28
+ */
29
+ export function resolveConfig(configPath?: string): Result<AppConfig> {
30
+ const file = resolveEffectiveConfigPath(configPath);
31
+
32
+ let raw: string;
33
+ try {
34
+ raw = readFileSync(file, "utf8");
35
+ } catch (err) {
36
+ return {
37
+ ok: false,
38
+ error: `[config] cannot read config file "${file}": ${(err as Error).message}`,
39
+ };
40
+ }
41
+
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ } catch (err) {
46
+ return {
47
+ ok: false,
48
+ error: `[config] invalid JSON in "${file}": ${(err as Error).message}`,
49
+ };
50
+ }
51
+
52
+ const obj = parsed as Record<string, unknown>;
53
+ const sandboxRoot = obj.sandboxRoot;
54
+ if (typeof sandboxRoot !== "string" || sandboxRoot.trim() === "") {
55
+ return {
56
+ ok: false,
57
+ error: `[config] "sandboxRoot" is not set in "${file}"`,
58
+ };
59
+ }
60
+
61
+ let resolved: string;
62
+ try {
63
+ resolved = realpathSync(sandboxRoot);
64
+ } catch (err) {
65
+ return {
66
+ ok: false,
67
+ error: `[config] sandboxRoot directory not found: ${sandboxRoot} (${(err as Error).message})`,
68
+ };
69
+ }
70
+
71
+ return { ok: true, data: { sandboxRoot: resolved, configPath: file } };
72
+ }
73
+
74
+ /**
75
+ * Memuat konfigurasi saat startup. Bila tidak valid, log pesan jelas dan
76
+ * proses keluar dengan kode error (Requirement 10.1).
77
+ */
78
+ export function loadConfig(): AppConfig {
79
+ const res = resolveConfig();
80
+ if (!res.ok) {
81
+ console.error(res.error);
82
+ process.exit(1);
83
+ }
84
+ return res.data;
85
+ }
package/src/db.ts ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * `Session_Store` — komposisi root persistensi `bun:sqlite` (headless).
3
+ * Sesuai `design.md`: koneksi DB + migrasi + penyatuan repository per-domain.
4
+ *
5
+ * Lapisan repository hidup di `src/server/db/`:
6
+ * - `schema.ts` — skema SQLite & migrasi idempoten
7
+ * - `rows.ts` — pemetaan baris SQLite -> tipe domain
8
+ * - `projects.ts` — repository Projects
9
+ * - `sessions.ts` — repository Sessions + riwayat status (append-only)
10
+ * - `messages.ts` — repository pesan terstruktur (append-only)
11
+ * - `prompts.ts` — repository prompt interaktif
12
+ * - `auth.ts` — kunci aplikasi (lock screen) & sesi login
13
+ * - `tunnel.ts` — akun tunnel publik (Google + kcgcode-rp)
14
+ *
15
+ * Konvensi: `session_status_history` dan `messages` bersifat **append-only**
16
+ * (insert-only) sehingga Requirement 3.1/3.3 terpenuhi secara struktural.
17
+ * Seluruh operasi tulis dibungkus try/catch; kegagalan mengembalikan
18
+ * `{ ok: false, error }` tanpa menghapus data lama (Requirement 3.2).
19
+ */
20
+ import { Database } from "bun:sqlite";
21
+ import { mkdirSync } from "node:fs";
22
+ import path from "node:path";
23
+ import { DEFAULT_DB_PATH, resolveEffectiveDbPath } from "./paths";
24
+ import { type AuthRepo, createAuthRepo } from "./server/db/auth";
25
+ import { createMessageRepo, type MessageRepo } from "./server/db/messages";
26
+ import { createProjectRepo, type ProjectRepo } from "./server/db/projects";
27
+ import { createPromptRepo, type PromptRepo } from "./server/db/prompts";
28
+ import { runMigrations } from "./server/db/schema";
29
+ import { createSessionRepo, type SessionRepo, type StatusHistoryEntry } from "./server/db/sessions";
30
+ import { createTunnelRepo, type TunnelRepo } from "./server/db/tunnel";
31
+ import type { Result, SimpleResult } from "./server/result";
32
+ import type {
33
+ InteractivePrompt,
34
+ Project,
35
+ PromptStatus,
36
+ Session,
37
+ SessionMessage,
38
+ SessionModel,
39
+ SessionStatus,
40
+ } from "./types";
41
+
42
+ export { DEFAULT_DB_PATH };
43
+
44
+ export interface SessionStore extends AuthRepo, TunnelRepo {
45
+ // ---- Pesan terstruktur (append-only) ----
46
+ insertMessage(message: SessionMessage): Result<SessionMessage>;
47
+ getMessages(sessionId: string): Result<SessionMessage[]>;
48
+
49
+ // ---- Riwayat status (append-only) ----
50
+ insertStatusHistory(
51
+ sessionId: string,
52
+ status: SessionStatus,
53
+ changedAt?: number,
54
+ ): Result<StatusHistoryEntry>;
55
+ getStatusHistory(sessionId: string): Result<StatusHistoryEntry[]>;
56
+
57
+ // ---- Sessions (CRUD) ----
58
+ insertSession(session: Session): Result<Session>;
59
+ updateSessionStatus(
60
+ sessionId: string,
61
+ status: SessionStatus,
62
+ changedAt?: number,
63
+ ): Result<Session>;
64
+ /** Perbarui ocSessionId tanpa mengubah status (dipakai resumeSession). */
65
+ updateSessionOcId(sessionId: string, ocSessionId: string | null): Result<Session>;
66
+ /** Perbarui model pilihan Session; `null` = kembali ke default opencode. */
67
+ updateSessionModel(sessionId: string, model: SessionModel | null): Result<Session>;
68
+ /** Perbarui agent (mode) pilihan Session; `null` = default opencode. */
69
+ updateSessionAgent(sessionId: string, agent: string | null): Result<Session>;
70
+ /** Simpan judul Session hasil generate opencode (SSE `session.updated`). */
71
+ updateSessionTitle(sessionId: string, title: string): Result<Session>;
72
+ getSession(sessionId: string): Result<Session>;
73
+ getSessionByOcId(ocSessionId: string): Result<Session>;
74
+ listSessions(): Session[];
75
+ /**
76
+ * Hapus Session beserta seluruh baris anaknya (messages, prompts,
77
+ * status history) dalam satu transaksi. `ok: false` bila tidak ada.
78
+ */
79
+ deleteSession(sessionId: string): SimpleResult;
80
+
81
+ // ---- Projects (CRUD) ----
82
+ insertProject(project: Project): Result<Project>;
83
+ getProjectById(id: string): Result<Project>;
84
+ getProjectByName(name: string): Result<Project>;
85
+ getProjectByPath(filePath: string): Result<Project>;
86
+ listProjects(): Project[];
87
+ /** Simpan custom instruction Project; `null` = hapus instruksi. */
88
+ updateProjectInstructions(projectId: string, instructions: string | null): Result<Project>;
89
+ /** Session milik satu Project (dipakai sebelum menghapus Project). */
90
+ listProjectSessions(projectId: string): Session[];
91
+ /**
92
+ * Hapus baris Project. Menolak (`PROJECT_HAS_SESSIONS`) bila masih ada
93
+ * Session yang menunjuk Project ini — Session harus dihapus lebih dulu
94
+ * lewat `SessionManager.deleteSession` agar sesi remote opencode dan
95
+ * lampirannya ikut dibersihkan, bukan ditinggal yatim.
96
+ */
97
+ deleteProject(projectId: string): SimpleResult;
98
+
99
+ // ---- Prompts (CRUD) ----
100
+ insertPrompt(prompt: InteractivePrompt): Result<InteractivePrompt>;
101
+ getPrompt(promptId: string): Result<InteractivePrompt>;
102
+ listPendingPrompts(sessionId: string): InteractivePrompt[];
103
+ updatePromptStatus(
104
+ promptId: string,
105
+ status: PromptStatus,
106
+ resolvedAt?: number,
107
+ ): Result<InteractivePrompt>;
108
+
109
+ close(): void;
110
+ }
111
+
112
+ /**
113
+ * Membuka koneksi ke database, menyalakan WAL, menjalankan migrasi
114
+ * idempoten, lalu menyatukan repository per-domain menjadi satu `SessionStore`.
115
+ * Default sesuai mode: dev `./data/kcg-code.sqlite`, cli `~/.kcgcode/data/...`.
116
+ */
117
+ export function openSessionStore(dbPath?: string): SessionStore {
118
+ const resolvedPath = dbPath ?? resolveEffectiveDbPath();
119
+ if (resolvedPath !== ":memory:") {
120
+ mkdirSync(path.dirname(path.resolve(resolvedPath)), { recursive: true });
121
+ }
122
+ const db = new Database(resolvedPath, { create: true });
123
+ db.exec("PRAGMA journal_mode = WAL;");
124
+ runMigrations(db);
125
+
126
+ const projects: ProjectRepo = createProjectRepo(db);
127
+ const sessions: SessionRepo = createSessionRepo(db);
128
+ const messages: MessageRepo = createMessageRepo(db);
129
+ const prompts: PromptRepo = createPromptRepo(db);
130
+ const auth: AuthRepo = createAuthRepo(db);
131
+ const tunnel: TunnelRepo = createTunnelRepo(db);
132
+
133
+ const store: SessionStore = {
134
+ ...projects,
135
+ ...sessions,
136
+ ...messages,
137
+ ...prompts,
138
+ ...auth,
139
+ ...tunnel,
140
+ close() {
141
+ db.close();
142
+ },
143
+ };
144
+
145
+ return store;
146
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * This file is the entry point for the React app, it sets up the root
3
+ * element and renders the App component to the DOM.
4
+ *
5
+ * It is included in `src/index.html`.
6
+ */
7
+
8
+ import { StrictMode } from "react";
9
+ import { createRoot } from "react-dom/client";
10
+ import { App } from "./App";
11
+
12
+ const elem = document.getElementById("root");
13
+ if (!elem) {
14
+ throw new Error("Root element #root not found in index.html");
15
+ }
16
+ const app = (
17
+ <StrictMode>
18
+ <App />
19
+ </StrictMode>
20
+ );
21
+
22
+ // https://bun.com/docs/bundler/hot-reloading#import-meta-hot-data
23
+ //
24
+ // API HMR (`import.meta.hot`) hanya ada saat `bun --hot`; pada build produksi
25
+ // Bun mengganti/menghilangkannya, jadi pola di bawah memakai guard literal
26
+ // `if (import.meta.hot)` yang di-tree-shake bundler saat produksi — blok else
27
+ // itulah yang tersisa di bundle produksi (createRoot sekali, tanpa HMR API).
28
+ if (import.meta.hot) {
29
+ if (!import.meta.hot.data.root) {
30
+ import.meta.hot.data.root = createRoot(elem);
31
+ }
32
+ import.meta.hot.data.root.render(app);
33
+ } else {
34
+ // Produksi: PWA_Shell (Requirement 8.1) — registrasi service worker & inject
35
+ // link manifest hanya di sini agar asset statis di-cache untuk instalasi PWA
36
+ // tanpa merusak HMR saat dev. Manifest di-inject runtime agar URL tetap
37
+ // `/manifest.json` (tidak di-rewrite bundler menjadi hashed asset), disajikan
38
+ // route server.
39
+ createRoot(elem).render(app);
40
+
41
+ const manifestLink = document.createElement("link");
42
+ manifestLink.rel = "manifest";
43
+ manifestLink.href = "/manifest.json";
44
+ document.head.appendChild(manifestLink);
45
+
46
+ if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
47
+ navigator.serviceWorker.register("/sw.js").catch(() => {
48
+ // Gagal registrasi SW tidak menghalangi aplikasi.
49
+ });
50
+ }
51
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Unit test logika murni referensi `@file` (src/hooks/useMention.ts).
3
+ *
4
+ * `bun test` tidak menyediakan DOM, sehingga yang diuji adalah
5
+ * `activeMention()` (deteksi token @query pada posisi kursor) dan
6
+ * `applyMention()` (substitusi token saat saran dipilih) — pola yang sama
7
+ * dengan test `useTheme.ts` / `useRouter.ts`.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+ import { activeMention, applyMention } from "../useMention";
11
+
12
+ describe("activeMention", () => {
13
+ test("token @ aktif di akhir input", () => {
14
+ expect(activeMention("lihat @", 7)).toEqual({ query: "", start: 6, end: 7 });
15
+ expect(activeMention("lihat @sr", 9)).toEqual({ query: "sr", start: 6, end: 9 });
16
+ });
17
+
18
+ test("kursor di tengah token", () => {
19
+ // caret 6 pada "@src/app" -> token "@src/a"
20
+ expect(activeMention("@src/app", 6)).toEqual({ query: "src/a", start: 0, end: 6 });
21
+ // caret 8 -> token "@src/app"
22
+ expect(activeMention("@src/app", 8)).toEqual({ query: "src/app", start: 0, end: 8 });
23
+ });
24
+
25
+ test("bukan mention: tanpa @, @ tertutup spasi, atau kursor setelah spasi", () => {
26
+ expect(activeMention("halo", 4)).toBeNull();
27
+ expect(activeMention("@src done", 9)).toBeNull(); // spasi menutup token
28
+ expect(activeMention("a @ b", 5)).toBeNull();
29
+ });
30
+
31
+ test("@ yang menempel kata lain (email) bukan mention", () => {
32
+ expect(activeMention("user@mail", 9)).toBeNull();
33
+ });
34
+
35
+ test("spasi di dalam path menghentikan token", () => {
36
+ // kursor tepat sebelum spasi masih aktif
37
+ expect(activeMention("@src/ app", 5)).toEqual({ query: "src/", start: 0, end: 5 });
38
+ // setelah spasi -> tidak aktif
39
+ expect(activeMention("@src/ app", 9)).toBeNull();
40
+ });
41
+
42
+ test("properti — kursor di luar rentang wajar", () => {
43
+ expect(activeMention("abc", -1)).toBeNull();
44
+ expect(activeMention("abc", 10)).toBeNull();
45
+ });
46
+ });
47
+
48
+ describe("applyMention", () => {
49
+ test("mengganti token @query dengan @path di posisi tepat", () => {
50
+ const range = activeMention("lihat @sr", 9);
51
+ if (!range) throw new Error("range null");
52
+ expect(applyMention("lihat @sr", range, "src/App.tsx")).toBe("lihat @src/App.tsx");
53
+ });
54
+
55
+ test("menambah spasi penutup bila karakter berikut non-pemisah", () => {
56
+ // range.end = 8 tepat sebelum spasi? tidak — pakai teks tanpa spasi:
57
+ const t2 = "@src/app";
58
+ const range2 = activeMention(t2, 8);
59
+ if (!range2) throw new Error("range null");
60
+ expect(applyMention(t2, range2, "src/app.tsx")).toBe("@src/app.tsx");
61
+ // Suffix spasi terjadi bila token disusul karakter non-pemisah:
62
+ // "@sr|c x" (kursor di |, end=3) -> ganti "@sr" -> "@s/x.ts" + " c x".
63
+ const range = { query: "sr", start: 0, end: 3 };
64
+ expect(applyMention("@src x", range, "s/x.ts")).toBe("@s/x.ts c x");
65
+ });
66
+
67
+ test("tanpa spasi ekstra bila di akhir teks", () => {
68
+ const range = activeMention("@a", 2);
69
+ if (!range) throw new Error("range null");
70
+ expect(applyMention("@a", range, "a.ts")).toBe("@a.ts");
71
+ });
72
+
73
+ test("sisa teks setelah token tetap utuh", () => {
74
+ const text = "@sr dan sisa";
75
+ const range = activeMention(text, 3);
76
+ if (!range) throw new Error("range null");
77
+ expect(applyMention(text, range, "src/x.ts")).toBe("@src/x.ts dan sisa");
78
+ });
79
+ });
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Test `useResyncOnReconnect` di React sungguhan (tanpa DOM, renderer minimal
3
+ * dari `react-reconciler` — pola yang sama dengan `useSmoothText.react.test`).
4
+ */
5
+ import { beforeEach, describe, expect, test } from "bun:test";
6
+ import { act, createElement, StrictMode } from "react";
7
+ import Reconciler from "react-reconciler";
8
+ import { useResyncOnReconnect } from "../useResyncOnReconnect";
9
+ import type { WsConnectionStatus } from "../useWebSocket";
10
+
11
+ const g = globalThis as Record<string, unknown>;
12
+
13
+ const reconciler = Reconciler({
14
+ supportsMutation: true,
15
+ isPrimaryRenderer: false,
16
+ createInstance: () => ({}),
17
+ createTextInstance: () => ({}),
18
+ appendInitialChild: () => {},
19
+ appendChild: () => {},
20
+ appendChildToContainer: () => {},
21
+ insertBefore: () => {},
22
+ insertInContainerBefore: () => {},
23
+ removeChild: () => {},
24
+ removeChildFromContainer: () => {},
25
+ commitTextUpdate: () => {},
26
+ commitUpdate: () => {},
27
+ finalizeInitialChildren: () => false,
28
+ shouldSetTextContent: () => false,
29
+ getRootHostContext: () => ({}),
30
+ getChildHostContext: () => ({}),
31
+ getPublicInstance: (i: unknown) => i,
32
+ prepareForCommit: () => null,
33
+ resetAfterCommit: () => {},
34
+ preparePortalMount: () => {},
35
+ clearContainer: () => {},
36
+ scheduleTimeout: setTimeout,
37
+ cancelTimeout: clearTimeout,
38
+ noTimeout: -1,
39
+ getCurrentUpdatePriority: () => 32,
40
+ setCurrentUpdatePriority: () => {},
41
+ resolveUpdatePriority: () => 32,
42
+ getInstanceFromNode: () => null,
43
+ beforeActiveInstanceBlur: () => {},
44
+ afterActiveInstanceBlur: () => {},
45
+ prepareScopeUpdate: () => {},
46
+ getInstanceFromScope: () => null,
47
+ detachDeletedInstance: () => {},
48
+ maySuspendCommit: () => false,
49
+ preloadInstance: () => true,
50
+ startSuspendingCommit: () => {},
51
+ suspendInstance: () => {},
52
+ waitForCommitToBeReady: () => null,
53
+ NotPendingTransition: null,
54
+ HostTransitionContext: { $$typeof: Symbol.for("react.context"), _currentValue: null },
55
+ resetFormInstance: () => {},
56
+ requestPostPaintCallback: () => {},
57
+ shouldAttemptEagerTransition: () => false,
58
+ trackSchedulerEvent: () => {},
59
+ resolveEventType: () => null,
60
+ resolveEventTimeStamp: () => -1.1,
61
+ });
62
+
63
+ function mount() {
64
+ let calls = 0;
65
+ function Probe({ status }: { status: WsConnectionStatus }) {
66
+ useResyncOnReconnect(status, () => {
67
+ calls += 1;
68
+ });
69
+ return null;
70
+ }
71
+ const root = reconciler.createContainer(
72
+ {},
73
+ 1,
74
+ null,
75
+ false,
76
+ null,
77
+ "",
78
+ () => {},
79
+ () => {},
80
+ () => {},
81
+ null,
82
+ );
83
+ const render = (status: WsConnectionStatus) =>
84
+ act(() => {
85
+ reconciler.updateContainer(
86
+ createElement(StrictMode, null, createElement(Probe, { status })),
87
+ root,
88
+ null,
89
+ null,
90
+ );
91
+ });
92
+ return { render, calls: () => calls };
93
+ }
94
+
95
+ beforeEach(() => {
96
+ g.IS_REACT_ACT_ENVIRONMENT = true;
97
+ });
98
+
99
+ describe("useResyncOnReconnect", () => {
100
+ test("koneksi pertama TIDAK memicu resync", () => {
101
+ const h = mount();
102
+ h.render("connecting");
103
+ h.render("connected");
104
+ expect(h.calls()).toBe(0);
105
+ });
106
+
107
+ test("tersambung ulang setelah putus -> resync sekali", () => {
108
+ const h = mount();
109
+ h.render("connecting");
110
+ h.render("connected");
111
+ h.render("reconnecting");
112
+ h.render("connected");
113
+ expect(h.calls()).toBe(1);
114
+ // Tetap terhubung -> tidak berulang.
115
+ h.render("connected");
116
+ expect(h.calls()).toBe(1);
117
+ });
118
+
119
+ test("setiap putus-sambung memicu resync", () => {
120
+ const h = mount();
121
+ h.render("connected");
122
+ for (let i = 0; i < 3; i++) {
123
+ h.render("reconnecting");
124
+ h.render("connected");
125
+ }
126
+ expect(h.calls()).toBe(3);
127
+ });
128
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Unit test navigasi SPA (src/hooks/useRouter.ts).
3
+ *
4
+ * `bun test` tidak menyediakan DOM, sehingga hanya `navigate` yang diuji
5
+ * aman dipanggil tanpa History API. Pemetaan pathname -> view diuji lewat
6
+ * `parseRoute()` di `src/lib/__tests__/routes.test.ts`; hook `useRouter`
7
+ * diuji manual via dev server.
8
+ */
9
+ import { describe, expect, test } from "bun:test";
10
+ import { navigate } from "../useRouter";
11
+
12
+ describe("navigate", () => {
13
+ test("aman dipanggil tanpa History API (lingkungan non-browser)", () => {
14
+ // Di bun test tidak ada `window` -> tidak boleh melempar.
15
+ expect(() => navigate("/projects/p1")).not.toThrow();
16
+ });
17
+ });