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,218 @@
1
+ /**
2
+ * Custom instruction Project — grup di panel samping (gaya libadwaita:
3
+ * judul grup + satu baris yang membuka editor) + dialog editor.
4
+ *
5
+ * Instruksi disimpan lewat `PATCH /api/projects/:id` dan dikirim server
6
+ * sebagai `system` di setiap prompt Session milik Project ini (ditambahkan
7
+ * di atas AGENTS.md / instruksi bawaan opencode). Perubahan langsung berlaku
8
+ * untuk prompt berikutnya, termasuk di Session yang sudah berjalan.
9
+ */
10
+ import { ScrollTextIcon } from "lucide-react";
11
+ import { useEffect, useId, useState } from "react";
12
+ import { toast } from "sonner";
13
+ import { ActionRow, PrefsGroup } from "@/components/settings/prefs";
14
+ import { Alert, AlertDescription } 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 { Spinner } from "@/components/ui/spinner";
25
+ import { Textarea } from "@/components/ui/textarea";
26
+ import { ApiError, apiFetch } from "@/lib/api";
27
+ import { cn } from "@/lib/utils";
28
+ import type { Project } from "@/types";
29
+
30
+ /** Sama dengan batas server (`MAX_PROJECT_INSTRUCTIONS_LENGTH`). */
31
+ const MAX_LENGTH = 20_000;
32
+
33
+ export interface ProjectInstructionsProps {
34
+ project: Project;
35
+ /** Dipanggil dengan Project terbaru setelah instruksi tersimpan. */
36
+ onSaved: (project: Project) => void;
37
+ }
38
+
39
+ export function ProjectInstructions({ project, onSaved }: ProjectInstructionsProps) {
40
+ const [open, setOpen] = useState(false);
41
+ const instructions = project.instructions ?? "";
42
+ const hasInstructions = instructions.length > 0;
43
+
44
+ return (
45
+ <>
46
+ {/* AdwPreferencesGroup + satu AdwActionRow yang bisa diaktifkan:
47
+ klik baris membuka editor. */}
48
+ <PrefsGroup
49
+ title="Instructions"
50
+ description="Sent with every prompt in this project, for both OpenCode and Claude Code."
51
+ >
52
+ <ActionRow
53
+ prefix={<ScrollTextIcon className="size-[18px] text-muted-foreground" aria-hidden />}
54
+ title={hasInstructions ? "Custom instructions" : "Add instructions"}
55
+ subtitle={
56
+ hasInstructions ? (
57
+ <span className="line-clamp-2 whitespace-pre-line">{instructions}</span>
58
+ ) : (
59
+ "Like the language to reply in or tools to prefer"
60
+ )
61
+ }
62
+ onActivate={() => setOpen(true)}
63
+ />
64
+ </PrefsGroup>
65
+
66
+ <InstructionsDialog
67
+ open={open}
68
+ onOpenChange={setOpen}
69
+ project={project}
70
+ onSaved={(p) => {
71
+ onSaved(p);
72
+ setOpen(false);
73
+ }}
74
+ />
75
+ </>
76
+ );
77
+ }
78
+
79
+ function InstructionsDialog({
80
+ open,
81
+ onOpenChange,
82
+ project,
83
+ onSaved,
84
+ }: {
85
+ open: boolean;
86
+ onOpenChange: (open: boolean) => void;
87
+ project: Project;
88
+ onSaved: (project: Project) => void;
89
+ }) {
90
+ const fieldId = useId();
91
+ const hintId = useId();
92
+ const [draft, setDraft] = useState(project.instructions ?? "");
93
+ const [saving, setSaving] = useState(false);
94
+ const [error, setError] = useState<string | null>(null);
95
+
96
+ // Reset draft tiap dialog dibuka — batal edit tidak meninggalkan sisa.
97
+ useEffect(() => {
98
+ if (!open) return;
99
+ setDraft(project.instructions ?? "");
100
+ setError(null);
101
+ }, [open, project.instructions]);
102
+
103
+ const tooLong = draft.length > MAX_LENGTH;
104
+ const unchanged = draft.trim() === (project.instructions ?? "");
105
+
106
+ const save = async () => {
107
+ setSaving(true);
108
+ setError(null);
109
+ try {
110
+ const res = await apiFetch(`/api/projects/${project.id}`, {
111
+ method: "PATCH",
112
+ body: JSON.stringify({ instructions: draft }),
113
+ });
114
+ const body = (await res.json()) as { project: Project };
115
+ onSaved(body.project);
116
+ toast.success(body.project.instructions ? "Instructions saved." : "Instructions cleared.");
117
+ } catch (e) {
118
+ setError(e instanceof ApiError ? e.message : "Failed to save instructions");
119
+ } finally {
120
+ setSaving(false);
121
+ }
122
+ };
123
+
124
+ return (
125
+ <Dialog open={open} onOpenChange={(o) => !saving && onOpenChange(o)}>
126
+ <DialogContent className="sm:max-w-xl">
127
+ <DialogHeader>
128
+ <DialogTitle>Custom instructions</DialogTitle>
129
+ <DialogDescription>
130
+ Sent to the agent with every prompt in {project.name}, on top of AGENTS.md / CLAUDE.md.
131
+ Changes apply from the next message.
132
+ </DialogDescription>
133
+ </DialogHeader>
134
+
135
+ <form
136
+ id={`${fieldId}-form`}
137
+ onSubmit={(e) => {
138
+ e.preventDefault();
139
+ if (!tooLong && !unchanged) void save();
140
+ }}
141
+ className="flex flex-col gap-2"
142
+ >
143
+ <label htmlFor={fieldId} className="sr-only">
144
+ Instructions
145
+ </label>
146
+ <Textarea
147
+ id={fieldId}
148
+ value={draft}
149
+ onChange={(e) => setDraft(e.target.value)}
150
+ onKeyDown={(e) => {
151
+ // Ctrl/Cmd+Enter = simpan, seperti editor pada umumnya.
152
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
153
+ e.preventDefault();
154
+ e.currentTarget.form?.requestSubmit();
155
+ }
156
+ }}
157
+ placeholder={
158
+ "e.g.\n- Reply in Bahasa Indonesia\n- Use Bun, not Node\n- Keep commits small"
159
+ }
160
+ aria-describedby={hintId}
161
+ aria-invalid={tooLong || undefined}
162
+ disabled={saving}
163
+ className="max-h-[50dvh] min-h-40 font-mono text-sm leading-relaxed"
164
+ autoFocus
165
+ />
166
+ <p
167
+ id={hintId}
168
+ className={cn(
169
+ "text-right text-xs tabular-nums text-muted-foreground",
170
+ tooLong && "text-destructive",
171
+ )}
172
+ >
173
+ {draft.length.toLocaleString()} / {MAX_LENGTH.toLocaleString()}
174
+ </p>
175
+ {error && (
176
+ <Alert variant="destructive">
177
+ <AlertDescription>{error}</AlertDescription>
178
+ </Alert>
179
+ )}
180
+ </form>
181
+
182
+ <DialogFooter className="gap-2 sm:justify-between">
183
+ {project.instructions ? (
184
+ <Button
185
+ type="button"
186
+ variant="ghost"
187
+ className="text-muted-foreground sm:mr-auto"
188
+ disabled={saving}
189
+ onClick={() => setDraft("")}
190
+ >
191
+ Clear
192
+ </Button>
193
+ ) : (
194
+ <span className="hidden sm:block" />
195
+ )}
196
+ <div className="flex flex-col-reverse gap-2 sm:flex-row">
197
+ <Button
198
+ type="button"
199
+ variant="outline"
200
+ disabled={saving}
201
+ onClick={() => onOpenChange(false)}
202
+ >
203
+ Cancel
204
+ </Button>
205
+ <Button
206
+ type="submit"
207
+ form={`${fieldId}-form`}
208
+ disabled={saving || tooLong || unchanged}
209
+ >
210
+ {saving && <Spinner data-icon="inline-start" />}
211
+ Save
212
+ </Button>
213
+ </div>
214
+ </DialogFooter>
215
+ </DialogContent>
216
+ </Dialog>
217
+ );
218
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Satu baris Project di homepage. Seluruh baris adalah tombol tunggal (bukan
3
+ * tombol di dalam tombol) agar target sentuh besar di layar HP dan tetap
4
+ * dapat diakses lewat keyboard.
5
+ */
6
+ import { ArrowRightIcon } from "lucide-react";
7
+ import { formatRelativeTime, type ProjectOverview } from "@/lib/project-overview";
8
+
9
+ export interface ProjectRowProps {
10
+ overview: ProjectOverview;
11
+ onOpen: () => void;
12
+ }
13
+
14
+ export function ProjectRow({ overview, onOpen }: ProjectRowProps) {
15
+ const { project, sessionCount, runningCount, lastSession, lastActivityAt } = overview;
16
+ const hasSession = lastSession !== null;
17
+
18
+ return (
19
+ <button
20
+ type="button"
21
+ onClick={onOpen}
22
+ className="group flex w-full items-center gap-3 rounded-xl border bg-card px-4 py-3 text-left shadow-sm transition-all hover:border-primary/40 hover:bg-accent hover:shadow-md focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"
23
+ aria-label={
24
+ hasSession ? `Resume last session of ${project.name}` : `Open project ${project.name}`
25
+ }
26
+ >
27
+ <span className="relative flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-base font-semibold text-primary uppercase">
28
+ {project.name.trim().charAt(0) || "?"}
29
+ {runningCount > 0 && (
30
+ <span
31
+ className="absolute -top-0.5 -right-0.5 size-2.5 rounded-full bg-emerald-500 ring-2 ring-card"
32
+ aria-hidden
33
+ />
34
+ )}
35
+ </span>
36
+
37
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5">
38
+ <span className="truncate text-base font-medium group-hover:text-primary">
39
+ {project.name}
40
+ </span>
41
+ <span className="truncate font-mono text-sm text-muted-foreground">{project.path}</span>
42
+ <span className="truncate text-sm text-muted-foreground">
43
+ {sessionCount === 0
44
+ ? "No sessions"
45
+ : `${sessionCount} session${sessionCount === 1 ? "" : "s"}`}
46
+ {runningCount > 0 && ` · ${runningCount} running`}
47
+ {" · "}
48
+ {formatRelativeTime(lastActivityAt)}
49
+ </span>
50
+ </span>
51
+
52
+ <span className="flex shrink-0 items-center gap-1 text-sm font-medium text-muted-foreground group-hover:text-primary">
53
+ <span className="hidden sm:inline">{hasSession ? "Resume" : "Open"}</span>
54
+ <ArrowRightIcon className="size-4 transition-transform group-hover:translate-x-0.5" />
55
+ </span>
56
+ </button>
57
+ );
58
+ }