kcg-code 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/README.md +230 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +84 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/daemon.test.ts +84 -0
  13. package/src/cli/__tests__/dashboard.test.ts +224 -0
  14. package/src/cli/__tests__/keep-awake.test.ts +44 -0
  15. package/src/cli/__tests__/port.test.ts +86 -0
  16. package/src/cli/args.ts +186 -0
  17. package/src/cli/attach.ts +154 -0
  18. package/src/cli/commands.ts +327 -0
  19. package/src/cli/daemon.ts +275 -0
  20. package/src/cli/dashboard.tsx +1060 -0
  21. package/src/cli/index.ts +47 -0
  22. package/src/cli/keep-awake.ts +148 -0
  23. package/src/cli/port.ts +92 -0
  24. package/src/cli/theme.ts +100 -0
  25. package/src/components/auth/AuthGate.tsx +182 -0
  26. package/src/components/auth/AvatarEditor.tsx +236 -0
  27. package/src/components/auth/LockScreen.tsx +714 -0
  28. package/src/components/auth/ProfileAvatar.tsx +60 -0
  29. package/src/components/auth/WelcomeScreen.tsx +672 -0
  30. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  31. package/src/components/auth/avatar-icons.tsx +114 -0
  32. package/src/components/layout/AppShell.tsx +147 -0
  33. package/src/components/layout/AppSidebar.tsx +419 -0
  34. package/src/components/layout/SearchDialog.tsx +189 -0
  35. package/src/components/layout/SidebarAccount.tsx +111 -0
  36. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  37. package/src/components/layout/ThemeToggle.tsx +67 -0
  38. package/src/components/projects/FolderBrowser.tsx +142 -0
  39. package/src/components/projects/HomeChat.tsx +287 -0
  40. package/src/components/projects/HomeRecentSessions.tsx +90 -0
  41. package/src/components/projects/NewProjectDialog.tsx +249 -0
  42. package/src/components/projects/ProjectInstructions.tsx +218 -0
  43. package/src/components/projects/ProjectRow.tsx +58 -0
  44. package/src/components/projects/ProjectSidePanel.tsx +514 -0
  45. package/src/components/sessions/AgentLogo.tsx +42 -0
  46. package/src/components/sessions/AgentModeList.tsx +265 -0
  47. package/src/components/sessions/AgentPicker.tsx +132 -0
  48. package/src/components/sessions/AssistantTurn.tsx +145 -0
  49. package/src/components/sessions/AttachmentTile.tsx +161 -0
  50. package/src/components/sessions/CodeBlock.tsx +107 -0
  51. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  52. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  53. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +92 -0
  54. package/src/components/sessions/ContextPanel.tsx +661 -0
  55. package/src/components/sessions/DropPlate.tsx +165 -0
  56. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  57. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  58. package/src/components/sessions/ImageLightbox.tsx +173 -0
  59. package/src/components/sessions/MarkdownContent.tsx +187 -0
  60. package/src/components/sessions/Mascot.tsx +47 -0
  61. package/src/components/sessions/MessageActions.tsx +60 -0
  62. package/src/components/sessions/MessageRail.tsx +121 -0
  63. package/src/components/sessions/ModelPicker.tsx +247 -0
  64. package/src/components/sessions/ModelSearchList.tsx +231 -0
  65. package/src/components/sessions/NewSessionDialog.tsx +172 -0
  66. package/src/components/sessions/PendingAttachments.tsx +127 -0
  67. package/src/components/sessions/PromptCard.tsx +1085 -0
  68. package/src/components/sessions/PromptPanel.tsx +112 -0
  69. package/src/components/sessions/SessionComposer.tsx +641 -0
  70. package/src/components/sessions/SessionHeader.tsx +176 -0
  71. package/src/components/sessions/SessionList.tsx +503 -0
  72. package/src/components/sessions/SessionRow.tsx +237 -0
  73. package/src/components/sessions/SessionTimeline.tsx +275 -0
  74. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  75. package/src/components/sessions/SessionView.tsx +240 -0
  76. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  77. package/src/components/sessions/UserMessage.tsx +177 -0
  78. package/src/components/sessions/__tests__/ContextPanel.test.tsx +256 -0
  79. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  80. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  81. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  82. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  83. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  84. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  85. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  86. package/src/components/settings/AboutGroup.tsx +170 -0
  87. package/src/components/settings/AccountGroup.tsx +122 -0
  88. package/src/components/settings/DevicesGroup.tsx +151 -0
  89. package/src/components/settings/LhrRemoteAccess.tsx +257 -0
  90. package/src/components/settings/PasskeysGroup.tsx +551 -0
  91. package/src/components/settings/RemoteAccessGroup.tsx +1201 -0
  92. package/src/components/settings/SecurityGroup.tsx +566 -0
  93. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  94. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  95. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  96. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  97. package/src/components/settings/prefs.tsx +333 -0
  98. package/src/components/skills/InstallNotifications.tsx +326 -0
  99. package/src/components/skills/InstallSkillDialog.tsx +390 -0
  100. package/src/components/skills/SkillCards.tsx +254 -0
  101. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  102. package/src/components/ui/adw-dialog.tsx +114 -0
  103. package/src/components/ui/adw-toggle-group.tsx +75 -0
  104. package/src/components/ui/alert-dialog.tsx +179 -0
  105. package/src/components/ui/alert.tsx +60 -0
  106. package/src/components/ui/badge.tsx +46 -0
  107. package/src/components/ui/bubble.tsx +125 -0
  108. package/src/components/ui/button.tsx +62 -0
  109. package/src/components/ui/card.tsx +75 -0
  110. package/src/components/ui/collapsible.tsx +19 -0
  111. package/src/components/ui/dialog.tsx +142 -0
  112. package/src/components/ui/dropdown-menu.tsx +225 -0
  113. package/src/components/ui/empty.tsx +94 -0
  114. package/src/components/ui/field.tsx +234 -0
  115. package/src/components/ui/input-group.tsx +161 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +19 -0
  118. package/src/components/ui/message-scroller.tsx +131 -0
  119. package/src/components/ui/message.tsx +85 -0
  120. package/src/components/ui/popover.tsx +46 -0
  121. package/src/components/ui/search-entry.tsx +70 -0
  122. package/src/components/ui/select.tsx +172 -0
  123. package/src/components/ui/separator.tsx +28 -0
  124. package/src/components/ui/sheet.tsx +138 -0
  125. package/src/components/ui/sidebar.tsx +699 -0
  126. package/src/components/ui/skeleton.tsx +15 -0
  127. package/src/components/ui/sonner.tsx +45 -0
  128. package/src/components/ui/spinner.tsx +16 -0
  129. package/src/components/ui/switch.tsx +32 -0
  130. package/src/components/ui/textarea.tsx +18 -0
  131. package/src/components/ui/tooltip.tsx +51 -0
  132. package/src/config.ts +114 -0
  133. package/src/db.ts +146 -0
  134. package/src/frontend.tsx +51 -0
  135. package/src/hooks/__tests__/useMention.test.ts +79 -0
  136. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  137. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  138. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  139. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  140. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  141. package/src/hooks/use-mobile.ts +16 -0
  142. package/src/hooks/useAgentPicker.ts +101 -0
  143. package/src/hooks/useMediaQuery.ts +49 -0
  144. package/src/hooks/useMention.ts +236 -0
  145. package/src/hooks/useProjects.ts +88 -0
  146. package/src/hooks/useResyncOnReconnect.ts +28 -0
  147. package/src/hooks/useRouter.ts +92 -0
  148. package/src/hooks/useSessionChat.ts +427 -0
  149. package/src/hooks/useSessionContext.ts +106 -0
  150. package/src/hooks/useSessionList.ts +327 -0
  151. package/src/hooks/useSidebarData.ts +108 -0
  152. package/src/hooks/useSmoothText.ts +122 -0
  153. package/src/hooks/useTheme.ts +164 -0
  154. package/src/hooks/useWebSocket.ts +196 -0
  155. package/src/index.css +27 -0
  156. package/src/index.html +14 -0
  157. package/src/index.ts +49 -0
  158. package/src/lib/__tests__/attachments.test.ts +58 -0
  159. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  160. package/src/lib/__tests__/collapsible.test.ts +101 -0
  161. package/src/lib/__tests__/composer.test.ts +118 -0
  162. package/src/lib/__tests__/highlight.test.ts +42 -0
  163. package/src/lib/__tests__/home-chat.test.ts +64 -0
  164. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  165. package/src/lib/__tests__/model-picker.test.ts +205 -0
  166. package/src/lib/__tests__/onboarding.test.ts +41 -0
  167. package/src/lib/__tests__/permission-info.test.ts +61 -0
  168. package/src/lib/__tests__/project-extensions.test.ts +83 -0
  169. package/src/lib/__tests__/project-overview.test.ts +188 -0
  170. package/src/lib/__tests__/references.test.ts +121 -0
  171. package/src/lib/__tests__/routes.test.ts +134 -0
  172. package/src/lib/__tests__/session-selection.test.ts +41 -0
  173. package/src/lib/__tests__/session-summary.test.ts +115 -0
  174. package/src/lib/__tests__/session-title.test.ts +58 -0
  175. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  176. package/src/lib/__tests__/skill-installs.test.ts +135 -0
  177. package/src/lib/__tests__/skills-catalog.test.ts +124 -0
  178. package/src/lib/__tests__/tone.test.ts +37 -0
  179. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  180. package/src/lib/agent-types.ts +49 -0
  181. package/src/lib/api.ts +306 -0
  182. package/src/lib/attachments.ts +203 -0
  183. package/src/lib/auth.ts +92 -0
  184. package/src/lib/avatar-presets.ts +31 -0
  185. package/src/lib/collapsible.ts +75 -0
  186. package/src/lib/composer.ts +89 -0
  187. package/src/lib/data-events.ts +17 -0
  188. package/src/lib/headlines.ts +30 -0
  189. package/src/lib/highlight.ts +186 -0
  190. package/src/lib/layout.ts +20 -0
  191. package/src/lib/lock-screen.ts +54 -0
  192. package/src/lib/model-picker.ts +133 -0
  193. package/src/lib/onboarding.ts +38 -0
  194. package/src/lib/passkeys.ts +113 -0
  195. package/src/lib/pending-prompt.ts +53 -0
  196. package/src/lib/permission-info.ts +101 -0
  197. package/src/lib/project-extensions.ts +136 -0
  198. package/src/lib/project-overview.ts +123 -0
  199. package/src/lib/prompts.ts +87 -0
  200. package/src/lib/references.ts +106 -0
  201. package/src/lib/routes.ts +124 -0
  202. package/src/lib/session-handoff.ts +25 -0
  203. package/src/lib/session-selection.ts +40 -0
  204. package/src/lib/session-status.ts +45 -0
  205. package/src/lib/session-summary.ts +68 -0
  206. package/src/lib/session-title.ts +63 -0
  207. package/src/lib/sidebar-groups.ts +78 -0
  208. package/src/lib/skill-installs.ts +362 -0
  209. package/src/lib/skills-catalog.ts +138 -0
  210. package/src/lib/time.ts +22 -0
  211. package/src/lib/tone.ts +80 -0
  212. package/src/lib/turns.ts +320 -0
  213. package/src/lib/utils.ts +6 -0
  214. package/src/logo.svg +31 -0
  215. package/src/pages/projects/ProjectDetailPage.tsx +131 -0
  216. package/src/pages/projects/ProjectsPage.tsx +97 -0
  217. package/src/pages/sessions/SessionPage.tsx +122 -0
  218. package/src/pages/settings/SettingsPage.tsx +366 -0
  219. package/src/pages/settings/__tests__/SettingsPage.test.ts +16 -0
  220. package/src/pages/skills/SkillsPage.tsx +745 -0
  221. package/src/paths.ts +121 -0
  222. package/src/runtime.ts +24 -0
  223. package/src/server/__tests__/app.test.ts +1044 -0
  224. package/src/server/__tests__/auth.test.ts +370 -0
  225. package/src/server/__tests__/config.test.ts +101 -0
  226. package/src/server/__tests__/db.test.ts +485 -0
  227. package/src/server/__tests__/host.test.ts +14 -0
  228. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  229. package/src/server/app.ts +427 -0
  230. package/src/server/db/auth.ts +311 -0
  231. package/src/server/db/messages.ts +50 -0
  232. package/src/server/db/projects.ts +106 -0
  233. package/src/server/db/prompts.ts +78 -0
  234. package/src/server/db/rows.ts +150 -0
  235. package/src/server/db/schema.ts +221 -0
  236. package/src/server/db/sessions.ts +239 -0
  237. package/src/server/db/tunnel.ts +124 -0
  238. package/src/server/host.ts +12 -0
  239. package/src/server/result.ts +15 -0
  240. package/src/server/routes/auth-guard.ts +156 -0
  241. package/src/server/routes/auth.routes.ts +287 -0
  242. package/src/server/routes/helpers.ts +165 -0
  243. package/src/server/routes/meta.routes.ts +24 -0
  244. package/src/server/routes/passkeys.routes.ts +161 -0
  245. package/src/server/routes/projects.routes.ts +167 -0
  246. package/src/server/routes/sessions.routes.ts +197 -0
  247. package/src/server/routes/skills.routes.ts +136 -0
  248. package/src/server/routes/tunnel.routes.ts +132 -0
  249. package/src/server/routes/types.ts +35 -0
  250. package/src/server/routes/uploads.routes.ts +54 -0
  251. package/src/server/routes/workspace.routes.ts +127 -0
  252. package/src/server/services/__tests__/app-info.test.ts +18 -0
  253. package/src/server/services/__tests__/attachments.test.ts +159 -0
  254. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  255. package/src/server/services/__tests__/auth.test.ts +341 -0
  256. package/src/server/services/__tests__/claude-code.test.ts +609 -0
  257. package/src/server/services/__tests__/claude-mcp.test.ts +90 -0
  258. package/src/server/services/__tests__/claude-skills.test.ts +63 -0
  259. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  260. package/src/server/services/__tests__/git-info.test.ts +74 -0
  261. package/src/server/services/__tests__/opencode.test.ts +320 -0
  262. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  263. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  264. package/src/server/services/__tests__/session-manager.test.ts +3509 -0
  265. package/src/server/services/__tests__/skill-install-jobs.test.ts +245 -0
  266. package/src/server/services/__tests__/skills-registry.test.ts +365 -0
  267. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  268. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  269. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  270. package/src/server/services/__tests__/workspace.test.ts +249 -0
  271. package/src/server/services/app-info.ts +29 -0
  272. package/src/server/services/attachments.ts +193 -0
  273. package/src/server/services/auth-passkeys.ts +459 -0
  274. package/src/server/services/auth.ts +633 -0
  275. package/src/server/services/claude-code-client.ts +1093 -0
  276. package/src/server/services/claude-code-translate.ts +370 -0
  277. package/src/server/services/claude-mcp.ts +122 -0
  278. package/src/server/services/claude-skills.ts +102 -0
  279. package/src/server/services/frpc-installer.ts +183 -0
  280. package/src/server/services/git-info.ts +87 -0
  281. package/src/server/services/opencode-client.ts +714 -0
  282. package/src/server/services/opencode-server.ts +319 -0
  283. package/src/server/services/opencode-sse.ts +82 -0
  284. package/src/server/services/project-manager.ts +147 -0
  285. package/src/server/services/sandbox.ts +83 -0
  286. package/src/server/services/session-events.ts +256 -0
  287. package/src/server/services/session-manager.ts +1439 -0
  288. package/src/server/services/skill-install-jobs.ts +275 -0
  289. package/src/server/services/skills-registry.ts +480 -0
  290. package/src/server/services/tunnel-api.ts +207 -0
  291. package/src/server/services/tunnel-config.ts +45 -0
  292. package/src/server/services/tunnel-lhr.ts +387 -0
  293. package/src/server/services/tunnel.ts +633 -0
  294. package/src/server/services/turn-stream.ts +339 -0
  295. package/src/server/services/websocket-gateway.ts +374 -0
  296. package/src/server/services/workspace.ts +223 -0
  297. package/src/server/services/ws-transport.ts +121 -0
  298. package/src/types/context.ts +69 -0
  299. package/src/types/index.ts +13 -0
  300. package/src/types/message.ts +58 -0
  301. package/src/types/project.ts +16 -0
  302. package/src/types/prompt.ts +118 -0
  303. package/src/types/session.ts +52 -0
  304. package/src/types/turns.ts +47 -0
  305. package/src/ws-protocol.ts +93 -0
  306. package/styles/globals.css +311 -0
  307. package/tsconfig.json +37 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Transformasi pesan percakapan terstruktur → model tampilan timeline.
