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
package/src/lib/api.ts ADDED
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Helper fetch API KCG Code (task 24).
3
+ *
4
+ * - Kesalahan HTTP dilempar sebagai `ApiError` dengan `status` dan `code`
5
+ * (dari body `{ error }`) agar komponen menampilkan pesan yang tepat.
6
+ */
7
+
8
+ /** Error hasil permintaan API dengan status HTTP dan kode domain. */
9
+ export class ApiError extends Error {
10
+ readonly status: number;
11
+ readonly code: string;
12
+ /** Body JSON respons error (detail tambahan, mis. daftar item penghalang). */
13
+ readonly body: Record<string, unknown> | null;
14
+
15
+ constructor(
16
+ status: number,
17
+ code: string,
18
+ message: string,
19
+ body?: Record<string, unknown> | null,
20
+ ) {
21
+ super(message);
22
+ this.name = "ApiError";
23
+ this.status = status;
24
+ this.code = code;
25
+ this.body = body ?? null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Dipanggil saat server membalas 401 `AUTH_REQUIRED` (sesi habis / terkunci
31
+ * otomatis / dicabut) — lock screen dipasang oleh `lib/auth.ts`.
32
+ */
33
+ let onAuthRequired: (() => void) | null = null;
34
+ export function setAuthRequiredHandler(fn: (() => void) | null): void {
35
+ onAuthRequired = fn;
36
+ }
37
+
38
+ /** Pemetaan kode error domain -> pesan ramah pengguna (design.md — Error Handling). */
39
+ export function apiErrorMessage(code: string): string {
40
+ // Kode dari server headless opencode membawa detail status, mis.
41
+ // "OC_LIST_MODELS_FAILED(500)" -> cocokkan berdasarkan prefiks.
42
+ if (code.startsWith("OC_LIST_MODELS_FAILED")) {
43
+ return "Failed to load the model list from the opencode server.";
44
+ }
45
+ if (code.startsWith("OC_CREATE_SESSION_FAILED")) {
46
+ return "Failed to create the session on the opencode server.";
47
+ }
48
+ if (code.startsWith("OC_DELETE_SESSION_FAILED")) {
49
+ return "Failed to delete the session on the opencode server.";
50
+ }
51
+ if (code.startsWith("SKILLS_SEARCH_FAILED") || code.startsWith("SKILLS_AUDIT_FAILED")) {
52
+ return "Couldn't reach skills.sh. Check your connection and try again.";
53
+ }
54
+ if (code.startsWith("SERVER_START_FAILED")) {
55
+ return "Failed to start the opencode server for this project.";
56
+ }
57
+
58
+ switch (code) {
59
+ case "NAME_REQUIRED":
60
+ return "Project name is required.";
61
+ case "NAME_TAKEN":
62
+ return "That project name is already in use.";
63
+ case "PATH_TAKEN":
64
+ return "That path is already used by another project.";
65
+ case "PATH_OUTSIDE_SANDBOX":
66
+ return "The path is outside the Sandbox root.";
67
+ case "INVALID_PATH_CHARS":
68
+ return "The path contains invalid characters.";
69
+ case "PROJECT_NOT_FOUND":
70
+ return "Project not found.";
71
+ case "PROJECT_DIR_NOT_FOUND":
72
+ return "The project directory was not found on the server.";
73
+ case "PROJECT_HAS_SESSIONS":
74
+ return "The project still has sessions. Delete them first.";
75
+ case "UNSUPPORTED_AGENT_TYPE":
76
+ return "This CLI_Agent type is not supported.";
77
+ case "SESSION_NOT_FOUND":
78
+ return "Session not found.";
79
+ case "SESSION_NOT_RUNNING":
80
+ return "The session is not running.";
81
+ case "SESSION_ALREADY_RUNNING":
82
+ return "The session is already running.";
83
+ case "MODEL_NOT_FOUND":
84
+ return "The model is not available on this project's opencode server.";
85
+ case "INVALID_INSTRUCTIONS":
86
+ return "Instructions must be text.";
87
+ case "INSTRUCTIONS_TOO_LONG":
88
+ return "Instructions are too long (max 20,000 characters).";
89
+ case "INVALID_SKILL_QUERY":
90
+ return "Type at least 2 characters to search.";
91
+ case "INVALID_SKILL_ID":
92
+ return "That skill identifier is not valid.";
93
+ case "SKILLS_RATE_LIMITED":
94
+ return "skills.sh is rate limiting requests. Wait a moment and try again.";
95
+ case "SKILL_INSTALL_IN_PROGRESS":
96
+ return "Another skill is being installed in this project. Try again when it finishes.";
97
+ case "SKILL_INSTALL_FAILED":
98
+ return "Failed to install the skill. See the output for details.";
99
+ case "SKILL_INSTALL_CANCELLED":
100
+ return "Installation was cancelled.";
101
+ case "SKILL_INSTALL_TIMEOUT":
102
+ return "Installation timed out and was stopped.";
103
+ case "INSTALL_JOB_NOT_RUNNING":
104
+ return "That installation has already finished.";
105
+ case "INSTALL_JOB_LOST":
106
+ return "The server restarted before the installation finished.";
107
+ case "INSTALL_JOB_NOT_FOUND":
108
+ return "That installation is no longer available.";
109
+ case "AUTH_REQUIRED":
110
+ return "Your session is locked. Unlock to continue.";
111
+ case "AUTH_INVALID":
112
+ return "Incorrect PIN or password.";
113
+ case "CURRENT_SECRET_INVALID":
114
+ return "Your current PIN or password is incorrect.";
115
+ case "AUTH_RATE_LIMITED":
116
+ return "Too many attempts. Wait a moment and try again.";
117
+ case "AUTH_NOT_CONFIGURED":
118
+ return "No lock has been set up yet.";
119
+ case "WORKSPACE_PATH_REQUIRED":
120
+ return "Enter a folder path.";
121
+ case "WORKSPACE_PATH_INVALID":
122
+ return "That path isn't valid.";
123
+ case "WORKSPACE_PATH_NOT_ABSOLUTE":
124
+ return "Use a full path, like ~/code or /srv/projects.";
125
+ case "WORKSPACE_NOT_FOUND":
126
+ return 'That folder doesn\'t exist. Tick "Create the folder" to make it.';
127
+ case "WORKSPACE_PARENT_NOT_FOUND":
128
+ return "The folder above it doesn't exist. Only the last folder can be created.";
129
+ case "WORKSPACE_NOT_DIRECTORY":
130
+ return "That path is a file, not a folder.";
131
+ case "WORKSPACE_TOO_BROAD":
132
+ return "That folder is too broad. Pick a dedicated folder, not your home or a system folder.";
133
+ case "WORKSPACE_CONTAINS_APP_DATA":
134
+ return "That folder overlaps KCG Code's own data. Pick a different folder.";
135
+ case "WORKSPACE_NOT_WRITABLE":
136
+ return "KCG Code can't write to that folder. Check its permissions.";
137
+ case "WORKSPACE_PROJECTS_OUTSIDE":
138
+ return "Some projects are outside the new folder. Remove them or pick a folder that contains them:";
139
+ case "WORKSPACE_CREATE_FAILED":
140
+ return "Couldn't create the folder.";
141
+ case "WORKSPACE_CONFIG_UNREADABLE":
142
+ case "WORKSPACE_CONFIG_WRITE_FAILED":
143
+ return "Couldn't save the config file. Check its permissions.";
144
+ case "PASSKEY_INVALID":
145
+ return "That passkey wasn't accepted. Try again or use your PIN or password.";
146
+ case "PASSKEY_NONE":
147
+ return "No passkey is set up for this address yet.";
148
+ case "PASSKEY_UNSUPPORTED_ORIGIN":
149
+ return "Passkeys need a secure address (https or localhost).";
150
+ case "PASSKEY_CHALLENGE_INVALID":
151
+ return "The passkey request expired. Try again.";
152
+ case "PASSKEY_VERIFY_FAILED":
153
+ return "Couldn't verify the passkey. Try again.";
154
+ case "PASSKEY_ALREADY_REGISTERED":
155
+ return "This passkey is already added.";
156
+ case "PASSKEY_LIMIT":
157
+ return "You've reached the maximum of 20 passkeys. Remove one first.";
158
+ case "PASSKEY_LOCK_REQUIRED":
159
+ return "Set up a PIN or password before adding a passkey.";
160
+ case "PASSKEY_NAME_INVALID":
161
+ return "Passkey names must be 1–60 characters.";
162
+ case "PASSKEY_NOT_FOUND":
163
+ return "That passkey no longer exists.";
164
+ case "PIN_DIGITS_ONLY":
165
+ return "PIN can only contain numbers.";
166
+ case "PIN_LENGTH":
167
+ return "PIN must be 6–12 digits.";
168
+ case "PIN_TOO_SIMPLE":
169
+ return "That PIN is too easy to guess. Avoid repeated or sequential digits.";
170
+ case "PASSWORD_TOO_SHORT":
171
+ return "Password must be at least 8 characters.";
172
+ case "PASSWORD_TOO_LONG":
173
+ return "Password must be at most 128 characters.";
174
+ case "NICKNAME_TOO_LONG":
175
+ return "Nickname must be at most 40 characters.";
176
+ case "NICKNAME_INVALID":
177
+ return "Nickname contains invalid characters.";
178
+ case "AVATAR_PRESET_INVALID":
179
+ return "That avatar isn't available.";
180
+ case "AVATAR_TOO_LARGE":
181
+ return "The photo exceeds the 2 MB limit.";
182
+ case "ORIGIN_FORBIDDEN":
183
+ return "Request blocked: it didn't come from this app.";
184
+ case "INVALID_JSON":
185
+ return "The request JSON format is invalid.";
186
+ case "UNSUPPORTED_IMAGE_MIME":
187
+ return "Unsupported image format. Use PNG, JPEG, GIF, or WebP.";
188
+ case "IMAGE_TOO_LARGE":
189
+ return "The image exceeds the 20 MiB limit.";
190
+ case "FILE_TOO_LARGE":
191
+ return "The file exceeds the 20 MiB limit.";
192
+ case "EMPTY_UPLOAD":
193
+ return "The file is empty.";
194
+ case "ATTACHMENT_NOT_FOUND":
195
+ return "The attachment was not found (it may have been deleted).";
196
+ case "TUNNEL_LOCK_REQUIRED":
197
+ return "Set an app lock before turning on remote access.";
198
+ case "TUNNEL_NOT_CONFIGURED":
199
+ return "Remote access isn't configured. Set KCG_TUNNEL_API_URL and restart kcgcode.";
200
+ case "TUNNEL_SIGNED_OUT":
201
+ return "Connect this machine first.";
202
+ case "TUNNEL_PAIRING_EXPIRED":
203
+ return "The code expired before it was approved. Connect again.";
204
+ case "TUNNEL_PAIRING_DENIED":
205
+ return "The request was denied on the sign-in page.";
206
+ case "TUNNEL_DEVICE_REVOKED":
207
+ return "This machine was disconnected from your account. Connect again.";
208
+ case "TUNNEL_RATE_LIMITED":
209
+ return "Too many attempts. Wait a few minutes and try again.";
210
+ case "TUNNEL_API_UNREACHABLE":
211
+ return "Couldn't reach the remote access service. Check your connection.";
212
+ case "TUNNEL_AUTH_FAILED":
213
+ return "The tunnel server rejected this device. Try rotating the secret.";
214
+ case "TUNNEL_PROXY_FAILED":
215
+ return "The tunnel server couldn't publish your address. See the log for details.";
216
+ case "FRPC_UNSUPPORTED_PLATFORM":
217
+ return "Remote access isn't supported on this OS or CPU yet.";
218
+ case "LHR_SSH_MISSING":
219
+ return "localhost.run needs the ssh command, which wasn't found on this computer.";
220
+ case "LHR_START_FAILED":
221
+ return "Couldn't start the connection to localhost.run.";
222
+ case "LHR_DENIED":
223
+ return "localhost.run refused the connection. Try again in a moment.";
224
+ case "INVALID_PROVIDER":
225
+ return "Unknown remote access option.";
226
+ case "FRPC_DOWNLOAD_FAILED":
227
+ case "FRPC_NOT_FOUND":
228
+ return "Couldn't install the tunnel client (frpc). See the log for details.";
229
+ case "FRPC_CHECKSUM_MISMATCH":
230
+ return "The downloaded tunnel client failed verification and was discarded.";
231
+ case "FRPC_START_FAILED":
232
+ return "Couldn't start the tunnel client. See the log for details.";
233
+ default:
234
+ return `Something went wrong (${code}).`;
235
+ }
236
+ }
237
+
238
+ /**
239
+ * `fetch` dengan header otentikasi & penanganan error terstruktur.
240
+ * Melempar `ApiError` untuk status selain 2xx.
241
+ */
242
+ export async function apiFetch(path: string, init: RequestInit = {}): Promise<Response> {
243
+ const headers = new Headers(init.headers);
244
+ // FormData/Blob: browser yang menetapkan content-type (+ boundary multipart).
245
+ const browserTyped =
246
+ init.body instanceof FormData ||
247
+ init.body instanceof Blob ||
248
+ init.body instanceof URLSearchParams;
249
+ if (init.body != null && !browserTyped && !headers.has("content-type")) {
250
+ headers.set("content-type", "application/json");
251
+ }
252
+ const res = await fetch(path, { ...init, headers });
253
+ if (!res.ok) {
254
+ let code = `HTTP_${res.status}`;
255
+ let body: Record<string, unknown> | null = null;
256
+ try {
257
+ const parsed = (await res.json()) as unknown;
258
+ if (parsed && typeof parsed === "object") {
259
+ body = parsed as Record<string, unknown>;
260
+ if (typeof body.error === "string" && body.error !== "") code = body.error;
261
+ }
262
+ } catch {
263
+ // body bukan JSON — pakai kode HTTP default.
264
+ }
265
+ if (res.status === 401 && code === "AUTH_REQUIRED") onAuthRequired?.();
266
+ throw new ApiError(res.status, code, apiErrorMessage(code), body);
267
+ }
268
+ return res;
269
+ }
270
+
271
+ /**
272
+ * Upload satu file (jenis apa pun) ke endpoint lampiran Session.
273
+ * `fetch` multipart mengelola boundary-nya sendiri, jadi content-type TIDAK
274
+ * di-set manual.
275
+ */
276
+ export async function apiUploadFile(
277
+ path: string,
278
+ file: File,
279
+ ): Promise<{ id: string; filename: string; mime: string; size: number }> {
280
+ const body = new FormData();
281
+ body.append("file", file);
282
+ const res = await fetch(path, { method: "POST", body });
283
+ if (!res.ok) {
284
+ let code = `HTTP_${res.status}`;
285
+ try {
286
+ const parsed = (await res.json()) as { error?: string };
287
+ if (typeof parsed.error === "string" && parsed.error !== "") code = parsed.error;
288
+ } catch {
289
+ // body bukan JSON — pakai kode HTTP default.
290
+ }
291
+ if (res.status === 401 && code === "AUTH_REQUIRED") onAuthRequired?.();
292
+ throw new ApiError(res.status, code, apiErrorMessage(code));
293
+ }
294
+ const parsed = (await res.json()) as {
295
+ upload?: { id: string; filename: string; mime: string; size: number };
296
+ };
297
+ if (typeof parsed.upload?.id !== "string") {
298
+ throw new ApiError(res.status, "INVALID_UPLOAD_RESPONSE", "Invalid upload response.");
299
+ }
300
+ return parsed.upload;
301
+ }
302
+
303
+ /** URL HTTP untuk memuat gambar lampiran di `<img>`. */
304
+ export function attachmentUrl(sessionId: string, attachmentId: string): string {
305
+ return `/api/uploads/${encodeURIComponent(sessionId)}/${encodeURIComponent(attachmentId)}`;
306
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Jenis lampiran upload — logika murni, dipakai server (routing ke model)
3
+ * maupun browser (thumbnail vs kartu file).
4
+ *
5
+ * Provider AI yang dibungkus opencode hanya menerima part `file` bermime
6
+ * `image/*` (format umum), `application/pdf`, dan `text/plain`. Mime lain
7
+ * (mis. `text/csv`, `application/json`) memicu error "functionality not
8
+ * supported" dan — karena part tersimpan di riwayat — merusak seluruh turn
9
+ * berikutnya. Karena itu:
10
+ * - gambar umum & PDF dikirim apa adanya,
11
+ * - file teks/kode dikirim ulang sebagai `text/plain` (opencode membacanya
12
+ * lewat tool Read, sama seperti `@file`),
13
+ * - file biner lain TIDAK dikirim sebagai part; model hanya diberi tahu
14
+ * path-nya agar bisa memakai tool bila perlu.
15
+ */
16
+
17
+ /** Batas ukuran upload (byte) — mengikuti limit attachment opencode (20 MiB). */
18
+ export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
19
+
20
+ /** Gambar yang bisa dipratinjau browser & diterima provider sebagai gambar. */
21
+ const PREVIEWABLE_IMAGE_MIME = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
22
+
23
+ /** Ekstensi file teks/kode yang aman dibaca sebagai teks. */
24
+ const TEXT_EXTENSIONS = new Set([
25
+ "txt",
26
+ "md",
27
+ "markdown",
28
+ "mdx",
29
+ "rst",
30
+ "log",
31
+ "csv",
32
+ "tsv",
33
+ "json",
34
+ "jsonc",
35
+ "json5",
36
+ "ndjson",
37
+ "yaml",
38
+ "yml",
39
+ "toml",
40
+ "ini",
41
+ "cfg",
42
+ "conf",
43
+ "env",
44
+ "xml",
45
+ "html",
46
+ "htm",
47
+ "css",
48
+ "scss",
49
+ "sass",
50
+ "less",
51
+ "svg",
52
+ "js",
53
+ "mjs",
54
+ "cjs",
55
+ "jsx",
56
+ "ts",
57
+ "mts",
58
+ "cts",
59
+ "tsx",
60
+ "vue",
61
+ "svelte",
62
+ "astro",
63
+ "py",
64
+ "rb",
65
+ "php",
66
+ "java",
67
+ "kt",
68
+ "kts",
69
+ "scala",
70
+ "go",
71
+ "rs",
72
+ "c",
73
+ "h",
74
+ "cc",
75
+ "cpp",
76
+ "hpp",
77
+ "cs",
78
+ "swift",
79
+ "m",
80
+ "dart",
81
+ "lua",
82
+ "pl",
83
+ "r",
84
+ "jl",
85
+ "ex",
86
+ "exs",
87
+ "erl",
88
+ "hs",
89
+ "clj",
90
+ "sql",
91
+ "graphql",
92
+ "gql",
93
+ "proto",
94
+ "sh",
95
+ "bash",
96
+ "zsh",
97
+ "fish",
98
+ "ps1",
99
+ "bat",
100
+ "dockerfile",
101
+ "makefile",
102
+ "gradle",
103
+ "tf",
104
+ "hcl",
105
+ "nix",
106
+ "lock",
107
+ "diff",
108
+ "patch",
109
+ "tex",
110
+ "bib",
111
+ "srt",
112
+ "vtt",
113
+ ]);
114
+
115
+ /** Nama file tanpa ekstensi yang umum berisi teks. */
116
+ const TEXT_BASENAMES = new Set(["dockerfile", "makefile", "readme", "license", "gemfile"]);
117
+
118
+ export type AttachmentKind = "image" | "pdf" | "text" | "binary";
119
+
120
+ /** Ekstensi huruf kecil (tanpa titik), "" bila tidak ada. */
121
+ export function fileExtension(filename: string): string {
122
+ const base = filename.split(/[\\/]/).pop() ?? "";
123
+ const dot = base.lastIndexOf(".");
124
+ return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
125
+ }
126
+
127
+ function isTextMime(mime: string): boolean {
128
+ return (
129
+ mime.startsWith("text/") ||
130
+ mime === "application/json" ||
131
+ mime === "application/xml" ||
132
+ mime === "application/javascript" ||
133
+ mime === "application/x-sh" ||
134
+ mime === "application/sql" ||
135
+ mime === "application/yaml" ||
136
+ mime === "application/toml" ||
137
+ mime.endsWith("+json") ||
138
+ mime.endsWith("+xml")
139
+ );
140
+ }
141
+
142
+ /** Gambar yang boleh ditampilkan inline di browser (thumbnail / bubble). */
143
+ export function isPreviewableImage(mime: string): boolean {
144
+ return PREVIEWABLE_IMAGE_MIME.has(mime);
145
+ }
146
+
147
+ /**
148
+ * Golongkan lampiran. Ekstensi dicek lebih dulu untuk file kode karena
149
+ * browser sering salah menebak mime-nya (mis. `.ts` jadi `video/mp2t`,
150
+ * `.rs` kosong).
151
+ */
152
+ export function attachmentKind(mime: string, filename: string): AttachmentKind {
153
+ const m = mime.toLowerCase();
154
+ if (isPreviewableImage(m)) return "image";
155
+ if (m === "application/pdf") return "pdf";
156
+ const ext = fileExtension(filename);
157
+ const base = (filename.split(/[\\/]/).pop() ?? "").toLowerCase();
158
+ if (TEXT_EXTENSIONS.has(ext) || (ext === "" && TEXT_BASENAMES.has(base))) return "text";
159
+ if (isTextMime(m)) return "text";
160
+ return "binary";
161
+ }
162
+
163
+ /**
164
+ * Mime part `file` yang dikirim ke model, atau null bila file tidak boleh
165
+ * dikirim sebagai part (biner) — cukup dirujuk lewat path.
166
+ */
167
+ export function modelFacingMime(mime: string, filename: string): string | null {
168
+ switch (attachmentKind(mime, filename)) {
169
+ case "image":
170
+ return mime.toLowerCase();
171
+ case "pdf":
172
+ return "application/pdf";
173
+ case "text":
174
+ return "text/plain";
175
+ default:
176
+ return null;
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Mime yang disimpan server: hanya bentuk `type/subtype` sederhana yang
182
+ * diterima (header Content-Type tidak boleh bisa disuntik); lainnya jadi
183
+ * `application/octet-stream`.
184
+ */
185
+ export function normalizeMime(mime: string): string {
186
+ const m = mime.trim().toLowerCase();
187
+ return /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,127}$/.test(m)
188
+ ? m
189
+ : "application/octet-stream";
190
+ }
191
+
192
+ /** Ukuran file ramah baca (mis. "1.2 MB"). */
193
+ export function formatBytes(bytes: number): string {
194
+ if (bytes < 1024) return `${bytes} B`;
195
+ const units = ["KB", "MB", "GB"];
196
+ let v = bytes / 1024;
197
+ let i = 0;
198
+ while (v >= 1024 && i < units.length - 1) {
199
+ v /= 1024;
200
+ i++;
201
+ }
202
+ return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${units[i]}`;
203
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Store global kunci aplikasi (FE): status dari server + pemicu lock screen.
3
+ *
4
+ * - `refreshAuth()` memuat `GET /api/auth/status` (publik).
5
+ * - 401 `AUTH_REQUIRED` dari API mana pun -> `markLocked()` (lock screen
6
+ * muncul tanpa reload; data di belakangnya tidak lagi bisa dimuat).
7
+ * - `lockNow()` = tombol Lock: hapus sesi di server lalu tampilkan lock screen.
8
+ * - Kunci otomatis ditegakkan SERVER (sesi idle kedaluwarsa); klien hanya
9
+ * mengetahuinya lebih cepat lewat timer idle lokal + heartbeat.
10
+ */
11
+ import { useSyncExternalStore } from "react";
12
+ import { apiFetch, setAuthRequiredHandler } from "@/lib/api";
13
+ import type { AuthStatus } from "@/server/services/auth";
14
+
15
+ export type AuthState =
16
+ | { phase: "loading" }
17
+ /** Server tak terjangkau saat memuat status pertama kali. */
18
+ | { phase: "error" }
19
+ | { phase: "ready"; status: AuthStatus };
20
+
21
+ let state: AuthState = { phase: "loading" };
22
+ const listeners = new Set<() => void>();
23
+
24
+ function set(next: AuthState): void {
25
+ state = next;
26
+ for (const l of listeners) l();
27
+ }
28
+
29
+ export async function refreshAuth(): Promise<AuthStatus | null> {
30
+ try {
31
+ const res = await fetch("/api/auth/status", { cache: "no-store" });
32
+ if (!res.ok) throw new Error(String(res.status));
33
+ const status = (await res.json()) as AuthStatus;
34
+ // Status sama persis -> jangan picu render ulang (heartbeat berkala).
35
+ if (state.phase !== "ready" || JSON.stringify(state.status) !== JSON.stringify(status)) {
36
+ set({ phase: "ready", status });
37
+ }
38
+ return status;
39
+ } catch {
40
+ if (state.phase === "loading") set({ phase: "error" });
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** Server menolak sesi (401) -> kunci UI segera, lalu sinkronkan status. */
46
+ export function markLocked(): void {
47
+ if (state.phase === "ready" && state.status.protected && state.status.authenticated) {
48
+ set({ phase: "ready", status: { ...state.status, authenticated: false } });
49
+ }
50
+ void refreshAuth();
51
+ }
52
+
53
+ /** Tombol Lock: akhiri sesi perangkat ini. */
54
+ export async function lockNow(): Promise<void> {
55
+ try {
56
+ await apiFetch("/api/auth/logout", { method: "POST" });
57
+ } catch {
58
+ // Sesi mungkin sudah tidak berlaku — tetap tampilkan lock screen.
59
+ }
60
+ markLocked();
61
+ }
62
+
63
+ setAuthRequiredHandler(markLocked);
64
+
65
+ function subscribe(l: () => void): () => void {
66
+ listeners.add(l);
67
+ return () => listeners.delete(l);
68
+ }
69
+
70
+ function snapshot(): AuthState {
71
+ return state;
72
+ }
73
+
74
+ export function useAuth(): AuthState {
75
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
76
+ }
77
+
78
+ /** Label jenis kunci untuk UI. */
79
+ export function lockLabel(kind: "pin" | "password"): string {
80
+ return kind === "pin" ? "PIN" : "password";
81
+ }
82
+
83
+ /** Inisial nickname untuk avatar pengganti ("Irsyad Ibad" -> "II"). */
84
+ export function initialsOf(nickname: string | null): string {
85
+ if (!nickname) return "";
86
+ const words = nickname.trim().split(/\s+/).filter(Boolean);
87
+ const letters = words.length > 1 ? [words[0], words[words.length - 1]] : [words[0]];
88
+ return letters
89
+ .map((w) => [...(w ?? "")][0] ?? "")
90
+ .join("")
91
+ .toUpperCase();
92
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Katalog avatar bawaan (preset). Dibagi server & FE:
3
+ * - Server hanya memvalidasi id (`isAvatarPresetId`) — tidak mengimpor ikon.
4
+ * - FE memetakan id -> ikon lucide + warna di `components/auth/avatar-icons.tsx`.
5
+ *
6
+ * Menambah preset: tambahkan id di sini DAN pemetaannya di FE (dijaga test).
7
+ */
8
+ export const AVATAR_PRESET_IDS = [
9
+ "cat",
10
+ "dog",
11
+ "rabbit",
12
+ "panda",
13
+ "bird",
14
+ "fish",
15
+ "turtle",
16
+ "squirrel",
17
+ "rocket",
18
+ "bot",
19
+ "ghost",
20
+ "gamepad",
21
+ "flower",
22
+ "leaf",
23
+ "mountain",
24
+ "coffee",
25
+ ] as const;
26
+
27
+ export type AvatarPresetId = (typeof AVATAR_PRESET_IDS)[number];
28
+
29
+ export function isAvatarPresetId(v: unknown): v is AvatarPresetId {
30
+ return typeof v === "string" && (AVATAR_PRESET_IDS as readonly string[]).includes(v);
31
+ }