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,427 @@
1
+ /**
2
+ * Engine percakapan Session (headless) — state & efek yang dulu tinggal di
3
+ * `SessionView.tsx`, dipisah menjadi custom hook agar halaman tinggal
4
+ * merender.
5
+ *
6
+ * Konsumen:
7
+ * - `SessionView` — menyusun header/timeline/prompt/composer dari hasil hook.
8
+ * - Koneksi WS (`useWebSocket`) dilampirkan ke `session.id`; `history`,
9
+ * `message`, `prompt`, `session_status`, dst. diterjemahkan ke state di
10
+ * sini (daftar pesan, kartu prompt, status Session, error banner).
11
+ */
12
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
13
+ import { toast } from "sonner";
14
+ import { useWebSocket, type WsConnectionStatus } from "@/hooks/useWebSocket";
15
+ import { ApiError, apiFetch } from "@/lib/api";
16
+ import { type CollapsibleState, extendCollapsed, toggleCollapsible } from "@/lib/collapsible";
17
+ import { notifyDataChanged } from "@/lib/data-events";
18
+ import { peekPendingPrompt, setPendingPrompt, takePendingPrompt } from "@/lib/pending-prompt";
19
+ import { groupPrompts } from "@/lib/prompts";
20
+ import { wsStatusLabel } from "@/lib/session-status";
21
+ import {
22
+ appendMessagePartDelta,
23
+ groupTurns,
24
+ turnBlocks,
25
+ turnSegments,
26
+ upsertMessage,
27
+ upsertMessagePart,
28
+ } from "@/lib/turns";
29
+ import type {
30
+ InteractivePrompt,
31
+ PromptResponse,
32
+ Session,
33
+ SessionMessage,
34
+ SessionStatus,
35
+ } from "@/types";
36
+ import type { ClientMessage, ServerMessage } from "@/ws-protocol";
37
+
38
+ export interface UseSessionChatOptions {
39
+ session: Session;
40
+ onBack: () => void;
41
+ /** Dipanggil setelah Session dihapus dari menu aksi header. */
42
+ onDeleted?: () => void;
43
+ }
44
+
45
+ export interface SessionChat {
46
+ // Koneksi & status
47
+ wsStatus: WsConnectionStatus;
48
+ /** Kirim pesan Client -> Server (dipakai composer untuk `input`/`interrupt`). */
49
+ send: (msg: ClientMessage) => void;
50
+ status: SessionStatus;
51
+ /** Model sedang merespon: turn aktif atau masih ada pesan streaming. */
52
+ busy: boolean;
53
+ /** Model sedang merespon DAN Session berjalan (untuk tombol Stop). */
54
+ generating: boolean;
55
+ /** Input dapat dipakai (koneksi + Session running + tidak sibuk). */
56
+ canInput: boolean;
57
+ error: string | null;
58
+ /** Set error banner global (dipakai composer saat upload/kirim gagal). */
59
+ reportError: (message: string | null) => void;
60
+
61
+ // Data percakapan
62
+ messages: SessionMessage[];
63
+ /**
64
+ * Riwayat awal (`history`) sudah diterima dari server. Sebelum ini,
65
+ * `messages` kosong BUKAN berarti percakapan kosong — UI menampilkan
66
+ * kerangka loading, bukan empty-state "No conversation yet".
67
+ */
68
+ historyLoaded: boolean;
69
+ /** Status toggle blok "Thought process" per turn. */
70
+ collapsible: CollapsibleState;
71
+ toggleBlock: (key: string) => void;
72
+
73
+ // Prompt pending
74
+ prompts: InteractivePrompt[];
75
+ /** Prompt pending yang digroup (kind + title identik) jadi satu kartu. */
76
+ promptGroups: InteractivePrompt[][];
77
+ /** Ada question pending — composer digantikan panel question docked. */
78
+ hasPendingQuestion: boolean;
79
+ promptError: string | null;
80
+ consumePromptError: () => void;
81
+ /** Prompt yang sedang memainkan animasi keluar sebelum dihapus. */
82
+ resolvePrompt: (promptId: string, response: PromptResponse) => void;
83
+
84
+ // Aksi Session
85
+ interrupt: () => void;
86
+ starting: boolean;
87
+ start: () => Promise<void>;
88
+ stopping: boolean;
89
+ stop: () => Promise<void>;
90
+ /** Dialog konfirmasi hapus Session (dirender `SessionView`). */
91
+ deleteOpen: boolean;
92
+ beginDelete: () => void;
93
+ cancelDelete: () => void;
94
+ deleting: boolean;
95
+ confirmDelete: () => Promise<void>;
96
+ }
97
+
98
+ /** Id pesan user optimistis (prompt pertama dari homepage). */
99
+ const OPTIMISTIC_ID = "optimistic:first-prompt";
100
+
101
+ export function useSessionChat({ session, onBack, onDeleted }: UseSessionChatOptions): SessionChat {
102
+ /**
103
+ * Prompt pertama dari homepage langsung tampil sebagai pesan user sejak
104
+ * frame pertama — layout Session view sudah dalam bentuk percakapan
105
+ * (composer di dasar) saat View Transition memotret halaman tujuan, dan
106
+ * user tidak melihat kilatan "No conversation yet". Dilepas begitu echo
107
+ * pesan user asli (id server) tiba.
108
+ */
109
+ const [optimistic, setOptimistic] = useState<SessionMessage | null>(() => {
110
+ const text = peekPendingPrompt(session.id);
111
+ return text === null
112
+ ? null
113
+ : {
114
+ id: OPTIMISTIC_ID,
115
+ sessionId: session.id,
116
+ role: "user",
117
+ parts: [{ type: "text", text }],
118
+ createdAt: Date.now(),
119
+ };
120
+ });
121
+ const [serverMessages, setMessages] = useState<SessionMessage[]>([]);
122
+ const hasRealUser = serverMessages.some((m) => m.role === "user");
123
+ const messages = useMemo(
124
+ () => (optimistic && !hasRealUser ? [optimistic, ...serverMessages] : serverMessages),
125
+ [optimistic, hasRealUser, serverMessages],
126
+ );
127
+ // Echo asli tiba -> buang pesan optimistis.
128
+ useEffect(() => {
129
+ if (optimistic && hasRealUser) setOptimistic(null);
130
+ }, [optimistic, hasRealUser]);
131
+ const [collapsible, setCollapsible] = useState<CollapsibleState>({});
132
+ const [prompts, setPrompts] = useState<InteractivePrompt[]>([]);
133
+ const [status, setStatus] = useState<SessionStatus>(session.status);
134
+ const [error, setError] = useState<string | null>(null);
135
+ /** Error resolusi prompt — tampil di kartunya (bukan banner global). */
136
+ const [promptError, setPromptError] = useState<string | null>(null);
137
+ /** Turn aktif dari server (`turn_active`): model sedang merespon. */
138
+ const [turnActive, setTurnActive] = useState(false);
139
+ const [starting, setStarting] = useState(false);
140
+ const [stopping, setStopping] = useState(false);
141
+ const [deleteOpen, setDeleteOpen] = useState(false);
142
+ const [deleting, setDeleting] = useState(false);
143
+ /** `history` sudah diterima — attach selesai, aman mengirim input. */
144
+ const [historyLoaded, setHistoryLoaded] = useState(false);
145
+
146
+ /**
147
+ * `onBack` dibaca lewat ref agar handler WS tidak perlu dibuat ulang
148
+ * (dan koneksi WebSocket tidak di-attach ulang) tiap callback berubah.
149
+ */
150
+ const onBackRef = useRef(onBack);
151
+ onBackRef.current = onBack;
152
+
153
+ const onMessage = useCallback(
154
+ (msg: ServerMessage) => {
155
+ switch (msg.type) {
156
+ case "history":
157
+ setError(null);
158
+ setMessages(msg.messages);
159
+ setPrompts(msg.prompts);
160
+ // Pesan lama tampil penuh; turn tidak mungkin aktif saat reattach.
161
+ setTurnActive(false);
162
+ // Blok "Thought process" di-reset: state collapsible diisi ulang oleh
163
+ // effect sinkronisasi di bawah (default collapsed per turn).
164
+ setCollapsible({});
165
+ setHistoryLoaded(true);
166
+ break;
167
+ case "message":
168
+ setError(null);
169
+ // Replace versi streaming (id sama) atau tambahkan pesan baru.
170
+ setMessages((prev) => upsertMessage(prev, msg.message));
171
+ break;
172
+ case "message_part":
173
+ setError(null);
174
+ // Upsert part streaming (snapshot utuh): opencode mengirimnya saat part
175
+ // dibuat (teks kosong) dan saat selesai (teks lengkap). Isi di
176
+ // antaranya datang lewat `message_part_delta` di bawah.
177
+ setMessages((prev) => upsertMessagePart(prev, msg.sessionId, msg.messageId, msg.part));
178
+ break;
179
+ case "message_part_delta":
180
+ // Token model (opencode `message.part.delta`): teks bertambah per
181
+ // potongan -> efek mengetik seperti TUI opencode.
182
+ setMessages((prev) =>
183
+ appendMessagePartDelta(prev, msg.messageId, msg.partId, msg.field, msg.delta),
184
+ );
185
+ break;
186
+ case "prompt":
187
+ setError(null);
188
+ setPrompts((prev) =>
189
+ prev.some((p) => p.id === msg.prompt.id) ? prev : [...prev, msg.prompt],
190
+ );
191
+ break;
192
+ case "prompt_resolved":
193
+ // Langsung dihapus; animasi keluar kartu ditangani `AnimatePresence`
194
+ // di `PromptPanel` (kartu tetap di DOM sampai animasinya selesai).
195
+ setPrompts((prev) => prev.filter((p) => p.id !== msg.promptId));
196
+ break;
197
+ case "session_status":
198
+ // Kini dikirim ke semua koneksi — abaikan milik Session lain.
199
+ if (msg.sessionId !== session.id) break;
200
+ setStatus(msg.status);
201
+ // Session non-running -> tidak mungkin ada model yang merespon.
202
+ if (msg.status !== "running") setTurnActive(false);
203
+ break;
204
+ case "turn_active":
205
+ setTurnActive(msg.active);
206
+ break;
207
+ case "session_deleted":
208
+ // Kini dikirim ke semua koneksi — hanya Session ini yang ditinggalkan.
209
+ if (msg.sessionId !== session.id) break;
210
+ // Session dihapus dari tempat lain — kembali ke daftar Session.
211
+ onBackRef.current();
212
+ break;
213
+ case "error":
214
+ // Prompt kedaluwarsa: kartunya sudah dihapus lewat `prompt_resolved`,
215
+ // jadi pesannya tampil di timeline (bukan di kartu — kartu
216
+ // berikutnya tidak boleh mewarisi error ini).
217
+ if (msg.code === "PROMPT_EXPIRED") {
218
+ setError(msg.message);
219
+ break;
220
+ }
221
+ // Error terkait prompt ditampilkan di kartunya masing-masing —
222
+ // banner global di atas chat tidak terlihat oleh user yang sedang
223
+ // fokus ke kartu (tombol terasa "mati" tanpa feedback).
224
+ if (
225
+ msg.code === "PROMPT_NOT_FOUND" ||
226
+ msg.code === "PROMPT_ALREADY_RESOLVED" ||
227
+ msg.code === "PROMPT_FAILED"
228
+ ) {
229
+ setPromptError(msg.message);
230
+ break;
231
+ }
232
+ setError(msg.message);
233
+ break;
234
+ }
235
+ // `session.id` stabil per mount (SessionView di-key per id).
236
+ },
237
+ [session.id],
238
+ );
239
+
240
+ const {
241
+ status: wsStatus,
242
+ attach,
243
+ send,
244
+ disconnect,
245
+ } = useWebSocket({
246
+ onMessage,
247
+ });
248
+
249
+ useEffect(() => {
250
+ attach(session.id);
251
+ return () => disconnect();
252
+ }, [attach, disconnect, session.id]);
253
+
254
+ /**
255
+ * Prompt pertama dari chat homepage (`HomeChat`): dikirim sekali setelah
256
+ * attach selesai (`history` diterima). `takePendingPrompt` menghapusnya
257
+ * sehingga tidak terkirim ulang saat reconnect / remount.
258
+ */
259
+ useEffect(() => {
260
+ if (!historyLoaded || wsStatus !== "connected") return;
261
+ const text = takePendingPrompt(session.id);
262
+ if (text === null) return;
263
+ // Socket belum siap -> kembalikan agar dicoba lagi saat terhubung ulang.
264
+ if (!send({ type: "input", sessionId: session.id, text })) setPendingPrompt(session.id, text);
265
+ }, [historyLoaded, wsStatus, send, session.id]);
266
+
267
+ /**
268
+ * Sinkronisasi key collapsible "Thought process": satu blok per turn
269
+ * (id pesan pertama grup), default collapsed. `extendCollapsed` tidak
270
+ * mengubah key yang sudah ada sehingga toggle user tetap dipertahankan
271
+ * saat daftar pesan bertambah/berubah saat streaming.
272
+ */
273
+ useEffect(() => {
274
+ setCollapsible((prev) => {
275
+ const keys: string[] = [];
276
+ for (const g of groupTurns(messages)) {
277
+ if (g.kind !== "assistant") continue;
278
+ if (
279
+ !g.messages.some((m) => m.parts.some((p) => p.type === "reasoning" || p.type === "tool"))
280
+ )
281
+ continue;
282
+ const segs = turnSegments(g.messages);
283
+ for (const block of turnBlocks(segs)) {
284
+ if (block.steps.length > 0) keys.push(`${g.id}:${block.key}`);
285
+ }
286
+ }
287
+ return extendCollapsed(prev, keys);
288
+ });
289
+ }, [messages]);
290
+
291
+ const toggleBlock = useCallback((key: string) => {
292
+ setCollapsible((s) => toggleCollapsible(s, key));
293
+ }, []);
294
+
295
+ const consumePromptError = useCallback(() => setPromptError(null), []);
296
+
297
+ const resolvePrompt = useCallback(
298
+ (promptId: string, response: PromptResponse) => {
299
+ // Melempar bila tidak terhubung: kartu menangkapnya dan menampilkan
300
+ // error — send() yang diam-diam dibuang membuat tombol terasa mati.
301
+ if (wsStatus !== "connected") {
302
+ throw new Error(`Not connected (${wsStatusLabel(wsStatus)}). Try again.`);
303
+ }
304
+ send({ type: "prompt_response", sessionId: session.id, promptId, response });
305
+ },
306
+ [send, session.id, wsStatus],
307
+ );
308
+
309
+ const reportError = useCallback((message: string | null) => setError(message), []);
310
+
311
+ /**
312
+ * Hentikan balasan model (interrupt ala opencode) — Session tetap aktif.
313
+ * Server membuang parts turn parsial (tidak disimpan), dan di sini kita
314
+ * juga membersihkan respon parsial yang sudah tampil di UI agar hilang
315
+ * dari percakapan (pesan user & turn sebelumnya tetap utuh).
316
+ */
317
+ const interrupt = useCallback(() => {
318
+ send({ type: "interrupt", sessionId: session.id });
319
+ setMessages((prev) => prev.filter((m) => !(m.role === "assistant" && m.streaming === true)));
320
+ setTurnActive(false);
321
+ }, [send, session.id]);
322
+
323
+ /** Resume via API langsung — hasil status baru tiba via WS `session_status`. */
324
+ const start = useCallback(async () => {
325
+ setStarting(true);
326
+ setError(null);
327
+ try {
328
+ await apiFetch(`/api/sessions/${session.id}`, { method: "POST" });
329
+ // Status baru dikirim gateway ke semua subscriber; tak perlu setState di sini.
330
+ toast.success("Session resumed.");
331
+ } catch (e) {
332
+ toast.error(e instanceof ApiError ? e.message : "Couldn't resume the session.");
333
+ } finally {
334
+ setStarting(false);
335
+ }
336
+ }, [session.id]);
337
+
338
+ /**
339
+ * Hentikan Session (bukan hanya balasan model): status -> `stopped`.
340
+ * Berbeda dari tombol Stop di composer yang hanya meng-interrupt turn.
341
+ */
342
+ const stop = useCallback(async () => {
343
+ setStopping(true);
344
+ setError(null);
345
+ try {
346
+ await apiFetch(`/api/sessions/${session.id}/stop`, { method: "POST" });
347
+ toast.success("Session stopped.");
348
+ } catch (e) {
349
+ toast.error(e instanceof ApiError ? e.message : "Couldn't stop the session.");
350
+ } finally {
351
+ setStopping(false);
352
+ }
353
+ }, [session.id]);
354
+
355
+ /** Hapus Session permanen dari menu aksi header (dikonfirmasi lebih dulu). */
356
+ const beginDelete = useCallback(() => setDeleteOpen(true), []);
357
+ const cancelDelete = useCallback(() => setDeleteOpen(false), []);
358
+ const confirmDelete = useCallback(async () => {
359
+ setDeleting(true);
360
+ setError(null);
361
+ try {
362
+ await apiFetch(`/api/sessions/${session.id}`, { method: "DELETE" });
363
+ setDeleteOpen(false);
364
+ toast.success("Session deleted.");
365
+ notifyDataChanged();
366
+ (onDeleted ?? onBack)();
367
+ } catch (e) {
368
+ toast.error(e instanceof ApiError ? e.message : "Couldn't delete the session.");
369
+ } finally {
370
+ setDeleting(false);
371
+ }
372
+ }, [session.id, onBack, onDeleted]);
373
+
374
+ /**
375
+ * Model sedang merespon: server melaporkan turn aktif (`turn_active`), atau
376
+ * masih ada pesan assistant `streaming` yang belum digantikan versi final
377
+ * (fallback saat reattach di tengah turn).
378
+ */
379
+ const busy = turnActive || messages.some((m) => m.role === "assistant" && m.streaming === true);
380
+ const generating = busy && status === "running";
381
+ // Saat model merespon input dinonaktifkan — satu-satunya aksi adalah Stop.
382
+ const canInput = wsStatus === "connected" && status === "running" && !busy;
383
+
384
+ /**
385
+ * Grup prompt pending untuk panel floating di atas composer — permission
386
+ * identik (kind+title sama) digroup jadi SATU kartu (bug kartu nyepam).
387
+ */
388
+ const promptGroups = useMemo(() => groupPrompts(prompts), [prompts]);
389
+ /**
390
+ * Ada question pending? Composer disembunyikan dan panel question docked
391
+ * menggantikannya — jawaban pertanyaan adalah input saat ini (ala TUI
392
+ * opencode yang menutup input teks selama question aktif). Permission
393
+ * kartu tidak menggantikan composer (tetap floating).
394
+ */
395
+ const hasPendingQuestion = useMemo(() => prompts.some((p) => p.kind === "question"), [prompts]);
396
+
397
+ return {
398
+ wsStatus,
399
+ send,
400
+ status,
401
+ busy,
402
+ generating,
403
+ canInput,
404
+ error,
405
+ reportError,
406
+ messages,
407
+ historyLoaded,
408
+ collapsible,
409
+ toggleBlock,
410
+ prompts,
411
+ promptGroups,
412
+ hasPendingQuestion,
413
+ promptError,
414
+ consumePromptError,
415
+ resolvePrompt,
416
+ interrupt,
417
+ starting,
418
+ start,
419
+ stopping,
420
+ stop,
421
+ deleteOpen,
422
+ beginDelete,
423
+ cancelDelete,
424
+ deleting,
425
+ confirmDelete,
426
+ };
427
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Konteks kerja Session untuk panel kanan: perubahan file, cabang git, dan
3
+ * task list agent (`GET /api/sessions/:id/context`).
4
+ *
5
+ * Polling, bukan WebSocket: sumbernya (git + `todowrite`) tidak memancarkan
6
+ * event ke KCG Code, dan panel hanya perlu "cukup baru". Supaya tidak memboros
7
+ * request:
8
+ * - Hanya di-poll saat panel TERBUKA (`enabled`).
9
+ * - Saat model sedang bekerja (`busy`) interval dipercepat — di situlah file &
10
+ * task list berubah; selainnya lambat.
11
+ * - Tab di latar belakang -> poll dihentikan (`visibilitychange`), lalu
12
+ * langsung menyegarkan begitu tab aktif kembali.
13
+ */
14
+ import { useCallback, useEffect, useRef, useState } from "react";
15
+ import { apiFetch } from "@/lib/api";
16
+ import type { SessionContext } from "@/types";
17
+
18
+ /** Interval poll saat model bekerja vs idle (ms). */
19
+ const ACTIVE_MS = 2500;
20
+ const IDLE_MS = 12000;
21
+
22
+ export interface UseSessionContextResult {
23
+ context: SessionContext | null;
24
+ /** Fetch pertama belum selesai (panel menampilkan skeleton). */
25
+ loading: boolean;
26
+ /** Muat ulang sekarang (mis. setelah aksi pengguna). */
27
+ refresh: () => void;
28
+ }
29
+
30
+ export function useSessionContext(
31
+ sessionId: string,
32
+ /** Panel terbuka — data hanya diambil saat dibutuhkan. */
33
+ enabled: boolean,
34
+ /** Model sedang merespon: konteks berubah cepat, poll dipercepat. */
35
+ busy: boolean,
36
+ ): UseSessionContextResult {
37
+ const [context, setContext] = useState<SessionContext | null>(null);
38
+ const [loading, setLoading] = useState(false);
39
+ /** Ada data yang bisa ditampilkan — skeleton hanya untuk fetch pertama. */
40
+ const hasData = useRef(false);
41
+ /** Request terakhir yang masih relevan (hindari balasan basi saling timpa). */
42
+ const runId = useRef(0);
43
+
44
+ const load = useCallback(async () => {
45
+ const id = ++runId.current;
46
+ try {
47
+ const res = await apiFetch(`/api/sessions/${sessionId}/context`);
48
+ const body = (await res.json()) as { context?: SessionContext };
49
+ if (id !== runId.current || !body.context) return;
50
+ hasData.current = true;
51
+ setContext(body.context);
52
+ } catch {
53
+ // Panel ini bukan alur utama: kegagalan jaringan cukup diabaikan,
54
+ // poll berikutnya mencoba lagi. Data terakhir tetap ditampilkan.
55
+ } finally {
56
+ if (id === runId.current) setLoading(false);
57
+ }
58
+ }, [sessionId]);
59
+
60
+ const refresh = useCallback(() => {
61
+ if (!enabled) return;
62
+ void load();
63
+ }, [enabled, load]);
64
+
65
+ // Session berganti -> buang data lama agar panel tidak menampilkan konteks
66
+ // Session sebelumnya. Efeknya hanya me-reset (tidak membaca nilai apa pun),
67
+ // jadi `sessionId` sengaja dipakai sebagai PEMICU, bukan dependency nilai.
68
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `sessionId` adalah pemicu reset
69
+ useEffect(() => {
70
+ hasData.current = false;
71
+ setContext(null);
72
+ }, [sessionId]);
73
+
74
+ useEffect(() => {
75
+ if (!enabled) return;
76
+ setLoading(!hasData.current);
77
+ void load();
78
+
79
+ let timer: ReturnType<typeof setInterval> | null = null;
80
+ const start = () => {
81
+ if (timer !== null) return;
82
+ timer = setInterval(() => void load(), busy ? ACTIVE_MS : IDLE_MS);
83
+ };
84
+ const stop = () => {
85
+ if (timer !== null) clearInterval(timer);
86
+ timer = null;
87
+ };
88
+ const onVisibility = () => {
89
+ if (document.visibilityState === "visible") {
90
+ void load();
91
+ start();
92
+ } else {
93
+ stop();
94
+ }
95
+ };
96
+
97
+ if (document.visibilityState === "visible") start();
98
+ document.addEventListener("visibilitychange", onVisibility);
99
+ return () => {
100
+ stop();
101
+ document.removeEventListener("visibilitychange", onVisibility);
102
+ };
103
+ }, [enabled, busy, load]);
104
+
105
+ return { context, loading, refresh };
106
+ }