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,186 @@
1
+ /**
2
+ * Syntax highlighting blok kode — Shiki v4 (`shiki/core`, fine-grained).
3
+ *
4
+ * - Engine JavaScript RegExp (tanpa WASM Oniguruma): lebih ringan untuk
5
+ * browser; semua bahasa bawaan Shiki didukung sejak 3.9.
6
+ * - Bahasa & tema di-import dinamis (`shiki/langs/*.mjs`,
7
+ * `shiki/themes/*.mjs` — subpath resmi paket `shiki`) per kebutuhan: bundle awal tidak
8
+ * membawa grammar apa pun; grammar dimuat saat blok kode bahasa itu
9
+ * pertama kali tampil, lalu di-cache (highlighter singleton).
10
+ * - Dual theme (github-light / github-dark) sebagai CSS variable per token
11
+ * (`--shiki-light` / `--shiki-dark`); warna dipilih lewat kelas `.dark`
12
+ * aplikasi di `styles/globals.css`, jadi ganti tema tidak perlu
13
+ * highlight ulang.
14
+ * - Hasilnya berupa TOKEN (bukan string HTML) yang dirender sebagai elemen
15
+ * React — tidak ada `dangerouslySetInnerHTML`, isi kode tetap teks.
16
+ */
17
+ import type { HighlighterCore, TokenStyles } from "shiki/core";
18
+
19
+ /** Loader grammar per nama bahasa kanonis Shiki. */
20
+ const LANG_LOADERS = {
21
+ javascript: () => import("shiki/langs/javascript.mjs"),
22
+ typescript: () => import("shiki/langs/typescript.mjs"),
23
+ jsx: () => import("shiki/langs/jsx.mjs"),
24
+ tsx: () => import("shiki/langs/tsx.mjs"),
25
+ json: () => import("shiki/langs/json.mjs"),
26
+ jsonc: () => import("shiki/langs/jsonc.mjs"),
27
+ shellscript: () => import("shiki/langs/shellscript.mjs"),
28
+ powershell: () => import("shiki/langs/powershell.mjs"),
29
+ python: () => import("shiki/langs/python.mjs"),
30
+ go: () => import("shiki/langs/go.mjs"),
31
+ rust: () => import("shiki/langs/rust.mjs"),
32
+ java: () => import("shiki/langs/java.mjs"),
33
+ kotlin: () => import("shiki/langs/kotlin.mjs"),
34
+ c: () => import("shiki/langs/c.mjs"),
35
+ cpp: () => import("shiki/langs/cpp.mjs"),
36
+ csharp: () => import("shiki/langs/csharp.mjs"),
37
+ php: () => import("shiki/langs/php.mjs"),
38
+ ruby: () => import("shiki/langs/ruby.mjs"),
39
+ swift: () => import("shiki/langs/swift.mjs"),
40
+ dart: () => import("shiki/langs/dart.mjs"),
41
+ lua: () => import("shiki/langs/lua.mjs"),
42
+ sql: () => import("shiki/langs/sql.mjs"),
43
+ graphql: () => import("shiki/langs/graphql.mjs"),
44
+ yaml: () => import("shiki/langs/yaml.mjs"),
45
+ toml: () => import("shiki/langs/toml.mjs"),
46
+ ini: () => import("shiki/langs/ini.mjs"),
47
+ xml: () => import("shiki/langs/xml.mjs"),
48
+ html: () => import("shiki/langs/html.mjs"),
49
+ css: () => import("shiki/langs/css.mjs"),
50
+ scss: () => import("shiki/langs/scss.mjs"),
51
+ vue: () => import("shiki/langs/vue.mjs"),
52
+ svelte: () => import("shiki/langs/svelte.mjs"),
53
+ markdown: () => import("shiki/langs/markdown.mjs"),
54
+ diff: () => import("shiki/langs/diff.mjs"),
55
+ docker: () => import("shiki/langs/docker.mjs"),
56
+ make: () => import("shiki/langs/make.mjs"),
57
+ } as const;
58
+
59
+ export type HighlightLang = keyof typeof LANG_LOADERS;
60
+
61
+ /** Alias umum di fence Markdown -> nama kanonis. */
62
+ const ALIASES: Record<string, HighlightLang> = {
63
+ js: "javascript",
64
+ mjs: "javascript",
65
+ cjs: "javascript",
66
+ ts: "typescript",
67
+ mts: "typescript",
68
+ cts: "typescript",
69
+ sh: "shellscript",
70
+ bash: "shellscript",
71
+ zsh: "shellscript",
72
+ shell: "shellscript",
73
+ console: "shellscript",
74
+ ps1: "powershell",
75
+ ps: "powershell",
76
+ py: "python",
77
+ golang: "go",
78
+ rs: "rust",
79
+ kt: "kotlin",
80
+ "c++": "cpp",
81
+ cs: "csharp",
82
+ "c#": "csharp",
83
+ rb: "ruby",
84
+ gql: "graphql",
85
+ yml: "yaml",
86
+ htm: "html",
87
+ md: "markdown",
88
+ patch: "diff",
89
+ dockerfile: "docker",
90
+ makefile: "make",
91
+ svg: "xml",
92
+ };
93
+
94
+ /**
95
+ * Nama bahasa kanonis dari info-string fence (`ts`, `language-tsx`,
96
+ * `JSON`), atau null bila tidak didukung (tetap tampil sebagai teks polos).
97
+ */
98
+ export function resolveLang(raw: string | null | undefined): HighlightLang | null {
99
+ if (!raw) return null;
100
+ const name = raw
101
+ .trim()
102
+ .toLowerCase()
103
+ .replace(/^language-/, "");
104
+ if (name in LANG_LOADERS) return name as HighlightLang;
105
+ return ALIASES[name] ?? null;
106
+ }
107
+
108
+ let highlighter: Promise<HighlighterCore> | null = null;
109
+ const loading = new Map<HighlightLang, Promise<void>>();
110
+
111
+ function getHighlighter(): Promise<HighlighterCore> {
112
+ highlighter ??= (async () => {
113
+ const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([
114
+ import("shiki/core"),
115
+ import("shiki/engine/javascript"),
116
+ ]);
117
+ return createHighlighterCore({
118
+ themes: [import("shiki/themes/github-light.mjs"), import("shiki/themes/github-dark.mjs")],
119
+ langs: [],
120
+ // `forgiving`: pola grammar yang tak bisa dikonversi tidak melempar
121
+ // error — kode tetap tampil (sebagian mungkin tak berwarna).
122
+ engine: createJavaScriptRegexEngine({ forgiving: true }),
123
+ });
124
+ })();
125
+ return highlighter;
126
+ }
127
+
128
+ async function ensureLang(h: HighlighterCore, lang: HighlightLang): Promise<void> {
129
+ if (h.getLoadedLanguages().includes(lang)) return;
130
+ let p = loading.get(lang);
131
+ if (!p) {
132
+ p = h.loadLanguage(LANG_LOADERS[lang]()).catch((e) => {
133
+ loading.delete(lang); // izinkan coba lagi bila import gagal (mis. offline)
134
+ throw e;
135
+ });
136
+ loading.set(lang, p);
137
+ }
138
+ await p;
139
+ }
140
+
141
+ /** Satu token hasil highlight: teks + warna light/dark (CSS variable). */
142
+ export interface CodeToken {
143
+ content: string;
144
+ /** `--shiki-light` / `--shiki-dark` + gaya font (italic/bold). */
145
+ style: Record<string, string>;
146
+ }
147
+
148
+ /**
149
+ * Highlight kode jadi baris token. Gagal (bahasa tak dikenal / grammar
150
+ * gagal dimuat) -> null; pemanggil menampilkan teks polos.
151
+ */
152
+ export async function highlightCode(
153
+ code: string,
154
+ lang: HighlightLang,
155
+ ): Promise<CodeToken[][] | null> {
156
+ try {
157
+ const h = await getHighlighter();
158
+ await ensureLang(h, lang);
159
+ const lines = h.codeToTokensWithThemes(code, {
160
+ lang,
161
+ themes: { light: "github-light", dark: "github-dark" },
162
+ });
163
+ return lines.map((line) =>
164
+ line.map((t) => ({ content: t.content, style: tokenStyle(t.variants) })),
165
+ );
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ /** Gaya satu token dari varian tema (hanya warna & font style). */
172
+ function tokenStyle(variants: Record<string, TokenStyles>) {
173
+ const style: Record<string, string> = {};
174
+ const light = variants.light;
175
+ const dark = variants.dark;
176
+ if (light?.color) style["--shiki-light"] = light.color;
177
+ if (dark?.color) style["--shiki-dark"] = dark.color;
178
+ // FontStyle bitmask Shiki: 1 = italic, 2 = bold, 4 = underline.
179
+ const fs = light?.fontStyle ?? 0;
180
+ if (fs > 0) {
181
+ if (fs & 1) style.fontStyle = "italic";
182
+ if (fs & 2) style.fontWeight = "600";
183
+ if (fs & 4) style.textDecoration = "underline";
184
+ }
185
+ return style;
186
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Lebar kolom percakapan — SATU sumber untuk timeline, composer, dan kartu
3
+ * prompt, supaya tepi kiri-kanannya selalu sejajar.
4
+ *
5
+ * Kenapa bukan `max-w-3xl` (768px) di tiap tempat: timeline punya padding
6
+ * horizontal DI DALAM kolomnya (teks mulai 16px dari tepi), sedangkan
7
+ * composer & kartu prompt tidak — jadi dengan max-w yang sama, kotak input
8
+ * tampak 32px lebih lebar dari teks chat. `CONVERSATION_MAX_W` adalah lebar
9
+ * TEKS (736px = 768 - 2×16); timeline menambahkan padding-nya di luar nilai
10
+ * ini (`TIMELINE_MAX_W`).
11
+ */
12
+
13
+ /** Lebar isi kolom (teks chat, kotak input, kartu prompt). */
14
+ export const CONVERSATION_MAX_W = "max-w-[46rem]";
15
+
16
+ /**
17
+ * Lebar kotak timeline = isi + padding horizontalnya (`sm:px-4` = 2×1rem),
18
+ * sehingga TEKS timeline tepat selebar `CONVERSATION_MAX_W`.
19
+ */
20
+ export const TIMELINE_MAX_W = "max-w-[48rem]";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Logika murni lock screen (tanpa DOM) — dipisah agar dapat diuji `bun test`.
3
+ */
4
+
5
+ export const PIN_MAX = 12;
6
+
7
+ export type PinKeyAction =
8
+ | { type: "digit"; digit: string }
9
+ | { type: "backspace" }
10
+ | { type: "submit" }
11
+ | { type: "clear" };
12
+
13
+ /**
14
+ * Terjemahkan tombol keyboard fisik menjadi aksi PIN. Angka baris atas
15
+ * menghasilkan `key` "0"-"9". Numpad dibaca dari `code` (`Numpad0`-`Numpad9`)
16
+ * agar tetap jalan walau NumLock mati (saat itu `key` berisi panah/Home/End).
17
+ * Kombinasi dengan Ctrl/Cmd/Alt diabaikan agar pintasan browser tetap jalan.
18
+ */
19
+ export function pinKeyAction(e: {
20
+ key: string;
21
+ code?: string;
22
+ ctrlKey?: boolean;
23
+ metaKey?: boolean;
24
+ altKey?: boolean;
25
+ }): PinKeyAction | null {
26
+ if (e.ctrlKey || e.metaKey || e.altKey) return null;
27
+ if (/^[0-9]$/.test(e.key)) return { type: "digit", digit: e.key };
28
+ const numpad = /^Numpad([0-9])$/.exec(e.code ?? "");
29
+ if (numpad?.[1]) return { type: "digit", digit: numpad[1] };
30
+ if (e.key === "Backspace" || e.key === "Delete") return { type: "backspace" };
31
+ // `NumpadEnter` juga ber-key "Enter".
32
+ if (e.key === "Enter") return { type: "submit" };
33
+ if (e.key === "Escape") return { type: "clear" };
34
+ return null;
35
+ }
36
+
37
+ /** Terapkan aksi ke PIN saat ini (batas panjang dijaga). */
38
+ export function applyPinAction(pin: string, action: PinKeyAction): string {
39
+ switch (action.type) {
40
+ case "digit":
41
+ return pin.length >= PIN_MAX ? pin : pin + action.digit;
42
+ case "backspace":
43
+ return pin.slice(0, -1);
44
+ case "clear":
45
+ return "";
46
+ case "submit":
47
+ return pin;
48
+ }
49
+ }
50
+
51
+ /** Ambil angka dari teks tempelan (mis. "48 29 15" -> "482915"). */
52
+ export function digitsFromPaste(text: string, current: string): string {
53
+ return (current + text.replace(/\D/g, "")).slice(0, PIN_MAX);
54
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Pemilihan model LLM — logika murni (tanpa DOM / fetch), dipisah agar dapat
3
+ * diuji `bun test` seperti `lib/routes.ts` dan `lib/session-summary.ts`.
4
+ *
5
+ * Dipakai bersama oleh `components/sessions/ModelPicker.tsx` (ganti model
6
+ * Session yang sedang berjalan) dan `components/sessions/SessionList.tsx`
7
+ * (pilih model saat membuat Session) supaya keduanya memakai aturan kunci,
8
+ * pencarian, dan pengelompokan yang sama.
9
+ */
10
+
11
+ import type { ModelOption } from "@/server/services/opencode-client";
12
+ import type { SessionModel } from "@/types";
13
+
14
+ /** Nilai khusus "pakai model default opencode" (mengirim `model: null`). */
15
+ export const DEFAULT_MODEL_KEY = "default";
16
+
17
+ /** Pemisah dalam kunci gabungan; modelID boleh memuat "/" sehingga bukan "/". */
18
+ const KEY_SEPARATOR = "\u0000";
19
+
20
+ /** Kunci gabungan provider+model untuk dipakai sebagai nilai/`key` React. */
21
+ export function modelKey(providerID: string, modelID: string): string {
22
+ return `${providerID}${KEY_SEPARATOR}${modelID}`;
23
+ }
24
+
25
+ /** Kunci untuk sebuah `ModelOption`. */
26
+ export function optionKey(m: ModelOption): string {
27
+ return modelKey(m.providerID, m.modelID);
28
+ }
29
+
30
+ /** Kunci untuk model Session saat ini; `null` -> `DEFAULT_MODEL_KEY`. */
31
+ export function keyOfSessionModel(model: SessionModel | null): string {
32
+ return model ? modelKey(model.providerID, model.modelID) : DEFAULT_MODEL_KEY;
33
+ }
34
+
35
+ /**
36
+ * Kunci -> `SessionModel`. `DEFAULT_MODEL_KEY` dan kunci tak lengkap
37
+ * menghasilkan `null` (biarkan opencode memakai model default-nya).
38
+ */
39
+ export function parseModelKey(key: string): SessionModel | null {
40
+ if (key === DEFAULT_MODEL_KEY) return null;
41
+ const [providerID, modelID] = key.split(KEY_SEPARATOR);
42
+ if (!providerID || !modelID) return null;
43
+ return { providerID, modelID };
44
+ }
45
+
46
+ /** Satu provider beserta model-modelnya, untuk render berkelompok. */
47
+ export interface ModelGroup {
48
+ providerID: string;
49
+ providerName: string;
50
+ models: ModelOption[];
51
+ }
52
+
53
+ /**
54
+ * Kelompokkan model per provider, mempertahankan urutan kemunculan pertama
55
+ * tiap provider (urutan dari server dianggap sudah bermakna).
56
+ */
57
+ export function groupModels(models: readonly ModelOption[]): ModelGroup[] {
58
+ const byProvider = new Map<string, ModelGroup>();
59
+ for (const m of models) {
60
+ let g = byProvider.get(m.providerID);
61
+ if (!g) {
62
+ g = { providerID: m.providerID, providerName: m.providerName, models: [] };
63
+ byProvider.set(m.providerID, g);
64
+ }
65
+ g.models.push(m);
66
+ }
67
+ return [...byProvider.values()];
68
+ }
69
+
70
+ /**
71
+ * Cari model berdasarkan kata kunci: cocok pada nama model, modelID, atau
72
+ * nama provider (tanpa membedakan huruf besar/kecil). Seluruh kata dalam
73
+ * query harus cocok, sehingga "opus 5" menemukan "Claude Opus 5" walau
74
+ * urutan katanya berbeda dari teks aslinya.
75
+ */
76
+ export function filterModels(models: readonly ModelOption[], query: string): ModelOption[] {
77
+ const terms = query
78
+ .toLowerCase()
79
+ .split(/\s+/)
80
+ .filter((t) => t.length > 0);
81
+ if (terms.length === 0) return [...models];
82
+ return models.filter((m) => {
83
+ const haystack = `${m.name} ${m.modelID} ${m.providerName}`.toLowerCase();
84
+ return terms.every((t) => haystack.includes(t));
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Label ringkas model aktif untuk trigger picker. Model belum termuat dari
90
+ * server (daftar kosong) tetap tampil sebagai `modelID` — informasi yang
91
+ * sudah dimiliki Session, jadi trigger tidak pernah kosong.
92
+ */
93
+ export function activeModelLabel(
94
+ model: SessionModel | null,
95
+ models: readonly ModelOption[],
96
+ ): string {
97
+ if (model === null) return "Default model";
98
+ const found = models.find(
99
+ (m) => m.providerID === model.providerID && m.modelID === model.modelID,
100
+ );
101
+ return found?.name ?? model.modelID;
102
+ }
103
+
104
+ /** Batas panjang nama model di header (karakter) sebelum dipotong. */
105
+ export const MODEL_NAME_MAX_LENGTH = 22;
106
+
107
+ /**
108
+ * Nama model sependek mungkin untuk judul header.
109
+ *
110
+ * Provider sering memberi prefiks vendor pada `modelID`
111
+ * (`kiro/claude-opus-5`, `anthropic/claude-sonnet-4`), dan prefiks itu tidak
112
+ * menambah informasi saat nama sudah tampil di bawah judul Session. Segmen
113
+ * terakhir setelah `/` diambil, lalu dipotong bila masih terlalu panjang untuk
114
+ * lebar layar HP.
115
+ */
116
+ export function shortModelName(label: string, maxLength: number = MODEL_NAME_MAX_LENGTH): string {
117
+ // Segmen kosong dibuang lebih dulu, sehingga trailing slash diabaikan
118
+ // (`a/b/` -> `b`) dan label yang hanya berisi slash menghasilkan "".
119
+ const segments = label.split("/").filter((s) => s.trim().length > 0);
120
+ const name = segments.at(-1)?.trim() ?? "";
121
+ if (maxLength <= 0) return "";
122
+ if (name.length <= maxLength) return name;
123
+ // Sisakan satu karakter untuk elipsis agar total tepat `maxLength`.
124
+ return `${name.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
125
+ }
126
+
127
+ /**
128
+ * Ringkasan jumlah hasil pencarian untuk pembaca layar / footer dialog.
129
+ */
130
+ export function describeModelCount(count: number): string {
131
+ if (count === 0) return "No matching models";
132
+ return count === 1 ? "1 model" : `${count} models`;
133
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Alur welcome screen (onboarding) — logika murni, diuji tanpa DOM.
3
+ *
4
+ * Langkah: welcome -> profile -> lock -> done.
5
+ * - `profile` (nickname) boleh dikosongkan.
6
+ * - `lock` boleh dilewati (app tetap terbuka; Settings menampilkan
7
+ * peringatan seperti biasa).
8
+ */
9
+ export const ONBOARDING_STEPS = ["welcome", "profile", "lock", "done"] as const;
10
+ export type OnboardingStep = (typeof ONBOARDING_STEPS)[number];
11
+
12
+ export function stepIndex(step: OnboardingStep): number {
13
+ return ONBOARDING_STEPS.indexOf(step);
14
+ }
15
+
16
+ export function nextStep(step: OnboardingStep): OnboardingStep {
17
+ return ONBOARDING_STEPS[Math.min(stepIndex(step) + 1, ONBOARDING_STEPS.length - 1)] ?? "done";
18
+ }
19
+
20
+ export function prevStep(step: OnboardingStep): OnboardingStep {
21
+ return ONBOARDING_STEPS[Math.max(stepIndex(step) - 1, 0)] ?? "welcome";
22
+ }
23
+
24
+ /** Arah animasi geser saat berpindah langkah (1 maju, -1 mundur). */
25
+ export function stepDirection(from: OnboardingStep, to: OnboardingStep): 1 | -1 {
26
+ return stepIndex(to) >= stepIndex(from) ? 1 : -1;
27
+ }
28
+
29
+ /** Langkah yang ditampilkan sebagai titik progres (tanpa layar sambutan/akhir). */
30
+ export const PROGRESS_STEPS: readonly OnboardingStep[] = ["profile", "lock"];
31
+
32
+ /** Sapaan sesuai jam lokal. */
33
+ export function greeting(hour: number): string {
34
+ if (hour < 5) return "Good evening";
35
+ if (hour < 12) return "Good morning";
36
+ if (hour < 18) return "Good afternoon";
37
+ return "Good evening";
38
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Klien passkey (WebAuthn) — membungkus `@simplewebauthn/browser` + API
3
+ * `/api/auth/passkeys/*` agar Settings & lock screen memakai alur yang sama.
4
+ *
5
+ * Pembatalan oleh pengguna (menutup dialog sistem) bukan error: dikembalikan
6
+ * sebagai `{ ok: false, cancelled: true }` supaya UI tidak menampilkan pesan
7
+ * merah untuk tindakan yang disengaja.
8
+ */
9
+ import {
10
+ browserSupportsWebAuthn,
11
+ startAuthentication,
12
+ startRegistration,
13
+ } from "@simplewebauthn/browser";
14
+ import { ApiError, apiFetch } from "@/lib/api";
15
+ import type { PasskeyAvailability, PasskeyInfo } from "@/server/services/auth-passkeys";
16
+
17
+ export type { PasskeyAvailability, PasskeyInfo };
18
+
19
+ export type PasskeyResult<T> =
20
+ | { ok: true; data: T }
21
+ | { ok: false; cancelled: true }
22
+ | { ok: false; cancelled: false; message: string; code?: string };
23
+
24
+ /** Browser ini bisa memakai passkey (API WebAuthn + secure context). */
25
+ export function passkeysSupportedHere(): boolean {
26
+ if (typeof window === "undefined") return false;
27
+ return window.isSecureContext && browserSupportsWebAuthn();
28
+ }
29
+
30
+ /**
31
+ * Error dari `navigator.credentials` -> pesan ramah. `NotAllowedError` =
32
+ * dibatalkan / timeout oleh pengguna (diperlakukan sebagai batal).
33
+ */
34
+ export function describeWebAuthnError(e: unknown): PasskeyResult<never> {
35
+ if (e instanceof ApiError)
36
+ return { ok: false, cancelled: false, message: e.message, code: e.code };
37
+ const name = e instanceof Error ? e.name : "";
38
+ if (name === "NotAllowedError" || name === "AbortError") return { ok: false, cancelled: true };
39
+ if (name === "InvalidStateError") {
40
+ return { ok: false, cancelled: false, message: "This device already has a passkey here." };
41
+ }
42
+ if (name === "SecurityError") {
43
+ return {
44
+ ok: false,
45
+ cancelled: false,
46
+ message: "Passkeys aren't allowed on this address. Use https or localhost.",
47
+ };
48
+ }
49
+ return { ok: false, cancelled: false, message: "Something went wrong with the passkey." };
50
+ }
51
+
52
+ export async function getPasskeyAvailability(): Promise<PasskeyAvailability> {
53
+ const res = await apiFetch("/api/auth/passkeys/availability");
54
+ return (await res.json()) as PasskeyAvailability;
55
+ }
56
+
57
+ export async function listPasskeys(): Promise<PasskeyInfo[]> {
58
+ const res = await apiFetch("/api/auth/passkeys");
59
+ return ((await res.json()) as { passkeys: PasskeyInfo[] }).passkeys;
60
+ }
61
+
62
+ /** Daftarkan passkey baru; `current` = PIN/password saat ini (konfirmasi). */
63
+ export async function addPasskey(current: string): Promise<PasskeyResult<PasskeyInfo>> {
64
+ try {
65
+ const optRes = await apiFetch("/api/auth/passkeys/register/options", {
66
+ method: "POST",
67
+ body: JSON.stringify({ current }),
68
+ });
69
+ const { challengeId, options } = (await optRes.json()) as {
70
+ challengeId: string;
71
+ options: Parameters<typeof startRegistration>[0]["optionsJSON"];
72
+ };
73
+ const response = await startRegistration({ optionsJSON: options });
74
+ const res = await apiFetch("/api/auth/passkeys/register", {
75
+ method: "POST",
76
+ body: JSON.stringify({ challengeId, response }),
77
+ });
78
+ return { ok: true, data: ((await res.json()) as { passkey: PasskeyInfo }).passkey };
79
+ } catch (e) {
80
+ return describeWebAuthnError(e);
81
+ }
82
+ }
83
+
84
+ /** Buka kunci dengan passkey. Sukses = cookie sesi sudah diset server. */
85
+ export async function unlockWithPasskey(): Promise<PasskeyResult<null>> {
86
+ try {
87
+ const optRes = await apiFetch("/api/auth/passkeys/login/options", { method: "POST" });
88
+ const { challengeId, options } = (await optRes.json()) as {
89
+ challengeId: string;
90
+ options: Parameters<typeof startAuthentication>[0]["optionsJSON"];
91
+ };
92
+ const response = await startAuthentication({ optionsJSON: options });
93
+ await apiFetch("/api/auth/passkeys/login", {
94
+ method: "POST",
95
+ body: JSON.stringify({ challengeId, response }),
96
+ });
97
+ return { ok: true, data: null };
98
+ } catch (e) {
99
+ return describeWebAuthnError(e);
100
+ }
101
+ }
102
+
103
+ export async function renamePasskey(id: string, name: string): Promise<PasskeyInfo> {
104
+ const res = await apiFetch(`/api/auth/passkeys/${encodeURIComponent(id)}`, {
105
+ method: "PATCH",
106
+ body: JSON.stringify({ name }),
107
+ });
108
+ return ((await res.json()) as { passkey: PasskeyInfo }).passkey;
109
+ }
110
+
111
+ export async function removePasskey(id: string): Promise<void> {
112
+ await apiFetch(`/api/auth/passkeys/${encodeURIComponent(id)}`, { method: "DELETE" });
113
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Prompt pertama yang diketik di homepage, dititipkan sampai Session
3
+ * barunya ter-attach di Session view (`useSessionChat`) lalu dikirim sekali.
4
+ *
5
+ * Disimpan di `sessionStorage` (per tab) sehingga tetap aman walau halaman
6
+ * Session dimuat ulang sebelum koneksi WS siap; `take` menghapusnya agar
7
+ * prompt tidak terkirim dua kali.
8
+ */
9
+
10
+ const KEY_PREFIX = "kcg-pending-prompt:";
11
+
12
+ /** Storage minimal — diinjeksi saat test (default `sessionStorage`). */
13
+ export interface PendingStore {
14
+ getItem(key: string): string | null;
15
+ setItem(key: string, value: string): void;
16
+ removeItem(key: string): void;
17
+ }
18
+
19
+ function defaultStore(): PendingStore | null {
20
+ return typeof sessionStorage === "undefined" ? null : sessionStorage;
21
+ }
22
+
23
+ export function setPendingPrompt(
24
+ sessionId: string,
25
+ text: string,
26
+ store: PendingStore | null = defaultStore(),
27
+ ): void {
28
+ const trimmed = text.trim();
29
+ if (store === null || trimmed === "") return;
30
+ store.setItem(KEY_PREFIX + sessionId, trimmed);
31
+ }
32
+
33
+ /** Baca prompt tertunda tanpa menghapusnya (untuk pesan optimistis di UI). */
34
+ export function peekPendingPrompt(
35
+ sessionId: string,
36
+ store: PendingStore | null = defaultStore(),
37
+ ): string | null {
38
+ if (store === null) return null;
39
+ const value = store.getItem(KEY_PREFIX + sessionId);
40
+ return value !== null && value.trim() !== "" ? value : null;
41
+ }
42
+
43
+ /** Ambil sekaligus hapus prompt tertunda; null bila tidak ada. */
44
+ export function takePendingPrompt(
45
+ sessionId: string,
46
+ store: PendingStore | null = defaultStore(),
47
+ ): string | null {
48
+ if (store === null) return null;
49
+ const key = KEY_PREFIX + sessionId;
50
+ const value = store.getItem(key);
51
+ if (value !== null) store.removeItem(key);
52
+ return value !== null && value.trim() !== "" ? value : null;
53
+ }