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,111 @@
1
+ /**
2
+ * Footer sidebar: profil pemilik (avatar + nickname) dengan tombol Settings
3
+ * dan Lock. Bila kunci belum diatur, tombol Lock diganti peringatan
4
+ * "Not protected" yang membuka `/settings/security`.
5
+ */
6
+ import { LockIcon, SettingsIcon, ShieldAlertIcon } from "lucide-react";
7
+ import { ProfileAvatar } from "@/components/auth/ProfileAvatar";
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9
+ import { lockNow, useAuth } from "@/lib/auth";
10
+ import { settingsPath } from "@/lib/routes";
11
+ import { cn } from "@/lib/utils";
12
+
13
+ export interface SidebarAccountProps {
14
+ /** Halaman Settings sedang terbuka. */
15
+ active: boolean;
16
+ onNavigate: (path: string) => void;
17
+ }
18
+
19
+ export function SidebarAccount({ active, onNavigate }: SidebarAccountProps) {
20
+ const auth = useAuth();
21
+ const status = auth.phase === "ready" ? auth.status : null;
22
+ const nickname = status?.profile.nickname ?? null;
23
+ const avatarUrl = status?.profile.avatarUrl ?? null;
24
+ const avatarPreset = status?.profile.avatarPreset ?? null;
25
+ const protectedApp = status?.protected ?? false;
26
+
27
+ const iconBtn =
28
+ "flex size-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground/70 transition-colors outline-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring";
29
+
30
+ return (
31
+ <div className="flex flex-col gap-1">
32
+ {status && !protectedApp && (
33
+ <button
34
+ type="button"
35
+ onClick={() => onNavigate(settingsPath("security"))}
36
+ className="flex items-center gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-2.5 py-2 text-left text-xs text-amber-800 transition-colors hover:bg-amber-500/15 dark:text-amber-300"
37
+ >
38
+ <ShieldAlertIcon className="size-4 shrink-0" aria-hidden />
39
+ <span className="min-w-0">
40
+ <span className="block font-medium">Not protected</span>
41
+ <span className="block text-amber-800/80 dark:text-amber-300/80">
42
+ Set a lock before exposing this app.
43
+ </span>
44
+ </span>
45
+ </button>
46
+ )}
47
+
48
+ <div className="flex min-w-0 items-center gap-1">
49
+ <a
50
+ href={settingsPath()}
51
+ onClick={(e) => {
52
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
53
+ e.preventDefault();
54
+ onNavigate(settingsPath());
55
+ }}
56
+ aria-current={active ? "page" : undefined}
57
+ className={cn(
58
+ "flex min-w-0 flex-1 items-center gap-2.5 rounded-md p-1.5 text-left transition-colors outline-none hover:bg-sidebar-accent focus-visible:ring-2 focus-visible:ring-sidebar-ring",
59
+ active && "bg-sidebar-accent",
60
+ )}
61
+ >
62
+ <ProfileAvatar
63
+ nickname={nickname}
64
+ avatarUrl={avatarUrl}
65
+ avatarPreset={avatarPreset}
66
+ className="size-8"
67
+ textClassName="text-xs"
68
+ />
69
+ <span className="flex min-w-0 flex-col leading-tight">
70
+ <span className="truncate text-sm font-medium">{nickname ?? "Set up profile"}</span>
71
+ <span className="truncate text-xs text-muted-foreground">
72
+ {protectedApp
73
+ ? `Locked with ${status?.lockKind === "pin" ? "PIN" : "password"}`
74
+ : "Settings"}
75
+ </span>
76
+ </span>
77
+ </a>
78
+
79
+ <Tooltip>
80
+ <TooltipTrigger asChild>
81
+ <button
82
+ type="button"
83
+ onClick={() => onNavigate(settingsPath())}
84
+ aria-label="Settings"
85
+ className={cn(iconBtn, active && "text-sidebar-accent-foreground")}
86
+ >
87
+ <SettingsIcon className="size-4" />
88
+ </button>
89
+ </TooltipTrigger>
90
+ <TooltipContent side="top">Settings</TooltipContent>
91
+ </Tooltip>
92
+
93
+ {protectedApp && (
94
+ <Tooltip>
95
+ <TooltipTrigger asChild>
96
+ <button
97
+ type="button"
98
+ onClick={() => void lockNow()}
99
+ aria-label="Lock now"
100
+ className={iconBtn}
101
+ >
102
+ <LockIcon className="size-4" />
103
+ </button>
104
+ </TooltipTrigger>
105
+ <TooltipContent side="top">Lock now</TooltipContent>
106
+ </Tooltip>
107
+ )}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Menu "..." per baris Session di sidebar — KHUSUS desktop.
3
+ *
4
+ * - Titik tiga vertikal di celah kosong kanan baris (di luar area hover
5
+ * baris). Muncul saat baris di-hover / difokus keyboard (`group-hover`),
6
+ * tetap terlihat selama menu terbuka.
7
+ * - Aksi sesuai status: running -> Stop & Restart; stopped/crashed -> Start.
8
+ * Delete selalu ada (dengan dialog konfirmasi).
9
+ * - Setelah aksi, sidebar & daftar lain terbarui otomatis lewat broadcast
10
+ * server (`session_status`, `session_deleted`, `data_changed`) — di sini
11
+ * tidak perlu memuat ulang manual.
12
+ */
13
+ import { MoreVerticalIcon, PlayIcon, RotateCcwIcon, SquareIcon, Trash2Icon } from "lucide-react";
14
+ import { useState } from "react";
15
+ import { toast } from "sonner";
16
+ import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
17
+ import {
18
+ DropdownMenu,
19
+ DropdownMenuContent,
20
+ DropdownMenuItem,
21
+ DropdownMenuSeparator,
22
+ DropdownMenuTrigger,
23
+ } from "@/components/ui/dropdown-menu";
24
+ import { Spinner } from "@/components/ui/spinner";
25
+ import { ApiError, apiFetch } from "@/lib/api";
26
+ import { notifyDataChanged } from "@/lib/data-events";
27
+ import { cn } from "@/lib/utils";
28
+ import type { Session } from "@/types";
29
+
30
+ type Busy = "stop" | "start" | "restart" | null;
31
+
32
+ function errorText(e: unknown, fallback: string): string {
33
+ return e instanceof ApiError ? e.message : fallback;
34
+ }
35
+
36
+ export interface SidebarSessionMenuProps {
37
+ session: Session;
38
+ /** Judul tampilan (untuk label aksesibel). */
39
+ title: string;
40
+ /** Dipanggil setelah Session berhasil dihapus. */
41
+ onDeleted?: () => void;
42
+ }
43
+
44
+ export function SidebarSessionMenu({ session, title, onDeleted }: SidebarSessionMenuProps) {
45
+ const [open, setOpen] = useState(false);
46
+ const [busy, setBusy] = useState<Busy>(null);
47
+ const [confirmDelete, setConfirmDelete] = useState(false);
48
+ const [deleting, setDeleting] = useState(false);
49
+ const running = session.status === "running";
50
+
51
+ const stop = () => apiFetch(`/api/sessions/${session.id}/stop`, { method: "POST" });
52
+ const start = () => apiFetch(`/api/sessions/${session.id}`, { method: "POST" });
53
+
54
+ const run = async (kind: Exclude<Busy, null>) => {
55
+ setBusy(kind);
56
+ try {
57
+ if (kind === "stop") {
58
+ await stop();
59
+ toast.success("Session stopped.");
60
+ } else if (kind === "start") {
61
+ await start();
62
+ toast.success("Session started.");
63
+ } else {
64
+ await stop();
65
+ await start();
66
+ toast.success("Session restarted.");
67
+ }
68
+ notifyDataChanged();
69
+ } catch (e) {
70
+ toast.error(
71
+ errorText(
72
+ e,
73
+ kind === "stop"
74
+ ? "Couldn't stop the session."
75
+ : kind === "start"
76
+ ? "Couldn't start the session."
77
+ : "Couldn't restart the session.",
78
+ ),
79
+ );
80
+ } finally {
81
+ setBusy(null);
82
+ }
83
+ };
84
+
85
+ const remove = async () => {
86
+ setDeleting(true);
87
+ try {
88
+ await apiFetch(`/api/sessions/${session.id}`, { method: "DELETE" });
89
+ setConfirmDelete(false);
90
+ toast.success("Session deleted.");
91
+ notifyDataChanged();
92
+ onDeleted?.();
93
+ } catch (e) {
94
+ toast.error(errorText(e, "Couldn't delete the session."));
95
+ } finally {
96
+ setDeleting(false);
97
+ }
98
+ };
99
+
100
+ return (
101
+ <>
102
+ <DropdownMenu open={open} onOpenChange={setOpen}>
103
+ <DropdownMenuTrigger asChild>
104
+ <button
105
+ type="button"
106
+ aria-label={`Session actions: ${title}`}
107
+ disabled={busy !== null}
108
+ className={cn(
109
+ // Diselipkan di celah kosong KANAN baris (margin kanan
110
+ // `SidebarMenuSub` = 24px), bukan menimpa judul: lebar 24px
111
+ // menempel tepat di tepi baris agar hover tidak "putus" saat
112
+ // kursor bergeser dari baris ke tombol.
113
+ "absolute top-1/2 -right-6 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-sidebar-foreground/60 outline-hidden transition-opacity",
114
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring",
115
+ // Tersembunyi sampai baris di-hover / difokus; tetap terlihat
116
+ // selama menu terbuka atau aksi berjalan.
117
+ "opacity-0 group-focus-within/menu-sub-item:opacity-100 group-hover/menu-sub-item:opacity-100 data-[state=open]:opacity-100",
118
+ busy !== null && "opacity-100",
119
+ "[&>svg]:size-4",
120
+ )}
121
+ >
122
+ {busy !== null ? <Spinner className="size-3.5" /> : <MoreVerticalIcon />}
123
+ </button>
124
+ </DropdownMenuTrigger>
125
+ <DropdownMenuContent side="right" align="start" className="min-w-40">
126
+ {running ? (
127
+ <>
128
+ <DropdownMenuItem onSelect={() => void run("stop")}>
129
+ <SquareIcon />
130
+ Stop
131
+ </DropdownMenuItem>
132
+ <DropdownMenuItem onSelect={() => void run("restart")}>
133
+ <RotateCcwIcon />
134
+ Restart
135
+ </DropdownMenuItem>
136
+ </>
137
+ ) : (
138
+ <DropdownMenuItem onSelect={() => void run("start")}>
139
+ <PlayIcon />
140
+ {session.status === "crashed" ? "Restart" : "Start"}
141
+ </DropdownMenuItem>
142
+ )}
143
+ <DropdownMenuSeparator />
144
+ <DropdownMenuItem
145
+ variant="destructive"
146
+ // Tunda buka dialog sampai menu selesai menutup (fokus aman).
147
+ onSelect={() => setConfirmDelete(true)}
148
+ >
149
+ <Trash2Icon />
150
+ Delete
151
+ </DropdownMenuItem>
152
+ </DropdownMenuContent>
153
+ </DropdownMenu>
154
+
155
+ <ConfirmSessionDeleteDialog
156
+ open={confirmDelete}
157
+ onOpenChange={setConfirmDelete}
158
+ deleting={deleting}
159
+ onConfirm={() => void remove()}
160
+ />
161
+ </>
162
+ );
163
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Pemilih tema (Requirement 8.2) di footer sidebar: label "Theme" di kiri,
3
+ * tiga tombol ikon di kanan — Light, Dark, System (ikuti perangkat) — dalam
4
+ * satu segmented control ringkas. Pilihan disimpan `useTheme` di
5
+ * `localStorage["kcg-theme"]` dan diterapkan ke `<html>` (Requirement 8.6).
6
+ *
7
+ * Aksesibilitas: grup radio native (`<input type="radio">` tersembunyi di
8
+ * dalam label), jadi Tab masuk ke pilihan aktif dan panah berpindah opsi.
9
+ * Tombol hanya ikon, jadi nama tiap opsi diberikan lewat teks `sr-only` dan
10
+ * tooltip (`title`).
11
+ */
12
+ import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
13
+ import { useId } from "react";
14
+ import { type ThemePreference, useTheme } from "@/hooks/useTheme";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ const OPTIONS: { value: ThemePreference; label: string; Icon: typeof SunIcon }[] = [
18
+ { value: "light", label: "Light", Icon: SunIcon },
19
+ { value: "dark", label: "Dark", Icon: MoonIcon },
20
+ { value: "system", label: "System", Icon: MonitorIcon },
21
+ ];
22
+
23
+ export function ThemeToggle({ className }: { className?: string }) {
24
+ const { preference, setPreference } = useTheme();
25
+ const name = useId();
26
+ const labelId = useId();
27
+
28
+ return (
29
+ <div className={cn("flex items-center justify-between gap-3 px-2", className)}>
30
+ <span id={labelId} className="text-sm text-sidebar-foreground/80">
31
+ Theme
32
+ </span>
33
+ <fieldset
34
+ aria-labelledby={labelId}
35
+ className="flex shrink-0 items-center gap-0.5 rounded-lg border border-sidebar-border bg-sidebar-accent/60 p-0.5"
36
+ >
37
+ {OPTIONS.map(({ value, label, Icon }) => {
38
+ const active = preference === value;
39
+ return (
40
+ <label
41
+ key={value}
42
+ title={value === "system" ? "System (match device)" : label}
43
+ className={cn(
44
+ "flex size-7 cursor-pointer items-center justify-center rounded-md transition-colors select-none",
45
+ "has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-sidebar-ring",
46
+ active
47
+ ? "bg-sidebar text-sidebar-foreground shadow-sm"
48
+ : "text-sidebar-foreground/55 hover:text-sidebar-foreground",
49
+ )}
50
+ >
51
+ <input
52
+ type="radio"
53
+ name={name}
54
+ value={value}
55
+ checked={active}
56
+ onChange={() => setPreference(value)}
57
+ className="sr-only"
58
+ />
59
+ <Icon className="size-4 shrink-0" aria-hidden />
60
+ <span className="sr-only">{label}</span>
61
+ </label>
62
+ );
63
+ })}
64
+ </fieldset>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Folder_Browser (Requirement 10.2, 10.3).
3
+ *
4
+ * Menjelajah direktori di dalam Sandbox_Root via `GET /api/fs?path=...`:
5
+ * breadcrumb path saat ini dan daftar sub-direktori. Fully controlled —
6
+ * direktori yang sedang dijelajahi (`path`) adalah direktori yang otomatis
7
+ * aktif/terpilih (Requirement 10.4); tidak ada tombol "Pilih" terpisah.
8
+ */
9
+
10
+ import { ChevronRightIcon, FolderIcon, Undo2Icon } from "lucide-react";
11
+ import { useCallback, useEffect, useState } from "react";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Spinner } from "@/components/ui/spinner";
14
+ import { ApiError, apiFetch } from "@/lib/api";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ export interface FolderBrowserProps {
18
+ /** Path relatif yang sedang aktif/dijelajahi ("" = Sandbox_Root). */
19
+ path: string;
20
+ /** Dipanggil setiap kali pengguna berpindah direktori. */
21
+ onNavigate: (path: string) => void;
22
+ }
23
+
24
+ function joinDir(base: string, name: string): string {
25
+ return base === "" ? name : `${base}/${name}`;
26
+ }
27
+
28
+ export function FolderBrowser({ path, onNavigate }: FolderBrowserProps) {
29
+ const [entries, setEntries] = useState<string[]>([]);
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ const load = useCallback(async (p: string) => {
34
+ setLoading(true);
35
+ setError(null);
36
+ try {
37
+ const res = await apiFetch(`/api/fs?path=${encodeURIComponent(p)}`);
38
+ const body = (await res.json()) as { entries: string[] };
39
+ setEntries(body.entries);
40
+ } catch (e) {
41
+ setEntries([]);
42
+ setError(e instanceof ApiError ? e.message : "Failed to load directory");
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ load(path);
50
+ }, [path, load]);
51
+
52
+ const segments = path === "" ? [] : path.split("/");
53
+
54
+ return (
55
+ <div className="flex h-full min-h-0 flex-col gap-2">
56
+ {/* Breadcrumb */}
57
+ <div className="flex shrink-0 flex-wrap items-center gap-1 text-sm">
58
+ <Button
59
+ type="button"
60
+ variant="ghost"
61
+ size="sm"
62
+ className={cn("h-7 px-2 font-medium", path === "" && "text-foreground")}
63
+ onClick={() => onNavigate("")}
64
+ >
65
+ Sandbox
66
+ </Button>
67
+ {segments.map((seg, i) => {
68
+ const target = segments.slice(0, i + 1).join("/");
69
+ return (
70
+ <span key={target} className="flex items-center gap-1">
71
+ <ChevronRightIcon
72
+ className="size-3.5 text-muted-foreground"
73
+ data-icon="inline-start"
74
+ />
75
+ <Button
76
+ type="button"
77
+ variant="ghost"
78
+ size="sm"
79
+ className={cn(
80
+ "h-7 px-2 font-medium",
81
+ i === segments.length - 1 && "text-foreground",
82
+ )}
83
+ onClick={() => onNavigate(target)}
84
+ >
85
+ {seg}
86
+ </Button>
87
+ </span>
88
+ );
89
+ })}
90
+ </div>
91
+
92
+ {/* Daftar sub-direktori */}
93
+ <div className="min-h-0 flex-1 overflow-y-auto rounded-lg border bg-card p-2">
94
+ {loading ? (
95
+ <div className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground">
96
+ <Spinner className="size-4" />
97
+ Loading directories…
98
+ </div>
99
+ ) : error ? (
100
+ <p className="px-2 py-4 text-sm text-destructive">{error}</p>
101
+ ) : entries.length === 0 ? (
102
+ <p className="px-2 py-4 text-sm text-muted-foreground">No subdirectories.</p>
103
+ ) : (
104
+ <div className="flex flex-col gap-1">
105
+ {path !== "" && (
106
+ <Button
107
+ type="button"
108
+ variant="ghost"
109
+ size="sm"
110
+ className="h-8 justify-start gap-2 px-2 text-muted-foreground"
111
+ onClick={() => onNavigate(segments.slice(0, -1).join("/"))}
112
+ >
113
+ <Undo2Icon data-icon="inline-start" />
114
+ Back
115
+ </Button>
116
+ )}
117
+ {entries.map((name) => (
118
+ <Button
119
+ key={name}
120
+ type="button"
121
+ variant="ghost"
122
+ size="sm"
123
+ className="h-8 justify-start gap-2 px-2"
124
+ onClick={() => onNavigate(joinDir(path, name))}
125
+ >
126
+ <FolderIcon data-icon="inline-start" />
127
+ <span className="truncate">{name}</span>
128
+ </Button>
129
+ ))}
130
+ </div>
131
+ )}
132
+ </div>
133
+
134
+ {/* Path aktif saat ini — selalu nempel di bawah, tidak ikut naik saat
135
+ daftar sub-direktori pendek. */}
136
+ <div className="flex min-w-0 shrink-0 items-center gap-1.5 text-sm">
137
+ <span className="shrink-0 text-muted-foreground">Current directory:</span>
138
+ <span className="min-w-0 flex-1 truncate font-mono">{path === "" ? "/" : path}</span>
139
+ </div>
140
+ </div>
141
+ );
142
+ }