@coze-arch/cli 0.0.18 → 0.0.19-alpha.502ddf

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 (97) hide show
  1. package/lib/__templates__/expo/.coze +1 -0
  2. package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
  3. package/lib/__templates__/expo/package.json +2 -1
  4. package/lib/__templates__/nextjs/.coze +1 -0
  5. package/lib/__templates__/nextjs/package.json +3 -1
  6. package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
  7. package/lib/__templates__/nuxt-vue/.coze +1 -0
  8. package/lib/__templates__/nuxt-vue/app/pages/index.vue +6 -0
  9. package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
  10. package/lib/__templates__/nuxt-vue/nuxt.config.ts +2 -2
  11. package/lib/__templates__/nuxt-vue/package.json +9 -2
  12. package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
  13. package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
  14. package/lib/__templates__/pi-agent/.coze +10 -0
  15. package/lib/__templates__/pi-agent/AGENTS.md +149 -0
  16. package/lib/__templates__/pi-agent/README.md +218 -0
  17. package/lib/__templates__/pi-agent/_gitignore +3 -0
  18. package/lib/__templates__/pi-agent/_npmrc +23 -0
  19. package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
  20. package/lib/__templates__/pi-agent/docs/project-overview.md +368 -0
  21. package/lib/__templates__/pi-agent/docs/user/getting-started.md +46 -0
  22. package/lib/__templates__/pi-agent/package.json +63 -0
  23. package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
  24. package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
  25. package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +30 -0
  26. package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +29 -0
  27. package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +57 -0
  28. package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +40 -0
  29. package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
  30. package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
  31. package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
  32. package/lib/__templates__/pi-agent/src/agent.ts +363 -0
  33. package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
  34. package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
  35. package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
  36. package/lib/__templates__/pi-agent/src/cli.ts +117 -0
  37. package/lib/__templates__/pi-agent/src/config.ts +749 -0
  38. package/lib/__templates__/pi-agent/src/core.ts +219 -0
  39. package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
  40. package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
  41. package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
  42. package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
  43. package/lib/__templates__/pi-agent/src/dashboard/index.ts +74 -0
  44. package/lib/__templates__/pi-agent/src/dashboard/server.ts +610 -0
  45. package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
  46. package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
  47. package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
  48. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +172 -0
  49. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
  50. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
  51. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
  52. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
  53. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
  54. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
  55. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
  56. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
  57. package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
  58. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
  59. package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
  60. package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +24 -0
  61. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +440 -0
  62. package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +330 -0
  63. package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
  64. package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +203 -0
  65. package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
  66. package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
  67. package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
  68. package/lib/__templates__/pi-agent/src/index.ts +123 -0
  69. package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
  70. package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
  71. package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
  72. package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
  73. package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
  74. package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
  75. package/lib/__templates__/pi-agent/template.config.js +45 -0
  76. package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
  77. package/lib/__templates__/pi-agent/tests/config.test.ts +377 -0
  78. package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
  79. package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
  80. package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
  81. package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
  82. package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
  83. package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
  84. package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
  85. package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
  86. package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
  87. package/lib/__templates__/pi-agent/tsconfig.json +21 -0
  88. package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
  89. package/lib/__templates__/taro/.coze +1 -0
  90. package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
  91. package/lib/__templates__/taro/package.json +1 -1
  92. package/lib/__templates__/templates.json +24 -0
  93. package/lib/__templates__/vite/.coze +1 -0
  94. package/lib/__templates__/vite/package.json +3 -1
  95. package/lib/__templates__/vite/scripts/validate.sh +10 -0
  96. package/lib/cli.js +13 -2
  97. package/package.json +1 -1
