kcg-code 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/README.md +228 -2
  2. package/bin/kcgcode.ts +13 -0
  3. package/bun-env.d.ts +19 -0
  4. package/kcg-code.config.example.json +3 -0
  5. package/package.json +83 -4
  6. package/public/logo.svg +1 -0
  7. package/public/manifest.json +18 -0
  8. package/public/sw.js +49 -0
  9. package/src/App.tsx +39 -0
  10. package/src/__tests__/manifest.test.ts +61 -0
  11. package/src/build-config.ts +9 -0
  12. package/src/cli/__tests__/dashboard.test.ts +62 -0
  13. package/src/cli/args.ts +123 -0
  14. package/src/cli/commands.ts +174 -0
  15. package/src/cli/dashboard.tsx +343 -0
  16. package/src/cli/index.ts +37 -0
  17. package/src/cli/theme.ts +100 -0
  18. package/src/components/auth/AuthGate.tsx +182 -0
  19. package/src/components/auth/AvatarEditor.tsx +236 -0
  20. package/src/components/auth/LockScreen.tsx +714 -0
  21. package/src/components/auth/ProfileAvatar.tsx +60 -0
  22. package/src/components/auth/WelcomeScreen.tsx +672 -0
  23. package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
  24. package/src/components/auth/avatar-icons.tsx +114 -0
  25. package/src/components/layout/AppShell.tsx +142 -0
  26. package/src/components/layout/AppSidebar.tsx +419 -0
  27. package/src/components/layout/SearchDialog.tsx +189 -0
  28. package/src/components/layout/SidebarAccount.tsx +111 -0
  29. package/src/components/layout/SidebarSessionMenu.tsx +163 -0
  30. package/src/components/layout/ThemeToggle.tsx +67 -0
  31. package/src/components/projects/FolderBrowser.tsx +142 -0
  32. package/src/components/projects/HomeChat.tsx +250 -0
  33. package/src/components/projects/NewProjectDialog.tsx +249 -0
  34. package/src/components/projects/PanelSection.tsx +102 -0
  35. package/src/components/projects/ProjectInstructions.tsx +229 -0
  36. package/src/components/projects/ProjectRow.tsx +58 -0
  37. package/src/components/projects/ProjectSidePanel.tsx +448 -0
  38. package/src/components/sessions/AgentModeList.tsx +265 -0
  39. package/src/components/sessions/AgentPicker.tsx +123 -0
  40. package/src/components/sessions/AssistantTurn.tsx +145 -0
  41. package/src/components/sessions/AttachmentTile.tsx +161 -0
  42. package/src/components/sessions/CodeBlock.tsx +107 -0
  43. package/src/components/sessions/ComposerActionSheet.tsx +96 -0
  44. package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
  45. package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
  46. package/src/components/sessions/ContextPanel.tsx +561 -0
  47. package/src/components/sessions/DropPlate.tsx +165 -0
  48. package/src/components/sessions/FileDropOverlay.tsx +232 -0
  49. package/src/components/sessions/FileMentionDropdown.tsx +47 -0
  50. package/src/components/sessions/ImageLightbox.tsx +173 -0
  51. package/src/components/sessions/MarkdownContent.tsx +187 -0
  52. package/src/components/sessions/Mascot.tsx +47 -0
  53. package/src/components/sessions/MessageActions.tsx +60 -0
  54. package/src/components/sessions/MessageRail.tsx +121 -0
  55. package/src/components/sessions/ModelPicker.tsx +209 -0
  56. package/src/components/sessions/ModelSearchList.tsx +151 -0
  57. package/src/components/sessions/NewSessionDialog.tsx +189 -0
  58. package/src/components/sessions/PendingAttachments.tsx +127 -0
  59. package/src/components/sessions/PromptCard.tsx +1085 -0
  60. package/src/components/sessions/PromptPanel.tsx +112 -0
  61. package/src/components/sessions/SessionComposer.tsx +640 -0
  62. package/src/components/sessions/SessionHeader.tsx +175 -0
  63. package/src/components/sessions/SessionList.tsx +396 -0
  64. package/src/components/sessions/SessionRow.tsx +137 -0
  65. package/src/components/sessions/SessionTimeline.tsx +275 -0
  66. package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
  67. package/src/components/sessions/SessionView.tsx +236 -0
  68. package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
  69. package/src/components/sessions/UserMessage.tsx +177 -0
  70. package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
  71. package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
  72. package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
  73. package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
  74. package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
  75. package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
  76. package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
  77. package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
  78. package/src/components/settings/AboutGroup.tsx +170 -0
  79. package/src/components/settings/AccountGroup.tsx +122 -0
  80. package/src/components/settings/DevicesGroup.tsx +151 -0
  81. package/src/components/settings/LhrRemoteAccess.tsx +262 -0
  82. package/src/components/settings/PasskeysGroup.tsx +551 -0
  83. package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
  84. package/src/components/settings/SecurityGroup.tsx +566 -0
  85. package/src/components/settings/WorkspaceGroup.tsx +370 -0
  86. package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
  87. package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
  88. package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
  89. package/src/components/settings/prefs.tsx +265 -0
  90. package/src/components/skills/InstallNotifications.tsx +325 -0
  91. package/src/components/skills/InstallSkillDialog.tsx +233 -0
  92. package/src/components/skills/SkillCards.tsx +210 -0
  93. package/src/components/skills/SkillsProjectPicker.tsx +178 -0
  94. package/src/components/ui/alert-dialog.tsx +179 -0
  95. package/src/components/ui/alert.tsx +60 -0
  96. package/src/components/ui/badge.tsx +46 -0
  97. package/src/components/ui/bubble.tsx +125 -0
  98. package/src/components/ui/button.tsx +62 -0
  99. package/src/components/ui/card.tsx +75 -0
  100. package/src/components/ui/collapsible.tsx +19 -0
  101. package/src/components/ui/dialog.tsx +142 -0
  102. package/src/components/ui/dropdown-menu.tsx +225 -0
  103. package/src/components/ui/empty.tsx +94 -0
  104. package/src/components/ui/field.tsx +234 -0
  105. package/src/components/ui/input-group.tsx +161 -0
  106. package/src/components/ui/input.tsx +21 -0
  107. package/src/components/ui/label.tsx +19 -0
  108. package/src/components/ui/message-scroller.tsx +131 -0
  109. package/src/components/ui/message.tsx +85 -0
  110. package/src/components/ui/popover.tsx +46 -0
  111. package/src/components/ui/select.tsx +172 -0
  112. package/src/components/ui/separator.tsx +28 -0
  113. package/src/components/ui/sheet.tsx +138 -0
  114. package/src/components/ui/sidebar.tsx +699 -0
  115. package/src/components/ui/skeleton.tsx +15 -0
  116. package/src/components/ui/sonner.tsx +45 -0
  117. package/src/components/ui/spinner.tsx +16 -0
  118. package/src/components/ui/switch.tsx +32 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/components/ui/tooltip.tsx +51 -0
  121. package/src/config.ts +85 -0
  122. package/src/db.ts +146 -0
  123. package/src/frontend.tsx +51 -0
  124. package/src/hooks/__tests__/useMention.test.ts +79 -0
  125. package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
  126. package/src/hooks/__tests__/useRouter.test.ts +17 -0
  127. package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
  128. package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
  129. package/src/hooks/__tests__/useTheme.test.ts +118 -0
  130. package/src/hooks/use-mobile.ts +16 -0
  131. package/src/hooks/useAgentPicker.ts +97 -0
  132. package/src/hooks/useMediaQuery.ts +49 -0
  133. package/src/hooks/useMention.ts +236 -0
  134. package/src/hooks/useProjects.ts +88 -0
  135. package/src/hooks/useResyncOnReconnect.ts +28 -0
  136. package/src/hooks/useRouter.ts +92 -0
  137. package/src/hooks/useSessionChat.ts +427 -0
  138. package/src/hooks/useSessionContext.ts +106 -0
  139. package/src/hooks/useSessionList.ts +234 -0
  140. package/src/hooks/useSidebarData.ts +108 -0
  141. package/src/hooks/useSmoothText.ts +122 -0
  142. package/src/hooks/useTheme.ts +164 -0
  143. package/src/hooks/useWebSocket.ts +196 -0
  144. package/src/index.css +27 -0
  145. package/src/index.html +14 -0
  146. package/src/index.ts +49 -0
  147. package/src/lib/__tests__/attachments.test.ts +58 -0
  148. package/src/lib/__tests__/avatar-presets.test.ts +25 -0
  149. package/src/lib/__tests__/collapsible.test.ts +101 -0
  150. package/src/lib/__tests__/composer.test.ts +118 -0
  151. package/src/lib/__tests__/highlight.test.ts +42 -0
  152. package/src/lib/__tests__/home-chat.test.ts +64 -0
  153. package/src/lib/__tests__/lock-screen.test.ts +64 -0
  154. package/src/lib/__tests__/model-picker.test.ts +205 -0
  155. package/src/lib/__tests__/onboarding.test.ts +41 -0
  156. package/src/lib/__tests__/permission-info.test.ts +61 -0
  157. package/src/lib/__tests__/project-extensions.test.ts +52 -0
  158. package/src/lib/__tests__/project-overview.test.ts +173 -0
  159. package/src/lib/__tests__/references.test.ts +121 -0
  160. package/src/lib/__tests__/routes.test.ts +134 -0
  161. package/src/lib/__tests__/session-summary.test.ts +115 -0
  162. package/src/lib/__tests__/session-title.test.ts +58 -0
  163. package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
  164. package/src/lib/__tests__/skill-installs.test.ts +134 -0
  165. package/src/lib/__tests__/skills-catalog.test.ts +96 -0
  166. package/src/lib/__tests__/tone.test.ts +37 -0
  167. package/src/lib/__tests__/turns-delta.test.ts +55 -0
  168. package/src/lib/api.ts +306 -0
  169. package/src/lib/attachments.ts +203 -0
  170. package/src/lib/auth.ts +92 -0
  171. package/src/lib/avatar-presets.ts +31 -0
  172. package/src/lib/collapsible.ts +75 -0
  173. package/src/lib/composer.ts +89 -0
  174. package/src/lib/data-events.ts +17 -0
  175. package/src/lib/headlines.ts +30 -0
  176. package/src/lib/highlight.ts +186 -0
  177. package/src/lib/layout.ts +20 -0
  178. package/src/lib/lock-screen.ts +54 -0
  179. package/src/lib/model-picker.ts +133 -0
  180. package/src/lib/onboarding.ts +38 -0
  181. package/src/lib/passkeys.ts +113 -0
  182. package/src/lib/pending-prompt.ts +53 -0
  183. package/src/lib/permission-info.ts +101 -0
  184. package/src/lib/project-extensions.ts +74 -0
  185. package/src/lib/project-overview.ts +117 -0
  186. package/src/lib/prompts.ts +87 -0
  187. package/src/lib/references.ts +106 -0
  188. package/src/lib/routes.ts +115 -0
  189. package/src/lib/session-handoff.ts +25 -0
  190. package/src/lib/session-status.ts +45 -0
  191. package/src/lib/session-summary.ts +68 -0
  192. package/src/lib/session-title.ts +63 -0
  193. package/src/lib/sidebar-groups.ts +78 -0
  194. package/src/lib/skill-installs.ts +360 -0
  195. package/src/lib/skills-catalog.ts +99 -0
  196. package/src/lib/time.ts +22 -0
  197. package/src/lib/tone.ts +80 -0
  198. package/src/lib/turns.ts +320 -0
  199. package/src/lib/utils.ts +6 -0
  200. package/src/logo.svg +31 -0
  201. package/src/pages/projects/ProjectDetailPage.tsx +130 -0
  202. package/src/pages/projects/ProjectsPage.tsx +97 -0
  203. package/src/pages/sessions/SessionPage.tsx +122 -0
  204. package/src/pages/settings/SettingsPage.tsx +299 -0
  205. package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
  206. package/src/pages/skills/SkillsPage.tsx +701 -0
  207. package/src/paths.ts +121 -0
  208. package/src/runtime.ts +24 -0
  209. package/src/server/__tests__/app.test.ts +1031 -0
  210. package/src/server/__tests__/auth.test.ts +370 -0
  211. package/src/server/__tests__/config.test.ts +101 -0
  212. package/src/server/__tests__/db.test.ts +485 -0
  213. package/src/server/__tests__/host.test.ts +14 -0
  214. package/src/server/__tests__/tunnel-config.test.ts +38 -0
  215. package/src/server/app.ts +415 -0
  216. package/src/server/db/auth.ts +311 -0
  217. package/src/server/db/messages.ts +50 -0
  218. package/src/server/db/projects.ts +106 -0
  219. package/src/server/db/prompts.ts +78 -0
  220. package/src/server/db/rows.ts +150 -0
  221. package/src/server/db/schema.ts +221 -0
  222. package/src/server/db/sessions.ts +239 -0
  223. package/src/server/db/tunnel.ts +124 -0
  224. package/src/server/host.ts +12 -0
  225. package/src/server/result.ts +15 -0
  226. package/src/server/routes/auth-guard.ts +156 -0
  227. package/src/server/routes/auth.routes.ts +287 -0
  228. package/src/server/routes/helpers.ts +165 -0
  229. package/src/server/routes/meta.routes.ts +24 -0
  230. package/src/server/routes/passkeys.routes.ts +161 -0
  231. package/src/server/routes/projects.routes.ts +157 -0
  232. package/src/server/routes/sessions.routes.ts +197 -0
  233. package/src/server/routes/skills.routes.ts +117 -0
  234. package/src/server/routes/tunnel.routes.ts +132 -0
  235. package/src/server/routes/types.ts +35 -0
  236. package/src/server/routes/uploads.routes.ts +54 -0
  237. package/src/server/routes/workspace.routes.ts +127 -0
  238. package/src/server/services/__tests__/app-info.test.ts +18 -0
  239. package/src/server/services/__tests__/attachments.test.ts +159 -0
  240. package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
  241. package/src/server/services/__tests__/auth.test.ts +341 -0
  242. package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
  243. package/src/server/services/__tests__/git-info.test.ts +74 -0
  244. package/src/server/services/__tests__/opencode.test.ts +320 -0
  245. package/src/server/services/__tests__/project-manager.test.ts +190 -0
  246. package/src/server/services/__tests__/sandbox.test.ts +141 -0
  247. package/src/server/services/__tests__/session-manager.test.ts +3469 -0
  248. package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
  249. package/src/server/services/__tests__/skills-registry.test.ts +359 -0
  250. package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
  251. package/src/server/services/__tests__/tunnel.test.ts +367 -0
  252. package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
  253. package/src/server/services/__tests__/workspace.test.ts +249 -0
  254. package/src/server/services/app-info.ts +29 -0
  255. package/src/server/services/attachments.ts +193 -0
  256. package/src/server/services/auth-passkeys.ts +459 -0
  257. package/src/server/services/auth.ts +633 -0
  258. package/src/server/services/frpc-installer.ts +183 -0
  259. package/src/server/services/git-info.ts +87 -0
  260. package/src/server/services/opencode-client.ts +714 -0
  261. package/src/server/services/opencode-server.ts +319 -0
  262. package/src/server/services/opencode-sse.ts +82 -0
  263. package/src/server/services/project-manager.ts +147 -0
  264. package/src/server/services/sandbox.ts +83 -0
  265. package/src/server/services/session-events.ts +256 -0
  266. package/src/server/services/session-manager.ts +1374 -0
  267. package/src/server/services/skill-install-jobs.ts +262 -0
  268. package/src/server/services/skills-registry.ts +468 -0
  269. package/src/server/services/tunnel-api.ts +207 -0
  270. package/src/server/services/tunnel-config.ts +45 -0
  271. package/src/server/services/tunnel-lhr.ts +387 -0
  272. package/src/server/services/tunnel.ts +633 -0
  273. package/src/server/services/turn-stream.ts +339 -0
  274. package/src/server/services/websocket-gateway.ts +374 -0
  275. package/src/server/services/workspace.ts +223 -0
  276. package/src/server/services/ws-transport.ts +121 -0
  277. package/src/types/context.ts +69 -0
  278. package/src/types/index.ts +13 -0
  279. package/src/types/message.ts +58 -0
  280. package/src/types/project.ts +16 -0
  281. package/src/types/prompt.ts +118 -0
  282. package/src/types/session.ts +52 -0
  283. package/src/types/turns.ts +47 -0
  284. package/src/ws-protocol.ts +93 -0
  285. package/styles/globals.css +311 -0
  286. package/tsconfig.json +37 -0
