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,176 @@
1
+ /**
2
+ * Header chat ala aplikasi chat mobile: toggle sidebar — nama model (pembuka
3
+ * picker) — aksi. Nama model jadi judul karena itulah informasi yang paling
4
+ * sering dilihat & diganti; identitas Session (agentType + id) turun ke baris
5
+ * kedua yang hanya tampil di layar lebar.
6
+ *
7
+ * Tanpa tombol back: navigasi antar Session/Project lewat sidebar.
8
+ */
9
+ import {
10
+ MoonIcon,
11
+ MoreVerticalIcon,
12
+ PlayIcon,
13
+ SquareIcon,
14
+ SunIcon,
15
+ Trash2Icon,
16
+ } from "lucide-react";
17
+ import { useState } from "react";
18
+ import { ModelPicker } from "@/components/sessions/ModelPicker";
19
+ import { Button } from "@/components/ui/button";
20
+ import {
21
+ DropdownMenu,
22
+ DropdownMenuContent,
23
+ DropdownMenuItem,
24
+ DropdownMenuSeparator,
25
+ DropdownMenuTrigger,
26
+ } from "@/components/ui/dropdown-menu";
27
+ import { SidebarTrigger } from "@/components/ui/sidebar";
28
+ import { Spinner } from "@/components/ui/spinner";
29
+ import { useTheme } from "@/hooks/useTheme";
30
+ import type { WsConnectionStatus } from "@/hooks/useWebSocket";
31
+ import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL, wsStatusLabel } from "@/lib/session-status";
32
+ import { cn } from "@/lib/utils";
33
+ import type { Session, SessionModel, SessionStatus } from "@/types";
34
+
35
+ export interface SessionHeaderProps {
36
+ session: Session;
37
+ status: SessionStatus;
38
+ wsStatus: WsConnectionStatus;
39
+ /** Aksi stop Session (engine) — status baru tiba via WS `session_status`. */
40
+ onStop: () => void;
41
+ /** Aksi start/resume Session (engine). */
42
+ onStart: () => void;
43
+ /** Buka dialog konfirmasi hapus Session (dirender di SessionView). */
44
+ onRequestDelete: () => void;
45
+ stopping: boolean;
46
+ starting: boolean;
47
+ }
48
+
49
+ export function SessionHeader({
50
+ session,
51
+ status,
52
+ wsStatus,
53
+ onStop,
54
+ onStart,
55
+ onRequestDelete,
56
+ stopping,
57
+ starting,
58
+ }: SessionHeaderProps) {
59
+ const { theme, toggleTheme } = useTheme();
60
+ const isDark = theme === "dark";
61
+ /** Model pilihan Session — dapat diganti live; null = default opencode. */
62
+ const [model, setModel] = useState<SessionModel | null>(session.model);
63
+
64
+ return (
65
+ <header className="shrink-0">
66
+ {/* Tanpa border — sidebar sudah jadi pembatas. Lebar penuh (bukan kolom
67
+ percakapan) agar toggle sidebar menempel di tepi kiri, sama seperti
68
+ header shell di halaman lain. Navigasi kembali lewat sidebar. */}
69
+ <div className="flex w-full items-center gap-1 px-3 py-3 sm:gap-2 sm:px-4">
70
+ {/* Buka/ciutkan sidebar Session (Sheet di HP). */}
71
+ <SidebarTrigger className="size-9 shrink-0" />
72
+
73
+ {/* Judul = nama model, sekaligus pembuka dialog pemilihan model.
74
+ Ikon chevron memberi tahu bahwa ini dapat diganti. */}
75
+ <div className="flex min-w-0 flex-1 flex-col items-start">
76
+ <ModelPicker
77
+ projectId={session.projectId}
78
+ agentType={session.agentType}
79
+ sessionId={session.id}
80
+ model={model}
81
+ onChanged={setModel}
82
+ variant="heading"
83
+ />
84
+ <span className="hidden truncate px-2 font-mono text-[11px] text-muted-foreground sm:block">
85
+ {session.agentType} · {session.id.slice(0, 8)}
86
+ {wsStatus !== "connected" && ` · ${wsStatusLabel(wsStatus)}`}
87
+ </span>
88
+ </div>
89
+
90
+ {/* Status = teks biasa + titik (bukan pil yang terlihat seperti
91
+ tombol). Di HP cukup titiknya; label teks mulai dari sm. */}
92
+ <span
93
+ role="status"
94
+ className="flex shrink-0 items-center gap-1.5 px-1 text-xs font-medium text-muted-foreground"
95
+ title={`Session ${SESSION_STATUS_LABEL[status].toLowerCase()}`}
96
+ >
97
+ <span className="relative flex size-2" aria-hidden>
98
+ {status === "running" && (
99
+ <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500/60 motion-reduce:hidden" />
100
+ )}
101
+ <span
102
+ className={cn("relative inline-flex size-2 rounded-full", SESSION_STATUS_DOT[status])}
103
+ />
104
+ </span>
105
+ <span className="sr-only sm:not-sr-only">{SESSION_STATUS_LABEL[status]}</span>
106
+ </span>
107
+
108
+ {/* Stop/Start berlabel teks agar jelas fungsinya (bukan ikon kotak
109
+ tanpa arti). Di HP label disembunyikan, tapi tombol tetap punya
110
+ aria-label & ukuran sentuh 40px. Saat model sedang merespon,
111
+ penghentian balasan ada di composer. */}
112
+ {status === "running" ? (
113
+ <Button
114
+ type="button"
115
+ variant="outline"
116
+ size="sm"
117
+ onClick={onStop}
118
+ disabled={stopping}
119
+ aria-label="Stop session"
120
+ className="relative size-8 shrink-0 rounded-full px-0 after:absolute after:-inset-1 hover:border-destructive/40 hover:bg-destructive/10 hover:text-destructive sm:h-8 sm:w-auto sm:px-3 sm:after:hidden dark:hover:bg-destructive/15"
121
+ >
122
+ {stopping ? (
123
+ <Spinner data-icon="inline-start" />
124
+ ) : (
125
+ <SquareIcon data-icon="inline-start" className="size-3.5 fill-current" />
126
+ )}
127
+ <span className="hidden sm:inline">{stopping ? "Stopping…" : "Stop"}</span>
128
+ </Button>
129
+ ) : (
130
+ <Button
131
+ type="button"
132
+ size="sm"
133
+ onClick={onStart}
134
+ disabled={starting}
135
+ aria-label={status === "crashed" ? "Restart session" : "Start session"}
136
+ className="relative size-8 shrink-0 rounded-full px-0 after:absolute after:-inset-1 sm:h-8 sm:w-auto sm:px-3 sm:after:hidden"
137
+ >
138
+ {starting ? (
139
+ <Spinner data-icon="inline-start" />
140
+ ) : (
141
+ <PlayIcon data-icon="inline-start" className="size-3.5 fill-current" />
142
+ )}
143
+ <span className="hidden sm:inline">
144
+ {starting ? "Starting…" : status === "crashed" ? "Restart" : "Start"}
145
+ </span>
146
+ </Button>
147
+ )}
148
+
149
+ <DropdownMenu>
150
+ <DropdownMenuTrigger asChild>
151
+ <Button
152
+ type="button"
153
+ variant="ghost"
154
+ size="icon"
155
+ aria-label="Session actions"
156
+ className="size-10 shrink-0 sm:size-9"
157
+ >
158
+ <MoreVerticalIcon />
159
+ </Button>
160
+ </DropdownMenuTrigger>
161
+ <DropdownMenuContent align="end">
162
+ <DropdownMenuItem onSelect={toggleTheme}>
163
+ {isDark ? <SunIcon /> : <MoonIcon />}
164
+ {isDark ? "Light mode" : "Dark mode"}
165
+ </DropdownMenuItem>
166
+ <DropdownMenuSeparator />
167
+ <DropdownMenuItem variant="destructive" onSelect={onRequestDelete}>
168
+ <Trash2Icon />
169
+ Delete session
170
+ </DropdownMenuItem>
171
+ </DropdownMenuContent>
172
+ </DropdownMenu>
173
+ </div>
174
+ </header>
175
+ );
176
+ }
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Daftar & pembuatan Session milik sebuah Project (Requirement 1.1, 1.3, 1.5).
3
+ *
4
+ * Halaman detail Project — gaya libadwaita (GNOME Settings), sama dengan
5
+ * panel samping: grup berjudul berisi "boxed list" (primitif `settings/prefs`).
6
+ * - Header: nama Project besar, path (salin saat hover). Aksi: tombol pil
7
+ * "New session" + tombol bundar "..." Reload/Delete (`ProjectMenu`; hapus
8
+ * tetap lewat dialog konfirmasi). Desktop: di kanan judul. Layar sempit:
9
+ * "New session" jadi tombol melayang kanan-bawah (area jempol); toolbar di
10
+ * bawah judul berisi "Details" (panel samping) dan menu "...".
11
+ * - Panel samping (`ProjectSidePanel`, gaya Claude Projects): custom
12
+ * instruction, MCP server, dan skill. Layar `lg`+ = kolom kanan sticky;
13
+ * lebih sempit = Sheet dari kanan lewat tombol di header.
14
+ * Tanpa tombol back — navigasi lewat sidebar.
15
+ * - Daftar Session: grup "Sessions" (ringkasan "3 sessions · 1 running"
16
+ * sebagai deskripsi) berisi baris AdwActionRow; kosong -> AdwStatusPage.
17
+ * Urutan dari `sortSessions` (running -> crashed -> stopped, terbaru dulu).
18
+ * - `GET /api/sessions` di-filter per `projectId` (Requirement 1.5).
19
+ *
20
+ * File ini hanya menyusun area dari bagian yang sudah dipisah:
21
+ * - State & aksi daftar: `hooks/useSessionList.ts`.
22
+ * - Form pembuatan Session: `NewSessionDialog.tsx`.
23
+ * - Satu baris Session: `SessionRow.tsx`; dialog konfirmasi hapus:
24
+ * `ConfirmSessionDeleteDialog.tsx` & `ConfirmDeleteProjectDialog.tsx`.
25
+ */
26
+ import {
27
+ CheckIcon,
28
+ CopyIcon,
29
+ ListChecksIcon,
30
+ MessagesSquareIcon,
31
+ MoreHorizontalIcon,
32
+ PanelRightIcon,
33
+ PlusIcon,
34
+ RefreshCwIcon,
35
+ Trash2Icon,
36
+ TriangleAlertIcon,
37
+ } from "lucide-react";
38
+ import { useEffect, useState } from "react";
39
+ import { toast } from "sonner";
40
+ import { ProjectSidePanel } from "@/components/projects/ProjectSidePanel";
41
+ import { ConfirmDeleteProjectDialog } from "@/components/sessions/ConfirmDeleteProjectDialog";
42
+ import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
43
+ import { NewSessionDialog } from "@/components/sessions/NewSessionDialog";
44
+ import { SessionListSkeleton, SessionRow } from "@/components/sessions/SessionRow";
45
+ import { ActionRow, PrefsGroup } from "@/components/settings/prefs";
46
+ import { Button } from "@/components/ui/button";
47
+ import {
48
+ DropdownMenu,
49
+ DropdownMenuContent,
50
+ DropdownMenuItem,
51
+ DropdownMenuSeparator,
52
+ DropdownMenuTrigger,
53
+ } from "@/components/ui/dropdown-menu";
54
+ import {
55
+ Sheet,
56
+ SheetContent,
57
+ SheetDescription,
58
+ SheetHeader,
59
+ SheetTitle,
60
+ } from "@/components/ui/sheet";
61
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
62
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
63
+ import { useSessionList } from "@/hooks/useSessionList";
64
+ import { notifyDataChanged } from "@/lib/data-events";
65
+ import { describeSelection, pluralSessions } from "@/lib/session-selection";
66
+ import { describeSessionSummary } from "@/lib/session-summary";
67
+ import { cn } from "@/lib/utils";
68
+ import type { Project, Session } from "@/types";
69
+
70
+ /** Breakpoint `lg` Tailwind — di atasnya panel samping tampil inline. */
71
+ const WIDE_QUERY = "(min-width: 1024px)";
72
+
73
+ export interface SessionListProps {
74
+ project: Project;
75
+ onOpenSession: (session: Session) => void;
76
+ /** Dipanggil setelah Project dihapus, untuk kembali ke daftar Project. */
77
+ onDeleted?: () => void;
78
+ }
79
+
80
+ /** Path Project yang bisa diklik untuk disalin — ikon muncul saat hover. */
81
+ function CopyablePath({ path }: { path: string }) {
82
+ const [copied, setCopied] = useState(false);
83
+
84
+ useEffect(() => {
85
+ if (!copied) return;
86
+ const t = setTimeout(() => setCopied(false), 1500);
87
+ return () => clearTimeout(t);
88
+ }, [copied]);
89
+
90
+ const copy = async () => {
91
+ try {
92
+ await navigator.clipboard.writeText(path);
93
+ setCopied(true);
94
+ toast.success("Path copied.");
95
+ } catch {
96
+ // Clipboard API butuh konteks aman (HTTPS / localhost).
97
+ toast.error("Couldn't copy. Select the path and copy it manually.");
98
+ }
99
+ };
100
+
101
+ return (
102
+ <button
103
+ type="button"
104
+ onClick={() => void copy()}
105
+ title={path}
106
+ aria-label={copied ? "Path copied" : `Copy path ${path}`}
107
+ className="group/path -mx-1 flex max-w-full min-w-0 items-center gap-1.5 rounded px-1 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
108
+ >
109
+ <span className="truncate">{path}</span>
110
+ {copied ? (
111
+ <CheckIcon className="size-3 shrink-0 text-emerald-500" aria-hidden />
112
+ ) : (
113
+ <CopyIcon
114
+ className="size-3 shrink-0 opacity-0 transition-opacity group-hover/path:opacity-100 group-focus-visible/path:opacity-100"
115
+ aria-hidden
116
+ />
117
+ )}
118
+ </button>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Aksi sekunder Project (Reload, Delete) di satu menu "..." — Delete jarang
124
+ * dipakai & berbahaya, jadi tidak perlu tombol sendiri yang selalu terlihat.
125
+ * Delete tetap lewat dialog konfirmasi.
126
+ */
127
+ function ProjectMenu({
128
+ loading,
129
+ onReload,
130
+ onDelete,
131
+ className,
132
+ }: {
133
+ loading: boolean;
134
+ onReload: () => void;
135
+ onDelete: () => void;
136
+ className?: string;
137
+ }) {
138
+ return (
139
+ <DropdownMenu>
140
+ <Tooltip>
141
+ <TooltipTrigger asChild>
142
+ <DropdownMenuTrigger asChild>
143
+ {/* Tombol bundar `.circular` Adwaita: latar terisi, tanpa border. */}
144
+ <Button
145
+ type="button"
146
+ variant="secondary"
147
+ size="icon"
148
+ aria-label="More project actions"
149
+ className={cn("rounded-full", className)}
150
+ >
151
+ <MoreHorizontalIcon />
152
+ </Button>
153
+ </DropdownMenuTrigger>
154
+ </TooltipTrigger>
155
+ <TooltipContent>More</TooltipContent>
156
+ </Tooltip>
157
+ <DropdownMenuContent align="end" className="min-w-44">
158
+ <DropdownMenuItem onSelect={onReload} disabled={loading}>
159
+ <RefreshCwIcon className={cn(loading && "animate-spin")} />
160
+ Reload sessions
161
+ </DropdownMenuItem>
162
+ <DropdownMenuSeparator />
163
+ <DropdownMenuItem variant="destructive" onSelect={onDelete}>
164
+ <Trash2Icon />
165
+ Delete project
166
+ </DropdownMenuItem>
167
+ </DropdownMenuContent>
168
+ </DropdownMenu>
169
+ );
170
+ }
171
+
172
+ /**
173
+ * Empty state saat Project belum punya Session — pola AdwStatusPage: ikon
174
+ * simbolik besar redup, judul, deskripsi, satu tombol pil aksi utama.
175
+ */
176
+ function EmptySessions({ projectName, onStart }: { projectName: string; onStart: () => void }) {
177
+ return (
178
+ <div className="flex flex-col items-center gap-6 px-6 py-12 text-center sm:py-20">
179
+ <MessagesSquareIcon
180
+ className="size-16 text-muted-foreground/50"
181
+ strokeWidth={1.5}
182
+ aria-hidden
183
+ />
184
+
185
+ <div className="flex max-w-sm flex-col gap-2">
186
+ <h2 className="text-2xl font-bold tracking-tight">No Sessions Yet</h2>
187
+ <p className="text-[15px] leading-relaxed text-muted-foreground">
188
+ A session is a conversation with the agent inside{" "}
189
+ <span className="font-medium text-foreground">{projectName}</span>. It can read and edit
190
+ files here for you.
191
+ </p>
192
+ </div>
193
+
194
+ <Button type="button" onClick={onStart} className="h-10 rounded-full px-6">
195
+ <PlusIcon data-icon="inline-start" />
196
+ New session
197
+ </Button>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ export function SessionList({
203
+ project: initialProject,
204
+ onOpenSession,
205
+ onDeleted,
206
+ }: SessionListProps) {
207
+ /** Salinan lokal agar perubahan (mis. instruksi) langsung tampil. */
208
+ const [project, setProject] = useState(initialProject);
209
+ useEffect(() => setProject(initialProject), [initialProject]);
210
+ const list = useSessionList({ projectId: project.id, onDeleted });
211
+ /** Form pembuatan Session — dialog, dibuka lewat tombol. */
212
+ const [formOpen, setFormOpen] = useState(false);
213
+ const openForm = () => setFormOpen(true);
214
+
215
+ const hasSessions = !list.loading && !list.loadError && list.ordered.length > 0;
216
+ const allSelected =
217
+ list.sessions.length > 0 && list.sessions.every((s) => list.selected.has(s.id));
218
+
219
+ // Esc keluar dari selection mode (kecuali ada dialog terbuka — Esc
220
+ // miliknya). Daftar kosong (semua terhapus) juga mengakhiri mode ini.
221
+ const { selecting, stopSelecting, bulkDeleteOpen } = list;
222
+ useEffect(() => {
223
+ if (!selecting) return;
224
+ const onKey = (e: KeyboardEvent) => {
225
+ if (e.key !== "Escape" || bulkDeleteOpen || e.defaultPrevented) return;
226
+ if (document.querySelector("[role=dialog],[role=alertdialog]")) return;
227
+ stopSelecting();
228
+ };
229
+ window.addEventListener("keydown", onKey);
230
+ return () => window.removeEventListener("keydown", onKey);
231
+ }, [selecting, stopSelecting, bulkDeleteOpen]);
232
+ useEffect(() => {
233
+ if (selecting && !list.loading && list.sessions.length === 0) stopSelecting();
234
+ }, [selecting, stopSelecting, list.loading, list.sessions.length]);
235
+ /** Layar lebar: panel samping inline. Sempit: panel di Sheet (tombol header). */
236
+ const wide = useMediaQuery(WIDE_QUERY);
237
+ const [detailsOpen, setDetailsOpen] = useState(false);
238
+ const sidePanel = <ProjectSidePanel project={project} onProjectChange={setProject} />;
239
+
240
+ return (
241
+ // lg+: grid setinggi viewport; tiap kolom scroll sendiri (min-h-0).
242
+ <div className="grid grid-cols-1 gap-x-10 gap-y-8 pt-4 sm:pt-8 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_20rem] xl:grid-cols-[minmax(0,1fr)_22rem]">
243
+ <div className="flex min-w-0 flex-col gap-8 lg:min-h-0 lg:gap-0">
244
+ {/* Header tetap di atas; hanya daftar Session yang scroll (lg+). */}
245
+ <header className="flex shrink-0 flex-col gap-4 lg:pb-6">
246
+ <div className="flex items-start justify-between gap-4">
247
+ <div className="flex min-w-0 flex-col gap-1.5">
248
+ <h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">
249
+ {project.name}
250
+ </h1>
251
+ <CopyablePath path={project.path} />
252
+ </div>
253
+
254
+ {/* Desktop: aksi di kanan judul (panel detail sudah inline). */}
255
+ {wide && (
256
+ <div className="flex shrink-0 items-center gap-2">
257
+ <ProjectMenu
258
+ loading={list.loading}
259
+ onReload={() => void list.refresh()}
260
+ onDelete={list.openProjectDelete}
261
+ />
262
+ <Button type="button" onClick={openForm} className="rounded-full px-4">
263
+ <PlusIcon data-icon="inline-start" />
264
+ New session
265
+ </Button>
266
+ </div>
267
+ )}
268
+ </div>
269
+
270
+ {/* Layar sempit: toolbar aksi sekunder di bawah judul. Aksi utama
271
+ (New session) jadi tombol melayang kanan-bawah — area jempol. */}
272
+ {!wide && (
273
+ <div className="flex items-center justify-between gap-2">
274
+ <Button
275
+ type="button"
276
+ variant="secondary"
277
+ onClick={() => setDetailsOpen(true)}
278
+ aria-label="Project details: instructions, MCP servers, skills"
279
+ className="h-10 rounded-full px-3.5"
280
+ >
281
+ <PanelRightIcon data-icon="inline-start" />
282
+ Details
283
+ </Button>
284
+ <ProjectMenu
285
+ className="size-10"
286
+ loading={list.loading}
287
+ onReload={() => void list.refresh()}
288
+ onDelete={list.openProjectDelete}
289
+ />
290
+ </div>
291
+ )}
292
+ </header>
293
+
294
+ <section
295
+ aria-label="Sessions"
296
+ // pb-24 (mobile): ruang agar baris terakhir tidak tertutup FAB.
297
+ className="-mx-3 px-3 pb-24 lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-contain lg:pb-16"
298
+ >
299
+ {list.loading ? (
300
+ <SessionListSkeleton />
301
+ ) : list.loadError ? (
302
+ <PrefsGroup title="Sessions">
303
+ <ActionRow
304
+ prefix={<TriangleAlertIcon className="size-[18px] text-destructive" aria-hidden />}
305
+ title="Failed to load sessions"
306
+ subtitle={list.loadError}
307
+ suffix={
308
+ <Button
309
+ type="button"
310
+ variant="outline"
311
+ size="sm"
312
+ onClick={() => void list.refresh()}
313
+ >
314
+ <RefreshCwIcon data-icon="inline-start" />
315
+ Retry
316
+ </Button>
317
+ }
318
+ />
319
+ </PrefsGroup>
320
+ ) : hasSessions ? (
321
+ <>
322
+ {/* AdwPreferencesGroup: ringkasan status sebagai deskripsi grup,
323
+ Session sebagai boxed list. Tombol "Select" masuk selection
324
+ mode (pola Nautilus/Contacts) untuk hapus massal. */}
325
+ <PrefsGroup
326
+ title="Sessions"
327
+ description={
328
+ list.selecting
329
+ ? describeSelection(list.selected.size, list.sessions.length)
330
+ : describeSessionSummary(list.summary)
331
+ }
332
+ suffix={
333
+ list.selecting ? (
334
+ <Button
335
+ type="button"
336
+ variant="ghost"
337
+ size="sm"
338
+ onClick={list.toggleSelectAll}
339
+ disabled={list.bulkProgress !== null}
340
+ >
341
+ {allSelected ? "Deselect All" : "Select All"}
342
+ </Button>
343
+ ) : (
344
+ <Button
345
+ type="button"
346
+ variant="ghost"
347
+ size="sm"
348
+ onClick={list.startSelecting}
349
+ aria-label="Select sessions"
350
+ >
351
+ <ListChecksIcon data-icon="inline-start" />
352
+ Select
353
+ </Button>
354
+ )
355
+ }
356
+ >
357
+ {list.ordered.map((session) => (
358
+ <li key={session.id}>
359
+ <SessionRow
360
+ session={session}
361
+ busy={
362
+ list.stopping === session.id ||
363
+ list.starting === session.id ||
364
+ list.deleting === session.id
365
+ }
366
+ onOpen={() => onOpenSession(session)}
367
+ onStop={() => void list.stop(session.id)}
368
+ onStart={() => void list.start(session.id)}
369
+ onDelete={() => list.requestDelete(session)}
370
+ selectionMode={list.selecting}
371
+ selected={list.selected.has(session.id)}
372
+ onToggleSelect={() => list.toggleSelect(session.id)}
373
+ />
374
+ </li>
375
+ ))}
376
+ </PrefsGroup>
377
+
378
+ {/* Action bar selection mode (GtkActionBar): melayang di dasar
379
+ area scroll — jumlah terpilih, Cancel, Delete (destruktif). */}
380
+ {list.selecting && (
381
+ <div className="sticky bottom-[max(1rem,env(safe-area-inset-bottom))] z-20 mt-4 flex items-center gap-2 rounded-2xl border bg-popover py-2 pr-2 pl-4 shadow-lg">
382
+ <span className="min-w-0 truncate text-sm font-medium" aria-live="polite">
383
+ {list.selected.size === 0
384
+ ? "None selected"
385
+ : `${pluralSessions(list.selected.size)} selected`}
386
+ </span>
387
+ <Button
388
+ type="button"
389
+ variant="ghost"
390
+ size="sm"
391
+ className="ml-auto"
392
+ onClick={list.stopSelecting}
393
+ disabled={list.bulkProgress !== null}
394
+ >
395
+ Cancel
396
+ </Button>
397
+ <Button
398
+ type="button"
399
+ variant="destructive"
400
+ size="sm"
401
+ className="rounded-full px-3.5"
402
+ onClick={list.requestBulkDelete}
403
+ disabled={list.selected.size === 0 || list.bulkProgress !== null}
404
+ >
405
+ <Trash2Icon data-icon="inline-start" />
406
+ Delete
407
+ </Button>
408
+ </div>
409
+ )}
410
+ </>
411
+ ) : (
412
+ <EmptySessions projectName={project.name} onStart={openForm} />
413
+ )}
414
+ </section>
415
+ </div>
416
+
417
+ {/* Panel samping: Instructions, MCP, Skills (gaya Claude Projects). */}
418
+ {wide ? (
419
+ <aside
420
+ aria-label="Project details"
421
+ // Panel mandiri: padding sama di keempat sisi; scrollbar-gutter
422
+ // stable di kedua tepi agar scrollbar tidak menggeser isi ke kiri.
423
+ // Tanpa kotak pembungkus: tiap seksi sudah berupa kartu sendiri.
424
+ // Scroll mandiri; pr kecil agar scrollbar tidak menempel ke kartu.
425
+ className="-mr-2 min-h-0 overflow-y-auto overscroll-contain pr-2 pb-6 [scrollbar-width:thin]"
426
+ >
427
+ {sidePanel}
428
+ </aside>
429
+ ) : (
430
+ <Sheet open={detailsOpen} onOpenChange={setDetailsOpen}>
431
+ <SheetContent side="right" className="w-[88vw] gap-0 overflow-y-auto sm:max-w-sm">
432
+ <SheetHeader className="pb-2">
433
+ <SheetTitle>{project.name}</SheetTitle>
434
+ <SheetDescription>Instructions, MCP servers, and skills.</SheetDescription>
435
+ </SheetHeader>
436
+ <div className="px-4 pt-2 pb-6">{sidePanel}</div>
437
+ </SheetContent>
438
+ </Sheet>
439
+ )}
440
+
441
+ {/* Mobile/tablet: aksi utama melayang di kanan-bawah (jangkauan jempol,
442
+ selalu terlihat saat scroll). Label teks tetap ada agar jelas —
443
+ bukan ikon "+" tanpa arti. Safe-area iOS dihormati. */}
444
+ {/* Disembunyikan saat selection mode: tempatnya dipakai action bar. */}
445
+ {!wide && !list.selecting && (
446
+ <Button
447
+ type="button"
448
+ onClick={openForm}
449
+ className="fixed right-4 bottom-[max(1rem,env(safe-area-inset-bottom))] z-30 h-12 rounded-full px-5 text-[15px] shadow-lg shadow-primary/25 sm:right-6 sm:bottom-6"
450
+ >
451
+ <PlusIcon data-icon="inline-start" className="size-5" />
452
+ New session
453
+ </Button>
454
+ )}
455
+
456
+ {/* Form pembuatan Session — dialog agar daftar tidak terdorong di HP */}
457
+ <NewSessionDialog
458
+ open={formOpen}
459
+ onOpenChange={setFormOpen}
460
+ projectId={project.id}
461
+ projectName={project.name}
462
+ onCreated={(session) => {
463
+ void list.refresh();
464
+ notifyDataChanged();
465
+ // Session baru langsung dibuka — siap menerima prompt pertama.
466
+ onOpenSession(session);
467
+ }}
468
+ />
469
+
470
+ {/* Konfirmasi hapus Session permanen */}
471
+ <ConfirmSessionDeleteDialog
472
+ open={list.pendingDelete !== null}
473
+ onOpenChange={(open) => {
474
+ if (!open) list.cancelDelete();
475
+ }}
476
+ deleting={list.deleting !== null}
477
+ onConfirm={() => void list.confirmRemove()}
478
+ />
479
+
480
+ {/* Konfirmasi hapus massal (selection mode) — progres di tombol. */}
481
+ <ConfirmSessionDeleteDialog
482
+ open={list.bulkDeleteOpen}
483
+ onOpenChange={(open) => {
484
+ if (!open) list.cancelBulkDelete();
485
+ }}
486
+ count={list.selected.size}
487
+ deleting={list.bulkProgress !== null}
488
+ progress={list.bulkProgress}
489
+ onConfirm={() => void list.confirmBulkRemove()}
490
+ />
491
+
492
+ {/* Konfirmasi hapus Project (beserta seluruh Session-nya) */}
493
+ <ConfirmDeleteProjectDialog
494
+ open={list.projectDeleteOpen}
495
+ onOpenChange={(open) => (open ? list.openProjectDelete() : list.closeProjectDelete())}
496
+ projectName={project.name}
497
+ sessionTotal={list.summary.total}
498
+ deleting={list.deletingProject}
499
+ onConfirm={() => void list.confirmRemoveProject()}
500
+ />
501
+ </div>
502
+ );
503
+ }