@blade-hq/agent-react 2610.0.0-beta.8 → 2610.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,6 +17,9 @@ function useBladeClient() {
17
17
  }
18
18
  return client;
19
19
  }
20
+ function useOptionalBladeClient() {
21
+ return useContext(BladeClientContext);
22
+ }
20
23
 
21
24
  // src/hooks/use-agent-session.ts
22
25
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
@@ -30,11 +33,14 @@ function useAgentSession(sessionId, options = {}) {
30
33
  const connRef = useRef({
31
34
  id: null,
32
35
  session: null,
36
+ cleanup: null,
33
37
  gen: 0
34
38
  });
35
39
  const createdIdPromiseRef = useRef(null);
36
40
  const onCreatedRef = useRef(options.onSessionCreated);
37
41
  onCreatedRef.current = options.onSessionCreated;
42
+ const onConnectedRef = useRef(options.onSessionConnected);
43
+ onConnectedRef.current = options.onSessionConnected;
38
44
  const createOptionsRef = useRef(options.createOptions);
39
45
  createOptionsRef.current = options.createOptions;
40
46
  const sessionIdRef = useRef(sessionId);
@@ -42,6 +48,7 @@ function useAgentSession(sessionId, options = {}) {
42
48
  const connect = useMemo(() => {
43
49
  return (targetId) => {
44
50
  const gen = ++connRef.current.gen;
51
+ let pendingCleanup = null;
45
52
  const idPromise = targetId ? Promise.resolve(targetId) : (
46
53
  // biome-ignore lint/suspicious/noAssignInExpressions: ??= 挂 ref 是 StrictMode 下"只创建一次"的关键
47
54
  createdIdPromiseRef.current ??= client.sessions.create(createOptionsRef.current ?? {}).then((created) => {
@@ -51,16 +58,25 @@ function useAgentSession(sessionId, options = {}) {
51
58
  return id;
52
59
  })
53
60
  );
54
- idPromise.then((id) => client.hub.connect(id)).then((next) => {
61
+ idPromise.then(
62
+ (id) => client.hub.connect(id, {
63
+ setup: (next) => {
64
+ pendingCleanup = onConnectedRef.current?.(next) ?? null;
65
+ }
66
+ })
67
+ ).then((next) => {
55
68
  if (connRef.current.gen !== gen) {
69
+ pendingCleanup?.();
56
70
  next.dispose();
57
71
  return;
58
72
  }
59
73
  connRef.current.id = next.sessionId;
60
74
  connRef.current.session = next;
75
+ connRef.current.cleanup = pendingCleanup;
61
76
  setSession(next);
62
77
  setError(null);
63
78
  }).catch((err) => {
79
+ pendingCleanup?.();
64
80
  if (connRef.current.gen !== gen) return;
65
81
  createdIdPromiseRef.current = null;
66
82
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -72,8 +88,11 @@ function useAgentSession(sessionId, options = {}) {
72
88
  return () => {
73
89
  connRef.current.gen++;
74
90
  const toRelease = connRef.current.session;
91
+ const cleanup = connRef.current.cleanup;
75
92
  connRef.current.id = null;
76
93
  connRef.current.session = null;
94
+ connRef.current.cleanup = null;
95
+ cleanup?.();
77
96
  setSession(null);
78
97
  if (toRelease) setTimeout(() => toRelease.dispose(), DISPOSE_DELAY_MS);
79
98
  };
@@ -83,8 +102,11 @@ function useAgentSession(sessionId, options = {}) {
83
102
  if (connRef.current.id === null) return;
84
103
  if (sessionId === connRef.current.id) return;
85
104
  const previous = connRef.current.session;
105
+ const cleanup = connRef.current.cleanup;
86
106
  connRef.current.id = null;
87
107
  connRef.current.session = null;
108
+ connRef.current.cleanup = null;
109
+ cleanup?.();
88
110
  if (previous) setTimeout(() => previous.dispose(), DISPOSE_DELAY_MS);
89
111
  connect(sessionId);
90
112
  }, [sessionId, connect]);
@@ -245,6 +267,8 @@ function useLlmChat(options) {
245
267
  const [error, setError] = useState3(null);
246
268
  const [isStreaming, setIsStreaming] = useState3(false);
247
269
  const abortRef = useRef2(null);
270
+ const activeStartedAtRef = useRef2(null);
271
+ const assistantTimingsRef = useRef2(/* @__PURE__ */ new WeakMap());
248
272
  const generationRef = useRef2(0);
249
273
  const historyRef = useRef2([]);
250
274
  const optionsRef = useRef2(options);
@@ -263,16 +287,31 @@ function useLlmChat(options) {
263
287
  setFailedToolIds([]);
264
288
  setError(null);
265
289
  setIsStreaming(false);
290
+ activeStartedAtRef.current = null;
291
+ assistantTimingsRef.current = /* @__PURE__ */ new WeakMap();
266
292
  }, [stop]);
267
293
  const send = useCallback2(async (text) => {
268
294
  const content = text.trim();
269
295
  if (!content || abortRef.current) return false;
270
296
  const opts = optionsRef.current;
271
297
  const maxRounds = opts.maxToolRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
298
+ const startedAt = Date.now();
299
+ activeStartedAtRef.current = startedAt;
272
300
  const commit = (message) => {
273
301
  historyRef.current = [...historyRef.current, message];
274
302
  setHistory(historyRef.current);
275
303
  };
304
+ const latestAssistantTiming = { current: null };
305
+ const commitAssistant = (message) => {
306
+ const timing = { startedAt };
307
+ assistantTimingsRef.current.set(message, timing);
308
+ latestAssistantTiming.current = timing;
309
+ commit(message);
310
+ return timing;
311
+ };
312
+ const finishTiming = (timing) => {
313
+ timing.durationMs = Math.max(0, Date.now() - timing.startedAt);
314
+ };
276
315
  setError(null);
277
316
  setIsStreaming(true);
278
317
  setStreamingText("");
@@ -298,8 +337,11 @@ function useLlmChat(options) {
298
337
  };
299
338
  setStreamingText(null);
300
339
  setStreamingCalls([]);
301
- commit(assistant);
302
- if (!result.toolCalls.length) return true;
340
+ const assistantTiming = commitAssistant(assistant);
341
+ if (!result.toolCalls.length) {
342
+ finishTiming(assistantTiming);
343
+ return true;
344
+ }
303
345
  const bail = (reason) => {
304
346
  for (const call of result.toolCalls) {
305
347
  commit({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: reason }) });
@@ -308,11 +350,13 @@ function useLlmChat(options) {
308
350
  };
309
351
  if (!opts.onToolCall) {
310
352
  bail("\u8FD9\u4E2A\u5E94\u7528\u6CA1\u6709\u63D0\u4F9B\u5DE5\u5177\u6267\u884C\u5165\u53E3");
353
+ finishTiming(assistantTiming);
311
354
  return true;
312
355
  }
313
356
  if (round >= maxRounds) {
314
357
  bail(`\u5DE5\u5177\u8C03\u7528\u5DF2\u8FBE\u4E0A\u9650 ${maxRounds} \u8F6E\uFF0C\u6CA1\u6709\u6267\u884C`);
315
358
  setError(`\u5DE5\u5177\u8C03\u7528\u8D85\u8FC7 ${maxRounds} \u8F6E\u4ECD\u672A\u7ED9\u51FA\u7ED3\u8BBA\uFF0C\u5DF2\u505C\u4E0B\u3002`);
359
+ finishTiming(assistantTiming);
316
360
  return false;
317
361
  }
318
362
  for (const call of result.toolCalls) {
@@ -337,21 +381,39 @@ function useLlmChat(options) {
337
381
  setStreamingCalls([]);
338
382
  if (controller.signal.aborted) {
339
383
  if (generation !== generationRef.current) return false;
340
- commit({ role: "assistant", content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09" });
384
+ const timing = commitAssistant({
385
+ role: "assistant",
386
+ content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09"
387
+ });
388
+ timing.status = "interrupted";
389
+ finishTiming(timing);
341
390
  return false;
342
391
  }
343
392
  if (partial && generation === generationRef.current) {
344
- commit({ role: "assistant", content: partial });
393
+ const timing = commitAssistant({ role: "assistant", content: partial });
394
+ timing.status = "failed";
395
+ finishTiming(timing);
396
+ } else if (latestAssistantTiming.current) {
397
+ latestAssistantTiming.current.status = "failed";
398
+ finishTiming(latestAssistantTiming.current);
345
399
  }
346
400
  setError(err instanceof Error && err.message ? err.message : "\u5BF9\u8BDD\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
347
401
  return false;
348
402
  } finally {
349
403
  if (abortRef.current === controller) abortRef.current = null;
404
+ activeStartedAtRef.current = null;
350
405
  setIsStreaming(false);
351
406
  }
352
407
  }, []);
353
408
  const messages = useMemo2(
354
- () => toChatMessages(history, streamingText, streamingCalls, failedToolIds),
409
+ () => toChatMessages(
410
+ history,
411
+ streamingText,
412
+ streamingCalls,
413
+ failedToolIds,
414
+ assistantTimingsRef.current,
415
+ activeStartedAtRef.current
416
+ ),
355
417
  [history, streamingText, streamingCalls, failedToolIds]
356
418
  );
357
419
  return { messages, isStreaming, error, send, stop, reset };
@@ -503,7 +565,7 @@ function parseSse(raw) {
503
565
  if (!delta) return null;
504
566
  return { text: delta.content, toolCallDeltas: delta.tool_calls };
505
567
  }
506
- function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
568
+ function toChatMessages(history, streamingText, streamingCalls, failedToolIds, assistantTimings, activeStartedAt) {
507
569
  const results = /* @__PURE__ */ new Map();
508
570
  for (const msg of history) {
509
571
  if (msg.role === "tool") results.set(msg.tool_call_id, msg.content);
@@ -515,10 +577,13 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
515
577
  messages.push({ role: "user", content: msg.content, status: "completed" });
516
578
  continue;
517
579
  }
580
+ const timing = assistantTimings.get(msg);
518
581
  messages.push({
519
582
  role: "assistant",
520
583
  content: msg.content,
521
- status: "completed",
584
+ status: timing?.status ?? "completed",
585
+ ...timing ? { timestamp: new Date(timing.startedAt).toISOString() } : {},
586
+ ...timing?.durationMs === void 0 ? {} : { duration_ms: timing.durationMs },
522
587
  ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
523
588
  });
524
589
  }
@@ -527,6 +592,7 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
527
592
  role: "assistant",
528
593
  content: streamingText,
529
594
  status: "streaming",
595
+ ...activeStartedAt === null ? {} : { timestamp: new Date(activeStartedAt).toISOString() },
530
596
  ...streamingCalls.length ? { tool_calls: streamingCalls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
531
597
  });
532
598
  }
@@ -544,6 +610,184 @@ function toToolCallInfo(call, results, failedToolIds) {
544
610
  };
545
611
  }
546
612
 
613
+ // src/hooks/use-message-pin.ts
614
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3 } from "react";
615
+ var DEFAULT_MARGIN_PX = 24;
616
+ var MANUAL_SCROLL_TOLERANCE_PX = 2;
617
+ function useMessagePin({
618
+ targetKey,
619
+ pinTarget,
620
+ layoutKey,
621
+ getScrollElement,
622
+ getContentElement,
623
+ getTargetElement,
624
+ getTargetScrollTop,
625
+ getSpacerHeight,
626
+ setSpacerHeight,
627
+ stopAutoScroll,
628
+ scrollToBottom,
629
+ margin = DEFAULT_MARGIN_PX
630
+ }) {
631
+ const pinActiveRef = useRef3(false);
632
+ const frameRef = useRef3(null);
633
+ const retryTimeoutRef = useRef3(null);
634
+ const manualScrollCheckTimeoutRef = useRef3(null);
635
+ const repositionPendingRef = useRef3(false);
636
+ const pinnedTargetScrollTopRef = useRef3(null);
637
+ const appliedTargetKeyRef = useRef3(null);
638
+ const initialObservedTargetKeyRef = useRef3(pinTarget ? null : targetKey);
639
+ const observedTargetKeyRef = useRef3(initialObservedTargetKeyRef.current);
640
+ const setSpacerHeightRef = useRef3(setSpacerHeight);
641
+ setSpacerHeightRef.current = setSpacerHeight;
642
+ const release = useCallback3(() => {
643
+ if (!pinActiveRef.current) return;
644
+ pinActiveRef.current = false;
645
+ repositionPendingRef.current = false;
646
+ pinnedTargetScrollTopRef.current = null;
647
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
648
+ frameRef.current = null;
649
+ if (manualScrollCheckTimeoutRef.current != null) {
650
+ clearTimeout(manualScrollCheckTimeoutRef.current);
651
+ manualScrollCheckTimeoutRef.current = null;
652
+ }
653
+ setSpacerHeightRef.current(0);
654
+ }, []);
655
+ const reposition = useCallback3(
656
+ () => {
657
+ const scroll = getScrollElement();
658
+ const target = getTargetElement();
659
+ if (!scroll || !pinActiveRef.current) return;
660
+ const targetScrollTop = target ? Math.max(
661
+ 0,
662
+ scroll.scrollTop + target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - margin
663
+ ) : getTargetScrollTop?.(scroll);
664
+ if (targetScrollTop == null) return;
665
+ const previousSpacerHeight = getSpacerHeight();
666
+ const baseScrollHeight = scroll.scrollHeight - previousSpacerHeight;
667
+ const nextSpacerHeight = Math.max(
668
+ 0,
669
+ targetScrollTop + scroll.clientHeight - baseScrollHeight
670
+ );
671
+ setSpacerHeightRef.current(nextSpacerHeight);
672
+ pinnedTargetScrollTopRef.current = targetScrollTop;
673
+ stopAutoScroll();
674
+ scroll.scrollTop = targetScrollTop;
675
+ if (nextSpacerHeight === 0 && previousSpacerHeight > 0) {
676
+ pinActiveRef.current = false;
677
+ scrollToBottom();
678
+ }
679
+ },
680
+ [
681
+ getScrollElement,
682
+ getSpacerHeight,
683
+ getTargetElement,
684
+ getTargetScrollTop,
685
+ margin,
686
+ scrollToBottom,
687
+ stopAutoScroll
688
+ ]
689
+ );
690
+ const scheduleReposition = useCallback3(
691
+ () => {
692
+ if (!pinActiveRef.current) return;
693
+ repositionPendingRef.current = true;
694
+ if (frameRef.current != null) return;
695
+ frameRef.current = requestAnimationFrame(() => {
696
+ frameRef.current = null;
697
+ const shouldReposition = repositionPendingRef.current;
698
+ repositionPendingRef.current = false;
699
+ if (shouldReposition) reposition();
700
+ });
701
+ },
702
+ [reposition]
703
+ );
704
+ useEffect3(() => {
705
+ if (!targetKey) {
706
+ release();
707
+ appliedTargetKeyRef.current = null;
708
+ observedTargetKeyRef.current = null;
709
+ return;
710
+ }
711
+ if (!pinTarget) {
712
+ if (pinActiveRef.current && appliedTargetKeyRef.current != null && appliedTargetKeyRef.current !== targetKey) {
713
+ release();
714
+ }
715
+ observedTargetKeyRef.current = targetKey;
716
+ return;
717
+ }
718
+ if (appliedTargetKeyRef.current === targetKey) {
719
+ observedTargetKeyRef.current = targetKey;
720
+ return;
721
+ }
722
+ if (observedTargetKeyRef.current === targetKey) return;
723
+ appliedTargetKeyRef.current = targetKey;
724
+ observedTargetKeyRef.current = targetKey;
725
+ pinActiveRef.current = true;
726
+ stopAutoScroll();
727
+ scheduleReposition();
728
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
729
+ retryTimeoutRef.current = window.setTimeout(() => {
730
+ retryTimeoutRef.current = null;
731
+ scheduleReposition();
732
+ }, 80);
733
+ }, [pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
734
+ useEffect3(() => {
735
+ if (layoutKey !== void 0) scheduleReposition();
736
+ }, [layoutKey, scheduleReposition]);
737
+ useEffect3(() => {
738
+ const scroll = getScrollElement();
739
+ if (!scroll) return;
740
+ const content = getContentElement?.();
741
+ const observer = new ResizeObserver(scheduleReposition);
742
+ observer.observe(scroll);
743
+ if (content && content !== scroll) observer.observe(content);
744
+ window.addEventListener("resize", scheduleReposition);
745
+ window.visualViewport?.addEventListener("resize", scheduleReposition);
746
+ const handleScroll = () => {
747
+ if (!pinActiveRef.current) return;
748
+ if (manualScrollCheckTimeoutRef.current != null) {
749
+ clearTimeout(manualScrollCheckTimeoutRef.current);
750
+ }
751
+ manualScrollCheckTimeoutRef.current = window.setTimeout(() => {
752
+ manualScrollCheckTimeoutRef.current = null;
753
+ if (!pinActiveRef.current || repositionPendingRef.current) return;
754
+ const targetScrollTop = pinnedTargetScrollTopRef.current;
755
+ if (targetScrollTop != null && Math.abs(scroll.scrollTop - targetScrollTop) > MANUAL_SCROLL_TOLERANCE_PX) {
756
+ release();
757
+ }
758
+ }, 0);
759
+ };
760
+ scroll.addEventListener("scroll", handleScroll, { passive: true });
761
+ return () => {
762
+ observer.disconnect();
763
+ window.removeEventListener("resize", scheduleReposition);
764
+ window.visualViewport?.removeEventListener("resize", scheduleReposition);
765
+ scroll.removeEventListener("scroll", handleScroll);
766
+ };
767
+ }, [getContentElement, getScrollElement, release, scheduleReposition]);
768
+ useEffect3(
769
+ () => () => {
770
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
771
+ frameRef.current = null;
772
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
773
+ retryTimeoutRef.current = null;
774
+ if (manualScrollCheckTimeoutRef.current != null) {
775
+ clearTimeout(manualScrollCheckTimeoutRef.current);
776
+ }
777
+ manualScrollCheckTimeoutRef.current = null;
778
+ repositionPendingRef.current = false;
779
+ pinnedTargetScrollTopRef.current = null;
780
+ appliedTargetKeyRef.current = null;
781
+ observedTargetKeyRef.current = initialObservedTargetKeyRef.current;
782
+ pinActiveRef.current = false;
783
+ setSpacerHeightRef.current(0);
784
+ },
785
+ []
786
+ );
787
+ const isActive = useCallback3(() => pinActiveRef.current, []);
788
+ return { release, isActive };
789
+ }
790
+
547
791
  // src/components/AgentChat.tsx
548
792
  import { BladeApiError, latestPostChatFollowup } from "@blade-hq/agent-client";
549
793
 
@@ -636,6 +880,18 @@ var ArrowUp = createLucideIcon("ArrowUp", [
636
880
  ["path", { d: "M12 19V5", key: "x0mq9r" }]
637
881
  ]);
638
882
 
883
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/book-open.js
884
+ var BookOpen = createLucideIcon("BookOpen", [
885
+ ["path", { d: "M12 7v14", key: "1akyts" }],
886
+ [
887
+ "path",
888
+ {
889
+ d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",
890
+ key: "ruj8y"
891
+ }
892
+ ]
893
+ ]);
894
+
639
895
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/bot.js
640
896
  var Bot = createLucideIcon("Bot", [
641
897
  ["path", { d: "M12 8V4H8", key: "hb8ula" }],
@@ -646,31 +902,6 @@ var Bot = createLucideIcon("Bot", [
646
902
  ["path", { d: "M9 13v2", key: "rq6x2g" }]
647
903
  ]);
648
904
 
649
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/brain.js
650
- var Brain = createLucideIcon("Brain", [
651
- [
652
- "path",
653
- {
654
- d: "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z",
655
- key: "l5xja"
656
- }
657
- ],
658
- [
659
- "path",
660
- {
661
- d: "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z",
662
- key: "ep3f8r"
663
- }
664
- ],
665
- ["path", { d: "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4", key: "1p4c4q" }],
666
- ["path", { d: "M17.599 6.5a3 3 0 0 0 .399-1.375", key: "tmeiqw" }],
667
- ["path", { d: "M6.003 5.125A3 3 0 0 0 6.401 6.5", key: "105sqy" }],
668
- ["path", { d: "M3.477 10.896a4 4 0 0 1 .585-.396", key: "ql3yin" }],
669
- ["path", { d: "M19.938 10.5a4 4 0 0 1 .585.396", key: "1qfode" }],
670
- ["path", { d: "M6 18a4 4 0 0 1-1.967-.516", key: "2e4loj" }],
671
- ["path", { d: "M19.967 17.484A4 4 0 0 1 18 18", key: "159ez6" }]
672
- ]);
673
-
674
905
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/check.js
675
906
  var Check = createLucideIcon("Check", [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]);
676
907
 
@@ -691,17 +922,54 @@ var CircleAlert = createLucideIcon("CircleAlert", [
691
922
  ["line", { x1: "12", x2: "12.01", y1: "16", y2: "16", key: "4dfq90" }]
692
923
  ]);
693
924
 
925
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/circle-dot.js
926
+ var CircleDot = createLucideIcon("CircleDot", [
927
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
928
+ ["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }]
929
+ ]);
930
+
931
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/circle.js
932
+ var Circle = createLucideIcon("Circle", [
933
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
934
+ ]);
935
+
694
936
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/copy.js
695
937
  var Copy = createLucideIcon("Copy", [
696
938
  ["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2", key: "17jyea" }],
697
939
  ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
698
940
  ]);
699
941
 
700
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/download.js
701
- var Download = createLucideIcon("Download", [
702
- ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
703
- ["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }],
704
- ["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }]
942
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/earth.js
943
+ var Earth = createLucideIcon("Earth", [
944
+ ["path", { d: "M21.54 15H17a2 2 0 0 0-2 2v4.54", key: "1djwo0" }],
945
+ [
946
+ "path",
947
+ {
948
+ d: "M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",
949
+ key: "1tzkfa"
950
+ }
951
+ ],
952
+ ["path", { d: "M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05", key: "14pb5j" }],
953
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
954
+ ]);
955
+
956
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-pen-line.js
957
+ var FilePenLine = createLucideIcon("FilePenLine", [
958
+ [
959
+ "path",
960
+ {
961
+ d: "m18 5-2.414-2.414A2 2 0 0 0 14.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2",
962
+ key: "142zxg"
963
+ }
964
+ ],
965
+ [
966
+ "path",
967
+ {
968
+ d: "M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z",
969
+ key: "2t3380"
970
+ }
971
+ ],
972
+ ["path", { d: "M8 18h1", key: "13wk12" }]
705
973
  ]);
706
974
 
707
975
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
@@ -713,24 +981,6 @@ var FileText = createLucideIcon("FileText", [
713
981
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
714
982
  ]);
715
983
 
716
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file.js
717
- var File = createLucideIcon("File", [
718
- ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", key: "1rqfz7" }],
719
- ["path", { d: "M14 2v4a2 2 0 0 0 2 2h4", key: "tnqrlb" }]
720
- ]);
721
-
722
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/film.js
723
- var Film = createLucideIcon("Film", [
724
- ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
725
- ["path", { d: "M7 3v18", key: "bbkbws" }],
726
- ["path", { d: "M3 7.5h4", key: "zfgn84" }],
727
- ["path", { d: "M3 12h18", key: "1i2n21" }],
728
- ["path", { d: "M3 16.5h4", key: "1230mu" }],
729
- ["path", { d: "M17 3v18", key: "in4fa5" }],
730
- ["path", { d: "M17 7.5h4", key: "myr1c1" }],
731
- ["path", { d: "M17 16.5h4", key: "go4c1d" }]
732
- ]);
733
-
734
984
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/globe.js
735
985
  var Globe = createLucideIcon("Globe", [
736
986
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
@@ -776,6 +1026,15 @@ var Lightbulb = createLucideIcon("Lightbulb", [
776
1026
  ["path", { d: "M10 22h4", key: "ceow96" }]
777
1027
  ]);
778
1028
 
1029
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/list-checks.js
1030
+ var ListChecks = createLucideIcon("ListChecks", [
1031
+ ["path", { d: "m3 17 2 2 4-4", key: "1jhpwq" }],
1032
+ ["path", { d: "m3 7 2 2 4-4", key: "1obspn" }],
1033
+ ["path", { d: "M13 6h8", key: "15sg57" }],
1034
+ ["path", { d: "M13 12h8", key: "h98zly" }],
1035
+ ["path", { d: "M13 18h8", key: "oe0vm4" }]
1036
+ ]);
1037
+
779
1038
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/loader-circle.js
780
1039
  var LoaderCircle = createLucideIcon("LoaderCircle", [
781
1040
  ["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]
@@ -806,6 +1065,20 @@ var Play = createLucideIcon("Play", [
806
1065
  ["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
807
1066
  ]);
808
1067
 
1068
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/refresh-ccw.js
1069
+ var RefreshCcw = createLucideIcon("RefreshCcw", [
1070
+ ["path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8", key: "14sxne" }],
1071
+ ["path", { d: "M3 3v5h5", key: "1xhq8a" }],
1072
+ ["path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16", key: "1hlbsb" }],
1073
+ ["path", { d: "M16 16h5v5", key: "ccwih5" }]
1074
+ ]);
1075
+
1076
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/search.js
1077
+ var Search = createLucideIcon("Search", [
1078
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
1079
+ ["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
1080
+ ]);
1081
+
809
1082
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
810
1083
  var Settings2 = createLucideIcon("Settings2", [
811
1084
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -834,6 +1107,12 @@ var Square = createLucideIcon("Square", [
834
1107
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
835
1108
  ]);
836
1109
 
1110
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/terminal.js
1111
+ var Terminal = createLucideIcon("Terminal", [
1112
+ ["polyline", { points: "4 17 10 11 4 5", key: "akl6gq" }],
1113
+ ["line", { x1: "12", x2: "20", y1: "19", y2: "19", key: "q2wloq" }]
1114
+ ]);
1115
+
837
1116
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
838
1117
  var TriangleAlert = createLucideIcon("TriangleAlert", [
839
1118
  [
@@ -847,6 +1126,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
847
1126
  ["path", { d: "M12 17h.01", key: "p32p05" }]
848
1127
  ]);
849
1128
 
1129
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/wrench.js
1130
+ var Wrench = createLucideIcon("Wrench", [
1131
+ [
1132
+ "path",
1133
+ {
1134
+ d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z",
1135
+ key: "cbrjhi"
1136
+ }
1137
+ ]
1138
+ ]);
1139
+
850
1140
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
851
1141
  var X = createLucideIcon("X", [
852
1142
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -854,7 +1144,7 @@ var X = createLucideIcon("X", [
854
1144
  ]);
855
1145
 
856
1146
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo8, useState as useState13 } from "react";
1147
+ import { useCallback as useCallback8, useEffect as useEffect13, useMemo as useMemo8, useState as useState15 } from "react";
858
1148
 
859
1149
  // src/lib/utils.ts
860
1150
  function cn(...inputs) {
@@ -981,87 +1271,491 @@ function ReplayMismatchPrompt({ mismatch, className }) {
981
1271
  );
982
1272
  }
983
1273
 
984
- // src/components/ChatInput.tsx
1274
+ // src/components/PlanUpdateBlock.tsx
1275
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1276
+
1277
+ // src/components/display-utils.ts
1278
+ var TOOL_NAME_ALIASES = {
1279
+ agent: "Agent",
1280
+ ask_user_question: "AskUserQuestion",
1281
+ bash: "Bash",
1282
+ bg_bash: "BgBash",
1283
+ edit: "Edit",
1284
+ exit_plan_mode: "ExitPlanMode",
1285
+ file_edit: "Edit",
1286
+ file_read: "Read",
1287
+ file_write: "Write",
1288
+ finish_task: "FinishTask",
1289
+ glob: "Glob",
1290
+ grep: "Grep",
1291
+ kb_search: "KbSearch",
1292
+ ls: "Ls",
1293
+ multi_edit: "MultiEdit",
1294
+ read: "Read",
1295
+ read_skill: "ReadSkill",
1296
+ update_plan: "UpdatePlan",
1297
+ web_fetch: "WebFetch",
1298
+ web_search: "WebSearch",
1299
+ write: "Write"
1300
+ };
1301
+ var TOOL_DISPLAY_LABELS = {
1302
+ Bash: "\u6267\u884C\u547D\u4EE4",
1303
+ BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1304
+ Read: "\u8BFB\u53D6\u6587\u4EF6",
1305
+ Write: "\u5199\u5165\u6587\u4EF6",
1306
+ Edit: "\u7F16\u8F91\u6587\u4EF6",
1307
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1308
+ Ls: "\u5217\u51FA\u76EE\u5F55",
1309
+ Glob: "\u5339\u914D\u6587\u4EF6",
1310
+ Grep: "\u641C\u7D22\u6587\u672C",
1311
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1312
+ WebSearch: "\u641C\u7D22\u7F51\u9875",
1313
+ WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1314
+ Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1315
+ AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1316
+ ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1317
+ FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1318
+ ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1319
+ ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1320
+ GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1321
+ };
1322
+ function safeParseJson(value) {
1323
+ if (!value) return null;
1324
+ try {
1325
+ return JSON.parse(value);
1326
+ } catch {
1327
+ return null;
1328
+ }
1329
+ }
1330
+ function getStringArgValue(args, key) {
1331
+ const value = args?.[key];
1332
+ return typeof value === "string" ? value.trim() : "";
1333
+ }
1334
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1335
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1336
+ function getSkillNameFromFilePath(filePath) {
1337
+ if (!filePath) return null;
1338
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1339
+ const fileName = segments.pop();
1340
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1341
+ const dirName = segments.pop();
1342
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1343
+ return dirName;
1344
+ }
1345
+ function formatToolName(name) {
1346
+ const trimmed = name.trim();
1347
+ if (!trimmed) return name;
1348
+ const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1349
+ const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1350
+ return TOOL_NAME_ALIASES[normalized] ?? stripped;
1351
+ }
1352
+ function getToolDisplayLabel(toolCall) {
1353
+ const normalized = formatToolName(toolCall.name);
1354
+ const args = safeParseJson(toolCall.arguments);
1355
+ const displayName = toolCall.display_name?.trim() ?? "";
1356
+ const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1357
+ const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1358
+ if (metaDisplayName) {
1359
+ return metaDisplayName;
1360
+ }
1361
+ const description = getStringArgValue(args, "description");
1362
+ if (normalized === "BgBash") {
1363
+ return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1364
+ }
1365
+ if (normalized === "ReadSkill") {
1366
+ const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1367
+ return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1368
+ }
1369
+ if (normalized === "Read") {
1370
+ const skillName = getSkillNameFromFilePath(
1371
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1372
+ );
1373
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1374
+ }
1375
+ if (normalized === "FinishTask") {
1376
+ const title = getStringArgValue(args, "title");
1377
+ return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1378
+ }
1379
+ return description || baseLabel;
1380
+ }
1381
+ function getToolTone(status) {
1382
+ if (status === "error" || status === "cancelled") return "red";
1383
+ if (status === "awaiting_answer") return "amber";
1384
+ if (status === "pending") return "blue";
1385
+ return "emerald";
1386
+ }
1387
+ function getToolStatusLabel(status) {
1388
+ if (status === "pending") return "\u8FD0\u884C\u4E2D";
1389
+ if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1390
+ if (status === "error") return "\u9519\u8BEF";
1391
+ if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1392
+ return "\u5B8C\u6210";
1393
+ }
1394
+ function formatToolDuration(ms) {
1395
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
1396
+ const seconds = ms / 1e3;
1397
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
1398
+ const minutes = Math.floor(seconds / 60);
1399
+ const remainingSeconds = Math.round(seconds % 60);
1400
+ return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
1401
+ }
1402
+ function formatToolArgs(args) {
1403
+ try {
1404
+ return JSON.stringify(JSON.parse(args), null, 2);
1405
+ } catch {
1406
+ return args;
1407
+ }
1408
+ }
1409
+ var RESULT_PREVIEW_LIMIT = 4e3;
1410
+ function formatToolResult(result) {
1411
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1412
+ if (text == null) return "";
1413
+ if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1414
+ return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1415
+ \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
1416
+ }
1417
+
1418
+ // src/components/PlanUpdateBlock.tsx
985
1419
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
986
- function ChatInput({
987
- value,
988
- onValueChange,
989
- onSend,
990
- onStop,
991
- isStreaming,
992
- isStopping = false,
993
- placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
994
- className
995
- }) {
996
- const trimmed = value.trim();
997
- const canSend = trimmed.length > 0 && !isStreaming;
998
- const handleSend = async () => {
999
- if (!canSend) return;
1000
- const accepted = await onSend(trimmed);
1001
- if (!accepted) return;
1002
- onValueChange("");
1003
- };
1004
- const handleKeyDown = (event) => {
1005
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1006
- event.preventDefault();
1007
- void handleSend();
1420
+ var PLAN_STEP_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
1421
+ var PLAN_AUTO_COLLAPSE_MS = 5e3;
1422
+ function isPlanUpdateTool(toolCall) {
1423
+ return formatToolName(toolCall.name) === "UpdatePlan";
1424
+ }
1425
+ function getPlanUpdateDisplayState(messages) {
1426
+ let current = null;
1427
+ let latestAttempt = null;
1428
+ let latestAttemptStreaming = false;
1429
+ for (const message of messages) {
1430
+ if ((message.loop_name ?? "root") !== "root") continue;
1431
+ for (const toolCall of message.tool_calls ?? []) {
1432
+ if (!isPlanUpdateTool(toolCall)) continue;
1433
+ latestAttempt = toolCall;
1434
+ latestAttemptStreaming = message.status === "streaming";
1435
+ if (toolCall.status === "done" && parsePlanUpdate(toolCall.arguments)) current = toolCall;
1008
1436
  }
1437
+ }
1438
+ return {
1439
+ current,
1440
+ updating: latestAttempt?.status === "pending" && latestAttemptStreaming
1009
1441
  };
1010
- return /* @__PURE__ */ jsx4("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: /* @__PURE__ */ jsxs3("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: [
1011
- /* @__PURE__ */ jsx4(
1012
- "textarea",
1013
- {
1014
- value,
1015
- onChange: (event) => onValueChange(event.target.value),
1016
- onKeyDown: handleKeyDown,
1017
- onInput: (event) => {
1018
- const el = event.currentTarget;
1019
- el.style.height = "auto";
1020
- el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
1021
- },
1022
- rows: 1,
1023
- placeholder,
1024
- "aria-label": "\u804A\u5929\u8F93\u5165",
1025
- 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)]"
1442
+ }
1443
+ function parsePlanUpdate(argumentsJson) {
1444
+ try {
1445
+ const raw = JSON.parse(argumentsJson);
1446
+ if (!raw || typeof raw !== "object") return null;
1447
+ const candidate = raw;
1448
+ if (!Array.isArray(candidate.plan)) return null;
1449
+ const plan = candidate.plan.map((item) => {
1450
+ if (!item || typeof item !== "object") return null;
1451
+ const step = item.step;
1452
+ const status = item.status;
1453
+ if (typeof step !== "string" || !step.trim() || typeof status !== "string" || !PLAN_STEP_STATUSES.has(status)) {
1454
+ return null;
1026
1455
  }
1027
- ),
1028
- isStreaming ? /* @__PURE__ */ jsx4(
1029
- "button",
1456
+ return { step: step.trim(), status };
1457
+ });
1458
+ if (plan.some((item) => item === null)) return null;
1459
+ if (plan.filter((item) => item?.status === "in_progress").length > 1) return null;
1460
+ return { plan };
1461
+ } catch {
1462
+ return null;
1463
+ }
1464
+ }
1465
+ function pickCurrentPlanStep(plan) {
1466
+ return plan.find((item) => item.status === "in_progress") ?? plan.find((item) => item.status === "pending") ?? plan[plan.length - 1] ?? null;
1467
+ }
1468
+ function PlanStepIcon({
1469
+ status,
1470
+ size = 17,
1471
+ running = false
1472
+ }) {
1473
+ if (status === "completed") {
1474
+ return /* @__PURE__ */ jsx4(Check, { size, strokeWidth: 2, className: "shrink-0 text-emerald-500" });
1475
+ }
1476
+ if (status === "in_progress") {
1477
+ return running ? /* @__PURE__ */ jsx4(LoaderCircle, { size, className: "shrink-0 animate-spin text-[hsl(var(--muted-foreground))]" }) : /* @__PURE__ */ jsx4(CircleDot, { size, className: "shrink-0 text-amber-500" });
1478
+ }
1479
+ return /* @__PURE__ */ jsx4(Circle, { size, className: "shrink-0 text-[hsl(var(--muted-foreground))]/60" });
1480
+ }
1481
+ function PlanUpdateBlock({
1482
+ toolCall,
1483
+ running = false,
1484
+ autoReveal = false
1485
+ }) {
1486
+ const updateKey = `${toolCall.id}:${toolCall.arguments}`;
1487
+ const revealKey = autoReveal ? updateKey : null;
1488
+ const [collapsed, setCollapsed] = useState4(!autoReveal);
1489
+ const collapseTimerRef = useRef4(null);
1490
+ const data = parsePlanUpdate(toolCall.arguments);
1491
+ useEffect4(() => {
1492
+ if (!revealKey) return;
1493
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1494
+ setCollapsed(false);
1495
+ collapseTimerRef.current = setTimeout(() => {
1496
+ setCollapsed(true);
1497
+ collapseTimerRef.current = null;
1498
+ }, PLAN_AUTO_COLLAPSE_MS);
1499
+ }, [revealKey]);
1500
+ useEffect4(
1501
+ () => () => {
1502
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1503
+ },
1504
+ []
1505
+ );
1506
+ if (!data) return null;
1507
+ const completed = data.plan.filter((item) => item.status === "completed").length;
1508
+ const currentStep = pickCurrentPlanStep(data.plan);
1509
+ const pausedAtCurrentStep = !running && currentStep?.status === "in_progress";
1510
+ return /* @__PURE__ */ jsxs3("section", { className: "overflow-hidden", children: [
1511
+ /* @__PURE__ */ jsxs3(
1512
+ "button",
1030
1513
  {
1031
1514
  type: "button",
1032
- onClick: onStop,
1033
- disabled: isStopping,
1034
- "aria-label": isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
1035
- title: isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
1036
- 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",
1037
- children: isStopping ? /* @__PURE__ */ jsx4(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx4(Square, { size: 12, fill: "currentColor" })
1515
+ "aria-expanded": !collapsed,
1516
+ onClick: () => {
1517
+ if (collapseTimerRef.current) {
1518
+ clearTimeout(collapseTimerRef.current);
1519
+ collapseTimerRef.current = null;
1520
+ }
1521
+ setCollapsed((value) => !value);
1522
+ },
1523
+ className: cn(
1524
+ "flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[hsl(var(--muted)/0.3)]",
1525
+ !collapsed && "border-b border-[hsl(var(--border))]"
1526
+ ),
1527
+ children: [
1528
+ collapsed && currentStep ? /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs text-[hsl(var(--foreground))]", children: [
1529
+ /* @__PURE__ */ jsx4(PlanStepIcon, { status: currentStep.status, size: 14, running }),
1530
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: currentStep.step }),
1531
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[11px] text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1532
+ ] }) : /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-[11px] text-[hsl(var(--muted-foreground))]", children: [
1533
+ /* @__PURE__ */ jsx4(ListChecks, { size: 14, className: "shrink-0", "aria-hidden": "true" }),
1534
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: "\u4EFB\u52A1\u8FDB\u5EA6" }),
1535
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1536
+ ] }),
1537
+ /* @__PURE__ */ jsxs3("span", { className: "shrink-0 text-[11px] tabular-nums text-[hsl(var(--muted-foreground))]", children: [
1538
+ completed,
1539
+ "/",
1540
+ data.plan.length
1541
+ ] }),
1542
+ /* @__PURE__ */ jsx4(
1543
+ ChevronDown,
1544
+ {
1545
+ size: 14,
1546
+ className: cn(
1547
+ "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform duration-300 ease-out motion-reduce:transition-none",
1548
+ !collapsed && "rotate-180"
1549
+ )
1550
+ }
1551
+ )
1552
+ ]
1038
1553
  }
1039
- ) : /* @__PURE__ */ jsx4(
1040
- "button",
1554
+ ),
1555
+ /* @__PURE__ */ jsx4(
1556
+ "div",
1041
1557
  {
1042
- type: "button",
1043
- onClick: handleSend,
1044
- disabled: !canSend,
1045
- "aria-label": "\u53D1\u9001\u6D88\u606F",
1046
- title: "\u53D1\u9001\u6D88\u606F",
1047
- 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",
1048
- children: /* @__PURE__ */ jsx4(ArrowUp, { size: 15 })
1558
+ "aria-hidden": collapsed,
1559
+ className: cn(
1560
+ "grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none",
1561
+ collapsed ? "grid-rows-[0fr] opacity-0" : "grid-rows-[1fr] opacity-100"
1562
+ ),
1563
+ children: /* @__PURE__ */ jsx4("div", { className: "min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx4("div", { className: "flex max-h-40 flex-col gap-0.5 overflow-y-auto px-3 py-2", children: data.plan.length === 0 ? /* @__PURE__ */ jsx4("span", { className: "text-xs text-[hsl(var(--muted-foreground))]", children: "\u6682\u65E0\u4EFB\u52A1\u6B65\u9AA4" }) : data.plan.map((item, index) => /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2 py-0.5", children: [
1564
+ /* @__PURE__ */ jsx4("span", { className: "mt-[3px] flex shrink-0", children: /* @__PURE__ */ jsx4(PlanStepIcon, { status: item.status, size: 14, running }) }),
1565
+ /* @__PURE__ */ jsx4(
1566
+ "span",
1567
+ {
1568
+ className: cn(
1569
+ "min-w-0 flex-1 break-words text-[13px] leading-5",
1570
+ item.status === "completed" ? "text-[hsl(var(--muted-foreground))]" : item.status === "in_progress" ? "font-medium text-[hsl(var(--foreground))]" : "text-[hsl(var(--muted-foreground))]"
1571
+ ),
1572
+ children: item.step
1573
+ }
1574
+ )
1575
+ ] }, `${index}-${item.step}`)) }) })
1049
1576
  }
1050
1577
  )
1051
- ] }) });
1578
+ ] });
1579
+ }
1580
+ function CurrentPlanPanel({
1581
+ messages,
1582
+ running = false,
1583
+ revealRevision = 0,
1584
+ sessionId,
1585
+ className
1586
+ }) {
1587
+ const { current, updating } = getPlanUpdateDisplayState(messages);
1588
+ const revealBaselinesRef = useRef4(/* @__PURE__ */ new Map([[sessionId, revealRevision]]));
1589
+ const autoReveal = (revealBaselinesRef.current.get(sessionId) ?? 0) !== revealRevision;
1590
+ useEffect4(() => {
1591
+ if (!current) return;
1592
+ revealBaselinesRef.current.set(sessionId, revealRevision);
1593
+ }, [current, revealRevision, sessionId]);
1594
+ if (!current && !updating) return null;
1595
+ return /* @__PURE__ */ jsxs3("div", { className: cn("blade-chat-plan mx-auto w-full max-w-[748px] px-4", className), children: [
1596
+ updating ? /* @__PURE__ */ jsxs3("div", { className: "mb-2 flex items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
1597
+ /* @__PURE__ */ jsx4(LoaderCircle, { size: 14, className: "shrink-0 animate-spin" }),
1598
+ /* @__PURE__ */ jsx4("span", { children: "\u6B63\u5728\u66F4\u65B0\u4EFB\u52A1\u8FDB\u5EA6\u2026" })
1599
+ ] }) : null,
1600
+ current ? /* @__PURE__ */ jsx4(
1601
+ PlanUpdateBlock,
1602
+ {
1603
+ toolCall: current,
1604
+ running,
1605
+ autoReveal
1606
+ },
1607
+ sessionId ?? "current-session"
1608
+ ) : null
1609
+ ] });
1610
+ }
1611
+
1612
+ // src/components/ChatSurface.tsx
1613
+ import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1614
+
1615
+ // src/components/ChatInput.tsx
1616
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
1617
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1618
+ function isImeCompositionKey(event) {
1619
+ return event.isComposing || event.keyCode === 229;
1620
+ }
1621
+ function shouldSubmitChatInput(event, menuOpen) {
1622
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1623
+ }
1624
+ function ChatInput({
1625
+ value,
1626
+ onValueChange,
1627
+ onSend,
1628
+ onAppend,
1629
+ onStop,
1630
+ isStreaming,
1631
+ isStopping = false,
1632
+ placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
1633
+ className,
1634
+ queueKey
1635
+ }) {
1636
+ const trimmed = value.trim();
1637
+ const [sendMode, setSendMode] = useState5("direct");
1638
+ const [promptQueue, setPromptQueue] = useState5([]);
1639
+ const queueSendingRef = useRef5(false);
1640
+ const queueBlockedRef = useRef5(false);
1641
+ const previousQueueKeyRef = useRef5(queueKey);
1642
+ useEffect5(() => {
1643
+ if (previousQueueKeyRef.current === queueKey) return;
1644
+ previousQueueKeyRef.current = queueKey;
1645
+ setPromptQueue([]);
1646
+ queueBlockedRef.current = false;
1647
+ }, [queueKey]);
1648
+ const canSend = trimmed.length > 0 && (!isStreaming || sendMode === "queue");
1649
+ useEffect5(() => {
1650
+ if (isStreaming) {
1651
+ queueBlockedRef.current = false;
1652
+ return;
1653
+ }
1654
+ if (queueBlockedRef.current || promptQueue.length === 0 || queueSendingRef.current) return;
1655
+ const next = promptQueue[0];
1656
+ queueSendingRef.current = true;
1657
+ Promise.resolve().then(() => onSend(next)).then((accepted) => {
1658
+ if (accepted) setPromptQueue((current) => current.slice(1));
1659
+ else queueBlockedRef.current = true;
1660
+ }).finally(() => {
1661
+ queueSendingRef.current = false;
1662
+ });
1663
+ }, [isStreaming, onSend, promptQueue]);
1664
+ const handleSend = async () => {
1665
+ if (!canSend) return;
1666
+ if (isStreaming && sendMode === "queue") {
1667
+ setPromptQueue((current) => [...current, trimmed]);
1668
+ onValueChange("");
1669
+ return;
1670
+ }
1671
+ if (isStreaming && sendMode === "direct") {
1672
+ if (!onAppend) return;
1673
+ onAppend(trimmed);
1674
+ onValueChange("");
1675
+ return;
1676
+ }
1677
+ const accepted = await onSend(trimmed);
1678
+ if (!accepted) return;
1679
+ onValueChange("");
1680
+ };
1681
+ const handleKeyDown = (event) => {
1682
+ if (shouldSubmitChatInput({
1683
+ key: event.key,
1684
+ shiftKey: event.shiftKey,
1685
+ isComposing: event.nativeEvent.isComposing,
1686
+ keyCode: event.nativeEvent.keyCode
1687
+ }, false)) {
1688
+ event.preventDefault();
1689
+ void handleSend();
1690
+ }
1691
+ };
1692
+ return /* @__PURE__ */ jsxs4("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: [
1693
+ /* @__PURE__ */ jsxs4("div", { className: "mx-auto mb-2 flex max-w-[748px] items-center justify-between px-1 text-xs text-[hsl(var(--muted-foreground))]", children: [
1694
+ /* @__PURE__ */ jsxs4("fieldset", { className: "flex items-center gap-1 rounded-md border border-[hsl(var(--border))] p-0.5", children: [
1695
+ /* @__PURE__ */ jsx5("legend", { className: "sr-only", children: "\u53D1\u9001\u65B9\u5F0F" }),
1696
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("direct"), "aria-pressed": sendMode === "direct", disabled: isStreaming && !onAppend, className: `rounded px-2 py-1 ${sendMode === "direct" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u76F4\u63A5\u63D2\u5165" }),
1697
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setSendMode("queue"), "aria-pressed": sendMode === "queue", className: `rounded px-2 py-1 ${sendMode === "queue" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u6392\u961F\u6267\u884C" })
1698
+ ] }),
1699
+ promptQueue.length > 0 ? /* @__PURE__ */ jsxs4("details", { className: "relative", children: [
1700
+ /* @__PURE__ */ jsxs4("summary", { className: "cursor-pointer list-none rounded px-2 py-1 hover:bg-[hsl(var(--accent))]", children: [
1701
+ "\u5F85\u6267\u884C ",
1702
+ promptQueue.length,
1703
+ " \u6761"
1704
+ ] }),
1705
+ /* @__PURE__ */ jsx5("div", { className: "absolute bottom-full right-0 z-20 mb-2 w-64 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-2 shadow-lg", children: promptQueue.map((item, index) => /* @__PURE__ */ jsxs4("div", { className: "flex gap-2 border-b border-[hsl(var(--border))] py-2 last:border-0", children: [
1706
+ /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate", children: item }),
1707
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: () => setPromptQueue((current) => current.filter((_, i) => i !== index)), children: "\u53D6\u6D88" })
1708
+ ] }, `${index}-${item}`)) })
1709
+ ] }) : null
1710
+ ] }),
1711
+ /* @__PURE__ */ jsxs4("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: [
1712
+ /* @__PURE__ */ jsx5(
1713
+ "textarea",
1714
+ {
1715
+ value,
1716
+ onChange: (event) => onValueChange(event.target.value),
1717
+ onKeyDown: handleKeyDown,
1718
+ onInput: (event) => {
1719
+ const el = event.currentTarget;
1720
+ el.style.height = "auto";
1721
+ el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
1722
+ },
1723
+ rows: 1,
1724
+ placeholder,
1725
+ "aria-label": "\u804A\u5929\u8F93\u5165",
1726
+ 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)]"
1727
+ }
1728
+ ),
1729
+ isStreaming ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1730
+ sendMode === "queue" ? /* @__PURE__ */ jsx5("button", { type: "button", onClick: handleSend, disabled: !canSend, "aria-label": "\u52A0\u5165\u6392\u961F", title: "\u52A0\u5165\u6392\u961F", className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] disabled:opacity-40", children: /* @__PURE__ */ jsx5(ArrowUp, { size: 15 }) }) : null,
1731
+ /* @__PURE__ */ jsx5("button", { type: "button", onClick: onStop, disabled: isStopping, "aria-label": isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D", title: isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D", 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", children: isStopping ? /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx5(Square, { size: 12, fill: "currentColor" }) })
1732
+ ] }) : /* @__PURE__ */ jsx5(
1733
+ "button",
1734
+ {
1735
+ type: "button",
1736
+ onClick: handleSend,
1737
+ disabled: !canSend,
1738
+ "aria-label": "\u53D1\u9001\u6D88\u606F",
1739
+ title: "\u53D1\u9001\u6D88\u606F",
1740
+ 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",
1741
+ children: /* @__PURE__ */ jsx5(ArrowUp, { size: 15 })
1742
+ }
1743
+ )
1744
+ ] })
1745
+ ] });
1052
1746
  }
1053
1747
 
1054
1748
  // src/components/ConnectionBanner.tsx
1055
- import { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
1056
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1749
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState6 } from "react";
1750
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1057
1751
  var CONNECTION_NOTICE_DELAY_MS = 3e3;
1058
1752
  var CONNECTION_ERROR_DELAY_MS = 15e3;
1059
1753
  function useConnectionNoticePhase(connected) {
1060
- const [phase, setPhase] = useState4("hidden");
1061
- const connectedRef = useRef3(connected);
1062
- const timersRef = useRef3([]);
1754
+ const [phase, setPhase] = useState6("hidden");
1755
+ const connectedRef = useRef6(connected);
1756
+ const timersRef = useRef6([]);
1063
1757
  connectedRef.current = connected;
1064
- useEffect3(() => {
1758
+ useEffect6(() => {
1065
1759
  const clearTimers = () => {
1066
1760
  for (const timer of timersRef.current) clearTimeout(timer);
1067
1761
  timersRef.current = [];
@@ -1101,14 +1795,14 @@ function useConnectionNoticePhase(connected) {
1101
1795
  return phase;
1102
1796
  }
1103
1797
  function ConnectionBanner({ connection, className }) {
1104
- const hasConnectedRef = useRef3(connection === "connected" || connection === "reconnecting");
1798
+ const hasConnectedRef = useRef6(connection === "connected" || connection === "reconnecting");
1105
1799
  if (connection === "connected") hasConnectedRef.current = true;
1106
1800
  const connected = connection === "connected";
1107
1801
  const phase = useConnectionNoticePhase(connected);
1108
1802
  if (connected || phase === "hidden") return null;
1109
1803
  const recovering = phase === "recovering";
1110
1804
  const firstConnection = !hasConnectedRef.current;
1111
- return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1805
+ return /* @__PURE__ */ jsx6("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs5(
1112
1806
  "div",
1113
1807
  {
1114
1808
  className: cn(
@@ -1116,10 +1810,10 @@ function ConnectionBanner({ connection, className }) {
1116
1810
  recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1117
1811
  ),
1118
1812
  children: [
1119
- /* @__PURE__ */ jsx5("span", { className: "mt-0.5 shrink-0", children: recovering ? /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx5(TriangleAlert, { size: 14 }) }),
1120
- /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
1121
- /* @__PURE__ */ jsx5("div", { className: "text-sm font-medium", children: recovering ? firstConnection ? "\u6B63\u5728\u8FDE\u63A5\u2026" : "\u6B63\u5728\u6062\u590D\u8FDE\u63A5\u2026" : "\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5" }),
1122
- /* @__PURE__ */ jsx5("div", { className: "text-xs opacity-80", children: recovering ? "\u6062\u590D\u540E\u4F1A\u81EA\u52A8\u540C\u6B65\u6700\u65B0\u6D88\u606F\uFF0C\u8BF7\u7A0D\u5019" : "\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u670D\u52A1\u72B6\u6001\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
1813
+ /* @__PURE__ */ jsx6("span", { className: "mt-0.5 shrink-0", children: recovering ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx6(TriangleAlert, { size: 14 }) }),
1814
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1815
+ /* @__PURE__ */ jsx6("div", { className: "text-sm font-medium", children: recovering ? firstConnection ? "\u6B63\u5728\u8FDE\u63A5\u2026" : "\u6B63\u5728\u6062\u590D\u8FDE\u63A5\u2026" : "\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5" }),
1816
+ /* @__PURE__ */ jsx6("div", { className: "text-xs opacity-80", children: recovering ? "\u6062\u590D\u540E\u4F1A\u81EA\u52A8\u540C\u6B65\u6700\u65B0\u6D88\u606F\uFF0C\u8BF7\u7A0D\u5019" : "\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u670D\u52A1\u72B6\u6001\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
1123
1817
  ] })
1124
1818
  ]
1125
1819
  }
@@ -1128,10 +1822,10 @@ function ConnectionBanner({ connection, className }) {
1128
1822
 
1129
1823
  // src/components/MessageList.tsx
1130
1824
  import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1131
- import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef8, useState as useState12 } from "react";
1825
+ import { useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef13, useState as useState14 } from "react";
1132
1826
 
1133
1827
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1134
- import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
1828
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef7, useState as useState7 } from "react";
1135
1829
  var DEFAULT_SPRING_ANIMATION = {
1136
1830
  /**
1137
1831
  * A value from 0 to 1, on how much to damp the animation.
@@ -1168,12 +1862,12 @@ globalThis.document?.addEventListener("click", () => {
1168
1862
  mouseDown = false;
1169
1863
  });
1170
1864
  var useStickToBottom = (options = {}) => {
1171
- const [escapedFromLock, updateEscapedFromLock] = useState5(false);
1172
- const [isAtBottom, updateIsAtBottom] = useState5(options.initial !== false);
1173
- const [isNearBottom, setIsNearBottom] = useState5(false);
1174
- const optionsRef = useRef4(null);
1865
+ const [escapedFromLock, updateEscapedFromLock] = useState7(false);
1866
+ const [isAtBottom, updateIsAtBottom] = useState7(options.initial !== false);
1867
+ const [isNearBottom, setIsNearBottom] = useState7(false);
1868
+ const optionsRef = useRef7(null);
1175
1869
  optionsRef.current = options;
1176
- const isSelecting = useCallback3(() => {
1870
+ const isSelecting = useCallback4(() => {
1177
1871
  if (!mouseDown) {
1178
1872
  return false;
1179
1873
  }
@@ -1184,11 +1878,11 @@ var useStickToBottom = (options = {}) => {
1184
1878
  const range = selection.getRangeAt(0);
1185
1879
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1186
1880
  }, []);
1187
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1881
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1188
1882
  state.isAtBottom = isAtBottom2;
1189
1883
  updateIsAtBottom(isAtBottom2);
1190
1884
  }, []);
1191
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1885
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1192
1886
  state.escapedFromLock = escapedFromLock2;
1193
1887
  updateEscapedFromLock(escapedFromLock2);
1194
1888
  }, []);
@@ -1245,7 +1939,7 @@ var useStickToBottom = (options = {}) => {
1245
1939
  }
1246
1940
  };
1247
1941
  }, []);
1248
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1942
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1249
1943
  if (typeof scrollOptions === "string") {
1250
1944
  scrollOptions = { animation: scrollOptions };
1251
1945
  }
@@ -1330,11 +2024,11 @@ var useStickToBottom = (options = {}) => {
1330
2024
  }
1331
2025
  return next();
1332
2026
  }, [setIsAtBottom, isSelecting, state]);
1333
- const stopScroll = useCallback3(() => {
2027
+ const stopScroll = useCallback4(() => {
1334
2028
  setEscapedFromLock(true);
1335
2029
  setIsAtBottom(false);
1336
2030
  }, [setEscapedFromLock, setIsAtBottom]);
1337
- const handleScroll = useCallback3(({ target }) => {
2031
+ const handleScroll = useCallback4(({ target }) => {
1338
2032
  if (target !== scrollRef.current) {
1339
2033
  return;
1340
2034
  }
@@ -1373,7 +2067,7 @@ var useStickToBottom = (options = {}) => {
1373
2067
  }
1374
2068
  }, 1);
1375
2069
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1376
- const handleWheel = useCallback3(({ target, deltaY }) => {
2070
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1377
2071
  let element = target;
1378
2072
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1379
2073
  if (!element.parentElement) {
@@ -1443,7 +2137,7 @@ var useStickToBottom = (options = {}) => {
1443
2137
  };
1444
2138
  };
1445
2139
  function useRefCallback(callback, deps) {
1446
- const result = useCallback3((ref) => {
2140
+ const result = useCallback4((ref) => {
1447
2141
  result.current = ref;
1448
2142
  return callback(ref);
1449
2143
  }, deps);
@@ -1475,11 +2169,11 @@ function mergeAnimations(...animations) {
1475
2169
 
1476
2170
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1477
2171
  import * as React from "react";
1478
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect4, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef5 } from "react";
2172
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef8 } from "react";
1479
2173
  var StickToBottomContext = createContext2(null);
1480
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect4;
2174
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect7;
1481
2175
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1482
- const customTargetScrollTop = useRef5(null);
2176
+ const customTargetScrollTop = useRef8(null);
1483
2177
  const targetScrollTop = React.useCallback((target, elements) => {
1484
2178
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1485
2179
  return get?.(target, elements) ?? target;
@@ -1555,133 +2249,17 @@ function useStickToBottomContext() {
1555
2249
  }
1556
2250
 
1557
2251
  // src/components/AssistantTurnBlock.tsx
1558
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1559
- import { useState as useState10 } from "react";
1560
-
1561
- // src/components/AgentLoopBlock.tsx
1562
- import { useState as useState6 } from "react";
1563
-
1564
- // src/components/display-utils.ts
1565
- var TOOL_NAME_ALIASES = {
1566
- agent: "Agent",
1567
- ask_user_question: "AskUserQuestion",
1568
- bash: "Bash",
1569
- bg_bash: "BgBash",
1570
- edit: "Edit",
1571
- exit_plan_mode: "ExitPlanMode",
1572
- file_edit: "Edit",
1573
- file_read: "Read",
1574
- file_write: "Write",
1575
- finish_task: "FinishTask",
1576
- glob: "Glob",
1577
- grep: "Grep",
1578
- ls: "Ls",
1579
- read: "Read",
1580
- read_skill: "ReadSkill",
1581
- web_fetch: "WebFetch",
1582
- web_search: "WebSearch",
1583
- write: "Write"
1584
- };
1585
- var TOOL_DISPLAY_LABELS = {
1586
- Bash: "\u6267\u884C\u547D\u4EE4",
1587
- BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1588
- Read: "\u8BFB\u53D6\u6587\u4EF6",
1589
- Write: "\u5199\u5165\u6587\u4EF6",
1590
- Edit: "\u7F16\u8F91\u6587\u4EF6",
1591
- Ls: "\u5217\u51FA\u76EE\u5F55",
1592
- Glob: "\u5339\u914D\u6587\u4EF6",
1593
- Grep: "\u641C\u7D22\u6587\u672C",
1594
- WebSearch: "\u641C\u7D22\u7F51\u9875",
1595
- WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1596
- Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1597
- AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1598
- ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1599
- FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1600
- ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1601
- ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1602
- GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1603
- };
1604
- function safeParseJson(value) {
1605
- if (!value) return null;
1606
- try {
1607
- return JSON.parse(value);
1608
- } catch {
1609
- return null;
1610
- }
1611
- }
1612
- function getStringArgValue(args, key) {
1613
- const value = args?.[key];
1614
- return typeof value === "string" ? value.trim() : "";
1615
- }
1616
- function formatToolName(name) {
1617
- const trimmed = name.trim();
1618
- if (!trimmed) return name;
1619
- const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1620
- const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1621
- return TOOL_NAME_ALIASES[normalized] ?? stripped;
1622
- }
1623
- function getToolDisplayLabel(toolCall) {
1624
- const normalized = formatToolName(toolCall.name);
1625
- const args = safeParseJson(toolCall.arguments);
1626
- const displayName = toolCall.display_name?.trim() ?? "";
1627
- const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1628
- const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1629
- if (metaDisplayName) {
1630
- return metaDisplayName;
1631
- }
1632
- const description = getStringArgValue(args, "description");
1633
- if (normalized === "BgBash") {
1634
- return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1635
- }
1636
- if (normalized === "ReadSkill") {
1637
- const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1638
- return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1639
- }
1640
- if (normalized === "FinishTask") {
1641
- const title = getStringArgValue(args, "title");
1642
- return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1643
- }
1644
- return description || baseLabel;
1645
- }
1646
- function getToolTone(status) {
1647
- if (status === "error" || status === "cancelled") return "red";
1648
- if (status === "awaiting_answer") return "amber";
1649
- if (status === "pending") return "blue";
1650
- return "emerald";
1651
- }
1652
- function getToolStatusLabel(status) {
1653
- if (status === "pending") return "\u8FD0\u884C\u4E2D";
1654
- if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1655
- if (status === "error") return "\u9519\u8BEF";
1656
- if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1657
- return "\u5B8C\u6210";
1658
- }
1659
- function formatToolDuration(ms) {
1660
- if (ms < 1e3) return `${Math.round(ms)}ms`;
1661
- const seconds = ms / 1e3;
1662
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
1663
- const minutes = Math.floor(seconds / 60);
1664
- const remainingSeconds = Math.round(seconds % 60);
1665
- return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
1666
- }
1667
- function formatToolArgs(args) {
1668
- try {
1669
- return JSON.stringify(JSON.parse(args), null, 2);
1670
- } catch {
1671
- return args;
1672
- }
1673
- }
1674
- var RESULT_PREVIEW_LIMIT = 4e3;
1675
- function formatToolResult(result) {
1676
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1677
- if (text == null) return "";
1678
- if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1679
- return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1680
- \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
1681
- }
2252
+ import {
2253
+ getFileParts,
2254
+ getImageParts,
2255
+ getTextContent,
2256
+ normalizeMessageContent
2257
+ } from "@blade-hq/agent-client";
2258
+ import { useEffect as useEffect10, useRef as useRef11, useState as useState12 } from "react";
1682
2259
 
1683
2260
  // src/components/AgentLoopBlock.tsx
1684
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2261
+ import { useState as useState8 } from "react";
2262
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1685
2263
  function parseAgentDescription(argumentsJson) {
1686
2264
  try {
1687
2265
  const parsed = JSON.parse(argumentsJson);
@@ -1691,83 +2269,81 @@ function parseAgentDescription(argumentsJson) {
1691
2269
  }
1692
2270
  }
1693
2271
  function AgentLoopBlock({ toolCall }) {
1694
- const [expanded, setExpanded] = useState6(false);
2272
+ const [expanded, setExpanded] = useState8(false);
1695
2273
  const description = parseAgentDescription(toolCall.arguments);
1696
2274
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1697
2275
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1698
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
1699
- /* @__PURE__ */ jsxs5(
1700
- "div",
2276
+ const hasResult = toolCall.result != null;
2277
+ const iconClass = cn(
2278
+ "size-3.5 shrink-0",
2279
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2280
+ );
2281
+ return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
2282
+ /* @__PURE__ */ jsxs6(
2283
+ "button",
1701
2284
  {
2285
+ type: "button",
2286
+ onClick: () => hasResult && setExpanded(!expanded),
2287
+ disabled: !hasResult,
2288
+ "aria-expanded": hasResult ? expanded : void 0,
2289
+ "data-testid": "execution-tool-intent",
1702
2290
  className: cn(
1703
- "border-l-[3px] flex items-center gap-2 px-3 py-2",
1704
- failed ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : running ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]"
2291
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2292
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2293
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1705
2294
  ),
2295
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1706
2296
  children: [
1707
- /* @__PURE__ */ jsxs5(
1708
- "button",
2297
+ running ? /* @__PURE__ */ jsx7(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx7(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx7(X, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx7(Bot, { className: iconClass, "aria-hidden": "true" }),
2298
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 truncate", children: [
2299
+ "\u5B50\u4EFB\u52A1\uFF1A",
2300
+ description
2301
+ ] }),
2302
+ hasResult ? /* @__PURE__ */ jsx7(
2303
+ ChevronRight,
1709
2304
  {
1710
- type: "button",
1711
- onClick: () => setExpanded(!expanded),
1712
- 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",
1713
- "aria-expanded": expanded,
1714
- children: [
1715
- /* @__PURE__ */ jsx6(
1716
- ChevronRight,
1717
- {
1718
- size: 11,
1719
- className: cn(
1720
- "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform",
1721
- expanded && "rotate-90"
1722
- )
1723
- }
1724
- ),
1725
- /* @__PURE__ */ jsx6(Bot, { size: 12, className: "shrink-0 text-[hsl(var(--muted-foreground))]" }),
1726
- /* @__PURE__ */ jsxs5(
1727
- "span",
1728
- {
1729
- className: cn(
1730
- "flex shrink-0 items-center gap-1 text-[10px]",
1731
- failed ? "text-[hsl(var(--muted-foreground))]" : running ? "text-blue-300" : "text-[hsl(var(--primary))]"
1732
- ),
1733
- children: [
1734
- running ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 11, className: "animate-spin" }) : failed ? /* @__PURE__ */ jsx6(X, { size: 11 }) : /* @__PURE__ */ jsx6(Check, { size: 11 }),
1735
- /* @__PURE__ */ jsx6("span", { children: running ? "\u6267\u884C\u4E2D" : failed ? "\u5DF2\u7EC8\u6B62" : "\u5B8C\u6210" })
1736
- ]
1737
- }
1738
- ),
1739
- /* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: [
1740
- "\u5B50\u667A\u80FD\u4F53\uFF1A",
1741
- description
1742
- ] })
1743
- ]
2305
+ size: 14,
2306
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
2307
+ className: cn(
2308
+ "shrink-0 transition-transform",
2309
+ expanded && "rotate-90"
2310
+ ),
2311
+ "aria-hidden": "true"
1744
2312
  }
1745
- ),
1746
- typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx6("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
2313
+ ) : null
1747
2314
  ]
1748
2315
  }
1749
2316
  ),
1750
- expanded && toolCall.result != null && /* @__PURE__ */ jsxs5("div", { className: "ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
1751
- /* @__PURE__ */ jsx6("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
1752
- /* @__PURE__ */ jsx6("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) })
1753
- ] })
2317
+ expanded && hasResult ? /* @__PURE__ */ jsx7("div", { className: "ml-[18px] mt-1.5 max-h-[400px] overflow-auto whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: formatToolResult(toolCall.result) }) : null
1754
2318
  ] });
1755
2319
  }
1756
2320
 
1757
2321
  // src/components/MarkdownContent.tsx
1758
2322
  import {
1759
- useEffect as useEffect5,
2323
+ useEffect as useEffect8,
1760
2324
  useMemo as useMemo5,
1761
- useRef as useRef6,
1762
- useState as useState7
2325
+ useRef as useRef9,
2326
+ useState as useState9
1763
2327
  } from "react";
1764
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2328
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1765
2329
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
2330
+ function normalizeAdjacentUrlFormatting(value) {
2331
+ const protectedSegments = [];
2332
+ const protectedValue = value.replace(/(`{1,3}[\s\S]*?`{1,3}|\[[^\]]*\]\([^)]*\))/g, (segment) => {
2333
+ const index = protectedSegments.push(segment) - 1;
2334
+ return `blade-url-protected-${index}-marker`;
2335
+ });
2336
+ const normalized = protectedValue.replace(
2337
+ /(\*\*|__|~~|\*|_)(https?:\/\/[^\s<>]+?)\1(?=[\s。,、!?;:,.!?;:]|$)/g,
2338
+ (_, marker, url) => `${marker}[${url}](<${url}>)${marker}`
2339
+ );
2340
+ return normalized.replace(/blade-url-protected-(\d+)-marker/g, (_, index) => protectedSegments[Number(index)]);
2341
+ }
1766
2342
  function CodeBlockPre({ children, node: _node, ...props }) {
1767
- const preRef = useRef6(null);
1768
- const [copied, setCopied] = useState7(false);
1769
- const [language, setLanguage] = useState7("");
1770
- useEffect5(() => {
2343
+ const preRef = useRef9(null);
2344
+ const [copied, setCopied] = useState9(false);
2345
+ const [language, setLanguage] = useState9("");
2346
+ useEffect8(() => {
1771
2347
  const codeEl = preRef.current?.querySelector("code");
1772
2348
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1773
2349
  }, []);
@@ -1778,10 +2354,10 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1778
2354
  setTimeout(() => setCopied(false), 2e3);
1779
2355
  }
1780
2356
  };
1781
- return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
1782
- /* @__PURE__ */ jsxs6("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: [
1783
- /* @__PURE__ */ jsx7("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
1784
- /* @__PURE__ */ jsxs6(
2357
+ return /* @__PURE__ */ jsxs7("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
2358
+ /* @__PURE__ */ jsxs7("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: [
2359
+ /* @__PURE__ */ jsx8("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
2360
+ /* @__PURE__ */ jsxs7(
1785
2361
  "button",
1786
2362
  {
1787
2363
  type: "button",
@@ -1791,13 +2367,13 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1791
2367
  copied ? "text-[hsl(var(--primary))]" : "text-[hsl(var(--muted-foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]"
1792
2368
  ),
1793
2369
  children: [
1794
- copied ? /* @__PURE__ */ jsx7(Check, { size: 12 }) : /* @__PURE__ */ jsx7(Copy, { size: 12 }),
1795
- /* @__PURE__ */ jsx7("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
2370
+ copied ? /* @__PURE__ */ jsx8(Check, { size: 12 }) : /* @__PURE__ */ jsx8(Copy, { size: 12 }),
2371
+ /* @__PURE__ */ jsx8("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
1796
2372
  ]
1797
2373
  }
1798
2374
  )
1799
2375
  ] }),
1800
- /* @__PURE__ */ jsx7(
2376
+ /* @__PURE__ */ jsx8(
1801
2377
  "pre",
1802
2378
  {
1803
2379
  ref: preRef,
@@ -1809,7 +2385,7 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1809
2385
  ] });
1810
2386
  }
1811
2387
  function ExternalAnchor({ node: _node, children, ...props }) {
1812
- return /* @__PURE__ */ jsx7("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
2388
+ return /* @__PURE__ */ jsx8("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
1813
2389
  }
1814
2390
  var MARKDOWN_COMPONENTS = {
1815
2391
  pre: CodeBlockPre,
@@ -1817,9 +2393,9 @@ var MARKDOWN_COMPONENTS = {
1817
2393
  };
1818
2394
  function MarkdownContent({ children, className, mode, sessionId }) {
1819
2395
  const resolvedChildren = useMemo5(() => {
1820
- return children.replace(SYSTEM_REMINDER_RE, "");
2396
+ return normalizeAdjacentUrlFormatting(children.replace(SYSTEM_REMINDER_RE, ""));
1821
2397
  }, [children]);
1822
- return /* @__PURE__ */ jsx7(
2398
+ return /* @__PURE__ */ jsx8(
1823
2399
  _r,
1824
2400
  {
1825
2401
  className: cn("blade-chat-markdown break-words", className),
@@ -1832,17 +2408,46 @@ function MarkdownContent({ children, className, mode, sessionId }) {
1832
2408
  }
1833
2409
 
1834
2410
  // src/components/Shimmer.tsx
1835
- import { jsx as jsx8 } from "react/jsx-runtime";
2411
+ import { jsx as jsx9 } from "react/jsx-runtime";
1836
2412
  function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1837
- return /* @__PURE__ */ jsx8("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
2413
+ return /* @__PURE__ */ jsx9("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
1838
2414
  }
1839
2415
 
1840
2416
  // src/components/ToolCallBlock.tsx
1841
- import { useState as useState9 } from "react";
2417
+ import { useState as useState11 } from "react";
1842
2418
 
1843
2419
  // src/components/AskUserQuestionBlock.tsx
1844
- import { useEffect as useEffect6, useMemo as useMemo6, useState as useState8 } from "react";
1845
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2420
+ import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef10, useState as useState10 } from "react";
2421
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2422
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2423
+ function resizeCustomTextarea(textarea) {
2424
+ textarea.style.height = "auto";
2425
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2426
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2427
+ }
2428
+ function useAutoResizeTextarea(value) {
2429
+ const textareaRef = useRef10(null);
2430
+ useEffect9(() => {
2431
+ const textarea = textareaRef.current;
2432
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2433
+ }, [value]);
2434
+ useEffect9(() => {
2435
+ const textarea = textareaRef.current;
2436
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2437
+ let previousWidth = textarea.clientWidth;
2438
+ const observer = new ResizeObserver(([entry]) => {
2439
+ if (!entry || entry.contentRect.width === previousWidth) return;
2440
+ previousWidth = entry.contentRect.width;
2441
+ resizeCustomTextarea(textarea);
2442
+ });
2443
+ observer.observe(textarea);
2444
+ return () => observer.disconnect();
2445
+ }, []);
2446
+ return textareaRef;
2447
+ }
2448
+ function indentAnswerContinuationLines(answer) {
2449
+ return answer.replaceAll("\n", "\n ");
2450
+ }
1846
2451
  function AskUserQuestionBlock({
1847
2452
  data,
1848
2453
  answered,
@@ -1851,18 +2456,19 @@ function AskUserQuestionBlock({
1851
2456
  answerData,
1852
2457
  onAnswer
1853
2458
  }) {
1854
- const [selections, setSelections] = useState8(/* @__PURE__ */ new Map());
1855
- const [customTexts, setCustomTexts] = useState8(/* @__PURE__ */ new Map());
1856
- const [usingCustom, setUsingCustom] = useState8(/* @__PURE__ */ new Set());
1857
- const [submitted, setSubmitted] = useState8(false);
1858
- useEffect6(() => {
2459
+ const [selections, setSelections] = useState10(/* @__PURE__ */ new Map());
2460
+ const [customTexts, setCustomTexts] = useState10(/* @__PURE__ */ new Map());
2461
+ const [usingCustom, setUsingCustom] = useState10(/* @__PURE__ */ new Set());
2462
+ const [note, setNote] = useState10("");
2463
+ const [submitted, setSubmitted] = useState10(false);
2464
+ useEffect9(() => {
1859
2465
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1860
2466
  setSubmitted(false);
1861
2467
  }
1862
2468
  }, [sessionStatus]);
1863
2469
  const displayAnswerState = useMemo6(() => {
1864
2470
  if (!(answered && answerData)) {
1865
- return { selections, customTexts, usingCustom };
2471
+ return { selections, customTexts, usingCustom, note };
1866
2472
  }
1867
2473
  const nextSelections = /* @__PURE__ */ new Map();
1868
2474
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1878,9 +2484,10 @@ function AskUserQuestionBlock({
1878
2484
  return {
1879
2485
  selections: nextSelections,
1880
2486
  customTexts: nextCustomTexts,
1881
- usingCustom: nextUsingCustom
2487
+ usingCustom: nextUsingCustom,
2488
+ note: answerData.note ?? ""
1882
2489
  };
1883
- }, [answerData, answered, customTexts, selections, usingCustom]);
2490
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1884
2491
  const toggleOption = (qIdx, optIdx, multi) => {
1885
2492
  if (answered || submitted) return;
1886
2493
  setSelections((prev) => {
@@ -1928,6 +2535,7 @@ function AskUserQuestionBlock({
1928
2535
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1929
2536
  const handleSubmit = () => {
1930
2537
  if (answered || submitted || !allAnswered || !onAnswer) return;
2538
+ const trimmedNote = note.trim();
1931
2539
  const nextAnswerData = {
1932
2540
  selections: Object.fromEntries(
1933
2541
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1937,15 +2545,21 @@ function AskUserQuestionBlock({
1937
2545
  ),
1938
2546
  custom: Object.fromEntries(
1939
2547
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1940
- )
2548
+ ),
2549
+ ...trimmedNote ? { note: trimmedNote } : {}
1941
2550
  };
1942
- const parts = data.questions.map((q, i) => `- ${q.question} -> ${getAnswer(i)}`);
1943
- const text = `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
1944
- ${parts.join("\n")}`;
2551
+ const parts = data.questions.map(
2552
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2553
+ );
2554
+ const text = [
2555
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2556
+ ${parts.join("\n")}`,
2557
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2558
+ ].filter(Boolean).join("\n");
1945
2559
  setSubmitted(true);
1946
2560
  onAnswer(text, toolCallId, nextAnswerData);
1947
2561
  };
1948
- return /* @__PURE__ */ jsxs7(
2562
+ return /* @__PURE__ */ jsxs8(
1949
2563
  "div",
1950
2564
  {
1951
2565
  className: cn(
@@ -1953,12 +2567,12 @@ ${parts.join("\n")}`;
1953
2567
  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"
1954
2568
  ),
1955
2569
  children: [
1956
- data.source_loop?.description && /* @__PURE__ */ jsxs7("div", { className: "rounded-lg bg-[hsl(var(--muted)/0.35)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2570
+ data.source_loop?.description && /* @__PURE__ */ jsxs8("div", { className: "rounded-lg bg-[hsl(var(--muted)/0.35)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
1957
2571
  "\u5B50\u667A\u80FD\u4F53\u300C",
1958
2572
  data.source_loop.description,
1959
2573
  "\u300D\u5728\u7B49\u5F85\u4F60\u7684\u56DE\u7B54"
1960
2574
  ] }),
1961
- data.questions.map((q, qIdx) => /* @__PURE__ */ jsx9(
2575
+ data.questions.map((q, qIdx) => /* @__PURE__ */ jsx10(
1962
2576
  QuestionCard,
1963
2577
  {
1964
2578
  question: q,
@@ -1973,7 +2587,16 @@ ${parts.join("\n")}`;
1973
2587
  },
1974
2588
  q.question
1975
2589
  )),
1976
- !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
2590
+ /* @__PURE__ */ jsx10(
2591
+ NoteField,
2592
+ {
2593
+ answered,
2594
+ submitted,
2595
+ note: displayAnswerState.note,
2596
+ onChange: setNote
2597
+ }
2598
+ ),
2599
+ !answered && !submitted && onAnswer && /* @__PURE__ */ jsx10(
1977
2600
  "button",
1978
2601
  {
1979
2602
  type: "button",
@@ -1983,14 +2606,14 @@ ${parts.join("\n")}`;
1983
2606
  children: allAnswered ? "\u786E\u8BA4" : "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u9009\u9879"
1984
2607
  }
1985
2608
  ),
1986
- submitted && !answered && /* @__PURE__ */ jsxs7(
2609
+ submitted && !answered && /* @__PURE__ */ jsxs8(
1987
2610
  "button",
1988
2611
  {
1989
2612
  type: "button",
1990
2613
  disabled: true,
1991
2614
  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",
1992
2615
  children: [
1993
- /* @__PURE__ */ jsx9(LoaderCircle, { size: 14, className: "animate-spin" }),
2616
+ /* @__PURE__ */ jsx10(LoaderCircle, { size: 14, className: "animate-spin" }),
1994
2617
  "\u786E\u8BA4\u4E2D"
1995
2618
  ]
1996
2619
  }
@@ -2011,30 +2634,31 @@ function QuestionCard({
2011
2634
  onCustomChange
2012
2635
  }) {
2013
2636
  const multi = question.multiSelect ?? false;
2014
- return /* @__PURE__ */ jsxs7("div", { children: [
2015
- /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
2016
- /* @__PURE__ */ jsx9(
2637
+ const customTextareaRef = useAutoResizeTextarea(customText);
2638
+ return /* @__PURE__ */ jsxs8("div", { children: [
2639
+ /* @__PURE__ */ jsxs8("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
2640
+ /* @__PURE__ */ jsx10(
2017
2641
  MessageSquareMore,
2018
2642
  {
2019
2643
  size: answered ? 12 : 13,
2020
2644
  className: "mt-0.5 shrink-0 text-[hsl(var(--primary))]"
2021
2645
  }
2022
2646
  ),
2023
- /* @__PURE__ */ jsx9(
2647
+ /* @__PURE__ */ jsx10(
2024
2648
  "div",
2025
2649
  {
2026
2650
  className: cn(
2027
2651
  "min-w-0 flex-1 font-medium text-[hsl(var(--foreground))]",
2028
2652
  answered ? "text-xs" : "text-sm"
2029
2653
  ),
2030
- children: /* @__PURE__ */ jsx9(MarkdownContent, { className: "blade-chat-prose", children: question.question })
2654
+ children: /* @__PURE__ */ jsx10(MarkdownContent, { className: "blade-chat-prose", children: question.question })
2031
2655
  }
2032
2656
  )
2033
2657
  ] }),
2034
- /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
2658
+ /* @__PURE__ */ jsxs8("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
2035
2659
  question.options.map((opt, optIdx) => {
2036
2660
  const isSel = selected.has(optIdx);
2037
- return /* @__PURE__ */ jsxs7(
2661
+ return /* @__PURE__ */ jsxs8(
2038
2662
  "button",
2039
2663
  {
2040
2664
  type: "button",
@@ -2048,14 +2672,14 @@ function QuestionCard({
2048
2672
  answered && "cursor-default opacity-70"
2049
2673
  ),
2050
2674
  children: [
2051
- multi && /* @__PURE__ */ jsx9(
2675
+ multi && /* @__PURE__ */ jsx10(
2052
2676
  "div",
2053
2677
  {
2054
2678
  className: cn(
2055
2679
  "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
2056
2680
  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))]"
2057
2681
  ),
2058
- children: isSel && /* @__PURE__ */ jsx9(
2682
+ children: isSel && /* @__PURE__ */ jsx10(
2059
2683
  Check,
2060
2684
  {
2061
2685
  size: 9,
@@ -2064,9 +2688,9 @@ function QuestionCard({
2064
2688
  )
2065
2689
  }
2066
2690
  ),
2067
- /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
2068
- /* @__PURE__ */ jsx9("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2069
- opt.description && /* @__PURE__ */ jsx9(
2691
+ /* @__PURE__ */ jsxs8("div", { className: "min-w-0", children: [
2692
+ /* @__PURE__ */ jsx10("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2693
+ opt.description && /* @__PURE__ */ jsx10(
2070
2694
  "div",
2071
2695
  {
2072
2696
  className: cn(
@@ -2083,29 +2707,30 @@ function QuestionCard({
2083
2707
  opt.label
2084
2708
  );
2085
2709
  }),
2086
- answered && !isCustom ? null : /* @__PURE__ */ jsxs7(
2710
+ answered && !isCustom ? null : /* @__PURE__ */ jsxs8(
2087
2711
  "div",
2088
2712
  {
2089
2713
  className: cn(
2090
- "flex items-center gap-2 rounded-lg border transition-all",
2714
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2091
2715
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2092
2716
  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))]",
2093
2717
  answered && "cursor-default opacity-70"
2094
2718
  ),
2095
2719
  children: [
2096
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2097
- /* @__PURE__ */ jsx9(
2098
- "input",
2720
+ /* @__PURE__ */ jsx10("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2721
+ /* @__PURE__ */ jsx10(
2722
+ "textarea",
2099
2723
  {
2100
- type: "text",
2724
+ ref: customTextareaRef,
2725
+ rows: 2,
2101
2726
  value: customText,
2102
- disabled: answered,
2727
+ readOnly: answered,
2103
2728
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2104
2729
  onFocus: () => onCustomFocus(qIdx),
2105
2730
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2106
2731
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2107
2732
  className: cn(
2108
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2733
+ "min-h-10 min-w-0 flex-1 resize-none bg-transparent leading-5 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2109
2734
  answered ? "text-xs" : "text-sm"
2110
2735
  )
2111
2736
  }
@@ -2116,6 +2741,49 @@ function QuestionCard({
2116
2741
  ] })
2117
2742
  ] });
2118
2743
  }
2744
+ function NoteField({
2745
+ answered,
2746
+ submitted,
2747
+ note,
2748
+ onChange
2749
+ }) {
2750
+ const textareaRef = useAutoResizeTextarea(note);
2751
+ const readOnly = answered || submitted;
2752
+ if (answered && !note.trim()) return null;
2753
+ return /* @__PURE__ */ jsxs8(
2754
+ "label",
2755
+ {
2756
+ className: cn(
2757
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2758
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2759
+ note.trim() ? "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))]",
2760
+ readOnly && "cursor-default opacity-70"
2761
+ ),
2762
+ children: [
2763
+ /* @__PURE__ */ jsx10("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2764
+ /* @__PURE__ */ jsx10(
2765
+ "textarea",
2766
+ {
2767
+ ref: textareaRef,
2768
+ rows: 2,
2769
+ value: note,
2770
+ readOnly,
2771
+ onChange: (event) => {
2772
+ if (readOnly) return;
2773
+ onChange(event.target.value);
2774
+ },
2775
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2776
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2777
+ className: cn(
2778
+ "min-h-10 w-full resize-none bg-transparent leading-5 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2779
+ answered ? "text-xs" : "text-sm"
2780
+ )
2781
+ }
2782
+ )
2783
+ ]
2784
+ }
2785
+ );
2786
+ }
2119
2787
  function parseAskUserQuestion(toolResult) {
2120
2788
  if (!toolResult) return null;
2121
2789
  try {
@@ -2138,6 +2806,26 @@ function parseAskUserQuestion(toolResult) {
2138
2806
  }
2139
2807
  return null;
2140
2808
  }
2809
+ function parseAskUserQuestionError(toolResult) {
2810
+ if (!toolResult) return null;
2811
+ try {
2812
+ const parsed = JSON.parse(toolResult);
2813
+ let detail = null;
2814
+ if (typeof parsed.error === "string") detail = parsed.error;
2815
+ if (parsed.error && typeof parsed.error === "object") {
2816
+ const message = parsed.error.message;
2817
+ if (typeof message === "string") detail = message;
2818
+ }
2819
+ if (!detail && typeof parsed.message === "string") detail = parsed.message;
2820
+ if (!detail) return null;
2821
+ return {
2822
+ message: parsed.error_code === "invalid_questions" ? "\u63D0\u95EE\u5185\u5BB9\u4E0D\u5B8C\u6574\uFF0C\u5F53\u524D\u6CA1\u6709\u7B49\u5F85\u4F60\u56DE\u7B54\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" : "\u8FD9\u6B21\u63D0\u95EE\u6CA1\u6709\u6210\u529F\uFF0C\u5F53\u524D\u6CA1\u6709\u7B49\u5F85\u4F60\u56DE\u7B54\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002",
2823
+ detail
2824
+ };
2825
+ } catch {
2826
+ return null;
2827
+ }
2828
+ }
2141
2829
  function normalizeQuestionItem(value) {
2142
2830
  if (!value || typeof value !== "object") return null;
2143
2831
  const item = value;
@@ -2162,13 +2850,14 @@ function normalizeOptionItem(value) {
2162
2850
  }
2163
2851
 
2164
2852
  // src/components/ToolCallBlock.tsx
2165
- import { Fragment, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2853
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2166
2854
  function resolveAskQuestionState({
2167
2855
  toolStatus,
2168
2856
  hasAnswerData,
2169
- fallbackAnswered
2857
+ fallbackAnswered,
2858
+ fallbackAwaiting
2170
2859
  }) {
2171
- const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
2860
+ const awaitingAnswer = !hasAnswerData && (toolStatus === "awaiting_answer" || toolStatus === "pending" && fallbackAwaiting === true);
2172
2861
  return {
2173
2862
  awaitingAnswer,
2174
2863
  answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
@@ -2180,14 +2869,15 @@ function ToolCallBlock({
2180
2869
  answered,
2181
2870
  answerData,
2182
2871
  sessionStatus,
2872
+ isActiveQuestion,
2183
2873
  renderer
2184
2874
  }) {
2185
- const [expanded, setExpanded] = useState9(false);
2875
+ const [expanded, setExpanded] = useState11(false);
2186
2876
  const normalizedName = formatToolName(toolCall.name);
2187
2877
  if (renderer) {
2188
2878
  const custom = renderer(toolCall);
2189
2879
  if (custom !== null && custom !== void 0) {
2190
- return /* @__PURE__ */ jsx10(Fragment, { children: custom });
2880
+ return /* @__PURE__ */ jsx11(Fragment2, { children: custom });
2191
2881
  }
2192
2882
  }
2193
2883
  if (normalizedName === "AskUserQuestion") {
@@ -2195,11 +2885,12 @@ function ToolCallBlock({
2195
2885
  const questionState = resolveAskQuestionState({
2196
2886
  toolStatus: toolCall.status,
2197
2887
  hasAnswerData: Boolean(answerData),
2198
- fallbackAnswered: answered
2888
+ fallbackAnswered: answered,
2889
+ fallbackAwaiting: isActiveQuestion === true && (sessionStatus === "paused" || sessionStatus === "waiting_for_input")
2199
2890
  });
2200
2891
  const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
2201
2892
  if (askData) {
2202
- return /* @__PURE__ */ jsx10(
2893
+ return /* @__PURE__ */ jsx11(
2203
2894
  AskUserQuestionBlock,
2204
2895
  {
2205
2896
  data: askData,
@@ -2212,24 +2903,31 @@ function ToolCallBlock({
2212
2903
  );
2213
2904
  }
2214
2905
  if (toolCall.status === "pending") {
2215
- return /* @__PURE__ */ jsxs8("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: [
2216
- /* @__PURE__ */ jsx10(LoaderCircle, { size: 14, className: "animate-spin" }),
2217
- /* @__PURE__ */ jsx10("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2906
+ return /* @__PURE__ */ jsxs9("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: [
2907
+ /* @__PURE__ */ jsx11(LoaderCircle, { size: 14, className: "animate-spin" }),
2908
+ /* @__PURE__ */ jsx11("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2218
2909
  ] });
2219
2910
  }
2220
- return /* @__PURE__ */ jsxs8("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: [
2221
- /* @__PURE__ */ jsx10("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
2222
- /* @__PURE__ */ jsx10("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" })
2911
+ const errorDetail = parseAskUserQuestionError(
2912
+ typeof toolCall.result === "string" ? toolCall.result : null
2913
+ );
2914
+ return /* @__PURE__ */ jsxs9("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: [
2915
+ /* @__PURE__ */ jsx11("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
2916
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: errorDetail?.message ?? "\u6536\u5230\u7684\u4EA4\u4E92\u6570\u636E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" }),
2917
+ errorDetail?.detail ? /* @__PURE__ */ jsxs9("details", { className: "mt-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2918
+ /* @__PURE__ */ jsx11("summary", { className: "cursor-pointer", children: "\u67E5\u770B\u5177\u4F53\u539F\u56E0" }),
2919
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-words font-mono", children: errorDetail.detail })
2920
+ ] }) : null
2223
2921
  ] });
2224
2922
  }
2225
2923
  const tone = getToolTone(toolCall.status);
2226
2924
  const displayName = getToolDisplayLabel(toolCall);
2227
2925
  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))]";
2228
- const statusIcon = toolCall.status === "pending" ? /* @__PURE__ */ jsx10(LoaderCircle, { size: 11, className: "animate-spin" }) : toolCall.status === "awaiting_answer" ? /* @__PURE__ */ jsx10(MessageSquareMore, { size: 11 }) : toolCall.status === "cancelled" || toolCall.status === "error" ? /* @__PURE__ */ jsx10(X, { size: 11 }) : /* @__PURE__ */ jsx10(Check, { size: 11 });
2926
+ const statusIcon = toolCall.status === "pending" ? /* @__PURE__ */ jsx11(LoaderCircle, { size: 11, className: "animate-spin" }) : toolCall.status === "awaiting_answer" ? /* @__PURE__ */ jsx11(MessageSquareMore, { size: 11 }) : toolCall.status === "cancelled" || toolCall.status === "error" ? /* @__PURE__ */ jsx11(X, { size: 11 }) : /* @__PURE__ */ jsx11(Check, { size: 11 });
2229
2927
  const statusTextClass = tone === "red" ? "text-[hsl(var(--muted-foreground))]" : tone === "amber" ? "text-amber-300" : tone === "blue" ? "text-blue-300" : "text-[hsl(var(--primary))]";
2230
- return /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool ml-4 text-xs", children: [
2231
- /* @__PURE__ */ jsxs8("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
2232
- /* @__PURE__ */ jsxs8(
2928
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-tool ml-4 text-xs", children: [
2929
+ /* @__PURE__ */ jsxs9("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
2930
+ /* @__PURE__ */ jsxs9(
2233
2931
  "button",
2234
2932
  {
2235
2933
  type: "button",
@@ -2237,7 +2935,7 @@ function ToolCallBlock({
2237
2935
  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",
2238
2936
  "aria-expanded": expanded,
2239
2937
  children: [
2240
- /* @__PURE__ */ jsx10(
2938
+ /* @__PURE__ */ jsx11(
2241
2939
  ChevronRight,
2242
2940
  {
2243
2941
  size: 11,
@@ -2247,24 +2945,24 @@ function ToolCallBlock({
2247
2945
  )
2248
2946
  }
2249
2947
  ),
2250
- /* @__PURE__ */ jsxs8("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
2948
+ /* @__PURE__ */ jsxs9("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
2251
2949
  statusIcon,
2252
- /* @__PURE__ */ jsx10("span", { children: getToolStatusLabel(toolCall.status) })
2950
+ /* @__PURE__ */ jsx11("span", { children: getToolStatusLabel(toolCall.status) })
2253
2951
  ] }),
2254
- /* @__PURE__ */ jsx10("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
2952
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
2255
2953
  ]
2256
2954
  }
2257
2955
  ),
2258
- typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx10("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
2956
+ typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx11("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
2259
2957
  ] }),
2260
- expanded && /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
2261
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
2262
- /* @__PURE__ */ jsx10("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
2263
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
2264
- /* @__PURE__ */ jsx10("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) }),
2265
- toolCall.result != null && /* @__PURE__ */ jsxs8(Fragment, { children: [
2266
- /* @__PURE__ */ jsx10("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
2267
- /* @__PURE__ */ jsx10("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) })
2958
+ expanded && /* @__PURE__ */ jsxs9("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
2959
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
2960
+ /* @__PURE__ */ jsx11("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
2961
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
2962
+ /* @__PURE__ */ jsx11("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) }),
2963
+ toolCall.result != null && /* @__PURE__ */ jsxs9(Fragment2, { children: [
2964
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
2965
+ /* @__PURE__ */ jsx11("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) })
2268
2966
  ] })
2269
2967
  ] })
2270
2968
  ] });
@@ -2282,110 +2980,555 @@ function buildAskUserPayload(argumentsJson) {
2282
2980
  }
2283
2981
 
2284
2982
  // src/components/AssistantTurnBlock.tsx
2285
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2983
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2286
2984
  function ThinkingBlock({ reasoning, isStreaming }) {
2287
- const [open, setOpen] = useState10(false);
2288
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2289
- /* @__PURE__ */ jsxs9(
2985
+ const [open, setOpen] = useState12(false);
2986
+ if (!isStreaming) return null;
2987
+ return /* @__PURE__ */ jsxs10("div", { className: "blade-chat-thinking text-xs", children: [
2988
+ /* @__PURE__ */ jsxs10(
2290
2989
  "button",
2291
2990
  {
2292
2991
  type: "button",
2293
2992
  onClick: () => setOpen(!open),
2294
2993
  "aria-expanded": open,
2295
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2994
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2296
2995
  children: [
2297
- /* @__PURE__ */ jsx11(Brain, { size: 12, className: "shrink-0" }),
2298
- isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u601D\u8003\u8FC7\u7A0B" }),
2299
- /* @__PURE__ */ jsxs9("span", { className: "text-[hsl(var(--muted-foreground))]/70", children: [
2300
- "\xB7 ",
2301
- new Intl.NumberFormat("zh-CN").format(reasoning.length),
2302
- " \u5B57"
2303
- ] }),
2304
- /* @__PURE__ */ jsx11(
2305
- ChevronDown,
2996
+ /* @__PURE__ */ jsx12(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }),
2997
+ /* @__PURE__ */ jsx12(
2998
+ ChevronRight,
2306
2999
  {
2307
- size: 12,
2308
- className: cn("shrink-0 transition-transform", open && "rotate-180")
3000
+ size: 14,
3001
+ className: cn(
3002
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
3003
+ open && "rotate-90 opacity-100"
3004
+ )
2309
3005
  }
2310
3006
  )
2311
3007
  ]
2312
3008
  }
2313
3009
  ),
2314
- open && /* @__PURE__ */ jsx11("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 })
3010
+ open ? /* @__PURE__ */ jsx12("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning }) : null
2315
3011
  ] });
2316
3012
  }
2317
3013
  function getMessageText(message) {
2318
3014
  return getTextContent(normalizeMessageContent(message.content)).trim();
2319
3015
  }
2320
- function findLatestReasoningMessageIndex(messages) {
2321
- for (let index = messages.length - 1; index >= 0; index -= 1) {
2322
- if (messages[index].reasoning) return index;
3016
+ function hasRenderableMessageContent(message) {
3017
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
3018
+ }
3019
+ function getLastContentMessage(messages) {
3020
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3021
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
3022
+ }
3023
+ return null;
3024
+ }
3025
+ function getOrderedMessageParts(message, toolCalls) {
3026
+ const blocks = message.blocks ?? [];
3027
+ if (!blocks.some((block) => block.type === "text") || !blocks.some((block) => block.type === "tool_use")) return [];
3028
+ const toolsById = new Map(toolCalls.map((toolCall) => [toolCall.id, toolCall]));
3029
+ const seenToolIds = /* @__PURE__ */ new Set();
3030
+ const parts = [];
3031
+ for (const [index, block] of blocks.entries()) {
3032
+ if (block.type === "text" && block.content != null && block.content !== "") {
3033
+ const content = Array.isArray(block.content) ? block.content : String(block.content);
3034
+ parts.push({ type: "text", key: `text-${index}`, content });
3035
+ }
3036
+ if (block.type !== "tool_use" || !block.tool_call_id) continue;
3037
+ const toolCall = toolsById.get(block.tool_call_id);
3038
+ if (!toolCall) continue;
3039
+ seenToolIds.add(toolCall.id);
3040
+ const previous = parts[parts.length - 1];
3041
+ if (previous?.type === "tools") previous.toolCalls.push(toolCall);
3042
+ else parts.push({ type: "tools", key: `tools-${index}`, toolCalls: [toolCall] });
3043
+ }
3044
+ const missingTools = toolCalls.filter((toolCall) => !seenToolIds.has(toolCall.id));
3045
+ if (seenToolIds.size === 0) return [];
3046
+ if (missingTools.length > 0) {
3047
+ parts.push({ type: "tools", key: "tools-missing", toolCalls: missingTools });
3048
+ }
3049
+ return parts;
3050
+ }
3051
+ function findLatestReasoningMessageIndex(messages) {
3052
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3053
+ if (messages[index].reasoning) return index;
3054
+ }
3055
+ return -1;
3056
+ }
3057
+ function resolveTurnDisplayMode({
3058
+ isStreaming: _isStreaming,
3059
+ displayMode
3060
+ }) {
3061
+ return displayMode;
3062
+ }
3063
+ function formatExecutionDuration(durationMs) {
3064
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
3065
+ const minutes = Math.floor(totalSeconds / 60);
3066
+ const seconds = totalSeconds % 60;
3067
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
3068
+ }
3069
+ function getExecutionDurationMs({
3070
+ messages,
3071
+ isStreaming,
3072
+ now = Date.now()
3073
+ }) {
3074
+ const knownDuration = messages.reduce(
3075
+ (total, message) => {
3076
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
3077
+ return total + message.duration_ms;
3078
+ }
3079
+ return total + (message.tool_calls ?? []).reduce(
3080
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
3081
+ 0
3082
+ );
3083
+ },
3084
+ 0
3085
+ );
3086
+ if (!isStreaming) return knownDuration;
3087
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
3088
+ if (startedAt === void 0) return knownDuration;
3089
+ return Math.max(knownDuration, now - startedAt);
3090
+ }
3091
+ function findLastExceptionalEvent(messages) {
3092
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
3093
+ const messageStatus = messages[messageIndex].status;
3094
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
3095
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
3096
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
3097
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
3098
+ const status = toolCalls[toolIndex].status;
3099
+ if (status === "error" || status === "cancelled") {
3100
+ return { messageIndex, status };
3101
+ }
3102
+ }
3103
+ }
3104
+ return null;
3105
+ }
3106
+ function executionSummaryLabel({
3107
+ messages,
3108
+ isStreaming,
3109
+ durationMs,
3110
+ sessionStatus,
3111
+ askAnswers
3112
+ }) {
3113
+ if (isStreaming) {
3114
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
3115
+ }
3116
+ if (sessionStatus === "waiting_for_input" && messages.some(
3117
+ (message) => (message.tool_calls ?? []).some(
3118
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
3119
+ )
3120
+ )) {
3121
+ return "\u7B49\u5F85\u8F93\u5165";
3122
+ }
3123
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
3124
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
3125
+ if (lastExceptionalEvent) {
3126
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
3127
+ if (lastExceptionalEvent.status === "error") {
3128
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
3129
+ }
3130
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
3131
+ }
3132
+ return completedLabel;
3133
+ }
3134
+ function businessToolDisplayName(toolCall) {
3135
+ const displayName = toolCall.display_name?.trim() ?? "";
3136
+ if (!displayName) return "";
3137
+ const rawName = toolCall.name.trim();
3138
+ return displayName !== rawName && formatToolName(displayName) !== formatToolName(rawName) ? displayName : "";
3139
+ }
3140
+ function executionToolTypeLabel(toolCall) {
3141
+ switch (formatToolName(toolCall.name)) {
3142
+ case "WebSearch":
3143
+ case "WebFetch":
3144
+ return "\u7F51\u7EDC\u68C0\u7D22";
3145
+ case "Bash":
3146
+ case "BgBash":
3147
+ return "\u547D\u4EE4\u6267\u884C";
3148
+ case "Read":
3149
+ case "ReadSkill":
3150
+ return "\u5185\u5BB9\u8BFB\u53D6";
3151
+ case "Write":
3152
+ case "Edit":
3153
+ case "MultiEdit":
3154
+ return "\u6587\u4EF6\u5904\u7406";
3155
+ case "Grep":
3156
+ case "Glob":
3157
+ return "\u5185\u5BB9\u641C\u7D22";
3158
+ case "Agent":
3159
+ return "\u5B50\u4EFB\u52A1";
3160
+ case "search_skills":
3161
+ return "\u6280\u80FD\u68C0\u7D22";
3162
+ case "get_skill_content":
3163
+ return "\u8BFB\u53D6\u6280\u80FD";
3164
+ case "run_skill_tool":
3165
+ return "\u6267\u884C\u6280\u80FD";
3166
+ default:
3167
+ return businessToolDisplayName(toolCall) || "\u6267\u884C\u6B65\u9AA4";
3168
+ }
3169
+ }
3170
+ function executionToolIntent(toolCall) {
3171
+ const normalizedName = formatToolName(toolCall.name);
3172
+ let args = null;
3173
+ try {
3174
+ const parsed = JSON.parse(toolCall.arguments);
3175
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
3176
+ } catch {
3177
+ args = null;
3178
+ }
3179
+ const getString = (key) => {
3180
+ const value = args?.[key];
3181
+ return typeof value === "string" ? value.trim() : "";
3182
+ };
3183
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || "";
3184
+ if (explicitIntent) return explicitIntent;
3185
+ if (normalizedName === "search_skills") return getString("query");
3186
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
3187
+ return getString("skill_name") || getString("skill");
3188
+ }
3189
+ if (normalizedName === "FinishTask") return getString("title");
3190
+ return "";
3191
+ }
3192
+ function ExecutionToolRow({ toolCall }) {
3193
+ const normalizedName = formatToolName(toolCall.name);
3194
+ const typeLabel = executionToolTypeLabel(toolCall);
3195
+ const intent = executionToolIntent(toolCall);
3196
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
3197
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
3198
+ const iconClass = cn(
3199
+ "size-3.5 shrink-0",
3200
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3201
+ );
3202
+ const icon = toolCall.status === "pending" ? /* @__PURE__ */ jsx12(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx12(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx12(X, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "WebSearch" || normalizedName === "WebFetch" ? /* @__PURE__ */ jsx12(Earth, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Bash" || normalizedName === "BgBash" ? /* @__PURE__ */ jsx12(Terminal, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit" ? /* @__PURE__ */ jsx12(FilePenLine, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Read" || normalizedName === "ReadSkill" || normalizedName === "get_skill_content" ? /* @__PURE__ */ jsx12(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Grep" || normalizedName === "Glob" || normalizedName === "search_skills" ? /* @__PURE__ */ jsx12(Search, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Agent" ? /* @__PURE__ */ jsx12(Bot, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx12(Wrench, { className: iconClass, "aria-hidden": "true" });
3203
+ const rowClassName = cn(
3204
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
3205
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3206
+ );
3207
+ return /* @__PURE__ */ jsxs10("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
3208
+ icon,
3209
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate", children: label })
3210
+ ] });
3211
+ }
3212
+ function AssistantTurnBlock({
3213
+ messages,
3214
+ isStreaming = false,
3215
+ askAnswers,
3216
+ onAnswer,
3217
+ sessionStatus,
3218
+ toolCallRenderer,
3219
+ hidePlanUpdateTools = false,
3220
+ sessionId
3221
+ }) {
3222
+ const shouldHideToolCall = (message, toolCall) => {
3223
+ if (!hidePlanUpdateTools || !isPlanUpdateTool(toolCall) || parsePlanUpdate(toolCall.arguments) === null) {
3224
+ return false;
3225
+ }
3226
+ return toolCall.status === "done" || toolCall.status === "pending" && message.status === "streaming";
3227
+ };
3228
+ const hasInterrupted = messages.some((message) => message.status === "interrupted");
3229
+ const hasFailedWithoutContent = messages.some(
3230
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
3231
+ );
3232
+ const finalMessage = getLastContentMessage(messages);
3233
+ const turnToolCalls = messages.flatMap((message) => message.tool_calls ?? []);
3234
+ const finalOrderedParts = finalMessage ? getOrderedMessageParts(
3235
+ finalMessage,
3236
+ (finalMessage.tool_calls ?? []).filter(
3237
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(finalMessage, toolCall)
3238
+ )
3239
+ ) : [];
3240
+ const hasExecutionProcess = messages.some(
3241
+ (message) => message.reasoning || (message.tool_calls ?? []).some((toolCall) => !shouldHideToolCall(message, toolCall))
3242
+ );
3243
+ const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
3244
+ const hasActionableToolCall = messages.some(
3245
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
3246
+ (toolCall) => !shouldHideToolCall(message, toolCall) && (toolCall.status === "error" || toolCall.status === "cancelled")
3247
+ )
3248
+ );
3249
+ const questionToolCalls = messages.flatMap(
3250
+ (message) => (message.tool_calls ?? []).filter(
3251
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
3252
+ )
3253
+ );
3254
+ const activeQuestionId = questionToolCalls.filter((toolCall) => toolCall.status === "pending").at(-1)?.id;
3255
+ const [displayMode, setDisplayMode] = useState12(
3256
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
3257
+ );
3258
+ const userSelectedDisplayModeRef = useRef11(false);
3259
+ const wasStreamingRef = useRef11(isStreaming);
3260
+ useEffect10(() => {
3261
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
3262
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
3263
+ }
3264
+ wasStreamingRef.current = isStreaming;
3265
+ }, [hasActionableToolCall, isStreaming]);
3266
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
3267
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
3268
+ const [clock, setClock] = useState12(() => Date.now());
3269
+ const hasLiveStartTime = messages.some(
3270
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
3271
+ );
3272
+ useEffect10(() => {
3273
+ if (!isStreaming || !hasLiveStartTime) return;
3274
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
3275
+ return () => window.clearInterval(timer);
3276
+ }, [hasLiveStartTime, isStreaming]);
3277
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
3278
+ const memoryRefs = collectMemoryRefs(messages);
3279
+ if (!hasExecutionProcess) {
3280
+ return /* @__PURE__ */ jsxs10(
3281
+ "div",
3282
+ {
3283
+ "aria-busy": isStreaming || void 0,
3284
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3285
+ children: [
3286
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx12(MemoryRefsHint, { refs: memoryRefs }) : null,
3287
+ hasInterrupted && /* @__PURE__ */ jsx12("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" }),
3288
+ hasFailedWithoutContent && /* @__PURE__ */ jsx12("div", { className: "ml-4 w-fit rounded-full border border-red-500/30 bg-red-500/10 px-2.5 py-1 text-[10px] font-medium text-red-400", children: "\u751F\u6210\u5931\u8D25" }),
3289
+ messages.map((message, index) => {
3290
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx12(
3291
+ "div",
3292
+ {
3293
+ className: "flex flex-col gap-3",
3294
+ children: /* @__PURE__ */ jsx12(
3295
+ AssistantMessageContent,
3296
+ {
3297
+ message,
3298
+ sessionId,
3299
+ streaming: isStreaming && index === messages.length - 1
3300
+ }
3301
+ )
3302
+ },
3303
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3304
+ ) : null;
3305
+ })
3306
+ ]
3307
+ }
3308
+ );
3309
+ }
3310
+ return /* @__PURE__ */ jsxs10(
3311
+ "div",
3312
+ {
3313
+ "aria-busy": isStreaming || void 0,
3314
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3315
+ children: [
3316
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx12(MemoryRefsHint, { refs: memoryRefs }) : null,
3317
+ hasInterrupted && /* @__PURE__ */ jsx12("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" }),
3318
+ hasFailedWithoutContent && /* @__PURE__ */ jsx12("div", { className: "ml-4 w-fit rounded-full border border-red-500/30 bg-red-500/10 px-2.5 py-1 text-[10px] font-medium text-red-400", children: "\u751F\u6210\u5931\u8D25" }),
3319
+ /* @__PURE__ */ jsxs10("div", { className: "flex w-full items-start gap-2.5", children: [
3320
+ /* @__PURE__ */ jsx12(
3321
+ "span",
3322
+ {
3323
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3324
+ "aria-hidden": "true",
3325
+ children: /* @__PURE__ */ jsx12(Bot, { size: 16 })
3326
+ }
3327
+ ),
3328
+ /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3329
+ /* @__PURE__ */ jsxs10(
3330
+ "button",
3331
+ {
3332
+ type: "button",
3333
+ onClick: () => {
3334
+ userSelectedDisplayModeRef.current = true;
3335
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
3336
+ },
3337
+ "aria-expanded": effectiveMode === "detail",
3338
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3339
+ "data-testid": "assistant-execution-summary",
3340
+ className: "inline-flex min-w-0 max-w-full select-none items-center gap-1 bg-transparent p-0 text-left text-xs leading-[22px] text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))] focus-visible:ring-1 focus-visible:ring-[hsl(var(--ring))] focus:outline-none",
3341
+ children: [
3342
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3343
+ messages,
3344
+ isStreaming,
3345
+ durationMs: liveExecutionDurationMs,
3346
+ sessionStatus,
3347
+ askAnswers
3348
+ }) }),
3349
+ /* @__PURE__ */ jsx12(
3350
+ ChevronRight,
3351
+ {
3352
+ size: 14,
3353
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
3354
+ className: cn(
3355
+ "shrink-0 transition-transform",
3356
+ effectiveMode === "detail" && "rotate-90"
3357
+ ),
3358
+ "aria-hidden": "true"
3359
+ }
3360
+ )
3361
+ ]
3362
+ }
3363
+ ),
3364
+ /* @__PURE__ */ jsx12("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3365
+ ] })
3366
+ ] }),
3367
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx12("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
3368
+ const isLast = index === messages.length - 1;
3369
+ const streamingThis = isStreaming && isLast;
3370
+ const text = getMessageText(message);
3371
+ const toolCalls = (message.tool_calls ?? []).filter(
3372
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(message, toolCall)
3373
+ );
3374
+ const orderedParts = getOrderedMessageParts(message, toolCalls);
3375
+ const showReasoning = !!message.reasoning && isStreaming && index === latestReasoningIndex;
3376
+ return /* @__PURE__ */ jsxs10(
3377
+ "div",
3378
+ {
3379
+ className: "flex flex-col gap-3",
3380
+ children: [
3381
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx12(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3382
+ orderedParts.length > 0 ? orderedParts.map(
3383
+ (part) => part.type === "text" ? /* @__PURE__ */ jsx12(
3384
+ AssistantMessageContent,
3385
+ {
3386
+ message: { ...message, content: part.content, tool_calls: turnToolCalls },
3387
+ sessionId,
3388
+ streaming: streamingThis,
3389
+ compact: true
3390
+ },
3391
+ part.key
3392
+ ) : /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-0.5", children: part.toolCalls.map((toolCall) => {
3393
+ const custom = toolCallRenderer?.(toolCall);
3394
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx12("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx12(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx12(ExecutionToolRow, { toolCall }, toolCall.id);
3395
+ }) }, part.key)
3396
+ ) : hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx12(
3397
+ AssistantMessageContent,
3398
+ {
3399
+ message,
3400
+ sessionId,
3401
+ streaming: streamingThis,
3402
+ compact: true
3403
+ }
3404
+ ) : null,
3405
+ orderedParts.length === 0 && toolCalls.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3406
+ const custom = toolCallRenderer?.(toolCall);
3407
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx12("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx12(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx12(ExecutionToolRow, { toolCall }, toolCall.id);
3408
+ }) }) : null
3409
+ ]
3410
+ },
3411
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3412
+ );
3413
+ }) }) : null,
3414
+ finalMessage && (effectiveMode === "compact" || finalOrderedParts.length === 0) ? /* @__PURE__ */ jsx12("div", { className: "ml-10", children: /* @__PURE__ */ jsx12(
3415
+ AssistantMessageContent,
3416
+ {
3417
+ message: finalMessage,
3418
+ sessionId,
3419
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3420
+ }
3421
+ ) }) : null,
3422
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx12(
3423
+ ToolCallBlock,
3424
+ {
3425
+ toolCall,
3426
+ answerData: askAnswers?.[toolCall.id],
3427
+ onAnswer,
3428
+ answered: sessionStatus !== "waiting_for_input",
3429
+ sessionStatus,
3430
+ isActiveQuestion: toolCall.id === activeQuestionId,
3431
+ renderer: toolCallRenderer
3432
+ },
3433
+ toolCall.id
3434
+ ))
3435
+ ]
3436
+ }
3437
+ );
3438
+ }
3439
+ function collectMemoryRefs(messages) {
3440
+ const refs = /* @__PURE__ */ new Map();
3441
+ for (const message of messages) {
3442
+ for (const ref of message.memory_refs ?? []) if (!refs.has(ref.id)) refs.set(ref.id, ref);
2323
3443
  }
2324
- return -1;
3444
+ return [...refs.values()];
2325
3445
  }
2326
- function AssistantTurnBlock({
2327
- messages,
2328
- isStreaming = false,
2329
- askAnswers,
2330
- onAnswer,
2331
- sessionStatus,
2332
- toolCallRenderer,
2333
- sessionId
3446
+ function MemoryRefsHint({ refs }) {
3447
+ const [expanded, setExpanded] = useState12(false);
3448
+ const label = refs.some((ref) => ref.skill_name) ? "\u53C2\u8003\u4E86\u8BE5\u6280\u80FD\u7684\u5386\u53F2\u7ECF\u9A8C" : "\u53C2\u8003\u4E86\u5386\u53F2\u7ECF\u9A8C";
3449
+ return /* @__PURE__ */ jsxs10("div", { className: "blade-chat-memory-refs ml-1 w-full max-w-[680px]", children: [
3450
+ /* @__PURE__ */ jsxs10("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[hsl(var(--primary)/0.22)] bg-[hsl(var(--primary)/0.07)] px-3 text-xs font-medium text-[hsl(var(--primary))]", children: [
3451
+ /* @__PURE__ */ jsx12(BookOpen, { size: 12 }),
3452
+ /* @__PURE__ */ jsxs10("span", { children: [
3453
+ label,
3454
+ "\uFF08",
3455
+ refs.length,
3456
+ "\uFF09"
3457
+ ] }),
3458
+ /* @__PURE__ */ jsx12(ChevronRight, { size: 10, className: cn("transition-transform", expanded && "rotate-90") })
3459
+ ] }),
3460
+ expanded ? /* @__PURE__ */ jsx12("div", { className: "mt-2 flex flex-col gap-2 rounded-xl border border-[hsl(var(--border)/0.8)] bg-[hsl(var(--muted)/0.28)] p-2.5", children: refs.map((ref) => /* @__PURE__ */ jsxs10("div", { className: "rounded-lg border border-[hsl(var(--border)/0.55)] bg-[hsl(var(--background)/0.72)] px-3 py-2.5 text-xs", children: [
3461
+ /* @__PURE__ */ jsx12("p", { className: "line-clamp-2 break-words leading-5", children: ref.content_preview }),
3462
+ ref.skill_name ? /* @__PURE__ */ jsx12("span", { className: "mt-1 inline-flex text-[10px] text-[hsl(var(--primary))]", children: ref.skill_name }) : null
3463
+ ] }, ref.id)) }) : null
3464
+ ] });
3465
+ }
3466
+ function AssistantMessageContent({
3467
+ message,
3468
+ sessionId,
3469
+ streaming,
3470
+ compact = false
2334
3471
  }) {
2335
- const hasInterrupted = messages.some((message) => message.status === "interrupted");
2336
- const hasAnyContent = messages.some(
2337
- (message) => getMessageText(message) || message.reasoning || (message.tool_calls?.length ?? 0) > 0
2338
- );
2339
- const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
2340
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2341
- hasInterrupted && /* @__PURE__ */ jsx11("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" }),
2342
- messages.map((message, index) => {
2343
- const isLast = index === messages.length - 1;
2344
- const streamingThis = isStreaming && isLast;
2345
- const text = getMessageText(message);
2346
- const toolCalls = message.tool_calls ?? [];
2347
- const showReasoning = !!message.reasoning && (!isStreaming || index === latestReasoningIndex);
2348
- return /* @__PURE__ */ jsxs9(
2349
- "div",
3472
+ const text = getMessageText(message);
3473
+ const imageParts = getImageParts(message.content);
3474
+ const fileParts = getFileParts(message.content);
3475
+ const failed = message.status === "failed";
3476
+ const failedBadge = failed ? /* @__PURE__ */ jsx12("div", { className: "w-fit rounded-full border border-red-500/30 bg-red-500/10 px-2.5 py-1 text-[10px] font-medium text-red-400", children: "\u751F\u6210\u5931\u8D25" }) : null;
3477
+ const textContent = text ? /* @__PURE__ */ jsx12(
3478
+ "div",
3479
+ {
3480
+ className: cn(
3481
+ "blade-chat-assistant-text",
3482
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3483
+ ),
3484
+ children: /* @__PURE__ */ jsx12(
3485
+ MarkdownContent,
2350
3486
  {
2351
- className: "flex flex-col gap-3",
2352
- children: [
2353
- showReasoning && message.reasoning && /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }),
2354
- text && /* @__PURE__ */ jsx11("div", { className: "blade-chat-assistant-text text-[15px] leading-8 text-[hsl(var(--foreground))]", children: /* @__PURE__ */ jsx11(
2355
- MarkdownContent,
2356
- {
2357
- mode: streamingThis ? "streaming" : "static",
2358
- className: "blade-chat-prose",
2359
- sessionId,
2360
- children: text
2361
- }
2362
- ) }),
2363
- toolCalls.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-2", children: toolCalls.map(
2364
- (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(
2365
- ToolCallBlock,
2366
- {
2367
- toolCall,
2368
- answerData: askAnswers?.[toolCall.id],
2369
- onAnswer,
2370
- answered: sessionStatus !== "waiting_for_input",
2371
- sessionStatus,
2372
- renderer: toolCallRenderer
2373
- },
2374
- toolCall.id
2375
- )
2376
- ) })
2377
- ]
2378
- },
2379
- message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2380
- );
2381
- }),
2382
- isStreaming && !hasAnyContent && /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
3487
+ mode: streaming ? "streaming" : "static",
3488
+ className: "blade-chat-prose",
3489
+ sessionId,
3490
+ children: text
3491
+ }
3492
+ )
3493
+ }
3494
+ ) : null;
3495
+ if (imageParts.length === 0 && fileParts.length === 0) {
3496
+ if (!failed) return textContent;
3497
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-2", children: [
3498
+ failedBadge,
3499
+ textContent
3500
+ ] }) : null;
3501
+ }
3502
+ return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-3", children: [
3503
+ failedBadge,
3504
+ imageParts.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx12(
3505
+ "img",
3506
+ {
3507
+ src: part.image_url.url,
3508
+ alt: "\u6D88\u606F\u9644\u4EF6",
3509
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3510
+ },
3511
+ part.image_url.url
3512
+ )) }) : null,
3513
+ fileParts.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs10(
3514
+ "div",
3515
+ {
3516
+ className: "flex min-w-0 items-center gap-1.5 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.3)] px-2.5 py-1.5 text-xs text-[hsl(var(--muted-foreground))]",
3517
+ title: part.name,
3518
+ children: [
3519
+ /* @__PURE__ */ jsx12(FileText, { size: 12, className: "shrink-0" }),
3520
+ /* @__PURE__ */ jsx12("span", { className: "max-w-56 truncate", children: part.name })
3521
+ ]
3522
+ },
3523
+ `${part.name}-${part.data.slice(0, 32)}`
3524
+ )) }) : null,
3525
+ textContent
2383
3526
  ] });
2384
3527
  }
2385
3528
 
2386
3529
  // src/components/RenderErrorBoundary.tsx
2387
3530
  import { Component } from "react";
2388
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3531
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2389
3532
  function getFirstComponentName(componentStack) {
2390
3533
  const match = componentStack.match(/\n\s+at\s+([^\s(]+)/);
2391
3534
  return match?.[1] ?? null;
@@ -2418,26 +3561,26 @@ var RenderErrorBoundary = class extends Component {
2418
3561
  return children;
2419
3562
  }
2420
3563
  const componentName = getFirstComponentName(componentStack);
2421
- return /* @__PURE__ */ jsx12("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__ */ jsxs10("div", { className: "flex items-start gap-2", children: [
2422
- /* @__PURE__ */ jsx12(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
2423
- /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1", children: [
2424
- /* @__PURE__ */ jsxs10("div", { className: "font-medium", children: [
3564
+ return /* @__PURE__ */ jsx13("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__ */ jsxs11("div", { className: "flex items-start gap-2", children: [
3565
+ /* @__PURE__ */ jsx13(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
3566
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1", children: [
3567
+ /* @__PURE__ */ jsxs11("div", { className: "font-medium", children: [
2425
3568
  label,
2426
3569
  "\u6E32\u67D3\u5931\u8D25"
2427
3570
  ] }),
2428
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
3571
+ /* @__PURE__ */ jsxs11("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
2429
3572
  componentName ? `\u7EC4\u4EF6\uFF1A${componentName}\u3002` : null,
2430
3573
  error.message || "\u53D1\u751F\u4E86\u672A\u9884\u671F\u7684\u6E32\u67D3\u9519\u8BEF\u3002"
2431
3574
  ] }),
2432
- details ? /* @__PURE__ */ jsx12("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
3575
+ details ? /* @__PURE__ */ jsx13("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
2433
3576
  ] })
2434
3577
  ] }) });
2435
3578
  }
2436
3579
  };
2437
3580
 
2438
3581
  // src/components/PostChatFollowupBlock.tsx
2439
- import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef7, useState as useState11 } from "react";
2440
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3582
+ import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef12, useState as useState13 } from "react";
3583
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2441
3584
  function emitInteraction(callback, event) {
2442
3585
  try {
2443
3586
  callback?.(event);
@@ -2447,9 +3590,6 @@ function emitInteraction(callback, event) {
2447
3590
  function basename(path) {
2448
3591
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2449
3592
  }
2450
- function isVideo(path) {
2451
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2452
- }
2453
3593
  function ArtifactCard({
2454
3594
  artifact,
2455
3595
  sessionId,
@@ -2459,10 +3599,10 @@ function ArtifactCard({
2459
3599
  onArtifactOpened
2460
3600
  }) {
2461
3601
  const client = useBladeClient();
2462
- const [downloading, setDownloading] = useState11(false);
3602
+ const [downloading, setDownloading] = useState13(false);
2463
3603
  const name = artifact.label || basename(artifact.target);
2464
3604
  if (artifact.kind === "link") {
2465
- return /* @__PURE__ */ jsxs11(
3605
+ return /* @__PURE__ */ jsxs12(
2466
3606
  "a",
2467
3607
  {
2468
3608
  href: artifact.target,
@@ -2473,51 +3613,55 @@ function ArtifactCard({
2473
3613
  ${artifact.target}`,
2474
3614
  className: "group relative flex min-w-0 items-center gap-1.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-2 py-1.5 text-xs text-[hsl(var(--card-foreground))] hover:bg-[hsl(var(--accent))]",
2475
3615
  children: [
2476
- /* @__PURE__ */ jsx13(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2477
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2478
- /* @__PURE__ */ jsx13(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
3616
+ /* @__PURE__ */ jsx14(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
3617
+ /* @__PURE__ */ jsx14("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
3618
+ /* @__PURE__ */ jsx14(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2479
3619
  ]
2480
3620
  }
2481
3621
  );
2482
3622
  }
2483
- const Icon2 = isVideo(artifact.target) ? Film : File;
2484
- return /* @__PURE__ */ jsxs11(
2485
- "button",
3623
+ const fileName = basename(artifact.target);
3624
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3625
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3626
+ ) : void 0;
3627
+ const handleDownload = async (event) => {
3628
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3629
+ event.preventDefault();
3630
+ if (!sessionId || downloading) return;
3631
+ setDownloading(true);
3632
+ emitInteraction(onInteraction, {
3633
+ type: "artifact_download_started",
3634
+ sessionId,
3635
+ assistantEntryId,
3636
+ artifactIndex,
3637
+ artifactKind: "file"
3638
+ });
3639
+ try {
3640
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3641
+ emitInteraction(onInteraction, {
3642
+ type: "artifact_download_succeeded",
3643
+ sessionId,
3644
+ assistantEntryId,
3645
+ artifactIndex,
3646
+ artifactKind: "file"
3647
+ });
3648
+ } catch {
3649
+ } finally {
3650
+ setDownloading(false);
3651
+ }
3652
+ };
3653
+ return /* @__PURE__ */ jsx14(
3654
+ "a",
2486
3655
  {
2487
- type: "button",
2488
- disabled: !sessionId || downloading,
2489
- onClick: async () => {
2490
- if (!sessionId || downloading) return;
2491
- setDownloading(true);
2492
- emitInteraction(onInteraction, {
2493
- type: "artifact_download_started",
2494
- sessionId,
2495
- assistantEntryId,
2496
- artifactIndex,
2497
- artifactKind: "file"
2498
- });
2499
- try {
2500
- await client.sessions.downloadFile(sessionId, artifact.target, basename(artifact.target));
2501
- emitInteraction(onInteraction, {
2502
- type: "artifact_download_succeeded",
2503
- sessionId,
2504
- assistantEntryId,
2505
- artifactIndex,
2506
- artifactKind: "file"
2507
- });
2508
- } catch {
2509
- } finally {
2510
- setDownloading(false);
2511
- }
2512
- },
2513
- title: name,
2514
- "aria-label": `\u4E0B\u8F7D ${name}`,
2515
- className: "group relative flex min-w-0 items-center gap-1.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-2 py-1.5 text-left text-xs text-[hsl(var(--card-foreground))] hover:bg-[hsl(var(--accent))] disabled:opacity-60",
2516
- children: [
2517
- /* @__PURE__ */ jsx13(Icon2, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2518
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2519
- /* @__PURE__ */ jsx13(Download, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2520
- ]
3656
+ href: downloadUrl,
3657
+ download: fileName,
3658
+ onClick: handleDownload,
3659
+ title: fileName,
3660
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3661
+ "aria-disabled": !sessionId || void 0,
3662
+ "aria-busy": downloading || void 0,
3663
+ className: "min-w-0 cursor-pointer break-all text-xs text-[hsl(var(--primary))] underline aria-disabled:cursor-not-allowed aria-disabled:opacity-60 aria-busy:cursor-wait",
3664
+ children: fileName
2521
3665
  }
2522
3666
  );
2523
3667
  }
@@ -2534,17 +3678,17 @@ function feedbackReasonLabel(reason) {
2534
3678
  }
2535
3679
  function HistoricalResultFeedback({ feedback }) {
2536
3680
  const label = feedbackReasonLabel(feedback.reason);
2537
- return /* @__PURE__ */ jsxs11(
3681
+ return /* @__PURE__ */ jsxs12(
2538
3682
  "section",
2539
3683
  {
2540
3684
  "aria-label": "\u5386\u53F2\u7ED3\u679C\u53CD\u9988",
2541
3685
  className: "mt-3 w-fit max-w-full rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.2)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]",
2542
3686
  children: [
2543
- /* @__PURE__ */ jsxs11("span", { children: [
3687
+ /* @__PURE__ */ jsxs12("span", { children: [
2544
3688
  "\u4F60\u5BF9\u6B64\u8F6E\u7ED3\u679C\u7684\u8BC4\u4EF7\uFF1A",
2545
3689
  feedback.helpful ? "\u6709\u5E2E\u52A9" : "\u6CA1\u5E2E\u52A9"
2546
3690
  ] }),
2547
- label ? /* @__PURE__ */ jsxs11("span", { children: [
3691
+ label ? /* @__PURE__ */ jsxs12("span", { children: [
2548
3692
  " \xB7 ",
2549
3693
  label
2550
3694
  ] }) : null
@@ -2561,15 +3705,15 @@ function ResultFeedback({
2561
3705
  onFeedbackSaved
2562
3706
  }) {
2563
3707
  const client = useBladeClient();
2564
- const [saved, setSaved] = useState11(savedFeedback ?? null);
2565
- const [helpful, setHelpful] = useState11(savedFeedback?.helpful ?? null);
2566
- const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
2567
- const [saving, setSaving] = useState11(false);
2568
- const [saveError, setSaveError] = useState11(false);
2569
- const reportedShown = useRef7(false);
2570
- const latestChoice = useRef7(null);
3708
+ const [saved, setSaved] = useState13(savedFeedback ?? null);
3709
+ const [helpful, setHelpful] = useState13(savedFeedback?.helpful ?? null);
3710
+ const [reason, setReason] = useState13(savedFeedback?.reason ?? null);
3711
+ const [saving, setSaving] = useState13(false);
3712
+ const [saveError, setSaveError] = useState13(false);
3713
+ const reportedShown = useRef12(false);
3714
+ const latestChoice = useRef12(null);
2571
3715
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2572
- useEffect7(() => {
3716
+ useEffect11(() => {
2573
3717
  if (!eligible || reportedShown.current) return;
2574
3718
  reportedShown.current = true;
2575
3719
  emitInteraction(onInteraction, {
@@ -2578,13 +3722,13 @@ function ResultFeedback({
2578
3722
  assistantEntryId: followup.assistant_entry_id
2579
3723
  });
2580
3724
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2581
- useEffect7(() => {
3725
+ useEffect11(() => {
2582
3726
  if (!savedFeedback || latestChoice.current) return;
2583
3727
  setSaved(savedFeedback);
2584
3728
  setHelpful(savedFeedback.helpful);
2585
3729
  setReason(savedFeedback.reason);
2586
3730
  }, [savedFeedback]);
2587
- const submit = useCallback5(
3731
+ const submit = useCallback6(
2588
3732
  async (nextHelpful, nextReason) => {
2589
3733
  if (!sessionId) return;
2590
3734
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2619,15 +3763,15 @@ function ResultFeedback({
2619
3763
  [client, followup.assistant_entry_id, onFeedbackSaved, onInteraction, sessionId]
2620
3764
  );
2621
3765
  if (!eligible) return null;
2622
- return /* @__PURE__ */ jsxs11(
3766
+ return /* @__PURE__ */ jsxs12(
2623
3767
  "section",
2624
3768
  {
2625
3769
  "aria-label": "\u7ED3\u679C\u53CD\u9988",
2626
3770
  className: "flex flex-col gap-2 border-t border-[hsl(var(--border))] pt-3",
2627
3771
  children: [
2628
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u76EE\u524D\u7684\u6574\u4F53\u7ED3\u679C\u6709\u5E2E\u52A9\u5417\uFF1F" }),
2629
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap gap-1.5", children: [
2630
- /* @__PURE__ */ jsx13(
3772
+ /* @__PURE__ */ jsx14("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u76EE\u524D\u7684\u6574\u4F53\u7ED3\u679C\u6709\u5E2E\u52A9\u5417\uFF1F" }),
3773
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap gap-1.5", children: [
3774
+ /* @__PURE__ */ jsx14(
2631
3775
  "button",
2632
3776
  {
2633
3777
  type: "button",
@@ -2638,7 +3782,7 @@ function ResultFeedback({
2638
3782
  children: "\u6709\u5E2E\u52A9"
2639
3783
  }
2640
3784
  ),
2641
- /* @__PURE__ */ jsx13(
3785
+ /* @__PURE__ */ jsx14(
2642
3786
  "button",
2643
3787
  {
2644
3788
  type: "button",
@@ -2650,7 +3794,7 @@ function ResultFeedback({
2650
3794
  }
2651
3795
  )
2652
3796
  ] }),
2653
- helpful === false ? /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-1.5", "aria-label": "\u6CA1\u5E2E\u52A9\u7684\u4E3B\u8981\u539F\u56E0", children: FEEDBACK_REASONS.map((item) => /* @__PURE__ */ jsx13(
3797
+ helpful === false ? /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-1.5", "aria-label": "\u6CA1\u5E2E\u52A9\u7684\u4E3B\u8981\u539F\u56E0", children: FEEDBACK_REASONS.map((item) => /* @__PURE__ */ jsx14(
2654
3798
  "button",
2655
3799
  {
2656
3800
  type: "button",
@@ -2662,9 +3806,9 @@ function ResultFeedback({
2662
3806
  },
2663
3807
  item.value
2664
3808
  )) }) : null,
2665
- saveError ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
2666
- /* @__PURE__ */ jsx13("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
2667
- /* @__PURE__ */ jsx13(
3809
+ saveError ? /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
3810
+ /* @__PURE__ */ jsx14("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
3811
+ /* @__PURE__ */ jsx14(
2668
3812
  "button",
2669
3813
  {
2670
3814
  type: "button",
@@ -2676,7 +3820,7 @@ function ResultFeedback({
2676
3820
  children: "\u91CD\u8BD5"
2677
3821
  }
2678
3822
  )
2679
- ] }) : saved ? /* @__PURE__ */ jsx13("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
3823
+ ] }) : saved ? /* @__PURE__ */ jsx14("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
2680
3824
  ]
2681
3825
  }
2682
3826
  );
@@ -2690,14 +3834,14 @@ function PostChatFollowupBlock({
2690
3834
  savedFeedback,
2691
3835
  onFeedbackSaved
2692
3836
  }) {
2693
- const [expanded, setExpanded] = useState11(false);
2694
- const adopted = useRef7(/* @__PURE__ */ new Set());
2695
- const reportedSuggestions = useRef7(false);
2696
- const reportedArtifacts = useRef7(/* @__PURE__ */ new Set());
2697
- const openedArtifacts = useRef7(/* @__PURE__ */ new Set());
3837
+ const [expanded, setExpanded] = useState13(false);
3838
+ const adopted = useRef12(/* @__PURE__ */ new Set());
3839
+ const reportedSuggestions = useRef12(false);
3840
+ const reportedArtifacts = useRef12(/* @__PURE__ */ new Set());
3841
+ const openedArtifacts = useRef12(/* @__PURE__ */ new Set());
2698
3842
  const artifacts = followup.final_artifacts ?? [];
2699
3843
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2700
- useEffect7(() => {
3844
+ useEffect11(() => {
2701
3845
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2702
3846
  reportedSuggestions.current = true;
2703
3847
  emitInteraction(onInteraction, {
@@ -2727,7 +3871,7 @@ function PostChatFollowupBlock({
2727
3871
  sessionId,
2728
3872
  visibleArtifacts
2729
3873
  ]);
2730
- const reportArtifactOpened = useCallback5(
3874
+ const reportArtifactOpened = useCallback6(
2731
3875
  (artifactIndex, artifactKind) => {
2732
3876
  if (openedArtifacts.current.has(artifactIndex)) return;
2733
3877
  openedArtifacts.current.add(artifactIndex);
@@ -2743,15 +3887,15 @@ function PostChatFollowupBlock({
2743
3887
  );
2744
3888
  if (!followup.recaption && artifacts.length === 0 && followup.suggestions.length === 0 && !followup.feedback_eligible)
2745
3889
  return null;
2746
- return /* @__PURE__ */ jsxs11("div", { className: "mt-3 flex w-fit max-w-full flex-col gap-3 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.28)] p-3 sm:max-w-[680px]", children: [
2747
- followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
2748
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
2749
- /* @__PURE__ */ jsx13(Sparkles, { size: 14 }),
3890
+ return /* @__PURE__ */ jsxs12("div", { className: "mt-3 flex w-fit max-w-full flex-col gap-3 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.28)] p-3 sm:max-w-[680px]", children: [
3891
+ followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
3892
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
3893
+ /* @__PURE__ */ jsx14(Sparkles, { size: 14 }),
2750
3894
  "\u672C\u8F6E\u5C0F\u7ED3"
2751
3895
  ] }),
2752
- followup.recaption ? /* @__PURE__ */ jsx13("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
2753
- artifacts.length > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2754
- /* @__PURE__ */ jsx13("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx13(
3896
+ followup.recaption ? /* @__PURE__ */ jsx14("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
3897
+ artifacts.length > 0 ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
3898
+ /* @__PURE__ */ jsx14("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx14(
2755
3899
  ArtifactCard,
2756
3900
  {
2757
3901
  artifact,
@@ -2763,7 +3907,7 @@ function PostChatFollowupBlock({
2763
3907
  },
2764
3908
  `${artifact.kind}:${artifactIndex}`
2765
3909
  )) }),
2766
- artifacts.length > 3 ? /* @__PURE__ */ jsxs11(
3910
+ artifacts.length > 3 ? /* @__PURE__ */ jsxs12(
2767
3911
  "button",
2768
3912
  {
2769
3913
  type: "button",
@@ -2772,15 +3916,15 @@ function PostChatFollowupBlock({
2772
3916
  className: "flex w-fit items-center gap-0.5 text-[11px] text-[hsl(var(--muted-foreground))]",
2773
3917
  children: [
2774
3918
  expanded ? "\u6536\u8D77" : `\u5C55\u5F00 ${artifacts.length - 3} \u4E2A`,
2775
- /* @__PURE__ */ jsx13(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
3919
+ /* @__PURE__ */ jsx14(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
2776
3920
  ]
2777
3921
  }
2778
3922
  ) : null
2779
3923
  ] }) : null
2780
3924
  ] }) : null,
2781
- followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
2782
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
2783
- followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs11(
3925
+ followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
3926
+ /* @__PURE__ */ jsx14("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
3927
+ followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs12(
2784
3928
  "button",
2785
3929
  {
2786
3930
  type: "button",
@@ -2799,14 +3943,14 @@ function PostChatFollowupBlock({
2799
3943
  },
2800
3944
  className: "group flex items-center gap-2 rounded-xl bg-[hsl(var(--muted)/0.62)] px-3 py-2 text-left text-[13px] disabled:cursor-default disabled:opacity-60",
2801
3945
  children: [
2802
- /* @__PURE__ */ jsx13("span", { children: suggestion }),
2803
- /* @__PURE__ */ jsx13(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
3946
+ /* @__PURE__ */ jsx14("span", { children: suggestion }),
3947
+ /* @__PURE__ */ jsx14(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
2804
3948
  ]
2805
3949
  },
2806
3950
  suggestion
2807
3951
  ))
2808
3952
  ] }) : null,
2809
- /* @__PURE__ */ jsx13(
3953
+ /* @__PURE__ */ jsx14(
2810
3954
  ResultFeedback,
2811
3955
  {
2812
3956
  followup,
@@ -2821,8 +3965,91 @@ function PostChatFollowupBlock({
2821
3965
  }
2822
3966
 
2823
3967
  // src/components/UserMessageBubble.tsx
2824
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
2825
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3968
+ import {
3969
+ chatErrorForDisplay,
3970
+ getFileParts as getFileParts2,
3971
+ getImageParts as getImageParts2,
3972
+ getTextContent as getTextContent2
3973
+ } from "@blade-hq/agent-client";
3974
+
3975
+ // src/lib/whatif-prompt.ts
3976
+ var HEADER_RE = /^以下消息和 step 产物标记为 deprecated_by_rerun,请基于最新用户假设从 step(\d+) 开始完整重新推演,不要复用旧结论。$/;
3977
+ var QUOTE_HEADER_RE = /^\[步骤(\d+)\s*·\s*(.+?)\]$/;
3978
+ var USER_INPUT_TAG = "[\u7528\u6237\u8F93\u5165]";
3979
+ function parseWhatIfPrompt(text) {
3980
+ const lines = text.replace(/\r\n/g, "\n").trimEnd().split("\n");
3981
+ const headerMatch = lines[0]?.match(HEADER_RE);
3982
+ if (!headerMatch) return null;
3983
+ const userTagIdx = lines.indexOf(USER_INPUT_TAG);
3984
+ const hasUserTag = userTagIdx >= 0;
3985
+ const quoteBlockEndExclusive = hasUserTag ? userTagIdx : lines.length;
3986
+ const quoteHeaderIdxs = [];
3987
+ let quoteBlockFound = false;
3988
+ for (let i = 1; i < quoteBlockEndExclusive; i++) {
3989
+ if (!quoteBlockFound && lines[i].trim() === "[\u5F15\u7528]") {
3990
+ quoteBlockFound = true;
3991
+ } else if (quoteBlockFound && QUOTE_HEADER_RE.test(lines[i])) {
3992
+ quoteHeaderIdxs.push(i);
3993
+ }
3994
+ }
3995
+ let legacyUserTextStart = -1;
3996
+ if (!hasUserTag && quoteHeaderIdxs.length > 0) {
3997
+ const lastSnapshotStart = quoteHeaderIdxs.at(-1) + 1;
3998
+ let i = lines.length - 1;
3999
+ while (i >= lastSnapshotStart && lines[i].trim() === "") i -= 1;
4000
+ while (i >= lastSnapshotStart && lines[i].trim() !== "") i -= 1;
4001
+ if (i >= lastSnapshotStart) legacyUserTextStart = i + 1;
4002
+ }
4003
+ const quotes = quoteHeaderIdxs.map((headerIdx, index) => {
4004
+ const match = lines[headerIdx].match(QUOTE_HEADER_RE);
4005
+ const nextHeader = quoteHeaderIdxs[index + 1];
4006
+ const end = nextHeader ?? (legacyUserTextStart >= 0 ? legacyUserTextStart : quoteBlockEndExclusive);
4007
+ const snapshotLines = lines.slice(headerIdx + 1, end);
4008
+ while (snapshotLines.at(-1)?.trim() === "") snapshotLines.pop();
4009
+ return {
4010
+ stepNumber: Number.parseInt(match[1], 10),
4011
+ label: match[2].trim(),
4012
+ snapshot: snapshotLines.join("\n")
4013
+ };
4014
+ });
4015
+ const userTextStart = hasUserTag ? userTagIdx + 1 : legacyUserTextStart;
4016
+ const userLines = userTextStart >= 0 ? lines.slice(userTextStart) : [];
4017
+ while (userLines[0]?.trim() === "") userLines.shift();
4018
+ while (userLines.at(-1)?.trim() === "") userLines.pop();
4019
+ const fromStep = Number.parseInt(headerMatch[1], 10);
4020
+ return {
4021
+ fromStep: Number.isFinite(fromStep) ? fromStep : null,
4022
+ quotes,
4023
+ userText: userLines.join("\n")
4024
+ };
4025
+ }
4026
+
4027
+ // src/components/WhatIfUserBubble.tsx
4028
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4029
+ function WhatIfUserBubble({ parsed, onQuoteClick }) {
4030
+ const { fromStep, quotes, userText } = parsed;
4031
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col items-end gap-2", children: [
4032
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1.5 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.5)] px-2.5 py-0.5 text-[10px] text-[hsl(var(--muted-foreground))]", children: [
4033
+ /* @__PURE__ */ jsx15(RefreshCcw, { size: 10 }),
4034
+ /* @__PURE__ */ jsx15("span", { children: fromStep != null ? `\u91CD\u8DD1\u81EA step ${fromStep}` : "\u91CD\u8DD1" })
4035
+ ] }),
4036
+ quotes.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex max-w-[min(72vw,42rem)] flex-col items-stretch gap-2", children: quotes.map((quote, index) => {
4037
+ const clickable = quote.stepNumber != null && !!onQuoteClick;
4038
+ const label = quote.stepNumber != null ? `\u6B65\u9AA4${quote.stepNumber} \xB7 ${quote.label}` : quote.label;
4039
+ return /* @__PURE__ */ jsxs13("div", { className: "rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card)/0.8)] px-3 py-2 text-left", children: [
4040
+ /* @__PURE__ */ jsxs13("button", { type: "button", disabled: !clickable, onClick: () => clickable && onQuoteClick(quote.stepNumber), className: "inline-flex max-w-full items-center gap-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] disabled:cursor-default", title: clickable ? "\u8DF3\u8F6C\u5230\u5BF9\u5E94\u6B65\u9AA4\u5361\u7247" : void 0, children: [
4041
+ /* @__PURE__ */ jsx15("span", { children: "\u21B3" }),
4042
+ /* @__PURE__ */ jsx15("span", { className: "truncate", children: label })
4043
+ ] }),
4044
+ quote.snapshot ? /* @__PURE__ */ jsx15("div", { className: "mt-1 border-l-2 border-[hsl(var(--accent-foreground)/0.35)] pl-2 text-xs leading-relaxed text-[hsl(var(--foreground)/0.8)]", children: /* @__PURE__ */ jsx15(MarkdownContent, { className: "blade-chat-prose", children: quote.snapshot }) }) : null
4045
+ ] }, `${quote.stepNumber ?? "x"}-${index}`);
4046
+ }) }),
4047
+ userText && /* @__PURE__ */ jsx15("div", { className: "rounded-2xl border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-4 py-2.5 text-sm leading-relaxed text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx15(MarkdownContent, { className: "blade-chat-prose", children: userText }) })
4048
+ ] });
4049
+ }
4050
+
4051
+ // src/components/UserMessageBubble.tsx
4052
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2826
4053
  function isUserMessage(message) {
2827
4054
  return message.role === "user";
2828
4055
  }
@@ -2832,10 +4059,14 @@ function isErrorMessage(message) {
2832
4059
  var isSending = (message) => message.status === "streaming";
2833
4060
  function UserMessageBubble({ message, className }) {
2834
4061
  const text = getTextContent2(message.content).trim();
2835
- const fileParts = getFileParts(message.content);
2836
- const imageParts = getImageParts(message.content);
2837
- return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs12("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
2838
- imageParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx14(
4062
+ const fileParts = getFileParts2(message.content);
4063
+ const imageParts = getImageParts2(message.content);
4064
+ const whatifParsed = text && imageParts.length === 0 && fileParts.length === 0 ? parseWhatIfPrompt(text) : null;
4065
+ if (whatifParsed) {
4066
+ return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsx16("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: /* @__PURE__ */ jsx16(WhatIfUserBubble, { parsed: whatifParsed }) }) });
4067
+ }
4068
+ return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs14("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
4069
+ imageParts.length > 0 && /* @__PURE__ */ jsx16("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx16(
2839
4070
  "img",
2840
4071
  {
2841
4072
  src: part.image_url.url,
@@ -2844,21 +4075,21 @@ function UserMessageBubble({ message, className }) {
2844
4075
  },
2845
4076
  part.image_url.url
2846
4077
  )) }),
2847
- fileParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs12(
4078
+ fileParts.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs14(
2848
4079
  "div",
2849
4080
  {
2850
4081
  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))]",
2851
4082
  children: [
2852
- /* @__PURE__ */ jsx14(FileText, { size: 12, className: "shrink-0" }),
2853
- /* @__PURE__ */ jsx14("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
4083
+ /* @__PURE__ */ jsx16(FileText, { size: 12, className: "shrink-0" }),
4084
+ /* @__PURE__ */ jsx16("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
2854
4085
  ]
2855
4086
  },
2856
4087
  `${part.name}-${part.data.length}`
2857
4088
  )) }),
2858
- text && /* @__PURE__ */ jsx14("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__ */ jsx14(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
2859
- text && isSending(message) && /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
2860
- /* @__PURE__ */ jsx14(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
2861
- /* @__PURE__ */ jsx14("span", { children: "\u53D1\u9001\u4E2D" })
4089
+ text && /* @__PURE__ */ jsx16("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__ */ jsx16(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
4090
+ text && isSending(message) && /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
4091
+ /* @__PURE__ */ jsx16(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
4092
+ /* @__PURE__ */ jsx16("span", { children: "\u53D1\u9001\u4E2D" })
2862
4093
  ] })
2863
4094
  ] }) });
2864
4095
  }
@@ -2866,12 +4097,12 @@ function ErrorMessageBlock({
2866
4097
  message,
2867
4098
  className
2868
4099
  }) {
2869
- const text = getTextContent2(message.content);
2870
- return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat-error-row flex justify-center", className), children: /* @__PURE__ */ jsx14("div", { className: "blade-chat-error-block max-w-[85%] border-l-[3px] border-[hsl(var(--border))] px-4 py-1 text-sm leading-7 text-[hsl(var(--muted-foreground))]", children: text }) });
4100
+ const text = chatErrorForDisplay(getTextContent2(message.content));
4101
+ return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-error-row flex min-w-0 justify-start", className), children: /* @__PURE__ */ jsx16("div", { className: "blade-chat-error-block min-w-0 max-w-full whitespace-pre-wrap break-words border-l-[3px] border-[hsl(var(--border))] px-3 py-1 text-left text-sm leading-7 text-[hsl(var(--muted-foreground))] [overflow-wrap:anywhere]", children: text }) });
2871
4102
  }
2872
4103
 
2873
4104
  // src/components/MessageList.tsx
2874
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4105
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2875
4106
  function parseModeChange(message) {
2876
4107
  if (message.kind !== "mode_change" || typeof message.content !== "string") {
2877
4108
  return null;
@@ -2912,6 +4143,7 @@ function MessageList({
2912
4143
  askAnswers,
2913
4144
  onAnswer,
2914
4145
  toolCallRenderer,
4146
+ hidePlanUpdateTools = false,
2915
4147
  emptyState,
2916
4148
  className,
2917
4149
  sessionId,
@@ -2920,10 +4152,22 @@ function MessageList({
2920
4152
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2921
4153
  onResultFeedbackSaved
2922
4154
  }) {
4155
+ const visibleRootMessages = messages.filter((message) => {
4156
+ if ((message.loop_name ?? "root") !== "root") return false;
4157
+ if (isHiddenInternalMessage(message)) return false;
4158
+ if (message.kind === "context") return false;
4159
+ return message.role !== "tool" || getPlanningDividerKind(message) !== null;
4160
+ });
4161
+ const userMessages = visibleRootMessages.filter((message) => isUserMessage(message));
4162
+ const latestUserMessage = userMessages.at(-1);
4163
+ const latestPromptText = latestUserMessage ? (typeof latestUserMessage.content === "string" ? latestUserMessage.content : latestUserMessage.content.filter((part) => part.type === "text").map((part) => part.text).join("")).replace(/\s+/g, " ").trim() : "";
4164
+ const latestPromptPreview = latestPromptText.length > 80 ? `${latestPromptText.slice(0, 80)}\u2026` : latestPromptText;
4165
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
2923
4166
  const renderBlocks = useMemo7(() => {
2924
4167
  const visible = messages.filter((message) => {
2925
4168
  if ((message.loop_name ?? "root") !== "root") return false;
2926
4169
  if (isHiddenInternalMessage(message)) return false;
4170
+ if (message.kind === "context") return false;
2927
4171
  if (message.kind === "compaction") return true;
2928
4172
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2929
4173
  });
@@ -2993,98 +4237,164 @@ function MessageList({
2993
4237
  }
2994
4238
  return blocks;
2995
4239
  }, [messages, isStreaming]);
2996
- return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs13(StickToBottom, { className: "h-full overflow-y-hidden", initial: "instant", resize: "instant", children: [
2997
- /* @__PURE__ */ jsx15(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsx15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs13("div", { className: "flex min-w-0 flex-col", children: [
2998
- renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs13("div", { className: "blade-chat-empty", children: [
2999
- /* @__PURE__ */ jsx15(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3000
- /* @__PURE__ */ jsx15("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3001
- /* @__PURE__ */ jsx15("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3002
- ] }) : renderBlocks.map((block) => {
3003
- if (block.type === "message") {
3004
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx15(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx15(ErrorMessageBlock, { message: block.message }) : null }, block.key);
3005
- }
3006
- if (block.type === "assistant_turn") {
3007
- const blockFeedback = block.messages.map(
3008
- (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3009
- ).find((feedback) => feedback != null);
3010
- const hasActiveFollowup = Boolean(
3011
- postChatFollowup && block.messages.some(
3012
- (message) => message.entry_id === postChatFollowup.assistant_entry_id
3013
- )
3014
- );
3015
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs13(
3016
- RenderErrorBoundary,
3017
- {
3018
- label: "\u52A9\u624B\u6D88\u606F",
3019
- details: block.key,
3020
- resetKey: getMessageResetSignature(block.messages),
3021
- children: [
3022
- /* @__PURE__ */ jsx15(
3023
- AssistantTurnBlock,
3024
- {
3025
- messages: block.messages,
3026
- isStreaming: block.isStreaming,
3027
- askAnswers,
3028
- onAnswer,
3029
- sessionStatus,
3030
- toolCallRenderer,
3031
- sessionId
3032
- }
3033
- ),
3034
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3035
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
3036
- PostChatFollowupBlock,
3037
- {
3038
- followup: postChatFollowup,
3039
- sessionId,
3040
- onSuggestion,
3041
- isViewer,
3042
- onInteraction: onFollowupInteraction,
3043
- savedFeedback: blockFeedback,
3044
- onFeedbackSaved: onResultFeedbackSaved
3045
- }
3046
- ) : null
3047
- ]
3048
- }
3049
- ) }, block.key);
3050
- }
3051
- if (block.type === "compaction") {
3052
- return /* @__PURE__ */ jsxs13(
3053
- "div",
4240
+ return /* @__PURE__ */ jsxs15("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: [
4241
+ isStreaming ? /* @__PURE__ */ jsx17("output", { className: "sr-only", children: "\u6B63\u5728\u751F\u6210\u56DE\u590D" }) : null,
4242
+ /* @__PURE__ */ jsxs15(
4243
+ StickToBottom,
4244
+ {
4245
+ className: "h-full overflow-y-hidden",
4246
+ initial: "instant",
4247
+ resize: "instant",
4248
+ children: [
4249
+ /* @__PURE__ */ jsx17(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsxs15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: [
4250
+ isStreaming && latestUserMessage && latestPromptPreview ? /* @__PURE__ */ jsx17(
4251
+ "button",
4252
+ {
4253
+ type: "button",
4254
+ className: "sticky top-0 z-20 mb-4 w-full truncate rounded-2xl border border-[hsl(var(--primary)/0.2)] bg-[hsl(var(--background)/0.92)] px-4 py-3 text-left text-sm font-medium shadow-sm backdrop-blur",
4255
+ onClick: () => Array.from(document.querySelectorAll("[data-entry-id]")).find((el) => el.dataset.entryId === latestUserMessage.entry_id)?.scrollIntoView({ behavior: "smooth", block: "start" }),
4256
+ "aria-label": "\u8DF3\u8F6C\u5230\u5F53\u524D\u63D0\u95EE",
4257
+ children: latestPromptPreview
4258
+ }
4259
+ ) : null,
4260
+ /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 flex-col", children: [
4261
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs15("div", { className: "blade-chat-empty", children: [
4262
+ /* @__PURE__ */ jsx17(MessageSquare, { size: 40, strokeWidth: 1.5 }),
4263
+ /* @__PURE__ */ jsx17("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
4264
+ /* @__PURE__ */ jsx17("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
4265
+ ] }) : renderBlocks.map((block) => {
4266
+ if (block.type === "message") {
4267
+ return /* @__PURE__ */ jsx17("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx17(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx17(ErrorMessageBlock, { message: block.message }) : null }, block.key);
4268
+ }
4269
+ if (block.type === "assistant_turn") {
4270
+ const blockFeedback = block.messages.map(
4271
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
4272
+ ).find((feedback) => feedback != null);
4273
+ const hasActiveFollowup = Boolean(
4274
+ postChatFollowup && block.messages.some(
4275
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
4276
+ )
4277
+ );
4278
+ return /* @__PURE__ */ jsx17("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs15(
4279
+ RenderErrorBoundary,
4280
+ {
4281
+ label: "\u52A9\u624B\u6D88\u606F",
4282
+ details: block.key,
4283
+ resetKey: getMessageResetSignature(block.messages),
4284
+ children: [
4285
+ /* @__PURE__ */ jsx17(
4286
+ AssistantTurnBlock,
4287
+ {
4288
+ messages: block.messages,
4289
+ isStreaming: block.isStreaming,
4290
+ askAnswers,
4291
+ onAnswer,
4292
+ sessionStatus,
4293
+ toolCallRenderer,
4294
+ hidePlanUpdateTools,
4295
+ sessionId
4296
+ }
4297
+ ),
4298
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx17(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
4299
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx17(
4300
+ PostChatFollowupBlock,
4301
+ {
4302
+ followup: postChatFollowup,
4303
+ sessionId,
4304
+ onSuggestion,
4305
+ isViewer,
4306
+ onInteraction: onFollowupInteraction,
4307
+ savedFeedback: blockFeedback,
4308
+ onFeedbackSaved: onResultFeedbackSaved
4309
+ }
4310
+ ) : null
4311
+ ]
4312
+ }
4313
+ ) }, block.key);
4314
+ }
4315
+ if (block.type === "compaction") {
4316
+ return /* @__PURE__ */ jsxs15(
4317
+ "div",
4318
+ {
4319
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
4320
+ children: [
4321
+ /* @__PURE__ */ jsx17(Layers, { size: 12 }),
4322
+ /* @__PURE__ */ jsx17("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
4323
+ ]
4324
+ },
4325
+ block.key
4326
+ );
4327
+ }
4328
+ return /* @__PURE__ */ jsx17(PlanningDivider, { kind: block.kind }, block.key);
4329
+ }),
4330
+ sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx17("div", { className: "flex", children: /* @__PURE__ */ jsx17("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
4331
+ ] })
4332
+ ] }) }),
4333
+ /* @__PURE__ */ jsx17(
4334
+ PinLatestUserMessage,
3054
4335
  {
3055
- className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3056
- children: [
3057
- /* @__PURE__ */ jsx15(Layers, { size: 12 }),
3058
- /* @__PURE__ */ jsx15("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3059
- ]
4336
+ userMessageCount: userMessages.length,
4337
+ shouldPinLatestUser,
4338
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3060
4339
  },
3061
- block.key
3062
- );
3063
- }
3064
- return /* @__PURE__ */ jsx15(PlanningDivider, { kind: block.kind }, block.key);
3065
- }),
3066
- sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx15("div", { className: "flex", children: /* @__PURE__ */ jsx15("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
3067
- ] }) }) }),
3068
- /* @__PURE__ */ jsx15(AutoScrollOnUserSend, { userMessageCount: messages.filter((m) => isUserMessage(m)).length }),
3069
- /* @__PURE__ */ jsx15(ScrollToBottomButton, {})
3070
- ] }) });
4340
+ sessionId ?? "no-session"
4341
+ ),
4342
+ /* @__PURE__ */ jsx17(ScrollToBottomButton, {})
4343
+ ]
4344
+ },
4345
+ sessionId ?? "no-session"
4346
+ )
4347
+ ] });
3071
4348
  }
3072
- function AutoScrollOnUserSend({ userMessageCount }) {
3073
- const { scrollToBottom } = useStickToBottomContext();
3074
- const previousCountRef = useRef8(userMessageCount);
3075
- useEffect8(() => {
3076
- if (userMessageCount > previousCountRef.current) {
4349
+ function PinLatestUserMessage({
4350
+ userMessageCount,
4351
+ shouldPinLatestUser,
4352
+ targetKey
4353
+ }) {
4354
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
4355
+ const previousCountRef = useRef13(userMessageCount);
4356
+ const spacerHeightRef = useRef13(0);
4357
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
4358
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
4359
+ const getTargetElement = useCallback7(() => {
4360
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
4361
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
4362
+ }, [contentRef]);
4363
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
4364
+ const setSpacerHeight = useCallback7(
4365
+ (height) => {
4366
+ spacerHeightRef.current = height;
4367
+ const content = contentRef.current;
4368
+ if (!content) return;
4369
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
4370
+ else content.style.removeProperty("--blade-chat-pin-spacer");
4371
+ },
4372
+ [contentRef]
4373
+ );
4374
+ useMessagePin({
4375
+ targetKey,
4376
+ pinTarget: shouldPinLatestUser,
4377
+ getScrollElement,
4378
+ getContentElement,
4379
+ getTargetElement,
4380
+ getSpacerHeight,
4381
+ setSpacerHeight,
4382
+ stopAutoScroll: stopScroll,
4383
+ scrollToBottom
4384
+ });
4385
+ useEffect12(() => {
4386
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3077
4387
  scrollToBottom("instant");
3078
4388
  }
3079
4389
  previousCountRef.current = userMessageCount;
3080
- }, [userMessageCount, scrollToBottom]);
4390
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3081
4391
  return null;
3082
4392
  }
3083
4393
  function ScrollToBottomButton() {
3084
4394
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3085
- const [visible, setVisible] = useState12(false);
3086
- const hideTimerRef = useRef8(null);
3087
- useEffect8(() => {
4395
+ const [visible, setVisible] = useState14(false);
4396
+ const hideTimerRef = useRef13(null);
4397
+ useEffect12(() => {
3088
4398
  if (isAtBottom) {
3089
4399
  if (!hideTimerRef.current) {
3090
4400
  hideTimerRef.current = setTimeout(() => {
@@ -3106,7 +4416,7 @@ function ScrollToBottomButton() {
3106
4416
  }
3107
4417
  };
3108
4418
  }, [isAtBottom]);
3109
- const handleClick = useCallback6(() => {
4419
+ const handleClick = useCallback7(() => {
3110
4420
  if (hideTimerRef.current) {
3111
4421
  clearTimeout(hideTimerRef.current);
3112
4422
  hideTimerRef.current = null;
@@ -3115,7 +4425,7 @@ function ScrollToBottomButton() {
3115
4425
  scrollToBottom();
3116
4426
  }, [scrollToBottom]);
3117
4427
  if (!visible) return null;
3118
- return /* @__PURE__ */ jsxs13(
4428
+ return /* @__PURE__ */ jsxs15(
3119
4429
  "button",
3120
4430
  {
3121
4431
  type: "button",
@@ -3123,25 +4433,25 @@ function ScrollToBottomButton() {
3123
4433
  "aria-label": "\u6EDA\u52A8\u5230\u5E95\u90E8",
3124
4434
  className: "blade-chat-scroll-bottom 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))]",
3125
4435
  children: [
3126
- /* @__PURE__ */ jsx15(ChevronDown, { size: 14 }),
3127
- /* @__PURE__ */ jsx15("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
4436
+ /* @__PURE__ */ jsx17(ChevronDown, { size: 14 }),
4437
+ /* @__PURE__ */ jsx17("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
3128
4438
  ]
3129
4439
  }
3130
4440
  );
3131
4441
  }
3132
4442
  function PlanningDivider({ kind }) {
3133
- return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 py-1", children: [
3134
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
3135
- /* @__PURE__ */ jsxs13("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: [
3136
- /* @__PURE__ */ jsx15(Lightbulb, { size: 12 }),
3137
- /* @__PURE__ */ jsx15("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
4443
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-3 py-1", children: [
4444
+ /* @__PURE__ */ jsx17("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
4445
+ /* @__PURE__ */ jsxs15("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: [
4446
+ /* @__PURE__ */ jsx17(Lightbulb, { size: 12 }),
4447
+ /* @__PURE__ */ jsx17("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
3138
4448
  ] }),
3139
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
4449
+ /* @__PURE__ */ jsx17("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
3140
4450
  ] });
3141
4451
  }
3142
4452
 
3143
4453
  // src/components/ChatSurface.tsx
3144
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4454
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3145
4455
  function themeAttr(theme) {
3146
4456
  return theme === "dark" ? "dark" : void 0;
3147
4457
  }
@@ -3161,6 +4471,7 @@ function ChatSurface({
3161
4471
  onInputChange,
3162
4472
  onSuggestion,
3163
4473
  onSend,
4474
+ onAppend,
3164
4475
  onStop,
3165
4476
  sessionStatus,
3166
4477
  askAnswers,
@@ -3171,9 +4482,11 @@ function ChatSurface({
3171
4482
  onResultFeedbackSaved,
3172
4483
  onFollowupInteraction,
3173
4484
  beforeInput,
4485
+ showPlanUpdates = false,
4486
+ planRevealRevision = 0,
3174
4487
  banner
3175
4488
  }) {
3176
- return /* @__PURE__ */ jsxs14(
4489
+ return /* @__PURE__ */ jsxs16(
3177
4490
  "div",
3178
4491
  {
3179
4492
  "data-theme": themeAttr(theme),
@@ -3182,14 +4495,14 @@ function ChatSurface({
3182
4495
  classNames?.root
3183
4496
  ),
3184
4497
  children: [
3185
- /* @__PURE__ */ jsx16(ConnectionBanner, { connection, className: classNames?.banner }),
4498
+ /* @__PURE__ */ jsx18(ConnectionBanner, { connection, className: classNames?.banner }),
3186
4499
  banner,
3187
- errorMessage && /* @__PURE__ */ jsxs14("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
3188
- /* @__PURE__ */ jsx16(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
3189
- /* @__PURE__ */ jsx16("span", { children: errorMessage })
4500
+ errorMessage && /* @__PURE__ */ jsxs16("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
4501
+ /* @__PURE__ */ jsx18(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
4502
+ /* @__PURE__ */ jsx18("span", { className: "min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: chatErrorForDisplay2(errorMessage) })
3190
4503
  ] }),
3191
4504
  slots?.header,
3192
- /* @__PURE__ */ jsx16(
4505
+ /* @__PURE__ */ jsx18(
3193
4506
  MessageList,
3194
4507
  {
3195
4508
  messages,
@@ -3200,6 +4513,7 @@ function ChatSurface({
3200
4513
  askAnswers,
3201
4514
  onAnswer,
3202
4515
  toolCallRenderer: renderers?.toolCall,
4516
+ hidePlanUpdateTools: showPlanUpdates,
3203
4517
  emptyState: slots?.emptyState,
3204
4518
  className: classNames?.messageList,
3205
4519
  sessionId,
@@ -3209,17 +4523,29 @@ function ChatSurface({
3209
4523
  onFollowupInteraction
3210
4524
  }
3211
4525
  ),
4526
+ showPlanUpdates ? /* @__PURE__ */ jsx18(
4527
+ CurrentPlanPanel,
4528
+ {
4529
+ messages,
4530
+ running: isStreaming,
4531
+ revealRevision: planRevealRevision,
4532
+ sessionId,
4533
+ className: "border-t border-[hsl(var(--border))]"
4534
+ }
4535
+ ) : null,
3212
4536
  beforeInput,
3213
- /* @__PURE__ */ jsx16(
4537
+ /* @__PURE__ */ jsx18(
3214
4538
  ChatInput,
3215
4539
  {
3216
4540
  value: inputText,
3217
4541
  onValueChange: onInputChange,
3218
4542
  onSend,
4543
+ onAppend,
3219
4544
  onStop,
3220
4545
  isStreaming,
3221
4546
  isStopping,
3222
4547
  placeholder,
4548
+ queueKey: sessionId,
3223
4549
  className: classNames?.chatInput
3224
4550
  }
3225
4551
  ),
@@ -3230,13 +4556,13 @@ function ChatSurface({
3230
4556
  }
3231
4557
 
3232
4558
  // src/components/AgentChat.tsx
3233
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
4559
+ import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3234
4560
  function isUnauthorizedError(error) {
3235
4561
  return error instanceof BladeApiError && error.status === 401;
3236
4562
  }
3237
4563
  function LoginCard({ client, onLoggedIn }) {
3238
- const [loggingIn, setLoggingIn] = useState13(false);
3239
- const [loginError, setLoginError] = useState13(null);
4564
+ const [loggingIn, setLoggingIn] = useState15(false);
4565
+ const [loginError, setLoginError] = useState15(null);
3240
4566
  const handleLogin = async () => {
3241
4567
  setLoggingIn(true);
3242
4568
  setLoginError(null);
@@ -3249,11 +4575,11 @@ function LoginCard({ client, onLoggedIn }) {
3249
4575
  setLoggingIn(false);
3250
4576
  }
3251
4577
  };
3252
- return /* @__PURE__ */ jsx17("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs15("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: [
3253
- /* @__PURE__ */ jsx17(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
3254
- /* @__PURE__ */ jsx17("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
3255
- /* @__PURE__ */ jsx17("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" }),
3256
- /* @__PURE__ */ jsx17(
4578
+ return /* @__PURE__ */ jsx19("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs17("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: [
4579
+ /* @__PURE__ */ jsx19(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
4580
+ /* @__PURE__ */ jsx19("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
4581
+ /* @__PURE__ */ jsx19("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" }),
4582
+ /* @__PURE__ */ jsx19(
3257
4583
  "button",
3258
4584
  {
3259
4585
  type: "button",
@@ -3263,20 +4589,20 @@ function LoginCard({ client, onLoggedIn }) {
3263
4589
  children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
3264
4590
  }
3265
4591
  ),
3266
- loginError && /* @__PURE__ */ jsx17("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
4592
+ loginError && /* @__PURE__ */ jsx19("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
3267
4593
  ] }) });
3268
4594
  }
3269
4595
  function AgentChat(props) {
3270
4596
  const client = useBladeClient();
3271
- const [attempt, setAttempt] = useState13(0);
3272
- const [needLogin, setNeedLogin] = useState13(() => !client.hasToken());
4597
+ const [attempt, setAttempt] = useState15(0);
4598
+ const [needLogin, setNeedLogin] = useState15(() => !client.hasToken());
3273
4599
  if (needLogin) {
3274
- return /* @__PURE__ */ jsx17(
4600
+ return /* @__PURE__ */ jsx19(
3275
4601
  "div",
3276
4602
  {
3277
4603
  "data-theme": themeAttr(props.theme),
3278
4604
  className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
3279
- children: /* @__PURE__ */ jsx17(
4605
+ children: /* @__PURE__ */ jsx19(
3280
4606
  LoginCard,
3281
4607
  {
3282
4608
  client,
@@ -3289,7 +4615,7 @@ function AgentChat(props) {
3289
4615
  }
3290
4616
  );
3291
4617
  }
3292
- return /* @__PURE__ */ jsx17(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
4618
+ return /* @__PURE__ */ jsx19(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
3293
4619
  }
3294
4620
  function ChatSessionView({
3295
4621
  sessionId,
@@ -3306,17 +4632,33 @@ function ChatSessionView({
3306
4632
  onUnauthorized
3307
4633
  }) {
3308
4634
  const client = useBladeClient();
4635
+ const [planRevealRevisions, setPlanRevealRevisions] = useState15(
4636
+ () => /* @__PURE__ */ new Map()
4637
+ );
4638
+ const handleSessionConnected = useCallback8((connectedSession) => {
4639
+ return connectedSession.on("toolResult", ({ toolCall, turn, source }) => {
4640
+ if (source === "reconnect_replay" || (turn.loop_id || "root") !== "root" || toolCall.status !== "done" || !isPlanUpdateTool(toolCall) || !parsePlanUpdate(toolCall.arguments)) {
4641
+ return;
4642
+ }
4643
+ setPlanRevealRevisions((current) => {
4644
+ const next = new Map(current);
4645
+ next.set(connectedSession.sessionId, (current.get(connectedSession.sessionId) ?? 0) + 1);
4646
+ return next;
4647
+ });
4648
+ });
4649
+ }, []);
3309
4650
  const { session, state, error } = useAgentSession(sessionId, {
3310
4651
  createOptions,
3311
- onSessionCreated
4652
+ onSessionCreated,
4653
+ onSessionConnected: handleSessionConnected
3312
4654
  });
3313
4655
  const replay = useReplay(session);
3314
- const [stopRequested, setStopRequested] = useState13(false);
3315
- const [inputText, setInputText] = useState13("");
3316
- const [resultFeedback, setResultFeedback] = useState13([]);
4656
+ const [stopRequested, setStopRequested] = useState15(false);
4657
+ const [inputText, setInputText] = useState15("");
4658
+ const [resultFeedback, setResultFeedback] = useState15([]);
3317
4659
  const resolvedSessionId = session?.sessionId;
3318
4660
  const isViewer = state?.viewerRole === "viewer";
3319
- useEffect9(() => {
4661
+ useEffect13(() => {
3320
4662
  setResultFeedback([]);
3321
4663
  if (!resolvedSessionId || isViewer) return;
3322
4664
  let cancelled = false;
@@ -3345,18 +4687,18 @@ function ChatSessionView({
3345
4687
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3346
4688
  [resultFeedback]
3347
4689
  );
3348
- const handleResultFeedbackSaved = useCallback7((saved) => {
4690
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3349
4691
  setResultFeedback((current) => [
3350
4692
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3351
4693
  saved
3352
4694
  ]);
3353
4695
  }, []);
3354
- useEffect9(() => {
4696
+ useEffect13(() => {
3355
4697
  if (session) {
3356
4698
  onSessionReady?.(session);
3357
4699
  }
3358
4700
  }, [session, onSessionReady]);
3359
- useEffect9(() => {
4701
+ useEffect13(() => {
3360
4702
  if (!session) return;
3361
4703
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3362
4704
  setInputText((prev) => `${prev ? `${prev}
@@ -3372,12 +4714,12 @@ ${content}`);
3372
4714
  offInsert();
3373
4715
  };
3374
4716
  }, [session]);
3375
- useEffect9(() => {
4717
+ useEffect13(() => {
3376
4718
  if (isUnauthorizedError(error)) {
3377
4719
  onUnauthorized();
3378
4720
  }
3379
4721
  }, [error, onUnauthorized]);
3380
- useEffect9(() => {
4722
+ useEffect13(() => {
3381
4723
  if (!session || !commands) return;
3382
4724
  const unsubscribes = Object.entries(commands).map(
3383
4725
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3387,6 +4729,7 @@ ${content}`);
3387
4729
  };
3388
4730
  }, [session, commands]);
3389
4731
  const isStreaming = state?.isStreaming ?? false;
4732
+ const planRevealRevision = resolvedSessionId ? planRevealRevisions.get(resolvedSessionId) ?? 0 : 0;
3390
4733
  const isStopping = stopRequested && isStreaming;
3391
4734
  const connectError = error && !isUnauthorizedError(error) ? error.message || "\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : null;
3392
4735
  const errorMessage = connectError ?? state?.errorMessage ?? replay.error?.message ?? null;
@@ -3398,7 +4741,7 @@ ${content}`);
3398
4741
  setStopRequested(true);
3399
4742
  void session?.stop();
3400
4743
  };
3401
- return /* @__PURE__ */ jsx17(
4744
+ return /* @__PURE__ */ jsx19(
3402
4745
  ChatSurface,
3403
4746
  {
3404
4747
  theme,
@@ -3408,8 +4751,8 @@ ${content}`);
3408
4751
  slots,
3409
4752
  placeholder,
3410
4753
  connection: state?.connection ?? "connecting",
3411
- banner: /* @__PURE__ */ jsxs15(Fragment3, { children: [
3412
- /* @__PURE__ */ jsx17(
4754
+ banner: /* @__PURE__ */ jsxs17(Fragment4, { children: [
4755
+ /* @__PURE__ */ jsx19(
3413
4756
  ReplayBar,
3414
4757
  {
3415
4758
  isReplay: replay.isReplay,
@@ -3419,7 +4762,7 @@ ${content}`);
3419
4762
  onExit: () => void replay.exitToAutonomous()
3420
4763
  }
3421
4764
  ),
3422
- /* @__PURE__ */ jsx17(ReplayMismatchPrompt, { mismatch: replay.mismatch })
4765
+ /* @__PURE__ */ jsx19(ReplayMismatchPrompt, { mismatch: replay.mismatch })
3423
4766
  ] }),
3424
4767
  errorMessage,
3425
4768
  messages: state?.messages ?? [],
@@ -3427,11 +4770,16 @@ ${content}`);
3427
4770
  resultFeedbackByEntry,
3428
4771
  onResultFeedbackSaved: handleResultFeedbackSaved,
3429
4772
  isStreaming,
4773
+ showPlanUpdates: true,
4774
+ planRevealRevision,
3430
4775
  isStopping,
3431
4776
  inputText,
3432
4777
  onInputChange: setInputText,
3433
4778
  onSuggestion: setInputText,
3434
4779
  onSend: handleSend,
4780
+ onAppend: (text) => {
4781
+ void session?.send(text, { mode: state?.mode ?? void 0 });
4782
+ },
3435
4783
  onStop: handleStop,
3436
4784
  sessionStatus: state?.status ?? void 0,
3437
4785
  askAnswers: state?.askAnswers,
@@ -3448,11 +4796,11 @@ ${content}`);
3448
4796
  }
3449
4797
 
3450
4798
  // src/components/LlmChat.tsx
3451
- import { useEffect as useEffect10, useMemo as useMemo9, useState as useState15 } from "react";
4799
+ import { useEffect as useEffect14, useMemo as useMemo9, useState as useState17 } from "react";
3452
4800
 
3453
4801
  // src/components/LlmAdvancedSettings.tsx
3454
- import { useState as useState14 } from "react";
3455
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
4802
+ import { useState as useState16 } from "react";
4803
+ import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3456
4804
  var FIELDS = [
3457
4805
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
3458
4806
  { id: "model", label: "\u6A21\u578B", placeholder: "\u6A21\u578B\u540D\u79F0" },
@@ -3498,13 +4846,13 @@ function writeOverride(settings, baseURL, override) {
3498
4846
  }
3499
4847
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3500
4848
  const normalized = normalizeAdvanced(settings);
3501
- const [open, setOpen] = useState14(false);
3502
- const [draft, setDraft] = useState14(override);
4849
+ const [open, setOpen] = useState16(false);
4850
+ const [draft, setDraft] = useState16(override);
3503
4851
  if (!normalized) return null;
3504
4852
  const fields = FIELDS.filter((field) => normalized[field.id]);
3505
4853
  const dirty = Object.keys(override).length > 0;
3506
- return /* @__PURE__ */ jsxs16("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
3507
- /* @__PURE__ */ jsxs16(
4854
+ return /* @__PURE__ */ jsxs18("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
4855
+ /* @__PURE__ */ jsxs18(
3508
4856
  "button",
3509
4857
  {
3510
4858
  type: "button",
@@ -3514,16 +4862,16 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3514
4862
  },
3515
4863
  className: "flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3516
4864
  children: [
3517
- /* @__PURE__ */ jsx18(Settings2, { size: 13 }),
4865
+ /* @__PURE__ */ jsx20(Settings2, { size: 13 }),
3518
4866
  "\u9AD8\u7EA7\u8BBE\u7F6E",
3519
- dirty && /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
4867
+ dirty && /* @__PURE__ */ jsx20("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
3520
4868
  ]
3521
4869
  }
3522
4870
  ),
3523
- open && /* @__PURE__ */ jsxs16("div", { className: "mt-2 flex flex-col gap-2", children: [
3524
- fields.map((field) => /* @__PURE__ */ jsxs16("label", { className: "flex flex-col gap-1", children: [
3525
- /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
3526
- /* @__PURE__ */ jsx18(
4871
+ open && /* @__PURE__ */ jsxs18("div", { className: "mt-2 flex flex-col gap-2", children: [
4872
+ fields.map((field) => /* @__PURE__ */ jsxs18("label", { className: "flex flex-col gap-1", children: [
4873
+ /* @__PURE__ */ jsx20("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
4874
+ /* @__PURE__ */ jsx20(
3527
4875
  "input",
3528
4876
  {
3529
4877
  type: field.secret ? "password" : "text",
@@ -3534,9 +4882,9 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3534
4882
  }
3535
4883
  )
3536
4884
  ] }, field.id)),
3537
- normalized.apiKey && /* @__PURE__ */ jsx18("p", { className: "text-[hsl(var(--muted-foreground))]", children: "\u5BC6\u94A5\u4F1A\u5B58\u5728\u8FD9\u53F0\u6D4F\u89C8\u5668\u91CC\u3002\u53EA\u5728\u4F60\u4FE1\u5F97\u8FC7\u8FD9\u53F0\u673A\u5668\u65F6\u586B\u3002" }),
3538
- /* @__PURE__ */ jsxs16("div", { className: "flex gap-2", children: [
3539
- /* @__PURE__ */ jsx18(
4885
+ normalized.apiKey && /* @__PURE__ */ jsx20("p", { className: "text-[hsl(var(--muted-foreground))]", children: "\u5BC6\u94A5\u4F1A\u5B58\u5728\u8FD9\u53F0\u6D4F\u89C8\u5668\u91CC\u3002\u53EA\u5728\u4F60\u4FE1\u5F97\u8FC7\u8FD9\u53F0\u673A\u5668\u65F6\u586B\u3002" }),
4886
+ /* @__PURE__ */ jsxs18("div", { className: "flex gap-2", children: [
4887
+ /* @__PURE__ */ jsx20(
3540
4888
  "button",
3541
4889
  {
3542
4890
  type: "button",
@@ -3551,7 +4899,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3551
4899
  children: "\u4FDD\u5B58"
3552
4900
  }
3553
4901
  ),
3554
- /* @__PURE__ */ jsx18(
4902
+ /* @__PURE__ */ jsx20(
3555
4903
  "button",
3556
4904
  {
3557
4905
  type: "button",
@@ -3570,7 +4918,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3570
4918
  }
3571
4919
 
3572
4920
  // src/components/LlmChat.tsx
3573
- import { jsx as jsx19 } from "react/jsx-runtime";
4921
+ import { jsx as jsx21 } from "react/jsx-runtime";
3574
4922
  function LlmChat({
3575
4923
  classNames,
3576
4924
  renderers,
@@ -3582,11 +4930,11 @@ function LlmChat({
3582
4930
  onOverrideChange,
3583
4931
  ...options
3584
4932
  }) {
3585
- const [override, setOverride] = useState15(() => readOverride(advanced, options.baseURL));
4933
+ const [override, setOverride] = useState17(() => readOverride(advanced, options.baseURL));
3586
4934
  const effective = { ...options, ...override };
3587
4935
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3588
- const [inputText, setInputText] = useState15("");
3589
- const [stopRequested, setStopRequested] = useState15(false);
4936
+ const [inputText, setInputText] = useState17("");
4937
+ const [stopRequested, setStopRequested] = useState17(false);
3590
4938
  const handle = useMemo9(
3591
4939
  () => ({
3592
4940
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3596,10 +4944,10 @@ ${text}` : text),
3596
4944
  }),
3597
4945
  [send, reset]
3598
4946
  );
3599
- useEffect10(() => {
4947
+ useEffect14(() => {
3600
4948
  onReady?.(handle);
3601
4949
  }, [handle, onReady]);
3602
- return /* @__PURE__ */ jsx19(
4950
+ return /* @__PURE__ */ jsx21(
3603
4951
  ChatSurface,
3604
4952
  {
3605
4953
  theme,
@@ -3624,7 +4972,7 @@ ${text}` : text),
3624
4972
  setStopRequested(true);
3625
4973
  stop();
3626
4974
  },
3627
- beforeInput: advanced ? /* @__PURE__ */ jsx19(
4975
+ beforeInput: advanced ? /* @__PURE__ */ jsx21(
3628
4976
  LlmAdvancedSettingsBar,
3629
4977
  {
3630
4978
  settings: advanced,
@@ -3642,14 +4990,14 @@ ${text}` : text),
3642
4990
  }
3643
4991
 
3644
4992
  // src/components/ChatView.tsx
3645
- import { jsx as jsx20 } from "react/jsx-runtime";
4993
+ import { jsx as jsx22 } from "react/jsx-runtime";
3646
4994
  function ChatView(props) {
3647
4995
  const { mode, llm, onLlmReady, ...rest } = props;
3648
4996
  if (mode === "llm") {
3649
4997
  if (!llm) {
3650
4998
  throw new Error('ChatView: mode="llm" \u9700\u8981\u540C\u65F6\u4F20 llm={{ baseURL, model }}');
3651
4999
  }
3652
- return /* @__PURE__ */ jsx20(
5000
+ return /* @__PURE__ */ jsx22(
3653
5001
  LlmChat,
3654
5002
  {
3655
5003
  ...llm,
@@ -3662,7 +5010,232 @@ function ChatView(props) {
3662
5010
  }
3663
5011
  );
3664
5012
  }
3665
- return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
5013
+ return /* @__PURE__ */ jsx22(AgentChat, { ...rest });
5014
+ }
5015
+
5016
+ // src/components/ContextCard.tsx
5017
+ import {
5018
+ getContextDisplayState,
5019
+ getContextGroupDisplayState
5020
+ } from "@blade-hq/agent-client";
5021
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
5022
+ function ContextCard({ context, className }) {
5023
+ const display = getContextDisplayState(context);
5024
+ return /* @__PURE__ */ jsxs19(
5025
+ "details",
5026
+ {
5027
+ className: `blade-chat-context-card group/context-card rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
5028
+ children: [
5029
+ /* @__PURE__ */ jsxs19("summary", { className: "blade-chat-context-summary flex cursor-pointer list-none items-center gap-2 px-3 py-2.5 [&::-webkit-details-marker]:hidden", children: [
5030
+ /* @__PURE__ */ jsx23(
5031
+ Layers,
5032
+ {
5033
+ size: 15,
5034
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5035
+ "aria-hidden": "true"
5036
+ }
5037
+ ),
5038
+ /* @__PURE__ */ jsxs19("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5039
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
5040
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
5041
+ ] }),
5042
+ /* @__PURE__ */ jsx23(
5043
+ ChevronDown,
5044
+ {
5045
+ size: 14,
5046
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-card:rotate-180",
5047
+ "aria-hidden": "true"
5048
+ }
5049
+ )
5050
+ ] }),
5051
+ /* @__PURE__ */ jsx23("div", { className: "blade-chat-context-detail border-t border-[hsl(var(--border))] px-3 py-2.5 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: display.detail })
5052
+ ]
5053
+ }
5054
+ );
5055
+ }
5056
+ function ContextGroupCard({ contexts, className }) {
5057
+ if (contexts.length === 0) return null;
5058
+ const single = contexts.length === 1 ? getContextDisplayState(contexts[0]) : null;
5059
+ const group = single ? null : getContextGroupDisplayState(contexts);
5060
+ return /* @__PURE__ */ jsxs19("details", { className: `blade-chat-context-card group/context-group rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`, children: [
5061
+ /* @__PURE__ */ jsxs19("summary", { className: "blade-chat-context-summary flex cursor-pointer list-none items-center gap-2 px-3 py-2.5 [&::-webkit-details-marker]:hidden", children: [
5062
+ /* @__PURE__ */ jsx23(
5063
+ Layers,
5064
+ {
5065
+ size: 15,
5066
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5067
+ "aria-hidden": "true"
5068
+ }
5069
+ ),
5070
+ /* @__PURE__ */ jsxs19("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5071
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: single ? single.title : `${group?.title} \xB7 ${group?.count} \u9879` }),
5072
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: single ? single.summary : group?.summary })
5073
+ ] }),
5074
+ /* @__PURE__ */ jsx23(
5075
+ ChevronDown,
5076
+ {
5077
+ size: 14,
5078
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-group:rotate-180",
5079
+ "aria-hidden": "true"
5080
+ }
5081
+ )
5082
+ ] }),
5083
+ single ? /* @__PURE__ */ jsx23("div", { className: "blade-chat-context-detail border-t border-[hsl(var(--border))] px-3 py-2.5 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: single.detail }) : /* @__PURE__ */ jsx23("div", { className: "blade-chat-context-group-items flex flex-col gap-1.5 border-t border-[hsl(var(--border))] p-2", children: contexts.map((context) => /* @__PURE__ */ jsx23(
5084
+ ContextCard,
5085
+ {
5086
+ context
5087
+ },
5088
+ `${context.context_kind}:${context.context_key}`
5089
+ )) })
5090
+ ] });
5091
+ }
5092
+
5093
+ // src/components/SessionMemoryToggle.tsx
5094
+ import { useCallback as useCallback9, useEffect as useEffect15, useRef as useRef14, useState as useState18, useSyncExternalStore as useSyncExternalStore2 } from "react";
5095
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
5096
+ var saveStates = /* @__PURE__ */ new WeakMap();
5097
+ function getSaveState(client, sessionId) {
5098
+ let clientStates = saveStates.get(client);
5099
+ if (!clientStates) {
5100
+ clientStates = /* @__PURE__ */ new Map();
5101
+ saveStates.set(client, clientStates);
5102
+ }
5103
+ let state = clientStates.get(sessionId);
5104
+ if (!state) {
5105
+ state = { saving: false, listeners: /* @__PURE__ */ new Set() };
5106
+ clientStates.set(sessionId, state);
5107
+ }
5108
+ return state;
5109
+ }
5110
+ function notify(state) {
5111
+ for (const listener of state.listeners) listener();
5112
+ }
5113
+ function cleanupSaveState(client, sessionId, state) {
5114
+ if (state.saving || state.listeners.size > 0) return;
5115
+ const clientStates = saveStates.get(client);
5116
+ if (clientStates?.get(sessionId) === state) clientStates.delete(sessionId);
5117
+ }
5118
+ function SessionMemoryToggle({
5119
+ sessionId,
5120
+ enabled,
5121
+ client: clientProp,
5122
+ disabled = false,
5123
+ label = "\u5F53\u524D\u4F1A\u8BDD\u4F7F\u7528\u8BB0\u5FC6",
5124
+ className,
5125
+ labelClassName,
5126
+ inputClassName,
5127
+ onSaved,
5128
+ onError
5129
+ }) {
5130
+ const contextClient = useOptionalBladeClient();
5131
+ const client = clientProp ?? contextClient;
5132
+ if (!client) {
5133
+ throw new Error("SessionMemoryToggle \u5FC5\u987B\u5728 <BladeProvider> \u5185\u4F7F\u7528\u6216\u663E\u5F0F\u4F20\u5165 client");
5134
+ }
5135
+ const saveState = getSaveState(client, sessionId);
5136
+ const subscribe = useCallback9(
5137
+ (listener) => {
5138
+ saveState.listeners.add(listener);
5139
+ return () => {
5140
+ saveState.listeners.delete(listener);
5141
+ cleanupSaveState(client, sessionId, saveState);
5142
+ };
5143
+ },
5144
+ [client, saveState, sessionId]
5145
+ );
5146
+ const getSaving = useCallback9(() => saveState.saving, [saveState]);
5147
+ const saving = useSyncExternalStore2(
5148
+ subscribe,
5149
+ getSaving,
5150
+ getSaving
5151
+ );
5152
+ const [draftEnabled, setDraftEnabled] = useState18(enabled);
5153
+ const activeSessionIdRef = useRef14(sessionId);
5154
+ activeSessionIdRef.current = sessionId;
5155
+ useEffect15(() => {
5156
+ setDraftEnabled(enabled);
5157
+ }, [enabled, sessionId]);
5158
+ const update = useCallback9(
5159
+ (nextEnabled) => {
5160
+ const currentSaveState = getSaveState(client, sessionId);
5161
+ if (currentSaveState.saving) return;
5162
+ currentSaveState.saving = true;
5163
+ notify(currentSaveState);
5164
+ setDraftEnabled(nextEnabled);
5165
+ void client.sessions.updateSessionMemory(sessionId, nextEnabled).then(
5166
+ (updated) => {
5167
+ if (activeSessionIdRef.current === sessionId) {
5168
+ setDraftEnabled(updated.memory_enabled);
5169
+ }
5170
+ onSaved?.(sessionId, updated.memory_enabled);
5171
+ },
5172
+ (error) => {
5173
+ if (activeSessionIdRef.current === sessionId) setDraftEnabled(enabled);
5174
+ onError?.(error);
5175
+ }
5176
+ ).finally(() => {
5177
+ currentSaveState.saving = false;
5178
+ notify(currentSaveState);
5179
+ cleanupSaveState(client, sessionId, currentSaveState);
5180
+ });
5181
+ },
5182
+ [client, enabled, onError, onSaved, sessionId]
5183
+ );
5184
+ return /* @__PURE__ */ jsxs20("label", { className: cn("flex items-center justify-between", className), children: [
5185
+ /* @__PURE__ */ jsx24("span", { className: labelClassName, children: label }),
5186
+ /* @__PURE__ */ jsx24(
5187
+ "input",
5188
+ {
5189
+ type: "checkbox",
5190
+ checked: draftEnabled,
5191
+ onChange: (event) => update(event.target.checked),
5192
+ disabled: disabled || saving,
5193
+ className: inputClassName
5194
+ }
5195
+ )
5196
+ ] });
5197
+ }
5198
+
5199
+ // src/lib/agent-computer-command.ts
5200
+ var COMPUTER_LAUNCH_COMMAND_PATTERN = /(?:^|[\n;&|(]\s*)computer\s+launch(?:\s|$)/;
5201
+ function isAgentComputerCommand(command) {
5202
+ return COMPUTER_LAUNCH_COMMAND_PATTERN.test(command);
5203
+ }
5204
+ function isAgentComputerToolCall(argumentsJson) {
5205
+ if (!argumentsJson) return false;
5206
+ let command;
5207
+ try {
5208
+ const parsed = JSON.parse(argumentsJson);
5209
+ if (typeof parsed !== "object" || parsed === null) return false;
5210
+ command = parsed.command;
5211
+ } catch {
5212
+ return false;
5213
+ }
5214
+ return typeof command === "string" && isAgentComputerCommand(command);
5215
+ }
5216
+ var LAUNCH_SUCCESS_MARKER = "\u5DF2\u542F\u52A8 ";
5217
+ function resultContainsLaunchSuccessMarker(result, depth = 0) {
5218
+ if (depth > 2) return false;
5219
+ if (typeof result === "string") {
5220
+ if (result.includes(LAUNCH_SUCCESS_MARKER)) return true;
5221
+ try {
5222
+ return resultContainsLaunchSuccessMarker(JSON.parse(result), depth + 1);
5223
+ } catch {
5224
+ return false;
5225
+ }
5226
+ }
5227
+ if (typeof result === "object" && result !== null) {
5228
+ for (const value of Object.values(result)) {
5229
+ if (typeof value === "string" && value.includes(LAUNCH_SUCCESS_MARKER)) return true;
5230
+ }
5231
+ }
5232
+ return false;
5233
+ }
5234
+ function classifyAgentComputerLaunchOutcome(toolCall) {
5235
+ if (toolCall.status === "error" || toolCall.status === "cancelled") return "failed";
5236
+ if (toolCall.status !== "done") return "pending";
5237
+ if (toolCall.result === void 0 || toolCall.result === null) return "unknown";
5238
+ return resultContainsLaunchSuccessMarker(toolCall.result) ? "succeeded" : "failed";
3666
5239
  }
3667
5240
 
3668
5241
  // src/index.ts
@@ -3671,13 +5244,32 @@ export {
3671
5244
  AgentChat,
3672
5245
  BladeProvider,
3673
5246
  ChatView,
5247
+ ContextCard,
5248
+ ContextGroupCard,
5249
+ CurrentPlanPanel,
3674
5250
  LlmChat,
3675
5251
  MarkdownContent,
5252
+ MemoryRefsHint,
5253
+ PLAN_AUTO_COLLAPSE_MS,
5254
+ PlanUpdateBlock,
3676
5255
  ReplayBar,
3677
5256
  ReplayMismatchPrompt,
5257
+ SessionMemoryToggle,
5258
+ WhatIfUserBubble,
5259
+ classifyAgentComputerLaunchOutcome,
5260
+ collectMemoryRefs,
5261
+ getPlanUpdateDisplayState,
5262
+ isAgentComputerCommand,
5263
+ isAgentComputerToolCall,
5264
+ isPlanUpdateTool,
5265
+ normalizeAdjacentUrlFormatting,
5266
+ parsePlanUpdate,
5267
+ parseWhatIfPrompt,
5268
+ pickCurrentPlanStep,
3678
5269
  useAgentSession,
3679
5270
  useBladeClient,
3680
5271
  useLlmChat,
5272
+ useMessagePin,
3681
5273
  useReplay
3682
5274
  };
3683
5275
  /*! Bundled license information:
@@ -3689,29 +5281,35 @@ lucide-react/dist/esm/createLucideIcon.js:
3689
5281
  lucide-react/dist/esm/icons/arrow-right.js:
3690
5282
  lucide-react/dist/esm/icons/arrow-up-right.js:
3691
5283
  lucide-react/dist/esm/icons/arrow-up.js:
5284
+ lucide-react/dist/esm/icons/book-open.js:
3692
5285
  lucide-react/dist/esm/icons/bot.js:
3693
- lucide-react/dist/esm/icons/brain.js:
3694
5286
  lucide-react/dist/esm/icons/check.js:
3695
5287
  lucide-react/dist/esm/icons/chevron-down.js:
3696
5288
  lucide-react/dist/esm/icons/chevron-right.js:
3697
5289
  lucide-react/dist/esm/icons/circle-alert.js:
5290
+ lucide-react/dist/esm/icons/circle-dot.js:
5291
+ lucide-react/dist/esm/icons/circle.js:
3698
5292
  lucide-react/dist/esm/icons/copy.js:
3699
- lucide-react/dist/esm/icons/download.js:
5293
+ lucide-react/dist/esm/icons/earth.js:
5294
+ lucide-react/dist/esm/icons/file-pen-line.js:
3700
5295
  lucide-react/dist/esm/icons/file-text.js:
3701
- lucide-react/dist/esm/icons/file.js:
3702
- lucide-react/dist/esm/icons/film.js:
3703
5296
  lucide-react/dist/esm/icons/globe.js:
3704
5297
  lucide-react/dist/esm/icons/layers.js:
3705
5298
  lucide-react/dist/esm/icons/lightbulb.js:
5299
+ lucide-react/dist/esm/icons/list-checks.js:
3706
5300
  lucide-react/dist/esm/icons/loader-circle.js:
3707
5301
  lucide-react/dist/esm/icons/lock-keyhole.js:
3708
5302
  lucide-react/dist/esm/icons/message-square-more.js:
3709
5303
  lucide-react/dist/esm/icons/message-square.js:
3710
5304
  lucide-react/dist/esm/icons/play.js:
5305
+ lucide-react/dist/esm/icons/refresh-ccw.js:
5306
+ lucide-react/dist/esm/icons/search.js:
3711
5307
  lucide-react/dist/esm/icons/settings-2.js:
3712
5308
  lucide-react/dist/esm/icons/sparkles.js:
3713
5309
  lucide-react/dist/esm/icons/square.js:
5310
+ lucide-react/dist/esm/icons/terminal.js:
3714
5311
  lucide-react/dist/esm/icons/triangle-alert.js:
5312
+ lucide-react/dist/esm/icons/wrench.js:
3715
5313
  lucide-react/dist/esm/icons/x.js:
3716
5314
  lucide-react/dist/esm/lucide-react.js:
3717
5315
  (**