3
+ *
4
+ * Logika murni (tanpa React/DOM) yang sebelumnya tinggal di `SessionView.tsx`,
5
+ * dipisah agar dapat diuji `bun test` tanpa render. Tipe hasil (`TurnSegment`,
6
+ * `TurnBlock`, `MessageGroup`, dst.) ada di `src/types/turns.ts`.
7
+ */
8
+ import type {
9
+ AssistantTurnGroup,
10
+ MessageGroup,
11
+ MessagePart,
12
+ SessionMessage,
13
+ TurnBlock,
14
+ TurnSegment,
15
+ } from "@/types";
16
+
17
+ /** Teks part apa pun yang membawa field `text` (atau null). */
18
+ export function partText(p: MessagePart): string | null {
19
+ return typeof p.text === "string" ? p.text : null;
20
+ }
21
+
22
+ /**
23
+ * Gabungkan teks jawaban dari part `text` saja — reasoning/tool/step tidak
24
+ * ikut, walau part tersebut juga membawa field `text` dari opencode.
25
+ */
26
+ export function textOf(parts: MessagePart[]): string {
27
+ return parts
28
+ .filter((p) => p.type === "text")
29
+ .map(partText)
30
+ .filter((t): t is string => t !== null)
31
+ .join("\n");
32
+ }
33
+
34
+ /**
35
+ * Ratakan seluruh parts satu turn (bisa beberapa pesan assistant) menjadi
36
+ * daftar segmen BERURUTAN: setiap part reasoning jadi satu segmen Thinking
37
+ * tersendiri (bisa tampil lebih dari sekali, sesuai urutan berpikir model),
38
+ * tool jadi segmen badge, part `text` berurutan digabung, dan segmen teks
39
+ * non-kosong TERAKHIR ditandai `final` (jawaban akhir — satu-satunya yang
40
+ * dapat efek typewriter).
41
+ */
42
+ export function turnSegments(messages: readonly SessionMessage[]): TurnSegment[] {
43
+ const segments: TurnSegment[] = [];
44
+ for (const msg of messages) {
45
+ msg.parts.forEach((part, i) => {
46
+ const key = `${msg.id}:${part.id ?? i}`;
47
+ if (part.type === "reasoning" || part.type === "error") {
48
+ segments.push({ kind: part.type, key, part });
49
+ return;
50
+ }
51
+ if (part.type === "tool" || part.type === "shell" || part.type === "file") {
52
+ segments.push({ kind: "tool", key, part });
53
+ return;
54
+ }
55
+ if (part.type !== "text") return;
56
+ const text = partText(part) ?? "";
57
+ const last = segments[segments.length - 1];
58
+ // Part text berdempetan digabung agar tidak pecah jadi beberapa bubble.
59
+ if (last?.kind === "text") last.text = last.text === "" ? text : `${last.text}\n${text}`;
60
+ else segments.push({ kind: "text", key, text, final: false });
61
+ });
62
+ }
63
+ // Segmen teks non-kosong terakhir = jawaban akhir turn.
64
+ for (let i = segments.length - 1; i >= 0; i--) {
65
+ const seg = segments[i];
66
+ if (seg?.kind !== "text") continue;
67
+ if (seg.text.trim() !== "") seg.final = true;
68
+ break;
69
+ }
70
+ return segments;
71
+ }
72
+
73
+ /** Key kandidat `state.input` tool yang berisi target utama (path/argumen). */
74
+ const TOOL_TARGET_KEYS = [
75
+ "filePath",
76
+ "path",
77
+ "file",
78
+ "dir",
79
+ "directory",
80
+ "pattern",
81
+ "query",
82
+ "url",
83
+ "command",
84
+ "description",
85
+ ] as const;
86
+
87
+ /**
88
+ * Target utama sebuah tool call (mis. path file yang di-read) — diambil
89
+ * dari `state.input` part tool opencode dengan toleransi beberapa nama key.
90
+ * Part `file` (echo @file) memakai `filename`-nya langsung.
91
+ */
92
+ export function toolTarget(part: MessagePart): string | null {
93
+ if (part.type === "file" && typeof part.filename === "string" && part.filename.trim() !== "") {
94
+ return part.filename;
95
+ }
96
+ const state = part.state;
97
+ if (typeof state !== "object" || state === null) return null;
98
+ const input = (state as { input?: unknown }).input;
99
+ if (typeof input !== "object" || input === null) return null;
100
+ for (const key of TOOL_TARGET_KEYS) {
101
+ const v = (input as Record<string, unknown>)[key];
102
+ if (typeof v === "string" && v.trim() !== "") return v;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ /** Label badge tool: nama tool + target (mis. `read package.json`). */
108
+ export function toolLabel(part: MessagePart): string {
109
+ const name = typeof part.tool === "string" && part.tool.trim() !== "" ? part.tool : part.type;
110
+ const target = toolTarget(part);
111
+ return target === null ? name : `${name} ${target}`;
112
+ }
113
+
114
+ /**
115
+ * Kelompokkan pesan berurutan: assistant yang berdempetan (satu turn —
116
+ * sering dipecah opencode menjadi beberapa `msg_...` saat reasoning/tool/
117
+ * sub-agent) menjadi SATU grup sehingga tampil sebagai satu kesatuan;
118
+ * pesan user selalu grup tersendiri.
119
+ */
120
+ export function groupTurns(messages: readonly SessionMessage[]): MessageGroup[] {
121
+ const groups: MessageGroup[] = [];
122
+ for (const m of messages) {
123
+ const last = groups[groups.length - 1];
124
+ if (m.role === "assistant") {
125
+ if (last?.kind === "assistant") last.messages.push(m);
126
+ else groups.push({ kind: "assistant", id: m.id, messages: [m] });
127
+ } else {
128
+ groups.push({ kind: "user", message: m });
129
+ }
130
+ }
131
+ return groups;
132
+ }
133
+
134
+ /**
135
+ * Apakah satu turn sudah menampilkan sesuatu ke user: reasoning non-kosong,
136
+ * tool call, error, maupun teks. Dipakai untuk memutuskan kapan indikator
137
+ * "Memproses…" perlu tampil (respon masih kosong sama sekali).
138
+ */
139
+ export function hasVisibleContent(segments: readonly TurnSegment[]): boolean {
140
+ return segments.some(
141
+ (s) =>
142
+ s.kind === "tool" ||
143
+ s.kind === "error" ||
144
+ (s.kind === "reasoning" && (partText(s.part) ?? "").trim() !== "") ||
145
+ (s.kind === "text" && s.text.trim() !== ""),
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Status LIVE satu turn untuk maskot (murni, diuji):
151
+ * - `null` -> turn belum ada / selesai (maskot disembunyikan).
152
+ * - "Working…" -> belum ada konten apa pun (baru mulai).
153
+ * - "Thinking…" -> reasoning part terakhir masih berjalan.
154
+ * - "Writing…" -> model sedang menulis jawaban (segmen teks terakhir tampil).
155
+ * - lainnya -> label tool yang sedang dijalankan (mis. "read package.json").
156
+ *
157
+ * Catatan: akhir turn ditandai `streaming=false` (pesan final menggantikan
158
+ * versi streaming), jadi penilaian "selesai" cukup dari flag itu.
159
+ */
160
+ export function turnStatus(messages: readonly SessionMessage[], streaming: boolean): string | null {
161
+ if (!streaming || messages.length === 0) return null;
162
+ const segments = turnSegments(messages);
163
+ if (!hasVisibleContent(segments)) return "Working…";
164
+ for (let i = segments.length - 1; i >= 0; i--) {
165
+ const seg = segments[i];
166
+ if (!seg) continue;
167
+ if (seg.kind === "text") return "Writing…";
168
+ if (seg.kind === "reasoning") return "Thinking…";
169
+ if (seg.kind === "tool") return toolLabel(seg.part);
170
+ // error: proses berhenti di error -> selesai.
171
+ return null;
172
+ }
173
+ return "Working…";
174
+ }
175
+
176
+ /**
177
+ * Total durasi thinking (ms) dari satu turn — dipakai untuk label "thoughts Xs"
178
+ * di footer setelah turn selesai.
179
+ *
180
+ * Strategi (urut prioritas):
181
+ * 1. Pakai `time.start`/`time.end` dari `ReasoningPart` asli opencode jika ada.
182
+ * 2. Fallback ke `time.start` part pertama vs `time.end` part terakhir dari
183
+ * seluruh parts reasoning yang ada (estimasi kasar).
184
+ * 3. Jika tidak ada timing sama sekali, kembalikan null.
185
+ */
186
+ export function turnThoughtDuration(messages: readonly SessionMessage[]): number | null {
187
+ let earliest: number | null = null;
188
+ let latest: number | null = null;
189
+ for (const msg of messages) {
190
+ for (const part of msg.parts) {
191
+ if (part.type !== "reasoning") continue;
192
+ const t = part.time;
193
+ if (!t) continue;
194
+ const start = t.start ?? t.created;
195
+ if (typeof start === "number") {
196
+ if (earliest === null || start < earliest) earliest = start;
197
+ }
198
+ const end = t.end;
199
+ if (typeof end === "number") {
200
+ if (latest === null || end > latest) latest = end;
201
+ }
202
+ }
203
+ }
204
+ if (earliest !== null && latest !== null && latest > earliest) {
205
+ return latest - earliest;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ export function turnBlocks(segments: readonly TurnSegment[]): TurnBlock[] {
211
+ const blocks: TurnBlock[] = [];
212
+ let pendingSteps: Extract<TurnSegment, { kind: "reasoning" | "tool" }>[] = [];
213
+
214
+ for (const seg of segments) {
215
+ if (seg.kind === "reasoning" || seg.kind === "tool") {
216
+ pendingSteps.push(seg);
217
+ } else if (seg.kind === "text" || seg.kind === "error") {
218
+ blocks.push({
219
+ key: pendingSteps[0]?.key ?? seg.key,
220
+ steps: pendingSteps,
221
+ content: seg,
222
+ });
223
+ pendingSteps = [];
224
+ }
225
+ }
226
+ // Steps tersisa tanpa teks (masih streaming atau turn selesai tanpa teks akhir)
227
+ const firstPending = pendingSteps[0];
228
+ if (firstPending) {
229
+ blocks.push({ key: firstPending.key, steps: pendingSteps, content: null });
230
+ }
231
+ return blocks;
232
+ }
233
+
234
+ /**
235
+ * Upsert satu part ke dalam daftar pesan (streaming, event `message_part`).
236
+ * - Belum ada pesan dengan `messageId` -> buat placeholder assistant streaming.
237
+ * - Sudah ada -> part dengan `id` sama diganti, selainnya ditambahkan.
238
+ * Mengembalikan array baru (immutable) agar mudah diverifikasi.
239
+ */
240
+ export function upsertMessagePart(
241
+ messages: SessionMessage[],
242
+ sessionId: string,
243
+ messageId: string,
244
+ part: MessagePart,
245
+ ): SessionMessage[] {
246
+ const idx = messages.findIndex((m) => m.id === messageId);
247
+ if (idx === -1) {
248
+ return [
249
+ ...messages,
250
+ {
251
+ id: messageId,
252
+ sessionId,
253
+ role: "assistant",
254
+ parts: [part],
255
+ createdAt: Date.now(),
256
+ streaming: true,
257
+ },
258
+ ];
259
+ }
260
+ const cur = messages[idx];
261
+ if (!cur) return messages;
262
+ const partIdx = cur.parts.findIndex((p) => p.id !== undefined && p.id === part.id);
263
+ const nextParts =
264
+ partIdx === -1 ? [...cur.parts, part] : cur.parts.map((p, i) => (i === partIdx ? part : p));
265
+ const next: SessionMessage = { ...cur, parts: nextParts, streaming: true };
266
+ return messages.map((m, i) => (i === idx ? next : m));
267
+ }
268
+
269
+ /**
270
+ * Tambahkan potongan teks streaming (`message_part_delta`) ke part yang sudah
271
+ * ada: `part[field] += delta`. Delta untuk pesan/part yang belum dikenal
272
+ * diabaikan (daftar tidak berubah) — `message_part` final membawa teks utuh.
273
+ * Mengembalikan array yang sama bila tidak ada perubahan (hemat re-render).
274
+ */
275
+ export function appendMessagePartDelta(
276
+ messages: SessionMessage[],
277
+ messageId: string,
278
+ partId: string,
279
+ field: string,
280
+ delta: string,
281
+ ): SessionMessage[] {
282
+ if (delta === "") return messages;
283
+ const idx = messages.findIndex((m) => m.id === messageId);
284
+ const cur = idx === -1 ? undefined : messages[idx];
285
+ if (!cur) return messages;
286
+ const partIdx = cur.parts.findIndex((p) => p.id === partId);
287
+ const part = partIdx === -1 ? undefined : cur.parts[partIdx];
288
+ if (!part) return messages;
289
+ const prev = (part as Record<string, unknown>)[field];
290
+ if (prev !== undefined && typeof prev !== "string") return messages;
291
+ const nextPart: MessagePart = { ...part, [field]: (prev ?? "") + delta };
292
+ const next: SessionMessage = {
293
+ ...cur,
294
+ parts: cur.parts.map((p, i) => (i === partIdx ? nextPart : p)),
295
+ streaming: true,
296
+ };
297
+ return messages.map((m, i) => (i === idx ? next : m));
298
+ }
299
+
300
+ /**
301
+ * Ganti pesan final (hasil POST, non-streaming) — replace bila id sama
302
+ * (menutup versi streaming), selainnya tambahkan.
303
+ */
304
+ export function upsertMessage(
305
+ messages: SessionMessage[],
306
+ message: SessionMessage,
307
+ ): SessionMessage[] {
308
+ const idx = messages.findIndex((m) => m.id === message.id);
309
+ if (idx === -1) return [...messages, message];
310
+ return messages.map((m, i) => (i === idx ? message : m));
311
+ }
312
+
313
+ /** Grup assistant terakhir dalam daftar grup (dipakai status maskot global). */
314
+ export function lastAssistantGroup(groups: readonly MessageGroup[]): AssistantTurnGroup | null {
315
+ for (let i = groups.length - 1; i >= 0; i--) {
316
+ const g = groups[i];
317
+ if (g?.kind === "assistant") return g;
318
+ }
319
+ return null;
320
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
package/src/logo.svg ADDED
@@ -0,0 +1,31 @@
1
+ <svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="KCG Code logo">
2
+ <title>KCG Code</title>
3
+ <defs>
4
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
5
+ <stop offset="0" stop-color="#6D5CFB"/>
6
+ <stop offset="1" stop-color="#4C3FD9"/>
7
+ </linearGradient>
8
+ </defs>
9
+
10
+ <path d="
11
+ M 14 74
12
+ L 14 44
13
+ C 14 36 16 29 20 23
14
+ L 31 9
15
+ L 39 24
16
+ C 42.5 22.8 46.2 22 50 22
17
+ C 53.8 22 57.5 22.8 61 24
18
+ L 69 9
19
+ L 80 23
20
+ C 84 29 86 36 86 44
21
+ L 86 62
22
+ C 86 76 76 86 62 86
23
+ L 38 86
24
+ C 27 86 19 81 16 74
25
+ " stroke="url(#g)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
26
+
27
+ <circle cx="14" cy="74" r="7" fill="url(#g)"/>
28
+ <circle cx="34" cy="52" r="6" fill="url(#g)"/>
29
+ <circle cx="66" cy="52" r="6" fill="url(#g)"/>
30
+ <circle cx="50" cy="86" r="7" fill="url(#g)"/>
31
+ </svg>
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Halaman daftar Session milik satu Project (`/projects/:projectId`).
3
+ *
4
+ * - `Project` di-resolve dari id di URL via `GET /api/projects` (Requirement
5
+ * 10.8) sehingga halaman dapat di-refresh / diakses langsung (direct link)
6
+ * tanpa state dari halaman sebelumnya.
7
+ * - Project tidak ditemukan (sudah dihapus / id salah) -> pesan error dengan
8
+ * aksi kembali ke daftar Project.
9
+ * - Komposisi identik dengan `SessionList`: form pembuatan + daftar.
10
+ */
11
+
12
+ import { useCallback, useEffect, useState } from "react";
13
+ import { SessionList } from "@/components/sessions/SessionList";
14
+ import { SessionListSkeleton } from "@/components/sessions/SessionRow";
15
+ import { Skeleton } from "@/components/ui/skeleton";
16
+ import { useRouter } from "@/hooks/useRouter";
17
+ import { ApiError, apiErrorMessage, apiFetch } from "@/lib/api";
18
+ import { projectsPath, sessionPath } from "@/lib/routes";
19
+ import type { Project } from "@/types";
20
+
21
+ export interface ProjectDetailPageProps {
22
+ projectId: string;
23
+ }
24
+
25
+ type LoadState =
26
+ | { phase: "loading" }
27
+ | { phase: "error"; code: string }
28
+ | { phase: "ready"; project: Project };
29
+
30
+ export function ProjectDetailPage({ projectId }: ProjectDetailPageProps) {
31
+ const { navigate } = useRouter();
32
+ const [state, setState] = useState<LoadState>({ phase: "loading" });
33
+
34
+ const load = useCallback(async () => {
35
+ setState({ phase: "loading" });
36
+ try {
37
+ const res = await apiFetch("/api/projects");
38
+ const body = (await res.json()) as { projects: Project[] };
39
+ const project = body.projects.find((p) => p.id === projectId);
40
+ if (project === undefined) {
41
+ setState({ phase: "error", code: "PROJECT_NOT_FOUND" });
42
+ } else {
43
+ setState({ phase: "ready", project });
44
+ }
45
+ } catch (e) {
46
+ setState({
47
+ phase: "error",
48
+ code: e instanceof ApiError ? e.code : "INTERNAL_ERROR",
49
+ });
50
+ }
51
+ }, [projectId]);
52
+
53
+ useEffect(() => {
54
+ void load();
55
+ }, [load]);
56
+
57
+ if (state.phase === "loading") {
58
+ return (
59
+ <div
60
+ className="grid grid-cols-1 gap-x-10 gap-y-8 pt-4 sm:pt-8 lg:grid-cols-[minmax(0,1fr)_20rem] xl:grid-cols-[minmax(0,1fr)_22rem]"
61
+ aria-hidden
62
+ >
63
+ {/* Bentuk sama dengan SessionList: kolom Session + panel samping. */}
64
+ <div className="flex flex-col gap-8">
65
+ <div className="flex flex-col gap-4">
66
+ <div className="flex items-start justify-between gap-4">
67
+ <div className="flex min-w-0 flex-1 flex-col gap-2">
68
+ <Skeleton className="h-8 w-48" />
69
+ <Skeleton className="h-3 w-64" />
70
+ </div>
71
+ <Skeleton className="h-8 w-28 rounded-full" />
72
+ </div>
73
+ <Skeleton className="h-4 w-36" />
74
+ </div>
75
+ <SessionListSkeleton />
76
+ </div>
77
+ {/* Sama dengan ProjectSidePanel: grup (judul + deskripsi) berisi
78
+ boxed list. */}
79
+ <div className="hidden flex-col gap-6 self-start lg:flex">
80
+ {[1, 2, 3].map((rows, i) => (
81
+ // biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
82
+ <div key={i} className="flex flex-col gap-2.5">
83
+ <div className="flex flex-col gap-1.5 px-1">
84
+ <Skeleton className="h-4 w-28" />
85
+ <Skeleton className="h-3 w-40" />
86
+ </div>
87
+ <div className="flex flex-col divide-y overflow-hidden rounded-xl border bg-card">
88
+ {Array.from({ length: rows }, (_, j) => (
89
+ // biome-ignore lint/suspicious/noArrayIndexKey: placeholder statis
90
+ <div key={j} className="flex min-h-14 flex-col justify-center gap-1.5 px-4">
91
+ <Skeleton className="h-4" style={{ width: `${60 - j * 12}%` }} />
92
+ <Skeleton className="h-3 w-1/3" />
93
+ </div>
94
+ ))}
95
+ </div>
96
+ </div>
97
+ ))}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ if (state.phase === "error") {
104
+ return (
105
+ <div className="flex flex-col items-center gap-4 py-10 text-center">
106
+ <p className="text-sm text-muted-foreground">{apiErrorMessage(state.code)}</p>
107
+ <a
108
+ href={projectsPath()}
109
+ onClick={(e) => {
110
+ e.preventDefault();
111
+ navigate(projectsPath());
112
+ }}
113
+ className="text-sm font-medium underline underline-offset-4"
114
+ >
115
+ Back to projects
116
+ </a>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <SessionList
123
+ key={state.project.id}
124
+ project={state.project}
125
+ onOpenSession={(session) => navigate(sessionPath(projectId, session.id))}
126
+ // Project sudah tidak ada — halaman ini tak punya data lagi untuk
127
+ // ditampilkan, jadi langsung kembali ke daftar Project.
128
+ onDeleted={() => navigate(projectsPath())}
129
+ />
130
+ );
131
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Homepage (`/`).
3
+ *
4
+ * - Sudah ada Project -> chat (`HomeChat`): headline acak + composer dengan
5
+ * pemilih Project di kiri bawah. Kirim = Session baru di Project terpilih.
6
+ * Daftar Project & Session-nya ada di sidebar (`AppSidebar`).
7
+ * - Belum ada Project -> empty state dengan satu CTA "New project".
8
+ *
9
+ * Data dimuat `hooks/useProjects.ts`; ringkasan per Project (urutan aktivitas
10
+ * terbaru) dihitung `lib/project-overview.ts`.
11
+ */
12
+
13
+ import { FolderPlusIcon, FoldersIcon, RefreshCwIcon } from "lucide-react";
14
+ import { HomeChat } from "@/components/projects/HomeChat";
15
+ import { NewProjectDialog } from "@/components/projects/NewProjectDialog";
16
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Empty,
20
+ EmptyContent,
21
+ EmptyDescription,
22
+ EmptyHeader,
23
+ EmptyMedia,
24
+ EmptyTitle,
25
+ } from "@/components/ui/empty";
26
+ import { Skeleton } from "@/components/ui/skeleton";
27
+ import { useProjects } from "@/hooks/useProjects";
28
+ import { notifyDataChanged } from "@/lib/data-events";
29
+
30
+ export function ProjectsPage() {
31
+ const { overviews, loading, loadError, refresh, dialogOpen, setDialogOpen } = useProjects();
32
+
33
+ const onCreated = () => {
34
+ void refresh();
35
+ // Sidebar memuat datanya sendiri — beri tahu agar Project baru muncul.
36
+ notifyDataChanged();
37
+ };
38
+
39
+ return (
40
+ <div className="mx-auto flex w-full max-w-2xl flex-1 flex-col">
41
+ <NewProjectDialog open={dialogOpen} onOpenChange={setDialogOpen} onCreated={onCreated} />
42
+
43
+ {loading && overviews.length === 0 ? (
44
+ <HomeSkeleton />
45
+ ) : loadError ? (
46
+ <div className="flex flex-1 flex-col justify-center">
47
+ <Alert variant="destructive">
48
+ <AlertTitle>Failed to load projects</AlertTitle>
49
+ <AlertDescription className="flex flex-col items-start gap-3">
50
+ {loadError}
51
+ <Button type="button" variant="outline" size="sm" onClick={() => void refresh()}>
52
+ <RefreshCwIcon data-icon="inline-start" />
53
+ Retry
54
+ </Button>
55
+ </AlertDescription>
56
+ </Alert>
57
+ </div>
58
+ ) : overviews.length === 0 ? (
59
+ <div className="flex flex-1 flex-col justify-center">
60
+ <Empty className="rounded-xl border border-dashed bg-card">
61
+ <EmptyHeader>
62
+ <EmptyMedia variant="icon">
63
+ <FoldersIcon />
64
+ </EmptyMedia>
65
+ <EmptyTitle>No projects yet</EmptyTitle>
66
+ <EmptyDescription>
67
+ Add a project first — pick a working directory in the Sandbox, then run CLI_Agent
68
+ inside it.
69
+ </EmptyDescription>
70
+ <EmptyContent>
71
+ <Button type="button" onClick={() => setDialogOpen(true)}>
72
+ <FolderPlusIcon data-icon="inline-start" />
73
+ New project
74
+ </Button>
75
+ </EmptyContent>
76
+ </EmptyHeader>
77
+ </Empty>
78
+ </div>
79
+ ) : (
80
+ <HomeChat overviews={overviews} onNewProject={() => setDialogOpen(true)} />
81
+ )}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ /** Placeholder chat saat memuat — bentuk serupa agar tidak melompat. */
87
+ function HomeSkeleton() {
88
+ return (
89
+ <div className="flex flex-1 flex-col justify-center gap-6 pb-[10dvh]" aria-hidden>
90
+ <div className="flex items-center justify-center gap-3">
91
+ <Skeleton className="size-12 rounded-full" />
92
+ <Skeleton className="h-8 w-64" />
93
+ </div>
94
+ <Skeleton className="h-32 w-full rounded-2xl" />
95
+ </div>
96
+ );
97
+ }