@blade-hq/agent-react 1.1.1

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/dist/index.js ADDED
@@ -0,0 +1,1527 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var BladeClientContext = createContext(null);
5
+ function BladeProvider({ client, children }) {
6
+ return /* @__PURE__ */ jsx(BladeClientContext.Provider, { value: client, children });
7
+ }
8
+ function useBladeClient() {
9
+ const client = useContext(BladeClientContext);
10
+ if (!client) {
11
+ throw new Error("useBladeClient \u5FC5\u987B\u5728 <BladeProvider> \u5185\u4F7F\u7528");
12
+ }
13
+ return client;
14
+ }
15
+
16
+ // src/hooks/use-agent-session.ts
17
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
18
+ var EMPTY_SUBSCRIBE = () => () => {
19
+ };
20
+ var DISPOSE_DELAY_MS = 1e3;
21
+ function useAgentSession(sessionId, options = {}) {
22
+ const client = useBladeClient();
23
+ const [session, setSession] = useState(null);
24
+ const [error, setError] = useState(null);
25
+ const connRef = useRef({
26
+ id: null,
27
+ session: null,
28
+ gen: 0
29
+ });
30
+ const createdIdPromiseRef = useRef(null);
31
+ const onCreatedRef = useRef(options.onSessionCreated);
32
+ onCreatedRef.current = options.onSessionCreated;
33
+ const createOptionsRef = useRef(options.createOptions);
34
+ createOptionsRef.current = options.createOptions;
35
+ const sessionIdRef = useRef(sessionId);
36
+ sessionIdRef.current = sessionId;
37
+ const connect = useMemo(() => {
38
+ return (targetId) => {
39
+ const gen = ++connRef.current.gen;
40
+ const idPromise = targetId ? Promise.resolve(targetId) : (
41
+ // biome-ignore lint/suspicious/noAssignInExpressions: ??= 挂 ref 是 StrictMode 下"只创建一次"的关键
42
+ createdIdPromiseRef.current ??= client.sessions.create(createOptionsRef.current ?? {}).then((created) => {
43
+ const id = created.sessionId;
44
+ created.dispose();
45
+ onCreatedRef.current?.(id);
46
+ return id;
47
+ })
48
+ );
49
+ idPromise.then((id) => client.hub.connect(id)).then((next) => {
50
+ if (connRef.current.gen !== gen) {
51
+ next.dispose();
52
+ return;
53
+ }
54
+ connRef.current.id = next.sessionId;
55
+ connRef.current.session = next;
56
+ setSession(next);
57
+ setError(null);
58
+ }).catch((err) => {
59
+ if (connRef.current.gen !== gen) return;
60
+ createdIdPromiseRef.current = null;
61
+ setError(err instanceof Error ? err : new Error(String(err)));
62
+ });
63
+ };
64
+ }, [client]);
65
+ useEffect(() => {
66
+ connect(sessionIdRef.current);
67
+ return () => {
68
+ connRef.current.gen++;
69
+ const toRelease = connRef.current.session;
70
+ connRef.current.id = null;
71
+ connRef.current.session = null;
72
+ setSession(null);
73
+ if (toRelease) setTimeout(() => toRelease.dispose(), DISPOSE_DELAY_MS);
74
+ };
75
+ }, [connect]);
76
+ useEffect(() => {
77
+ if (!sessionId) return;
78
+ if (connRef.current.id === null) return;
79
+ if (sessionId === connRef.current.id) return;
80
+ const previous = connRef.current.session;
81
+ connRef.current.id = null;
82
+ connRef.current.session = null;
83
+ if (previous) setTimeout(() => previous.dispose(), DISPOSE_DELAY_MS);
84
+ connect(sessionId);
85
+ }, [sessionId, connect]);
86
+ const store = useMemo(
87
+ () => ({
88
+ subscribe: session ? (listener) => session.subscribe(listener) : EMPTY_SUBSCRIBE,
89
+ getSnapshot: () => session ? session.getState() : null
90
+ }),
91
+ [session]
92
+ );
93
+ const state = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
94
+ return { session, state, error };
95
+ }
96
+
97
+ // src/components/ChatView.tsx
98
+ import { BladeApiError, VersionMismatchError } from "@blade-hq/agent-client";
99
+ import { CircleAlert, LockKeyhole } from "lucide-react";
100
+ import { useEffect as useEffect5, useState as useState7 } from "react";
101
+
102
+ // src/lib/utils.ts
103
+ import { clsx } from "clsx";
104
+ function cn(...inputs) {
105
+ return clsx(inputs);
106
+ }
107
+ async function copyToClipboard(text) {
108
+ try {
109
+ await navigator.clipboard.writeText(text);
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ // src/components/ChatInput.tsx
117
+ import { ArrowUp, Loader2, Square } from "lucide-react";
118
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
119
+ function ChatInput({
120
+ value,
121
+ onValueChange,
122
+ onSend,
123
+ onStop,
124
+ isStreaming,
125
+ isStopping = false,
126
+ placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
127
+ className
128
+ }) {
129
+ const trimmed = value.trim();
130
+ const canSend = trimmed.length > 0 && !isStreaming;
131
+ const handleSend = () => {
132
+ if (!canSend) return;
133
+ onSend(trimmed);
134
+ onValueChange("");
135
+ };
136
+ const handleKeyDown = (event) => {
137
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
138
+ event.preventDefault();
139
+ handleSend();
140
+ }
141
+ };
142
+ return /* @__PURE__ */ jsx2("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] px-4 py-3", className), children: /* @__PURE__ */ jsxs("div", { className: "blade-chat-input-inner mx-auto flex max-w-[748px] items-end gap-2 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2", children: [
143
+ /* @__PURE__ */ jsx2(
144
+ "textarea",
145
+ {
146
+ value,
147
+ onChange: (event) => onValueChange(event.target.value),
148
+ onKeyDown: handleKeyDown,
149
+ onInput: (event) => {
150
+ const el = event.currentTarget;
151
+ el.style.height = "auto";
152
+ el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
153
+ },
154
+ rows: 1,
155
+ placeholder,
156
+ "aria-label": "\u804A\u5929\u8F93\u5165",
157
+ className: "blade-chat-textarea max-h-48 min-h-[28px] flex-1 resize-none bg-transparent py-1 text-sm leading-6 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.6)]"
158
+ }
159
+ ),
160
+ isStreaming ? /* @__PURE__ */ jsx2(
161
+ "button",
162
+ {
163
+ type: "button",
164
+ onClick: onStop,
165
+ disabled: isStopping,
166
+ "aria-label": isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
167
+ title: isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
168
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] transition-opacity hover:opacity-90 disabled:opacity-60",
169
+ children: isStopping ? /* @__PURE__ */ jsx2(Loader2, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx2(Square, { size: 12, fill: "currentColor" })
170
+ }
171
+ ) : /* @__PURE__ */ jsx2(
172
+ "button",
173
+ {
174
+ type: "button",
175
+ onClick: handleSend,
176
+ disabled: !canSend,
177
+ "aria-label": "\u53D1\u9001\u6D88\u606F",
178
+ title: "\u53D1\u9001\u6D88\u606F",
179
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",
180
+ children: /* @__PURE__ */ jsx2(ArrowUp, { size: 15 })
181
+ }
182
+ )
183
+ ] }) });
184
+ }
185
+
186
+ // src/components/ConnectionBanner.tsx
187
+ import { AlertTriangle, LoaderCircle } from "lucide-react";
188
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
189
+ function ConnectionBanner({ connection, className }) {
190
+ if (connection === "connected" || connection === "connecting") {
191
+ return null;
192
+ }
193
+ const reconnecting = connection === "reconnecting";
194
+ return /* @__PURE__ */ jsx3("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs2(
195
+ "div",
196
+ {
197
+ className: cn(
198
+ "mx-auto flex max-w-3xl items-start gap-3 rounded-2xl border px-4 py-3",
199
+ reconnecting ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
200
+ ),
201
+ children: [
202
+ /* @__PURE__ */ jsx3("span", { className: "mt-0.5 shrink-0", children: reconnecting ? /* @__PURE__ */ jsx3(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx3(AlertTriangle, { size: 14 }) }),
203
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0", children: [
204
+ /* @__PURE__ */ jsx3("div", { className: "text-sm font-medium", children: reconnecting ? "\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026" : "\u8FDE\u63A5\u5DF2\u65AD\u5F00" }),
205
+ /* @__PURE__ */ jsx3("div", { className: "text-xs opacity-80", children: "\u6D88\u606F\u540C\u6B65\u53EF\u80FD\u4F1A\u5EF6\u8FDF\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
206
+ ] })
207
+ ]
208
+ }
209
+ ) });
210
+ }
211
+
212
+ // src/components/MessageList.tsx
213
+ import { isHiddenInternalMessage } from "@blade-hq/agent-client";
214
+ import { ChevronDown as ChevronDown2, Layers, Lightbulb, MessageSquare } from "lucide-react";
215
+ import { useEffect as useEffect4, useMemo as useMemo4, useRef as useRef3 } from "react";
216
+ import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
217
+
218
+ // src/components/AssistantTurnBlock.tsx
219
+ import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
220
+ import { Brain, ChevronDown } from "lucide-react";
221
+ import { useState as useState6 } from "react";
222
+
223
+ // src/components/AgentLoopBlock.tsx
224
+ import { Bot, Check, ChevronRight, Loader2 as Loader22, X } from "lucide-react";
225
+ import { useState as useState2 } from "react";
226
+
227
+ // src/components/display-utils.ts
228
+ var TOOL_NAME_ALIASES = {
229
+ agent: "Agent",
230
+ ask_user_question: "AskUserQuestion",
231
+ bash: "Bash",
232
+ bg_bash: "BgBash",
233
+ edit: "Edit",
234
+ exit_plan_mode: "ExitPlanMode",
235
+ file_edit: "Edit",
236
+ file_read: "Read",
237
+ file_write: "Write",
238
+ finish_task: "FinishTask",
239
+ glob: "Glob",
240
+ grep: "Grep",
241
+ ls: "Ls",
242
+ read: "Read",
243
+ read_skill: "ReadSkill",
244
+ web_fetch: "WebFetch",
245
+ web_search: "WebSearch",
246
+ write: "Write"
247
+ };
248
+ var TOOL_DISPLAY_LABELS = {
249
+ Bash: "\u6267\u884C\u547D\u4EE4",
250
+ BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
251
+ Read: "\u8BFB\u53D6\u6587\u4EF6",
252
+ Write: "\u5199\u5165\u6587\u4EF6",
253
+ Edit: "\u7F16\u8F91\u6587\u4EF6",
254
+ Ls: "\u5217\u51FA\u76EE\u5F55",
255
+ Glob: "\u5339\u914D\u6587\u4EF6",
256
+ Grep: "\u641C\u7D22\u6587\u672C",
257
+ WebSearch: "\u641C\u7D22\u7F51\u9875",
258
+ WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
259
+ Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
260
+ AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
261
+ ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
262
+ FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
263
+ ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
264
+ ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
265
+ GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
266
+ };
267
+ function safeParseJson(value) {
268
+ if (!value) return null;
269
+ try {
270
+ return JSON.parse(value);
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+ function getStringArgValue(args, key) {
276
+ const value = args?.[key];
277
+ return typeof value === "string" ? value.trim() : "";
278
+ }
279
+ function formatToolName(name) {
280
+ const trimmed = name.trim();
281
+ if (!trimmed) return name;
282
+ const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
283
+ const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
284
+ return TOOL_NAME_ALIASES[normalized] ?? stripped;
285
+ }
286
+ function getToolDisplayLabel(toolCall) {
287
+ const normalized = formatToolName(toolCall.name);
288
+ const args = safeParseJson(toolCall.arguments);
289
+ const displayName = toolCall.display_name?.trim() ?? "";
290
+ const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
291
+ const metaDisplayName = getStringArgValue(args, "_meta_display_name");
292
+ if (metaDisplayName) {
293
+ return metaDisplayName;
294
+ }
295
+ const description = getStringArgValue(args, "description");
296
+ if (normalized === "BgBash") {
297
+ return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
298
+ }
299
+ if (normalized === "ReadSkill") {
300
+ const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
301
+ return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
302
+ }
303
+ if (normalized === "FinishTask") {
304
+ const title = getStringArgValue(args, "title");
305
+ return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
306
+ }
307
+ return description || baseLabel;
308
+ }
309
+ function getToolTone(status) {
310
+ if (status === "error" || status === "cancelled") return "red";
311
+ if (status === "awaiting_answer") return "amber";
312
+ if (status === "pending") return "blue";
313
+ return "emerald";
314
+ }
315
+ function getToolStatusLabel(status) {
316
+ if (status === "pending") return "\u8FD0\u884C\u4E2D";
317
+ if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
318
+ if (status === "error") return "\u9519\u8BEF";
319
+ if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
320
+ return "\u5B8C\u6210";
321
+ }
322
+ function formatToolDuration(ms) {
323
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
324
+ const seconds = ms / 1e3;
325
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
326
+ const minutes = Math.floor(seconds / 60);
327
+ const remainingSeconds = Math.round(seconds % 60);
328
+ return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
329
+ }
330
+ function formatToolArgs(args) {
331
+ try {
332
+ return JSON.stringify(JSON.parse(args), null, 2);
333
+ } catch {
334
+ return args;
335
+ }
336
+ }
337
+ var RESULT_PREVIEW_LIMIT = 4e3;
338
+ function formatToolResult(result) {
339
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
340
+ if (text == null) return "";
341
+ if (text.length <= RESULT_PREVIEW_LIMIT) return text;
342
+ return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
343
+ \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
344
+ }
345
+
346
+ // src/components/AgentLoopBlock.tsx
347
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
348
+ function parseAgentDescription(argumentsJson) {
349
+ try {
350
+ const parsed = JSON.parse(argumentsJson);
351
+ return typeof parsed?.description === "string" && parsed.description ? parsed.description : "\u5B50\u667A\u80FD\u4F53";
352
+ } catch {
353
+ return "\u5B50\u667A\u80FD\u4F53";
354
+ }
355
+ }
356
+ function AgentLoopBlock({ toolCall }) {
357
+ const [expanded, setExpanded] = useState2(false);
358
+ const description = parseAgentDescription(toolCall.arguments);
359
+ const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
360
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
361
+ return /* @__PURE__ */ jsxs3("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
362
+ /* @__PURE__ */ jsxs3(
363
+ "div",
364
+ {
365
+ className: cn(
366
+ "border-l-[3px] flex items-center gap-2 px-3 py-2",
367
+ failed ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : running ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]"
368
+ ),
369
+ children: [
370
+ /* @__PURE__ */ jsxs3(
371
+ "button",
372
+ {
373
+ type: "button",
374
+ onClick: () => setExpanded(!expanded),
375
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:bg-white/3 focus-visible:ring-1 focus-visible:ring-[hsl(var(--ring))] focus:outline-none",
376
+ "aria-expanded": expanded,
377
+ children: [
378
+ /* @__PURE__ */ jsx4(
379
+ ChevronRight,
380
+ {
381
+ size: 11,
382
+ className: cn(
383
+ "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform",
384
+ expanded && "rotate-90"
385
+ )
386
+ }
387
+ ),
388
+ /* @__PURE__ */ jsx4(Bot, { size: 12, className: "shrink-0 text-[hsl(var(--muted-foreground))]" }),
389
+ /* @__PURE__ */ jsxs3(
390
+ "span",
391
+ {
392
+ className: cn(
393
+ "flex shrink-0 items-center gap-1 text-[10px]",
394
+ failed ? "text-[hsl(var(--muted-foreground))]" : running ? "text-blue-300" : "text-[hsl(var(--primary))]"
395
+ ),
396
+ children: [
397
+ running ? /* @__PURE__ */ jsx4(Loader22, { size: 11, className: "animate-spin" }) : failed ? /* @__PURE__ */ jsx4(X, { size: 11 }) : /* @__PURE__ */ jsx4(Check, { size: 11 }),
398
+ /* @__PURE__ */ jsx4("span", { children: running ? "\u6267\u884C\u4E2D" : failed ? "\u5DF2\u7EC8\u6B62" : "\u5B8C\u6210" })
399
+ ]
400
+ }
401
+ ),
402
+ /* @__PURE__ */ jsxs3("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: [
403
+ "\u5B50\u667A\u80FD\u4F53\uFF1A",
404
+ description
405
+ ] })
406
+ ]
407
+ }
408
+ ),
409
+ typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx4("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
410
+ ]
411
+ }
412
+ ),
413
+ expanded && toolCall.result != null && /* @__PURE__ */ jsxs3("div", { className: "ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
414
+ /* @__PURE__ */ jsx4("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
415
+ /* @__PURE__ */ jsx4("pre", { className: "max-h-[400px] overflow-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolResult(toolCall.result) })
416
+ ] })
417
+ ] });
418
+ }
419
+
420
+ // src/components/MarkdownContent.tsx
421
+ import { Check as Check2, Copy } from "lucide-react";
422
+ import {
423
+ useEffect as useEffect2,
424
+ useMemo as useMemo2,
425
+ useRef as useRef2,
426
+ useState as useState3
427
+ } from "react";
428
+ import { Streamdown } from "streamdown";
429
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
430
+ var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
431
+ function CodeBlockPre({ children, node: _node, ...props }) {
432
+ const preRef = useRef2(null);
433
+ const [copied, setCopied] = useState3(false);
434
+ const [language, setLanguage] = useState3("");
435
+ useEffect2(() => {
436
+ const codeEl = preRef.current?.querySelector("code");
437
+ setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
438
+ }, []);
439
+ const handleCopy = async () => {
440
+ const ok = await copyToClipboard(preRef.current?.querySelector("code")?.textContent ?? "");
441
+ if (ok) {
442
+ setCopied(true);
443
+ setTimeout(() => setCopied(false), 2e3);
444
+ }
445
+ };
446
+ return /* @__PURE__ */ jsxs4("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
447
+ /* @__PURE__ */ jsxs4("div", { className: "blade-chat-codeblock-header flex h-[34px] items-center justify-between border-b border-[hsl(var(--border))] bg-[hsl(var(--muted))/0.5] pl-3.5 pr-1.5", children: [
448
+ /* @__PURE__ */ jsx5("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
449
+ /* @__PURE__ */ jsxs4(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ onClick: handleCopy,
454
+ className: cn(
455
+ "flex h-6 items-center gap-1 rounded-md px-1.5 text-[11px] transition-colors",
456
+ copied ? "text-[hsl(var(--primary))]" : "text-[hsl(var(--muted-foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]"
457
+ ),
458
+ children: [
459
+ copied ? /* @__PURE__ */ jsx5(Check2, { size: 12 }) : /* @__PURE__ */ jsx5(Copy, { size: 12 }),
460
+ /* @__PURE__ */ jsx5("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
461
+ ]
462
+ }
463
+ )
464
+ ] }),
465
+ /* @__PURE__ */ jsx5(
466
+ "pre",
467
+ {
468
+ ref: preRef,
469
+ ...props,
470
+ className: cn(props.className, "blade-chat-codeblock-body m-0 overflow-x-auto"),
471
+ children
472
+ }
473
+ )
474
+ ] });
475
+ }
476
+ function ExternalAnchor({ node: _node, children, ...props }) {
477
+ return /* @__PURE__ */ jsx5("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
478
+ }
479
+ var MARKDOWN_COMPONENTS = {
480
+ pre: CodeBlockPre,
481
+ a: ExternalAnchor
482
+ };
483
+ function MarkdownContent({ children, className, mode }) {
484
+ const resolvedChildren = useMemo2(() => children.replace(SYSTEM_REMINDER_RE, ""), [children]);
485
+ return /* @__PURE__ */ jsx5(
486
+ Streamdown,
487
+ {
488
+ className: cn("blade-chat-markdown break-words", className),
489
+ mode: mode ?? "static",
490
+ components: MARKDOWN_COMPONENTS,
491
+ children: resolvedChildren
492
+ }
493
+ );
494
+ }
495
+
496
+ // src/components/Shimmer.tsx
497
+ import { jsx as jsx6 } from "react/jsx-runtime";
498
+ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
499
+ return /* @__PURE__ */ jsx6("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
500
+ }
501
+
502
+ // src/components/ToolCallBlock.tsx
503
+ import { Check as Check4, ChevronRight as ChevronRight2, Loader2 as Loader24, MessageSquareMore as MessageSquareMore2, X as X2 } from "lucide-react";
504
+ import { useState as useState5 } from "react";
505
+
506
+ // src/components/AskUserQuestionBlock.tsx
507
+ import { Check as Check3, Loader2 as Loader23, MessageSquareMore } from "lucide-react";
508
+ import { useEffect as useEffect3, useMemo as useMemo3, useState as useState4 } from "react";
509
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
510
+ function AskUserQuestionBlock({
511
+ data,
512
+ answered,
513
+ toolCallId,
514
+ sessionStatus,
515
+ answerData,
516
+ onAnswer
517
+ }) {
518
+ const [selections, setSelections] = useState4(/* @__PURE__ */ new Map());
519
+ const [customTexts, setCustomTexts] = useState4(/* @__PURE__ */ new Map());
520
+ const [usingCustom, setUsingCustom] = useState4(/* @__PURE__ */ new Set());
521
+ const [submitted, setSubmitted] = useState4(false);
522
+ useEffect3(() => {
523
+ if (sessionStatus === "failed" || sessionStatus === "interrupted") {
524
+ setSubmitted(false);
525
+ }
526
+ }, [sessionStatus]);
527
+ const displayAnswerState = useMemo3(() => {
528
+ if (!(answered && answerData)) {
529
+ return { selections, customTexts, usingCustom };
530
+ }
531
+ const nextSelections = /* @__PURE__ */ new Map();
532
+ const nextCustomTexts = /* @__PURE__ */ new Map();
533
+ const nextUsingCustom = /* @__PURE__ */ new Set();
534
+ for (const [questionKey, optionIndexes] of Object.entries(answerData.selections)) {
535
+ nextSelections.set(Number(questionKey), new Set(optionIndexes));
536
+ }
537
+ for (const [questionKey, text] of Object.entries(answerData.custom)) {
538
+ const qIdx = Number(questionKey);
539
+ nextCustomTexts.set(qIdx, text);
540
+ nextUsingCustom.add(qIdx);
541
+ }
542
+ return {
543
+ selections: nextSelections,
544
+ customTexts: nextCustomTexts,
545
+ usingCustom: nextUsingCustom
546
+ };
547
+ }, [answerData, answered, customTexts, selections, usingCustom]);
548
+ const toggleOption = (qIdx, optIdx, multi) => {
549
+ if (answered || submitted) return;
550
+ setSelections((prev) => {
551
+ const next = new Map(prev);
552
+ const current = new Set(next.get(qIdx) ?? []);
553
+ if (multi) {
554
+ if (current.has(optIdx)) current.delete(optIdx);
555
+ else current.add(optIdx);
556
+ } else {
557
+ current.clear();
558
+ current.add(optIdx);
559
+ }
560
+ next.set(qIdx, current);
561
+ return next;
562
+ });
563
+ setUsingCustom((prev) => {
564
+ const next = new Set(prev);
565
+ next.delete(qIdx);
566
+ return next;
567
+ });
568
+ };
569
+ const handleCustomFocus = (qIdx) => {
570
+ if (answered || submitted) return;
571
+ setSelections((prev) => {
572
+ const next = new Map(prev);
573
+ next.delete(qIdx);
574
+ return next;
575
+ });
576
+ setUsingCustom((prev) => new Set(prev).add(qIdx));
577
+ };
578
+ const setCustomText = (qIdx, text) => {
579
+ if (answered || submitted) return;
580
+ setCustomTexts((prev) => new Map(prev).set(qIdx, text));
581
+ };
582
+ const getAnswer = (qIdx) => {
583
+ const q = data.questions[qIdx];
584
+ if (usingCustom.has(qIdx)) {
585
+ const text = (customTexts.get(qIdx) ?? "").trim();
586
+ return text || null;
587
+ }
588
+ const sel = selections.get(qIdx);
589
+ if (!sel || sel.size === 0) return null;
590
+ return [...sel].sort().map((i) => q.options[i].label).join(", ");
591
+ };
592
+ const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
593
+ const handleSubmit = () => {
594
+ if (answered || submitted || !allAnswered || !onAnswer) return;
595
+ const nextAnswerData = {
596
+ selections: Object.fromEntries(
597
+ Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
598
+ qIdx,
599
+ Array.from(optionIndexes).sort((a, b) => a - b)
600
+ ])
601
+ ),
602
+ custom: Object.fromEntries(
603
+ Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
604
+ )
605
+ };
606
+ const parts = data.questions.map((q, i) => `- ${q.question} -> ${getAnswer(i)}`);
607
+ const text = `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
608
+ ${parts.join("\n")}`;
609
+ setSubmitted(true);
610
+ onAnswer(text, toolCallId, nextAnswerData);
611
+ };
612
+ return /* @__PURE__ */ jsxs5(
613
+ "div",
614
+ {
615
+ className: cn(
616
+ "blade-chat-ask-card ml-4 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))]",
617
+ answered ? "max-w-2xl space-y-3 p-3 text-xs text-[hsl(var(--muted-foreground))] opacity-80" : "max-w-lg space-y-5 p-4 text-sm"
618
+ ),
619
+ children: [
620
+ data.source_loop?.description && /* @__PURE__ */ jsxs5("div", { className: "rounded-lg bg-[hsl(var(--muted)/0.35)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
621
+ "\u5B50\u667A\u80FD\u4F53\u300C",
622
+ data.source_loop.description,
623
+ "\u300D\u5728\u7B49\u5F85\u4F60\u7684\u56DE\u7B54"
624
+ ] }),
625
+ data.questions.map((q, qIdx) => /* @__PURE__ */ jsx7(
626
+ QuestionCard,
627
+ {
628
+ question: q,
629
+ qIdx,
630
+ answered,
631
+ selected: displayAnswerState.selections.get(qIdx) ?? /* @__PURE__ */ new Set(),
632
+ isCustom: displayAnswerState.usingCustom.has(qIdx),
633
+ customText: displayAnswerState.customTexts.get(qIdx) ?? "",
634
+ onToggle: toggleOption,
635
+ onCustomFocus: handleCustomFocus,
636
+ onCustomChange: setCustomText
637
+ },
638
+ q.question
639
+ )),
640
+ !answered && !submitted && onAnswer && /* @__PURE__ */ jsx7(
641
+ "button",
642
+ {
643
+ type: "button",
644
+ onClick: handleSubmit,
645
+ disabled: !allAnswered,
646
+ className: "w-full rounded-lg bg-[hsl(var(--primary))] px-4 py-2 text-xs font-semibold text-[hsl(var(--primary-foreground))] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60",
647
+ children: allAnswered ? "\u786E\u8BA4" : "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u9009\u9879"
648
+ }
649
+ ),
650
+ submitted && !answered && /* @__PURE__ */ jsxs5(
651
+ "button",
652
+ {
653
+ type: "button",
654
+ disabled: true,
655
+ className: "flex w-full items-center justify-center gap-2 rounded-lg bg-[hsl(var(--primary))] px-4 py-2 text-xs font-semibold text-[hsl(var(--primary-foreground))] opacity-80",
656
+ children: [
657
+ /* @__PURE__ */ jsx7(Loader23, { size: 14, className: "animate-spin" }),
658
+ "\u786E\u8BA4\u4E2D"
659
+ ]
660
+ }
661
+ )
662
+ ]
663
+ }
664
+ );
665
+ }
666
+ function QuestionCard({
667
+ question,
668
+ qIdx,
669
+ answered,
670
+ selected,
671
+ isCustom,
672
+ customText,
673
+ onToggle,
674
+ onCustomFocus,
675
+ onCustomChange
676
+ }) {
677
+ const multi = question.multiSelect ?? false;
678
+ return /* @__PURE__ */ jsxs5("div", { children: [
679
+ /* @__PURE__ */ jsxs5("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
680
+ /* @__PURE__ */ jsx7(
681
+ MessageSquareMore,
682
+ {
683
+ size: answered ? 12 : 13,
684
+ className: "mt-0.5 shrink-0 text-[hsl(var(--primary))]"
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsx7(
688
+ "div",
689
+ {
690
+ className: cn(
691
+ "min-w-0 flex-1 font-medium text-[hsl(var(--foreground))]",
692
+ answered ? "text-xs" : "text-sm"
693
+ ),
694
+ children: /* @__PURE__ */ jsx7(MarkdownContent, { className: "blade-chat-prose", children: question.question })
695
+ }
696
+ )
697
+ ] }),
698
+ /* @__PURE__ */ jsxs5("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
699
+ question.options.map((opt, optIdx) => {
700
+ const isSel = selected.has(optIdx);
701
+ return /* @__PURE__ */ jsxs5(
702
+ "button",
703
+ {
704
+ type: "button",
705
+ disabled: answered,
706
+ onClick: () => onToggle(qIdx, optIdx, multi),
707
+ className: cn(
708
+ "flex items-start gap-2.5 rounded-lg border text-left transition-all",
709
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
710
+ isSel && !answered ? "border-[hsl(var(--primary)/0.8)] bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))]" : isSel ? "border-[hsl(var(--primary)/0.45)] bg-[hsl(var(--primary)/0.08)] text-[hsl(var(--foreground))]" : "border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.2)]",
711
+ !answered && !isSel && "hover:border-[hsl(var(--ring)/0.4)] hover:bg-[hsl(var(--accent))]",
712
+ answered && "cursor-default opacity-70"
713
+ ),
714
+ children: [
715
+ multi && /* @__PURE__ */ jsx7(
716
+ "div",
717
+ {
718
+ className: cn(
719
+ "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
720
+ isSel && !answered ? "border-[hsl(var(--primary-foreground)/0.6)] bg-[hsl(var(--primary-foreground)/0.2)]" : isSel ? "border-[hsl(var(--primary)/0.45)] bg-[hsl(var(--primary)/0.12)]" : "border-[hsl(var(--border))]"
721
+ ),
722
+ children: isSel && /* @__PURE__ */ jsx7(
723
+ Check3,
724
+ {
725
+ size: 9,
726
+ className: answered ? "text-[hsl(var(--primary))]" : "text-[hsl(var(--primary-foreground))]"
727
+ }
728
+ )
729
+ }
730
+ ),
731
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
732
+ /* @__PURE__ */ jsx7("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
733
+ opt.description && /* @__PURE__ */ jsx7(
734
+ "div",
735
+ {
736
+ className: cn(
737
+ "mt-0.5",
738
+ answered ? "text-[11px]" : "text-xs",
739
+ isSel && !answered ? "opacity-75" : "text-[hsl(var(--muted-foreground))]"
740
+ ),
741
+ children: opt.description
742
+ }
743
+ )
744
+ ] })
745
+ ]
746
+ },
747
+ opt.label
748
+ );
749
+ }),
750
+ answered && !isCustom ? null : /* @__PURE__ */ jsxs5(
751
+ "div",
752
+ {
753
+ className: cn(
754
+ "flex items-center gap-2 rounded-lg border transition-all",
755
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
756
+ isCustom ? "border-[hsl(var(--ring)/0.6)] bg-[hsl(var(--accent))]" : "border-[hsl(var(--border))] hover:border-[hsl(var(--ring)/0.3)] hover:bg-[hsl(var(--accent))]",
757
+ answered && "cursor-default opacity-70"
758
+ ),
759
+ children: [
760
+ /* @__PURE__ */ jsx7("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
761
+ /* @__PURE__ */ jsx7(
762
+ "input",
763
+ {
764
+ type: "text",
765
+ value: customText,
766
+ disabled: answered,
767
+ onChange: (e) => onCustomChange(qIdx, e.target.value),
768
+ onFocus: () => onCustomFocus(qIdx),
769
+ "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
770
+ placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
771
+ className: cn(
772
+ "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
773
+ answered ? "text-xs" : "text-sm"
774
+ )
775
+ }
776
+ )
777
+ ]
778
+ }
779
+ )
780
+ ] })
781
+ ] });
782
+ }
783
+ function parseAskUserQuestion(toolResult) {
784
+ if (!toolResult) return null;
785
+ try {
786
+ const parsed = JSON.parse(toolResult);
787
+ let questions = parsed?.questions;
788
+ if (typeof questions === "string") {
789
+ try {
790
+ questions = JSON.parse(questions);
791
+ } catch {
792
+ return null;
793
+ }
794
+ }
795
+ if (Array.isArray(questions) && questions.length > 0) {
796
+ const normalized = questions.map(normalizeQuestionItem);
797
+ if (normalized.every((question) => question !== null)) {
798
+ return { ...parsed, questions: normalized };
799
+ }
800
+ }
801
+ } catch {
802
+ }
803
+ return null;
804
+ }
805
+ function normalizeQuestionItem(value) {
806
+ if (!value || typeof value !== "object") return null;
807
+ const item = value;
808
+ if (typeof item.question !== "string" || !Array.isArray(item.options)) return null;
809
+ const options = item.options.map(normalizeOptionItem);
810
+ if (!options.every((option) => option !== null)) return null;
811
+ return {
812
+ question: item.question,
813
+ options,
814
+ multiSelect: item.multiSelect === true || item.multi_select === true
815
+ };
816
+ }
817
+ function normalizeOptionItem(value) {
818
+ if (typeof value === "string") return { label: value, description: "" };
819
+ if (!value || typeof value !== "object") return null;
820
+ const option = value;
821
+ if (typeof option.label !== "string") return null;
822
+ return {
823
+ label: option.label,
824
+ description: typeof option.description === "string" ? option.description : ""
825
+ };
826
+ }
827
+
828
+ // src/components/ToolCallBlock.tsx
829
+ import { Fragment, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
830
+ function ToolCallBlock({
831
+ toolCall,
832
+ onAnswer,
833
+ answered,
834
+ answerData,
835
+ sessionStatus,
836
+ renderer
837
+ }) {
838
+ const [expanded, setExpanded] = useState5(false);
839
+ const normalizedName = formatToolName(toolCall.name);
840
+ if (renderer) {
841
+ const custom = renderer(toolCall);
842
+ if (custom !== null && custom !== void 0) {
843
+ return /* @__PURE__ */ jsx8(Fragment, { children: custom });
844
+ }
845
+ }
846
+ if (normalizedName === "AskUserQuestion") {
847
+ const askData = parseAskUserQuestion(typeof toolCall.result === "string" ? toolCall.result : null) ?? parseAskUserQuestion(buildAskUserPayload(toolCall.arguments));
848
+ const canAnswer = toolCall.status === "awaiting_answer" && sessionStatus === "waiting_for_input" && Boolean(onAnswer);
849
+ if (askData) {
850
+ return /* @__PURE__ */ jsx8(
851
+ AskUserQuestionBlock,
852
+ {
853
+ data: askData,
854
+ answered: Boolean(answered) || toolCall.status === "done",
855
+ toolCallId: toolCall.id,
856
+ sessionStatus: sessionStatus ?? "",
857
+ answerData,
858
+ onAnswer: canAnswer ? onAnswer : void 0
859
+ }
860
+ );
861
+ }
862
+ if (toolCall.status === "pending") {
863
+ return /* @__PURE__ */ jsxs6("div", { className: "ml-4 flex max-w-lg items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-4 text-sm text-[hsl(var(--muted-foreground))]", children: [
864
+ /* @__PURE__ */ jsx8(Loader24, { size: 14, className: "animate-spin" }),
865
+ /* @__PURE__ */ jsx8("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
866
+ ] });
867
+ }
868
+ return /* @__PURE__ */ jsxs6("div", { className: "ml-4 max-w-lg rounded-xl border border-amber-500/35 bg-amber-500/10 p-4 text-sm text-[hsl(var(--foreground))]", children: [
869
+ /* @__PURE__ */ jsx8("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
870
+ /* @__PURE__ */ jsx8("div", { className: "mt-1 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: "\u6536\u5230\u7684\u4EA4\u4E92\u6570\u636E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" })
871
+ ] });
872
+ }
873
+ const tone = getToolTone(toolCall.status);
874
+ const displayName = getToolDisplayLabel(toolCall);
875
+ const toneClass = tone === "red" ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : tone === "amber" ? "border-l-amber-400" : tone === "blue" ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]";
876
+ const statusIcon = toolCall.status === "pending" ? /* @__PURE__ */ jsx8(Loader24, { size: 11, className: "animate-spin" }) : toolCall.status === "awaiting_answer" ? /* @__PURE__ */ jsx8(MessageSquareMore2, { size: 11 }) : toolCall.status === "cancelled" || toolCall.status === "error" ? /* @__PURE__ */ jsx8(X2, { size: 11 }) : /* @__PURE__ */ jsx8(Check4, { size: 11 });
877
+ const statusTextClass = tone === "red" ? "text-[hsl(var(--muted-foreground))]" : tone === "amber" ? "text-amber-300" : tone === "blue" ? "text-blue-300" : "text-[hsl(var(--primary))]";
878
+ return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-tool ml-4 text-xs", children: [
879
+ /* @__PURE__ */ jsxs6("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
880
+ /* @__PURE__ */ jsxs6(
881
+ "button",
882
+ {
883
+ type: "button",
884
+ onClick: () => setExpanded(!expanded),
885
+ className: "flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:bg-white/3 focus-visible:ring-1 focus-visible:ring-[hsl(var(--ring))] focus:outline-none",
886
+ "aria-expanded": expanded,
887
+ children: [
888
+ /* @__PURE__ */ jsx8(
889
+ ChevronRight2,
890
+ {
891
+ size: 11,
892
+ className: cn(
893
+ "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform",
894
+ expanded && "rotate-90"
895
+ )
896
+ }
897
+ ),
898
+ /* @__PURE__ */ jsxs6("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
899
+ statusIcon,
900
+ /* @__PURE__ */ jsx8("span", { children: getToolStatusLabel(toolCall.status) })
901
+ ] }),
902
+ /* @__PURE__ */ jsx8("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
903
+ ]
904
+ }
905
+ ),
906
+ typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx8("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
907
+ ] }),
908
+ expanded && /* @__PURE__ */ jsxs6("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
909
+ /* @__PURE__ */ jsx8("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
910
+ /* @__PURE__ */ jsx8("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
911
+ /* @__PURE__ */ jsx8("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
912
+ /* @__PURE__ */ jsx8("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolArgs(toolCall.arguments) }),
913
+ toolCall.result != null && /* @__PURE__ */ jsxs6(Fragment, { children: [
914
+ /* @__PURE__ */ jsx8("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
915
+ /* @__PURE__ */ jsx8("pre", { className: "max-h-[400px] overflow-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolResult(toolCall.result) })
916
+ ] })
917
+ ] })
918
+ ] });
919
+ }
920
+ function buildAskUserPayload(argumentsJson) {
921
+ try {
922
+ const parsed = JSON.parse(argumentsJson);
923
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.questions)) {
924
+ return JSON.stringify({ questions: parsed.questions });
925
+ }
926
+ } catch {
927
+ return null;
928
+ }
929
+ return null;
930
+ }
931
+
932
+ // src/components/AssistantTurnBlock.tsx
933
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
934
+ function ThinkingBlock({ reasoning, isStreaming }) {
935
+ const [open, setOpen] = useState6(false);
936
+ return /* @__PURE__ */ jsxs7("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
937
+ /* @__PURE__ */ jsxs7(
938
+ "button",
939
+ {
940
+ type: "button",
941
+ onClick: () => setOpen(!open),
942
+ "aria-expanded": open,
943
+ className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
944
+ children: [
945
+ /* @__PURE__ */ jsx9(Brain, { size: 12, className: "shrink-0" }),
946
+ isStreaming ? /* @__PURE__ */ jsx9(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003..." }) : /* @__PURE__ */ jsx9("span", { children: "\u601D\u8003\u8FC7\u7A0B" }),
947
+ /* @__PURE__ */ jsx9(
948
+ ChevronDown,
949
+ {
950
+ size: 12,
951
+ className: cn("shrink-0 transition-transform", open && "rotate-180")
952
+ }
953
+ )
954
+ ]
955
+ }
956
+ ),
957
+ open && /* @__PURE__ */ jsx9("div", { className: "mt-1.5 whitespace-pre-wrap border-l-2 border-[hsl(var(--border))] pl-3 text-[11px] leading-5 text-[hsl(var(--muted-foreground))]", children: reasoning })
958
+ ] });
959
+ }
960
+ function getMessageText(message) {
961
+ return getTextContent(normalizeMessageContent(message.content)).trim();
962
+ }
963
+ function AssistantTurnBlock({
964
+ messages,
965
+ isStreaming = false,
966
+ askAnswers,
967
+ onAnswer,
968
+ sessionStatus,
969
+ toolCallRenderer
970
+ }) {
971
+ const hasInterrupted = messages.some((message) => message.status === "interrupted");
972
+ const hasAnyContent = messages.some(
973
+ (message) => getMessageText(message) || message.reasoning || (message.tool_calls?.length ?? 0) > 0
974
+ );
975
+ return /* @__PURE__ */ jsxs7("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
976
+ hasInterrupted && /* @__PURE__ */ jsx9("div", { className: "ml-4 w-fit rounded-full border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.12em] text-amber-300", children: "\u5DF2\u4E2D\u65AD" }),
977
+ messages.map((message, index) => {
978
+ const isLast = index === messages.length - 1;
979
+ const streamingThis = isStreaming && isLast;
980
+ const text = getMessageText(message);
981
+ const toolCalls = message.tool_calls ?? [];
982
+ return /* @__PURE__ */ jsxs7(
983
+ "div",
984
+ {
985
+ className: "flex flex-col gap-3",
986
+ children: [
987
+ message.reasoning && /* @__PURE__ */ jsx9(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }),
988
+ text && /* @__PURE__ */ jsx9("div", { className: "blade-chat-assistant-text text-[15px] leading-8 text-[hsl(var(--foreground))]", children: /* @__PURE__ */ jsx9(
989
+ MarkdownContent,
990
+ {
991
+ mode: streamingThis ? "streaming" : "static",
992
+ className: "blade-chat-prose",
993
+ children: text
994
+ }
995
+ ) }),
996
+ toolCalls.length > 0 && /* @__PURE__ */ jsx9("div", { className: "flex flex-col gap-2", children: toolCalls.map(
997
+ (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx9(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx9(
998
+ ToolCallBlock,
999
+ {
1000
+ toolCall,
1001
+ answerData: askAnswers?.[toolCall.id],
1002
+ onAnswer,
1003
+ answered: sessionStatus !== "waiting_for_input",
1004
+ sessionStatus,
1005
+ renderer: toolCallRenderer
1006
+ },
1007
+ toolCall.id
1008
+ )
1009
+ ) })
1010
+ ]
1011
+ },
1012
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
1013
+ );
1014
+ }),
1015
+ isStreaming && !hasAnyContent && /* @__PURE__ */ jsx9(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
1016
+ ] });
1017
+ }
1018
+
1019
+ // src/components/RenderErrorBoundary.tsx
1020
+ import { AlertTriangle as AlertTriangle2 } from "lucide-react";
1021
+ import { Component } from "react";
1022
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1023
+ function getFirstComponentName(componentStack) {
1024
+ const match = componentStack.match(/\n\s+at\s+([^\s(]+)/);
1025
+ return match?.[1] ?? null;
1026
+ }
1027
+ var RenderErrorBoundary = class extends Component {
1028
+ state = {
1029
+ componentStack: "",
1030
+ error: null
1031
+ };
1032
+ static getDerivedStateFromError(error) {
1033
+ return {
1034
+ componentStack: "",
1035
+ error
1036
+ };
1037
+ }
1038
+ componentDidCatch(error, info) {
1039
+ const componentStack = info.componentStack ?? "";
1040
+ console.error(`${this.props.label} render error:`, error, info);
1041
+ this.setState({ componentStack });
1042
+ }
1043
+ componentDidUpdate(prevProps) {
1044
+ if (this.state.error && prevProps.resetKey !== this.props.resetKey) {
1045
+ this.setState({ componentStack: "", error: null });
1046
+ }
1047
+ }
1048
+ render() {
1049
+ const { children, details, label } = this.props;
1050
+ const { componentStack, error } = this.state;
1051
+ if (!error) {
1052
+ return children;
1053
+ }
1054
+ const componentName = getFirstComponentName(componentStack);
1055
+ return /* @__PURE__ */ jsx10("div", { className: "blade-chat-render-error rounded-xl border border-amber-500/30 bg-amber-500/8 px-4 py-3 text-sm text-amber-100", children: /* @__PURE__ */ jsxs8("div", { className: "flex items-start gap-2", children: [
1056
+ /* @__PURE__ */ jsx10(AlertTriangle2, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
1057
+ /* @__PURE__ */ jsxs8("div", { className: "min-w-0 flex-1", children: [
1058
+ /* @__PURE__ */ jsxs8("div", { className: "font-medium", children: [
1059
+ label,
1060
+ "\u6E32\u67D3\u5931\u8D25"
1061
+ ] }),
1062
+ /* @__PURE__ */ jsxs8("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
1063
+ componentName ? `\u7EC4\u4EF6\uFF1A${componentName}\u3002` : null,
1064
+ error.message || "\u53D1\u751F\u4E86\u672A\u9884\u671F\u7684\u6E32\u67D3\u9519\u8BEF\u3002"
1065
+ ] }),
1066
+ details ? /* @__PURE__ */ jsx10("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
1067
+ ] })
1068
+ ] }) });
1069
+ }
1070
+ };
1071
+
1072
+ // src/components/UserMessageBubble.tsx
1073
+ import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
1074
+ import { FileText, Loader2 as Loader25 } from "lucide-react";
1075
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1076
+ function isUserMessage(message) {
1077
+ return message.role === "user";
1078
+ }
1079
+ function isErrorMessage(message) {
1080
+ return message.role === "error";
1081
+ }
1082
+ var isSending = (message) => message.status === "streaming";
1083
+ function UserMessageBubble({ message, className }) {
1084
+ const text = getTextContent2(message.content).trim();
1085
+ const fileParts = getFileParts(message.content);
1086
+ const imageParts = getImageParts(message.content);
1087
+ return /* @__PURE__ */ jsx11("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs9("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
1088
+ imageParts.length > 0 && /* @__PURE__ */ jsx11("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx11(
1089
+ "img",
1090
+ {
1091
+ src: part.image_url.url,
1092
+ alt: "\u7528\u6237\u4E0A\u4F20\u7684\u56FE\u7247",
1093
+ className: "max-h-64 rounded-xl border border-[hsl(var(--user-msg-border))] object-cover"
1094
+ },
1095
+ part.image_url.url
1096
+ )) }),
1097
+ fileParts.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs9(
1098
+ "div",
1099
+ {
1100
+ className: "flex items-center gap-1.5 rounded-lg border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--muted)/0.3)] px-2.5 py-1.5 text-xs text-[hsl(var(--muted-foreground))]",
1101
+ children: [
1102
+ /* @__PURE__ */ jsx11(FileText, { size: 12, className: "shrink-0" }),
1103
+ /* @__PURE__ */ jsx11("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
1104
+ ]
1105
+ },
1106
+ `${part.name}-${part.data.length}`
1107
+ )) }),
1108
+ text && /* @__PURE__ */ jsx11("div", { className: "blade-chat-user-bubble max-w-full rounded-[20px] rounded-br-[6px] border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-[18px] py-[13px] text-sm leading-[1.65] text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx11(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
1109
+ text && isSending(message) && /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
1110
+ /* @__PURE__ */ jsx11(Loader25, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
1111
+ /* @__PURE__ */ jsx11("span", { children: "\u53D1\u9001\u4E2D" })
1112
+ ] })
1113
+ ] }) });
1114
+ }
1115
+ function ErrorMessageBlock({
1116
+ message,
1117
+ className
1118
+ }) {
1119
+ const text = getTextContent2(message.content);
1120
+ return /* @__PURE__ */ jsx11("div", { className: cn("blade-chat-error-row flex justify-center", className), children: /* @__PURE__ */ jsx11("div", { className: "blade-chat-error-block max-w-[85%] border-l-[3px] border-[hsl(var(--destructive))] px-4 py-1 text-sm leading-7 text-[hsl(var(--destructive))]", children: text }) });
1121
+ }
1122
+
1123
+ // src/components/MessageList.tsx
1124
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1125
+ function parseModeChange(message) {
1126
+ if (message.kind !== "mode_change" || typeof message.content !== "string") {
1127
+ return null;
1128
+ }
1129
+ try {
1130
+ const parsed = JSON.parse(message.content);
1131
+ if (typeof parsed.from === "string" && typeof parsed.to === "string") {
1132
+ return { from: parsed.from, to: parsed.to };
1133
+ }
1134
+ } catch {
1135
+ }
1136
+ return null;
1137
+ }
1138
+ function getPlanningDividerKind(message) {
1139
+ const modeChange = parseModeChange(message);
1140
+ if (modeChange?.to === "planning") return "enter";
1141
+ if (modeChange?.from === "planning") return "exit";
1142
+ return null;
1143
+ }
1144
+ function getMessageResetSignature(messages) {
1145
+ return messages.map((message) => {
1146
+ const contentSize = typeof message.content === "string" ? message.content.length : message.content.length;
1147
+ const toolSignature = message.tool_calls?.map((toolCall) => `${toolCall.id}:${toolCall.status ?? ""}`).join(",") ?? "";
1148
+ return [
1149
+ message.entry_id ?? message.timestamp ?? message.role,
1150
+ message.status ?? "",
1151
+ contentSize,
1152
+ toolSignature
1153
+ ].join(":");
1154
+ }).join("|");
1155
+ }
1156
+ function MessageList({
1157
+ messages,
1158
+ isStreaming,
1159
+ sessionStatus,
1160
+ askAnswers,
1161
+ onAnswer,
1162
+ toolCallRenderer,
1163
+ emptyState,
1164
+ className
1165
+ }) {
1166
+ const renderBlocks = useMemo4(() => {
1167
+ const visible = messages.filter((message) => {
1168
+ if ((message.loop_name ?? "root") !== "root") return false;
1169
+ if (isHiddenInternalMessage(message)) return false;
1170
+ if (message.kind === "compaction") return true;
1171
+ return message.role !== "tool" || getPlanningDividerKind(message) !== null;
1172
+ });
1173
+ const blocks = [];
1174
+ let assistantBuffer = [];
1175
+ let assistantTurnCount = 0;
1176
+ const flushAssistant = () => {
1177
+ if (assistantBuffer.length === 0) return;
1178
+ assistantTurnCount += 1;
1179
+ const first = assistantBuffer[0];
1180
+ blocks.push({
1181
+ type: "assistant_turn",
1182
+ messages: assistantBuffer,
1183
+ key: first.entry_id ?? `assistant-${assistantTurnCount}`,
1184
+ isStreaming: assistantBuffer.some((message) => message.status === "streaming")
1185
+ });
1186
+ assistantBuffer = [];
1187
+ };
1188
+ for (const message of visible) {
1189
+ const dividerKind = getPlanningDividerKind(message);
1190
+ if (dividerKind) {
1191
+ flushAssistant();
1192
+ blocks.push({
1193
+ type: "planning_divider",
1194
+ kind: dividerKind,
1195
+ key: message.entry_id ?? `${dividerKind}-${blocks.length}`
1196
+ });
1197
+ continue;
1198
+ }
1199
+ if (message.kind === "compaction") {
1200
+ flushAssistant();
1201
+ blocks.push({ type: "compaction", key: message.entry_id ?? `compaction-${blocks.length}` });
1202
+ continue;
1203
+ }
1204
+ if (message.role === "assistant") {
1205
+ assistantBuffer.push(message);
1206
+ continue;
1207
+ }
1208
+ flushAssistant();
1209
+ blocks.push({
1210
+ type: "message",
1211
+ message,
1212
+ key: message.entry_id ?? `${message.role}-${blocks.length}`
1213
+ });
1214
+ }
1215
+ flushAssistant();
1216
+ const last = blocks[blocks.length - 1];
1217
+ if (isStreaming) {
1218
+ if (last?.type === "assistant_turn") {
1219
+ last.isStreaming = true;
1220
+ } else if (last?.type === "message" && isUserMessage(last.message)) {
1221
+ blocks.push({
1222
+ type: "assistant_turn",
1223
+ messages: [
1224
+ {
1225
+ role: "assistant",
1226
+ content: "",
1227
+ entry_id: `pending-assistant-${last.key}`,
1228
+ status: "streaming",
1229
+ loop_name: "root"
1230
+ }
1231
+ ],
1232
+ key: `pending-assistant-${last.key}`,
1233
+ isStreaming: true
1234
+ });
1235
+ }
1236
+ }
1237
+ return blocks;
1238
+ }, [messages, isStreaming]);
1239
+ return /* @__PURE__ */ jsx12("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs10(StickToBottom, { className: "h-full overflow-y-hidden", initial: "instant", resize: "instant", children: [
1240
+ /* @__PURE__ */ jsx12(StickToBottom.Content, { className: "px-5 pb-7 pt-8 sm:px-10 sm:pt-10", children: /* @__PURE__ */ jsx12("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs10("div", { className: "flex min-w-0 flex-col gap-9", children: [
1241
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs10("div", { className: "flex flex-col items-center justify-center gap-3 py-24 text-[hsl(var(--muted-foreground))]", children: [
1242
+ /* @__PURE__ */ jsx12(MessageSquare, { size: 40, strokeWidth: 1.5 }),
1243
+ /* @__PURE__ */ jsx12("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
1244
+ /* @__PURE__ */ jsx12("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
1245
+ ] }) : renderBlocks.map((block) => {
1246
+ if (block.type === "message") {
1247
+ return /* @__PURE__ */ jsx12("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx12(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx12(ErrorMessageBlock, { message: block.message }) : null }, block.key);
1248
+ }
1249
+ if (block.type === "assistant_turn") {
1250
+ return /* @__PURE__ */ jsx12("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsx12(
1251
+ RenderErrorBoundary,
1252
+ {
1253
+ label: "\u52A9\u624B\u6D88\u606F",
1254
+ details: block.key,
1255
+ resetKey: getMessageResetSignature(block.messages),
1256
+ children: /* @__PURE__ */ jsx12(
1257
+ AssistantTurnBlock,
1258
+ {
1259
+ messages: block.messages,
1260
+ isStreaming: block.isStreaming,
1261
+ askAnswers,
1262
+ onAnswer,
1263
+ sessionStatus,
1264
+ toolCallRenderer
1265
+ }
1266
+ )
1267
+ }
1268
+ ) }, block.key);
1269
+ }
1270
+ if (block.type === "compaction") {
1271
+ return /* @__PURE__ */ jsxs10(
1272
+ "div",
1273
+ {
1274
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
1275
+ children: [
1276
+ /* @__PURE__ */ jsx12(Layers, { size: 12 }),
1277
+ /* @__PURE__ */ jsx12("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
1278
+ ]
1279
+ },
1280
+ block.key
1281
+ );
1282
+ }
1283
+ return /* @__PURE__ */ jsx12(PlanningDivider, { kind: block.kind }, block.key);
1284
+ }),
1285
+ sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx12("div", { className: "flex", children: /* @__PURE__ */ jsx12("div", { className: "rounded-full border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.12em] text-amber-300", children: "\u5DF2\u4E2D\u65AD" }) }) : null
1286
+ ] }) }) }),
1287
+ /* @__PURE__ */ jsx12(AutoScrollOnUserSend, { userMessageCount: messages.filter((m) => isUserMessage(m)).length }),
1288
+ /* @__PURE__ */ jsx12(ScrollToBottomButton, {})
1289
+ ] }) });
1290
+ }
1291
+ function AutoScrollOnUserSend({ userMessageCount }) {
1292
+ const { scrollToBottom } = useStickToBottomContext();
1293
+ const previousCountRef = useRef3(userMessageCount);
1294
+ useEffect4(() => {
1295
+ if (userMessageCount > previousCountRef.current) {
1296
+ scrollToBottom("instant");
1297
+ }
1298
+ previousCountRef.current = userMessageCount;
1299
+ }, [userMessageCount, scrollToBottom]);
1300
+ return null;
1301
+ }
1302
+ function ScrollToBottomButton() {
1303
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
1304
+ if (isAtBottom) return null;
1305
+ return /* @__PURE__ */ jsxs10(
1306
+ "button",
1307
+ {
1308
+ type: "button",
1309
+ onClick: () => scrollToBottom(),
1310
+ className: "absolute bottom-4 right-4 flex items-center gap-1 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-1.5 text-xs text-[hsl(var(--muted-foreground))] shadow-lg transition-colors hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]",
1311
+ children: [
1312
+ /* @__PURE__ */ jsx12(ChevronDown2, { size: 14 }),
1313
+ /* @__PURE__ */ jsx12("span", { children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
1314
+ ]
1315
+ }
1316
+ );
1317
+ }
1318
+ function PlanningDivider({ kind }) {
1319
+ return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-3 py-1", children: [
1320
+ /* @__PURE__ */ jsx12("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
1321
+ /* @__PURE__ */ jsxs10("div", { className: "inline-flex items-center gap-1.5 rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-300", children: [
1322
+ /* @__PURE__ */ jsx12(Lightbulb, { size: 12 }),
1323
+ /* @__PURE__ */ jsx12("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
1324
+ ] }),
1325
+ /* @__PURE__ */ jsx12("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
1326
+ ] });
1327
+ }
1328
+
1329
+ // src/components/ChatView.tsx
1330
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1331
+ function isUnauthorizedError(error) {
1332
+ return error instanceof BladeApiError && error.status === 401;
1333
+ }
1334
+ function LoginCard({ client, onLoggedIn }) {
1335
+ const [loggingIn, setLoggingIn] = useState7(false);
1336
+ const [loginError, setLoginError] = useState7(null);
1337
+ const handleLogin = async () => {
1338
+ setLoggingIn(true);
1339
+ setLoginError(null);
1340
+ try {
1341
+ await client.login();
1342
+ onLoggedIn();
1343
+ } catch (error) {
1344
+ setLoginError(error instanceof Error && error.message ? error.message : "\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
1345
+ } finally {
1346
+ setLoggingIn(false);
1347
+ }
1348
+ };
1349
+ return /* @__PURE__ */ jsx13("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs11("div", { className: "flex w-full max-w-sm flex-col items-center gap-4 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-6 py-8 text-center", children: [
1350
+ /* @__PURE__ */ jsx13(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
1351
+ /* @__PURE__ */ jsx13("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
1352
+ /* @__PURE__ */ jsx13("div", { className: "text-sm text-[hsl(var(--muted-foreground))]", children: "\u767B\u5F55\u540E\u5373\u53EF\u4E0E\u667A\u80FD\u4F53\u5BF9\u8BDD\uFF0C\u4F60\u7684\u4F1A\u8BDD\u5185\u5BB9\u4EC5\u81EA\u5DF1\u53EF\u89C1\u3002" }),
1353
+ /* @__PURE__ */ jsx13(
1354
+ "button",
1355
+ {
1356
+ type: "button",
1357
+ onClick: handleLogin,
1358
+ disabled: loggingIn,
1359
+ className: "w-full rounded-lg bg-[hsl(var(--primary))] px-4 py-2 text-sm font-semibold text-[hsl(var(--primary-foreground))] transition-opacity hover:opacity-90 disabled:opacity-60",
1360
+ children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
1361
+ }
1362
+ ),
1363
+ loginError && /* @__PURE__ */ jsx13("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
1364
+ ] }) });
1365
+ }
1366
+ function VersionMismatchCard({ error }) {
1367
+ return /* @__PURE__ */ jsx13("div", { className: "blade-chat-version-mismatch flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs11("div", { className: "flex w-full max-w-md flex-col gap-3 rounded-2xl border border-amber-500/30 bg-amber-500/10 px-6 py-6", children: [
1368
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-sm font-medium text-amber-300", children: [
1369
+ /* @__PURE__ */ jsx13(CircleAlert, { size: 16 }),
1370
+ /* @__PURE__ */ jsx13("span", { children: "\u7248\u672C\u4E0D\u5339\u914D" })
1371
+ ] }),
1372
+ /* @__PURE__ */ jsx13("div", { className: "break-words text-sm leading-6 text-[hsl(var(--foreground))]", children: error.message })
1373
+ ] }) });
1374
+ }
1375
+ function ChatView(props) {
1376
+ const client = useBladeClient();
1377
+ const [attempt, setAttempt] = useState7(0);
1378
+ const [needLogin, setNeedLogin] = useState7(() => !client.hasToken());
1379
+ if (needLogin) {
1380
+ return /* @__PURE__ */ jsx13("div", { className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root), children: /* @__PURE__ */ jsx13(
1381
+ LoginCard,
1382
+ {
1383
+ client,
1384
+ onLoggedIn: () => {
1385
+ setNeedLogin(false);
1386
+ setAttempt((value) => value + 1);
1387
+ }
1388
+ }
1389
+ ) });
1390
+ }
1391
+ return /* @__PURE__ */ jsx13(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
1392
+ }
1393
+ function ChatSessionView({
1394
+ sessionId,
1395
+ createOptions,
1396
+ onSessionCreated,
1397
+ onSessionReady,
1398
+ commands,
1399
+ classNames,
1400
+ renderers,
1401
+ slots,
1402
+ placeholder,
1403
+ onUnauthorized
1404
+ }) {
1405
+ const { session, state, error } = useAgentSession(sessionId, {
1406
+ createOptions,
1407
+ onSessionCreated
1408
+ });
1409
+ const [stopRequested, setStopRequested] = useState7(false);
1410
+ const [inputText, setInputText] = useState7("");
1411
+ useEffect5(() => {
1412
+ if (session) {
1413
+ onSessionReady?.(session);
1414
+ }
1415
+ }, [session, onSessionReady]);
1416
+ useEffect5(() => {
1417
+ if (!session) return;
1418
+ const offAttach = session.on("attachRequested", ({ label, content }) => {
1419
+ setInputText((prev) => `${prev ? `${prev}
1420
+
1421
+ ` : ""}[${label}]
1422
+ ${content}`);
1423
+ });
1424
+ const offInsert = session.on("insertTextRequested", ({ text }) => {
1425
+ setInputText((prev) => prev + text);
1426
+ });
1427
+ return () => {
1428
+ offAttach();
1429
+ offInsert();
1430
+ };
1431
+ }, [session]);
1432
+ useEffect5(() => {
1433
+ if (isUnauthorizedError(error)) {
1434
+ onUnauthorized();
1435
+ }
1436
+ }, [error, onUnauthorized]);
1437
+ useEffect5(() => {
1438
+ if (!session || !commands) return;
1439
+ const unsubscribes = Object.entries(commands).map(
1440
+ ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
1441
+ );
1442
+ return () => {
1443
+ for (const unsubscribe of unsubscribes) unsubscribe();
1444
+ };
1445
+ }, [session, commands]);
1446
+ if (error instanceof VersionMismatchError) {
1447
+ return /* @__PURE__ */ jsx13("div", { className: cn("blade-chat flex min-h-0 flex-1 flex-col", classNames?.root), children: /* @__PURE__ */ jsx13(VersionMismatchCard, { error }) });
1448
+ }
1449
+ const isStreaming = state?.isStreaming ?? false;
1450
+ const isStopping = stopRequested && isStreaming;
1451
+ const connectError = error && !isUnauthorizedError(error) ? error.message || "\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : null;
1452
+ const errorMessage = connectError ?? state?.errorMessage ?? null;
1453
+ const handleSend = (text) => {
1454
+ setStopRequested(false);
1455
+ void session?.send(text, { mode: state?.mode ?? void 0 });
1456
+ };
1457
+ const handleStop = () => {
1458
+ setStopRequested(true);
1459
+ void session?.stop();
1460
+ };
1461
+ return /* @__PURE__ */ jsxs11(
1462
+ "div",
1463
+ {
1464
+ className: cn(
1465
+ "blade-chat flex min-h-0 flex-1 flex-col overflow-hidden",
1466
+ classNames?.root
1467
+ ),
1468
+ children: [
1469
+ /* @__PURE__ */ jsx13(
1470
+ ConnectionBanner,
1471
+ {
1472
+ connection: state?.connection ?? "connecting",
1473
+ className: classNames?.banner
1474
+ }
1475
+ ),
1476
+ errorMessage && /* @__PURE__ */ jsxs11("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800 dark:border-red-800/30 dark:bg-red-950/30 dark:text-red-300", children: [
1477
+ /* @__PURE__ */ jsx13(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
1478
+ /* @__PURE__ */ jsx13("span", { children: errorMessage })
1479
+ ] }),
1480
+ slots?.header,
1481
+ /* @__PURE__ */ jsx13(
1482
+ MessageList,
1483
+ {
1484
+ messages: state?.messages ?? [],
1485
+ isStreaming,
1486
+ sessionStatus: state?.status ?? void 0,
1487
+ askAnswers: state?.askAnswers,
1488
+ onAnswer: (answer, toolCallId, answerData) => {
1489
+ void session?.send(answer, {
1490
+ mode: state?.mode ?? void 0,
1491
+ askUserAnswer: { tool_call_id: toolCallId, ...answerData }
1492
+ });
1493
+ },
1494
+ toolCallRenderer: renderers?.toolCall,
1495
+ emptyState: slots?.emptyState,
1496
+ className: classNames?.messageList
1497
+ }
1498
+ ),
1499
+ /* @__PURE__ */ jsx13(
1500
+ ChatInput,
1501
+ {
1502
+ value: inputText,
1503
+ onValueChange: setInputText,
1504
+ onSend: handleSend,
1505
+ onStop: handleStop,
1506
+ isStreaming,
1507
+ isStopping,
1508
+ placeholder,
1509
+ className: classNames?.chatInput
1510
+ }
1511
+ ),
1512
+ slots?.footer
1513
+ ]
1514
+ }
1515
+ );
1516
+ }
1517
+
1518
+ // src/index.ts
1519
+ export * from "@blade-hq/agent-client";
1520
+ export {
1521
+ BladeProvider,
1522
+ ChatView,
1523
+ MarkdownContent,
1524
+ useAgentSession,
1525
+ useBladeClient
1526
+ };
1527
+ //# sourceMappingURL=index.js.map