@@ -0,0 +1,330 @@
1
+ import React from "react";
2
+ import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
3
+ import { Button } from "../components/ui/button";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
5
+ import { Input } from "../components/ui/input";
6
+ import { Label } from "../components/ui/label";
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
8
+ import { useFetch } from "../hooks/use-fetch";
9
+ import { toast } from "sonner";
10
+
11
+ type Overview = {
12
+ appName: string;
13
+ botStatus: string;
14
+ dashboardUrl: string;
15
+ agentMode: string;
16
+ workspaceDir: string;
17
+ agentDir: string;
18
+ enabledChannels: Array<{ id: string; enabled: boolean }>;
19
+ };
20
+
21
+ type Channels = {
22
+ routing: {
23
+ feishuGroupRequireMention: boolean;
24
+ wechatGroupRequireMention: boolean;
25
+ };
26
+ feishu: {
27
+ enabled: boolean;
28
+ requireMention?: boolean;
29
+ appId?: string;
30
+ domain?: string;
31
+ encryptKey?: string;
32
+ verificationToken?: string;
33
+ appSecret?: string;
34
+ thinkingReaction?: {
35
+ enabled?: boolean;
36
+ emojiType?: string;
37
+ };
38
+ };
39
+ wechat: {
40
+ enabled: boolean;
41
+ requireMention?: boolean;
42
+ implementation?: string;
43
+ };
44
+ };
45
+
46
+ type ModelOption = {
47
+ value: string;
48
+ label: string;
49
+ };
50
+
51
+ type ModelsConfig = {
52
+ defaultModel: string;
53
+ options: ModelOption[];
54
+ };
55
+
56
+ export function OverviewPage() {
57
+ const { data, error, loading } = useFetch<Overview>("/api/overview");
58
+ const enabledChannels = data?.enabledChannels ?? [];
59
+ const activeChannelCount = enabledChannels.filter((c) => c.enabled).length;
60
+ const botStatusText = data?.botStatus === "running" ? "运行中" : data?.botStatus === "stopped" ? "已停止" : "未知";
61
+
62
+ return (
63
+ <section className="space-y-4 p-4 sm:p-5 md:p-6">
64
+ <div className="grid gap-4 xl:grid-cols-[minmax(0,1.6fr)_minmax(320px,1fr)]">
65
+ <Card>
66
+ <CardHeader className="pb-3">
67
+ <CardTitle className="text-base">系统摘要</CardTitle>
68
+ <CardDescription>当前 bot、渠道与工作区的核心状态。</CardDescription>
69
+ </CardHeader>
70
+ <CardContent className="grid gap-3 sm:grid-cols-2">
71
+ <SummaryStat
72
+ label="运行状态"
73
+ value={botStatusText}
74
+ hint={data?.botStatus === "running" ? "服务运行中" : "等待服务状态"}
75
+ />
76
+ <SummaryStat
77
+ label="启用渠道"
78
+ value={String(activeChannelCount)}
79
+ hint={enabledChannels.length ? `${enabledChannels.length} 个渠道已接入` : "暂无渠道信息"}
80
+ />
81
+ </CardContent>
82
+ </Card>
83
+
84
+ <DefaultModelSection />
85
+ </div>
86
+
87
+ <ChannelsSection />
88
+ </section>
89
+ );
90
+ }
91
+
92
+ function DefaultModelSection() {
93
+ const { data, error, loading } = useFetch<ModelsConfig>("/api/models");
94
+ const [saving, setSaving] = React.useState(false);
95
+ const [form, setForm] = React.useState<ModelsConfig | null>(null);
96
+
97
+ React.useEffect(() => {
98
+ if (!data) return;
99
+ setForm({
100
+ defaultModel: typeof data.defaultModel === "string" ? data.defaultModel : "",
101
+ options: Array.isArray((data as unknown as { options?: unknown }).options)
102
+ ? (data as unknown as { options: ModelOption[] }).options
103
+ : [],
104
+ });
105
+ }, [data]);
106
+
107
+ const saveDefaultModel = async (defaultModel: string) => {
108
+ setSaving(true);
109
+ try {
110
+ const res = await fetch("/api/models", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ models: { defaultModel } }),
114
+ });
115
+ const json = (await res.json()) as { ok?: boolean; error?: string };
116
+ if (!res.ok || json.ok === false) {
117
+ throw new Error(json.error || `${res.status} ${res.statusText}`);
118
+ }
119
+ toast.success("已保存(需要重启进程生效)");
120
+ } catch (e) {
121
+ toast.error(`保存失败:${String(e)}`, { duration: 4500 });
122
+ throw e;
123
+ } finally {
124
+ setSaving(false);
125
+ }
126
+ };
127
+
128
+ const options = form?.options ?? [];
129
+ const onDefaultModelChange = async (nextValue: string) => {
130
+ if (!form || nextValue === form.defaultModel) return;
131
+ const previousValue = form.defaultModel;
132
+ setForm({ ...form, defaultModel: nextValue });
133
+ try {
134
+ await saveDefaultModel(nextValue);
135
+ } catch {
136
+ setForm((current) => (current ? { ...current, defaultModel: previousValue } : current));
137
+ }
138
+ };
139
+
140
+ return (
141
+ <Card className="border-border/60 bg-background/70 shadow-none">
142
+ <CardHeader className="pb-3">
143
+ <CardTitle className="text-base">默认模型</CardTitle>
144
+ <CardDescription>切换当前使用的默认模型。</CardDescription>
145
+ </CardHeader>
146
+ <CardContent>
147
+ {loading ? (
148
+ <Alert>
149
+ <AlertTitle>加载中</AlertTitle>
150
+ <AlertDescription>正在读取模型配置。</AlertDescription>
151
+ </Alert>
152
+ ) : error ? (
153
+ <Alert className="border-destructive/20 bg-destructive/5 text-destructive">
154
+ <AlertTitle>读取失败</AlertTitle>
155
+ <AlertDescription>错误:{error}</AlertDescription>
156
+ </Alert>
157
+ ) : form && options.length > 0 ? (
158
+ <Select
159
+ value={form.defaultModel}
160
+ onValueChange={onDefaultModelChange}
161
+ disabled={saving}
162
+ >
163
+ <SelectTrigger aria-label="选择默认模型">
164
+ <SelectValue placeholder="选择默认模型" />
165
+ </SelectTrigger>
166
+ <SelectContent>
167
+ {options.map((option) => (
168
+ <SelectItem key={option.value} value={option.value}>
169
+ {option.label}
170
+ </SelectItem>
171
+ ))}
172
+ </SelectContent>
173
+ </Select>
174
+ ) : form ? (
175
+ <Alert>
176
+ <AlertTitle>没有可选模型</AlertTitle>
177
+ <AlertDescription>当前还没有可用模型,请先补充模型定义。</AlertDescription>
178
+ </Alert>
179
+ ) : null}
180
+ </CardContent>
181
+ </Card>
182
+ );
183
+ }
184
+
185
+ function ChannelsSection() {
186
+ const { data, error, loading } = useFetch<Channels>("/api/channels");
187
+ const [saving, setSaving] = React.useState(false);
188
+ const [form, setForm] = React.useState<Channels | null>(null);
189
+
190
+ React.useEffect(() => {
191
+ if (data) setForm(data);
192
+ }, [data]);
193
+
194
+ const patch = (fn: (draft: Channels) => void) => {
195
+ if (!form) return;
196
+ const draft = { ...form, feishu: { ...form.feishu }, wechat: { ...form.wechat } };
197
+ fn(draft);
198
+ setForm(draft);
199
+ };
200
+
201
+ const onSave = async () => {
202
+ if (!form) return;
203
+ setSaving(true);
204
+ try {
205
+ const res = await fetch("/api/channels", {
206
+ method: "POST",
207
+ headers: { "Content-Type": "application/json" },
208
+ body: JSON.stringify({ channels: form }),
209
+ });
210
+ const json = (await res.json()) as { ok?: boolean; error?: string };
211
+ if (!res.ok || json.ok === false) {
212
+ throw new Error(json.error || `${res.status} ${res.statusText}`);
213
+ }
214
+ toast.success("已保存(需要重启进程生效)");
215
+ } catch (e) {
216
+ toast.error(`保存失败:${String(e)}`, { duration: 4500 });
217
+ } finally {
218
+ setSaving(false);
219
+ }
220
+ };
221
+
222
+ return (
223
+ <Card className="border-border/60 bg-background/70 shadow-none">
224
+ <CardHeader>
225
+ <CardTitle className="text-base">飞书配置</CardTitle>
226
+ <CardDescription>管理飞书渠道的启用状态与接入凭证。</CardDescription>
227
+ </CardHeader>
228
+ <CardContent>
229
+ {loading ? (
230
+ <Alert>
231
+ <AlertTitle>加载中</AlertTitle>
232
+ <AlertDescription>正在读取渠道配置。</AlertDescription>
233
+ </Alert>
234
+ ) : error ? (
235
+ <Alert className="border-destructive/20 bg-destructive/5 text-destructive">
236
+ <AlertTitle>读取失败</AlertTitle>
237
+ <AlertDescription>错误:{error}</AlertDescription>
238
+ </Alert>
239
+ ) : form ? (
240
+ <div className="space-y-4">
241
+ <div className="grid gap-4">
242
+ <CheckboxField
243
+ label="启用状态"
244
+ checked={!!form.feishu.enabled}
245
+ onChange={(checked) => patch((d) => (d.feishu.enabled = checked))}
246
+ />
247
+ <TextField
248
+ label="App ID"
249
+ value={form.feishu.appId ?? ""}
250
+ onChange={(value) => patch((d) => (d.feishu.appId = value))}
251
+ />
252
+ <TextField
253
+ label="App Secret"
254
+ value={form.feishu.appSecret ?? ""}
255
+ onChange={(value) => patch((d) => (d.feishu.appSecret = value))}
256
+ />
257
+ </div>
258
+ <div className="flex justify-start">
259
+ <Button
260
+ onClick={onSave}
261
+ disabled={!form || saving || loading || Boolean(error)}
262
+ className="shrink-0"
263
+ >
264
+ {saving ? "保存中…" : "保存"}
265
+ </Button>
266
+ </div>
267
+ </div>
268
+ ) : null}
269
+ </CardContent>
270
+ </Card>
271
+ );
272
+ }
273
+
274
+ function SummaryStat(props: { label: string; value: string; hint: string }) {
275
+ return (
276
+ <div className="rounded-2xl border border-border/70 bg-muted/30 p-4">
277
+ <div className="text-xs uppercase tracking-[0.18em] text-muted-foreground">{props.label}</div>
278
+ <div className="mt-3 text-2xl font-semibold tracking-tight">{props.value}</div>
279
+ <div className="mt-2 text-sm text-muted-foreground">{props.hint}</div>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ function FieldShell(props: { label: string; children: React.ReactNode; description?: string }) {
285
+ return (
286
+ <div className="space-y-2">
287
+ <Label className="text-xs uppercase tracking-[0.16em] text-muted-foreground">{props.label}</Label>
288
+ {props.children}
289
+ {props.description ? <p className="text-xs text-muted-foreground">{props.description}</p> : null}
290
+ </div>
291
+ );
292
+ }
293
+
294
+ function TextField(props: {
295
+ label: string;
296
+ value: string;
297
+ placeholder?: string;
298
+ onChange: (value: string) => void;
299
+ }) {
300
+ return (
301
+ <FieldShell label={props.label}>
302
+ <Input
303
+ className="font-mono text-xs sm:text-sm"
304
+ value={props.value}
305
+ placeholder={props.placeholder}
306
+ onChange={(e) => props.onChange(e.target.value)}
307
+ />
308
+ </FieldShell>
309
+ );
310
+ }
311
+
312
+ function CheckboxField(props: {
313
+ label: string;
314
+ checked: boolean;
315
+ onChange: (checked: boolean) => void;
316
+ }) {
317
+ return (
318
+ <FieldShell label={props.label}>
319
+ <label className="flex h-10 items-center gap-3 rounded-md border border-input bg-background/80 px-3 text-sm">
320
+ <input
321
+ type="checkbox"
322
+ className="h-4 w-4 rounded border-border accent-primary"
323
+ checked={props.checked}
324
+ onChange={(e) => props.onChange(e.target.checked)}
325
+ />
326
+ <span className="text-sm text-foreground">{props.checked ? "已启用" : "未启用"}</span>
327
+ </label>
328
+ </FieldShell>
329
+ );
330
+ }
@@ -0,0 +1,167 @@
1
+ export type ChatSessionIdentity = {
2
+ channel: "dashboard" | "feishu" | "wechat";
3
+ isDirectMessage: boolean;
4
+ senderId: string;
5
+ conversationId: string;
6
+ threadId?: string;
7
+ };
8
+
9
+ export type ChatWsClientFrame =
10
+ | { type: "chat.send"; runId: string; session?: ChatSessionIdentity; sessionKey?: string; text: string }
11
+ | { type: "chat.abort"; runId?: string; session?: ChatSessionIdentity; sessionKey?: string }
12
+ | { type: "ping" };
13
+
14
+ export type ChatWsServerFrame =
15
+ | { type: "ack"; runId: string; status: "started" | "in_flight" }
16
+ | { type: "meta"; runId: string; sessionKey: string }
17
+ | { type: "delta"; runId: string; sessionKey: string; delta: string }
18
+ | { type: "done"; runId: string; sessionKey: string; text: string }
19
+ | { type: "error"; runId: string; sessionKey?: string; error: string };
20
+
21
+ export type ChatWsLogFn = (
22
+ level: "debug" | "info" | "warn" | "error",
23
+ msg: string,
24
+ data?: Record<string, unknown>,
25
+ ) => void;
26
+
27
+ export class ChatWsService {
28
+ private ws: WebSocket | null = null;
29
+ private ready: Promise<WebSocket> | null = null;
30
+ private manualClose = false;
31
+ private reconnectTimer: number | null = null;
32
+ private reconnectDelayMs = 300;
33
+
34
+ constructor(
35
+ private readonly opts: {
36
+ resolveUrl: () => string;
37
+ onFrame: (frame: ChatWsServerFrame) => void;
38
+ log?: ChatWsLogFn;
39
+ },
40
+ ) {}
41
+
42
+ private log(level: "debug" | "info" | "warn" | "error", msg: string, data?: Record<string, unknown>) {
43
+ try {
44
+ this.opts.log?.(level, msg, data);
45
+ } catch {
46
+ // ignore
47
+ }
48
+ }
49
+
50
+ async connect(): Promise<WebSocket> {
51
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
52
+ return this.ws;
53
+ }
54
+ if (this.ready) {
55
+ return this.ready;
56
+ }
57
+
58
+ const wsUrl = this.opts.resolveUrl();
59
+ this.log("debug", "ws.connect.begin", { wsUrl });
60
+
61
+ this.ready = new Promise<WebSocket>((resolve, reject) => {
62
+ try {
63
+ const ws = new WebSocket(wsUrl);
64
+ this.ws = ws;
65
+ this.manualClose = false;
66
+
67
+ ws.onopen = () => {
68
+ this.log("info", "ws.open", { wsUrl });
69
+ this.reconnectDelayMs = 300;
70
+ resolve(ws);
71
+ };
72
+
73
+ ws.onerror = () => {
74
+ this.log("warn", "ws.onerror", { wsUrl });
75
+ };
76
+
77
+ ws.onclose = (evt) => {
78
+ this.log("warn", "ws.onclose", { wsUrl, code: evt.code, reason: evt.reason, manual: this.manualClose });
79
+ this.ws = null;
80
+ this.ready = null;
81
+ if (!this.manualClose && this.reconnectTimer == null) {
82
+ const delay = Math.min(2000, this.reconnectDelayMs);
83
+ this.reconnectDelayMs = Math.min(2000, this.reconnectDelayMs * 2);
84
+ this.reconnectTimer = window.setTimeout(() => {
85
+ this.reconnectTimer = null;
86
+ void this.connect();
87
+ }, delay);
88
+ }
89
+ };
90
+
91
+ ws.onmessage = (evt) => {
92
+ let msg: unknown = null;
93
+ try {
94
+ msg = JSON.parse(String(evt.data)) as unknown;
95
+ } catch {
96
+ this.log("warn", "ws.bad-json", { dataType: typeof evt.data });
97
+ return;
98
+ }
99
+ if (!msg || typeof msg !== "object") return;
100
+ const type = (msg as { type?: unknown }).type;
101
+ if (typeof type !== "string") return;
102
+ this.opts.onFrame(msg as ChatWsServerFrame);
103
+ };
104
+ } catch (e) {
105
+ reject(e);
106
+ }
107
+ });
108
+
109
+ try {
110
+ return await this.ready;
111
+ } catch (e) {
112
+ this.ready = null;
113
+ throw e;
114
+ }
115
+ }
116
+
117
+ async sendChat(params: { runId: string; session: ChatSessionIdentity; text: string }): Promise<void> {
118
+ const ws = await this.connect();
119
+ const frame: ChatWsClientFrame = {
120
+ type: "chat.send",
121
+ runId: params.runId,
122
+ session: params.session,
123
+ text: params.text,
124
+ };
125
+ ws.send(JSON.stringify(frame));
126
+ }
127
+
128
+ async sendChatByKey(params: { runId: string; sessionKey: string; text: string }): Promise<void> {
129
+ const ws = await this.connect();
130
+ const frame: ChatWsClientFrame = {
131
+ type: "chat.send",
132
+ runId: params.runId,
133
+ sessionKey: params.sessionKey,
134
+ text: params.text,
135
+ };
136
+ ws.send(JSON.stringify(frame));
137
+ }
138
+
139
+ abort(params: { runId?: string; session?: ChatSessionIdentity; sessionKey?: string }): void {
140
+ const ws = this.ws;
141
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
142
+ return;
143
+ }
144
+ const frame: ChatWsClientFrame = {
145
+ type: "chat.abort",
146
+ ...(params.runId ? { runId: params.runId } : {}),
147
+ ...(params.session ? { session: params.session } : {}),
148
+ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
149
+ };
150
+ ws.send(JSON.stringify(frame));
151
+ }
152
+
153
+ close(): void {
154
+ if (this.reconnectTimer != null) {
155
+ window.clearTimeout(this.reconnectTimer);
156
+ this.reconnectTimer = null;
157
+ }
158
+ this.manualClose = true;
159
+ try {
160
+ this.ws?.close();
161
+ } catch {
162
+ // ignore
163
+ }
164
+ this.ws = null;
165
+ this.ready = null;
166
+ }
167
+ }
@@ -0,0 +1,203 @@
1
+ @import "tailwindcss";
2
+ @source "../../../../node_modules/streamdown/dist/*.js";
3
+ @source "../../../../node_modules/@streamdown/code/dist/*.js";
4
+
5
+ :root {
6
+ color-scheme: light;
7
+ --background: oklch(1 0 0);
8
+ --foreground: oklch(0.145 0 0);
9
+ --card: oklch(1 0 0);
10
+ --card-foreground: oklch(0.145 0 0);
11
+ --popover: oklch(1 0 0);
12
+ --popover-foreground: oklch(0.145 0 0);
13
+ --primary: oklch(0.205 0 0);
14
+ --primary-foreground: oklch(0.985 0 0);
15
+ --secondary: oklch(0.97 0 0);
16
+ --secondary-foreground: oklch(0.205 0 0);
17
+ --muted: oklch(0.97 0 0);
18
+ --muted-foreground: oklch(0.556 0 0);
19
+ --accent: oklch(0.97 0 0);
20
+ --accent-foreground: oklch(0.205 0 0);
21
+ --destructive: oklch(0.577 0.245 27.325);
22
+ --destructive-foreground: oklch(0.985 0 0);
23
+ --border: oklch(0.922 0 0);
24
+ --input: oklch(0.922 0 0);
25
+ --ring: oklch(0.708 0 0);
26
+ --radius: 0.625rem;
27
+ }
28
+
29
+ .dark {
30
+ color-scheme: dark;
31
+ --background: oklch(0.145 0 0);
32
+ --foreground: oklch(0.985 0 0);
33
+ --card: oklch(0.205 0 0);
34
+ --card-foreground: oklch(0.985 0 0);
35
+ --popover: oklch(0.205 0 0);
36
+ --popover-foreground: oklch(0.985 0 0);
37
+ --primary: oklch(0.922 0 0);
38
+ --primary-foreground: oklch(0.205 0 0);
39
+ --secondary: oklch(0.269 0 0);
40
+ --secondary-foreground: oklch(0.985 0 0);
41
+ --muted: oklch(0.269 0 0);
42
+ --muted-foreground: oklch(0.708 0 0);
43
+ --accent: oklch(0.269 0 0);
44
+ --accent-foreground: oklch(0.985 0 0);
45
+ --destructive: oklch(0.704 0.191 22.216);
46
+ --destructive-foreground: oklch(0.985 0 0);
47
+ --border: oklch(1 0 0 / 10%);
48
+ --input: oklch(1 0 0 / 15%);
49
+ --ring: oklch(0.556 0 0);
50
+ }
51
+
52
+ @theme inline {
53
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Apple Color Emoji", "Segoe UI Emoji";
54
+ --color-background: var(--background);
55
+ --color-foreground: var(--foreground);
56
+ --color-card: var(--card);
57
+ --color-card-foreground: var(--card-foreground);
58
+ --color-popover: var(--popover);
59
+ --color-popover-foreground: var(--popover-foreground);
60
+ --color-primary: var(--primary);
61
+ --color-primary-foreground: var(--primary-foreground);
62
+ --color-secondary: var(--secondary);
63
+ --color-secondary-foreground: var(--secondary-foreground);
64
+ --color-muted: var(--muted);
65
+ --color-muted-foreground: var(--muted-foreground);
66
+ --color-accent: var(--accent);
67
+ --color-accent-foreground: var(--accent-foreground);
68
+ --color-destructive: var(--destructive);
69
+ --color-destructive-foreground: var(--destructive-foreground);
70
+ --color-border: var(--border);
71
+ --color-input: var(--input);
72
+ --color-ring: var(--ring);
73
+ --radius-sm: calc(var(--radius) - 6px);
74
+ --radius-md: calc(var(--radius) - 2px);
75
+ --radius-lg: var(--radius);
76
+ --radius-xl: calc(var(--radius) + 8px);
77
+ }
78
+
79
+ @layer base {
80
+ * {
81
+ box-sizing: border-box;
82
+ }
83
+
84
+ body {
85
+ @apply bg-background font-sans text-foreground antialiased transition-colors;
86
+ height: 100vh;
87
+ overflow: hidden;
88
+ }
89
+
90
+ #root {
91
+ height: 100%;
92
+ }
93
+
94
+ a {
95
+ @apply text-inherit;
96
+ }
97
+
98
+ h1,
99
+ h2,
100
+ h3,
101
+ p {
102
+ @apply m-0;
103
+ }
104
+ }
105
+
106
+ @supports (height: 100dvh) {
107
+ @layer base {
108
+ body {
109
+ height: 100dvh;
110
+ }
111
+ }
112
+ }
113
+
114
+ .streamdown-content [data-streamdown] {
115
+ overflow-wrap: anywhere;
116
+ }
117
+
118
+ .streamdown-content [data-streamdown="code-block"] {
119
+ position: relative;
120
+ margin: 1rem 0;
121
+ gap: 0.5rem;
122
+ border: 1px solid color-mix(in oklab, var(--border) 100%, transparent);
123
+ border-radius: 0.875rem;
124
+ background: color-mix(in oklab, var(--muted) 65%, var(--background));
125
+ padding: 0.75rem;
126
+ }
127
+
128
+ .streamdown-content [data-streamdown="code-block-header"] {
129
+ display: flex;
130
+ min-height: 1.75rem;
131
+ align-items: center;
132
+ height: auto;
133
+ padding: 0 4.5rem 0 0.125rem;
134
+ color: var(--muted-foreground);
135
+ font-size: 0.75rem;
136
+ }
137
+
138
+ .streamdown-content [data-streamdown="code-block-header"] + div {
139
+ position: absolute;
140
+ top: 0.75rem;
141
+ right: 0.75rem;
142
+ left: auto;
143
+ z-index: 20;
144
+ margin: 0;
145
+ padding: 0;
146
+ }
147
+
148
+ .streamdown-content [data-streamdown="code-block-actions"] {
149
+ gap: 0.375rem;
150
+ border: 1px solid color-mix(in oklab, var(--border) 100%, transparent);
151
+ border-radius: 0.625rem;
152
+ background: color-mix(in oklab, var(--background) 88%, transparent);
153
+ padding: 0.25rem;
154
+ box-shadow: none;
155
+ backdrop-filter: none;
156
+ }
157
+
158
+ .streamdown-content [data-streamdown="code-block-actions"] button {
159
+ display: inline-flex;
160
+ height: 1.75rem;
161
+ width: 1.75rem;
162
+ align-items: center;
163
+ justify-content: center;
164
+ border: 0;
165
+ border-radius: 0.5rem;
166
+ background: transparent;
167
+ color: var(--muted-foreground);
168
+ transition:
169
+ background-color 150ms ease,
170
+ color 150ms ease;
171
+ }
172
+
173
+ .streamdown-content [data-streamdown="code-block-actions"] button:hover {
174
+ background: color-mix(in oklab, var(--accent) 100%, transparent);
175
+ color: var(--foreground);
176
+ }
177
+
178
+ .streamdown-content [data-streamdown="code-block-actions"] button:focus-visible {
179
+ outline: 2px solid color-mix(in oklab, var(--ring) 65%, transparent);
180
+ outline-offset: 1px;
181
+ }
182
+
183
+ .streamdown-content [data-streamdown="code-block-body"] {
184
+ border: 1px solid color-mix(in oklab, var(--border) 100%, transparent);
185
+ border-radius: 0.75rem;
186
+ background: color-mix(in oklab, var(--background) 92%, var(--muted));
187
+ margin-top: 0.5rem;
188
+ padding: 0.875rem 1rem;
189
+ }
190
+
191
+ .streamdown-content [data-streamdown="code-block-body"] pre {
192
+ margin: 0;
193
+ background: transparent;
194
+ }
195
+
196
+ .streamdown-content pre {
197
+ overflow: auto;
198
+ }
199
+
200
+ .streamdown-content img {
201
+ max-width: 100%;
202
+ height: auto;
203
+ }
@@ -0,0 +1,11 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ import { cloneDeep } from "lodash-es";
4
+
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ export function deepClone<T>(value: T): T {
10
+ return cloneDeep(value);
11
+ }