@@ -0,0 +1,561 @@
1
+ /**
2
+ * Kartu konteks Session — keadaan kerja agent, MENGAPUNG di margin kanan.
3
+ * Tidak mengambil ruang layout, jadi percakapan & input tidak bergeser saat
4
+ * kartu dibuka/ditutup.
5
+ *
6
+ * Susunan (dari yang paling sering dilirik ke yang paling jarang):
7
+ * - **Environment** : perubahan file (+/-), cabang git, status push/pull.
8
+ * - **MCP** : server MCP Project; ringkasan "x of y connected" di
9
+ * judul supaya status terbaca tanpa membuka bagiannya.
10
+ * - **Task list** : `todowrite` agent + progres "selesai / total".
11
+ * - **Reference** : file project yang DIBACA agent (bukan yang ditulis),
12
+ * terbaru di atas; 8 pertama tampil, sisanya "Show N more".
13
+ *
14
+ * Pola UX:
15
+ * - Tiap bagian = judul yang bisa dilipat + ringkasan di kanan judul, jadi
16
+ * kartu tetap informatif walau semua bagian dilipat.
17
+ * - Tanpa judul kartu & tanpa kotak di dalam: judul tiap bagian (warna
18
+ * solid) menjadi penanda, dipisah garis tipis antar bagian.
19
+ * - Status diwakili titik berwarna + label, bukan warna saja (a11y).
20
+ * - Buka/tutup: `ContextPanelTrigger` (hamburger) HILANG saat kartu terbuka;
21
+ * penutupnya tombol X di kartu. Esc juga menutup.
22
+ * - Mobile: isi yang sama tampil sebagai bottom sheet (`ContextSheet`).
23
+ *
24
+ * Kartu ini hanya informasi (belum ada aksi git): barisnya tidak dibuat
25
+ * seperti tombol supaya tidak menyiratkan aksi yang belum ada.
26
+ */
27
+ import {
28
+ CheckIcon,
29
+ ChevronDownIcon,
30
+ CircleIcon,
31
+ FileDiffIcon,
32
+ FileTextIcon,
33
+ GitBranchIcon,
34
+ LoaderIcon,
35
+ MenuIcon,
36
+ PlugIcon,
37
+ UploadIcon,
38
+ XIcon,
39
+ } from "lucide-react";
40
+ import * as m from "motion/react-m";
41
+ import { type ReactNode, useEffect, useState } from "react";
42
+ import { Button } from "@/components/ui/button";
43
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
44
+ import {
45
+ Sheet,
46
+ SheetContent,
47
+ SheetDescription,
48
+ SheetHeader,
49
+ SheetTitle,
50
+ } from "@/components/ui/sheet";
51
+ import { Skeleton } from "@/components/ui/skeleton";
52
+ import { MCP_STATUS_DOT, MCP_STATUS_LABEL } from "@/lib/project-extensions";
53
+ import { baseName, type FileReference } from "@/lib/references";
54
+ import { cn } from "@/lib/utils";
55
+ import type { McpServerState, SessionContext, TodoItem } from "@/types";
56
+
57
+ // ----------------------------------------------------------- trigger ----
58
+
59
+ /** Tombol pembuka kartu — hanya dirender saat kartu tertutup. */
60
+ export function ContextPanelTrigger({ onOpen }: { onOpen: () => void }) {
61
+ return (
62
+ <Button
63
+ type="button"
64
+ variant="outline"
65
+ size="icon"
66
+ onClick={onOpen}
67
+ aria-label="Show session context"
68
+ title="Show context"
69
+ aria-expanded={false}
70
+ className="absolute end-3 top-2 z-30 size-9 rounded-full bg-background/80 shadow-sm backdrop-blur-sm sm:end-4"
71
+ >
72
+ <MenuIcon />
73
+ </Button>
74
+ );
75
+ }
76
+
77
+ // -------------------------------------------------------------- card ----
78
+
79
+ export interface ContextPanelProps {
80
+ context: SessionContext | null;
81
+ references: FileReference[];
82
+ loading: boolean;
83
+ onClose: () => void;
84
+ }
85
+
86
+ /**
87
+ * Isi kartu (keempat bagian) — dipakai bersama oleh kartu desktop
88
+ * (`ContextPanel`) dan bottom sheet mobile (`ContextSheet`), jadi tampilan &
89
+ * perilakunya selalu identik.
90
+ */
91
+ function ContextSections({
92
+ context,
93
+ references,
94
+ loading,
95
+ /** Cadangkan ruang tombol X di judul bagian pertama (kartu desktop). */
96
+ reserveClose = false,
97
+ }: Omit<ContextPanelProps, "onClose"> & { reserveClose?: boolean }) {
98
+ if (loading && context === null) return <CardSkeleton />;
99
+ return (
100
+ <div
101
+ className={cn(
102
+ "flex flex-col divide-y [&>*]:py-2.5 [&>*:first-child]:pt-0 [&>*:last-child]:pb-0.5",
103
+ reserveClose && "[&>*:first-child>button]:pe-7",
104
+ )}
105
+ >
106
+ <EnvironmentSection context={context} />
107
+ <McpSection servers={context?.mcp ?? []} live={context?.live ?? false} />
108
+ <TaskSection todos={context?.todos ?? []} live={context?.live ?? false} />
109
+ <ReferenceSection references={references} />
110
+ </div>
111
+ );
112
+ }
113
+
114
+ /** Kartu desktop: mengapung di margin kanan. */
115
+ export function ContextPanel({ context, references, loading, onClose }: ContextPanelProps) {
116
+ // Esc menutup kartu (pola sama dengan popover/sheet aplikasi).
117
+ useEffect(() => {
118
+ const onKey = (e: KeyboardEvent) => {
119
+ if (e.key === "Escape") onClose();
120
+ };
121
+ window.addEventListener("keydown", onKey);
122
+ return () => window.removeEventListener("keydown", onKey);
123
+ }, [onClose]);
124
+
125
+ return (
126
+ <m.aside
127
+ aria-label="Session context"
128
+ initial={{ opacity: 0, x: 10 }}
129
+ animate={{ opacity: 1, x: 0 }}
130
+ transition={{ type: "spring", visualDuration: 0.25, bounce: 0.1 }}
131
+ className="pointer-events-auto relative flex max-h-full flex-col overflow-hidden rounded-2xl border bg-popover shadow-lg"
132
+ >
133
+ {/* Tanpa judul kartu: judul tiap bagian sudah menjadi penanda. Tombol
134
+ tutup mengapung di pojok kanan atas, sejajar judul bagian pertama. */}
135
+ <Button
136
+ type="button"
137
+ variant="ghost"
138
+ size="icon"
139
+ onClick={onClose}
140
+ aria-label="Hide session context"
141
+ title="Hide context (Esc)"
142
+ className="absolute end-1.5 top-1.5 z-10 size-6 rounded-full [&_svg]:size-3.5"
143
+ >
144
+ <XIcon />
145
+ </Button>
146
+
147
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-2 pt-1.5 pb-2 [scrollbar-width:thin]">
148
+ <ContextSections context={context} references={references} loading={loading} reserveClose />
149
+ </div>
150
+ </m.aside>
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Bottom sheet mobile: isi sama dengan kartu desktop. Sheet sudah menangani
156
+ * overlay, drag handle, Esc, tap di luar, dan focus trap — jadi tidak perlu
157
+ * tombol X (pola sama dengan sheet aksi composer).
158
+ */
159
+ export function ContextSheet({
160
+ open,
161
+ onOpenChange,
162
+ context,
163
+ references,
164
+ loading,
165
+ }: Omit<ContextPanelProps, "onClose"> & {
166
+ open: boolean;
167
+ onOpenChange: (open: boolean) => void;
168
+ }) {
169
+ return (
170
+ <Sheet open={open} onOpenChange={onOpenChange}>
171
+ <SheetContent
172
+ side="bottom"
173
+ className="max-h-[80dvh] gap-0 pb-[max(0.75rem,env(safe-area-inset-bottom))]"
174
+ >
175
+ <SheetHeader className="sr-only">
176
+ <SheetTitle>Session context</SheetTitle>
177
+ <SheetDescription>
178
+ Environment, MCP servers, task list, and files read by the agent.
179
+ </SheetDescription>
180
+ </SheetHeader>
181
+ <div className="min-h-0 overflow-y-auto overscroll-contain px-3 pt-2 pb-1">
182
+ <ContextSections context={context} references={references} loading={loading} />
183
+ </div>
184
+ </SheetContent>
185
+ </Sheet>
186
+ );
187
+ }
188
+
189
+ // ----------------------------------------------------------- sections ----
190
+
191
+ function EnvironmentSection({ context }: { context: SessionContext | null }) {
192
+ const changed = context?.changes ?? [];
193
+ const added = changed.reduce((n, c) => n + c.added, 0);
194
+ const removed = changed.reduce((n, c) => n + c.removed, 0);
195
+ const git = context?.git;
196
+
197
+ return (
198
+ <Section title="Environment">
199
+ <Box>
200
+ <Row
201
+ icon={<FileDiffIcon />}
202
+ label={
203
+ changed.length === 0
204
+ ? "No changes"
205
+ : `${changed.length} ${changed.length === 1 ? "file" : "files"} changed`
206
+ }
207
+ value={changed.length > 0 ? <DiffStat added={added} removed={removed} /> : undefined}
208
+ />
209
+ <Row
210
+ icon={<GitBranchIcon />}
211
+ label={git?.branch ?? "Not a git repository"}
212
+ muted={!git?.branch}
213
+ mono={Boolean(git?.branch)}
214
+ />
215
+ {git?.branch && (
216
+ <Row
217
+ icon={<UploadIcon />}
218
+ label={syncLabel(git.ahead, git.behind, git.hasRemote)}
219
+ muted
220
+ />
221
+ )}
222
+ </Box>
223
+ </Section>
224
+ );
225
+ }
226
+
227
+ /** Urutan tampil MCP: yang bermasalah dulu agar langsung terlihat. */
228
+ const MCP_ORDER: Record<McpServerState["status"], number> = {
229
+ failed: 0,
230
+ needs_auth: 1,
231
+ needs_client_registration: 2,
232
+ connected: 3,
233
+ unknown: 4,
234
+ disabled: 5,
235
+ };
236
+
237
+ function McpSection({ servers, live }: { servers: McpServerState[]; live: boolean }) {
238
+ const connected = servers.filter((s) => s.status === "connected").length;
239
+ const hasProblem = servers.some((s) => MCP_ORDER[s.status] <= 2);
240
+ const sorted = [...servers].sort(
241
+ (a, b) => MCP_ORDER[a.status] - MCP_ORDER[b.status] || a.name.localeCompare(b.name),
242
+ );
243
+
244
+ return (
245
+ <Section
246
+ title="MCP"
247
+ summary={
248
+ servers.length > 0 ? (
249
+ <span className={cn(hasProblem && "text-amber-700 dark:text-amber-400")}>
250
+ {connected}/{servers.length} connected
251
+ </span>
252
+ ) : undefined
253
+ }
254
+ >
255
+ {servers.length === 0 ? (
256
+ <Empty>{live ? "No MCP servers configured" : "Start the session to see MCP servers"}</Empty>
257
+ ) : (
258
+ <Box>
259
+ {sorted.map((server) => (
260
+ <Row
261
+ key={server.name}
262
+ icon={<PlugIcon />}
263
+ label={server.name}
264
+ // Pesan error lengkap terbaca lewat `title` (tanpa memanjangkan baris).
265
+ title={server.error ?? undefined}
266
+ value={
267
+ <span className="flex items-center gap-1.5 text-[11.5px] text-muted-foreground">
268
+ <span
269
+ aria-hidden
270
+ className={cn("size-1.5 rounded-full", MCP_STATUS_DOT[server.status])}
271
+ />
272
+ {MCP_STATUS_LABEL[server.status]}
273
+ </span>
274
+ }
275
+ />
276
+ ))}
277
+ </Box>
278
+ )}
279
+ </Section>
280
+ );
281
+ }
282
+
283
+ function TaskSection({ todos, live }: { todos: TodoItem[]; live: boolean }) {
284
+ const done = todos.filter((t) => t.status.toLowerCase() === "completed").length;
285
+ const active = todos.filter((t) => t.status.toLowerCase() !== "cancelled").length;
286
+
287
+ return (
288
+ <Section title="Task list" summary={todos.length > 0 ? `${done}/${active} done` : undefined}>
289
+ {todos.length === 0 ? (
290
+ <Empty>{live ? "The agent hasn't planned any tasks yet" : "Session is not running"}</Empty>
291
+ ) : (
292
+ <div className="flex flex-col gap-2">
293
+ {/* Progres: bar tipis — cepat dibaca tanpa menghitung item. */}
294
+ {active > 0 && (
295
+ <div
296
+ role="progressbar"
297
+ aria-label="Task progress"
298
+ aria-valuemin={0}
299
+ aria-valuemax={active}
300
+ aria-valuenow={done}
301
+ className="mx-1 h-1 overflow-hidden rounded-full bg-muted"
302
+ >
303
+ <div
304
+ className="h-full rounded-full bg-emerald-500 transition-[width] duration-500"
305
+ style={{ width: `${(done / active) * 100}%` }}
306
+ />
307
+ </div>
308
+ )}
309
+ <Box>
310
+ {todos.map((t, i) => (
311
+ // biome-ignore lint/suspicious/noArrayIndexKey: urutan = urutan task list agent
312
+ <TodoRow key={`${i}-${t.content}`} todo={t} />
313
+ ))}
314
+ </Box>
315
+ </div>
316
+ )}
317
+ </Section>
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Jumlah file yang langsung tampil. Sisanya di balik "Show N more" supaya
323
+ * Reference tidak mendorong bagian lain (Task list, MCP) keluar pandangan
324
+ * saat agent membaca puluhan file. Daftar sudah terbaru-di-atas, jadi yang
325
+ * tersembunyi adalah file yang paling lama tidak disentuh.
326
+ */
327
+ export const REFERENCE_PREVIEW = 8;
328
+
329
+ function ReferenceSection({ references }: { references: FileReference[] }) {
330
+ const [showAll, setShowAll] = useState(false);
331
+ const hidden = Math.max(0, references.length - REFERENCE_PREVIEW);
332
+ const visible = showAll ? references : references.slice(0, REFERENCE_PREVIEW);
333
+
334
+ return (
335
+ <Section
336
+ title="Reference"
337
+ summary={references.length > 0 ? String(references.length) : undefined}
338
+ >
339
+ {references.length === 0 ? (
340
+ <Empty>Files the agent reads will appear here</Empty>
341
+ ) : (
342
+ <>
343
+ <Box>
344
+ {visible.map((ref) => (
345
+ <Row
346
+ key={ref.path}
347
+ icon={<FileTextIcon />}
348
+ label={baseName(ref.path)}
349
+ // Folder induk sebagai konteks; path lengkap di `title`.
350
+ hint={parentDir(ref.path)}
351
+ title={ref.path}
352
+ />
353
+ ))}
354
+ </Box>
355
+ {hidden > 0 && (
356
+ <button
357
+ type="button"
358
+ onClick={() => setShowAll((v) => !v)}
359
+ aria-expanded={showAll}
360
+ className="mt-0.5 flex w-full items-center gap-1 rounded-md px-1 py-1 text-left text-[12px] text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/40"
361
+ >
362
+ <ChevronDownIcon
363
+ aria-hidden
364
+ className={cn("size-3 transition-transform duration-200", showAll && "rotate-180")}
365
+ />
366
+ {showAll ? "Show less" : `Show ${hidden} more`}
367
+ </button>
368
+ )}
369
+ </>
370
+ )}
371
+ </Section>
372
+ );
373
+ }
374
+
375
+ // ------------------------------------------------------------ helpers ----
376
+
377
+ /** Teks sinkronisasi cabang terhadap upstream. */
378
+ function syncLabel(ahead: number | null, behind: number | null, hasRemote: boolean): string {
379
+ if (!hasRemote) return "No remote";
380
+ if (ahead === null) return "Not pushed yet";
381
+ const pull = behind ?? 0;
382
+ if (ahead === 0 && pull === 0) return "Up to date";
383
+ const parts: string[] = [];
384
+ if (ahead > 0) parts.push(`${ahead} to push`);
385
+ if (pull > 0) parts.push(`${pull} to pull`);
386
+ return parts.join(" · ");
387
+ }
388
+
389
+ /** Folder induk dari path relatif ("src/lib/a.ts" -> "src/lib"). */
390
+ function parentDir(p: string): string | undefined {
391
+ const i = p.lastIndexOf("/");
392
+ return i > 0 ? p.slice(0, i) : undefined;
393
+ }
394
+
395
+ // ---------------------------------------------------------- primitives ----
396
+
397
+ /** Bagian yang bisa dilipat: judul + ringkasan di kanan judul. */
398
+ function Section({
399
+ title,
400
+ summary,
401
+ children,
402
+ }: {
403
+ title: string;
404
+ summary?: ReactNode;
405
+ children: ReactNode;
406
+ }) {
407
+ const [expanded, setExpanded] = useState(true);
408
+ return (
409
+ <Collapsible open={expanded} onOpenChange={setExpanded} className="flex flex-col gap-1.5">
410
+ <CollapsibleTrigger asChild>
411
+ <button
412
+ type="button"
413
+ 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"
414
+ >
415
+ <ChevronDownIcon
416
+ aria-hidden
417
+ className={cn(
418
+ "size-3 shrink-0 text-muted-foreground transition-transform duration-200",
419
+ !expanded && "-rotate-90",
420
+ )}
421
+ />
422
+ <span className="text-[13px] font-semibold tracking-tight text-foreground">{title}</span>
423
+ {summary !== undefined && (
424
+ <span className="ml-auto truncate ps-2 text-[11px] text-muted-foreground">
425
+ {summary}
426
+ </span>
427
+ )}
428
+ </button>
429
+ </CollapsibleTrigger>
430
+ <CollapsibleContent>{children}</CollapsibleContent>
431
+ </Collapsible>
432
+ );
433
+ }
434
+
435
+ /** Daftar baris polos (tanpa kotak/garis — hemat ruang). */
436
+ function Box({ children }: { children: ReactNode }) {
437
+ return <ul className="flex flex-col">{children}</ul>;
438
+ }
439
+
440
+ /** Satu baris dalam `Box`: ikon + label (+ petunjuk kecil) + nilai di kanan. */
441
+ function Row({
442
+ icon,
443
+ label,
444
+ hint,
445
+ value,
446
+ muted,
447
+ mono,
448
+ title,
449
+ }: {
450
+ icon: ReactNode;
451
+ label: string;
452
+ hint?: string;
453
+ value?: ReactNode;
454
+ muted?: boolean;
455
+ mono?: boolean;
456
+ title?: string;
457
+ }) {
458
+ return (
459
+ <li title={title} className="flex min-h-8 min-w-0 items-center gap-2 px-1 py-1 text-[12.5px]">
460
+ <span
461
+ aria-hidden
462
+ className="flex shrink-0 items-center text-muted-foreground [&>svg]:size-3.5"
463
+ >
464
+ {icon}
465
+ </span>
466
+ <span className="flex min-w-0 flex-1 flex-col">
467
+ <span
468
+ className={cn(
469
+ "truncate leading-5",
470
+ muted && "text-muted-foreground",
471
+ mono && "font-mono text-[12px]",
472
+ )}
473
+ >
474
+ {label}
475
+ </span>
476
+ {hint && (
477
+ <span className="truncate text-[11px] leading-4 text-muted-foreground">{hint}</span>
478
+ )}
479
+ </span>
480
+ {value !== undefined && <span className="shrink-0">{value}</span>}
481
+ </li>
482
+ );
483
+ }
484
+
485
+ /** +tambah / -hapus berwarna. */
486
+ function DiffStat({ added, removed }: { added: number; removed: number }) {
487
+ return (
488
+ <span className="flex items-center gap-1.5 font-mono text-[11.5px] tabular-nums">
489
+ <span className="text-emerald-600 dark:text-emerald-400">+{added}</span>
490
+ <span className="text-destructive">-{removed}</span>
491
+ </span>
492
+ );
493
+ }
494
+
495
+ /** Label status task untuk pembaca layar. */
496
+ const TODO_STATUS_LABEL: Record<string, string> = {
497
+ completed: "Completed",
498
+ in_progress: "In progress",
499
+ pending: "Pending",
500
+ cancelled: "Cancelled",
501
+ };
502
+
503
+ /** Satu item task list: ikon status + teks (selesai dicoret). */
504
+ function TodoRow({ todo }: { todo: TodoItem }) {
505
+ const status = todo.status.toLowerCase();
506
+ const done = status === "completed";
507
+ const cancelled = status === "cancelled";
508
+ const running = status === "in_progress";
509
+
510
+ return (
511
+ <li className="flex min-w-0 items-start gap-2 px-1 py-1 text-[12.5px] leading-snug">
512
+ {/* Ikon status dekoratif; statusnya dibacakan lewat teks sr-only di
513
+ bawah (aria-label pada span/svg biasa tidak dibaca pembaca layar). */}
514
+ <span aria-hidden className="mt-0.5 flex shrink-0 items-center">
515
+ {done ? (
516
+ <span className="flex size-3.5 items-center justify-center rounded-full bg-emerald-500 text-white">
517
+ <CheckIcon className="size-2" strokeWidth={4} />
518
+ </span>
519
+ ) : running ? (
520
+ <LoaderIcon className="size-3.5 animate-spin text-sky-600 motion-reduce:animate-none dark:text-sky-400" />
521
+ ) : (
522
+ <CircleIcon className="size-3.5 text-muted-foreground/60" />
523
+ )}
524
+ </span>
525
+ <span className="sr-only">{TODO_STATUS_LABEL[status] ?? "Pending"}: </span>
526
+ <span
527
+ className={cn(
528
+ "min-w-0 break-words",
529
+ done && "text-muted-foreground line-through decoration-muted-foreground/50",
530
+ cancelled && "text-muted-foreground/60 line-through",
531
+ running && "font-medium",
532
+ )}
533
+ >
534
+ {todo.content}
535
+ </span>
536
+ </li>
537
+ );
538
+ }
539
+
540
+ function Empty({ children }: { children: ReactNode }) {
541
+ return <p className="px-1 py-0.5 text-[12px] leading-snug text-muted-foreground">{children}</p>;
542
+ }
543
+
544
+ function CardSkeleton() {
545
+ return (
546
+ <div className="flex flex-col gap-3" aria-hidden>
547
+ {[3, 2, 2].map((rows, i) => (
548
+ // biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
549
+ <div key={i} className="flex flex-col gap-1.5">
550
+ <Skeleton className="mx-1 h-3.5 w-24" />
551
+ <div className="flex flex-col gap-1.5 px-1">
552
+ {Array.from({ length: rows }, (_, j) => (
553
+ // biome-ignore lint/suspicious/noArrayIndexKey: kerangka statis
554
+ <Skeleton key={j} className="h-6" />
555
+ ))}
556
+ </div>
557
+ </div>
558
+ ))}
559
+ </div>
560
+ );
561
+ }