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,256 @@
1
+ /**
2
+ * Interpretasi event SSE opencode — fungsi murni (tanpa state).
3
+ *
4
+ * Dipisah dari `session-manager.ts` agar dapat diuji & dibaca tersendiri:
5
+ * - Pemetaan event `permission.asked` / `question.asked` (v1 & v2) menjadi
6
+ * `InteractivePrompt` + kunci pengelompokan kartu kembar.
7
+ * - Deskripsi ramah error `session.error` dan kegagalan kirim prompt.
8
+ */
9
+ import { randomUUID } from "node:crypto";
10
+ import { type InteractivePrompt, normalizePromptOptions, type PromptQuestion } from "../../types";
11
+ import type { OpenCodeEvent } from "./opencode-client";
12
+
13
+ /** Ambil nilai field event dengan toleransi beberapa nama kunci. */
14
+ export function eventField(ev: OpenCodeEvent, ...keys: string[]): unknown {
15
+ for (const k of keys) {
16
+ const v = ev[k];
17
+ if (v !== undefined && v !== null) return v;
18
+ }
19
+ return undefined;
20
+ }
21
+
22
+ /**
23
+ * Deskripsi singkat sebuah permission request untuk judul kartu.
24
+ *
25
+ * Menerima penamaan v1 (`permission` + `patterns`) maupun v2
26
+ * (`action` + `resources`) — lihat `EventPermissionAsked` vs
27
+ * `EventPermissionV2Asked` pada skema opencode.
28
+ */
29
+ export function describePermission(ev: OpenCodeEvent): string {
30
+ const permission = eventField(ev, "permission", "action", "name");
31
+ const patterns = eventField(ev, "patterns", "resources");
32
+ const parts: string[] = [];
33
+ if (typeof permission === "string") parts.push(permission);
34
+ if (Array.isArray(patterns)) {
35
+ for (const p of patterns.slice(0, 3)) {
36
+ if (typeof p === "string") parts.push(`\`${p}\``);
37
+ }
38
+ }
39
+ return parts.length > 0 ? parts.join(" — ") : "Izin tool";
40
+ }
41
+
42
+ /**
43
+ * Baris pertama pesan error dari event `session.error` opencode (bentuk SSE
44
+ * ternormalisasi: `error: { name, data: { message } }`). Sisa `data.message`
45
+ * berupa stack trace — tidak berguna untuk UI.
46
+ */
47
+ export function sessionErrorMessage(ev: OpenCodeEvent): string {
48
+ const err = eventField(ev, "error");
49
+ if (typeof err !== "object" || err === null) return "";
50
+ const e = err as { data?: { message?: unknown } };
51
+ if (typeof e.data !== "object" || e.data === null) return "";
52
+ const raw = e.data.message;
53
+ if (typeof raw !== "string" || raw.trim() === "") return "";
54
+ return raw.split("\n")[0]?.trim() ?? "";
55
+ }
56
+
57
+ /**
58
+ * Deskripsi ramah event `session.error` opencode. Nama error (`error.name`)
59
+ * dipetakan ke pesan Inggris yang bisa dibaca user; pesan asli (baris
60
+ * pertama) disertakan bila ada karena sering lebih informatif
61
+ * (mis. `TypeError: File URL host …`).
62
+ */
63
+ export function describeSessionError(ev: OpenCodeEvent): string {
64
+ const err = eventField(ev, "error");
65
+ const name =
66
+ typeof err === "object" && err !== null ? (err as { name?: unknown }).name : undefined;
67
+ const line = sessionErrorMessage(ev);
68
+ // Petunjuk ramah per nama error; null = pesan asli lebih informatif
69
+ // (mis. `UnknownError` yang membawa TypeError asli).
70
+ const hint = (() => {
71
+ switch (name) {
72
+ case "ProviderAuthError":
73
+ return "Model provider authentication failed. Check the opencode login (`opencode auth`).";
74
+ case "APIError":
75
+ return "The model provider returned an API error. Try again or switch models.";
76
+ case "ContentFilterError":
77
+ return "The model response was blocked by a content filter.";
78
+ case "ContextOverflowError":
79
+ return "The conversation context exceeded the model limit. Start a new session or compact.";
80
+ case "MessageOutputLengthError":
81
+ return "The model output exceeded the message length limit.";
82
+ case "MessageAbortedError":
83
+ return "Prompt processing was aborted.";
84
+ case "StructuredOutputError":
85
+ return "Failed to parse the model's structured output.";
86
+ default:
87
+ return null;
88
+ }
89
+ })();
90
+ if (hint === null) return line || "Something went wrong while processing the prompt.";
91
+ return line ? `${hint} — ${line}` : hint;
92
+ }
93
+
94
+ /** Panjang aman untuk teks retry/error yang dimasukkan ke banner (hindari JSON blob raksasa). */
95
+ const BANNER_MAX_LENGTH = 240;
96
+
97
+ /**
98
+ * Pesan ramah dari event `session.status` opencode bertipe `retry`
99
+ * (`status: { type: "retry", attempt, message, action }`).
100
+ *
101
+ * Saat provider model mengembalikan error yang bisa dicoba ulang (rate limit,
102
+ * kredit habis, cooldown akun, 5xx), opencode TIDAK langsung gagal — ia retry
103
+ * dengan backoff (hingga 5 percobaan, menghormati header `retry-after` yang
104
+ * bisa sangat lama). Selama retry hanya event `session.status` yang dipancarkan
105
+ * (`busy`/`retry`), tanpa `session.error`/`session.idle` — tanpa banner ini,
106
+ * Client diam-diam menunggu sampai retry habis atau batas idle turn.
107
+ *
108
+ * Mengembalikan `null` untuk status non-retry agar pemanggil cukup memfilter.
109
+ */
110
+ export function retryStatusMessage(status: unknown): string | null {
111
+ if (typeof status !== "object" || status === null) return null;
112
+ const s = status as { type?: unknown; attempt?: unknown; message?: unknown; action?: unknown };
113
+ if (s.type !== "retry") return null;
114
+ // Upsell (mis. "Free limit reached") membawa `action.title` yang lebih
115
+ // ringkas daripada `message` mentah (yang bisa berupa JSON body error).
116
+ const action =
117
+ typeof s.action === "object" && s.action !== null
118
+ ? (s.action as { title?: unknown; message?: unknown })
119
+ : undefined;
120
+ const raw =
121
+ typeof action?.title === "string" && action.title.trim() !== ""
122
+ ? action.title
123
+ : typeof s.message === "string" && s.message.trim() !== ""
124
+ ? s.message
125
+ : null;
126
+ if (raw === null) return null;
127
+ const attempt = typeof s.attempt === "number" ? ` (attempt ${s.attempt})` : "";
128
+ const text = raw.split("\n")[0]?.trim() ?? raw;
129
+ const clipped = text.length > BANNER_MAX_LENGTH ? `${text.slice(0, BANNER_MAX_LENGTH)}…` : text;
130
+ return `The model provider is retrying${attempt}: ${clipped}`;
131
+ }
132
+
133
+ /** Terjemahkan kode error pengiriman prompt ke pesan yang bisa dibaca user. */
134
+ export function friendlySendError(raw: string): string {
135
+ const r = raw.trim();
136
+ if (r === "TURN_TIMEOUT") {
137
+ return "The model stopped responding — no activity for 5 minutes. Try sending the message again.";
138
+ }
139
+ const asyncMatch = r.match(/^OC_PROMPT_ASYNC_FAILED(?:\((\d+)\))?(?::\s*(.*))?$/);
140
+ if (asyncMatch) {
141
+ const status = asyncMatch[1];
142
+ const detail = asyncMatch[2];
143
+ if (status) return `Failed to send the prompt to opencode (status ${status}). Try again.`;
144
+ if (detail)
145
+ return `Failed to send the prompt to opencode: ${detail.split("\n")[0]?.trim() ?? detail}`;
146
+ return "Failed to send the prompt to opencode. Try again.";
147
+ }
148
+ const sendFail = r.match(/^SEND_FAILED:\s*(.*)$/);
149
+ if (sendFail) {
150
+ const detail = sendFail[1];
151
+ return detail
152
+ ? `Failed to send the prompt: ${detail.split("\n")[0]?.trim() ?? detail}`
153
+ : "Failed to send the prompt: the opencode connection is having trouble.";
154
+ }
155
+ return r.split("\n")[0] ?? r;
156
+ }
157
+
158
+ // Placeholder & deriveSessionTitle hidup di lib/session-title.ts (dipakai Client & Server).
159
+ // Re-export di sini agar import lama (`session-events`) tetap berfungsi.
160
+ export {
161
+ deriveSessionTitle,
162
+ displaySessionTitle,
163
+ isPlaceholderTitle,
164
+ SESSION_TITLE_PLACEHOLDERS,
165
+ } from "@/lib/session-title";
166
+
167
+ /** Bangun Interactive_Prompt dari event `permission.asked` (lihat juga `permissionGroupKey`). */
168
+ export function promptFromPermission(
169
+ ev: OpenCodeEvent,
170
+ sessionId: string,
171
+ now: number,
172
+ ): InteractivePrompt {
173
+ const requestId = String(eventField(ev, "requestID", "id") ?? randomUUID());
174
+ return {
175
+ id: requestId,
176
+ sessionId,
177
+ kind: "permission",
178
+ type: "confirmation",
179
+ title: describePermission(ev),
180
+ options: null,
181
+ status: "pending",
182
+ createdAt: now,
183
+ resolvedAt: null,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Bangun Interactive_Prompt dari event `question.asked`.
189
+ *
190
+ * Skema tool `question` opencode: `questions[] = { header?, question,
191
+ * options: [{label, description?}], multiple?, custom? }`. SELURUH
192
+ * pertanyaan disimpan (`questions`) untuk kartu multi-question; pertanyaan
193
+ * pertama juga disalin ke field legacy (title/options/multiple/custom) agar
194
+ * kode lama & data DB lama tetap bekerja. Opsi dinormalisasi ke `PromptOption`
195
+ * agar keterangan ikut tampil di kartu.
196
+ */
197
+ export function promptFromQuestion(
198
+ ev: OpenCodeEvent,
199
+ sessionId: string,
200
+ now: number,
201
+ ): InteractivePrompt {
202
+ const requestId = String(eventField(ev, "requestID", "id") ?? randomUUID());
203
+ const questions = eventField(ev, "questions");
204
+ const list = Array.isArray(questions)
205
+ ? (questions as {
206
+ question?: unknown;
207
+ header?: unknown;
208
+ options?: unknown;
209
+ multiple?: unknown;
210
+ custom?: unknown;
211
+ }[])
212
+ : [];
213
+ const first = list[0] ?? null;
214
+ const firstOptions = normalizePromptOptions(first?.options);
215
+ const legacyOptions = firstOptions.length > 0 ? firstOptions : null;
216
+ // Multi-question hanya disimpan bila benar-benar ada >1 pertanyaan —
217
+ // request tunggal cukup memakai field legacy (hemat kolom DB).
218
+ const multi: PromptQuestion[] | undefined =
219
+ list.length > 1
220
+ ? list.map((q) => ({
221
+ question: typeof q.question === "string" ? q.question : null,
222
+ header: typeof q.header === "string" ? q.header : null,
223
+ options: normalizePromptOptions(q.options),
224
+ multiple: q.multiple === true ? true : undefined,
225
+ custom: q.custom !== false,
226
+ }))
227
+ : undefined;
228
+ return {
229
+ id: requestId,
230
+ sessionId,
231
+ kind: "question",
232
+ type: "menu",
233
+ title: first && typeof first.question === "string" ? first.question : null,
234
+ options: legacyOptions,
235
+ multiple: first?.multiple === true ? true : undefined,
236
+ custom: first?.custom !== false,
237
+ questions: multi,
238
+ status: "pending",
239
+ createdAt: now,
240
+ resolvedAt: null,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Kunci pengelompokan prompt permission yang identik.
246
+ *
247
+ * opencode memancarkan SATU request permission per tool call, tanpa
248
+ * deduplikasi — meminta akses yang sama berulang (mis. `bash` pada direktori
249
+ * eksternal yang sama) membanjiri UI dengan kartu identik. Request dengan
250
+ * kind + judul yang sama dianggap satu keputusan yang sama: tampil sebagai
251
+ * SATU kartu, dan jawaban user diteruskan ke SELURUH request anggota grup
252
+ * (fan-out di `resolvePrompt`) agar turn tidak menggantung.
253
+ */
254
+ export function permissionGroupKey(p: InteractivePrompt): string {
255
+ return `${p.kind}\u0000${p.title ?? ""}`;
256
+ }