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,287 @@
1
+ /**
2
+ * Chat homepage: headline acak + logo, lalu composer. Di bawah kotak input
3
+ * ada baris polos (tanpa border): pemilih Project di kiri, agent + model di
4
+ * kanan (satu trigger `ModelPicker` — model bergantung pada agent).
5
+ *
6
+ * Tata letak: semuanya di tengah layar. Desktop menampilkan daftar Session
7
+ * terbaru (`HomeRecentSessions`) di bawah composer; di HP daftar itu
8
+ * disembunyikan dan logo ditumpuk di atas headline.
9
+ *
10
+ * Kirim = buat Session baru di Project terpilih (`POST /api/sessions`),
11
+ * titipkan teks sebagai prompt tertunda (`lib/pending-prompt.ts`), lalu
12
+ * pindah ke Session view — `useSessionChat` mengirimnya sekali setelah
13
+ * koneksi WS ter-attach. Project terakhir dipakai diingat di localStorage.
14
+ *
15
+ * Mode agent ada di dalam kotak input, kiri bawah (seperti composer Session).
16
+ * Pemilih model & agent sama dengan Session (`ModelPicker`, `AgentPicker`) —
17
+ * tanpa Session, pilihan dikirim di body `POST /api/sessions`.
18
+ * Keduanya di-reset saat Project berganti karena daftarnya per Project.
19
+ */
20
+ import {
21
+ CheckIcon,
22
+ ChevronDownIcon,
23
+ FolderIcon,
24
+ FolderPlusIcon,
25
+ SendHorizontalIcon,
26
+ } from "lucide-react";
27
+ import { useEffect, useRef, useState } from "react";
28
+ import { HomeRecentSessions } from "@/components/projects/HomeRecentSessions";
29
+ import { AgentPicker } from "@/components/sessions/AgentPicker";
30
+ import { ModelPicker } from "@/components/sessions/ModelPicker";
31
+ import { Alert, AlertDescription } from "@/components/ui/alert";
32
+ import { Button } from "@/components/ui/button";
33
+ import {
34
+ DropdownMenu,
35
+ DropdownMenuContent,
36
+ DropdownMenuGroup,
37
+ DropdownMenuItem,
38
+ DropdownMenuLabel,
39
+ DropdownMenuSeparator,
40
+ DropdownMenuTrigger,
41
+ } from "@/components/ui/dropdown-menu";
42
+ import {
43
+ InputGroup,
44
+ InputGroupAddon,
45
+ InputGroupButton,
46
+ InputGroupTextarea,
47
+ } from "@/components/ui/input-group";
48
+ import { Spinner } from "@/components/ui/spinner";
49
+ import { useRouter } from "@/hooks/useRouter";
50
+ import { readPreferredAgentType, storePreferredAgentType } from "@/lib/agent-types";
51
+ import { ApiError, apiFetch } from "@/lib/api";
52
+ import { notifyDataChanged } from "@/lib/data-events";
53
+ import { pickHeadline } from "@/lib/headlines";
54
+ import { setPendingPrompt } from "@/lib/pending-prompt";
55
+ import type { ProjectOverview } from "@/lib/project-overview";
56
+ import { sessionPath } from "@/lib/routes";
57
+ import { seedSession } from "@/lib/session-handoff";
58
+ import logo from "@/logo.svg";
59
+ import type { AgentType, Session, SessionModel } from "@/types";
60
+
61
+ const LAST_PROJECT_KEY = "kcg-home-project";
62
+
63
+ export interface HomeChatProps {
64
+ /** Project terurut aktivitas terbaru (`buildProjectOverviews`). */
65
+ overviews: ProjectOverview[];
66
+ /** Buka dialog pembuatan Project baru. */
67
+ onNewProject: () => void;
68
+ }
69
+
70
+ function readLastProject(): string | null {
71
+ return typeof localStorage === "undefined" ? null : localStorage.getItem(LAST_PROJECT_KEY);
72
+ }
73
+
74
+ export function HomeChat({ overviews, onNewProject }: HomeChatProps) {
75
+ const { navigate } = useRouter();
76
+ // Dipilih sekali per kunjungan — tidak berganti saat re-render.
77
+ const [headline] = useState(() => pickHeadline());
78
+ const [text, setText] = useState("");
79
+ const [projectId, setProjectId] = useState<string | null>(readLastProject);
80
+ /** Backend Session baru (OpenCode / Claude Code), diingat per perangkat. */
81
+ const [agentType, setAgentType] = useState<AgentType>(readPreferredAgentType);
82
+ const [sending, setSending] = useState(false);
83
+ const [error, setError] = useState<string | null>(null);
84
+ /** Model & agent untuk Session baru; null = default opencode. */
85
+ const [model, setModel] = useState<SessionModel | null>(null);
86
+ const [agent, setAgent] = useState<string | null>(null);
87
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
88
+
89
+ // Project tersimpan sudah dihapus / belum ada -> pakai yang paling baru aktif.
90
+ const selectedOverview =
91
+ overviews.find((o) => o.project.id === projectId) ?? overviews[0] ?? null;
92
+ const selected = selectedOverview?.project ?? null;
93
+
94
+ useEffect(() => {
95
+ textareaRef.current?.focus();
96
+ }, []);
97
+
98
+ const choose = (id: string) => {
99
+ if (id !== selected?.id) {
100
+ // Model/agent tersedia per Project (config opencode masing-masing).
101
+ setModel(null);
102
+ setAgent(null);
103
+ }
104
+ setProjectId(id);
105
+ if (typeof localStorage !== "undefined") localStorage.setItem(LAST_PROJECT_KEY, id);
106
+ requestAnimationFrame(() => textareaRef.current?.focus());
107
+ };
108
+
109
+ const chooseAgentType = (next: AgentType) => {
110
+ if (next !== agentType) {
111
+ // Model & mode berasal dari backend masing-masing.
112
+ setModel(null);
113
+ setAgent(null);
114
+ }
115
+ setAgentType(next);
116
+ storePreferredAgentType(next);
117
+ // Tanpa fokus ke textarea: dipanggil dari dialog ModelPicker yang masih
118
+ // terbuka (fokus terkunci di dialog sampai ditutup).
119
+ };
120
+
121
+ const canSend = selected !== null && text.trim() !== "" && !sending;
122
+
123
+ const submit = async (event?: React.FormEvent<HTMLFormElement>) => {
124
+ event?.preventDefault();
125
+ if (!canSend || selected === null) return;
126
+ setSending(true);
127
+ setError(null);
128
+ try {
129
+ const res = await apiFetch("/api/sessions", {
130
+ method: "POST",
131
+ body: JSON.stringify({ agentType, projectId: selected.id, model, agent }),
132
+ });
133
+ const { session } = (await res.json()) as { session: Session };
134
+ setPendingPrompt(session.id, text);
135
+ // Session view dirender langsung dari data ini (tanpa fetch ulang),
136
+ // lalu composer "berpindah" ke dasar layar lewat View Transition.
137
+ seedSession(session);
138
+ notifyDataChanged();
139
+ navigate(sessionPath(selected.id, session.id), { transition: "to-session" });
140
+ } catch (e) {
141
+ setError(e instanceof ApiError ? e.message : "Failed to start a new session");
142
+ setSending(false);
143
+ }
144
+ };
145
+
146
+ return (
147
+ <div className="mx-auto flex w-full max-w-2xl flex-1 flex-col justify-center gap-6 pb-[10dvh]">
148
+ {/* HP: logo di atas headline (tengah); desktop: berdampingan. */}
149
+ <div
150
+ data-vt-name="home-headline"
151
+ className="flex flex-col items-center justify-center gap-3 px-2 sm:flex-row"
152
+ >
153
+ <img src={logo} alt="" aria-hidden className="size-10 shrink-0 sm:size-12" />
154
+ <h1 className="text-balance text-center text-2xl font-semibold tracking-tight sm:text-left sm:text-3xl">
155
+ {headline}
156
+ </h1>
157
+ </div>
158
+
159
+ <form onSubmit={submit} className="flex flex-col gap-3">
160
+ {/* Container luar: hanya border (tanpa latar), membungkus dua section —
161
+ kotak input berlatar di atas, baris Project/model polos di bawah. */}
162
+ {/* `data-vt-name`: selama transisi "to-session" kotak ini dan area
163
+ Session view berbagi nama view-transition -> kotak composer
164
+ MELUAS menjadi halaman Session (container transform). */}
165
+ <div data-vt-name="session-surface" className="flex flex-col rounded-2xl border">
166
+ {/* Section 1 — kotak input: textarea + baris aksi (mode agent kiri,
167
+ kirim kanan), tata letak sama dengan composer Session. `-m-px`
168
+ menumpuk border kotak ini di atas border container luar. */}
169
+ <InputGroup className="-m-px w-auto rounded-2xl bg-card shadow-sm dark:bg-card">
170
+ <InputGroupTextarea
171
+ ref={textareaRef}
172
+ value={text}
173
+ onChange={(e) => setText(e.target.value)}
174
+ onKeyDown={(e) => {
175
+ // Enter kirim, Shift+Enter baris baru (abaikan saat komposisi IME).
176
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
177
+ e.preventDefault();
178
+ void submit();
179
+ }
180
+ }}
181
+ placeholder={selected ? `Ask anything in ${selected.name}…` : "Pick a project first"}
182
+ aria-label="Message"
183
+ disabled={sending}
184
+ rows={2}
185
+ className="max-h-60 min-h-20 px-4 text-base leading-6 md:text-base"
186
+ />
187
+ <InputGroupAddon align="block-end" className="gap-2 px-2 pb-2">
188
+ {selected && (
189
+ <AgentPicker
190
+ projectId={selected.id}
191
+ agentType={agentType}
192
+ sessionId={null}
193
+ agent={agent}
194
+ onChanged={setAgent}
195
+ disabled={sending}
196
+ />
197
+ )}
198
+ <InputGroupButton
199
+ type="submit"
200
+ size="icon-sm"
201
+ variant="default"
202
+ className="ml-auto size-9 rounded-full"
203
+ disabled={!canSend}
204
+ aria-label="Send"
205
+ >
206
+ {sending ? <Spinner /> : <SendHorizontalIcon />}
207
+ </InputGroupButton>
208
+ </InputGroupAddon>
209
+ </InputGroup>
210
+
211
+ {/* Section 2 — konteks Session baru: Project (kiri) & agent + model
212
+ (kanan), tanpa latar (hanya border container luar). */}
213
+ <div className="flex min-w-0 items-center justify-between gap-2 px-1.5 py-1">
214
+ <div className="flex min-w-0 items-center">
215
+ <DropdownMenu>
216
+ <DropdownMenuTrigger asChild>
217
+ <Button
218
+ type="button"
219
+ variant="ghost"
220
+ size="sm"
221
+ className="h-9 min-w-0 gap-1.5 px-2 font-normal text-muted-foreground hover:text-foreground"
222
+ aria-label={`Project: ${selected?.name ?? "none"}. Change project`}
223
+ disabled={sending}
224
+ >
225
+ <FolderIcon data-icon="inline-start" />
226
+ <span className="truncate">{selected?.name ?? "Select project"}</span>
227
+ <ChevronDownIcon data-icon="inline-end" />
228
+ </Button>
229
+ </DropdownMenuTrigger>
230
+ <DropdownMenuContent align="start" className="max-h-72 w-64">
231
+ <DropdownMenuLabel>Project</DropdownMenuLabel>
232
+ <DropdownMenuGroup>
233
+ {overviews.map(({ project }) => (
234
+ <DropdownMenuItem key={project.id} onSelect={() => choose(project.id)}>
235
+ <FolderIcon />
236
+ <span className="flex min-w-0 flex-1 flex-col">
237
+ <span className="truncate">{project.name}</span>
238
+ <span className="truncate text-xs text-muted-foreground">
239
+ {project.path}
240
+ </span>
241
+ </span>
242
+ {project.id === selected?.id && <CheckIcon />}
243
+ </DropdownMenuItem>
244
+ ))}
245
+ </DropdownMenuGroup>
246
+ <DropdownMenuSeparator />
247
+ <DropdownMenuGroup>
248
+ <DropdownMenuItem onSelect={onNewProject}>
249
+ <FolderPlusIcon />
250
+ New project
251
+ </DropdownMenuItem>
252
+ </DropdownMenuGroup>
253
+ </DropdownMenuContent>
254
+ </DropdownMenu>
255
+ </div>
256
+
257
+ {selected && (
258
+ <div className="flex max-w-[60%] shrink-0 justify-end">
259
+ {/* Agent dipilih di dialog yang sama dengan model; trigger
260
+ menampilkan logo agent + nama model. */}
261
+ <ModelPicker
262
+ projectId={selected.id}
263
+ agentType={agentType}
264
+ sessionId={null}
265
+ model={model}
266
+ onChanged={setModel}
267
+ onAgentTypeChange={chooseAgentType}
268
+ variant="inline"
269
+ />
270
+ </div>
271
+ )}
272
+ </div>
273
+ </div>
274
+
275
+ {error && (
276
+ <Alert variant="destructive">
277
+ <AlertDescription>{error}</AlertDescription>
278
+ </Alert>
279
+ )}
280
+ </form>
281
+
282
+ {selectedOverview && (
283
+ <HomeRecentSessions overview={selectedOverview} className="hidden sm:mt-6 sm:flex" />
284
+ )}
285
+ </div>
286
+ );
287
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Daftar "lanjutkan" di homepage: beberapa Session terbaru milik Project yang
3
+ * sedang dipilih di composer (`HomeChat`). Ringkas — titik status, judul,
4
+ * logo agent, waktu relatif — plus tautan ke daftar Session lengkap.
5
+ *
6
+ * Datanya sudah ada di `ProjectOverview.recentSessions`, jadi tidak ada fetch.
7
+ */
8
+ import { ArrowRightIcon } from "lucide-react";
9
+ import { AgentLogo } from "@/components/sessions/AgentLogo";
10
+ import { useRouter } from "@/hooks/useRouter";
11
+ import { formatRelativeTime, type ProjectOverview } from "@/lib/project-overview";
12
+ import { projectPath, sessionPath } from "@/lib/routes";
13
+ import { SESSION_STATUS_DOT, SESSION_STATUS_LABEL } from "@/lib/session-status";
14
+ import { displaySessionTitle } from "@/lib/session-title";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ export interface HomeRecentSessionsProps {
18
+ overview: ProjectOverview;
19
+ className?: string;
20
+ }
21
+
22
+ export function HomeRecentSessions({ overview, className }: HomeRecentSessionsProps) {
23
+ const { navigate } = useRouter();
24
+ const { project, recentSessions, sessionCount } = overview;
25
+ if (recentSessions.length === 0) return null;
26
+
27
+ // Tautan biasa (bukan tombol) agar buka-di-tab-baru tetap berfungsi.
28
+ const go = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
29
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
30
+ e.preventDefault();
31
+ navigate(href);
32
+ };
33
+
34
+ const allHref = projectPath(project.id);
35
+
36
+ return (
37
+ <section
38
+ aria-label={`Recent sessions in ${project.name}`}
39
+ className={cn("flex flex-col", className)}
40
+ >
41
+ {/* Label + garis rambut: menandai section terpisah dari composer di
42
+ atasnya (bukan kelanjutan tray Project/model). */}
43
+ <div className="flex items-center gap-3 px-3 pb-2">
44
+ <h2 className="shrink-0 text-xs font-medium text-muted-foreground">Recent sessions</h2>
45
+ <span aria-hidden className="h-px flex-1 bg-border" />
46
+ {sessionCount > recentSessions.length && (
47
+ <a
48
+ href={allHref}
49
+ onClick={(e) => go(e, allHref)}
50
+ className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
51
+ >
52
+ View all {sessionCount}
53
+ <ArrowRightIcon className="size-3" />
54
+ </a>
55
+ )}
56
+ </div>
57
+ <ul className="flex flex-col">
58
+ {recentSessions.map((session) => {
59
+ const href = sessionPath(project.id, session.id);
60
+ const title = displaySessionTitle(session.title);
61
+ return (
62
+ <li key={session.id}>
63
+ <a
64
+ href={href}
65
+ onClick={(e) => go(e, href)}
66
+ aria-label={`Open session ${title}, ${SESSION_STATUS_LABEL[session.status]}`}
67
+ className="group flex min-h-10 items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-muted/50 focus-visible:bg-muted/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
68
+ >
69
+ <span
70
+ aria-hidden
71
+ className={cn("size-2 shrink-0 rounded-full", SESSION_STATUS_DOT[session.status])}
72
+ />
73
+ <span className="min-w-0 flex-1 truncate text-sm text-muted-foreground group-hover:text-foreground group-focus-visible:text-foreground">
74
+ {title}
75
+ </span>
76
+ <AgentLogo
77
+ agentType={session.agentType}
78
+ className="size-3.5 text-muted-foreground"
79
+ />
80
+ <span className="shrink-0 text-xs tabular-nums text-muted-foreground">
81
+ {formatRelativeTime(session.updatedAt)}
82
+ </span>
83
+ </a>
84
+ </li>
85
+ );
86
+ })}
87
+ </ul>
88
+ </section>
89
+ );
90
+ }
@@ -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
+ }