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,265 @@
1
+ /**
2
+ * Daftar opsi agent (mode) opencode — dipakai di dalam `PopoverContent`
3
+ * (`AgentPicker`, desktop) maupun langsung di `SheetContent` (mobile, tanpa
4
+ * popover bersarang). Setiap baris menampilkan ikon + nama + deskripsi
5
+ * singkat, supaya jelas ini adalah *pemilihan mode*, bukan tombol biasa.
6
+ *
7
+ * Kedua varian: nama dikapitalisasi, ikon dalam kotak, deskripsi 1 baris,
8
+ * dan agent pembantu (tanpa deskripsi / mode `subagent`, mis. compaction /
9
+ * title / summary) dilipat ke "More modes" agar pilihan utama tidak tenggelam.
10
+ * - `popover` (desktop): baris ringkas, centang di kanan.
11
+ * - `sheet` (mobile): baris lebih tinggi untuk jempol, indikator radio.
12
+ */
13
+ import {
14
+ CheckIcon,
15
+ ChevronDownIcon,
16
+ CompassIcon,
17
+ HammerIcon,
18
+ ListTreeIcon,
19
+ type LucideIcon,
20
+ } from "lucide-react";
21
+ import { useState } from "react";
22
+ import { Spinner } from "@/components/ui/spinner";
23
+ import { cn } from "@/lib/utils";
24
+ import type { AgentOption } from "@/server/services/opencode-client";
25
+
26
+ /** Ikon per agent: khusus dulu (build/plan), sisanya fallback per mode. */
27
+ function agentIcon(name: string, mode: AgentOption["mode"]) {
28
+ const n = name.toLowerCase();
29
+ if (n === "build") return HammerIcon;
30
+ if (n === "plan") return ListTreeIcon;
31
+ return mode === "primary" ? CompassIcon : ListTreeIcon;
32
+ }
33
+
34
+ /** Agent utama = primary/all yang punya deskripsi; sisanya pembantu. */
35
+ function isMainAgent(a: AgentOption): boolean {
36
+ return a.mode !== "subagent" && Boolean(a.description);
37
+ }
38
+
39
+ function titleCase(name: string): string {
40
+ return name.charAt(0).toUpperCase() + name.slice(1);
41
+ }
42
+
43
+ export interface AgentModeListProps {
44
+ agents: AgentOption[] | null;
45
+ loading: boolean;
46
+ error: string | null;
47
+ /** Agent aktif saat ini; null = default opencode (build). */
48
+ activeAgent: string | null;
49
+ saving?: boolean;
50
+ disabled?: boolean;
51
+ onPick: (name: string | null) => void;
52
+ className?: string;
53
+ variant?: "popover" | "sheet";
54
+ }
55
+
56
+ export function AgentModeList({
57
+ agents,
58
+ loading,
59
+ error,
60
+ activeAgent,
61
+ saving = false,
62
+ disabled = false,
63
+ onPick,
64
+ className,
65
+ variant = "popover",
66
+ }: AgentModeListProps) {
67
+ const sheet = variant === "sheet";
68
+ const all = agents ?? [];
69
+ const main = all.filter(isMainAgent);
70
+ const extra = all.filter((a) => !isMainAgent(a));
71
+ const [expanded, setExpanded] = useState(false);
72
+ // Agent aktif ada di "More modes" -> selalu terbuka agar pilihan terlihat.
73
+ const activeInExtra = extra.some((a) => a.name === activeAgent);
74
+ const showExtra = expanded || activeInExtra;
75
+
76
+ if (loading) {
77
+ return (
78
+ <div className="flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground">
79
+ <Spinner className="size-3.5" />
80
+ Loading agents…
81
+ </div>
82
+ );
83
+ }
84
+
85
+ if (error) {
86
+ return <div className="px-3 py-2.5 text-sm text-destructive">{error}</div>;
87
+ }
88
+
89
+ const row = (key: string, opts: RowProps) => (
90
+ <ModeRow key={key} {...opts} sheet={sheet} disabled={disabled || saving} />
91
+ );
92
+
93
+ return (
94
+ <div className={cn("flex flex-col gap-0.5", className)}>
95
+ {row("__default", {
96
+ icon: CompassIcon,
97
+ label: "Default",
98
+ description: "Let KCG Code choose (usually Build)",
99
+ selected: activeAgent === null,
100
+ onClick: () => onPick(null),
101
+ })}
102
+ {main.map((a) =>
103
+ row(a.name, {
104
+ icon: agentIcon(a.name, a.mode),
105
+ label: titleCase(a.name),
106
+ description: a.description,
107
+ selected: a.name === activeAgent,
108
+ accent: a.name.toLowerCase() === "plan",
109
+ onClick: () => onPick(a.name),
110
+ }),
111
+ )}
112
+
113
+ {extra.length > 0 && (
114
+ <>
115
+ <button
116
+ type="button"
117
+ onClick={() => !activeInExtra && setExpanded((v) => !v)}
118
+ aria-expanded={showExtra}
119
+ className="mt-1 flex w-full items-center gap-1.5 rounded-lg px-3 py-2.5 text-left text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
120
+ >
121
+ <ChevronDownIcon
122
+ className={cn("size-3.5 transition-transform", !showExtra && "-rotate-90")}
123
+ aria-hidden
124
+ />
125
+ More modes
126
+ <span className="tabular-nums opacity-70">{extra.length}</span>
127
+ </button>
128
+ {showExtra &&
129
+ extra.map((a) =>
130
+ row(a.name, {
131
+ icon: agentIcon(a.name, a.mode),
132
+ label: titleCase(a.name),
133
+ description: a.description,
134
+ selected: a.name === activeAgent,
135
+ compact: true,
136
+ onClick: () => onPick(a.name),
137
+ }),
138
+ )}
139
+ </>
140
+ )}
141
+
142
+ {all.length === 0 && (
143
+ <div className="px-3 py-2.5 text-sm text-muted-foreground">No agents found.</div>
144
+ )}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ interface RowProps {
150
+ icon: LucideIcon;
151
+ label: string;
152
+ description: string | null;
153
+ selected: boolean;
154
+ accent?: boolean;
155
+ /** Baris pembantu di "More modes": lebih pendek. */
156
+ compact?: boolean;
157
+ onClick: () => void;
158
+ }
159
+
160
+ function ModeRow({
161
+ icon: Icon,
162
+ label,
163
+ description,
164
+ selected,
165
+ accent,
166
+ compact,
167
+ onClick,
168
+ sheet,
169
+ disabled,
170
+ }: RowProps & { sheet: boolean; disabled: boolean }) {
171
+ if (!sheet) {
172
+ return (
173
+ <button
174
+ type="button"
175
+ aria-pressed={selected}
176
+ disabled={disabled}
177
+ onClick={onClick}
178
+ className={cn(
179
+ "group/row flex w-full items-center gap-2.5 rounded-lg px-2 text-left transition-colors outline-none",
180
+ "hover:bg-accent focus-visible:bg-accent disabled:opacity-50",
181
+ compact ? "min-h-9 py-1.5" : "min-h-12 py-2",
182
+ selected && "bg-accent/60",
183
+ )}
184
+ >
185
+ <span
186
+ className={cn(
187
+ "flex shrink-0 items-center justify-center rounded-md",
188
+ compact ? "size-6" : "size-8",
189
+ selected
190
+ ? "bg-primary/15 text-primary dark:bg-primary/25"
191
+ : accent
192
+ ? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
193
+ : "bg-muted text-muted-foreground group-hover/row:bg-background/60",
194
+ )}
195
+ aria-hidden
196
+ >
197
+ <Icon className={compact ? "size-3.5" : "size-4"} />
198
+ </span>
199
+ <span className="flex min-w-0 flex-1 flex-col">
200
+ <span className={cn("truncate text-sm leading-5", selected && "font-medium")}>
201
+ {label}
202
+ </span>
203
+ {description && !compact && (
204
+ <span className="truncate text-xs text-muted-foreground" title={description}>
205
+ {description}
206
+ </span>
207
+ )}
208
+ </span>
209
+ <CheckIcon
210
+ className={cn("size-4 shrink-0 text-primary", !selected && "invisible")}
211
+ aria-hidden
212
+ />
213
+ </button>
214
+ );
215
+ }
216
+
217
+ return (
218
+ <button
219
+ type="button"
220
+ aria-pressed={selected}
221
+ disabled={disabled}
222
+ onClick={onClick}
223
+ className={cn(
224
+ "flex w-full items-center gap-3 rounded-xl px-3 text-left transition-colors outline-none",
225
+ "active:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50",
226
+ compact ? "min-h-11 py-2" : "min-h-14 py-2.5",
227
+ selected ? "bg-primary/10 dark:bg-primary/15" : "hover:bg-muted/60",
228
+ )}
229
+ >
230
+ <span
231
+ className={cn(
232
+ "flex shrink-0 items-center justify-center rounded-lg",
233
+ compact ? "size-7" : "size-9",
234
+ selected
235
+ ? "bg-primary/15 text-primary dark:bg-primary/25"
236
+ : accent
237
+ ? "bg-amber-500/10 text-amber-600 dark:text-amber-400"
238
+ : "bg-muted text-muted-foreground",
239
+ )}
240
+ aria-hidden
241
+ >
242
+ <Icon className={compact ? "size-3.5" : "size-[18px]"} />
243
+ </span>
244
+ <span className="flex min-w-0 flex-1 flex-col">
245
+ <span className={cn("text-[15px] leading-5", selected && "font-medium")}>{label}</span>
246
+ {description && !compact && (
247
+ <span className="line-clamp-1 text-[13px] leading-snug text-muted-foreground">
248
+ {description}
249
+ </span>
250
+ )}
251
+ </span>
252
+ <span
253
+ className={cn(
254
+ "flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
255
+ selected
256
+ ? "border-primary bg-primary text-primary-foreground"
257
+ : "border-muted-foreground/40",
258
+ )}
259
+ aria-hidden
260
+ >
261
+ {selected && <CheckIcon className="size-3" strokeWidth={3} />}
262
+ </span>
263
+ </button>
264
+ );
265
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pemilih agent (mode) opencode untuk composer Session — padanan tombol
3
+ * ganti mode build/plan di TUI opencode (Tab).
4
+ *
5
+ * - Trigger: tombol kecil di toolbar BAWAH input prompt (ala aplikasi chat);
6
+ * menampilkan nama agent aktif + ikon. `plan` ditandai khusus (ikon +
7
+ * warna berbeda) supaya user sadar sedang "mode aman".
8
+ * - Daftar agent dari `GET /api/projects/:id/agents` (dimuat saat popover
9
+ * pertama dibuka — server headless tidak di-spawn kalau tidak dipakai).
10
+ * - Perubahan via `PUT /api/sessions/:id` (`agent`), berlaku di prompt
11
+ * berikutnya tanpa restart. Opsi "Default" mengirim `agent: null`.
12
+ *
13
+ * Fetch + pick logic ada di `useAgentPicker` dan daftar opsi di
14
+ * `AgentModeList` — keduanya dipakai ulang langsung (tanpa popover) di Sheet
15
+ * aksi mobile (`SessionView`).
16
+ */
17
+ import { ChevronDownIcon, CompassIcon, HammerIcon, ListTreeIcon } from "lucide-react";
18
+ import { useEffect, useState } from "react";
19
+ import { AgentModeList } from "@/components/sessions/AgentModeList";
20
+ import { Button } from "@/components/ui/button";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
22
+ import { Spinner } from "@/components/ui/spinner";
23
+ import { useAgentPicker } from "@/hooks/useAgentPicker";
24
+ import { cn } from "@/lib/utils";
25
+
26
+ export interface AgentPickerProps {
27
+ projectId: string;
28
+ /** null = Session belum ada (composer homepage): pilihan tidak di-PUT. */
29
+ sessionId: string | null;
30
+ /** Agent aktif saat ini; null = default opencode (build). */
31
+ agent: string | null;
32
+ /** Dipanggil setelah server menerima perubahan agent. */
33
+ onChanged: (agent: string | null) => void;
34
+ /** Nonaktifkan picker (mis. Session tidak running / WS sibuk). */
35
+ disabled?: boolean;
36
+ /** Override tampilan trigger, mis. `w-full justify-start` saat ditaruh di dalam Sheet aksi mobile. */
37
+ className?: string;
38
+ }
39
+
40
+ export function AgentPicker({
41
+ projectId,
42
+ sessionId,
43
+ agent,
44
+ onChanged,
45
+ disabled = false,
46
+ className,
47
+ }: AgentPickerProps) {
48
+ const [open, setOpen] = useState(false);
49
+ const { agents, loading, saving, error, load, pick } = useAgentPicker(
50
+ projectId,
51
+ sessionId,
52
+ (name) => {
53
+ onChanged(name);
54
+ setOpen(false);
55
+ },
56
+ );
57
+
58
+ const isPlan = agent?.toLowerCase() === "plan";
59
+ const label = agent ? agent.charAt(0).toUpperCase() + agent.slice(1) : "Default";
60
+ const ActiveIcon = isPlan
61
+ ? ListTreeIcon
62
+ : agent?.toLowerCase() === "build"
63
+ ? HammerIcon
64
+ : CompassIcon;
65
+
66
+ // Daftar dimuat saat popover pertama dibuka (lazy, ala ModelPicker).
67
+ useEffect(() => {
68
+ if (open) load();
69
+ }, [open, load]);
70
+
71
+ return (
72
+ <Popover open={open} onOpenChange={(o) => setOpen(o)}>
73
+ <PopoverTrigger asChild>
74
+ <Button
75
+ type="button"
76
+ variant="ghost"
77
+ size="sm"
78
+ disabled={disabled}
79
+ aria-label={`Agent mode: ${label}`}
80
+ title="Change mode"
81
+ className={cn(
82
+ "group h-8 shrink-0 gap-1.5 rounded-full border border-transparent px-2.5 text-[13px] font-medium text-muted-foreground",
83
+ "hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground",
84
+ isPlan &&
85
+ "border-amber-500/30 bg-amber-500/10 text-amber-700 hover:bg-amber-500/15 hover:text-amber-700 data-[state=open]:bg-amber-500/15 data-[state=open]:text-amber-700 dark:text-amber-400 dark:hover:text-amber-400 dark:data-[state=open]:text-amber-400",
86
+ className,
87
+ )}
88
+ >
89
+ {saving ? <Spinner className="size-4" /> : <ActiveIcon className="size-3.5" />}
90
+ <span className="inline max-w-24 truncate sm:max-w-32">{label}</span>
91
+ <ChevronDownIcon
92
+ className="size-3.5 opacity-60 transition-transform group-data-[state=open]:rotate-180"
93
+ aria-hidden
94
+ />
95
+ </Button>
96
+ </PopoverTrigger>
97
+ <PopoverContent
98
+ align="end"
99
+ side="top"
100
+ sideOffset={8}
101
+ collisionPadding={12}
102
+ className="w-80 p-0"
103
+ >
104
+ <div className="border-b px-3 pt-2.5 pb-2">
105
+ <p className="text-sm font-medium">Mode</p>
106
+ <p className="text-xs text-muted-foreground">How the agent handles your next message</p>
107
+ </div>
108
+ {/* Tinggi menyesuaikan ruang di atas trigger (Radix), dibatasi 22rem. */}
109
+ <div className="max-h-[min(22rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain p-1.5 [scrollbar-width:thin]">
110
+ <AgentModeList
111
+ agents={agents}
112
+ loading={loading}
113
+ error={error}
114
+ activeAgent={agent}
115
+ saving={saving}
116
+ disabled={disabled}
117
+ onPick={(name) => void pick(name)}
118
+ />
119
+ </div>
120
+ </PopoverContent>
121
+ </Popover>
122
+ );
123
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Render satu turn assistant (Satu kesatuan respon model).
3
+ *
4
+ * Isi: satu atau lebih blok "Thought process" collapsible (kluster steps
5
+ * reasoning/tool per round — `ThoughtProcessBlock.tsx`), diikuti jawaban
6
+ * teks / error, lalu footer waktu (+ durasi thinking) setelah turn selesai.
7
+ */
8
+ import { CircleAlertIcon } from "lucide-react";
9
+ import { MarkdownContent } from "@/components/sessions/MarkdownContent";
10
+ import { CopyMessageButton } from "@/components/sessions/MessageActions";
11
+ import { Bubble, BubbleContent } from "@/components/ui/bubble";
12
+ import { Message, MessageContent, MessageFooter } from "@/components/ui/message";
13
+ import { useSmoothText } from "@/hooks/useSmoothText";
14
+ import type { CollapsibleState } from "@/lib/collapsible";
15
+ import { formatDuration, formatTime } from "@/lib/time";
16
+ import { partText, turnBlocks, turnSegments, turnThoughtDuration } from "@/lib/turns";
17
+ import { cn } from "@/lib/utils";
18
+ import type { AssistantTurnGroup } from "@/types";
19
+ import { ThoughtProcessBlock } from "./ThoughtProcessBlock";
20
+
21
+ /**
22
+ * Markdown jawaban yang sedang di-stream: teks dikejar halus per frame
23
+ * (`useSmoothText`) agar tidak melompat per potongan delta. Hanya blok
24
+ * terakhir dari turn yang masih berjalan yang dianimasikan; riwayat & blok
25
+ * yang sudah lewat langsung tampil penuh.
26
+ */
27
+ function StreamingMarkdown({ text, streaming }: { text: string; streaming: boolean }) {
28
+ const shown = useSmoothText(text, streaming);
29
+ return <MarkdownContent>{shown}</MarkdownContent>;
30
+ }
31
+
32
+ /** Model kadang membalas "(empty)" saat tidak ada teks untuk ditampilkan. */
33
+ function isEmptyReply(text: string): boolean {
34
+ return text.trim().toLowerCase() === "(empty)";
35
+ }
36
+
37
+ export interface AssistantTurnProps {
38
+ /** Pesan assistant berurutan dari satu turn balasan model. */
39
+ group: AssistantTurnGroup;
40
+ /** Status toggle blok "Thought process" (key `group.id:blockKey`). */
41
+ collapsible: CollapsibleState;
42
+ /** Toggle satu blok Thought process. */
43
+ onToggle: (key: string) => void;
44
+ }
45
+
46
+ export function AssistantTurn({ group, collapsible, onToggle }: AssistantTurnProps) {
47
+ const m = group.messages[0];
48
+ if (!m) return null;
49
+ const segments = turnSegments(group.messages);
50
+ const streaming = m.streaming === true || group.messages.some((msg) => msg.streaming === true);
51
+ const blocks = turnBlocks(segments);
52
+ // Durasi thinking untuk footer — hanya dihitung setelah turn selesai.
53
+ const thoughtMs = !streaming ? turnThoughtDuration(group.messages) : null;
54
+ // Teks jawaban (tanpa thinking/tool) untuk tombol Copy, dipisah paragraf.
55
+ const answerText = blocks
56
+ .map((b) =>
57
+ b.content?.kind === "text" && !isEmptyReply(b.content.text) ? b.content.text.trim() : "",
58
+ )
59
+ .filter((t) => t !== "")
60
+ .join("\n\n");
61
+
62
+ return (
63
+ <Message align="start">
64
+ <MessageContent>
65
+ {blocks.map((block, bi) => {
66
+ const isLastBlock = bi === blocks.length - 1;
67
+ const hasSteps = block.steps.length > 0;
68
+ return (
69
+ <div key={block.key}>
70
+ {/* Thought process block — pisah per round thinking */}
71
+ <ThoughtProcessBlock
72
+ groupId={group.id}
73
+ blockKey={block.key}
74
+ steps={block.steps}
75
+ collapsible={collapsible}
76
+ onToggle={onToggle}
77
+ streaming={streaming}
78
+ isLastBlock={isLastBlock}
79
+ />
80
+ {/* Konten teks/error setelah thinking round ini */}
81
+ {block.content?.kind === "error" && (
82
+ <div className={cn(hasSteps && "mt-3")}>
83
+ <Bubble variant="destructive">
84
+ <BubbleContent>
85
+ <div className="flex gap-2">
86
+ <CircleAlertIcon
87
+ className="mt-0.5 size-4 shrink-0"
88
+ data-icon="inline-start"
89
+ />
90
+ <span className="whitespace-pre-wrap">
91
+ {partText(block.content.part) ??
92
+ "Something went wrong while processing the prompt."}
93
+ </span>
94
+ </div>
95
+ </BubbleContent>
96
+ </Bubble>
97
+ </div>
98
+ )}
99
+ {/* Balasan kosong dari model ("(empty)") — tetap tampil sebagai
100
+ penanda, tapi kecil & redup agar tidak terbaca sebagai jawaban. */}
101
+ {block.content?.kind === "text" && isEmptyReply(block.content.text) && (
102
+ <p className={cn("text-xs text-muted-foreground/70 italic", hasSteps && "mt-2")}>
103
+ {block.content.text.trim()}
104
+ </p>
105
+ )}
106
+ {block.content?.kind === "text" &&
107
+ block.content.text !== "" &&
108
+ !isEmptyReply(block.content.text) && (
109
+ <div className={cn(hasSteps && "mt-3")}>
110
+ <Bubble variant="ghost" className="max-w-full">
111
+ <BubbleContent className="w-full">
112
+ <StreamingMarkdown
113
+ text={block.content.text}
114
+ streaming={streaming && isLastBlock}
115
+ />
116
+ </BubbleContent>
117
+ </Bubble>
118
+ </div>
119
+ )}
120
+ </div>
121
+ );
122
+ })}
123
+ {/* Footer: tersembunyi saat streaming, muncul dengan animasi fade +
124
+ slide dari kanan setelah turn selesai. Format:
125
+ HH:MM:SS · ● · thoughts Xs */}
126
+ {!streaming && (
127
+ <MessageFooter className="gap-1 animate-in fade-in-0 slide-in-from-right-4 duration-500">
128
+ <span className="text-muted-foreground/80">
129
+ {formatTime(m.createdAt)}
130
+ {thoughtMs !== null && (
131
+ <>
132
+ <span className="mx-1.5 text-muted-foreground/40" aria-hidden>
133
+ ●
134
+ </span>
135
+ <span>thoughts {formatDuration(thoughtMs)}</span>
136
+ </>
137
+ )}
138
+ </span>
139
+ <CopyMessageButton text={answerText} className="ml-1" />
140
+ </MessageFooter>
141
+ )}
142
+ </MessageContent>
143
+ </Message>
144
+ );
145
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Tampilan satu lampiran non-gambar: ikon berwarna sesuai jenis file + nama
3
+ * + keterangan singkat (ekstensi · ukuran). Dipakai composer (lampiran
4
+ * pending & animasi plate) dan bubble pesan user (tautan unduh).
5
+ */
6
+ import {
7
+ FileArchiveIcon,
8
+ FileAudioIcon,
9
+ FileCodeIcon,
10
+ FileIcon,
11
+ FileSpreadsheetIcon,
12
+ FileTextIcon,
13
+ FileVideoIcon,
14
+ ImageIcon,
15
+ type LucideIcon,
16
+ } from "lucide-react";
17
+ import { attachmentKind, fileExtension, formatBytes } from "@/lib/attachments";
18
+ import { cn } from "@/lib/utils";
19
+
20
+ const CODE_EXT = new Set([
21
+ "js",
22
+ "mjs",
23
+ "cjs",
24
+ "jsx",
25
+ "ts",
26
+ "mts",
27
+ "cts",
28
+ "tsx",
29
+ "vue",
30
+ "svelte",
31
+ "astro",
32
+ "py",
33
+ "rb",
34
+ "php",
35
+ "java",
36
+ "kt",
37
+ "kts",
38
+ "scala",
39
+ "go",
40
+ "rs",
41
+ "c",
42
+ "h",
43
+ "cc",
44
+ "cpp",
45
+ "hpp",
46
+ "cs",
47
+ "swift",
48
+ "m",
49
+ "dart",
50
+ "lua",
51
+ "pl",
52
+ "r",
53
+ "jl",
54
+ "ex",
55
+ "exs",
56
+ "erl",
57
+ "hs",
58
+ "clj",
59
+ "sql",
60
+ "sh",
61
+ "bash",
62
+ "zsh",
63
+ "fish",
64
+ "ps1",
65
+ "bat",
66
+ "html",
67
+ "htm",
68
+ "css",
69
+ "scss",
70
+ "sass",
71
+ "less",
72
+ "json",
73
+ "jsonc",
74
+ "yaml",
75
+ "yml",
76
+ "toml",
77
+ "xml",
78
+ "graphql",
79
+ "gql",
80
+ "proto",
81
+ "tf",
82
+ "hcl",
83
+ "nix",
84
+ "svg",
85
+ ]);
86
+ const SHEET_EXT = new Set(["csv", "tsv", "xls", "xlsx", "ods", "numbers"]);
87
+ const ARCHIVE_EXT = new Set(["zip", "tar", "gz", "tgz", "bz2", "xz", "7z", "rar", "zst"]);
88
+
89
+ interface Glyph {
90
+ Icon: LucideIcon;
91
+ /** Kelas warna kotak ikon (latar tipis + warna ikon). */
92
+ tone: string;
93
+ }
94
+
95
+ export function fileGlyph(mime: string, filename: string): Glyph {
96
+ const ext = fileExtension(filename);
97
+ const kind = attachmentKind(mime, filename);
98
+ if (kind === "image")
99
+ return { Icon: ImageIcon, tone: "bg-sky-500/15 text-sky-600 dark:text-sky-400" };
100
+ if (kind === "pdf")
101
+ return { Icon: FileTextIcon, tone: "bg-red-500/15 text-red-600 dark:text-red-400" };
102
+ if (SHEET_EXT.has(ext)) {
103
+ return {
104
+ Icon: FileSpreadsheetIcon,
105
+ tone: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
106
+ };
107
+ }
108
+ if (CODE_EXT.has(ext)) {
109
+ return { Icon: FileCodeIcon, tone: "bg-violet-500/15 text-violet-600 dark:text-violet-400" };
110
+ }
111
+ if (ARCHIVE_EXT.has(ext)) {
112
+ return { Icon: FileArchiveIcon, tone: "bg-amber-500/15 text-amber-700 dark:text-amber-400" };
113
+ }
114
+ if (mime.startsWith("audio/")) {
115
+ return { Icon: FileAudioIcon, tone: "bg-pink-500/15 text-pink-600 dark:text-pink-400" };
116
+ }
117
+ if (mime.startsWith("video/")) {
118
+ return { Icon: FileVideoIcon, tone: "bg-indigo-500/15 text-indigo-600 dark:text-indigo-400" };
119
+ }
120
+ if (kind === "text") return { Icon: FileTextIcon, tone: "bg-muted text-muted-foreground" };
121
+ return { Icon: FileIcon, tone: "bg-muted text-muted-foreground" };
122
+ }
123
+
124
+ /** "PDF · 1.2 MB" — ekstensi (atau "File") + ukuran bila diketahui. */
125
+ export function fileMeta(filename: string, size?: number): string {
126
+ const ext = fileExtension(filename);
127
+ const label = ext ? ext.toUpperCase() : "File";
128
+ return size !== undefined && size > 0 ? `${label} · ${formatBytes(size)}` : label;
129
+ }
130
+
131
+ export function FileTile({
132
+ filename,
133
+ mime,
134
+ size,
135
+ className,
136
+ }: {
137
+ filename: string;
138
+ mime: string;
139
+ size?: number;
140
+ className?: string;
141
+ }) {
142
+ const { Icon, tone } = fileGlyph(mime, filename);
143
+ return (
144
+ <span
145
+ className={cn(
146
+ "flex min-w-0 items-center gap-2.5 rounded-lg border bg-card px-2.5 text-left",
147
+ className,
148
+ )}
149
+ >
150
+ <span className={cn("flex size-10 shrink-0 items-center justify-center rounded-md", tone)}>
151
+ <Icon className="size-5" aria-hidden />
152
+ </span>
153
+ <span className="flex min-w-0 flex-col">
154
+ <span className="truncate text-[13px] leading-5 font-medium">{filename}</span>
155
+ <span className="truncate text-[11.5px] leading-4 text-muted-foreground">
156
+ {fileMeta(filename, size)}
157
+ </span>
158
+ </span>
159
+ </span>
160
+ );
161
+ }