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,419 @@
1
+ /**
2
+ * Sidebar aplikasi: seluruh Session digroup per Project (shadcn `sidebar`).
3
+ *
4
+ * - Desktop (>= md): panel tetap di kiri, bisa diciutkan (tombol trigger /
5
+ * Ctrl+B). Mobile: Sheet off-canvas yang otomatis tertutup setelah memilih
6
+ * Session agar layar penuh kembali untuk chat.
7
+ * - Tiap Project = Collapsible berisi daftar Session (running di atas).
8
+ * Klik sekali = expand/collapse; dobel klik (mouse) = buka halaman Project.
9
+ * Layar sentuh & keyboard tidak punya dobel klik, jadi baris terakhir tiap
10
+ * Project selalu berupa tautan ke halaman Project ("Show all (n)" bila ada
11
+ * Session tersembunyi, selain itu "Project details").
12
+ * - Navigasi utama di atas daftar: "New session" (homepage/composer chat),
13
+ * "Search" (command palette `SearchDialog`, juga Ctrl/⌘+K), dan "Skills"
14
+ * (`/skills`).
15
+ * - Desktop: hover baris Session memunculkan menu "..." (Start/Stop/
16
+ * Restart, Delete) — `SidebarSessionMenu`. HP tidak (tanpa hover).
17
+ * - Footer: pemilih tema (Light / Dark / System), lalu profil pemilik +
18
+ * tombol Settings & Lock (`SidebarAccount`).
19
+ */
20
+ import {
21
+ ChevronRightIcon,
22
+ FolderIcon,
23
+ FolderOpenIcon,
24
+ MessageCirclePlusIcon,
25
+ RefreshCwIcon,
26
+ SearchIcon,
27
+ SparklesIcon,
28
+ } from "lucide-react";
29
+ import type * as React from "react";
30
+ import { useEffect, useRef, useState } from "react";
31
+ import { SearchDialog } from "@/components/layout/SearchDialog";
32
+ import { SidebarAccount } from "@/components/layout/SidebarAccount";
33
+ import { SidebarSessionMenu } from "@/components/layout/SidebarSessionMenu";
34
+ import { ThemeToggle } from "@/components/layout/ThemeToggle";
35
+ import { Button } from "@/components/ui/button";
36
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
37
+ import {
38
+ Sidebar,
39
+ SidebarContent,
40
+ SidebarFooter,
41
+ SidebarGroup,
42
+ SidebarGroupContent,
43
+ SidebarGroupLabel,
44
+ SidebarHeader,
45
+ SidebarMenu,
46
+ SidebarMenuBadge,
47
+ SidebarMenuButton,
48
+ SidebarMenuItem,
49
+ SidebarMenuSkeleton,
50
+ SidebarMenuSub,
51
+ SidebarMenuSubButton,
52
+ SidebarMenuSubItem,
53
+ SidebarRail,
54
+ useSidebar,
55
+ } from "@/components/ui/sidebar";
56
+ import { useRouter } from "@/hooks/useRouter";
57
+ import type { UseSidebarDataResult } from "@/hooks/useSidebarData";
58
+ import { type AppRoute, projectPath, projectsPath, sessionPath, skillsPath } from "@/lib/routes";
59
+ import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
60
+ import { displaySessionTitle } from "@/lib/session-title";
61
+ import type { SidebarProjectGroup } from "@/lib/sidebar-groups";
62
+ import { cn } from "@/lib/utils";
63
+
64
+ import logo from "../../logo.svg";
65
+
66
+ /** Indentasi kiri tombol navigasi utama (New session / Search / Skills). */
67
+ const NAV_BUTTON_CLASS = "pl-4";
68
+
69
+ /** Batas Session per Project yang tampil sebelum tautan "Show all". */
70
+ const MAX_VISIBLE_SESSIONS = 5;
71
+
72
+ export interface AppSidebarProps {
73
+ route: AppRoute;
74
+ data: UseSidebarDataResult;
75
+ }
76
+
77
+ export function AppSidebar({ route, data }: AppSidebarProps) {
78
+ const { navigate } = useRouter();
79
+ const { isMobile, setOpenMobile } = useSidebar();
80
+ const [searchOpen, setSearchOpen] = useState(false);
81
+
82
+ // Pintasan global Ctrl/⌘+K membuka pencarian.
83
+ useEffect(() => {
84
+ const onKey = (e: KeyboardEvent) => {
85
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
86
+ e.preventDefault();
87
+ setSearchOpen((o) => !o);
88
+ }
89
+ };
90
+ window.addEventListener("keydown", onKey);
91
+ return () => window.removeEventListener("keydown", onKey);
92
+ }, []);
93
+
94
+ /** Navigasi SPA dari tautan sidebar; Sheet mobile ditutup setelahnya. */
95
+ const go = (path: string) => (e: React.MouseEvent<HTMLAnchorElement>) => {
96
+ // Biarkan ctrl/cmd/middle-click membuka tab baru seperti tautan biasa.
97
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
98
+ e.preventDefault();
99
+ navigate(path);
100
+ if (isMobile) setOpenMobile(false);
101
+ };
102
+
103
+ const activeProjectId =
104
+ route.name === "project" || route.name === "session" ? route.projectId : null;
105
+ const activeSessionId = route.name === "session" ? route.sessionId : null;
106
+
107
+ return (
108
+ <Sidebar collapsible="offcanvas">
109
+ <SidebarHeader>
110
+ <SidebarMenu>
111
+ <SidebarMenuItem>
112
+ <SidebarMenuButton size="lg" asChild>
113
+ <a href={projectsPath()} onClick={go(projectsPath())}>
114
+ {/* Tint warna logo (sama dengan Settings -> About) agar logo
115
+ ungu tetap kontras di light & dark mode. */}
116
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-[#EEEBFF] p-1.5 ring-1 ring-[#6D5CFB]/20 dark:bg-[#6D5CFB]/15 dark:ring-[#6D5CFB]/30">
117
+ <img src={logo} alt="" className="size-full" />
118
+ </span>
119
+ <span className="truncate font-semibold">KCG Code</span>
120
+ </a>
121
+ </SidebarMenuButton>
122
+ </SidebarMenuItem>
123
+ </SidebarMenu>
124
+
125
+ {/* Navigasi utama */}
126
+ <SidebarMenu className="mt-1">
127
+ <SidebarMenuItem>
128
+ {/* Homepage = composer chat: kirim pesan di sana membuat Session
129
+ baru di Project yang dipilih. */}
130
+ <SidebarMenuButton
131
+ asChild
132
+ isActive={route.name === "projects"}
133
+ className={NAV_BUTTON_CLASS}
134
+ >
135
+ <a href={projectsPath()} onClick={go(projectsPath())}>
136
+ <MessageCirclePlusIcon />
137
+ <span>New session</span>
138
+ </a>
139
+ </SidebarMenuButton>
140
+ </SidebarMenuItem>
141
+ <SidebarMenuItem>
142
+ <SidebarMenuButton
143
+ type="button"
144
+ onClick={() => {
145
+ if (isMobile) setOpenMobile(false);
146
+ setSearchOpen(true);
147
+ }}
148
+ aria-haspopup="dialog"
149
+ aria-keyshortcuts="Control+K Meta+K"
150
+ className={NAV_BUTTON_CLASS}
151
+ >
152
+ <SearchIcon />
153
+ <span>Search</span>
154
+ <kbd className="ml-auto hidden rounded border bg-sidebar px-1.5 font-mono text-[10px] text-muted-foreground md:inline">
155
+ Ctrl K
156
+ </kbd>
157
+ </SidebarMenuButton>
158
+ </SidebarMenuItem>
159
+ <SidebarMenuItem>
160
+ <SidebarMenuButton
161
+ asChild
162
+ isActive={route.name === "skills"}
163
+ className={NAV_BUTTON_CLASS}
164
+ >
165
+ {/* Bawa Project aktif agar otomatis terpilih di halaman Skills. */}
166
+ <a href={skillsPath(activeProjectId)} onClick={go(skillsPath(activeProjectId))}>
167
+ <SparklesIcon />
168
+ <span>Skills</span>
169
+ </a>
170
+ </SidebarMenuButton>
171
+ </SidebarMenuItem>
172
+ </SidebarMenu>
173
+ </SidebarHeader>
174
+
175
+ <SidebarContent>
176
+ <SidebarGroup>
177
+ <SidebarGroupLabel>Projects</SidebarGroupLabel>
178
+ <SidebarGroupContent>
179
+ <SidebarBody
180
+ data={data}
181
+ activeProjectId={activeProjectId}
182
+ activeSessionId={activeSessionId}
183
+ go={go}
184
+ navigate={(path) => {
185
+ navigate(path);
186
+ if (isMobile) setOpenMobile(false);
187
+ }}
188
+ />
189
+ </SidebarGroupContent>
190
+ </SidebarGroup>
191
+ </SidebarContent>
192
+
193
+ <SidebarFooter className="gap-3 border-t">
194
+ {/* Tema: Light / Dark / System */}
195
+ <ThemeToggle />
196
+ {/* Profil + Settings + Lock */}
197
+ <SidebarAccount
198
+ active={route.name === "settings"}
199
+ onNavigate={(path) => {
200
+ navigate(path);
201
+ if (isMobile) setOpenMobile(false);
202
+ }}
203
+ />
204
+ </SidebarFooter>
205
+
206
+ <SidebarRail />
207
+
208
+ <SearchDialog
209
+ open={searchOpen}
210
+ onOpenChange={setSearchOpen}
211
+ groups={data.groups}
212
+ onSelect={navigate}
213
+ />
214
+ </Sidebar>
215
+ );
216
+ }
217
+
218
+ type GoHandler = (path: string) => (e: React.MouseEvent<HTMLAnchorElement>) => void;
219
+ /** Navigasi langsung (tanpa event anchor) — dipakai dobel klik Project. */
220
+ type NavigateFn = (path: string) => void;
221
+
222
+ interface SidebarBodyProps {
223
+ data: UseSidebarDataResult;
224
+ activeProjectId: string | null;
225
+ activeSessionId: string | null;
226
+ go: GoHandler;
227
+ navigate: NavigateFn;
228
+ }
229
+
230
+ /** Isi grup: skeleton, error, kosong, atau daftar Project. */
231
+ function SidebarBody({ data, activeProjectId, activeSessionId, go, navigate }: SidebarBodyProps) {
232
+ if (data.initialLoading) {
233
+ return (
234
+ <SidebarMenu aria-busy="true" aria-label="Loading sessions">
235
+ {["a", "b", "c", "d"].map((k) => (
236
+ <SidebarMenuItem key={k}>
237
+ <SidebarMenuSkeleton showIcon />
238
+ </SidebarMenuItem>
239
+ ))}
240
+ </SidebarMenu>
241
+ );
242
+ }
243
+
244
+ if (data.loadError && data.projectCount === 0) {
245
+ return (
246
+ <div className="flex flex-col items-start gap-2 px-2 py-1 text-xs text-muted-foreground">
247
+ <p role="alert">{data.loadError}</p>
248
+ <Button type="button" variant="outline" size="sm" onClick={() => void data.refresh()}>
249
+ <RefreshCwIcon data-icon="inline-start" />
250
+ Retry
251
+ </Button>
252
+ </div>
253
+ );
254
+ }
255
+
256
+ if (data.projectCount === 0) {
257
+ return <p className="px-2 py-1 text-xs text-muted-foreground">No projects yet.</p>;
258
+ }
259
+
260
+ return (
261
+ <SidebarMenu>
262
+ {data.groups.map((group) => (
263
+ <ProjectGroup
264
+ key={group.project.id}
265
+ group={group}
266
+ active={group.project.id === activeProjectId}
267
+ activeSessionId={activeSessionId}
268
+ go={go}
269
+ navigate={navigate}
270
+ />
271
+ ))}
272
+ </SidebarMenu>
273
+ );
274
+ }
275
+
276
+ interface ProjectGroupProps {
277
+ group: SidebarProjectGroup;
278
+ /** Project sedang dibuka (halaman Project atau salah satu Session-nya). */
279
+ active: boolean;
280
+ activeSessionId: string | null;
281
+ go: GoHandler;
282
+ navigate: NavigateFn;
283
+ }
284
+
285
+ function ProjectGroup({ group, active, activeSessionId, go, navigate }: ProjectGroupProps) {
286
+ const { isMobile } = useSidebar();
287
+ const [open, setOpen] = useState(active || group.sessions.length > 0);
288
+ /**
289
+ * Jenis pointer klik terakhir. Dobel klik hanya berlaku untuk mouse —
290
+ * double-tap di layar sentuh bisa ikut menghasilkan `detail === 2` dan
291
+ * tidak boleh tiba-tiba berpindah halaman.
292
+ */
293
+ const pointerType = useRef<string>("mouse");
294
+ const { project, sessions, runningCount } = group;
295
+ const visible = sessions.slice(0, MAX_VISIBLE_SESSIONS);
296
+ // Session aktif di luar potongan tetap ditampilkan agar posisi terlihat.
297
+ const activeHidden =
298
+ activeSessionId !== null &&
299
+ !visible.some((s) => s.id === activeSessionId) &&
300
+ sessions.find((s) => s.id === activeSessionId);
301
+ const shown = activeHidden ? [...visible, activeHidden] : visible;
302
+ const hiddenCount = sessions.length - shown.length;
303
+ const detailPath = projectPath(project.id);
304
+
305
+ /**
306
+ * Klik pertama (detail 1) dibiarkan ke Collapsible = toggle. Klik kedua
307
+ * dari dobel klik mouse (detail 2) membatalkan toggle-nya
308
+ * (`preventDefault` dihormati Radix) lalu membuka halaman Project; grup
309
+ * dipastikan terbuka agar Session-nya tetap terlihat.
310
+ */
311
+ const onProjectClick = (e: React.MouseEvent<HTMLButtonElement>) => {
312
+ if (e.detail >= 2 && pointerType.current === "mouse") {
313
+ e.preventDefault();
314
+ setOpen(true);
315
+ navigate(detailPath);
316
+ }
317
+ };
318
+
319
+ return (
320
+ <Collapsible asChild open={open} onOpenChange={setOpen} className="group/collapsible">
321
+ <SidebarMenuItem>
322
+ <CollapsibleTrigger asChild>
323
+ <SidebarMenuButton
324
+ isActive={active && activeSessionId === null}
325
+ tooltip={project.name}
326
+ aria-label={`${project.name}, ${sessions.length} sessions`}
327
+ onPointerDown={(e) => {
328
+ pointerType.current = e.pointerType;
329
+ }}
330
+ onClick={onProjectClick}
331
+ // Petunjuk dobel klik hanya untuk pointer mouse (bukan HP).
332
+ title={isMobile ? undefined : "Click to expand · Double-click to open project"}
333
+ // Dobel klik tidak boleh ikut memblok (select) nama Project.
334
+ className="select-none"
335
+ >
336
+ <ChevronRightIcon className="transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
337
+ {active ? <FolderOpenIcon /> : <FolderIcon />}
338
+ <span>{project.name}</span>
339
+ </SidebarMenuButton>
340
+ </CollapsibleTrigger>
341
+ {runningCount > 0 && (
342
+ <SidebarMenuBadge aria-label={`${runningCount} running`}>{runningCount}</SidebarMenuBadge>
343
+ )}
344
+
345
+ <CollapsibleContent>
346
+ <SidebarMenuSub>
347
+ {shown.length === 0 && (
348
+ <SidebarMenuSubItem>
349
+ <span className="block px-2 py-1 text-xs text-muted-foreground">
350
+ No sessions yet
351
+ </span>
352
+ </SidebarMenuSubItem>
353
+ )}
354
+ {shown.map((session) => {
355
+ const title = displaySessionTitle(session.title);
356
+ const path = sessionPath(project.id, session.id);
357
+ const isActive = session.id === activeSessionId;
358
+ return (
359
+ <SidebarMenuSubItem key={session.id}>
360
+ <SidebarMenuSubButton
361
+ href={path}
362
+ onClick={go(path)}
363
+ isActive={isActive}
364
+ aria-current={isActive ? "page" : undefined}
365
+ title={title}
366
+ // Target sentuh lebih tinggi di HP (h-7 default terlalu kecil).
367
+ className="h-9 md:h-7"
368
+ >
369
+ <span
370
+ aria-hidden
371
+ className={cn(
372
+ "size-2 shrink-0 rounded-full",
373
+ SESSION_STATUS_DOT[session.status],
374
+ )}
375
+ />
376
+ <span className="sr-only">{SESSION_STATUS_LABEL[session.status]}:</span>
377
+ <span>{title}</span>
378
+ </SidebarMenuSubButton>
379
+ {/* Aksi cepat khusus desktop (hover); HP pakai halaman Session. */}
380
+ {!isMobile && (
381
+ <SidebarSessionMenu
382
+ session={session}
383
+ title={title}
384
+ onDeleted={() => {
385
+ // Session yang sedang dibuka dihapus -> keluar ke Project.
386
+ if (isActive) navigate(detailPath);
387
+ }}
388
+ />
389
+ )}
390
+ </SidebarMenuSubItem>
391
+ );
392
+ })}
393
+ {/* Selalu ada: jalan ke halaman Project untuk layar sentuh &
394
+ keyboard (dobel klik hanya pintasan mouse). */}
395
+ {/* Sengaja lebih ringan dari baris Session: teks 11px, tipis,
396
+ redup, tanpa titik status — terbaca sebagai tautan sekunder.
397
+ Tinggi sentuh tetap 32px di HP. */}
398
+ <SidebarMenuSubItem>
399
+ <SidebarMenuSubButton
400
+ href={detailPath}
401
+ onClick={go(detailPath)}
402
+ size="sm"
403
+ isActive={active && activeSessionId === null}
404
+ aria-current={active && activeSessionId === null ? "page" : undefined}
405
+ className="group/detail h-8 w-fit gap-0.5 pr-1.5 text-[11px] font-normal text-muted-foreground/80 hover:text-foreground data-[active=true]:font-medium data-[active=true]:text-foreground md:h-6 [&>svg]:size-3 [&>svg]:text-current"
406
+ >
407
+ <span>{hiddenCount > 0 ? `Show all ${sessions.length}` : "Project details"}</span>
408
+ <ChevronRightIcon
409
+ className="transition-transform group-hover/detail:translate-x-0.5"
410
+ aria-hidden
411
+ />
412
+ </SidebarMenuSubButton>
413
+ </SidebarMenuSubItem>
414
+ </SidebarMenuSub>
415
+ </CollapsibleContent>
416
+ </SidebarMenuItem>
417
+ </Collapsible>
418
+ );
419
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Command palette pencarian Session (dibuka dari tombol "Search" di sidebar
3
+ * atau pintasan Ctrl/⌘+K).
4
+ *
5
+ * - Input di atas, daftar hasil di bawah: tiap baris = judul Session + nama
6
+ * Project. Query kosong -> Session terbaru dari seluruh Project.
7
+ * - Navigasi keyboard: ↑/↓ memindah sorotan, Enter membuka, Esc menutup.
8
+ * - Pencocokan memakai `filterSidebarGroups` (nama Project / judul Session).
9
+ */
10
+ import { FolderIcon, MessageSquareIcon, SearchIcon } from "lucide-react";
11
+ import { useEffect, useMemo, useRef, useState } from "react";
12
+ import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
13
+ import { sessionPath } from "@/lib/routes";
14
+ import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
15
+ import { displaySessionTitle } from "@/lib/session-title";
16
+ import { filterSidebarGroups, type SidebarProjectGroup } from "@/lib/sidebar-groups";
17
+ import { cn } from "@/lib/utils";
18
+ import type { Session } from "@/types";
19
+
20
+ /** Batas hasil yang dirender agar daftar tetap ringan. */
21
+ const MAX_RESULTS = 50;
22
+
23
+ interface SearchResult {
24
+ session: Session;
25
+ projectName: string;
26
+ title: string;
27
+ }
28
+
29
+ export interface SearchDialogProps {
30
+ open: boolean;
31
+ onOpenChange: (open: boolean) => void;
32
+ /** Seluruh grup (belum difilter). */
33
+ groups: SidebarProjectGroup[];
34
+ onSelect: (path: string) => void;
35
+ }
36
+
37
+ export function SearchDialog({ open, onOpenChange, groups, onSelect }: SearchDialogProps) {
38
+ const [query, setQuery] = useState("");
39
+ const [highlight, setHighlight] = useState(0);
40
+ const listRef = useRef<HTMLDivElement>(null);
41
+
42
+ // Reset setiap kali dibuka.
43
+ useEffect(() => {
44
+ if (open) {
45
+ setQuery("");
46
+ setHighlight(0);
47
+ }
48
+ }, [open]);
49
+
50
+ const results = useMemo<SearchResult[]>(() => {
51
+ const titleOf = (s: Session) => displaySessionTitle(s.title);
52
+ const filtered = filterSidebarGroups(groups, query, titleOf);
53
+ const flat = filtered.flatMap((g) =>
54
+ g.sessions.map((session) => ({
55
+ session,
56
+ projectName: g.project.name,
57
+ title: titleOf(session),
58
+ })),
59
+ );
60
+ // Terbaru di atas lintas Project.
61
+ flat.sort((a, b) => b.session.updatedAt - a.session.updatedAt);
62
+ return flat.slice(0, MAX_RESULTS);
63
+ }, [groups, query]);
64
+
65
+ // Sorotan tetap valid saat hasil berubah.
66
+ // biome-ignore lint/correctness/useExhaustiveDependencies: reset saat query berubah
67
+ useEffect(() => {
68
+ setHighlight(0);
69
+ }, [query]);
70
+
71
+ // Gulirkan baris tersorot ke dalam viewport.
72
+ useEffect(() => {
73
+ listRef.current
74
+ ?.querySelector<HTMLElement>(`[data-index="${highlight}"]`)
75
+ ?.scrollIntoView({ block: "nearest" });
76
+ }, [highlight]);
77
+
78
+ const choose = (r: SearchResult | undefined) => {
79
+ if (!r) return;
80
+ onOpenChange(false);
81
+ onSelect(sessionPath(r.session.projectId, r.session.id));
82
+ };
83
+
84
+ const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
85
+ if (e.key === "ArrowDown") {
86
+ e.preventDefault();
87
+ setHighlight((h) => Math.min(h + 1, results.length - 1));
88
+ } else if (e.key === "ArrowUp") {
89
+ e.preventDefault();
90
+ setHighlight((h) => Math.max(h - 1, 0));
91
+ } else if (e.key === "Enter") {
92
+ e.preventDefault();
93
+ choose(results[highlight]);
94
+ }
95
+ };
96
+
97
+ const trimmed = query.trim();
98
+ const activeId = results[highlight] ? `search-result-${highlight}` : undefined;
99
+
100
+ return (
101
+ <Dialog open={open} onOpenChange={onOpenChange}>
102
+ <DialogContent
103
+ showCloseButton={false}
104
+ className="top-[15%] translate-y-0 gap-0 overflow-hidden p-0 sm:max-w-xl"
105
+ >
106
+ <DialogTitle className="sr-only">Search sessions</DialogTitle>
107
+ <DialogDescription className="sr-only">
108
+ Type to filter sessions by title or project name.
109
+ </DialogDescription>
110
+
111
+ <div className="flex items-center gap-2 border-b px-4">
112
+ <SearchIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
113
+ <input
114
+ autoFocus
115
+ type="text"
116
+ role="combobox"
117
+ aria-expanded
118
+ aria-controls="search-results"
119
+ aria-activedescendant={activeId}
120
+ aria-label="Search sessions"
121
+ value={query}
122
+ onChange={(e) => setQuery(e.target.value)}
123
+ onKeyDown={onKeyDown}
124
+ placeholder="Search sessions…"
125
+ className="h-12 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
126
+ />
127
+ <kbd className="hidden shrink-0 rounded border bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground sm:inline">
128
+ Esc
129
+ </kbd>
130
+ </div>
131
+
132
+ <div
133
+ ref={listRef}
134
+ id="search-results"
135
+ role="listbox"
136
+ aria-label="Results"
137
+ className="max-h-[min(60vh,420px)] overflow-y-auto p-2"
138
+ >
139
+ {results.length === 0 ? (
140
+ <p className="px-3 py-8 text-center text-sm text-muted-foreground">
141
+ {trimmed === "" ? "No sessions yet." : `No matches for “${trimmed}”.`}
142
+ </p>
143
+ ) : (
144
+ <>
145
+ <p className="px-2 pt-1 pb-2 text-xs font-medium text-muted-foreground">
146
+ {trimmed === "" ? "Recent" : "Results"}
147
+ </p>
148
+ {results.map((r, i) => (
149
+ <div
150
+ key={r.session.id}
151
+ id={`search-result-${i}`}
152
+ data-index={i}
153
+ role="option"
154
+ aria-selected={i === highlight}
155
+ tabIndex={-1}
156
+ onMouseMove={() => setHighlight(i)}
157
+ onClick={() => choose(r)}
158
+ onKeyDown={() => {}}
159
+ className={cn(
160
+ "flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 text-sm",
161
+ i === highlight && "bg-accent text-accent-foreground",
162
+ )}
163
+ >
164
+ <MessageSquareIcon
165
+ className="size-4 shrink-0 text-muted-foreground"
166
+ aria-hidden
167
+ />
168
+ <span className="min-w-0 flex-1 truncate">{r.title}</span>
169
+ <span className="flex max-w-[40%] shrink-0 items-center gap-1 truncate text-xs text-muted-foreground">
170
+ <FolderIcon className="size-3 shrink-0" aria-hidden />
171
+ <span className="truncate">{r.projectName}</span>
172
+ </span>
173
+ <span
174
+ aria-hidden
175
+ className={cn(
176
+ "size-2 shrink-0 rounded-full",
177
+ SESSION_STATUS_DOT[r.session.status],
178
+ )}
179
+ />
180
+ <span className="sr-only">{SESSION_STATUS_LABEL[r.session.status]}</span>
181
+ </div>
182
+ ))}
183
+ </>
184
+ )}
185
+ </div>
186
+ </DialogContent>
187
+ </Dialog>
188
+ );
189
+ }