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,240 @@
1
+ /**
2
+ * Tampilan Session — versi headless (opencode serve).
3
+ *
4
+ * Berbeda dari versi PTY/TUI (yang me-render chunk byte terminal TUI):
5
+ * - Pesan datang sebagai `SessionMessage` terstruktur (`role` + `parts`):
6
+ * text, reasoning (collapsible per part, urut sesuai alur), tool/step.
7
+ * - Interactive_Prompt (permission/question) dirender via `PromptCard.tsx`.
8
+ * - Saat model merespon (`turn_active` true / pesan streaming) tombol kirim
9
+ * berubah jadi tombol Stop (kirim `{ type: "interrupt" }`) — menghentikan
10
+ * balasan saja ala opencode, Session tetap berjalan. `{ type: "stop" }`
11
+ * (menonaktifkan Session) tetap ada lewat daftar Session / HTTP.
12
+ *
13
+ * File ini sekarang HANYA menyusun area layar dari bagian-bagian yang
14
+ * sudah dipisah:
15
+ * - State & alur WS: `hooks/useSessionChat.ts`.
16
+ * - Logika transformasi pesan murni: `lib/turns.ts` (type di `types/turns.ts`).
17
+ * - Area UI: `SessionHeader`, `SessionTimeline`, `PromptPanel`,
18
+ * `SessionComposer` di folder yang sama.
19
+ * - Drag & drop file: `FileDropOverlay` (portal, menutupi seluruh viewport)
20
+ * meneruskan file ke composer (`SessionComposerHandle.addFiles`).
21
+ * - Kartu konteks kanan (`ContextPanel`): Environment / Task list / MCP /
22
+ * Reference. Desktop: kolom sendiri di kanan (berdampingan) — kolom chat
23
+ * menyempit & percakapan tetap terpusat di sisa ruang, tidak tertutup
24
+ * kartu. Di bawah `lg` (HP & tablet) tampil sebagai bottom sheet.
25
+ */
26
+ import { useMemo, useRef, useState } from "react";
27
+ import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
28
+ import {
29
+ ContextPanel,
30
+ ContextPanelTrigger,
31
+ ContextSheet,
32
+ } from "@/components/sessions/ContextPanel";
33
+ import { FileDropOverlay } from "@/components/sessions/FileDropOverlay";
34
+ import { SessionComposer, type SessionComposerHandle } from "@/components/sessions/SessionComposer";
35
+ import { SessionHeader } from "@/components/sessions/SessionHeader";
36
+ import { SessionTimeline } from "@/components/sessions/SessionTimeline";
37
+ import {
38
+ SessionComposerSkeleton,
39
+ SessionTimelineSkeleton,
40
+ } from "@/components/sessions/SessionTimelineSkeleton";
41
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
42
+ import { useSessionChat } from "@/hooks/useSessionChat";
43
+ import { useSessionContext } from "@/hooks/useSessionContext";
44
+ import { CONVERSATION_MAX_W } from "@/lib/layout";
45
+ import { collectReferences } from "@/lib/references";
46
+ import { cn } from "@/lib/utils";
47
+ import type { Session } from "@/types";
48
+ import { PromptPanel } from "./PromptPanel";
49
+
50
+ /** Lebar minimum untuk kartu konteks sebagai kolom samping (`lg`). */
51
+ const SIDE_PANEL_QUERY = "(min-width: 1024px)";
52
+
53
+ export interface SessionViewProps {
54
+ session: Session;
55
+ onBack: () => void;
56
+ /**
57
+ * Dipanggil setelah Session dihapus dari menu aksi header. Bila tidak
58
+ * diberikan, `onBack` dipakai — halaman ini tidak punya data lagi untuk
59
+ * ditampilkan setelah Session hilang.
60
+ */
61
+ onDeleted?: () => void;
62
+ }
63
+
64
+ export function SessionView({ session, onBack, onDeleted }: SessionViewProps) {
65
+ const chat = useSessionChat({ session, onBack, onDeleted });
66
+ /**
67
+ * Riwayat belum tiba dari WebSocket. `messages` kosong di fase ini BUKAN
68
+ * percakapan kosong — tampilkan kerangka, jangan empty-state. Pengecualian:
69
+ * pesan optimistis (prompt pertama dari homepage) sudah ada sejak frame
70
+ * pertama, jadi langsung tampil sebagai percakapan.
71
+ */
72
+ const loadingHistory = !chat.historyLoaded && chat.messages.length === 0;
73
+ const empty = !loadingHistory && chat.messages.length === 0;
74
+ const composerRef = useRef<SessionComposerHandle>(null);
75
+ /** Panel konteks kanan; default tertutup agar area chat tetap lega. */
76
+ const [contextOpen, setContextOpen] = useState(false);
77
+ // Kolom konteks berdampingan butuh ruang: di bawah `lg` kolom chat jadi
78
+ // terlalu sempit, jadi kartu tampil sebagai bottom sheet (seperti HP).
79
+ const sidePanel = useMediaQuery(SIDE_PANEL_QUERY);
80
+ const sessionCtx = useSessionContext(session.id, contextOpen, chat.generating);
81
+ /**
82
+ * Reference diturunkan dari parts pesan yang sudah ada di timeline
83
+ * (tanpa request tambahan). `session.cwd` dipakai untuk mengubah path
84
+ * absolut dari tool call menjadi path relatif project.
85
+ */
86
+ const references = useMemo(
87
+ () => (contextOpen ? collectReferences(chat.messages, session.cwd) : []),
88
+ [contextOpen, chat.messages, session.cwd],
89
+ );
90
+ // Composer tidak dirender saat pertanyaan menunggu jawaban (panel docked)
91
+ // maupun saat riwayat masih dimuat (belum tahu tata letak akhirnya).
92
+ const composerShown = !loadingHistory && (empty || !chat.hasPendingQuestion);
93
+ const acceptingFiles = composerShown && chat.canInput;
94
+ const blockedReason = loadingHistory
95
+ ? "Wait for the conversation to load, then drop your files."
96
+ : !composerShown
97
+ ? "Answer the question first, then add your files."
98
+ : chat.busy
99
+ ? "Wait for the response to finish, then drop your files."
100
+ : "Start the session to add files.";
101
+
102
+ return (
103
+ // `data-vt-name`: tujuan transisi "to-session" — composer homepage meluas
104
+ // menjadi seluruh area ini (lihat styles/globals.css).
105
+ <div data-vt-name="session-surface" className="flex h-full min-h-0 flex-col">
106
+ <SessionHeader
107
+ session={session}
108
+ status={chat.status}
109
+ wsStatus={chat.wsStatus}
110
+ onStop={() => void chat.stop()}
111
+ onStart={() => void chat.start()}
112
+ onRequestDelete={chat.beginDelete}
113
+ stopping={chat.stopping}
114
+ starting={chat.starting}
115
+ />
116
+
117
+ {/* Percakapan + kartu konteks berdampingan. `relative` jadi acuan tombol
118
+ hamburger (kanan bawah header) dan kartu saat mengapung di layar
119
+ sempit. */}
120
+ <div className="relative flex min-h-0 flex-1">
121
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col">
122
+ {loadingHistory ? (
123
+ // Kerangka sama persis dengan fase `SessionPage` -> tanpa kedipan.
124
+ <>
125
+ <SessionTimelineSkeleton />
126
+ <SessionComposerSkeleton />
127
+ </>
128
+ ) : empty ? (
129
+ /* Empty-state ala Gemini: sapaan + composer dipusatkan vertikal.
130
+ Timeline & prompt panel belum dirender (tidak ada isinya); spacer
131
+ flex-1 di atas & bawah menjaga blok ini tepat di tengah kolom di
132
+ bawah header. Begitu pesan pertama masuk (echo WS), layout pindah
133
+ ke varian percakapan di bawah — composer turun ke dasar. */
134
+ <div className="flex min-h-0 flex-1 flex-col px-3 sm:px-4">
135
+ <div className="min-h-0 flex-[2]" />
136
+ <div className={cn("mx-auto w-full", CONVERSATION_MAX_W)}>
137
+ <h1 className="mb-6 text-center text-2xl font-medium tracking-tight sm:text-3xl">
138
+ No conversation yet
139
+ </h1>
140
+ <SessionComposer
141
+ ref={composerRef}
142
+ session={session}
143
+ inputAllowed={chat.canInput}
144
+ busy={chat.busy}
145
+ generating={chat.generating}
146
+ wsStatus={chat.wsStatus}
147
+ send={chat.send}
148
+ reportError={chat.reportError}
149
+ onInterrupt={chat.interrupt}
150
+ />
151
+ </div>
152
+ <div className="min-h-0 flex-[3]" />
153
+ </div>
154
+ ) : (
155
+ <>
156
+ <SessionTimeline
157
+ error={chat.error}
158
+ status={chat.status}
159
+ messages={chat.messages}
160
+ collapsible={chat.collapsible}
161
+ onToggle={chat.toggleBlock}
162
+ generating={chat.generating}
163
+ />
164
+
165
+ {/* Question pending: panel DOCKED menggantikan composer — jawaban
166
+ pertanyaan adalah satu-satunya input saat ini (ala TUI opencode).
167
+ Hanya area jawaban kartu yang scroll; footer aksi tetap terlihat.
168
+ Permission (bukan question): panel tetap mengambang DI ATAS
169
+ composer seperti biasa. */}
170
+ <PromptPanel
171
+ groups={chat.promptGroups}
172
+ errorSignal={chat.promptError}
173
+ onResolve={chat.resolvePrompt}
174
+ onConsumeError={chat.consumePromptError}
175
+ docked={chat.hasPendingQuestion}
176
+ />
177
+
178
+ {!chat.hasPendingQuestion && (
179
+ <SessionComposer
180
+ ref={composerRef}
181
+ session={session}
182
+ inputAllowed={chat.canInput}
183
+ busy={chat.busy}
184
+ generating={chat.generating}
185
+ wsStatus={chat.wsStatus}
186
+ send={chat.send}
187
+ reportError={chat.reportError}
188
+ onInterrupt={chat.interrupt}
189
+ />
190
+ )}
191
+ </>
192
+ )}
193
+ </div>
194
+
195
+ {/* Tertutup: hanya tombol hamburger. Terbuka (desktop): tombol hilang
196
+ & kartu jadi kolom kanan selebar 17rem yang IKUT tata letak — kolom
197
+ chat (flex-1) menyempit, jadi pesan & composer tidak tertutup. */}
198
+ {!sidePanel ? (
199
+ // HP & tablet: bottom sheet. Tombol hamburger tetap terlihat — sheet
200
+ // menutupi layar & punya penutupnya sendiri (tap luar / geser / Esc).
201
+ <>
202
+ <ContextPanelTrigger onOpen={() => setContextOpen(true)} />
203
+ <ContextSheet
204
+ open={contextOpen}
205
+ onOpenChange={setContextOpen}
206
+ context={sessionCtx.context}
207
+ references={references}
208
+ loading={sessionCtx.loading}
209
+ />
210
+ </>
211
+ ) : contextOpen ? (
212
+ <div className="flex w-[17rem] shrink-0 flex-col pt-2 pe-3 pb-3 sm:pe-4">
213
+ <ContextPanel
214
+ context={sessionCtx.context}
215
+ references={references}
216
+ loading={sessionCtx.loading}
217
+ onClose={() => setContextOpen(false)}
218
+ />
219
+ </div>
220
+ ) : (
221
+ <ContextPanelTrigger onOpen={() => setContextOpen(true)} />
222
+ )}
223
+ </div>
224
+
225
+ <FileDropOverlay
226
+ accepting={acceptingFiles}
227
+ blockedReason={blockedReason}
228
+ onDropFiles={(files, from) => composerRef.current?.addFiles(files, from)}
229
+ />
230
+
231
+ {/* Konfirmasi hapus Session dari menu aksi header */}
232
+ <ConfirmSessionDeleteDialog
233
+ open={chat.deleteOpen}
234
+ onOpenChange={(open) => (open ? chat.beginDelete() : chat.cancelDelete())}
235
+ deleting={chat.deleting}
236
+ onConfirm={() => void chat.confirmDelete()}
237
+ />
238
+ </div>
239
+ );
240
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Satu blok "Thought process" collapsible di dalam turn assistant.
3
+ *
4
+ * Berisi kluster steps berurutan (reasoning/tool) dengan garis penghubung
5
+ * antar langkah + tanda "Done" setelah blok selesai di-stream. Status toggle
6
+ * dikelola pemilik daftar (key `groupId:blockKey`) agar blok baru saat
7
+ * streaming otomatis masuk keadaan collapsed tanpa menyentuh toggle user.
8
+ */
9
+ import {
10
+ ChevronRightIcon,
11
+ CircleCheckBigIcon,
12
+ ClockFadingIcon,
13
+ FileIcon,
14
+ FolderIcon,
15
+ GlobeIcon,
16
+ type LucideIcon,
17
+ PenLineIcon,
18
+ SearchIcon,
19
+ TerminalIcon,
20
+ WrenchIcon,
21
+ } from "lucide-react";
22
+ import type { ReactNode } from "react";
23
+ import { Button } from "@/components/ui/button";
24
+ import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
25
+ import { useSmoothText } from "@/hooks/useSmoothText";
26
+ import { type CollapsibleState, isCollapsibleExpanded } from "@/lib/collapsible";
27
+ import { partText, toolLabel } from "@/lib/turns";
28
+ import { cn } from "@/lib/utils";
29
+ import type { MessagePart, TurnSegment } from "@/types";
30
+
31
+ /** Step dalam blok: reasoning atau tool call. */
32
+ export type ThoughtStep = Extract<TurnSegment, { kind: "reasoning" | "tool" }>;
33
+
34
+ export interface ThoughtProcessBlockProps {
35
+ /** Id grup turn (untuk key collapsible `groupId:blockKey`). */
36
+ groupId: string;
37
+ /** Key blok (dari key segmen pertama). */
38
+ blockKey: string;
39
+ steps: ThoughtStep[];
40
+ collapsible: CollapsibleState;
41
+ onToggle: (key: string) => void;
42
+ /** Turn masih streaming — blok terakhir menampilkan indikator aktif. */
43
+ streaming: boolean;
44
+ /** Apakah ini blok terakhir dalam turn (menentukan indikator streaming). */
45
+ isLastBlock: boolean;
46
+ }
47
+
48
+ /**
49
+ * Ikon badge tool mengikuti nama tool (fallback: kunci inggris).
50
+ */
51
+ const TOOL_ICONS: Record<string, LucideIcon> = {
52
+ read: FileIcon,
53
+ list: FolderIcon,
54
+ glob: FolderIcon,
55
+ edit: PenLineIcon,
56
+ write: PenLineIcon,
57
+ patch: PenLineIcon,
58
+ bash: TerminalIcon,
59
+ shell: TerminalIcon,
60
+ grep: SearchIcon,
61
+ webfetch: GlobeIcon,
62
+ };
63
+
64
+ function toolIcon(part: MessagePart): LucideIcon {
65
+ const name = (typeof part.tool === "string" ? part.tool : part.type).toLowerCase();
66
+ return (name !== "" && TOOL_ICONS[name]) || WrenchIcon;
67
+ }
68
+
69
+ /** Ikon kecil (14px) untuk langkah timeline & indikator status. */
70
+ function stepIcon(Icon: LucideIcon, className?: string) {
71
+ return <Icon className={cn("size-3.5 shrink-0", className)} data-icon="inline-start" />;
72
+ }
73
+
74
+ /**
75
+ * Baris ringkas isi reasoning di timeline. Saat masih di-stream (teks belum
76
+ * lengkap) tampil redup; setelah selesai tampil apa adanya — keduanya bisa
77
+ * panjang, jadi tinggi dibatasi + scroll internal.
78
+ */
79
+ function ReasoningStep({ text, live = false }: { text: string; live?: boolean }) {
80
+ // Reasoning yang masih di-stream ikut dihaluskan (sama seperti jawaban).
81
+ const shown = useSmoothText(text, live);
82
+ const dim = shown.trim() === "";
83
+ return (
84
+ <div
85
+ className={cn(
86
+ "max-h-40 overflow-y-auto whitespace-pre-wrap text-[13px] leading-relaxed",
87
+ dim && "text-muted-foreground/50",
88
+ )}
89
+ >
90
+ {dim ? "organizing thoughts…" : shown}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Satu langkah timeline di dalam blok "Thought process": ikon status di
97
+ * kiri (dengan garis penghubung antar langkah) + isi di kanan.
98
+ *
99
+ * Garis digambar ABSOLUT (bukan flex-1 di kolom stretch): `align-self:
100
+ * stretch` hanya membentang sampai content box sehingga di langkah
101
+ * satu-baris sisa ruang garisnya ~0px (tak terlihat). Absolut terhadap
102
+ * PADDING box -> `bottom-0` menyentuh dasar `pb-5`, artinya garis selalu
103
+ * tersambung ke langkah berikutnya berapa pun tinggi kontennya.
104
+ */
105
+ function renderTimelineStep(
106
+ icon: ReactNode,
107
+ content: ReactNode,
108
+ opts?: { last?: boolean; streaming?: boolean },
109
+ ) {
110
+ return (
111
+ <div className={cn("relative flex gap-2.5", !opts?.last && "pb-5")}>
112
+ {/* Garis penghubung: mulai tepat di bawah ikon (mt-0.5 + size-5 =
113
+ 22px) sampai dasar row; x=10px = pusat kolom ikon (size-5). */}
114
+ {!opts?.last && (
115
+ <span className="absolute top-6 bottom-0 left-2.5 -ml-px w-px bg-border" aria-hidden />
116
+ )}
117
+ <span
118
+ className={cn(
119
+ "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full",
120
+ opts?.streaming && "animate-pulse text-foreground/70",
121
+ )}
122
+ >
123
+ {icon}
124
+ </span>
125
+ <div className="min-w-0 flex-1 text-sm text-muted-foreground">{content}</div>
126
+ </div>
127
+ );
128
+ }
129
+
130
+ export function ThoughtProcessBlock({
131
+ groupId,
132
+ blockKey,
133
+ steps,
134
+ collapsible,
135
+ onToggle,
136
+ streaming,
137
+ isLastBlock,
138
+ }: ThoughtProcessBlockProps) {
139
+ if (steps.length === 0) return null;
140
+ const collapsibleKey = `${groupId}:${blockKey}`;
141
+ const expanded = isCollapsibleExpanded(collapsible, collapsibleKey);
142
+ // Blok terakhir yang masih streaming: "Done" belum tampil.
143
+ const blockStreaming = streaming && isLastBlock;
144
+
145
+ return (
146
+ <Collapsible open={expanded}>
147
+ <Button
148
+ type="button"
149
+ variant="ghost"
150
+ size="sm"
151
+ className="h-7 self-start gap-1 px-2 text-xs text-muted-foreground"
152
+ onClick={() => onToggle(collapsibleKey)}
153
+ aria-expanded={expanded}
154
+ >
155
+ Thought process
156
+ {/* Ikon sengaja di KANAN label */}
157
+ <ChevronRightIcon
158
+ data-icon="inline-end"
159
+ className={cn("transition-transform", expanded && "rotate-90")}
160
+ />
161
+ </Button>
162
+ <CollapsibleContent>
163
+ <div className="flex flex-col px-3 pt-1">
164
+ {steps.map((seg, i) =>
165
+ seg.kind === "reasoning" ? (
166
+ <div key={seg.key}>
167
+ {renderTimelineStep(
168
+ stepIcon(ClockFadingIcon, "text-muted-foreground"),
169
+ <ReasoningStep
170
+ text={partText(seg.part) ?? ""}
171
+ live={blockStreaming && i === steps.length - 1}
172
+ />,
173
+ {
174
+ last: blockStreaming && i === steps.length - 1,
175
+ streaming: blockStreaming && i === steps.length - 1,
176
+ },
177
+ )}
178
+ </div>
179
+ ) : (
180
+ <div key={seg.key}>
181
+ {renderTimelineStep(
182
+ stepIcon(toolIcon(seg.part), "text-muted-foreground"),
183
+ <span className="font-mono text-[13px]">{toolLabel(seg.part)}</span>,
184
+ { last: blockStreaming && i === steps.length - 1, streaming: false },
185
+ )}
186
+ </div>
187
+ ),
188
+ )}
189
+ {!blockStreaming &&
190
+ renderTimelineStep(
191
+ stepIcon(CircleCheckBigIcon, "text-muted-foreground"),
192
+ <span>Done</span>,
193
+ { last: true },
194
+ )}
195
+ </div>
196
+ </CollapsibleContent>
197
+ </Collapsible>
198
+ );
199
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Render satu pesan user: teks + lampiran gambar (thumbnail) + @file.
3
+ *
4
+ * Part `file` gambar (punya `attachmentId` + mime image) dimuat bytes-nya
5
+ * lewat route HTTP `/api/uploads/...` (`attachmentUrl`); file upload lain
6
+ * tampil sebagai kartu yang bisa diunduh; part `file` tanpa `attachmentId`
7
+ * (echo @file) tampil sebagai chip nama file.
8
+ */
9
+ import { FileIcon } from "lucide-react";
10
+ import { useState } from "react";
11
+ import { FileTile } from "@/components/sessions/AttachmentTile";
12
+ import { ImageLightbox } from "@/components/sessions/ImageLightbox";
13
+ import { CopyMessageButton } from "@/components/sessions/MessageActions";
14
+ import { Bubble, BubbleContent } from "@/components/ui/bubble";
15
+ import { Message, MessageContent, MessageFooter } from "@/components/ui/message";
16
+ import { attachmentUrl } from "@/lib/api";
17
+ import { isPreviewableImage } from "@/lib/attachments";
18
+ import { splitComposerMentions } from "@/lib/composer";
19
+ import { formatTime } from "@/lib/time";
20
+ import { textOf } from "@/lib/turns";
21
+ import { cn } from "@/lib/utils";
22
+ import type { MessagePart, SessionMessage } from "@/types";
23
+
24
+ export interface UserMessageProps {
25
+ message: SessionMessage;
26
+ }
27
+
28
+ /** Part `file` yang merupakan gambar lampiran (punya attachmentId + mime image). */
29
+ function isImageAttachment(
30
+ p: MessagePart,
31
+ ): p is MessagePart & { mime: string; attachmentId: string } {
32
+ return (
33
+ p.type === "file" &&
34
+ typeof p.mime === "string" &&
35
+ isPreviewableImage(p.mime) &&
36
+ typeof p.attachmentId === "string"
37
+ );
38
+ }
39
+
40
+ /** File upload non-gambar (dokumen, kode, arsip, …). */
41
+ function isFileAttachment(
42
+ p: MessagePart,
43
+ ): p is MessagePart & { attachmentId: string; filename: string } {
44
+ return (
45
+ p.type === "file" &&
46
+ typeof p.attachmentId === "string" &&
47
+ typeof p.filename === "string" &&
48
+ !isImageAttachment(p)
49
+ );
50
+ }
51
+
52
+ export function UserMessage({ message: m }: UserMessageProps) {
53
+ const [preview, setPreview] = useState<number | null>(null);
54
+ const images = m.parts.filter(isImageAttachment);
55
+ const text = textOf(m.parts);
56
+ const lightboxImages = images.map((p) => ({
57
+ src: attachmentUrl(m.sessionId, p.attachmentId),
58
+ alt: p.filename ?? "Attached image",
59
+ }));
60
+ const uploadedFiles = m.parts.filter(isFileAttachment);
61
+ /** Echo @file: part `file` tanpa `attachmentId` (path relatif project). */
62
+ const isMention = (p: MessagePart) =>
63
+ p.type === "file" && typeof p.attachmentId !== "string" && typeof p.filename === "string";
64
+ const mentionedPaths = new Set(m.parts.filter(isMention).map((p) => p.filename as string));
65
+ const attachedFiles = m.parts
66
+ .filter(isMention)
67
+ .map((p) => ({ part: p, filename: p.filename as string }));
68
+ return (
69
+ <Message align="end">
70
+ <MessageContent>
71
+ {/* Gambar di atas bubble teks (rata kanan), ala aplikasi chat:
72
+ 1 gambar = satu kartu besar; 2+ = grid 2 kolom kotak. Klik =
73
+ pratinjau layar penuh. */}
74
+ {images.length > 0 && (
75
+ <ul
76
+ aria-label={`${images.length} attached ${images.length === 1 ? "image" : "images"}`}
77
+ className={cn(
78
+ "grid gap-1.5 self-end",
79
+ images.length === 1 ? "w-full max-w-72 grid-cols-1" : "w-full max-w-80 grid-cols-2",
80
+ )}
81
+ >
82
+ {lightboxImages.map((img, i) => (
83
+ <li key={img.src} className={cn(images.length === 3 && i === 0 && "col-span-2")}>
84
+ <button
85
+ type="button"
86
+ onClick={() => setPreview(i)}
87
+ aria-label={`Preview ${img.alt}`}
88
+ className="group/img block w-full overflow-hidden rounded-xl border bg-muted outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
89
+ >
90
+ <img
91
+ src={img.src}
92
+ alt={img.alt}
93
+ loading="lazy"
94
+ draggable={false}
95
+ className={cn(
96
+ "w-full object-cover transition-transform duration-200 group-hover/img:scale-[1.02]",
97
+ images.length === 1 ? "max-h-80" : "aspect-square",
98
+ images.length === 3 && i === 0 && "aspect-[2/1]",
99
+ )}
100
+ />
101
+ </button>
102
+ </li>
103
+ ))}
104
+ </ul>
105
+ )}
106
+
107
+ {/* File non-gambar: kartu unduhan di atas bubble (rata kanan). */}
108
+ {uploadedFiles.length > 0 && (
109
+ <ul
110
+ aria-label={`${uploadedFiles.length} attached ${uploadedFiles.length === 1 ? "file" : "files"}`}
111
+ className="flex max-w-full flex-wrap justify-end gap-1.5 self-end"
112
+ >
113
+ {uploadedFiles.map((p) => (
114
+ <li key={p.attachmentId} className="min-w-0">
115
+ <a
116
+ href={attachmentUrl(m.sessionId, p.attachmentId)}
117
+ download={p.filename}
118
+ title={`Download ${p.filename}`}
119
+ className="block rounded-lg outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] focus-visible:ring-ring/50"
120
+ >
121
+ <FileTile
122
+ filename={p.filename}
123
+ mime={p.mime ?? ""}
124
+ className="h-14 w-56 max-w-full"
125
+ />
126
+ </a>
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ )}
131
+
132
+ {(text !== "" || attachedFiles.length > 0) && (
133
+ <Bubble>
134
+ {text !== "" && (
135
+ <BubbleContent className="whitespace-pre-wrap">
136
+ {/* Mention @file disorot sama seperti di composer. */}
137
+ {splitComposerMentions(text, mentionedPaths).map((seg, i) =>
138
+ seg.kind === "mention" ? (
139
+ <mark
140
+ // biome-ignore lint/suspicious/noArrayIndexKey: urutan potongan = urutan teks
141
+ key={i}
142
+ className="rounded-[3px] bg-current/10 text-inherit"
143
+ >
144
+ {seg.text}
145
+ </mark>
146
+ ) : (
147
+ // biome-ignore lint/suspicious/noArrayIndexKey: urutan potongan = urutan teks
148
+ <span key={i}>{seg.text}</span>
149
+ ),
150
+ )}
151
+ </BubbleContent>
152
+ )}
153
+ {attachedFiles.length > 0 && (
154
+ <div className="mt-1.5 flex flex-wrap gap-1">
155
+ {attachedFiles.map(({ filename }) => (
156
+ <span
157
+ key={filename}
158
+ className="inline-flex max-w-full items-center gap-1 rounded bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
159
+ >
160
+ <FileIcon className="size-3 shrink-0" />
161
+ <span className="truncate">{filename}</span>
162
+ </span>
163
+ ))}
164
+ </div>
165
+ )}
166
+ </Bubble>
167
+ )}
168
+ {/* Copy di kiri waktu (pesan user rata kanan). */}
169
+ <MessageFooter className="gap-1">
170
+ <CopyMessageButton text={text} />
171
+ <span className="text-muted-foreground/80">{formatTime(m.createdAt)}</span>
172
+ </MessageFooter>
173
+ <ImageLightbox images={lightboxImages} index={preview} onIndexChange={setPreview} />
174
+ </MessageContent>
175
+ </Message>
176
+ );
177
+ }