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,661 @@
1
+ /**
2
+ * Kartu konteks Session — keadaan kerja agent, sebagai kolom di kanan
3
+ * percakapan (desktop). Kolom ini ikut tata letak: chat menyempit saat kartu
4
+ * dibuka, sehingga pesan & input tidak pernah tertutup kartu.
5
+ *
6
+ * Susunan (dari yang paling sering dilirik ke yang paling jarang):
7
+ * - **Environment** : perubahan file (+/-), cabang git, status push/pull.
8
+ * - **Task list** : `todowrite` agent + progres "selesai / total".
9
+ * - **MCP** : server MCP; ringkasan "x/y connected" di judul. Yang
10
+ * gagal & tersambung tampil per baris; yang perlu login
11
+ * (mis. puluhan konektor claude.ai) dan yang tidak aktif
12
+ * DILIPAT jadi satu baris "N need sign-in" agar tidak
13
+ * menenggelamkan bagian lain.
14
+ * - **Reference** : file project yang DIBACA agent (bukan yang ditulis),
15
+ * terbaru di atas; 8 pertama tampil, sisanya "Show N more".
16
+ *
17
+ * Pola UX (gaya libadwaita):
18
+ * - Tiap bagian = judul yang bisa dilipat + ringkasan di kanan judul, isi
19
+ * berupa "boxed list" (kartu membulat, baris berpemisah) di atas latar
20
+ * kartu yang lebih gelap — seperti AdwPreferencesGroup.
21
+ * - Status diwakili titik berwarna + label, bukan warna saja (a11y).
22
+ * - Buka/tutup: `ContextPanelTrigger` (hamburger) HILANG saat kartu terbuka;
23
+ * penutupnya tombol X di kartu. Esc juga menutup.
24
+ * - Mobile: isi yang sama tampil sebagai bottom sheet (`ContextSheet`).
25
+ *
26
+ * Kartu ini hanya informasi (belum ada aksi git): barisnya tidak dibuat
27
+ * seperti tombol supaya tidak menyiratkan aksi yang belum ada.
28
+ */
29
+ import {
30
+ CheckIcon,
31
+ ChevronDownIcon,
32
+ CircleIcon,
33
+ FileDiffIcon,
34
+ FileTextIcon,
35
+ GitBranchIcon,
36
+ LoaderIcon,
37
+ MenuIcon,
38
+ PlugIcon,
39
+ UploadIcon,
40
+ XIcon,
41
+ } from "lucide-react";
42
+ import * as m from "motion/react-m";
43
+ import { type ReactNode, useEffect, useState } from "react";
44
+ import { Button } from "@/components/ui/button";
45
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
46
+ import {
47
+ Sheet,
48
+ SheetContent,
49
+ SheetDescription,
50
+ SheetHeader,
51
+ SheetTitle,
52
+ } from "@/components/ui/sheet";
53
+ import { Skeleton } from "@/components/ui/skeleton";
54
+ import { MCP_STATUS_DOT, MCP_STATUS_LABEL } from "@/lib/project-extensions";
55
+ import { baseName, type FileReference } from "@/lib/references";
56
+ import { cn } from "@/lib/utils";
57
+ import type { McpServerState, SessionContext, TodoItem } from "@/types";
58
+
59
+ // ----------------------------------------------------------- trigger ----
60
+
61
+ /** Tombol pembuka kartu — hanya dirender saat kartu tertutup. */
62
+ export function ContextPanelTrigger({ onOpen }: { onOpen: () => void }) {
63
+ return (
64
+ <Button
65
+ type="button"
66
+ variant="outline"
67
+ size="icon"
68
+ onClick={onOpen}
69
+ aria-label="Show session context"
70
+ title="Show context"
71
+ aria-expanded={false}
72
+ className="absolute end-3 top-2 z-30 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur-sm sm:end-4"
73
+ >
74
+ <MenuIcon />
75
+ </Button>
76
+ );
77
+ }
78
+
79
+ // -------------------------------------------------------------- card ----
80
+
81
+ export interface ContextPanelProps {
82
+ context: SessionContext | null;
83
+ references: FileReference[];
84
+ loading: boolean;
85
+ onClose: () => void;
86
+ }
87
+
88
+ /**
89
+ * Isi kartu (keempat bagian) — dipakai bersama oleh kartu desktop
90
+ * (`ContextPanel`) dan bottom sheet mobile (`ContextSheet`), jadi tampilan &
91
+ * perilakunya selalu identik.
92
+ */
93
+ function ContextSections({
94
+ context,
95
+ references,
96
+ loading,
97
+ /** Cadangkan ruang tombol X di judul bagian pertama (kartu desktop). */
98
+ reserveClose = false,
99
+ }: Omit<ContextPanelProps, "onClose"> & { reserveClose?: boolean }) {
100
+ if (loading && context === null) return <CardSkeleton />;
101
+ return (
102
+ <div className={cn("flex flex-col gap-4", reserveClose && "[&>*:first-child>button]:pe-7")}>
103
+ <EnvironmentSection context={context} />
104
+ <TaskSection todos={context?.todos ?? []} live={context?.live ?? false} />
105
+ <McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
106
+ <ReferenceSection references={references} />
107
+ </div>
108
+ );
109
+ }
110
+
111
+ /** Kartu desktop: kolom kanan, berdampingan dengan percakapan. */
112
+ export function ContextPanel({ context, references, loading, onClose }: ContextPanelProps) {
113
+ // Esc menutup kartu (pola sama dengan popover/sheet aplikasi).
114
+ useEffect(() => {
115
+ const onKey = (e: KeyboardEvent) => {
116
+ if (e.key === "Escape") onClose();
117
+ };
118
+ window.addEventListener("keydown", onKey);
119
+ return () => window.removeEventListener("keydown", onKey);
120
+ }, [onClose]);
121
+
122
+ return (
123
+ <m.aside
124
+ aria-label="Session context"
125
+ initial={{ opacity: 0, x: 10 }}
126
+ animate={{ opacity: 1, x: 0 }}
127
+ transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
128
+ // Latar kartu = latar jendela (lebih gelap), boxed list di atasnya
129
+ // memakai `bg-card` — kontras ala AdwPreferencesPage.
130
+ className="relative flex max-h-full flex-col overflow-hidden rounded-2xl border bg-background shadow-xs"
131
+ >
132
+ {/* Tanpa judul kartu: judul tiap bagian sudah menjadi penanda. Tombol
133
+ tutup mengapung di pojok kanan atas, sejajar judul bagian pertama. */}
134
+ <Button
135
+ type="button"
136
+ variant="ghost"
137
+ size="icon"
138
+ onClick={onClose}
139
+ aria-label="Hide session context"
140
+ title="Hide context (Esc)"
141
+ className="absolute end-1.5 top-1.5 z-10 size-6 rounded-full [&_svg]:size-3.5"
142
+ >
143
+ <XIcon />
144
+ </Button>
145
+
146
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2.5 pt-2 pb-3 [scrollbar-width:thin]">
147
+ <ContextSections context={context} references={references} loading={loading} reserveClose />
148
+ </div>
149
+ </m.aside>
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Bottom sheet mobile: isi sama dengan kartu desktop. Sheet sudah menangani
155
+ * overlay, drag handle, Esc, tap di luar, dan focus trap — jadi tidak perlu
156
+ * tombol X (pola sama dengan sheet aksi composer).
157
+ */
158
+ export function ContextSheet({
159
+ open,
160
+ onOpenChange,
161
+ context,
162
+ references,
163
+ loading,
164
+ }: Omit<ContextPanelProps, "onClose"> & {
165
+ open: boolean;
166
+ onOpenChange: (open: boolean) => void;
167
+ }) {
168
+ return (
169
+ <Sheet open={open} onOpenChange={onOpenChange}>
170
+ <SheetContent
171
+ side="bottom"
172
+ className="max-h-[80dvh] gap-0 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
173
+ >
174
+ <SheetHeader className="sr-only">
175
+ <SheetTitle>Session context</SheetTitle>
176
+ <SheetDescription>
177
+ Environment, MCP servers, task list, and files read by the agent.
178
+ </SheetDescription>
179
+ </SheetHeader>
180
+ <div className="min-h-0 overflow-y-auto overscroll-contain px-3 pt-2 pb-1">
181
+ <ContextSections context={context} references={references} loading={loading} />
182
+ </div>
183
+ </SheetContent>
184
+ </Sheet>
185
+ );
186
+ }
187
+
188
+ // ----------------------------------------------------------- sections ----
189
+
190
+ function EnvironmentSection({ context }: { context: SessionContext | null }) {
191
+ const changed = context?.changes ?? [];
192
+ const added = changed.reduce((n, c) => n + c.added, 0);
193
+ const removed = changed.reduce((n, c) => n + c.removed, 0);
194
+ const git = context?.git;
195
+
196
+ return (
197
+ <Section title="Environment">
198
+ <Box>
199
+ <Row
200
+ icon={<FileDiffIcon />}
201
+ label={
202
+ changed.length === 0
203
+ ? "No changes"
204
+ : `${changed.length} ${changed.length === 1 ? "file" : "files"} changed`
205
+ }
206
+ value={changed.length > 0 ? <DiffStat added={added} removed={removed} /> : undefined}
207
+ />
208
+ <Row
209
+ icon={<GitBranchIcon />}
210
+ label={git?.branch ?? "Not a git repository"}
211
+ muted={!git?.branch}
212
+ mono={Boolean(git?.branch)}
213
+ />
214
+ {git?.branch && (
215
+ <Row
216
+ icon={<UploadIcon />}
217
+ label={syncLabel(git.ahead, git.behind, git.hasRemote)}
218
+ muted
219
+ />
220
+ )}
221
+ </Box>
222
+ </Section>
223
+ );
224
+ }
225
+
226
+ /** Status yang "perlu login" — dilipat jadi satu baris bila lebih dari satu. */
227
+ const NEEDS_SIGN_IN: McpServerState["status"][] = ["needs_auth", "needs_client_registration"];
228
+
229
+ function McpSection({ servers, live }: { servers: McpServerState[]; live: boolean }) {
230
+ const connected = servers.filter((s) => s.status === "connected").length;
231
+ const failed = servers.filter((s) => s.status === "failed").length;
232
+ const byName = (a: McpServerState, b: McpServerState) => a.name.localeCompare(b.name);
233
+ const signIn = servers.filter((s) => NEEDS_SIGN_IN.includes(s.status)).sort(byName);
234
+ const inactive = servers
235
+ .filter((s) => s.status === "disabled" || s.status === "unknown")
236
+ .sort(byName);
237
+ // Baris individual: gagal (paling atas) lalu tersambung. Kelompok berisi
238
+ // satu server tetap tampil sebagai baris biasa (tidak perlu dilipat).
239
+ const individual = [
240
+ ...servers.filter((s) => s.status === "failed").sort(byName),
241
+ ...servers.filter((s) => s.status === "connected").sort(byName),
242
+ ...(signIn.length === 1 ? signIn : []),
243
+ ...(inactive.length === 1 ? inactive : []),
244
+ ];
245
+
246
+ return (
247
+ <Section
248
+ title="MCP"
249
+ summary={
250
+ servers.length > 0 ? (
251
+ // Hanya kegagalan yang diberi warna peringatan — konektor yang
252
+ // belum login itu normal, bukan masalah.
253
+ <span className={cn(failed > 0 && "text-amber-700 dark:text-amber-400")}>
254
+ {connected}/{servers.length} connected
255
+ </span>
256
+ ) : undefined
257
+ }
258
+ >
259
+ {servers.length === 0 ? (
260
+ <Empty>{live ? "No MCP servers configured" : "Start the session to see MCP servers"}</Empty>
261
+ ) : (
262
+ <Box>
263
+ {individual.map((server) => (
264
+ <McpRow key={server.name} server={server} />
265
+ ))}
266
+ {signIn.length > 1 && (
267
+ <McpGroupRow
268
+ label={`${signIn.length} need sign-in`}
269
+ dotClass={MCP_STATUS_DOT.needs_auth}
270
+ servers={signIn}
271
+ />
272
+ )}
273
+ {inactive.length > 1 && (
274
+ <McpGroupRow
275
+ label={`${inactive.length} inactive`}
276
+ dotClass={MCP_STATUS_DOT.disabled}
277
+ servers={inactive}
278
+ />
279
+ )}
280
+ </Box>
281
+ )}
282
+ </Section>
283
+ );
284
+ }
285
+
286
+ /** Satu server MCP: nama + status (titik + label); error di baris kedua. */
287
+ function McpRow({ server }: { server: McpServerState }) {
288
+ return (
289
+ <Row
290
+ icon={<PlugIcon />}
291
+ label={server.name}
292
+ hint={server.error ?? undefined}
293
+ hintTone={server.error ? "error" : undefined}
294
+ // Pesan error lengkap terbaca lewat `title` (baris kedua terpotong).
295
+ title={server.error ?? undefined}
296
+ value={<StatusText status={server.status} />}
297
+ />
298
+ );
299
+ }
300
+
301
+ function StatusText({ status }: { status: McpServerState["status"] }) {
302
+ return (
303
+ <span className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
304
+ <span aria-hidden className={cn("size-1.5 rounded-full", MCP_STATUS_DOT[status])} />
305
+ {MCP_STATUS_LABEL[status]}
306
+ </span>
307
+ );
308
+ }
309
+
310
+ /**
311
+ * Baris kelompok yang bisa dibuka (AdwExpanderRow): "17 need sign-in" —
312
+ * isinya nama server, tanpa status berulang (sudah di judul baris).
313
+ */
314
+ function McpGroupRow({
315
+ label,
316
+ dotClass,
317
+ servers,
318
+ }: {
319
+ label: string;
320
+ dotClass: string;
321
+ servers: McpServerState[];
322
+ }) {
323
+ const [open, setOpen] = useState(false);
324
+ return (
325
+ <li>
326
+ <button
327
+ type="button"
328
+ onClick={() => setOpen((v) => !v)}
329
+ aria-expanded={open}
330
+ className="flex min-h-9 w-full min-w-0 items-center gap-2 px-2.5 py-1.5 text-left text-[12.5px] outline-none transition-colors hover:bg-muted/50 focus-visible:bg-muted/50"
331
+ >
332
+ <span aria-hidden className="flex w-3.5 shrink-0 justify-center">
333
+ <span className={cn("size-1.5 rounded-full", dotClass)} />
334
+ </span>
335
+ <span className="min-w-0 flex-1 truncate leading-5 text-muted-foreground">{label}</span>
336
+ <ChevronDownIcon
337
+ aria-hidden
338
+ className={cn(
339
+ "size-3.5 shrink-0 text-muted-foreground transition-transform duration-200",
340
+ open && "rotate-180",
341
+ )}
342
+ />
343
+ </button>
344
+ {open && (
345
+ <ul className="border-t bg-muted/30 py-1">
346
+ {servers.map((s) => (
347
+ <li
348
+ key={s.name}
349
+ title={s.error ?? s.name}
350
+ className="truncate px-2.5 py-1 ps-8 text-[12px] leading-5 text-muted-foreground"
351
+ >
352
+ {s.name}
353
+ </li>
354
+ ))}
355
+ </ul>
356
+ )}
357
+ </li>
358
+ );
359
+ }
360
+
361
+ function TaskSection({ todos, live }: { todos: TodoItem[]; live: boolean }) {
362
+ const done = todos.filter((t) => t.status.toLowerCase() === "completed").length;
363
+ const active = todos.filter((t) => t.status.toLowerCase() !== "cancelled").length;
364
+
365
+ return (
366
+ <Section title="Task list" summary={todos.length > 0 ? `${done}/${active} done` : undefined}>
367
+ {todos.length === 0 ? (
368
+ <Empty>{live ? "The agent hasn't planned any tasks yet" : "Session is not running"}</Empty>
369
+ ) : (
370
+ <div className="flex flex-col gap-2">
371
+ {/* Progres: bar tipis — cepat dibaca tanpa menghitung item. */}
372
+ {active > 0 && (
373
+ <div
374
+ role="progressbar"
375
+ aria-label="Task progress"
376
+ aria-valuemin={0}
377
+ aria-valuemax={active}
378
+ aria-valuenow={done}
379
+ className="mx-1 h-1 overflow-hidden rounded-full bg-muted"
380
+ >
381
+ <div
382
+ className="h-full rounded-full bg-emerald-500 transition-[width] duration-500"
383
+ style={{ width: `${(done / active) * 100}%` }}
384
+ />
385
+ </div>
386
+ )}
387
+ <Box>
388
+ {todos.map((t, i) => (
389
+ // biome-ignore lint/suspicious/noArrayIndexKey: urutan = urutan task list agent
390
+ <TodoRow key={`${i}-${t.content}`} todo={t} />
391
+ ))}
392
+ </Box>
393
+ </div>
394
+ )}
395
+ </Section>
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Jumlah file yang langsung tampil. Sisanya di balik "Show N more" supaya
401
+ * Reference tidak mendorong bagian lain (Task list, MCP) keluar pandangan
402
+ * saat agent membaca puluhan file. Daftar sudah terbaru-di-atas, jadi yang
403
+ * tersembunyi adalah file yang paling lama tidak disentuh.
404
+ */
405
+ export const REFERENCE_PREVIEW = 8;
406
+
407
+ function ReferenceSection({ references }: { references: FileReference[] }) {
408
+ const [showAll, setShowAll] = useState(false);
409
+ const hidden = Math.max(0, references.length - REFERENCE_PREVIEW);
410
+ const visible = showAll ? references : references.slice(0, REFERENCE_PREVIEW);
411
+
412
+ return (
413
+ <Section
414
+ title="Reference"
415
+ summary={references.length > 0 ? String(references.length) : undefined}
416
+ >
417
+ {references.length === 0 ? (
418
+ <Empty>Files the agent reads will appear here</Empty>
419
+ ) : (
420
+ <>
421
+ <Box>
422
+ {visible.map((ref) => (
423
+ <Row
424
+ key={ref.path}
425
+ icon={<FileTextIcon />}
426
+ label={baseName(ref.path)}
427
+ // Folder induk sebagai konteks; path lengkap di `title`.
428
+ hint={parentDir(ref.path)}
429
+ title={ref.path}
430
+ />
431
+ ))}
432
+ </Box>
433
+ {hidden > 0 && (
434
+ <button
435
+ type="button"
436
+ onClick={() => setShowAll((v) => !v)}
437
+ aria-expanded={showAll}
438
+ className="mt-1.5 flex w-full items-center justify-center gap-1 rounded-lg py-1.5 text-[12px] text-muted-foreground outline-none transition-colors hover:bg-muted/50 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40"
439
+ >
440
+ {showAll ? "Show less" : `Show ${hidden} more`}
441
+ <ChevronDownIcon
442
+ aria-hidden
443
+ className={cn("size-3 transition-transform duration-200", showAll && "rotate-180")}
444
+ />
445
+ </button>
446
+ )}
447
+ </>
448
+ )}
449
+ </Section>
450
+ );
451
+ }
452
+
453
+ // ------------------------------------------------------------ helpers ----
454
+
455
+ /** Teks sinkronisasi cabang terhadap upstream. */
456
+ function syncLabel(ahead: number | null, behind: number | null, hasRemote: boolean): string {
457
+ if (!hasRemote) return "No remote";
458
+ if (ahead === null) return "Not pushed yet";
459
+ const pull = behind ?? 0;
460
+ if (ahead === 0 && pull === 0) return "Up to date";
461
+ const parts: string[] = [];
462
+ if (ahead > 0) parts.push(`${ahead} to push`);
463
+ if (pull > 0) parts.push(`${pull} to pull`);
464
+ return parts.join(" · ");
465
+ }
466
+
467
+ /** Folder induk dari path relatif ("src/lib/a.ts" -> "src/lib"). */
468
+ function parentDir(p: string): string | undefined {
469
+ const i = p.lastIndexOf("/");
470
+ return i > 0 ? p.slice(0, i) : undefined;
471
+ }
472
+
473
+ // ---------------------------------------------------------- primitives ----
474
+
475
+ /** Bagian yang bisa dilipat: judul + ringkasan di kanan judul. */
476
+ function Section({
477
+ title,
478
+ summary,
479
+ children,
480
+ }: {
481
+ title: string;
482
+ summary?: ReactNode;
483
+ children: ReactNode;
484
+ }) {
485
+ const [expanded, setExpanded] = useState(true);
486
+ return (
487
+ <Collapsible open={expanded} onOpenChange={setExpanded} className="flex flex-col gap-1.5">
488
+ <CollapsibleTrigger asChild>
489
+ <button
490
+ type="button"
491
+ className="group/section flex min-h-7 items-center gap-1 rounded-md px-1 text-left outline-none hover:bg-muted/50 focus-visible:ring-[3px] focus-visible:ring-ring/40"
492
+ >
493
+ <ChevronDownIcon
494
+ aria-hidden
495
+ className={cn(
496
+ "size-3 shrink-0 text-muted-foreground transition-transform duration-200",
497
+ !expanded && "-rotate-90",
498
+ )}
499
+ />
500
+ <span className="text-[13px] font-semibold tracking-tight text-foreground">{title}</span>
501
+ {summary !== undefined && (
502
+ <span className="ml-auto truncate ps-2 text-[11px] text-muted-foreground">
503
+ {summary}
504
+ </span>
505
+ )}
506
+ </button>
507
+ </CollapsibleTrigger>
508
+ <CollapsibleContent>{children}</CollapsibleContent>
509
+ </Collapsible>
510
+ );
511
+ }
512
+
513
+ /** Boxed list (libadwaita): kartu membulat, baris dipisah garis tipis. */
514
+ function Box({ children }: { children: ReactNode }) {
515
+ return (
516
+ <ul className="flex flex-col divide-y overflow-hidden rounded-xl border bg-card shadow-xs">
517
+ {children}
518
+ </ul>
519
+ );
520
+ }
521
+
522
+ /** Satu baris dalam `Box`: ikon + label (+ petunjuk kecil) + nilai di kanan. */
523
+ function Row({
524
+ icon,
525
+ label,
526
+ hint,
527
+ hintTone,
528
+ value,
529
+ muted,
530
+ mono,
531
+ title,
532
+ }: {
533
+ icon: ReactNode;
534
+ label: string;
535
+ hint?: string;
536
+ /** Warna baris kedua (mis. pesan error). */
537
+ hintTone?: "error";
538
+ value?: ReactNode;
539
+ muted?: boolean;
540
+ mono?: boolean;
541
+ title?: string;
542
+ }) {
543
+ return (
544
+ <li
545
+ title={title}
546
+ className="flex min-h-9 min-w-0 items-center gap-2 px-2.5 py-1.5 text-[12.5px]"
547
+ >
548
+ <span
549
+ aria-hidden
550
+ className="flex shrink-0 items-center text-muted-foreground [&>svg]:size-3.5"
551
+ >
552
+ {icon}
553
+ </span>
554
+ <span className="flex min-w-0 flex-1 flex-col">
555
+ <span
556
+ className={cn(
557
+ "truncate leading-5",
558
+ muted && "text-muted-foreground",
559
+ mono && "font-mono text-[12px]",
560
+ )}
561
+ >
562
+ {label}
563
+ </span>
564
+ {hint && (
565
+ <span
566
+ className={cn(
567
+ "truncate text-[11px] leading-4",
568
+ hintTone === "error" ? "text-destructive/90" : "text-muted-foreground",
569
+ )}
570
+ >
571
+ {hint}
572
+ </span>
573
+ )}
574
+ </span>
575
+ {value !== undefined && <span className="shrink-0">{value}</span>}
576
+ </li>
577
+ );
578
+ }
579
+
580
+ /** +tambah / -hapus berwarna. */
581
+ function DiffStat({ added, removed }: { added: number; removed: number }) {
582
+ return (
583
+ <span className="flex items-center gap-1.5 font-mono text-[11.5px] tabular-nums">
584
+ <span className="text-emerald-600 dark:text-emerald-400">+{added}</span>
585
+ <span className="text-destructive">-{removed}</span>
586
+ </span>
587
+ );
588
+ }
589
+
590
+ /** Label status task untuk pembaca layar. */
591
+ const TODO_STATUS_LABEL: Record<string, string> = {
592
+ completed: "Completed",
593
+ in_progress: "In progress",
594
+ pending: "Pending",
595
+ cancelled: "Cancelled",
596
+ };
597
+
598
+ /** Satu item task list: ikon status + teks (selesai dicoret). */
599
+ function TodoRow({ todo }: { todo: TodoItem }) {
600
+ const status = todo.status.toLowerCase();
601
+ const done = status === "completed";
602
+ const cancelled = status === "cancelled";
603
+ const running = status === "in_progress";
604
+
605
+ return (
606
+ <li className="flex min-w-0 items-start gap-2 px-2.5 py-2 text-[12.5px] leading-snug">
607
+ {/* Ikon status dekoratif; statusnya dibacakan lewat teks sr-only di
608
+ bawah (aria-label pada span/svg biasa tidak dibaca pembaca layar). */}
609
+ <span aria-hidden className="mt-0.5 flex shrink-0 items-center">
610
+ {done ? (
611
+ <span className="flex size-3.5 items-center justify-center rounded-full bg-emerald-500 text-white">
612
+ <CheckIcon className="size-2" strokeWidth={4} />
613
+ </span>
614
+ ) : running ? (
615
+ <LoaderIcon className="size-3.5 animate-spin text-sky-600 motion-reduce:animate-none dark:text-sky-400" />
616
+ ) : (
617
+ <CircleIcon className="size-3.5 text-muted-foreground/60" />
618
+ )}
619
+ </span>
620
+ <span className="sr-only">{TODO_STATUS_LABEL[status] ?? "Pending"}: </span>
621
+ <span
622
+ className={cn(
623
+ "min-w-0 break-words",
624
+ done && "text-muted-foreground line-through decoration-muted-foreground/50",
625
+ cancelled && "text-muted-foreground/60 line-through",
626
+ running && "font-medium",
627
+ )}
628
+ >
629
+ {todo.content}
630
+ </span>
631
+ </li>
632
+ );
633
+ }
634
+
635
+ /** Keadaan kosong — tetap di dalam boxed list agar bentuk bagian konsisten. */
636
+ function Empty({ children }: { children: ReactNode }) {
637
+ return (
638
+ <p className="rounded-xl border border-dashed px-2.5 py-2.5 text-[12px] leading-snug text-muted-foreground">
639
+ {children}
640
+ </p>
641
+ );
642
+ }
643
+
644
+ function CardSkeleton() {
645
+ return (
646
+ <div className="flex flex-col gap-3" aria-hidden>
647
+ {[3, 2, 2].map((rows, i) => (
648
+ // biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
649
+ <div key={i} className="flex flex-col gap-1.5">
650
+ <Skeleton className="mx-1 h-3.5 w-24" />
651
+ <div className="flex flex-col gap-1.5 px-1">
652
+ {Array.from({ length: rows }, (_, j) => (
653
+ // biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
654
+ <Skeleton key={j} className="h-6" />
655
+ ))}
656
+ </div>
657
+ </div>
658
+ ))}
659
+ </div>
660
+ );
661
+ }