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.
- package/README.md +228 -2
- package/bin/kcgcode.ts +13 -0
- package/bun-env.d.ts +19 -0
- package/kcg-code.config.example.json +3 -0
- package/package.json +83 -4
- package/public/logo.svg +1 -0
- package/public/manifest.json +18 -0
- package/public/sw.js +49 -0
- package/src/App.tsx +39 -0
- package/src/__tests__/manifest.test.ts +61 -0
- package/src/build-config.ts +9 -0
- package/src/cli/__tests__/dashboard.test.ts +62 -0
- package/src/cli/args.ts +123 -0
- package/src/cli/commands.ts +174 -0
- package/src/cli/dashboard.tsx +343 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/theme.ts +100 -0
- package/src/components/auth/AuthGate.tsx +182 -0
- package/src/components/auth/AvatarEditor.tsx +236 -0
- package/src/components/auth/LockScreen.tsx +714 -0
- package/src/components/auth/ProfileAvatar.tsx +60 -0
- package/src/components/auth/WelcomeScreen.tsx +672 -0
- package/src/components/auth/__tests__/WelcomeScreen.test.tsx +42 -0
- package/src/components/auth/avatar-icons.tsx +114 -0
- package/src/components/layout/AppShell.tsx +142 -0
- package/src/components/layout/AppSidebar.tsx +419 -0
- package/src/components/layout/SearchDialog.tsx +189 -0
- package/src/components/layout/SidebarAccount.tsx +111 -0
- package/src/components/layout/SidebarSessionMenu.tsx +163 -0
- package/src/components/layout/ThemeToggle.tsx +67 -0
- package/src/components/projects/FolderBrowser.tsx +142 -0
- package/src/components/projects/HomeChat.tsx +250 -0
- package/src/components/projects/NewProjectDialog.tsx +249 -0
- package/src/components/projects/PanelSection.tsx +102 -0
- package/src/components/projects/ProjectInstructions.tsx +229 -0
- package/src/components/projects/ProjectRow.tsx +58 -0
- package/src/components/projects/ProjectSidePanel.tsx +448 -0
- package/src/components/sessions/AgentModeList.tsx +265 -0
- package/src/components/sessions/AgentPicker.tsx +123 -0
- package/src/components/sessions/AssistantTurn.tsx +145 -0
- package/src/components/sessions/AttachmentTile.tsx +161 -0
- package/src/components/sessions/CodeBlock.tsx +107 -0
- package/src/components/sessions/ComposerActionSheet.tsx +96 -0
- package/src/components/sessions/ConfirmDeleteProjectDialog.tsx +80 -0
- package/src/components/sessions/ConfirmSessionDeleteDialog.tsx +72 -0
- package/src/components/sessions/ContextPanel.tsx +561 -0
- package/src/components/sessions/DropPlate.tsx +165 -0
- package/src/components/sessions/FileDropOverlay.tsx +232 -0
- package/src/components/sessions/FileMentionDropdown.tsx +47 -0
- package/src/components/sessions/ImageLightbox.tsx +173 -0
- package/src/components/sessions/MarkdownContent.tsx +187 -0
- package/src/components/sessions/Mascot.tsx +47 -0
- package/src/components/sessions/MessageActions.tsx +60 -0
- package/src/components/sessions/MessageRail.tsx +121 -0
- package/src/components/sessions/ModelPicker.tsx +209 -0
- package/src/components/sessions/ModelSearchList.tsx +151 -0
- package/src/components/sessions/NewSessionDialog.tsx +189 -0
- package/src/components/sessions/PendingAttachments.tsx +127 -0
- package/src/components/sessions/PromptCard.tsx +1085 -0
- package/src/components/sessions/PromptPanel.tsx +112 -0
- package/src/components/sessions/SessionComposer.tsx +640 -0
- package/src/components/sessions/SessionHeader.tsx +175 -0
- package/src/components/sessions/SessionList.tsx +396 -0
- package/src/components/sessions/SessionRow.tsx +137 -0
- package/src/components/sessions/SessionTimeline.tsx +275 -0
- package/src/components/sessions/SessionTimelineSkeleton.tsx +83 -0
- package/src/components/sessions/SessionView.tsx +236 -0
- package/src/components/sessions/ThoughtProcessBlock.tsx +199 -0
- package/src/components/sessions/UserMessage.tsx +177 -0
- package/src/components/sessions/__tests__/ContextPanel.test.tsx +220 -0
- package/src/components/sessions/__tests__/MarkdownContent.test.tsx +89 -0
- package/src/components/sessions/__tests__/MessageRail.test.tsx +40 -0
- package/src/components/sessions/__tests__/PermissionCard.test.tsx +79 -0
- package/src/components/sessions/__tests__/PromptCard.test.ts +166 -0
- package/src/components/sessions/__tests__/QuestionCard.test.tsx +166 -0
- package/src/components/sessions/__tests__/SessionTimelineSkeleton.test.tsx +32 -0
- package/src/components/sessions/__tests__/SessionView.test.ts +386 -0
- package/src/components/settings/AboutGroup.tsx +170 -0
- package/src/components/settings/AccountGroup.tsx +122 -0
- package/src/components/settings/DevicesGroup.tsx +151 -0
- package/src/components/settings/LhrRemoteAccess.tsx +262 -0
- package/src/components/settings/PasskeysGroup.tsx +551 -0
- package/src/components/settings/RemoteAccessGroup.tsx +1226 -0
- package/src/components/settings/SecurityGroup.tsx +566 -0
- package/src/components/settings/WorkspaceGroup.tsx +370 -0
- package/src/components/settings/__tests__/AboutGroup.test.tsx +47 -0
- package/src/components/settings/__tests__/PasskeysGroup.test.tsx +109 -0
- package/src/components/settings/__tests__/WorkspaceGroup.test.tsx +49 -0
- package/src/components/settings/prefs.tsx +265 -0
- package/src/components/skills/InstallNotifications.tsx +325 -0
- package/src/components/skills/InstallSkillDialog.tsx +233 -0
- package/src/components/skills/SkillCards.tsx +210 -0
- package/src/components/skills/SkillsProjectPicker.tsx +178 -0
- package/src/components/ui/alert-dialog.tsx +179 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/bubble.tsx +125 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/dialog.tsx +142 -0
- package/src/components/ui/dropdown-menu.tsx +225 -0
- package/src/components/ui/empty.tsx +94 -0
- package/src/components/ui/field.tsx +234 -0
- package/src/components/ui/input-group.tsx +161 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +85 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +172 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +699 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +51 -0
- package/src/config.ts +85 -0
- package/src/db.ts +146 -0
- package/src/frontend.tsx +51 -0
- package/src/hooks/__tests__/useMention.test.ts +79 -0
- package/src/hooks/__tests__/useResyncOnReconnect.test.ts +128 -0
- package/src/hooks/__tests__/useRouter.test.ts +17 -0
- package/src/hooks/__tests__/useSmoothText.react.test.ts +194 -0
- package/src/hooks/__tests__/useSmoothText.test.ts +61 -0
- package/src/hooks/__tests__/useTheme.test.ts +118 -0
- package/src/hooks/use-mobile.ts +16 -0
- package/src/hooks/useAgentPicker.ts +97 -0
- package/src/hooks/useMediaQuery.ts +49 -0
- package/src/hooks/useMention.ts +236 -0
- package/src/hooks/useProjects.ts +88 -0
- package/src/hooks/useResyncOnReconnect.ts +28 -0
- package/src/hooks/useRouter.ts +92 -0
- package/src/hooks/useSessionChat.ts +427 -0
- package/src/hooks/useSessionContext.ts +106 -0
- package/src/hooks/useSessionList.ts +234 -0
- package/src/hooks/useSidebarData.ts +108 -0
- package/src/hooks/useSmoothText.ts +122 -0
- package/src/hooks/useTheme.ts +164 -0
- package/src/hooks/useWebSocket.ts +196 -0
- package/src/index.css +27 -0
- package/src/index.html +14 -0
- package/src/index.ts +49 -0
- package/src/lib/__tests__/attachments.test.ts +58 -0
- package/src/lib/__tests__/avatar-presets.test.ts +25 -0
- package/src/lib/__tests__/collapsible.test.ts +101 -0
- package/src/lib/__tests__/composer.test.ts +118 -0
- package/src/lib/__tests__/highlight.test.ts +42 -0
- package/src/lib/__tests__/home-chat.test.ts +64 -0
- package/src/lib/__tests__/lock-screen.test.ts +64 -0
- package/src/lib/__tests__/model-picker.test.ts +205 -0
- package/src/lib/__tests__/onboarding.test.ts +41 -0
- package/src/lib/__tests__/permission-info.test.ts +61 -0
- package/src/lib/__tests__/project-extensions.test.ts +52 -0
- package/src/lib/__tests__/project-overview.test.ts +173 -0
- package/src/lib/__tests__/references.test.ts +121 -0
- package/src/lib/__tests__/routes.test.ts +134 -0
- package/src/lib/__tests__/session-summary.test.ts +115 -0
- package/src/lib/__tests__/session-title.test.ts +58 -0
- package/src/lib/__tests__/sidebar-groups.test.ts +100 -0
- package/src/lib/__tests__/skill-installs.test.ts +134 -0
- package/src/lib/__tests__/skills-catalog.test.ts +96 -0
- package/src/lib/__tests__/tone.test.ts +37 -0
- package/src/lib/__tests__/turns-delta.test.ts +55 -0
- package/src/lib/api.ts +306 -0
- package/src/lib/attachments.ts +203 -0
- package/src/lib/auth.ts +92 -0
- package/src/lib/avatar-presets.ts +31 -0
- package/src/lib/collapsible.ts +75 -0
- package/src/lib/composer.ts +89 -0
- package/src/lib/data-events.ts +17 -0
- package/src/lib/headlines.ts +30 -0
- package/src/lib/highlight.ts +186 -0
- package/src/lib/layout.ts +20 -0
- package/src/lib/lock-screen.ts +54 -0
- package/src/lib/model-picker.ts +133 -0
- package/src/lib/onboarding.ts +38 -0
- package/src/lib/passkeys.ts +113 -0
- package/src/lib/pending-prompt.ts +53 -0
- package/src/lib/permission-info.ts +101 -0
- package/src/lib/project-extensions.ts +74 -0
- package/src/lib/project-overview.ts +117 -0
- package/src/lib/prompts.ts +87 -0
- package/src/lib/references.ts +106 -0
- package/src/lib/routes.ts +115 -0
- package/src/lib/session-handoff.ts +25 -0
- package/src/lib/session-status.ts +45 -0
- package/src/lib/session-summary.ts +68 -0
- package/src/lib/session-title.ts +63 -0
- package/src/lib/sidebar-groups.ts +78 -0
- package/src/lib/skill-installs.ts +360 -0
- package/src/lib/skills-catalog.ts +99 -0
- package/src/lib/time.ts +22 -0
- package/src/lib/tone.ts +80 -0
- package/src/lib/turns.ts +320 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +31 -0
- package/src/pages/projects/ProjectDetailPage.tsx +130 -0
- package/src/pages/projects/ProjectsPage.tsx +97 -0
- package/src/pages/sessions/SessionPage.tsx +122 -0
- package/src/pages/settings/SettingsPage.tsx +299 -0
- package/src/pages/settings/__tests__/SettingsPage.test.ts +9 -0
- package/src/pages/skills/SkillsPage.tsx +701 -0
- package/src/paths.ts +121 -0
- package/src/runtime.ts +24 -0
- package/src/server/__tests__/app.test.ts +1031 -0
- package/src/server/__tests__/auth.test.ts +370 -0
- package/src/server/__tests__/config.test.ts +101 -0
- package/src/server/__tests__/db.test.ts +485 -0
- package/src/server/__tests__/host.test.ts +14 -0
- package/src/server/__tests__/tunnel-config.test.ts +38 -0
- package/src/server/app.ts +415 -0
- package/src/server/db/auth.ts +311 -0
- package/src/server/db/messages.ts +50 -0
- package/src/server/db/projects.ts +106 -0
- package/src/server/db/prompts.ts +78 -0
- package/src/server/db/rows.ts +150 -0
- package/src/server/db/schema.ts +221 -0
- package/src/server/db/sessions.ts +239 -0
- package/src/server/db/tunnel.ts +124 -0
- package/src/server/host.ts +12 -0
- package/src/server/result.ts +15 -0
- package/src/server/routes/auth-guard.ts +156 -0
- package/src/server/routes/auth.routes.ts +287 -0
- package/src/server/routes/helpers.ts +165 -0
- package/src/server/routes/meta.routes.ts +24 -0
- package/src/server/routes/passkeys.routes.ts +161 -0
- package/src/server/routes/projects.routes.ts +157 -0
- package/src/server/routes/sessions.routes.ts +197 -0
- package/src/server/routes/skills.routes.ts +117 -0
- package/src/server/routes/tunnel.routes.ts +132 -0
- package/src/server/routes/types.ts +35 -0
- package/src/server/routes/uploads.routes.ts +54 -0
- package/src/server/routes/workspace.routes.ts +127 -0
- package/src/server/services/__tests__/app-info.test.ts +18 -0
- package/src/server/services/__tests__/attachments.test.ts +159 -0
- package/src/server/services/__tests__/auth-passkeys.test.ts +358 -0
- package/src/server/services/__tests__/auth.test.ts +341 -0
- package/src/server/services/__tests__/frpc-installer.test.ts +83 -0
- package/src/server/services/__tests__/git-info.test.ts +74 -0
- package/src/server/services/__tests__/opencode.test.ts +320 -0
- package/src/server/services/__tests__/project-manager.test.ts +190 -0
- package/src/server/services/__tests__/sandbox.test.ts +141 -0
- package/src/server/services/__tests__/session-manager.test.ts +3469 -0
- package/src/server/services/__tests__/skill-install-jobs.test.ts +226 -0
- package/src/server/services/__tests__/skills-registry.test.ts +359 -0
- package/src/server/services/__tests__/tunnel-lhr.test.ts +234 -0
- package/src/server/services/__tests__/tunnel.test.ts +367 -0
- package/src/server/services/__tests__/websocket-gateway.test.ts +677 -0
- package/src/server/services/__tests__/workspace.test.ts +249 -0
- package/src/server/services/app-info.ts +29 -0
- package/src/server/services/attachments.ts +193 -0
- package/src/server/services/auth-passkeys.ts +459 -0
- package/src/server/services/auth.ts +633 -0
- package/src/server/services/frpc-installer.ts +183 -0
- package/src/server/services/git-info.ts +87 -0
- package/src/server/services/opencode-client.ts +714 -0
- package/src/server/services/opencode-server.ts +319 -0
- package/src/server/services/opencode-sse.ts +82 -0
- package/src/server/services/project-manager.ts +147 -0
- package/src/server/services/sandbox.ts +83 -0
- package/src/server/services/session-events.ts +256 -0
- package/src/server/services/session-manager.ts +1374 -0
- package/src/server/services/skill-install-jobs.ts +262 -0
- package/src/server/services/skills-registry.ts +468 -0
- package/src/server/services/tunnel-api.ts +207 -0
- package/src/server/services/tunnel-config.ts +45 -0
- package/src/server/services/tunnel-lhr.ts +387 -0
- package/src/server/services/tunnel.ts +633 -0
- package/src/server/services/turn-stream.ts +339 -0
- package/src/server/services/websocket-gateway.ts +374 -0
- package/src/server/services/workspace.ts +223 -0
- package/src/server/services/ws-transport.ts +121 -0
- package/src/types/context.ts +69 -0
- package/src/types/index.ts +13 -0
- package/src/types/message.ts +58 -0
- package/src/types/project.ts +16 -0
- package/src/types/prompt.ts +118 -0
- package/src/types/session.ts +52 -0
- package/src/types/turns.ts +47 -0
- package/src/ws-protocol.ts +93 -0
- package/styles/globals.css +311 -0
- 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
|
+
}
|