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,175 @@
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
+ sessionId={session.id}
79
+ model={model}
80
+ onChanged={setModel}
81
+ variant="heading"
82
+ />
83
+ <span className="hidden truncate px-2 font-mono text-[11px] text-muted-foreground sm:block">
84
+ {session.agentType} · {session.id.slice(0, 8)}
85
+ {wsStatus !== "connected" && ` · ${wsStatusLabel(wsStatus)}`}
86
+ </span>
87
+ </div>
88
+
89
+ {/* Status = teks biasa + titik (bukan pil yang terlihat seperti
90
+ tombol). Di HP cukup titiknya; label teks mulai dari sm. */}
91
+ <span
92
+ role="status"
93
+ className="flex shrink-0 items-center gap-1.5 px-1 text-xs font-medium text-muted-foreground"
94
+ title={`Session ${SESSION_STATUS_LABEL[status].toLowerCase()}`}
95
+ >
96
+ <span className="relative flex size-2" aria-hidden>
97
+ {status === "running" && (
98
+ <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500/60 motion-reduce:hidden" />
99
+ )}
100
+ <span
101
+ className={cn("relative inline-flex size-2 rounded-full", SESSION_STATUS_DOT[status])}
102
+ />
103
+ </span>
104
+ <span className="sr-only sm:not-sr-only">{SESSION_STATUS_LABEL[status]}</span>
105
+ </span>
106
+
107
+ {/* Stop/Start berlabel teks agar jelas fungsinya (bukan ikon kotak
108
+ tanpa arti). Di HP label disembunyikan, tapi tombol tetap punya
109
+ aria-label & ukuran sentuh 40px. Saat model sedang merespon,
110
+ penghentian balasan ada di composer. */}
111
+ {status === "running" ? (
112
+ <Button
113
+ type="button"
114
+ variant="outline"
115
+ size="sm"
116
+ onClick={onStop}
117
+ disabled={stopping}
118
+ aria-label="Stop session"
119
+ 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"
120
+ >
121
+ {stopping ? (
122
+ <Spinner data-icon="inline-start" />
123
+ ) : (
124
+ <SquareIcon data-icon="inline-start" className="size-3.5 fill-current" />
125
+ )}
126
+ <span className="hidden sm:inline">{stopping ? "Stopping…" : "Stop"}</span>
127
+ </Button>
128
+ ) : (
129
+ <Button
130
+ type="button"
131
+ size="sm"
132
+ onClick={onStart}
133
+ disabled={starting}
134
+ aria-label={status === "crashed" ? "Restart session" : "Start session"}
135
+ 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"
136
+ >
137
+ {starting ? (
138
+ <Spinner data-icon="inline-start" />
139
+ ) : (
140
+ <PlayIcon data-icon="inline-start" className="size-3.5 fill-current" />
141
+ )}
142
+ <span className="hidden sm:inline">
143
+ {starting ? "Starting…" : status === "crashed" ? "Restart" : "Start"}
144
+ </span>
145
+ </Button>
146
+ )}
147
+
148
+ <DropdownMenu>
149
+ <DropdownMenuTrigger asChild>
150
+ <Button
151
+ type="button"
152
+ variant="ghost"
153
+ size="icon"
154
+ aria-label="Session actions"
155
+ className="size-10 shrink-0 sm:size-9"
156
+ >
157
+ <MoreVerticalIcon />
158
+ </Button>
159
+ </DropdownMenuTrigger>
160
+ <DropdownMenuContent align="end">
161
+ <DropdownMenuItem onSelect={toggleTheme}>
162
+ {isDark ? <SunIcon /> : <MoonIcon />}
163
+ {isDark ? "Light mode" : "Dark mode"}
164
+ </DropdownMenuItem>
165
+ <DropdownMenuSeparator />
166
+ <DropdownMenuItem variant="destructive" onSelect={onRequestDelete}>
167
+ <Trash2Icon />
168
+ Delete session
169
+ </DropdownMenuItem>
170
+ </DropdownMenuContent>
171
+ </DropdownMenu>
172
+ </div>
173
+ </header>
174
+ );
175
+ }
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Daftar & pembuatan Session milik sebuah Project (Requirement 1.1, 1.3, 1.5).
3
+ *
4
+ * Halaman detail Project — sengaja minimal: tipografi & ruang kosong yang
5
+ * bekerja, bukan kartu/tile.
6
+ * - Header: nama Project besar, path (salin saat hover), satu baris ringkasan
7
+ * ("3 sessions · 1 running"). Aksi: "New session" + menu "..." Reload/Delete
8
+ * (`ProjectMenu`; hapus tetap lewat dialog konfirmasi). Desktop: di
9
+ * kanan judul. Layar sempit: "New session" jadi tombol melayang kanan-bawah
10
+ * (area jempol); toolbar di bawah judul berisi "Details" (panel samping)
11
+ * dan grup Reload/Delete.
12
+ * - Panel samping (`ProjectSidePanel`, gaya Claude Projects): custom
13
+ * instruction, MCP server, dan skill. Layar `lg`+ = kolom kanan sticky;
14
+ * lebih sempit = Sheet dari kanan lewat tombol di header.
15
+ * Tanpa tombol back — navigasi lewat sidebar.
16
+ * - Daftar Session: baris polos dipisah hairline, status lewat titik warna.
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
+ MessagesSquareIcon,
30
+ MoreHorizontalIcon,
31
+ PanelRightIcon,
32
+ PlusIcon,
33
+ RefreshCwIcon,
34
+ Trash2Icon,
35
+ } from "lucide-react";
36
+ import { useEffect, useState } from "react";
37
+ import { toast } from "sonner";
38
+ import { ProjectSidePanel } from "@/components/projects/ProjectSidePanel";
39
+ import { ConfirmDeleteProjectDialog } from "@/components/sessions/ConfirmDeleteProjectDialog";
40
+ import { ConfirmSessionDeleteDialog } from "@/components/sessions/ConfirmSessionDeleteDialog";
41
+ import { NewSessionDialog } from "@/components/sessions/NewSessionDialog";
42
+ import { SessionListSkeleton, SessionRow } from "@/components/sessions/SessionRow";
43
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
44
+ import { Button } from "@/components/ui/button";
45
+ import {
46
+ DropdownMenu,
47
+ DropdownMenuContent,
48
+ DropdownMenuItem,
49
+ DropdownMenuSeparator,
50
+ DropdownMenuTrigger,
51
+ } from "@/components/ui/dropdown-menu";
52
+ import {
53
+ Sheet,
54
+ SheetContent,
55
+ SheetDescription,
56
+ SheetHeader,
57
+ SheetTitle,
58
+ } from "@/components/ui/sheet";
59
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
60
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
61
+ import { useSessionList } from "@/hooks/useSessionList";
62
+ import { notifyDataChanged } from "@/lib/data-events";
63
+ import { describeSessionSummary } from "@/lib/session-summary";
64
+ import { cn } from "@/lib/utils";
65
+ import type { Project, Session } from "@/types";
66
+
67
+ /** Breakpoint `lg` Tailwind — di atasnya panel samping tampil inline. */
68
+ const WIDE_QUERY = "(min-width: 1024px)";
69
+
70
+ export interface SessionListProps {
71
+ project: Project;
72
+ onOpenSession: (session: Session) => void;
73
+ /** Dipanggil setelah Project dihapus, untuk kembali ke daftar Project. */
74
+ onDeleted?: () => void;
75
+ }
76
+
77
+ /** Path Project yang bisa diklik untuk disalin — ikon muncul saat hover. */
78
+ function CopyablePath({ path }: { path: string }) {
79
+ const [copied, setCopied] = useState(false);
80
+
81
+ useEffect(() => {
82
+ if (!copied) return;
83
+ const t = setTimeout(() => setCopied(false), 1500);
84
+ return () => clearTimeout(t);
85
+ }, [copied]);
86
+
87
+ const copy = async () => {
88
+ try {
89
+ await navigator.clipboard.writeText(path);
90
+ setCopied(true);
91
+ toast.success("Path copied.");
92
+ } catch {
93
+ // Clipboard API butuh konteks aman (HTTPS / localhost).
94
+ toast.error("Couldn't copy. Select the path and copy it manually.");
95
+ }
96
+ };
97
+
98
+ return (
99
+ <button
100
+ type="button"
101
+ onClick={() => void copy()}
102
+ title={path}
103
+ aria-label={copied ? "Path copied" : `Copy path ${path}`}
104
+ 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"
105
+ >
106
+ <span className="truncate">{path}</span>
107
+ {copied ? (
108
+ <CheckIcon className="size-3 shrink-0 text-emerald-500" aria-hidden />
109
+ ) : (
110
+ <CopyIcon
111
+ className="size-3 shrink-0 opacity-0 transition-opacity group-hover/path:opacity-100 group-focus-visible/path:opacity-100"
112
+ aria-hidden
113
+ />
114
+ )}
115
+ </button>
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Aksi sekunder Project (Reload, Delete) di satu menu "..." — Delete jarang
121
+ * dipakai & berbahaya, jadi tidak perlu tombol sendiri yang selalu terlihat.
122
+ * Delete tetap lewat dialog konfirmasi.
123
+ */
124
+ function ProjectMenu({
125
+ loading,
126
+ onReload,
127
+ onDelete,
128
+ className,
129
+ }: {
130
+ loading: boolean;
131
+ onReload: () => void;
132
+ onDelete: () => void;
133
+ className?: string;
134
+ }) {
135
+ return (
136
+ <DropdownMenu>
137
+ <Tooltip>
138
+ <TooltipTrigger asChild>
139
+ <DropdownMenuTrigger asChild>
140
+ <Button
141
+ type="button"
142
+ variant="outline"
143
+ size="icon"
144
+ aria-label="More project actions"
145
+ className={cn("rounded-full text-muted-foreground", className)}
146
+ >
147
+ <MoreHorizontalIcon />
148
+ </Button>
149
+ </DropdownMenuTrigger>
150
+ </TooltipTrigger>
151
+ <TooltipContent>More</TooltipContent>
152
+ </Tooltip>
153
+ <DropdownMenuContent align="end" className="min-w-44">
154
+ <DropdownMenuItem onSelect={onReload} disabled={loading}>
155
+ <RefreshCwIcon className={cn(loading && "animate-spin")} />
156
+ Reload sessions
157
+ </DropdownMenuItem>
158
+ <DropdownMenuSeparator />
159
+ <DropdownMenuItem variant="destructive" onSelect={onDelete}>
160
+ <Trash2Icon />
161
+ Delete project
162
+ </DropdownMenuItem>
163
+ </DropdownMenuContent>
164
+ </DropdownMenu>
165
+ );
166
+ }
167
+
168
+ /**
169
+ * Empty state saat Project belum punya Session: kartu bergaris putus-putus
170
+ * (ikon, judul, penjelasan singkat, satu aksi utama).
171
+ */
172
+ function EmptySessions({ projectName, onStart }: { projectName: string; onStart: () => void }) {
173
+ return (
174
+ <div className="flex flex-col items-center gap-6 rounded-2xl border border-dashed bg-muted/20 px-6 py-12 text-center sm:py-16">
175
+ <span
176
+ className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary"
177
+ aria-hidden
178
+ >
179
+ <MessagesSquareIcon className="size-7" />
180
+ </span>
181
+
182
+ <div className="flex max-w-sm flex-col gap-1.5">
183
+ <h2 className="text-lg font-semibold tracking-tight">Start your first session</h2>
184
+ <p className="text-sm leading-relaxed text-muted-foreground">
185
+ A session is a conversation with the agent inside{" "}
186
+ <span className="font-medium text-foreground">{projectName}</span>. It can read and edit
187
+ files here for you.
188
+ </p>
189
+ </div>
190
+
191
+ <Button type="button" onClick={onStart} className="rounded-full px-5">
192
+ <PlusIcon data-icon="inline-start" />
193
+ New session
194
+ </Button>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export function SessionList({
200
+ project: initialProject,
201
+ onOpenSession,
202
+ onDeleted,
203
+ }: SessionListProps) {
204
+ /** Salinan lokal agar perubahan (mis. instruksi) langsung tampil. */
205
+ const [project, setProject] = useState(initialProject);
206
+ useEffect(() => setProject(initialProject), [initialProject]);
207
+ const list = useSessionList({ projectId: project.id, onDeleted });
208
+ /** Form pembuatan Session — dialog, dibuka lewat tombol. */
209
+ const [formOpen, setFormOpen] = useState(false);
210
+ const openForm = () => setFormOpen(true);
211
+
212
+ const hasSessions = !list.loading && !list.loadError && list.ordered.length > 0;
213
+ /** Layar lebar: panel samping inline. Sempit: panel di Sheet (tombol header). */
214
+ const wide = useMediaQuery(WIDE_QUERY);
215
+ const [detailsOpen, setDetailsOpen] = useState(false);
216
+ const sidePanel = <ProjectSidePanel project={project} onProjectChange={setProject} />;
217
+
218
+ return (
219
+ // lg+: grid setinggi viewport; tiap kolom scroll sendiri (min-h-0).
220
+ <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]">
221
+ <div className="flex min-w-0 flex-col gap-8 lg:min-h-0 lg:gap-0">
222
+ {/* Header tetap di atas; hanya daftar Session yang scroll (lg+). */}
223
+ <header className="flex shrink-0 flex-col gap-4 lg:pb-6">
224
+ <div className="flex items-start justify-between gap-4">
225
+ <div className="flex min-w-0 flex-col gap-1.5">
226
+ <h1 className="truncate text-2xl font-semibold tracking-tight sm:text-3xl">
227
+ {project.name}
228
+ </h1>
229
+ <CopyablePath path={project.path} />
230
+ </div>
231
+
232
+ {/* Desktop: aksi di kanan judul (panel detail sudah inline). */}
233
+ {wide && (
234
+ <div className="flex shrink-0 items-center gap-2">
235
+ <ProjectMenu
236
+ loading={list.loading}
237
+ onReload={() => void list.refresh()}
238
+ onDelete={list.openProjectDelete}
239
+ />
240
+ <Button type="button" onClick={openForm} className="rounded-full px-4">
241
+ <PlusIcon data-icon="inline-start" />
242
+ New session
243
+ </Button>
244
+ </div>
245
+ )}
246
+ </div>
247
+
248
+ {/* Satu baris ringkasan — cukup teks, tanpa tile. Disembunyikan saat
249
+ kosong: empty state di bawah sudah menyampaikannya. */}
250
+ {!list.loading && !list.loadError && list.summary.total > 0 && (
251
+ <p className="text-sm text-muted-foreground">{describeSessionSummary(list.summary)}</p>
252
+ )}
253
+
254
+ {/* Layar sempit: toolbar aksi sekunder di bawah judul. Aksi utama
255
+ (New session) jadi tombol melayang kanan-bawah — area jempol. */}
256
+ {!wide && (
257
+ <div className="flex items-center justify-between gap-2">
258
+ <Button
259
+ type="button"
260
+ variant="outline"
261
+ onClick={() => setDetailsOpen(true)}
262
+ aria-label="Project details: instructions, MCP servers, skills"
263
+ className="h-10 rounded-full px-3.5"
264
+ >
265
+ <PanelRightIcon data-icon="inline-start" />
266
+ Details
267
+ </Button>
268
+ <ProjectMenu
269
+ className="size-10"
270
+ loading={list.loading}
271
+ onReload={() => void list.refresh()}
272
+ onDelete={list.openProjectDelete}
273
+ />
274
+ </div>
275
+ )}
276
+ </header>
277
+
278
+ <section
279
+ aria-label="Sessions"
280
+ // pb-24 (mobile): ruang agar baris terakhir tidak tertutup FAB.
281
+ className="-mx-3 px-3 pb-24 lg:min-h-0 lg:flex-1 lg:overflow-y-auto lg:overscroll-contain lg:pb-16"
282
+ >
283
+ {list.loading ? (
284
+ <SessionListSkeleton />
285
+ ) : list.loadError ? (
286
+ <Alert variant="destructive">
287
+ <AlertTitle>Failed to load sessions</AlertTitle>
288
+ <AlertDescription className="flex flex-col items-start gap-3">
289
+ {list.loadError}
290
+ <Button
291
+ type="button"
292
+ variant="outline"
293
+ size="sm"
294
+ onClick={() => void list.refresh()}
295
+ >
296
+ <RefreshCwIcon data-icon="inline-start" />
297
+ Retry
298
+ </Button>
299
+ </AlertDescription>
300
+ </Alert>
301
+ ) : hasSessions ? (
302
+ <ul className="-mx-3 flex flex-col">
303
+ {list.ordered.map((session) => (
304
+ <li key={session.id}>
305
+ <SessionRow
306
+ session={session}
307
+ busy={
308
+ list.stopping === session.id ||
309
+ list.starting === session.id ||
310
+ list.deleting === session.id
311
+ }
312
+ onOpen={() => onOpenSession(session)}
313
+ onStop={() => void list.stop(session.id)}
314
+ onStart={() => void list.start(session.id)}
315
+ onDelete={() => list.requestDelete(session)}
316
+ />
317
+ </li>
318
+ ))}
319
+ </ul>
320
+ ) : (
321
+ <EmptySessions projectName={project.name} onStart={openForm} />
322
+ )}
323
+ </section>
324
+ </div>
325
+
326
+ {/* Panel samping: Instructions, MCP, Skills (gaya Claude Projects). */}
327
+ {wide ? (
328
+ <aside
329
+ aria-label="Project details"
330
+ // Panel mandiri: padding sama di keempat sisi; scrollbar-gutter
331
+ // stable di kedua tepi agar scrollbar tidak menggeser isi ke kiri.
332
+ // Tanpa kotak pembungkus: tiap seksi sudah berupa kartu sendiri.
333
+ // Scroll mandiri; pr kecil agar scrollbar tidak menempel ke kartu.
334
+ className="-mr-2 min-h-0 overflow-y-auto overscroll-contain pr-2 pb-6 [scrollbar-width:thin]"
335
+ >
336
+ {sidePanel}
337
+ </aside>
338
+ ) : (
339
+ <Sheet open={detailsOpen} onOpenChange={setDetailsOpen}>
340
+ <SheetContent side="right" className="w-[88vw] gap-0 overflow-y-auto sm:max-w-sm">
341
+ <SheetHeader className="pb-2">
342
+ <SheetTitle>{project.name}</SheetTitle>
343
+ <SheetDescription>Instructions, MCP servers, and skills.</SheetDescription>
344
+ </SheetHeader>
345
+ <div className="px-4 pt-2 pb-6">{sidePanel}</div>
346
+ </SheetContent>
347
+ </Sheet>
348
+ )}
349
+
350
+ {/* Mobile/tablet: aksi utama melayang di kanan-bawah (jangkauan jempol,
351
+ selalu terlihat saat scroll). Label teks tetap ada agar jelas —
352
+ bukan ikon "+" tanpa arti. Safe-area iOS dihormati. */}
353
+ {!wide && (
354
+ <Button
355
+ type="button"
356
+ onClick={openForm}
357
+ 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"
358
+ >
359
+ <PlusIcon data-icon="inline-start" className="size-5" />
360
+ New session
361
+ </Button>
362
+ )}
363
+
364
+ {/* Form pembuatan Session — dialog agar daftar tidak terdorong di HP */}
365
+ <NewSessionDialog
366
+ open={formOpen}
367
+ onOpenChange={setFormOpen}
368
+ projectId={project.id}
369
+ onCreated={() => {
370
+ void list.refresh();
371
+ notifyDataChanged();
372
+ }}
373
+ />
374
+
375
+ {/* Konfirmasi hapus Session permanen */}
376
+ <ConfirmSessionDeleteDialog
377
+ open={list.pendingDelete !== null}
378
+ onOpenChange={(open) => {
379
+ if (!open) list.cancelDelete();
380
+ }}
381
+ deleting={list.deleting !== null}
382
+ onConfirm={() => void list.confirmRemove()}
383
+ />
384
+
385
+ {/* Konfirmasi hapus Project (beserta seluruh Session-nya) */}
386
+ <ConfirmDeleteProjectDialog
387
+ open={list.projectDeleteOpen}
388
+ onOpenChange={(open) => (open ? list.openProjectDelete() : list.closeProjectDelete())}
389
+ projectName={project.name}
390
+ sessionTotal={list.summary.total}
391
+ deleting={list.deletingProject}
392
+ onConfirm={() => void list.confirmRemoveProject()}
393
+ />
394
+ </div>
395
+ );
396
+ }