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,250 @@
1
+ /**
2
+ * Chat homepage: headline acak + logo, lalu composer. Di bawah kotak input
3
+ * ada baris polos (tanpa border): pemilih Project di kiri, model di kanan.
4
+ *
5
+ * Kirim = buat Session baru di Project terpilih (`POST /api/sessions`),
6
+ * titipkan teks sebagai prompt tertunda (`lib/pending-prompt.ts`), lalu
7
+ * pindah ke Session view — `useSessionChat` mengirimnya sekali setelah
8
+ * koneksi WS ter-attach. Project terakhir dipakai diingat di localStorage.
9
+ *
10
+ * Mode agent ada di dalam kotak input, kiri bawah (seperti composer Session).
11
+ * Pemilih model & agent sama dengan Session (`ModelPicker`, `AgentPicker`) —
12
+ * tanpa Session, pilihan dikirim di body `POST /api/sessions`.
13
+ * Keduanya di-reset saat Project berganti karena daftarnya per Project.
14
+ */
15
+ import {
16
+ CheckIcon,
17
+ ChevronDownIcon,
18
+ FolderIcon,
19
+ FolderPlusIcon,
20
+ SendHorizontalIcon,
21
+ } from "lucide-react";
22
+ import { useEffect, useRef, useState } from "react";
23
+ import { AgentPicker } from "@/components/sessions/AgentPicker";
24
+ import { ModelPicker } from "@/components/sessions/ModelPicker";
25
+ import { Alert, AlertDescription } from "@/components/ui/alert";
26
+ import { Button } from "@/components/ui/button";
27
+ import {
28
+ DropdownMenu,
29
+ DropdownMenuContent,
30
+ DropdownMenuGroup,
31
+ DropdownMenuItem,
32
+ DropdownMenuLabel,
33
+ DropdownMenuSeparator,
34
+ DropdownMenuTrigger,
35
+ } from "@/components/ui/dropdown-menu";
36
+ import {
37
+ InputGroup,
38
+ InputGroupAddon,
39
+ InputGroupButton,
40
+ InputGroupTextarea,
41
+ } from "@/components/ui/input-group";
42
+ import { Spinner } from "@/components/ui/spinner";
43
+ import { useRouter } from "@/hooks/useRouter";
44
+ import { ApiError, apiFetch } from "@/lib/api";
45
+ import { notifyDataChanged } from "@/lib/data-events";
46
+ import { pickHeadline } from "@/lib/headlines";
47
+ import { setPendingPrompt } from "@/lib/pending-prompt";
48
+ import type { ProjectOverview } from "@/lib/project-overview";
49
+ import { sessionPath } from "@/lib/routes";
50
+ import { seedSession } from "@/lib/session-handoff";
51
+ import logo from "@/logo.svg";
52
+ import type { Session, SessionModel } from "@/types";
53
+
54
+ const LAST_PROJECT_KEY = "kcg-home-project";
55
+
56
+ export interface HomeChatProps {
57
+ /** Project terurut aktivitas terbaru (`buildProjectOverviews`). */
58
+ overviews: ProjectOverview[];
59
+ /** Buka dialog pembuatan Project baru. */
60
+ onNewProject: () => void;
61
+ }
62
+
63
+ function readLastProject(): string | null {
64
+ return typeof localStorage === "undefined" ? null : localStorage.getItem(LAST_PROJECT_KEY);
65
+ }
66
+
67
+ export function HomeChat({ overviews, onNewProject }: HomeChatProps) {
68
+ const { navigate } = useRouter();
69
+ // Dipilih sekali per kunjungan — tidak berganti saat re-render.
70
+ const [headline] = useState(() => pickHeadline());
71
+ const [text, setText] = useState("");
72
+ const [projectId, setProjectId] = useState<string | null>(readLastProject);
73
+ const [sending, setSending] = useState(false);
74
+ const [error, setError] = useState<string | null>(null);
75
+ /** Model & agent untuk Session baru; null = default opencode. */
76
+ const [model, setModel] = useState<SessionModel | null>(null);
77
+ const [agent, setAgent] = useState<string | null>(null);
78
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
79
+
80
+ // Project tersimpan sudah dihapus / belum ada -> pakai yang paling baru aktif.
81
+ const selected =
82
+ overviews.find((o) => o.project.id === projectId)?.project ?? overviews[0]?.project ?? null;
83
+
84
+ useEffect(() => {
85
+ textareaRef.current?.focus();
86
+ }, []);
87
+
88
+ const choose = (id: string) => {
89
+ if (id !== selected?.id) {
90
+ // Model/agent tersedia per Project (config opencode masing-masing).
91
+ setModel(null);
92
+ setAgent(null);
93
+ }
94
+ setProjectId(id);
95
+ if (typeof localStorage !== "undefined") localStorage.setItem(LAST_PROJECT_KEY, id);
96
+ requestAnimationFrame(() => textareaRef.current?.focus());
97
+ };
98
+
99
+ const canSend = selected !== null && text.trim() !== "" && !sending;
100
+
101
+ const submit = async (event?: React.FormEvent<HTMLFormElement>) => {
102
+ event?.preventDefault();
103
+ if (!canSend || selected === null) return;
104
+ setSending(true);
105
+ setError(null);
106
+ try {
107
+ const res = await apiFetch("/api/sessions", {
108
+ method: "POST",
109
+ body: JSON.stringify({ agentType: "opencode", projectId: selected.id, model, agent }),
110
+ });
111
+ const { session } = (await res.json()) as { session: Session };
112
+ setPendingPrompt(session.id, text);
113
+ // Session view dirender langsung dari data ini (tanpa fetch ulang),
114
+ // lalu composer "berpindah" ke dasar layar lewat View Transition.
115
+ seedSession(session);
116
+ notifyDataChanged();
117
+ navigate(sessionPath(selected.id, session.id), { transition: "to-session" });
118
+ } catch (e) {
119
+ setError(e instanceof ApiError ? e.message : "Failed to start a new session");
120
+ setSending(false);
121
+ }
122
+ };
123
+
124
+ return (
125
+ <div className="mx-auto flex w-full max-w-2xl flex-1 flex-col justify-center gap-6 pb-[10dvh]">
126
+ <div data-vt-name="home-headline" className="flex items-center justify-center gap-3 px-2">
127
+ <img src={logo} alt="" aria-hidden className="size-10 shrink-0 sm:size-12" />
128
+ <h1 className="text-balance text-2xl font-semibold tracking-tight sm:text-3xl">
129
+ {headline}
130
+ </h1>
131
+ </div>
132
+
133
+ <form onSubmit={submit} className="flex flex-col gap-3">
134
+ {/* Container luar: hanya border (tanpa latar), membungkus dua section —
135
+ kotak input berlatar di atas, baris Project/model polos di bawah. */}
136
+ {/* `data-vt-name`: selama transisi "to-session" kotak ini dan area
137
+ Session view berbagi nama view-transition -> kotak composer
138
+ MELUAS menjadi halaman Session (container transform). */}
139
+ <div data-vt-name="session-surface" className="flex flex-col rounded-2xl border">
140
+ {/* Section 1 — kotak input: textarea + baris aksi (mode agent kiri,
141
+ kirim kanan), tata letak sama dengan composer Session. `-m-px`
142
+ menumpuk border kotak ini di atas border container luar. */}
143
+ <InputGroup className="-m-px w-auto rounded-2xl bg-card shadow-sm dark:bg-card">
144
+ <InputGroupTextarea
145
+ ref={textareaRef}
146
+ value={text}
147
+ onChange={(e) => setText(e.target.value)}
148
+ onKeyDown={(e) => {
149
+ // Enter kirim, Shift+Enter baris baru (abaikan saat komposisi IME).
150
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
151
+ e.preventDefault();
152
+ void submit();
153
+ }
154
+ }}
155
+ placeholder={selected ? `Ask anything in ${selected.name}…` : "Pick a project first"}
156
+ aria-label="Message"
157
+ disabled={sending}
158
+ rows={2}
159
+ className="max-h-60 min-h-20 px-4 text-base leading-6 md:text-base"
160
+ />
161
+ <InputGroupAddon align="block-end" className="gap-2 px-2 pb-2">
162
+ {selected && (
163
+ <AgentPicker
164
+ projectId={selected.id}
165
+ sessionId={null}
166
+ agent={agent}
167
+ onChanged={setAgent}
168
+ disabled={sending}
169
+ />
170
+ )}
171
+ <InputGroupButton
172
+ type="submit"
173
+ size="icon-sm"
174
+ variant="default"
175
+ className="ml-auto size-9 rounded-full"
176
+ disabled={!canSend}
177
+ aria-label="Send"
178
+ >
179
+ {sending ? <Spinner /> : <SendHorizontalIcon />}
180
+ </InputGroupButton>
181
+ </InputGroupAddon>
182
+ </InputGroup>
183
+
184
+ {/* Section 2 — konteks Session baru: Project (kiri) & model (kanan),
185
+ tanpa latar (hanya border container luar). */}
186
+ <div className="flex min-w-0 items-center justify-between gap-2 px-1.5 py-1">
187
+ <DropdownMenu>
188
+ <DropdownMenuTrigger asChild>
189
+ <Button
190
+ type="button"
191
+ variant="ghost"
192
+ size="sm"
193
+ className="h-9 min-w-0 max-w-[55%] gap-1.5 px-2 font-normal text-muted-foreground hover:text-foreground"
194
+ aria-label={`Project: ${selected?.name ?? "none"}. Change project`}
195
+ disabled={sending}
196
+ >
197
+ <FolderIcon data-icon="inline-start" />
198
+ <span className="truncate">{selected?.name ?? "Select project"}</span>
199
+ <ChevronDownIcon data-icon="inline-end" />
200
+ </Button>
201
+ </DropdownMenuTrigger>
202
+ <DropdownMenuContent align="start" className="max-h-72 w-64">
203
+ <DropdownMenuLabel>Project</DropdownMenuLabel>
204
+ <DropdownMenuGroup>
205
+ {overviews.map(({ project }) => (
206
+ <DropdownMenuItem key={project.id} onSelect={() => choose(project.id)}>
207
+ <FolderIcon />
208
+ <span className="flex min-w-0 flex-1 flex-col">
209
+ <span className="truncate">{project.name}</span>
210
+ <span className="truncate text-xs text-muted-foreground">
211
+ {project.path}
212
+ </span>
213
+ </span>
214
+ {project.id === selected?.id && <CheckIcon />}
215
+ </DropdownMenuItem>
216
+ ))}
217
+ </DropdownMenuGroup>
218
+ <DropdownMenuSeparator />
219
+ <DropdownMenuGroup>
220
+ <DropdownMenuItem onSelect={onNewProject}>
221
+ <FolderPlusIcon />
222
+ New project
223
+ </DropdownMenuItem>
224
+ </DropdownMenuGroup>
225
+ </DropdownMenuContent>
226
+ </DropdownMenu>
227
+
228
+ {selected && (
229
+ <div className="flex min-w-0 max-w-[45%] justify-end">
230
+ <ModelPicker
231
+ projectId={selected.id}
232
+ sessionId={null}
233
+ model={model}
234
+ onChanged={setModel}
235
+ variant="inline"
236
+ />
237
+ </div>
238
+ )}
239
+ </div>
240
+ </div>
241
+
242
+ {error && (
243
+ <Alert variant="destructive">
244
+ <AlertDescription>{error}</AlertDescription>
245
+ </Alert>
246
+ )}
247
+ </form>
248
+ </div>
249
+ );
250
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Dialog multi-step untuk membuat Project baru (Requirement 10.2-10.4).
3
+ *
4
+ * Alur: Step 1 pilih direktori kerja via `FolderBrowser`, Step 2 beri nama
5
+ * project (pre-filled dari nama folder terpilih). Dipisah dari
6
+ * halaman daftar Project (`ProjectsPage`) agar logic wizard (step, reset
7
+ * state) tidak bercampur dengan logic daftar Project.
8
+ */
9
+
10
+ import { ArrowLeftIcon, ArrowRightIcon, FolderIcon, FolderPlusIcon } from "lucide-react";
11
+ import { useEffect, useState } from "react";
12
+ import { toast } from "sonner";
13
+ import { FolderBrowser } from "@/components/projects/FolderBrowser";
14
+ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
15
+ import { Button } from "@/components/ui/button";
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogFooter,
21
+ DialogHeader,
22
+ DialogTitle,
23
+ } from "@/components/ui/dialog";
24
+ import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
25
+ import { Input } from "@/components/ui/input";
26
+ import { Spinner } from "@/components/ui/spinner";
27
+ import { useRouter } from "@/hooks/useRouter";
28
+ import { ApiError, apiFetch } from "@/lib/api";
29
+ import { settingsPath } from "@/lib/routes";
30
+ import { cn } from "@/lib/utils";
31
+ import type { WorkspaceInfo } from "@/server/services/workspace";
32
+
33
+ export interface NewProjectDialogProps {
34
+ open: boolean;
35
+ onOpenChange: (open: boolean) => void;
36
+ /** Dipanggil setelah Project berhasil dibuat, untuk memuat ulang daftar. */
37
+ onCreated: () => void | Promise<void>;
38
+ }
39
+
40
+ type Step = 1 | 2;
41
+
42
+ /** Nama folder terakhir dari path relatif ("" bila root Sandbox). */
43
+ function basename(path: string): string {
44
+ const segments = path.split("/").filter((s) => s.length > 0);
45
+ return segments.at(-1) ?? "";
46
+ }
47
+
48
+ export function NewProjectDialog({ open, onOpenChange, onCreated }: NewProjectDialogProps) {
49
+ const [step, setStep] = useState<Step>(1);
50
+ const [path, setPath] = useState("");
51
+ const [name, setName] = useState("");
52
+ const [nameTouched, setNameTouched] = useState(false);
53
+ const [creating, setCreating] = useState(false);
54
+ const [formError, setFormError] = useState<string | null>(null);
55
+ /** Workspace (Sandbox_Root) aktif — ditampilkan di langkah 1. */
56
+ const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
57
+ const { navigate } = useRouter();
58
+
59
+ useEffect(() => {
60
+ if (!open) return;
61
+ let cancelled = false;
62
+ void apiFetch("/api/workspace")
63
+ .then((r) => r.json())
64
+ .then((b: { workspace: WorkspaceInfo }) => {
65
+ if (!cancelled) setWorkspace(b.workspace);
66
+ })
67
+ .catch(() => {
68
+ // Info tambahan saja; dialog tetap bisa dipakai.
69
+ });
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [open]);
74
+
75
+ const reset = () => {
76
+ setStep(1);
77
+ setPath("");
78
+ setName("");
79
+ setNameTouched(false);
80
+ setFormError(null);
81
+ setCreating(false);
82
+ };
83
+
84
+ const handleOpenChange = (next: boolean) => {
85
+ if (!next) reset();
86
+ onOpenChange(next);
87
+ };
88
+
89
+ const goToNaming = () => {
90
+ setFormError(null);
91
+ if (!nameTouched) setName(basename(path));
92
+ setStep(2);
93
+ };
94
+
95
+ const backToFolder = () => {
96
+ setFormError(null);
97
+ setStep(1);
98
+ };
99
+
100
+ const create = async (event: React.FormEvent<HTMLFormElement>) => {
101
+ event.preventDefault();
102
+ setCreating(true);
103
+ setFormError(null);
104
+ try {
105
+ await apiFetch("/api/projects", { method: "POST", body: JSON.stringify({ name, path }) });
106
+ await onCreated();
107
+ handleOpenChange(false);
108
+ toast.success(`Project "${name.trim()}" created.`);
109
+ } catch (e) {
110
+ setFormError(e instanceof ApiError ? e.message : "Failed to create project");
111
+ } finally {
112
+ setCreating(false);
113
+ }
114
+ };
115
+
116
+ return (
117
+ <Dialog open={open} onOpenChange={handleOpenChange}>
118
+ <DialogContent className="flex h-[560px] max-h-[85vh] flex-col sm:max-w-lg">
119
+ <DialogHeader>
120
+ <div className="mt-4 flex flex-col gap-1.5">
121
+ <div className="flex items-center gap-1.5">
122
+ <span
123
+ className={cn(
124
+ "h-1.5 flex-1 rounded-full transition-colors",
125
+ step >= 1 ? "bg-primary" : "bg-muted",
126
+ )}
127
+ />
128
+ <span
129
+ className={cn(
130
+ "h-1.5 flex-1 rounded-full transition-colors",
131
+ step >= 2 ? "bg-primary" : "bg-muted",
132
+ )}
133
+ />
134
+ </div>
135
+ <span className="text-xs font-medium text-muted-foreground">Step {step} of 2</span>
136
+ </div>
137
+ <DialogTitle>{step === 1 ? "Choose working directory" : "Name your project"}</DialogTitle>
138
+ <DialogDescription>
139
+ {step === 1
140
+ ? "Browse the Sandbox and pick the folder to use as the working directory."
141
+ : "This name identifies the project in the list."}
142
+ </DialogDescription>
143
+ </DialogHeader>
144
+
145
+ {step === 1 ? (
146
+ <>
147
+ <div className="min-h-0 flex-1">
148
+ <FolderBrowser path={path} onNavigate={setPath} />
149
+ </div>
150
+ {workspace && (
151
+ <p className="flex min-w-0 items-center gap-1.5 text-[12px] text-muted-foreground">
152
+ <span className="shrink-0">Projects live in</span>
153
+ <code className="min-w-0 truncate font-mono" title={workspace.root}>
154
+ {workspace.display}
155
+ </code>
156
+ <span aria-hidden>·</span>
157
+ <a
158
+ href={settingsPath("workspace")}
159
+ onClick={(e) => {
160
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
161
+ e.preventDefault();
162
+ handleOpenChange(false);
163
+ navigate(settingsPath("workspace"));
164
+ }}
165
+ className="shrink-0 font-medium text-foreground underline underline-offset-2"
166
+ >
167
+ Change
168
+ </a>
169
+ </p>
170
+ )}
171
+ <DialogFooter>
172
+ <Button type="button" onClick={goToNaming}>
173
+ Next
174
+ <ArrowRightIcon data-icon="inline-end" />
175
+ </Button>
176
+ </DialogFooter>
177
+ </>
178
+ ) : (
179
+ <form onSubmit={create} className="flex min-h-0 flex-1 flex-col gap-4">
180
+ <div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto">
181
+ <FieldGroup>
182
+ <Field>
183
+ <FieldLabel htmlFor="project-name">Project name</FieldLabel>
184
+ <Input
185
+ id="project-name"
186
+ value={name}
187
+ onChange={(e) => {
188
+ setName(e.target.value);
189
+ setNameTouched(true);
190
+ }}
191
+ placeholder="e.g. web-app"
192
+ maxLength={120}
193
+ autoFocus
194
+ />
195
+ </Field>
196
+ <Field>
197
+ <FieldLabel>Working directory</FieldLabel>
198
+ <div className="flex items-center gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm">
199
+ <FolderIcon className="size-4 shrink-0 text-muted-foreground" />
200
+ <span className="min-w-0 flex-1 truncate font-mono text-muted-foreground">
201
+ {path === "" ? "/" : path}
202
+ </span>
203
+ <Button
204
+ type="button"
205
+ variant="ghost"
206
+ size="sm"
207
+ className="h-7 shrink-0 px-2"
208
+ onClick={backToFolder}
209
+ disabled={creating}
210
+ >
211
+ Change
212
+ </Button>
213
+ </div>
214
+ </Field>
215
+ </FieldGroup>
216
+
217
+ {formError && (
218
+ <Alert variant="destructive">
219
+ <AlertTitle>Failed to create project</AlertTitle>
220
+ <AlertDescription>{formError}</AlertDescription>
221
+ </Alert>
222
+ )}
223
+ </div>
224
+
225
+ <DialogFooter>
226
+ <Button type="button" variant="ghost" onClick={backToFolder} disabled={creating}>
227
+ <ArrowLeftIcon data-icon="inline-start" />
228
+ Back
229
+ </Button>
230
+ <Button type="submit" disabled={creating || name.trim() === ""}>
231
+ {creating ? (
232
+ <>
233
+ <Spinner data-icon="inline-start" />
234
+ Creating…
235
+ </>
236
+ ) : (
237
+ <>
238
+ <FolderPlusIcon data-icon="inline-start" />
239
+ Create project
240
+ </>
241
+ )}
242
+ </Button>
243
+ </DialogFooter>
244
+ </form>
245
+ )}
246
+ </DialogContent>
247
+ </Dialog>
248
+ );
249
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Satu seksi di panel samping halaman Project (Instructions / MCP / Skills).
3
+ *
4
+ * Tiap seksi = satu kartu (gaya boxed list seperti Settings): header di dalam
5
+ * kartu (ikon, judul, ringkasan, aksi, chevron) dan isi di bawah garis
6
+ * pemisah. Status buka/tutup diingat per seksi di `localStorage`.
7
+ *
8
+ * Isi kartu tidak diberi padding otomatis: `PanelList` (baris berdivider),
9
+ * `PanelHint`, dan `PanelBody` mengatur jaraknya sendiri agar baris daftar
10
+ * bisa rata tepi ke tepi.
11
+ */
12
+ import { ChevronDownIcon } from "lucide-react";
13
+ import type { ReactNode } from "react";
14
+ import { useEffect, useState } from "react";
15
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
16
+ import { cn } from "@/lib/utils";
17
+
18
+ const STORAGE_PREFIX = "kcg.projectPanel.";
19
+
20
+ /** Buka/tutup seksi yang diingat di localStorage (default terbuka). */
21
+ function usePersistedOpen(id: string): [boolean, (open: boolean) => void] {
22
+ const [open, setOpen] = useState(() => {
23
+ try {
24
+ return localStorage.getItem(STORAGE_PREFIX + id) !== "0";
25
+ } catch {
26
+ return true;
27
+ }
28
+ });
29
+ useEffect(() => {
30
+ try {
31
+ localStorage.setItem(STORAGE_PREFIX + id, open ? "1" : "0");
32
+ } catch {
33
+ // Storage tidak tersedia (mode privat) — cukup state di memori.
34
+ }
35
+ }, [id, open]);
36
+ return [open, setOpen];
37
+ }
38
+
39
+ export interface PanelSectionProps {
40
+ /** Id stabil untuk mengingat status lipat (mis. "mcp"). */
41
+ id: string;
42
+ title: string;
43
+ /** Ikon seksi di kiri judul. */
44
+ icon: ReactNode;
45
+ /** Hitungan / ringkasan kecil di bawah judul. */
46
+ meta?: ReactNode;
47
+ /** Tombol aksi kecil di kanan header (tidak ikut melipat). */
48
+ action?: ReactNode;
49
+ children: ReactNode;
50
+ }
51
+
52
+ export function PanelSection({ id, title, icon, meta, action, children }: PanelSectionProps) {
53
+ const [open, setOpen] = usePersistedOpen(id);
54
+ return (
55
+ <Collapsible open={open} onOpenChange={setOpen} asChild>
56
+ <section
57
+ aria-label={title}
58
+ className="overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs"
59
+ >
60
+ <div className="flex items-center gap-1 pr-2">
61
+ <CollapsibleTrigger className="group flex min-h-14 min-w-0 flex-1 items-center gap-3 py-2.5 pl-3.5 text-left outline-none transition-colors hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:ring-[3px] focus-visible:ring-ring/40 focus-visible:ring-inset">
62
+ <span
63
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground [&_svg]:size-4"
64
+ aria-hidden
65
+ >
66
+ {icon}
67
+ </span>
68
+ <span className="flex min-w-0 flex-1 flex-col">
69
+ <span className="text-sm font-semibold leading-5 tracking-tight">{title}</span>
70
+ {meta !== undefined && (
71
+ <span className="truncate text-xs tabular-nums text-muted-foreground">{meta}</span>
72
+ )}
73
+ </span>
74
+ <ChevronDownIcon
75
+ className="mr-1.5 size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180"
76
+ aria-hidden
77
+ />
78
+ </CollapsibleTrigger>
79
+ {action && <div className="flex shrink-0 items-center">{action}</div>}
80
+ </div>
81
+ <CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
82
+ <div className="border-t">{children}</div>
83
+ </CollapsibleContent>
84
+ </section>
85
+ </Collapsible>
86
+ );
87
+ }
88
+
89
+ /** Isi kartu dengan padding standar (form, pencarian, teks). */
90
+ export function PanelBody({ className, children }: { className?: string; children: ReactNode }) {
91
+ return <div className={cn("px-3.5 py-3", className)}>{children}</div>;
92
+ }
93
+
94
+ /** Baris-baris berdivider, rata tepi ke tepi di dalam kartu. */
95
+ export function PanelList({ className, children }: { className?: string; children: ReactNode }) {
96
+ return <ul className={cn("flex flex-col divide-y", className)}>{children}</ul>;
97
+ }
98
+
99
+ /** Keterangan saat daftar kosong — satu gaya untuk semua seksi. */
100
+ export function PanelHint({ children }: { children: ReactNode }) {
101
+ return <p className="px-3.5 py-3.5 text-xs leading-relaxed text-muted-foreground">{children}</p>;
102
+ }