@blade-hq/agent-react 2610.0.0-beta.4 → 2610.0.0-beta.41

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,19 +1144,35 @@ var X = createLucideIcon("X", [
854
1144
  ]);
855
1145
 
856
1146
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState12 } from "react";
1147
+ import { useCallback as useCallback8, useEffect as useEffect12, useMemo as useMemo8, useState as useState15 } from "react";
858
1148
 
859
1149
  // src/lib/utils.ts
860
1150
  function cn(...inputs) {
861
1151
  return clsx(inputs);
862
1152
  }
863
1153
  async function copyToClipboard(text) {
864
- try {
865
- await navigator.clipboard.writeText(text);
866
- return true;
867
- } catch {
1154
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
1155
+ if (clipboard && typeof clipboard.writeText === "function") {
1156
+ try {
1157
+ await clipboard.writeText(text);
1158
+ return true;
1159
+ } catch {
1160
+ }
1161
+ }
1162
+ if (typeof document === "undefined" || typeof document.execCommand !== "function") {
868
1163
  return false;
869
1164
  }
1165
+ const textarea = document.createElement("textarea");
1166
+ textarea.value = text;
1167
+ textarea.style.position = "fixed";
1168
+ textarea.style.opacity = "0";
1169
+ document.body.appendChild(textarea);
1170
+ textarea.select();
1171
+ try {
1172
+ return document.execCommand("copy");
1173
+ } finally {
1174
+ document.body.removeChild(textarea);
1175
+ }
870
1176
  }
871
1177
 
872
1178
  // src/components/ReplayBar.tsx
@@ -981,164 +1287,581 @@ function ReplayMismatchPrompt({ mismatch, className }) {
981
1287
  );
982
1288
  }
983
1289
 
984
- // src/components/ChatInput.tsx
985
- 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();
1008
- }
1009
- };
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)]"
1026
- }
1027
- ),
1028
- isStreaming ? /* @__PURE__ */ jsx4(
1029
- "button",
1030
- {
1031
- 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" })
1038
- }
1039
- ) : /* @__PURE__ */ jsx4(
1040
- "button",
1041
- {
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 })
1049
- }
1050
- )
1051
- ] }) });
1052
- }
1290
+ // src/components/PlanUpdateBlock.tsx
1291
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1053
1292
 
1054
- // src/components/ConnectionBanner.tsx
1055
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1056
- function ConnectionBanner({ connection, className }) {
1057
- if (connection === "connected" || connection === "connecting") {
1293
+ // src/components/display-utils.ts
1294
+ var TOOL_NAME_ALIASES = {
1295
+ agent: "Agent",
1296
+ ask_user_question: "AskUserQuestion",
1297
+ bash: "Bash",
1298
+ bg_bash: "BgBash",
1299
+ edit: "Edit",
1300
+ exit_plan_mode: "ExitPlanMode",
1301
+ file_edit: "Edit",
1302
+ file_read: "Read",
1303
+ file_write: "Write",
1304
+ finish_task: "FinishTask",
1305
+ glob: "Glob",
1306
+ grep: "Grep",
1307
+ kb_search: "KbSearch",
1308
+ ls: "Ls",
1309
+ multi_edit: "MultiEdit",
1310
+ read: "Read",
1311
+ read_skill: "ReadSkill",
1312
+ update_plan: "UpdatePlan",
1313
+ web_fetch: "WebFetch",
1314
+ web_search: "WebSearch",
1315
+ write: "Write"
1316
+ };
1317
+ var TOOL_DISPLAY_LABELS = {
1318
+ Bash: "\u6267\u884C\u547D\u4EE4",
1319
+ BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1320
+ Read: "\u8BFB\u53D6\u6587\u4EF6",
1321
+ Write: "\u5199\u5165\u6587\u4EF6",
1322
+ Edit: "\u7F16\u8F91\u6587\u4EF6",
1323
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1324
+ Ls: "\u5217\u51FA\u76EE\u5F55",
1325
+ Glob: "\u5339\u914D\u6587\u4EF6",
1326
+ Grep: "\u641C\u7D22\u6587\u672C",
1327
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1328
+ WebSearch: "\u641C\u7D22\u7F51\u9875",
1329
+ WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1330
+ Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1331
+ AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1332
+ ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1333
+ FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1334
+ ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1335
+ ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1336
+ GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1337
+ };
1338
+ function safeParseJson(value) {
1339
+ if (!value) return null;
1340
+ try {
1341
+ return JSON.parse(value);
1342
+ } catch {
1058
1343
  return null;
1059
1344
  }
1060
- const reconnecting = connection === "reconnecting";
1061
- return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1062
- "div",
1063
- {
1064
- className: cn(
1065
- "mx-auto flex max-w-3xl items-start gap-3 rounded-2xl border px-4 py-3",
1066
- reconnecting ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1067
- ),
1068
- children: [
1069
- /* @__PURE__ */ jsx5("span", { className: "mt-0.5 shrink-0", children: reconnecting ? /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx5(TriangleAlert, { size: 14 }) }),
1070
- /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
1071
- /* @__PURE__ */ jsx5("div", { className: "text-sm font-medium", children: reconnecting ? "\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026" : "\u8FDE\u63A5\u5DF2\u65AD\u5F00" }),
1072
- /* @__PURE__ */ jsx5("div", { className: "text-xs opacity-80", children: "\u6D88\u606F\u540C\u6B65\u53EF\u80FD\u4F1A\u5EF6\u8FDF\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
1073
- ] })
1074
- ]
1075
- }
1076
- ) });
1077
1345
  }
1078
-
1079
- // src/components/MessageList.tsx
1080
- import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1081
- import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef7, useState as useState11 } from "react";
1082
-
1083
- // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1084
- import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
1085
- var DEFAULT_SPRING_ANIMATION = {
1086
- /**
1087
- * A value from 0 to 1, on how much to damp the animation.
1088
- * 0 means no damping, 1 means full damping.
1089
- *
1090
- * @default 0.7
1091
- */
1092
- damping: 0.7,
1093
- /**
1094
- * The stiffness of how fast/slow the animation gets up to speed.
1095
- *
1096
- * @default 0.05
1097
- */
1098
- stiffness: 0.05,
1099
- /**
1100
- * The inertial mass associated with the animation.
1101
- * Higher numbers make the animation slower.
1102
- *
1103
- * @default 1.25
1104
- */
1105
- mass: 1.25
1106
- };
1107
- var STICK_TO_BOTTOM_OFFSET_PX = 70;
1108
- var SIXTY_FPS_INTERVAL_MS = 1e3 / 60;
1109
- var RETAIN_ANIMATION_DURATION_MS = 350;
1110
- var mouseDown = false;
1111
- globalThis.document?.addEventListener("mousedown", () => {
1112
- mouseDown = true;
1113
- });
1114
- globalThis.document?.addEventListener("mouseup", () => {
1115
- mouseDown = false;
1116
- });
1117
- globalThis.document?.addEventListener("click", () => {
1118
- mouseDown = false;
1119
- });
1120
- var useStickToBottom = (options = {}) => {
1121
- const [escapedFromLock, updateEscapedFromLock] = useState4(false);
1122
- const [isAtBottom, updateIsAtBottom] = useState4(options.initial !== false);
1123
- const [isNearBottom, setIsNearBottom] = useState4(false);
1124
- const optionsRef = useRef3(null);
1125
- optionsRef.current = options;
1126
- const isSelecting = useCallback3(() => {
1127
- if (!mouseDown) {
1128
- return false;
1129
- }
1130
- const selection = window.getSelection();
1131
- if (!selection || !selection.rangeCount) {
1132
- return false;
1133
- }
1134
- const range = selection.getRangeAt(0);
1135
- return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1136
- }, []);
1137
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1138
- state.isAtBottom = isAtBottom2;
1346
+ function getStringArgValue(args, key) {
1347
+ const value = args?.[key];
1348
+ return typeof value === "string" ? value.trim() : "";
1349
+ }
1350
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1351
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1352
+ function getSkillNameFromFilePath(filePath) {
1353
+ if (!filePath) return null;
1354
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1355
+ const fileName = segments.pop();
1356
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1357
+ const dirName = segments.pop();
1358
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1359
+ return dirName;
1360
+ }
1361
+ function formatToolName(name) {
1362
+ const trimmed = name.trim();
1363
+ if (!trimmed) return name;
1364
+ const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1365
+ const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1366
+ return TOOL_NAME_ALIASES[normalized] ?? stripped;
1367
+ }
1368
+ function getToolDisplayLabel(toolCall) {
1369
+ const normalized = formatToolName(toolCall.name);
1370
+ const args = safeParseJson(toolCall.arguments);
1371
+ const displayName = toolCall.display_name?.trim() ?? "";
1372
+ const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1373
+ const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1374
+ if (metaDisplayName) {
1375
+ return metaDisplayName;
1376
+ }
1377
+ const description = getStringArgValue(args, "description");
1378
+ if (normalized === "BgBash") {
1379
+ return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1380
+ }
1381
+ if (normalized === "ReadSkill") {
1382
+ const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1383
+ return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1384
+ }
1385
+ if (normalized === "Read") {
1386
+ const skillName = getSkillNameFromFilePath(
1387
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1388
+ );
1389
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1390
+ }
1391
+ if (normalized === "FinishTask") {
1392
+ const title = getStringArgValue(args, "title");
1393
+ return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1394
+ }
1395
+ return description || baseLabel;
1396
+ }
1397
+ function getToolTone(status) {
1398
+ if (status === "error" || status === "cancelled") return "red";
1399
+ if (status === "awaiting_answer") return "amber";
1400
+ if (status === "pending") return "blue";
1401
+ return "emerald";
1402
+ }
1403
+ function getToolStatusLabel(status) {
1404
+ if (status === "pending") return "\u8FD0\u884C\u4E2D";
1405
+ if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1406
+ if (status === "error") return "\u9519\u8BEF";
1407
+ if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1408
+ return "\u5B8C\u6210";
1409
+ }
1410
+ function formatToolDuration(ms) {
1411
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
1412
+ const seconds = ms / 1e3;
1413
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
1414
+ const minutes = Math.floor(seconds / 60);
1415
+ const remainingSeconds = Math.round(seconds % 60);
1416
+ return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
1417
+ }
1418
+ function formatToolArgs(args) {
1419
+ try {
1420
+ return JSON.stringify(JSON.parse(args), null, 2);
1421
+ } catch {
1422
+ return args;
1423
+ }
1424
+ }
1425
+ var RESULT_PREVIEW_LIMIT = 4e3;
1426
+ function formatToolResult(result) {
1427
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1428
+ if (text == null) return "";
1429
+ if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1430
+ return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1431
+ \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
1432
+ }
1433
+
1434
+ // src/components/PlanUpdateBlock.tsx
1435
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1436
+ var PLAN_STEP_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
1437
+ var PLAN_AUTO_COLLAPSE_MS = 5e3;
1438
+ function isPlanUpdateTool(toolCall) {
1439
+ return formatToolName(toolCall.name) === "UpdatePlan";
1440
+ }
1441
+ function getPlanUpdateDisplayState(messages) {
1442
+ let current = null;
1443
+ let latestAttempt = null;
1444
+ let latestAttemptStreaming = false;
1445
+ for (const message of messages) {
1446
+ if ((message.loop_name ?? "root") !== "root") continue;
1447
+ for (const toolCall of message.tool_calls ?? []) {
1448
+ if (!isPlanUpdateTool(toolCall)) continue;
1449
+ latestAttempt = toolCall;
1450
+ latestAttemptStreaming = message.status === "streaming";
1451
+ if (toolCall.status === "done" && parsePlanUpdate(toolCall.arguments)) current = toolCall;
1452
+ }
1453
+ }
1454
+ return {
1455
+ current,
1456
+ updating: latestAttempt?.status === "pending" && latestAttemptStreaming
1457
+ };
1458
+ }
1459
+ function parsePlanUpdate(argumentsJson) {
1460
+ try {
1461
+ const raw = JSON.parse(argumentsJson);
1462
+ if (!raw || typeof raw !== "object") return null;
1463
+ const candidate = raw;
1464
+ if (!Array.isArray(candidate.plan)) return null;
1465
+ const plan = candidate.plan.map((item) => {
1466
+ if (!item || typeof item !== "object") return null;
1467
+ const step = item.step;
1468
+ const status = item.status;
1469
+ if (typeof step !== "string" || !step.trim() || typeof status !== "string" || !PLAN_STEP_STATUSES.has(status)) {
1470
+ return null;
1471
+ }
1472
+ return { step: step.trim(), status };
1473
+ });
1474
+ if (plan.some((item) => item === null)) return null;
1475
+ if (plan.filter((item) => item?.status === "in_progress").length > 1) return null;
1476
+ return { plan };
1477
+ } catch {
1478
+ return null;
1479
+ }
1480
+ }
1481
+ function pickCurrentPlanStep(plan) {
1482
+ return plan.find((item) => item.status === "in_progress") ?? plan.find((item) => item.status === "pending") ?? plan[plan.length - 1] ?? null;
1483
+ }
1484
+ function PlanStepIcon({
1485
+ status,
1486
+ size = 17,
1487
+ running = false
1488
+ }) {
1489
+ if (status === "completed") {
1490
+ return /* @__PURE__ */ jsx4(Check, { size, strokeWidth: 2, className: "shrink-0 text-emerald-500" });
1491
+ }
1492
+ if (status === "in_progress") {
1493
+ 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" });
1494
+ }
1495
+ return /* @__PURE__ */ jsx4(Circle, { size, className: "shrink-0 text-[hsl(var(--muted-foreground))]/60" });
1496
+ }
1497
+ function PlanUpdateBlock({
1498
+ toolCall,
1499
+ running = false,
1500
+ autoReveal = false
1501
+ }) {
1502
+ const updateKey = `${toolCall.id}:${toolCall.arguments}`;
1503
+ const revealKey = autoReveal ? updateKey : null;
1504
+ const [collapsed, setCollapsed] = useState4(!autoReveal);
1505
+ const collapseTimerRef = useRef4(null);
1506
+ const data = parsePlanUpdate(toolCall.arguments);
1507
+ useEffect4(() => {
1508
+ if (!revealKey) return;
1509
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1510
+ setCollapsed(false);
1511
+ collapseTimerRef.current = setTimeout(() => {
1512
+ setCollapsed(true);
1513
+ collapseTimerRef.current = null;
1514
+ }, PLAN_AUTO_COLLAPSE_MS);
1515
+ }, [revealKey]);
1516
+ useEffect4(
1517
+ () => () => {
1518
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1519
+ },
1520
+ []
1521
+ );
1522
+ if (!data) return null;
1523
+ const completed = data.plan.filter((item) => item.status === "completed").length;
1524
+ const currentStep = pickCurrentPlanStep(data.plan);
1525
+ const pausedAtCurrentStep = !running && currentStep?.status === "in_progress";
1526
+ return /* @__PURE__ */ jsxs3("section", { className: "overflow-hidden", children: [
1527
+ /* @__PURE__ */ jsxs3(
1528
+ "button",
1529
+ {
1530
+ type: "button",
1531
+ "aria-expanded": !collapsed,
1532
+ onClick: () => {
1533
+ if (collapseTimerRef.current) {
1534
+ clearTimeout(collapseTimerRef.current);
1535
+ collapseTimerRef.current = null;
1536
+ }
1537
+ setCollapsed((value) => !value);
1538
+ },
1539
+ className: cn(
1540
+ "flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[hsl(var(--muted)/0.3)]",
1541
+ !collapsed && "border-b border-[hsl(var(--border))]"
1542
+ ),
1543
+ children: [
1544
+ collapsed && currentStep ? /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs text-[hsl(var(--foreground))]", children: [
1545
+ /* @__PURE__ */ jsx4(PlanStepIcon, { status: currentStep.status, size: 14, running }),
1546
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: currentStep.step }),
1547
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[11px] text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1548
+ ] }) : /* @__PURE__ */ jsxs3("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-[11px] text-[hsl(var(--muted-foreground))]", children: [
1549
+ /* @__PURE__ */ jsx4(ListChecks, { size: 14, className: "shrink-0", "aria-hidden": "true" }),
1550
+ /* @__PURE__ */ jsx4("span", { className: "truncate", children: "\u4EFB\u52A1\u8FDB\u5EA6" }),
1551
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1552
+ ] }),
1553
+ /* @__PURE__ */ jsxs3("span", { className: "shrink-0 text-[11px] tabular-nums text-[hsl(var(--muted-foreground))]", children: [
1554
+ completed,
1555
+ "/",
1556
+ data.plan.length
1557
+ ] }),
1558
+ /* @__PURE__ */ jsx4(
1559
+ ChevronDown,
1560
+ {
1561
+ size: 14,
1562
+ className: cn(
1563
+ "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform duration-300 ease-out motion-reduce:transition-none",
1564
+ !collapsed && "rotate-180"
1565
+ )
1566
+ }
1567
+ )
1568
+ ]
1569
+ }
1570
+ ),
1571
+ /* @__PURE__ */ jsx4(
1572
+ "div",
1573
+ {
1574
+ "aria-hidden": collapsed,
1575
+ className: cn(
1576
+ "grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none",
1577
+ collapsed ? "grid-rows-[0fr] opacity-0" : "grid-rows-[1fr] opacity-100"
1578
+ ),
1579
+ 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: [
1580
+ /* @__PURE__ */ jsx4("span", { className: "mt-[3px] flex shrink-0", children: /* @__PURE__ */ jsx4(PlanStepIcon, { status: item.status, size: 14, running }) }),
1581
+ /* @__PURE__ */ jsx4(
1582
+ "span",
1583
+ {
1584
+ className: cn(
1585
+ "min-w-0 flex-1 break-words text-[13px] leading-5",
1586
+ item.status === "completed" ? "text-[hsl(var(--muted-foreground))]" : item.status === "in_progress" ? "font-medium text-[hsl(var(--foreground))]" : "text-[hsl(var(--muted-foreground))]"
1587
+ ),
1588
+ children: item.step
1589
+ }
1590
+ )
1591
+ ] }, `${index}-${item.step}`)) }) })
1592
+ }
1593
+ )
1594
+ ] });
1595
+ }
1596
+ function CurrentPlanPanel({
1597
+ messages,
1598
+ running = false,
1599
+ revealRevision = 0,
1600
+ sessionId,
1601
+ className
1602
+ }) {
1603
+ const { current, updating } = getPlanUpdateDisplayState(messages);
1604
+ const revealBaselinesRef = useRef4(/* @__PURE__ */ new Map([[sessionId, revealRevision]]));
1605
+ const autoReveal = (revealBaselinesRef.current.get(sessionId) ?? 0) !== revealRevision;
1606
+ useEffect4(() => {
1607
+ if (!current) return;
1608
+ revealBaselinesRef.current.set(sessionId, revealRevision);
1609
+ }, [current, revealRevision, sessionId]);
1610
+ if (!current && !updating) return null;
1611
+ return /* @__PURE__ */ jsxs3("div", { className: cn("blade-chat-plan mx-auto w-full max-w-[748px] px-4", className), children: [
1612
+ 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: [
1613
+ /* @__PURE__ */ jsx4(LoaderCircle, { size: 14, className: "shrink-0 animate-spin" }),
1614
+ /* @__PURE__ */ jsx4("span", { children: "\u6B63\u5728\u66F4\u65B0\u4EFB\u52A1\u8FDB\u5EA6\u2026" })
1615
+ ] }) : null,
1616
+ current ? /* @__PURE__ */ jsx4(
1617
+ PlanUpdateBlock,
1618
+ {
1619
+ toolCall: current,
1620
+ running,
1621
+ autoReveal
1622
+ },
1623
+ sessionId ?? "current-session"
1624
+ ) : null
1625
+ ] });
1626
+ }
1627
+
1628
+ // src/components/ChatSurface.tsx
1629
+ import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1630
+
1631
+ // src/components/ChatInput.tsx
1632
+ import { useState as useState5 } from "react";
1633
+ import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1634
+ function isImeCompositionKey(event) {
1635
+ return event.isComposing || event.keyCode === 229;
1636
+ }
1637
+ function shouldSubmitChatInput(event, menuOpen) {
1638
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1639
+ }
1640
+ function ChatInput({
1641
+ value,
1642
+ onValueChange,
1643
+ onSend,
1644
+ onAppend,
1645
+ onStop,
1646
+ isStreaming,
1647
+ isStopping = false,
1648
+ placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
1649
+ className,
1650
+ queueKey
1651
+ }) {
1652
+ const trimmed = value.trim();
1653
+ const [sendMode, setSendMode] = useState5("direct");
1654
+ void queueKey;
1655
+ const canSend = trimmed.length > 0 && (!isStreaming || sendMode === "queue");
1656
+ const handleSend = async () => {
1657
+ if (!canSend) return;
1658
+ if (isStreaming && sendMode === "queue") {
1659
+ if (onAppend) onAppend(trimmed);
1660
+ onValueChange("");
1661
+ return;
1662
+ }
1663
+ if (isStreaming && sendMode === "direct") {
1664
+ if (!onAppend) return;
1665
+ onAppend(trimmed);
1666
+ onValueChange("");
1667
+ return;
1668
+ }
1669
+ const accepted = await onSend(trimmed);
1670
+ if (!accepted) return;
1671
+ onValueChange("");
1672
+ };
1673
+ const handleKeyDown = (event) => {
1674
+ if (shouldSubmitChatInput({
1675
+ key: event.key,
1676
+ shiftKey: event.shiftKey,
1677
+ isComposing: event.nativeEvent.isComposing,
1678
+ keyCode: event.nativeEvent.keyCode
1679
+ }, false)) {
1680
+ event.preventDefault();
1681
+ void handleSend();
1682
+ }
1683
+ };
1684
+ return /* @__PURE__ */ jsxs4("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: [
1685
+ /* @__PURE__ */ jsx5("div", { className: "mx-auto mb-2 flex max-w-[748px] items-center justify-between px-1 text-xs text-[hsl(var(--muted-foreground))]", children: /* @__PURE__ */ jsxs4("fieldset", { className: "flex items-center gap-1 rounded-md border border-[hsl(var(--border))] p-0.5", children: [
1686
+ /* @__PURE__ */ jsx5("legend", { className: "sr-only", children: "\u53D1\u9001\u65B9\u5F0F" }),
1687
+ /* @__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" }),
1688
+ /* @__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" })
1689
+ ] }) }),
1690
+ /* @__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: [
1691
+ /* @__PURE__ */ jsx5(
1692
+ "textarea",
1693
+ {
1694
+ value,
1695
+ onChange: (event) => onValueChange(event.target.value),
1696
+ onKeyDown: handleKeyDown,
1697
+ onInput: (event) => {
1698
+ const el = event.currentTarget;
1699
+ el.style.height = "auto";
1700
+ el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
1701
+ },
1702
+ rows: 1,
1703
+ placeholder,
1704
+ "aria-label": "\u804A\u5929\u8F93\u5165",
1705
+ 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)]"
1706
+ }
1707
+ ),
1708
+ isStreaming ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1709
+ 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,
1710
+ /* @__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" }) })
1711
+ ] }) : /* @__PURE__ */ jsx5(
1712
+ "button",
1713
+ {
1714
+ type: "button",
1715
+ onClick: handleSend,
1716
+ disabled: !canSend,
1717
+ "aria-label": "\u53D1\u9001\u6D88\u606F",
1718
+ title: "\u53D1\u9001\u6D88\u606F",
1719
+ 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",
1720
+ children: /* @__PURE__ */ jsx5(ArrowUp, { size: 15 })
1721
+ }
1722
+ )
1723
+ ] })
1724
+ ] });
1725
+ }
1726
+
1727
+ // src/components/ConnectionBanner.tsx
1728
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState6 } from "react";
1729
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1730
+ var CONNECTION_NOTICE_DELAY_MS = 3e3;
1731
+ var CONNECTION_ERROR_DELAY_MS = 15e3;
1732
+ function useConnectionNoticePhase(connected) {
1733
+ const [phase, setPhase] = useState6("hidden");
1734
+ const connectedRef = useRef5(connected);
1735
+ const timersRef = useRef5([]);
1736
+ connectedRef.current = connected;
1737
+ useEffect5(() => {
1738
+ const clearTimers = () => {
1739
+ for (const timer of timersRef.current) clearTimeout(timer);
1740
+ timersRef.current = [];
1741
+ };
1742
+ const startGracePeriod = () => {
1743
+ clearTimers();
1744
+ setPhase("hidden");
1745
+ timersRef.current = [
1746
+ setTimeout(() => setPhase("recovering"), CONNECTION_NOTICE_DELAY_MS),
1747
+ setTimeout(() => setPhase("failed"), CONNECTION_ERROR_DELAY_MS)
1748
+ ];
1749
+ };
1750
+ if (connected) {
1751
+ clearTimers();
1752
+ setPhase("hidden");
1753
+ } else {
1754
+ startGracePeriod();
1755
+ }
1756
+ const handleForeground = () => {
1757
+ if (!connectedRef.current) startGracePeriod();
1758
+ };
1759
+ const handleVisibilityChange = () => {
1760
+ if (document.visibilityState === "visible") handleForeground();
1761
+ };
1762
+ window.addEventListener("blade:app-active", handleForeground);
1763
+ window.addEventListener("focus", handleForeground);
1764
+ window.addEventListener("pageshow", handleForeground);
1765
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1766
+ return () => {
1767
+ clearTimers();
1768
+ window.removeEventListener("blade:app-active", handleForeground);
1769
+ window.removeEventListener("focus", handleForeground);
1770
+ window.removeEventListener("pageshow", handleForeground);
1771
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1772
+ };
1773
+ }, [connected]);
1774
+ return phase;
1775
+ }
1776
+ function ConnectionBanner({ connection, className }) {
1777
+ const hasConnectedRef = useRef5(connection === "connected" || connection === "reconnecting");
1778
+ if (connection === "connected") hasConnectedRef.current = true;
1779
+ const connected = connection === "connected";
1780
+ const phase = useConnectionNoticePhase(connected);
1781
+ if (connected || phase === "hidden") return null;
1782
+ const recovering = phase === "recovering";
1783
+ const firstConnection = !hasConnectedRef.current;
1784
+ return /* @__PURE__ */ jsx6("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs5(
1785
+ "div",
1786
+ {
1787
+ className: cn(
1788
+ "mx-auto flex max-w-3xl items-start gap-3 rounded-2xl border px-4 py-3",
1789
+ recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1790
+ ),
1791
+ children: [
1792
+ /* @__PURE__ */ jsx6("span", { className: "mt-0.5 shrink-0", children: recovering ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx6(TriangleAlert, { size: 14 }) }),
1793
+ /* @__PURE__ */ jsxs5("div", { className: "min-w-0", children: [
1794
+ /* @__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" }),
1795
+ /* @__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" })
1796
+ ] })
1797
+ ]
1798
+ }
1799
+ ) });
1800
+ }
1801
+
1802
+ // src/components/MessageList.tsx
1803
+ import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1804
+ import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo7, useRef as useRef12, useState as useState14 } from "react";
1805
+
1806
+ // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1807
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef6, useState as useState7 } from "react";
1808
+ var DEFAULT_SPRING_ANIMATION = {
1809
+ /**
1810
+ * A value from 0 to 1, on how much to damp the animation.
1811
+ * 0 means no damping, 1 means full damping.
1812
+ *
1813
+ * @default 0.7
1814
+ */
1815
+ damping: 0.7,
1816
+ /**
1817
+ * The stiffness of how fast/slow the animation gets up to speed.
1818
+ *
1819
+ * @default 0.05
1820
+ */
1821
+ stiffness: 0.05,
1822
+ /**
1823
+ * The inertial mass associated with the animation.
1824
+ * Higher numbers make the animation slower.
1825
+ *
1826
+ * @default 1.25
1827
+ */
1828
+ mass: 1.25
1829
+ };
1830
+ var STICK_TO_BOTTOM_OFFSET_PX = 70;
1831
+ var SIXTY_FPS_INTERVAL_MS = 1e3 / 60;
1832
+ var RETAIN_ANIMATION_DURATION_MS = 350;
1833
+ var mouseDown = false;
1834
+ globalThis.document?.addEventListener("mousedown", () => {
1835
+ mouseDown = true;
1836
+ });
1837
+ globalThis.document?.addEventListener("mouseup", () => {
1838
+ mouseDown = false;
1839
+ });
1840
+ globalThis.document?.addEventListener("click", () => {
1841
+ mouseDown = false;
1842
+ });
1843
+ var useStickToBottom = (options = {}) => {
1844
+ const [escapedFromLock, updateEscapedFromLock] = useState7(false);
1845
+ const [isAtBottom, updateIsAtBottom] = useState7(options.initial !== false);
1846
+ const [isNearBottom, setIsNearBottom] = useState7(false);
1847
+ const optionsRef = useRef6(null);
1848
+ optionsRef.current = options;
1849
+ const isSelecting = useCallback4(() => {
1850
+ if (!mouseDown) {
1851
+ return false;
1852
+ }
1853
+ const selection = window.getSelection();
1854
+ if (!selection || !selection.rangeCount) {
1855
+ return false;
1856
+ }
1857
+ const range = selection.getRangeAt(0);
1858
+ return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1859
+ }, []);
1860
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1861
+ state.isAtBottom = isAtBottom2;
1139
1862
  updateIsAtBottom(isAtBottom2);
1140
1863
  }, []);
1141
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1864
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1142
1865
  state.escapedFromLock = escapedFromLock2;
1143
1866
  updateEscapedFromLock(escapedFromLock2);
1144
1867
  }, []);
@@ -1195,7 +1918,7 @@ var useStickToBottom = (options = {}) => {
1195
1918
  }
1196
1919
  };
1197
1920
  }, []);
1198
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1921
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1199
1922
  if (typeof scrollOptions === "string") {
1200
1923
  scrollOptions = { animation: scrollOptions };
1201
1924
  }
@@ -1280,11 +2003,11 @@ var useStickToBottom = (options = {}) => {
1280
2003
  }
1281
2004
  return next();
1282
2005
  }, [setIsAtBottom, isSelecting, state]);
1283
- const stopScroll = useCallback3(() => {
2006
+ const stopScroll = useCallback4(() => {
1284
2007
  setEscapedFromLock(true);
1285
2008
  setIsAtBottom(false);
1286
2009
  }, [setEscapedFromLock, setIsAtBottom]);
1287
- const handleScroll = useCallback3(({ target }) => {
2010
+ const handleScroll = useCallback4(({ target }) => {
1288
2011
  if (target !== scrollRef.current) {
1289
2012
  return;
1290
2013
  }
@@ -1323,7 +2046,7 @@ var useStickToBottom = (options = {}) => {
1323
2046
  }
1324
2047
  }, 1);
1325
2048
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1326
- const handleWheel = useCallback3(({ target, deltaY }) => {
2049
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1327
2050
  let element = target;
1328
2051
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1329
2052
  if (!element.parentElement) {
@@ -1393,7 +2116,7 @@ var useStickToBottom = (options = {}) => {
1393
2116
  };
1394
2117
  };
1395
2118
  function useRefCallback(callback, deps) {
1396
- const result = useCallback3((ref) => {
2119
+ const result = useCallback4((ref) => {
1397
2120
  result.current = ref;
1398
2121
  return callback(ref);
1399
2122
  }, deps);
@@ -1425,11 +2148,11 @@ function mergeAnimations(...animations) {
1425
2148
 
1426
2149
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1427
2150
  import * as React from "react";
1428
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect3, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef4 } from "react";
2151
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect6, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef7 } from "react";
1429
2152
  var StickToBottomContext = createContext2(null);
1430
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
2153
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect6;
1431
2154
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1432
- const customTargetScrollTop = useRef4(null);
2155
+ const customTargetScrollTop = useRef7(null);
1433
2156
  const targetScrollTop = React.useCallback((target, elements) => {
1434
2157
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1435
2158
  return get?.(target, elements) ?? target;
@@ -1478,160 +2201,44 @@ function StickToBottom({ instance, children, resize, initial, mass, damping, sti
1478
2201
  return React.createElement(
1479
2202
  StickToBottomContext.Provider,
1480
2203
  { value: context },
1481
- React.createElement("div", { ...props }, typeof children === "function" ? children(context) : children)
1482
- );
1483
- }
1484
- (function(StickToBottom2) {
1485
- function Content({ children, scrollClassName, ...props }) {
1486
- const context = useStickToBottomContext();
1487
- return React.createElement(
1488
- "div",
1489
- { ref: context.scrollRef, style: {
1490
- height: "100%",
1491
- width: "100%",
1492
- scrollbarGutter: "stable both-edges"
1493
- }, className: scrollClassName },
1494
- React.createElement("div", { ...props, ref: context.contentRef }, typeof children === "function" ? children(context) : children)
1495
- );
1496
- }
1497
- StickToBottom2.Content = Content;
1498
- })(StickToBottom || (StickToBottom = {}));
1499
- function useStickToBottomContext() {
1500
- const context = useContext2(StickToBottomContext);
1501
- if (!context) {
1502
- throw new Error("use-stick-to-bottom component context must be used within a StickToBottom component");
1503
- }
1504
- return context;
1505
- }
1506
-
1507
- // src/components/AssistantTurnBlock.tsx
1508
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1509
- import { useState as useState9 } from "react";
1510
-
1511
- // src/components/AgentLoopBlock.tsx
1512
- import { useState as useState5 } from "react";
1513
-
1514
- // src/components/display-utils.ts
1515
- var TOOL_NAME_ALIASES = {
1516
- agent: "Agent",
1517
- ask_user_question: "AskUserQuestion",
1518
- bash: "Bash",
1519
- bg_bash: "BgBash",
1520
- edit: "Edit",
1521
- exit_plan_mode: "ExitPlanMode",
1522
- file_edit: "Edit",
1523
- file_read: "Read",
1524
- file_write: "Write",
1525
- finish_task: "FinishTask",
1526
- glob: "Glob",
1527
- grep: "Grep",
1528
- ls: "Ls",
1529
- read: "Read",
1530
- read_skill: "ReadSkill",
1531
- web_fetch: "WebFetch",
1532
- web_search: "WebSearch",
1533
- write: "Write"
1534
- };
1535
- var TOOL_DISPLAY_LABELS = {
1536
- Bash: "\u6267\u884C\u547D\u4EE4",
1537
- BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1538
- Read: "\u8BFB\u53D6\u6587\u4EF6",
1539
- Write: "\u5199\u5165\u6587\u4EF6",
1540
- Edit: "\u7F16\u8F91\u6587\u4EF6",
1541
- Ls: "\u5217\u51FA\u76EE\u5F55",
1542
- Glob: "\u5339\u914D\u6587\u4EF6",
1543
- Grep: "\u641C\u7D22\u6587\u672C",
1544
- WebSearch: "\u641C\u7D22\u7F51\u9875",
1545
- WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1546
- Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1547
- AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1548
- ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1549
- FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1550
- ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1551
- ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1552
- GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1553
- };
1554
- function safeParseJson(value) {
1555
- if (!value) return null;
1556
- try {
1557
- return JSON.parse(value);
1558
- } catch {
1559
- return null;
1560
- }
1561
- }
1562
- function getStringArgValue(args, key) {
1563
- const value = args?.[key];
1564
- return typeof value === "string" ? value.trim() : "";
1565
- }
1566
- function formatToolName(name) {
1567
- const trimmed = name.trim();
1568
- if (!trimmed) return name;
1569
- const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1570
- const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1571
- return TOOL_NAME_ALIASES[normalized] ?? stripped;
1572
- }
1573
- function getToolDisplayLabel(toolCall) {
1574
- const normalized = formatToolName(toolCall.name);
1575
- const args = safeParseJson(toolCall.arguments);
1576
- const displayName = toolCall.display_name?.trim() ?? "";
1577
- const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1578
- const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1579
- if (metaDisplayName) {
1580
- return metaDisplayName;
1581
- }
1582
- const description = getStringArgValue(args, "description");
1583
- if (normalized === "BgBash") {
1584
- return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1585
- }
1586
- if (normalized === "ReadSkill") {
1587
- const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1588
- return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1589
- }
1590
- if (normalized === "FinishTask") {
1591
- const title = getStringArgValue(args, "title");
1592
- return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1593
- }
1594
- return description || baseLabel;
1595
- }
1596
- function getToolTone(status) {
1597
- if (status === "error" || status === "cancelled") return "red";
1598
- if (status === "awaiting_answer") return "amber";
1599
- if (status === "pending") return "blue";
1600
- return "emerald";
1601
- }
1602
- function getToolStatusLabel(status) {
1603
- if (status === "pending") return "\u8FD0\u884C\u4E2D";
1604
- if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1605
- if (status === "error") return "\u9519\u8BEF";
1606
- if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1607
- return "\u5B8C\u6210";
1608
- }
1609
- function formatToolDuration(ms) {
1610
- if (ms < 1e3) return `${Math.round(ms)}ms`;
1611
- const seconds = ms / 1e3;
1612
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
1613
- const minutes = Math.floor(seconds / 60);
1614
- const remainingSeconds = Math.round(seconds % 60);
1615
- return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
2204
+ React.createElement("div", { ...props }, typeof children === "function" ? children(context) : children)
2205
+ );
1616
2206
  }
1617
- function formatToolArgs(args) {
1618
- try {
1619
- return JSON.stringify(JSON.parse(args), null, 2);
1620
- } catch {
1621
- return args;
2207
+ (function(StickToBottom2) {
2208
+ function Content({ children, scrollClassName, ...props }) {
2209
+ const context = useStickToBottomContext();
2210
+ return React.createElement(
2211
+ "div",
2212
+ { ref: context.scrollRef, style: {
2213
+ height: "100%",
2214
+ width: "100%",
2215
+ scrollbarGutter: "stable both-edges"
2216
+ }, className: scrollClassName },
2217
+ React.createElement("div", { ...props, ref: context.contentRef }, typeof children === "function" ? children(context) : children)
2218
+ );
1622
2219
  }
2220
+ StickToBottom2.Content = Content;
2221
+ })(StickToBottom || (StickToBottom = {}));
2222
+ function useStickToBottomContext() {
2223
+ const context = useContext2(StickToBottomContext);
2224
+ if (!context) {
2225
+ throw new Error("use-stick-to-bottom component context must be used within a StickToBottom component");
2226
+ }
2227
+ return context;
1623
2228
  }
1624
- var RESULT_PREVIEW_LIMIT = 4e3;
1625
- function formatToolResult(result) {
1626
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1627
- if (text == null) return "";
1628
- if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1629
- return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1630
- \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
1631
- }
2229
+
2230
+ // src/components/AssistantTurnBlock.tsx
2231
+ import {
2232
+ getFileParts,
2233
+ getImageParts,
2234
+ getTextContent,
2235
+ normalizeMessageContent
2236
+ } from "@blade-hq/agent-client";
2237
+ import { useEffect as useEffect9, useRef as useRef10, useState as useState12 } from "react";
1632
2238
 
1633
2239
  // src/components/AgentLoopBlock.tsx
1634
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2240
+ import { useState as useState8 } from "react";
2241
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1635
2242
  function parseAgentDescription(argumentsJson) {
1636
2243
  try {
1637
2244
  const parsed = JSON.parse(argumentsJson);
@@ -1641,83 +2248,81 @@ function parseAgentDescription(argumentsJson) {
1641
2248
  }
1642
2249
  }
1643
2250
  function AgentLoopBlock({ toolCall }) {
1644
- const [expanded, setExpanded] = useState5(false);
2251
+ const [expanded, setExpanded] = useState8(false);
1645
2252
  const description = parseAgentDescription(toolCall.arguments);
1646
2253
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1647
2254
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1648
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
1649
- /* @__PURE__ */ jsxs5(
1650
- "div",
2255
+ const hasResult = toolCall.result != null;
2256
+ const iconClass = cn(
2257
+ "size-3.5 shrink-0",
2258
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2259
+ );
2260
+ return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
2261
+ /* @__PURE__ */ jsxs6(
2262
+ "button",
1651
2263
  {
2264
+ type: "button",
2265
+ onClick: () => hasResult && setExpanded(!expanded),
2266
+ disabled: !hasResult,
2267
+ "aria-expanded": hasResult ? expanded : void 0,
2268
+ "data-testid": "execution-tool-intent",
1652
2269
  className: cn(
1653
- "border-l-[3px] flex items-center gap-2 px-3 py-2",
1654
- failed ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : running ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]"
2270
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2271
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2272
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1655
2273
  ),
2274
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1656
2275
  children: [
1657
- /* @__PURE__ */ jsxs5(
1658
- "button",
2276
+ 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" }),
2277
+ /* @__PURE__ */ jsxs6("span", { className: "min-w-0 truncate", children: [
2278
+ "\u5B50\u4EFB\u52A1\uFF1A",
2279
+ description
2280
+ ] }),
2281
+ hasResult ? /* @__PURE__ */ jsx7(
2282
+ ChevronRight,
1659
2283
  {
1660
- type: "button",
1661
- onClick: () => setExpanded(!expanded),
1662
- 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",
1663
- "aria-expanded": expanded,
1664
- children: [
1665
- /* @__PURE__ */ jsx6(
1666
- ChevronRight,
1667
- {
1668
- size: 11,
1669
- className: cn(
1670
- "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform",
1671
- expanded && "rotate-90"
1672
- )
1673
- }
1674
- ),
1675
- /* @__PURE__ */ jsx6(Bot, { size: 12, className: "shrink-0 text-[hsl(var(--muted-foreground))]" }),
1676
- /* @__PURE__ */ jsxs5(
1677
- "span",
1678
- {
1679
- className: cn(
1680
- "flex shrink-0 items-center gap-1 text-[10px]",
1681
- failed ? "text-[hsl(var(--muted-foreground))]" : running ? "text-blue-300" : "text-[hsl(var(--primary))]"
1682
- ),
1683
- children: [
1684
- running ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 11, className: "animate-spin" }) : failed ? /* @__PURE__ */ jsx6(X, { size: 11 }) : /* @__PURE__ */ jsx6(Check, { size: 11 }),
1685
- /* @__PURE__ */ jsx6("span", { children: running ? "\u6267\u884C\u4E2D" : failed ? "\u5DF2\u7EC8\u6B62" : "\u5B8C\u6210" })
1686
- ]
1687
- }
1688
- ),
1689
- /* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: [
1690
- "\u5B50\u667A\u80FD\u4F53\uFF1A",
1691
- description
1692
- ] })
1693
- ]
2284
+ size: 14,
2285
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
2286
+ className: cn(
2287
+ "shrink-0 transition-transform",
2288
+ expanded && "rotate-90"
2289
+ ),
2290
+ "aria-hidden": "true"
1694
2291
  }
1695
- ),
1696
- 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) })
2292
+ ) : null
1697
2293
  ]
1698
2294
  }
1699
2295
  ),
1700
- expanded && toolCall.result != null && /* @__PURE__ */ jsxs5("div", { className: "ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
1701
- /* @__PURE__ */ jsx6("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
1702
- /* @__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) })
1703
- ] })
2296
+ 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
1704
2297
  ] });
1705
2298
  }
1706
2299
 
1707
2300
  // src/components/MarkdownContent.tsx
1708
2301
  import {
1709
- useEffect as useEffect4,
2302
+ useEffect as useEffect7,
1710
2303
  useMemo as useMemo5,
1711
- useRef as useRef5,
1712
- useState as useState6
2304
+ useRef as useRef8,
2305
+ useState as useState9
1713
2306
  } from "react";
1714
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2307
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1715
2308
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
2309
+ function normalizeAdjacentUrlFormatting(value) {
2310
+ const protectedSegments = [];
2311
+ const protectedValue = value.replace(/(`{1,3}[\s\S]*?`{1,3}|\[[^\]]*\]\([^)]*\))/g, (segment) => {
2312
+ const index = protectedSegments.push(segment) - 1;
2313
+ return `blade-url-protected-${index}-marker`;
2314
+ });
2315
+ const normalized = protectedValue.replace(
2316
+ /(\*\*|__|~~|\*|_)(https?:\/\/[^\s<>]+?)\1(?=[\s。,、!?;:,.!?;:]|$)/g,
2317
+ (_, marker, url) => `${marker}[${url}](<${url}>)${marker}`
2318
+ );
2319
+ return normalized.replace(/blade-url-protected-(\d+)-marker/g, (_, index) => protectedSegments[Number(index)]);
2320
+ }
1716
2321
  function CodeBlockPre({ children, node: _node, ...props }) {
1717
- const preRef = useRef5(null);
1718
- const [copied, setCopied] = useState6(false);
1719
- const [language, setLanguage] = useState6("");
1720
- useEffect4(() => {
2322
+ const preRef = useRef8(null);
2323
+ const [copied, setCopied] = useState9(false);
2324
+ const [language, setLanguage] = useState9("");
2325
+ useEffect7(() => {
1721
2326
  const codeEl = preRef.current?.querySelector("code");
1722
2327
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1723
2328
  }, []);
@@ -1728,10 +2333,10 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1728
2333
  setTimeout(() => setCopied(false), 2e3);
1729
2334
  }
1730
2335
  };
1731
- return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
1732
- /* @__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: [
1733
- /* @__PURE__ */ jsx7("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
1734
- /* @__PURE__ */ jsxs6(
2336
+ return /* @__PURE__ */ jsxs7("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
2337
+ /* @__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: [
2338
+ /* @__PURE__ */ jsx8("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
2339
+ /* @__PURE__ */ jsxs7(
1735
2340
  "button",
1736
2341
  {
1737
2342
  type: "button",
@@ -1741,13 +2346,13 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1741
2346
  copied ? "text-[hsl(var(--primary))]" : "text-[hsl(var(--muted-foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]"
1742
2347
  ),
1743
2348
  children: [
1744
- copied ? /* @__PURE__ */ jsx7(Check, { size: 12 }) : /* @__PURE__ */ jsx7(Copy, { size: 12 }),
1745
- /* @__PURE__ */ jsx7("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
2349
+ copied ? /* @__PURE__ */ jsx8(Check, { size: 12 }) : /* @__PURE__ */ jsx8(Copy, { size: 12 }),
2350
+ /* @__PURE__ */ jsx8("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
1746
2351
  ]
1747
2352
  }
1748
2353
  )
1749
2354
  ] }),
1750
- /* @__PURE__ */ jsx7(
2355
+ /* @__PURE__ */ jsx8(
1751
2356
  "pre",
1752
2357
  {
1753
2358
  ref: preRef,
@@ -1759,7 +2364,7 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1759
2364
  ] });
1760
2365
  }
1761
2366
  function ExternalAnchor({ node: _node, children, ...props }) {
1762
- return /* @__PURE__ */ jsx7("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
2367
+ return /* @__PURE__ */ jsx8("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
1763
2368
  }
1764
2369
  var MARKDOWN_COMPONENTS = {
1765
2370
  pre: CodeBlockPre,
@@ -1767,9 +2372,9 @@ var MARKDOWN_COMPONENTS = {
1767
2372
  };
1768
2373
  function MarkdownContent({ children, className, mode, sessionId }) {
1769
2374
  const resolvedChildren = useMemo5(() => {
1770
- return children.replace(SYSTEM_REMINDER_RE, "");
2375
+ return normalizeAdjacentUrlFormatting(children.replace(SYSTEM_REMINDER_RE, ""));
1771
2376
  }, [children]);
1772
- return /* @__PURE__ */ jsx7(
2377
+ return /* @__PURE__ */ jsx8(
1773
2378
  _r,
1774
2379
  {
1775
2380
  className: cn("blade-chat-markdown break-words", className),
@@ -1782,17 +2387,46 @@ function MarkdownContent({ children, className, mode, sessionId }) {
1782
2387
  }
1783
2388
 
1784
2389
  // src/components/Shimmer.tsx
1785
- import { jsx as jsx8 } from "react/jsx-runtime";
2390
+ import { jsx as jsx9 } from "react/jsx-runtime";
1786
2391
  function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1787
- return /* @__PURE__ */ jsx8("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
2392
+ return /* @__PURE__ */ jsx9("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
1788
2393
  }
1789
2394
 
1790
2395
  // src/components/ToolCallBlock.tsx
1791
- import { useState as useState8 } from "react";
2396
+ import { useState as useState11 } from "react";
1792
2397
 
1793
2398
  // src/components/AskUserQuestionBlock.tsx
1794
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState7 } from "react";
1795
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2399
+ import { useEffect as useEffect8, useMemo as useMemo6, useRef as useRef9, useState as useState10 } from "react";
2400
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2401
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2402
+ function resizeCustomTextarea(textarea) {
2403
+ textarea.style.height = "auto";
2404
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2405
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2406
+ }
2407
+ function useAutoResizeTextarea(value) {
2408
+ const textareaRef = useRef9(null);
2409
+ useEffect8(() => {
2410
+ const textarea = textareaRef.current;
2411
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2412
+ }, [value]);
2413
+ useEffect8(() => {
2414
+ const textarea = textareaRef.current;
2415
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2416
+ let previousWidth = textarea.clientWidth;
2417
+ const observer = new ResizeObserver(([entry]) => {
2418
+ if (!entry || entry.contentRect.width === previousWidth) return;
2419
+ previousWidth = entry.contentRect.width;
2420
+ resizeCustomTextarea(textarea);
2421
+ });
2422
+ observer.observe(textarea);
2423
+ return () => observer.disconnect();
2424
+ }, []);
2425
+ return textareaRef;
2426
+ }
2427
+ function indentAnswerContinuationLines(answer) {
2428
+ return answer.replaceAll("\n", "\n ");
2429
+ }
1796
2430
  function AskUserQuestionBlock({
1797
2431
  data,
1798
2432
  answered,
@@ -1801,18 +2435,19 @@ function AskUserQuestionBlock({
1801
2435
  answerData,
1802
2436
  onAnswer
1803
2437
  }) {
1804
- const [selections, setSelections] = useState7(/* @__PURE__ */ new Map());
1805
- const [customTexts, setCustomTexts] = useState7(/* @__PURE__ */ new Map());
1806
- const [usingCustom, setUsingCustom] = useState7(/* @__PURE__ */ new Set());
1807
- const [submitted, setSubmitted] = useState7(false);
1808
- useEffect5(() => {
2438
+ const [selections, setSelections] = useState10(/* @__PURE__ */ new Map());
2439
+ const [customTexts, setCustomTexts] = useState10(/* @__PURE__ */ new Map());
2440
+ const [usingCustom, setUsingCustom] = useState10(/* @__PURE__ */ new Set());
2441
+ const [note, setNote] = useState10("");
2442
+ const [submitted, setSubmitted] = useState10(false);
2443
+ useEffect8(() => {
1809
2444
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1810
2445
  setSubmitted(false);
1811
2446
  }
1812
2447
  }, [sessionStatus]);
1813
2448
  const displayAnswerState = useMemo6(() => {
1814
2449
  if (!(answered && answerData)) {
1815
- return { selections, customTexts, usingCustom };
2450
+ return { selections, customTexts, usingCustom, note };
1816
2451
  }
1817
2452
  const nextSelections = /* @__PURE__ */ new Map();
1818
2453
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1828,9 +2463,10 @@ function AskUserQuestionBlock({
1828
2463
  return {
1829
2464
  selections: nextSelections,
1830
2465
  customTexts: nextCustomTexts,
1831
- usingCustom: nextUsingCustom
2466
+ usingCustom: nextUsingCustom,
2467
+ note: answerData.note ?? ""
1832
2468
  };
1833
- }, [answerData, answered, customTexts, selections, usingCustom]);
2469
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1834
2470
  const toggleOption = (qIdx, optIdx, multi) => {
1835
2471
  if (answered || submitted) return;
1836
2472
  setSelections((prev) => {
@@ -1878,6 +2514,7 @@ function AskUserQuestionBlock({
1878
2514
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1879
2515
  const handleSubmit = () => {
1880
2516
  if (answered || submitted || !allAnswered || !onAnswer) return;
2517
+ const trimmedNote = note.trim();
1881
2518
  const nextAnswerData = {
1882
2519
  selections: Object.fromEntries(
1883
2520
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1887,15 +2524,21 @@ function AskUserQuestionBlock({
1887
2524
  ),
1888
2525
  custom: Object.fromEntries(
1889
2526
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1890
- )
2527
+ ),
2528
+ ...trimmedNote ? { note: trimmedNote } : {}
1891
2529
  };
1892
- const parts = data.questions.map((q, i) => `- ${q.question} -> ${getAnswer(i)}`);
1893
- const text = `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
1894
- ${parts.join("\n")}`;
2530
+ const parts = data.questions.map(
2531
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2532
+ );
2533
+ const text = [
2534
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2535
+ ${parts.join("\n")}`,
2536
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2537
+ ].filter(Boolean).join("\n");
1895
2538
  setSubmitted(true);
1896
2539
  onAnswer(text, toolCallId, nextAnswerData);
1897
2540
  };
1898
- return /* @__PURE__ */ jsxs7(
2541
+ return /* @__PURE__ */ jsxs8(
1899
2542
  "div",
1900
2543
  {
1901
2544
  className: cn(
@@ -1903,12 +2546,12 @@ ${parts.join("\n")}`;
1903
2546
  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"
1904
2547
  ),
1905
2548
  children: [
1906
- 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: [
2549
+ 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: [
1907
2550
  "\u5B50\u667A\u80FD\u4F53\u300C",
1908
2551
  data.source_loop.description,
1909
2552
  "\u300D\u5728\u7B49\u5F85\u4F60\u7684\u56DE\u7B54"
1910
2553
  ] }),
1911
- data.questions.map((q, qIdx) => /* @__PURE__ */ jsx9(
2554
+ data.questions.map((q, qIdx) => /* @__PURE__ */ jsx10(
1912
2555
  QuestionCard,
1913
2556
  {
1914
2557
  question: q,
@@ -1923,7 +2566,16 @@ ${parts.join("\n")}`;
1923
2566
  },
1924
2567
  q.question
1925
2568
  )),
1926
- !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
2569
+ /* @__PURE__ */ jsx10(
2570
+ NoteField,
2571
+ {
2572
+ answered,
2573
+ submitted,
2574
+ note: displayAnswerState.note,
2575
+ onChange: setNote
2576
+ }
2577
+ ),
2578
+ !answered && !submitted && onAnswer && /* @__PURE__ */ jsx10(
1927
2579
  "button",
1928
2580
  {
1929
2581
  type: "button",
@@ -1933,14 +2585,14 @@ ${parts.join("\n")}`;
1933
2585
  children: allAnswered ? "\u786E\u8BA4" : "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u9009\u9879"
1934
2586
  }
1935
2587
  ),
1936
- submitted && !answered && /* @__PURE__ */ jsxs7(
2588
+ submitted && !answered && /* @__PURE__ */ jsxs8(
1937
2589
  "button",
1938
2590
  {
1939
2591
  type: "button",
1940
2592
  disabled: true,
1941
2593
  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",
1942
2594
  children: [
1943
- /* @__PURE__ */ jsx9(LoaderCircle, { size: 14, className: "animate-spin" }),
2595
+ /* @__PURE__ */ jsx10(LoaderCircle, { size: 14, className: "animate-spin" }),
1944
2596
  "\u786E\u8BA4\u4E2D"
1945
2597
  ]
1946
2598
  }
@@ -1961,30 +2613,31 @@ function QuestionCard({
1961
2613
  onCustomChange
1962
2614
  }) {
1963
2615
  const multi = question.multiSelect ?? false;
1964
- return /* @__PURE__ */ jsxs7("div", { children: [
1965
- /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
1966
- /* @__PURE__ */ jsx9(
2616
+ const customTextareaRef = useAutoResizeTextarea(customText);
2617
+ return /* @__PURE__ */ jsxs8("div", { children: [
2618
+ /* @__PURE__ */ jsxs8("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
2619
+ /* @__PURE__ */ jsx10(
1967
2620
  MessageSquareMore,
1968
2621
  {
1969
2622
  size: answered ? 12 : 13,
1970
2623
  className: "mt-0.5 shrink-0 text-[hsl(var(--primary))]"
1971
2624
  }
1972
2625
  ),
1973
- /* @__PURE__ */ jsx9(
2626
+ /* @__PURE__ */ jsx10(
1974
2627
  "div",
1975
2628
  {
1976
2629
  className: cn(
1977
2630
  "min-w-0 flex-1 font-medium text-[hsl(var(--foreground))]",
1978
2631
  answered ? "text-xs" : "text-sm"
1979
2632
  ),
1980
- children: /* @__PURE__ */ jsx9(MarkdownContent, { className: "blade-chat-prose", children: question.question })
2633
+ children: /* @__PURE__ */ jsx10(MarkdownContent, { className: "blade-chat-prose", children: question.question })
1981
2634
  }
1982
2635
  )
1983
2636
  ] }),
1984
- /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
2637
+ /* @__PURE__ */ jsxs8("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
1985
2638
  question.options.map((opt, optIdx) => {
1986
2639
  const isSel = selected.has(optIdx);
1987
- return /* @__PURE__ */ jsxs7(
2640
+ return /* @__PURE__ */ jsxs8(
1988
2641
  "button",
1989
2642
  {
1990
2643
  type: "button",
@@ -1998,14 +2651,14 @@ function QuestionCard({
1998
2651
  answered && "cursor-default opacity-70"
1999
2652
  ),
2000
2653
  children: [
2001
- multi && /* @__PURE__ */ jsx9(
2654
+ multi && /* @__PURE__ */ jsx10(
2002
2655
  "div",
2003
2656
  {
2004
2657
  className: cn(
2005
2658
  "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
2006
2659
  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))]"
2007
2660
  ),
2008
- children: isSel && /* @__PURE__ */ jsx9(
2661
+ children: isSel && /* @__PURE__ */ jsx10(
2009
2662
  Check,
2010
2663
  {
2011
2664
  size: 9,
@@ -2014,9 +2667,9 @@ function QuestionCard({
2014
2667
  )
2015
2668
  }
2016
2669
  ),
2017
- /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
2018
- /* @__PURE__ */ jsx9("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2019
- opt.description && /* @__PURE__ */ jsx9(
2670
+ /* @__PURE__ */ jsxs8("div", { className: "min-w-0", children: [
2671
+ /* @__PURE__ */ jsx10("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2672
+ opt.description && /* @__PURE__ */ jsx10(
2020
2673
  "div",
2021
2674
  {
2022
2675
  className: cn(
@@ -2033,29 +2686,30 @@ function QuestionCard({
2033
2686
  opt.label
2034
2687
  );
2035
2688
  }),
2036
- answered && !isCustom ? null : /* @__PURE__ */ jsxs7(
2689
+ answered && !isCustom ? null : /* @__PURE__ */ jsxs8(
2037
2690
  "div",
2038
2691
  {
2039
2692
  className: cn(
2040
- "flex items-center gap-2 rounded-lg border transition-all",
2693
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2041
2694
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2042
2695
  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))]",
2043
2696
  answered && "cursor-default opacity-70"
2044
2697
  ),
2045
2698
  children: [
2046
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2047
- /* @__PURE__ */ jsx9(
2048
- "input",
2699
+ /* @__PURE__ */ jsx10("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2700
+ /* @__PURE__ */ jsx10(
2701
+ "textarea",
2049
2702
  {
2050
- type: "text",
2703
+ ref: customTextareaRef,
2704
+ rows: 2,
2051
2705
  value: customText,
2052
- disabled: answered,
2706
+ readOnly: answered,
2053
2707
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2054
2708
  onFocus: () => onCustomFocus(qIdx),
2055
2709
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2056
2710
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2057
2711
  className: cn(
2058
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2712
+ "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)]",
2059
2713
  answered ? "text-xs" : "text-sm"
2060
2714
  )
2061
2715
  }
@@ -2066,6 +2720,49 @@ function QuestionCard({
2066
2720
  ] })
2067
2721
  ] });
2068
2722
  }
2723
+ function NoteField({
2724
+ answered,
2725
+ submitted,
2726
+ note,
2727
+ onChange
2728
+ }) {
2729
+ const textareaRef = useAutoResizeTextarea(note);
2730
+ const readOnly = answered || submitted;
2731
+ if (answered && !note.trim()) return null;
2732
+ return /* @__PURE__ */ jsxs8(
2733
+ "label",
2734
+ {
2735
+ className: cn(
2736
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2737
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2738
+ 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))]",
2739
+ readOnly && "cursor-default opacity-70"
2740
+ ),
2741
+ children: [
2742
+ /* @__PURE__ */ jsx10("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2743
+ /* @__PURE__ */ jsx10(
2744
+ "textarea",
2745
+ {
2746
+ ref: textareaRef,
2747
+ rows: 2,
2748
+ value: note,
2749
+ readOnly,
2750
+ onChange: (event) => {
2751
+ if (readOnly) return;
2752
+ onChange(event.target.value);
2753
+ },
2754
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2755
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2756
+ className: cn(
2757
+ "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)]",
2758
+ answered ? "text-xs" : "text-sm"
2759
+ )
2760
+ }
2761
+ )
2762
+ ]
2763
+ }
2764
+ );
2765
+ }
2069
2766
  function parseAskUserQuestion(toolResult) {
2070
2767
  if (!toolResult) return null;
2071
2768
  try {
@@ -2088,6 +2785,26 @@ function parseAskUserQuestion(toolResult) {
2088
2785
  }
2089
2786
  return null;
2090
2787
  }
2788
+ function parseAskUserQuestionError(toolResult) {
2789
+ if (!toolResult) return null;
2790
+ try {
2791
+ const parsed = JSON.parse(toolResult);
2792
+ let detail = null;
2793
+ if (typeof parsed.error === "string") detail = parsed.error;
2794
+ if (parsed.error && typeof parsed.error === "object") {
2795
+ const message = parsed.error.message;
2796
+ if (typeof message === "string") detail = message;
2797
+ }
2798
+ if (!detail && typeof parsed.message === "string") detail = parsed.message;
2799
+ if (!detail) return null;
2800
+ return {
2801
+ 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",
2802
+ detail
2803
+ };
2804
+ } catch {
2805
+ return null;
2806
+ }
2807
+ }
2091
2808
  function normalizeQuestionItem(value) {
2092
2809
  if (!value || typeof value !== "object") return null;
2093
2810
  const item = value;
@@ -2112,13 +2829,14 @@ function normalizeOptionItem(value) {
2112
2829
  }
2113
2830
 
2114
2831
  // src/components/ToolCallBlock.tsx
2115
- import { Fragment, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2832
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2116
2833
  function resolveAskQuestionState({
2117
2834
  toolStatus,
2118
2835
  hasAnswerData,
2119
- fallbackAnswered
2836
+ fallbackAnswered,
2837
+ fallbackAwaiting
2120
2838
  }) {
2121
- const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
2839
+ const awaitingAnswer = !hasAnswerData && (toolStatus === "awaiting_answer" || toolStatus === "pending" && fallbackAwaiting === true);
2122
2840
  return {
2123
2841
  awaitingAnswer,
2124
2842
  answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
@@ -2130,14 +2848,15 @@ function ToolCallBlock({
2130
2848
  answered,
2131
2849
  answerData,
2132
2850
  sessionStatus,
2851
+ isActiveQuestion,
2133
2852
  renderer
2134
2853
  }) {
2135
- const [expanded, setExpanded] = useState8(false);
2854
+ const [expanded, setExpanded] = useState11(false);
2136
2855
  const normalizedName = formatToolName(toolCall.name);
2137
2856
  if (renderer) {
2138
2857
  const custom = renderer(toolCall);
2139
2858
  if (custom !== null && custom !== void 0) {
2140
- return /* @__PURE__ */ jsx10(Fragment, { children: custom });
2859
+ return /* @__PURE__ */ jsx11(Fragment2, { children: custom });
2141
2860
  }
2142
2861
  }
2143
2862
  if (normalizedName === "AskUserQuestion") {
@@ -2145,11 +2864,12 @@ function ToolCallBlock({
2145
2864
  const questionState = resolveAskQuestionState({
2146
2865
  toolStatus: toolCall.status,
2147
2866
  hasAnswerData: Boolean(answerData),
2148
- fallbackAnswered: answered
2867
+ fallbackAnswered: answered,
2868
+ fallbackAwaiting: isActiveQuestion === true && (sessionStatus === "paused" || sessionStatus === "waiting_for_input")
2149
2869
  });
2150
2870
  const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
2151
2871
  if (askData) {
2152
- return /* @__PURE__ */ jsx10(
2872
+ return /* @__PURE__ */ jsx11(
2153
2873
  AskUserQuestionBlock,
2154
2874
  {
2155
2875
  data: askData,
@@ -2162,24 +2882,31 @@ function ToolCallBlock({
2162
2882
  );
2163
2883
  }
2164
2884
  if (toolCall.status === "pending") {
2165
- 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: [
2166
- /* @__PURE__ */ jsx10(LoaderCircle, { size: 14, className: "animate-spin" }),
2167
- /* @__PURE__ */ jsx10("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2885
+ 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: [
2886
+ /* @__PURE__ */ jsx11(LoaderCircle, { size: 14, className: "animate-spin" }),
2887
+ /* @__PURE__ */ jsx11("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2168
2888
  ] });
2169
2889
  }
2170
- 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: [
2171
- /* @__PURE__ */ jsx10("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
2172
- /* @__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" })
2890
+ const errorDetail = parseAskUserQuestionError(
2891
+ typeof toolCall.result === "string" ? toolCall.result : null
2892
+ );
2893
+ 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: [
2894
+ /* @__PURE__ */ jsx11("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
2895
+ /* @__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" }),
2896
+ errorDetail?.detail ? /* @__PURE__ */ jsxs9("details", { className: "mt-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2897
+ /* @__PURE__ */ jsx11("summary", { className: "cursor-pointer", children: "\u67E5\u770B\u5177\u4F53\u539F\u56E0" }),
2898
+ /* @__PURE__ */ jsx11("div", { className: "mt-1 break-words font-mono", children: errorDetail.detail })
2899
+ ] }) : null
2173
2900
  ] });
2174
2901
  }
2175
2902
  const tone = getToolTone(toolCall.status);
2176
2903
  const displayName = getToolDisplayLabel(toolCall);
2177
2904
  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))]";
2178
- 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 });
2905
+ 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 });
2179
2906
  const statusTextClass = tone === "red" ? "text-[hsl(var(--muted-foreground))]" : tone === "amber" ? "text-amber-300" : tone === "blue" ? "text-blue-300" : "text-[hsl(var(--primary))]";
2180
- return /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool ml-4 text-xs", children: [
2181
- /* @__PURE__ */ jsxs8("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
2182
- /* @__PURE__ */ jsxs8(
2907
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-tool ml-4 text-xs", children: [
2908
+ /* @__PURE__ */ jsxs9("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
2909
+ /* @__PURE__ */ jsxs9(
2183
2910
  "button",
2184
2911
  {
2185
2912
  type: "button",
@@ -2187,7 +2914,7 @@ function ToolCallBlock({
2187
2914
  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",
2188
2915
  "aria-expanded": expanded,
2189
2916
  children: [
2190
- /* @__PURE__ */ jsx10(
2917
+ /* @__PURE__ */ jsx11(
2191
2918
  ChevronRight,
2192
2919
  {
2193
2920
  size: 11,
@@ -2197,24 +2924,24 @@ function ToolCallBlock({
2197
2924
  )
2198
2925
  }
2199
2926
  ),
2200
- /* @__PURE__ */ jsxs8("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
2927
+ /* @__PURE__ */ jsxs9("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
2201
2928
  statusIcon,
2202
- /* @__PURE__ */ jsx10("span", { children: getToolStatusLabel(toolCall.status) })
2929
+ /* @__PURE__ */ jsx11("span", { children: getToolStatusLabel(toolCall.status) })
2203
2930
  ] }),
2204
- /* @__PURE__ */ jsx10("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
2931
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
2205
2932
  ]
2206
2933
  }
2207
2934
  ),
2208
- 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) })
2935
+ 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) })
2209
2936
  ] }),
2210
- expanded && /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
2211
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
2212
- /* @__PURE__ */ jsx10("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
2213
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
2214
- /* @__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) }),
2215
- toolCall.result != null && /* @__PURE__ */ jsxs8(Fragment, { children: [
2216
- /* @__PURE__ */ jsx10("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
2217
- /* @__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) })
2937
+ expanded && /* @__PURE__ */ jsxs9("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
2938
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
2939
+ /* @__PURE__ */ jsx11("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
2940
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
2941
+ /* @__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) }),
2942
+ toolCall.result != null && /* @__PURE__ */ jsxs9(Fragment2, { children: [
2943
+ /* @__PURE__ */ jsx11("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
2944
+ /* @__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) })
2218
2945
  ] })
2219
2946
  ] })
2220
2947
  ] });
@@ -2232,110 +2959,555 @@ function buildAskUserPayload(argumentsJson) {
2232
2959
  }
2233
2960
 
2234
2961
  // src/components/AssistantTurnBlock.tsx
2235
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2962
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2236
2963
  function ThinkingBlock({ reasoning, isStreaming }) {
2237
- const [open, setOpen] = useState9(false);
2238
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2239
- /* @__PURE__ */ jsxs9(
2964
+ const [open, setOpen] = useState12(false);
2965
+ if (!isStreaming) return null;
2966
+ return /* @__PURE__ */ jsxs10("div", { className: "blade-chat-thinking text-xs", children: [
2967
+ /* @__PURE__ */ jsxs10(
2240
2968
  "button",
2241
2969
  {
2242
2970
  type: "button",
2243
2971
  onClick: () => setOpen(!open),
2244
2972
  "aria-expanded": open,
2245
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2973
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2246
2974
  children: [
2247
- /* @__PURE__ */ jsx11(Brain, { size: 12, className: "shrink-0" }),
2248
- isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u601D\u8003\u8FC7\u7A0B" }),
2249
- /* @__PURE__ */ jsxs9("span", { className: "text-[hsl(var(--muted-foreground))]/70", children: [
2250
- "\xB7 ",
2251
- new Intl.NumberFormat("zh-CN").format(reasoning.length),
2252
- " \u5B57"
2253
- ] }),
2254
- /* @__PURE__ */ jsx11(
2255
- ChevronDown,
2975
+ /* @__PURE__ */ jsx12(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }),
2976
+ /* @__PURE__ */ jsx12(
2977
+ ChevronRight,
2256
2978
  {
2257
- size: 12,
2258
- className: cn("shrink-0 transition-transform", open && "rotate-180")
2979
+ size: 14,
2980
+ className: cn(
2981
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
2982
+ open && "rotate-90 opacity-100"
2983
+ )
2259
2984
  }
2260
2985
  )
2261
2986
  ]
2262
2987
  }
2263
2988
  ),
2264
- 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 })
2989
+ open ? /* @__PURE__ */ jsx12("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning }) : null
2265
2990
  ] });
2266
2991
  }
2267
2992
  function getMessageText(message) {
2268
2993
  return getTextContent(normalizeMessageContent(message.content)).trim();
2269
2994
  }
2995
+ function hasRenderableMessageContent(message) {
2996
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
2997
+ }
2998
+ function getLastContentMessage(messages) {
2999
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3000
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
3001
+ }
3002
+ return null;
3003
+ }
3004
+ function getOrderedMessageParts(message, toolCalls) {
3005
+ const blocks = message.blocks ?? [];
3006
+ if (!blocks.some((block) => block.type === "text") || !blocks.some((block) => block.type === "tool_use")) return [];
3007
+ const toolsById = new Map(toolCalls.map((toolCall) => [toolCall.id, toolCall]));
3008
+ const seenToolIds = /* @__PURE__ */ new Set();
3009
+ const parts = [];
3010
+ for (const [index, block] of blocks.entries()) {
3011
+ if (block.type === "text" && block.content != null && block.content !== "") {
3012
+ const content = Array.isArray(block.content) ? block.content : String(block.content);
3013
+ parts.push({ type: "text", key: `text-${index}`, content });
3014
+ }
3015
+ if (block.type !== "tool_use" || !block.tool_call_id) continue;
3016
+ const toolCall = toolsById.get(block.tool_call_id);
3017
+ if (!toolCall) continue;
3018
+ seenToolIds.add(toolCall.id);
3019
+ const previous = parts[parts.length - 1];
3020
+ if (previous?.type === "tools") previous.toolCalls.push(toolCall);
3021
+ else parts.push({ type: "tools", key: `tools-${index}`, toolCalls: [toolCall] });
3022
+ }
3023
+ const missingTools = toolCalls.filter((toolCall) => !seenToolIds.has(toolCall.id));
3024
+ if (seenToolIds.size === 0) return [];
3025
+ if (missingTools.length > 0) {
3026
+ parts.push({ type: "tools", key: "tools-missing", toolCalls: missingTools });
3027
+ }
3028
+ return parts;
3029
+ }
2270
3030
  function findLatestReasoningMessageIndex(messages) {
2271
3031
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2272
3032
  if (messages[index].reasoning) return index;
2273
3033
  }
2274
3034
  return -1;
2275
3035
  }
2276
- function AssistantTurnBlock({
2277
- messages,
2278
- isStreaming = false,
2279
- askAnswers,
2280
- onAnswer,
2281
- sessionStatus,
2282
- toolCallRenderer,
2283
- sessionId
3036
+ function resolveTurnDisplayMode({
3037
+ isStreaming: _isStreaming,
3038
+ displayMode
3039
+ }) {
3040
+ return displayMode;
3041
+ }
3042
+ function formatExecutionDuration(durationMs) {
3043
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
3044
+ const minutes = Math.floor(totalSeconds / 60);
3045
+ const seconds = totalSeconds % 60;
3046
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
3047
+ }
3048
+ function getExecutionDurationMs({
3049
+ messages,
3050
+ isStreaming,
3051
+ now = Date.now()
3052
+ }) {
3053
+ const knownDuration = messages.reduce(
3054
+ (total, message) => {
3055
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
3056
+ return total + message.duration_ms;
3057
+ }
3058
+ return total + (message.tool_calls ?? []).reduce(
3059
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
3060
+ 0
3061
+ );
3062
+ },
3063
+ 0
3064
+ );
3065
+ if (!isStreaming) return knownDuration;
3066
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
3067
+ if (startedAt === void 0) return knownDuration;
3068
+ return Math.max(knownDuration, now - startedAt);
3069
+ }
3070
+ function findLastExceptionalEvent(messages) {
3071
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
3072
+ const messageStatus = messages[messageIndex].status;
3073
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
3074
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
3075
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
3076
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
3077
+ const status = toolCalls[toolIndex].status;
3078
+ if (status === "error" || status === "cancelled") {
3079
+ return { messageIndex, status };
3080
+ }
3081
+ }
3082
+ }
3083
+ return null;
3084
+ }
3085
+ function executionSummaryLabel({
3086
+ messages,
3087
+ isStreaming,
3088
+ durationMs,
3089
+ sessionStatus,
3090
+ askAnswers
3091
+ }) {
3092
+ if (isStreaming) {
3093
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
3094
+ }
3095
+ if (sessionStatus === "waiting_for_input" && messages.some(
3096
+ (message) => (message.tool_calls ?? []).some(
3097
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
3098
+ )
3099
+ )) {
3100
+ return "\u7B49\u5F85\u8F93\u5165";
3101
+ }
3102
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
3103
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
3104
+ if (lastExceptionalEvent) {
3105
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
3106
+ if (lastExceptionalEvent.status === "error") {
3107
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
3108
+ }
3109
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
3110
+ }
3111
+ return completedLabel;
3112
+ }
3113
+ function businessToolDisplayName(toolCall) {
3114
+ const displayName = toolCall.display_name?.trim() ?? "";
3115
+ if (!displayName) return "";
3116
+ const rawName = toolCall.name.trim();
3117
+ return displayName !== rawName && formatToolName(displayName) !== formatToolName(rawName) ? displayName : "";
3118
+ }
3119
+ function executionToolTypeLabel(toolCall) {
3120
+ switch (formatToolName(toolCall.name)) {
3121
+ case "WebSearch":
3122
+ case "WebFetch":
3123
+ return "\u7F51\u7EDC\u68C0\u7D22";
3124
+ case "Bash":
3125
+ case "BgBash":
3126
+ return "\u547D\u4EE4\u6267\u884C";
3127
+ case "Read":
3128
+ case "ReadSkill":
3129
+ return "\u5185\u5BB9\u8BFB\u53D6";
3130
+ case "Write":
3131
+ case "Edit":
3132
+ case "MultiEdit":
3133
+ return "\u6587\u4EF6\u5904\u7406";
3134
+ case "Grep":
3135
+ case "Glob":
3136
+ return "\u5185\u5BB9\u641C\u7D22";
3137
+ case "Agent":
3138
+ return "\u5B50\u4EFB\u52A1";
3139
+ case "search_skills":
3140
+ return "\u6280\u80FD\u68C0\u7D22";
3141
+ case "get_skill_content":
3142
+ return "\u8BFB\u53D6\u6280\u80FD";
3143
+ case "run_skill_tool":
3144
+ return "\u6267\u884C\u6280\u80FD";
3145
+ default:
3146
+ return businessToolDisplayName(toolCall) || "\u6267\u884C\u6B65\u9AA4";
3147
+ }
3148
+ }
3149
+ function executionToolIntent(toolCall) {
3150
+ const normalizedName = formatToolName(toolCall.name);
3151
+ let args = null;
3152
+ try {
3153
+ const parsed = JSON.parse(toolCall.arguments);
3154
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
3155
+ } catch {
3156
+ args = null;
3157
+ }
3158
+ const getString = (key) => {
3159
+ const value = args?.[key];
3160
+ return typeof value === "string" ? value.trim() : "";
3161
+ };
3162
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || "";
3163
+ if (explicitIntent) return explicitIntent;
3164
+ if (normalizedName === "search_skills") return getString("query");
3165
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
3166
+ return getString("skill_name") || getString("skill");
3167
+ }
3168
+ if (normalizedName === "FinishTask") return getString("title");
3169
+ return "";
3170
+ }
3171
+ function ExecutionToolRow({ toolCall }) {
3172
+ const normalizedName = formatToolName(toolCall.name);
3173
+ const typeLabel = executionToolTypeLabel(toolCall);
3174
+ const intent = executionToolIntent(toolCall);
3175
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
3176
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
3177
+ const iconClass = cn(
3178
+ "size-3.5 shrink-0",
3179
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3180
+ );
3181
+ 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" });
3182
+ const rowClassName = cn(
3183
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
3184
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3185
+ );
3186
+ return /* @__PURE__ */ jsxs10("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
3187
+ icon,
3188
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate", children: label })
3189
+ ] });
3190
+ }
3191
+ function AssistantTurnBlock({
3192
+ messages,
3193
+ isStreaming = false,
3194
+ askAnswers,
3195
+ onAnswer,
3196
+ sessionStatus,
3197
+ toolCallRenderer,
3198
+ hidePlanUpdateTools = false,
3199
+ sessionId
3200
+ }) {
3201
+ const shouldHideToolCall = (message, toolCall) => {
3202
+ if (!hidePlanUpdateTools || !isPlanUpdateTool(toolCall) || parsePlanUpdate(toolCall.arguments) === null) {
3203
+ return false;
3204
+ }
3205
+ return toolCall.status === "done" || toolCall.status === "pending" && message.status === "streaming";
3206
+ };
3207
+ const hasInterrupted = messages.some((message) => message.status === "interrupted");
3208
+ const hasFailedWithoutContent = messages.some(
3209
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
3210
+ );
3211
+ const finalMessage = getLastContentMessage(messages);
3212
+ const turnToolCalls = messages.flatMap((message) => message.tool_calls ?? []);
3213
+ const finalOrderedParts = finalMessage ? getOrderedMessageParts(
3214
+ finalMessage,
3215
+ (finalMessage.tool_calls ?? []).filter(
3216
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(finalMessage, toolCall)
3217
+ )
3218
+ ) : [];
3219
+ const hasExecutionProcess = messages.some(
3220
+ (message) => message.reasoning || (message.tool_calls ?? []).some((toolCall) => !shouldHideToolCall(message, toolCall))
3221
+ );
3222
+ const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
3223
+ const hasActionableToolCall = messages.some(
3224
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
3225
+ (toolCall) => !shouldHideToolCall(message, toolCall) && (toolCall.status === "error" || toolCall.status === "cancelled")
3226
+ )
3227
+ );
3228
+ const questionToolCalls = messages.flatMap(
3229
+ (message) => (message.tool_calls ?? []).filter(
3230
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
3231
+ )
3232
+ );
3233
+ const activeQuestionId = questionToolCalls.filter((toolCall) => toolCall.status === "pending").at(-1)?.id;
3234
+ const [displayMode, setDisplayMode] = useState12(
3235
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
3236
+ );
3237
+ const userSelectedDisplayModeRef = useRef10(false);
3238
+ const wasStreamingRef = useRef10(isStreaming);
3239
+ useEffect9(() => {
3240
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
3241
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
3242
+ }
3243
+ wasStreamingRef.current = isStreaming;
3244
+ }, [hasActionableToolCall, isStreaming]);
3245
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
3246
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
3247
+ const [clock, setClock] = useState12(() => Date.now());
3248
+ const hasLiveStartTime = messages.some(
3249
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
3250
+ );
3251
+ useEffect9(() => {
3252
+ if (!isStreaming || !hasLiveStartTime) return;
3253
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
3254
+ return () => window.clearInterval(timer);
3255
+ }, [hasLiveStartTime, isStreaming]);
3256
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
3257
+ const memoryRefs = collectMemoryRefs(messages);
3258
+ if (!hasExecutionProcess) {
3259
+ return /* @__PURE__ */ jsxs10(
3260
+ "div",
3261
+ {
3262
+ "aria-busy": isStreaming || void 0,
3263
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3264
+ children: [
3265
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx12(MemoryRefsHint, { refs: memoryRefs }) : null,
3266
+ 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" }),
3267
+ 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" }),
3268
+ messages.map((message, index) => {
3269
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx12(
3270
+ "div",
3271
+ {
3272
+ className: "flex flex-col gap-3",
3273
+ children: /* @__PURE__ */ jsx12(
3274
+ AssistantMessageContent,
3275
+ {
3276
+ message,
3277
+ sessionId,
3278
+ streaming: isStreaming && index === messages.length - 1
3279
+ }
3280
+ )
3281
+ },
3282
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3283
+ ) : null;
3284
+ })
3285
+ ]
3286
+ }
3287
+ );
3288
+ }
3289
+ return /* @__PURE__ */ jsxs10(
3290
+ "div",
3291
+ {
3292
+ "aria-busy": isStreaming || void 0,
3293
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3294
+ children: [
3295
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx12(MemoryRefsHint, { refs: memoryRefs }) : null,
3296
+ 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" }),
3297
+ 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" }),
3298
+ /* @__PURE__ */ jsxs10("div", { className: "flex w-full items-start gap-2.5", children: [
3299
+ /* @__PURE__ */ jsx12(
3300
+ "span",
3301
+ {
3302
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3303
+ "aria-hidden": "true",
3304
+ children: /* @__PURE__ */ jsx12(Bot, { size: 16 })
3305
+ }
3306
+ ),
3307
+ /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3308
+ /* @__PURE__ */ jsxs10(
3309
+ "button",
3310
+ {
3311
+ type: "button",
3312
+ onClick: () => {
3313
+ userSelectedDisplayModeRef.current = true;
3314
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
3315
+ },
3316
+ "aria-expanded": effectiveMode === "detail",
3317
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3318
+ "data-testid": "assistant-execution-summary",
3319
+ 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",
3320
+ children: [
3321
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3322
+ messages,
3323
+ isStreaming,
3324
+ durationMs: liveExecutionDurationMs,
3325
+ sessionStatus,
3326
+ askAnswers
3327
+ }) }),
3328
+ /* @__PURE__ */ jsx12(
3329
+ ChevronRight,
3330
+ {
3331
+ size: 14,
3332
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
3333
+ className: cn(
3334
+ "shrink-0 transition-transform",
3335
+ effectiveMode === "detail" && "rotate-90"
3336
+ ),
3337
+ "aria-hidden": "true"
3338
+ }
3339
+ )
3340
+ ]
3341
+ }
3342
+ ),
3343
+ /* @__PURE__ */ jsx12("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3344
+ ] })
3345
+ ] }),
3346
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx12("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
3347
+ const isLast = index === messages.length - 1;
3348
+ const streamingThis = isStreaming && isLast;
3349
+ const text = getMessageText(message);
3350
+ const toolCalls = (message.tool_calls ?? []).filter(
3351
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(message, toolCall)
3352
+ );
3353
+ const orderedParts = getOrderedMessageParts(message, toolCalls);
3354
+ const showReasoning = !!message.reasoning && isStreaming && index === latestReasoningIndex;
3355
+ return /* @__PURE__ */ jsxs10(
3356
+ "div",
3357
+ {
3358
+ className: "flex flex-col gap-3",
3359
+ children: [
3360
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx12(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3361
+ orderedParts.length > 0 ? orderedParts.map(
3362
+ (part) => part.type === "text" ? /* @__PURE__ */ jsx12(
3363
+ AssistantMessageContent,
3364
+ {
3365
+ message: { ...message, content: part.content, tool_calls: turnToolCalls },
3366
+ sessionId,
3367
+ streaming: streamingThis,
3368
+ compact: true
3369
+ },
3370
+ part.key
3371
+ ) : /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-0.5", children: part.toolCalls.map((toolCall) => {
3372
+ const custom = toolCallRenderer?.(toolCall);
3373
+ 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);
3374
+ }) }, part.key)
3375
+ ) : hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx12(
3376
+ AssistantMessageContent,
3377
+ {
3378
+ message,
3379
+ sessionId,
3380
+ streaming: streamingThis,
3381
+ compact: true
3382
+ }
3383
+ ) : null,
3384
+ orderedParts.length === 0 && toolCalls.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3385
+ const custom = toolCallRenderer?.(toolCall);
3386
+ 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);
3387
+ }) }) : null
3388
+ ]
3389
+ },
3390
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3391
+ );
3392
+ }) }) : null,
3393
+ finalMessage && (effectiveMode === "compact" || finalOrderedParts.length === 0) ? /* @__PURE__ */ jsx12("div", { className: "ml-10", children: /* @__PURE__ */ jsx12(
3394
+ AssistantMessageContent,
3395
+ {
3396
+ message: finalMessage,
3397
+ sessionId,
3398
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3399
+ }
3400
+ ) }) : null,
3401
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx12(
3402
+ ToolCallBlock,
3403
+ {
3404
+ toolCall,
3405
+ answerData: askAnswers?.[toolCall.id],
3406
+ onAnswer,
3407
+ answered: sessionStatus !== "waiting_for_input",
3408
+ sessionStatus,
3409
+ isActiveQuestion: toolCall.id === activeQuestionId,
3410
+ renderer: toolCallRenderer
3411
+ },
3412
+ toolCall.id
3413
+ ))
3414
+ ]
3415
+ }
3416
+ );
3417
+ }
3418
+ function collectMemoryRefs(messages) {
3419
+ const refs = /* @__PURE__ */ new Map();
3420
+ for (const message of messages) {
3421
+ for (const ref of message.memory_refs ?? []) if (!refs.has(ref.id)) refs.set(ref.id, ref);
3422
+ }
3423
+ return [...refs.values()];
3424
+ }
3425
+ function MemoryRefsHint({ refs }) {
3426
+ const [expanded, setExpanded] = useState12(false);
3427
+ 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";
3428
+ return /* @__PURE__ */ jsxs10("div", { className: "blade-chat-memory-refs ml-1 w-full max-w-[680px]", children: [
3429
+ /* @__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: [
3430
+ /* @__PURE__ */ jsx12(BookOpen, { size: 12 }),
3431
+ /* @__PURE__ */ jsxs10("span", { children: [
3432
+ label,
3433
+ "\uFF08",
3434
+ refs.length,
3435
+ "\uFF09"
3436
+ ] }),
3437
+ /* @__PURE__ */ jsx12(ChevronRight, { size: 10, className: cn("transition-transform", expanded && "rotate-90") })
3438
+ ] }),
3439
+ 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: [
3440
+ /* @__PURE__ */ jsx12("p", { className: "line-clamp-2 break-words leading-5", children: ref.content_preview }),
3441
+ ref.skill_name ? /* @__PURE__ */ jsx12("span", { className: "mt-1 inline-flex text-[10px] text-[hsl(var(--primary))]", children: ref.skill_name }) : null
3442
+ ] }, ref.id)) }) : null
3443
+ ] });
3444
+ }
3445
+ function AssistantMessageContent({
3446
+ message,
3447
+ sessionId,
3448
+ streaming,
3449
+ compact = false
2284
3450
  }) {
2285
- const hasInterrupted = messages.some((message) => message.status === "interrupted");
2286
- const hasAnyContent = messages.some(
2287
- (message) => getMessageText(message) || message.reasoning || (message.tool_calls?.length ?? 0) > 0
2288
- );
2289
- const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
2290
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2291
- 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" }),
2292
- messages.map((message, index) => {
2293
- const isLast = index === messages.length - 1;
2294
- const streamingThis = isStreaming && isLast;
2295
- const text = getMessageText(message);
2296
- const toolCalls = message.tool_calls ?? [];
2297
- const showReasoning = !!message.reasoning && (!isStreaming || index === latestReasoningIndex);
2298
- return /* @__PURE__ */ jsxs9(
2299
- "div",
3451
+ const text = getMessageText(message);
3452
+ const imageParts = getImageParts(message.content);
3453
+ const fileParts = getFileParts(message.content);
3454
+ const failed = message.status === "failed";
3455
+ 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;
3456
+ const textContent = text ? /* @__PURE__ */ jsx12(
3457
+ "div",
3458
+ {
3459
+ className: cn(
3460
+ "blade-chat-assistant-text",
3461
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3462
+ ),
3463
+ children: /* @__PURE__ */ jsx12(
3464
+ MarkdownContent,
2300
3465
  {
2301
- className: "flex flex-col gap-3",
2302
- children: [
2303
- showReasoning && message.reasoning && /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }),
2304
- text && /* @__PURE__ */ jsx11("div", { className: "blade-chat-assistant-text text-[15px] leading-8 text-[hsl(var(--foreground))]", children: /* @__PURE__ */ jsx11(
2305
- MarkdownContent,
2306
- {
2307
- mode: streamingThis ? "streaming" : "static",
2308
- className: "blade-chat-prose",
2309
- sessionId,
2310
- children: text
2311
- }
2312
- ) }),
2313
- toolCalls.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-2", children: toolCalls.map(
2314
- (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(
2315
- ToolCallBlock,
2316
- {
2317
- toolCall,
2318
- answerData: askAnswers?.[toolCall.id],
2319
- onAnswer,
2320
- answered: sessionStatus !== "waiting_for_input",
2321
- sessionStatus,
2322
- renderer: toolCallRenderer
2323
- },
2324
- toolCall.id
2325
- )
2326
- ) })
2327
- ]
2328
- },
2329
- message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2330
- );
2331
- }),
2332
- isStreaming && !hasAnyContent && /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
3466
+ mode: streaming ? "streaming" : "static",
3467
+ className: "blade-chat-prose",
3468
+ sessionId,
3469
+ children: text
3470
+ }
3471
+ )
3472
+ }
3473
+ ) : null;
3474
+ if (imageParts.length === 0 && fileParts.length === 0) {
3475
+ if (!failed) return textContent;
3476
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-2", children: [
3477
+ failedBadge,
3478
+ textContent
3479
+ ] }) : null;
3480
+ }
3481
+ return /* @__PURE__ */ jsxs10("div", { className: "flex flex-col gap-3", children: [
3482
+ failedBadge,
3483
+ imageParts.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx12(
3484
+ "img",
3485
+ {
3486
+ src: part.image_url.url,
3487
+ alt: "\u6D88\u606F\u9644\u4EF6",
3488
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3489
+ },
3490
+ part.image_url.url
3491
+ )) }) : null,
3492
+ fileParts.length > 0 ? /* @__PURE__ */ jsx12("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs10(
3493
+ "div",
3494
+ {
3495
+ 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))]",
3496
+ title: part.name,
3497
+ children: [
3498
+ /* @__PURE__ */ jsx12(FileText, { size: 12, className: "shrink-0" }),
3499
+ /* @__PURE__ */ jsx12("span", { className: "max-w-56 truncate", children: part.name })
3500
+ ]
3501
+ },
3502
+ `${part.name}-${part.data.slice(0, 32)}`
3503
+ )) }) : null,
3504
+ textContent
2333
3505
  ] });
2334
3506
  }
2335
3507
 
2336
3508
  // src/components/RenderErrorBoundary.tsx
2337
3509
  import { Component } from "react";
2338
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3510
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2339
3511
  function getFirstComponentName(componentStack) {
2340
3512
  const match = componentStack.match(/\n\s+at\s+([^\s(]+)/);
2341
3513
  return match?.[1] ?? null;
@@ -2368,26 +3540,26 @@ var RenderErrorBoundary = class extends Component {
2368
3540
  return children;
2369
3541
  }
2370
3542
  const componentName = getFirstComponentName(componentStack);
2371
- 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: [
2372
- /* @__PURE__ */ jsx12(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
2373
- /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1", children: [
2374
- /* @__PURE__ */ jsxs10("div", { className: "font-medium", children: [
3543
+ 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: [
3544
+ /* @__PURE__ */ jsx13(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
3545
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1", children: [
3546
+ /* @__PURE__ */ jsxs11("div", { className: "font-medium", children: [
2375
3547
  label,
2376
3548
  "\u6E32\u67D3\u5931\u8D25"
2377
3549
  ] }),
2378
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
3550
+ /* @__PURE__ */ jsxs11("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
2379
3551
  componentName ? `\u7EC4\u4EF6\uFF1A${componentName}\u3002` : null,
2380
3552
  error.message || "\u53D1\u751F\u4E86\u672A\u9884\u671F\u7684\u6E32\u67D3\u9519\u8BEF\u3002"
2381
3553
  ] }),
2382
- details ? /* @__PURE__ */ jsx12("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
3554
+ details ? /* @__PURE__ */ jsx13("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
2383
3555
  ] })
2384
3556
  ] }) });
2385
3557
  }
2386
3558
  };
2387
3559
 
2388
3560
  // src/components/PostChatFollowupBlock.tsx
2389
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState10 } from "react";
2390
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3561
+ import { useCallback as useCallback6, useEffect as useEffect10, useRef as useRef11, useState as useState13 } from "react";
3562
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2391
3563
  function emitInteraction(callback, event) {
2392
3564
  try {
2393
3565
  callback?.(event);
@@ -2397,9 +3569,6 @@ function emitInteraction(callback, event) {
2397
3569
  function basename(path) {
2398
3570
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2399
3571
  }
2400
- function isVideo(path) {
2401
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2402
- }
2403
3572
  function ArtifactCard({
2404
3573
  artifact,
2405
3574
  sessionId,
@@ -2409,10 +3578,10 @@ function ArtifactCard({
2409
3578
  onArtifactOpened
2410
3579
  }) {
2411
3580
  const client = useBladeClient();
2412
- const [downloading, setDownloading] = useState10(false);
3581
+ const [downloading, setDownloading] = useState13(false);
2413
3582
  const name = artifact.label || basename(artifact.target);
2414
3583
  if (artifact.kind === "link") {
2415
- return /* @__PURE__ */ jsxs11(
3584
+ return /* @__PURE__ */ jsxs12(
2416
3585
  "a",
2417
3586
  {
2418
3587
  href: artifact.target,
@@ -2423,51 +3592,55 @@ function ArtifactCard({
2423
3592
  ${artifact.target}`,
2424
3593
  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))]",
2425
3594
  children: [
2426
- /* @__PURE__ */ jsx13(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2427
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2428
- /* @__PURE__ */ jsx13(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
3595
+ /* @__PURE__ */ jsx14(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
3596
+ /* @__PURE__ */ jsx14("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
3597
+ /* @__PURE__ */ jsx14(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2429
3598
  ]
2430
3599
  }
2431
3600
  );
2432
3601
  }
2433
- const Icon2 = isVideo(artifact.target) ? Film : File;
2434
- return /* @__PURE__ */ jsxs11(
2435
- "button",
3602
+ const fileName = basename(artifact.target);
3603
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3604
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3605
+ ) : void 0;
3606
+ const handleDownload = async (event) => {
3607
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3608
+ event.preventDefault();
3609
+ if (!sessionId || downloading) return;
3610
+ setDownloading(true);
3611
+ emitInteraction(onInteraction, {
3612
+ type: "artifact_download_started",
3613
+ sessionId,
3614
+ assistantEntryId,
3615
+ artifactIndex,
3616
+ artifactKind: "file"
3617
+ });
3618
+ try {
3619
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3620
+ emitInteraction(onInteraction, {
3621
+ type: "artifact_download_succeeded",
3622
+ sessionId,
3623
+ assistantEntryId,
3624
+ artifactIndex,
3625
+ artifactKind: "file"
3626
+ });
3627
+ } catch {
3628
+ } finally {
3629
+ setDownloading(false);
3630
+ }
3631
+ };
3632
+ return /* @__PURE__ */ jsx14(
3633
+ "a",
2436
3634
  {
2437
- type: "button",
2438
- disabled: !sessionId || downloading,
2439
- onClick: async () => {
2440
- if (!sessionId || downloading) return;
2441
- setDownloading(true);
2442
- emitInteraction(onInteraction, {
2443
- type: "artifact_download_started",
2444
- sessionId,
2445
- assistantEntryId,
2446
- artifactIndex,
2447
- artifactKind: "file"
2448
- });
2449
- try {
2450
- await client.sessions.downloadFile(sessionId, artifact.target, basename(artifact.target));
2451
- emitInteraction(onInteraction, {
2452
- type: "artifact_download_succeeded",
2453
- sessionId,
2454
- assistantEntryId,
2455
- artifactIndex,
2456
- artifactKind: "file"
2457
- });
2458
- } catch {
2459
- } finally {
2460
- setDownloading(false);
2461
- }
2462
- },
2463
- title: name,
2464
- "aria-label": `\u4E0B\u8F7D ${name}`,
2465
- 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",
2466
- children: [
2467
- /* @__PURE__ */ jsx13(Icon2, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2468
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2469
- /* @__PURE__ */ jsx13(Download, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2470
- ]
3635
+ href: downloadUrl,
3636
+ download: fileName,
3637
+ onClick: handleDownload,
3638
+ title: fileName,
3639
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3640
+ "aria-disabled": !sessionId || void 0,
3641
+ "aria-busy": downloading || void 0,
3642
+ 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",
3643
+ children: fileName
2471
3644
  }
2472
3645
  );
2473
3646
  }
@@ -2484,17 +3657,17 @@ function feedbackReasonLabel(reason) {
2484
3657
  }
2485
3658
  function HistoricalResultFeedback({ feedback }) {
2486
3659
  const label = feedbackReasonLabel(feedback.reason);
2487
- return /* @__PURE__ */ jsxs11(
3660
+ return /* @__PURE__ */ jsxs12(
2488
3661
  "section",
2489
3662
  {
2490
3663
  "aria-label": "\u5386\u53F2\u7ED3\u679C\u53CD\u9988",
2491
3664
  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))]",
2492
3665
  children: [
2493
- /* @__PURE__ */ jsxs11("span", { children: [
3666
+ /* @__PURE__ */ jsxs12("span", { children: [
2494
3667
  "\u4F60\u5BF9\u6B64\u8F6E\u7ED3\u679C\u7684\u8BC4\u4EF7\uFF1A",
2495
3668
  feedback.helpful ? "\u6709\u5E2E\u52A9" : "\u6CA1\u5E2E\u52A9"
2496
3669
  ] }),
2497
- label ? /* @__PURE__ */ jsxs11("span", { children: [
3670
+ label ? /* @__PURE__ */ jsxs12("span", { children: [
2498
3671
  " \xB7 ",
2499
3672
  label
2500
3673
  ] }) : null
@@ -2511,15 +3684,15 @@ function ResultFeedback({
2511
3684
  onFeedbackSaved
2512
3685
  }) {
2513
3686
  const client = useBladeClient();
2514
- const [saved, setSaved] = useState10(savedFeedback ?? null);
2515
- const [helpful, setHelpful] = useState10(savedFeedback?.helpful ?? null);
2516
- const [reason, setReason] = useState10(savedFeedback?.reason ?? null);
2517
- const [saving, setSaving] = useState10(false);
2518
- const [saveError, setSaveError] = useState10(false);
2519
- const reportedShown = useRef6(false);
2520
- const latestChoice = useRef6(null);
3687
+ const [saved, setSaved] = useState13(savedFeedback ?? null);
3688
+ const [helpful, setHelpful] = useState13(savedFeedback?.helpful ?? null);
3689
+ const [reason, setReason] = useState13(savedFeedback?.reason ?? null);
3690
+ const [saving, setSaving] = useState13(false);
3691
+ const [saveError, setSaveError] = useState13(false);
3692
+ const reportedShown = useRef11(false);
3693
+ const latestChoice = useRef11(null);
2521
3694
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2522
- useEffect6(() => {
3695
+ useEffect10(() => {
2523
3696
  if (!eligible || reportedShown.current) return;
2524
3697
  reportedShown.current = true;
2525
3698
  emitInteraction(onInteraction, {
@@ -2528,13 +3701,13 @@ function ResultFeedback({
2528
3701
  assistantEntryId: followup.assistant_entry_id
2529
3702
  });
2530
3703
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2531
- useEffect6(() => {
3704
+ useEffect10(() => {
2532
3705
  if (!savedFeedback || latestChoice.current) return;
2533
3706
  setSaved(savedFeedback);
2534
3707
  setHelpful(savedFeedback.helpful);
2535
3708
  setReason(savedFeedback.reason);
2536
3709
  }, [savedFeedback]);
2537
- const submit = useCallback5(
3710
+ const submit = useCallback6(
2538
3711
  async (nextHelpful, nextReason) => {
2539
3712
  if (!sessionId) return;
2540
3713
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2569,15 +3742,15 @@ function ResultFeedback({
2569
3742
  [client, followup.assistant_entry_id, onFeedbackSaved, onInteraction, sessionId]
2570
3743
  );
2571
3744
  if (!eligible) return null;
2572
- return /* @__PURE__ */ jsxs11(
3745
+ return /* @__PURE__ */ jsxs12(
2573
3746
  "section",
2574
3747
  {
2575
3748
  "aria-label": "\u7ED3\u679C\u53CD\u9988",
2576
3749
  className: "flex flex-col gap-2 border-t border-[hsl(var(--border))] pt-3",
2577
3750
  children: [
2578
- /* @__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" }),
2579
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap gap-1.5", children: [
2580
- /* @__PURE__ */ jsx13(
3751
+ /* @__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" }),
3752
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap gap-1.5", children: [
3753
+ /* @__PURE__ */ jsx14(
2581
3754
  "button",
2582
3755
  {
2583
3756
  type: "button",
@@ -2588,7 +3761,7 @@ function ResultFeedback({
2588
3761
  children: "\u6709\u5E2E\u52A9"
2589
3762
  }
2590
3763
  ),
2591
- /* @__PURE__ */ jsx13(
3764
+ /* @__PURE__ */ jsx14(
2592
3765
  "button",
2593
3766
  {
2594
3767
  type: "button",
@@ -2600,7 +3773,7 @@ function ResultFeedback({
2600
3773
  }
2601
3774
  )
2602
3775
  ] }),
2603
- 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(
3776
+ 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(
2604
3777
  "button",
2605
3778
  {
2606
3779
  type: "button",
@@ -2612,9 +3785,9 @@ function ResultFeedback({
2612
3785
  },
2613
3786
  item.value
2614
3787
  )) }) : null,
2615
- saveError ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
2616
- /* @__PURE__ */ jsx13("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
2617
- /* @__PURE__ */ jsx13(
3788
+ saveError ? /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
3789
+ /* @__PURE__ */ jsx14("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
3790
+ /* @__PURE__ */ jsx14(
2618
3791
  "button",
2619
3792
  {
2620
3793
  type: "button",
@@ -2626,7 +3799,7 @@ function ResultFeedback({
2626
3799
  children: "\u91CD\u8BD5"
2627
3800
  }
2628
3801
  )
2629
- ] }) : saved ? /* @__PURE__ */ jsx13("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
3802
+ ] }) : saved ? /* @__PURE__ */ jsx14("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
2630
3803
  ]
2631
3804
  }
2632
3805
  );
@@ -2640,14 +3813,14 @@ function PostChatFollowupBlock({
2640
3813
  savedFeedback,
2641
3814
  onFeedbackSaved
2642
3815
  }) {
2643
- const [expanded, setExpanded] = useState10(false);
2644
- const adopted = useRef6(/* @__PURE__ */ new Set());
2645
- const reportedSuggestions = useRef6(false);
2646
- const reportedArtifacts = useRef6(/* @__PURE__ */ new Set());
2647
- const openedArtifacts = useRef6(/* @__PURE__ */ new Set());
3816
+ const [expanded, setExpanded] = useState13(false);
3817
+ const adopted = useRef11(/* @__PURE__ */ new Set());
3818
+ const reportedSuggestions = useRef11(false);
3819
+ const reportedArtifacts = useRef11(/* @__PURE__ */ new Set());
3820
+ const openedArtifacts = useRef11(/* @__PURE__ */ new Set());
2648
3821
  const artifacts = followup.final_artifacts ?? [];
2649
3822
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2650
- useEffect6(() => {
3823
+ useEffect10(() => {
2651
3824
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2652
3825
  reportedSuggestions.current = true;
2653
3826
  emitInteraction(onInteraction, {
@@ -2677,7 +3850,7 @@ function PostChatFollowupBlock({
2677
3850
  sessionId,
2678
3851
  visibleArtifacts
2679
3852
  ]);
2680
- const reportArtifactOpened = useCallback5(
3853
+ const reportArtifactOpened = useCallback6(
2681
3854
  (artifactIndex, artifactKind) => {
2682
3855
  if (openedArtifacts.current.has(artifactIndex)) return;
2683
3856
  openedArtifacts.current.add(artifactIndex);
@@ -2693,15 +3866,15 @@ function PostChatFollowupBlock({
2693
3866
  );
2694
3867
  if (!followup.recaption && artifacts.length === 0 && followup.suggestions.length === 0 && !followup.feedback_eligible)
2695
3868
  return null;
2696
- 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: [
2697
- followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
2698
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
2699
- /* @__PURE__ */ jsx13(Sparkles, { size: 14 }),
3869
+ 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: [
3870
+ followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
3871
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
3872
+ /* @__PURE__ */ jsx14(Sparkles, { size: 14 }),
2700
3873
  "\u672C\u8F6E\u5C0F\u7ED3"
2701
3874
  ] }),
2702
- followup.recaption ? /* @__PURE__ */ jsx13("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
2703
- artifacts.length > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2704
- /* @__PURE__ */ jsx13("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx13(
3875
+ followup.recaption ? /* @__PURE__ */ jsx14("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
3876
+ artifacts.length > 0 ? /* @__PURE__ */ jsxs12(Fragment3, { children: [
3877
+ /* @__PURE__ */ jsx14("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx14(
2705
3878
  ArtifactCard,
2706
3879
  {
2707
3880
  artifact,
@@ -2713,7 +3886,7 @@ function PostChatFollowupBlock({
2713
3886
  },
2714
3887
  `${artifact.kind}:${artifactIndex}`
2715
3888
  )) }),
2716
- artifacts.length > 3 ? /* @__PURE__ */ jsxs11(
3889
+ artifacts.length > 3 ? /* @__PURE__ */ jsxs12(
2717
3890
  "button",
2718
3891
  {
2719
3892
  type: "button",
@@ -2722,15 +3895,15 @@ function PostChatFollowupBlock({
2722
3895
  className: "flex w-fit items-center gap-0.5 text-[11px] text-[hsl(var(--muted-foreground))]",
2723
3896
  children: [
2724
3897
  expanded ? "\u6536\u8D77" : `\u5C55\u5F00 ${artifacts.length - 3} \u4E2A`,
2725
- /* @__PURE__ */ jsx13(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
3898
+ /* @__PURE__ */ jsx14(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
2726
3899
  ]
2727
3900
  }
2728
3901
  ) : null
2729
3902
  ] }) : null
2730
3903
  ] }) : null,
2731
- followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
2732
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
2733
- followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs11(
3904
+ followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
3905
+ /* @__PURE__ */ jsx14("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
3906
+ followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs12(
2734
3907
  "button",
2735
3908
  {
2736
3909
  type: "button",
@@ -2749,14 +3922,14 @@ function PostChatFollowupBlock({
2749
3922
  },
2750
3923
  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",
2751
3924
  children: [
2752
- /* @__PURE__ */ jsx13("span", { children: suggestion }),
2753
- /* @__PURE__ */ jsx13(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
3925
+ /* @__PURE__ */ jsx14("span", { children: suggestion }),
3926
+ /* @__PURE__ */ jsx14(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
2754
3927
  ]
2755
3928
  },
2756
3929
  suggestion
2757
3930
  ))
2758
3931
  ] }) : null,
2759
- /* @__PURE__ */ jsx13(
3932
+ /* @__PURE__ */ jsx14(
2760
3933
  ResultFeedback,
2761
3934
  {
2762
3935
  followup,
@@ -2771,8 +3944,91 @@ function PostChatFollowupBlock({
2771
3944
  }
2772
3945
 
2773
3946
  // src/components/UserMessageBubble.tsx
2774
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
2775
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3947
+ import {
3948
+ chatErrorForDisplay,
3949
+ getFileParts as getFileParts2,
3950
+ getImageParts as getImageParts2,
3951
+ getTextContent as getTextContent2
3952
+ } from "@blade-hq/agent-client";
3953
+
3954
+ // src/lib/whatif-prompt.ts
3955
+ var HEADER_RE = /^以下消息和 step 产物标记为 deprecated_by_rerun,请基于最新用户假设从 step(\d+) 开始完整重新推演,不要复用旧结论。$/;
3956
+ var QUOTE_HEADER_RE = /^\[步骤(\d+)\s*·\s*(.+?)\]$/;
3957
+ var USER_INPUT_TAG = "[\u7528\u6237\u8F93\u5165]";
3958
+ function parseWhatIfPrompt(text) {
3959
+ const lines = text.replace(/\r\n/g, "\n").trimEnd().split("\n");
3960
+ const headerMatch = lines[0]?.match(HEADER_RE);
3961
+ if (!headerMatch) return null;
3962
+ const userTagIdx = lines.indexOf(USER_INPUT_TAG);
3963
+ const hasUserTag = userTagIdx >= 0;
3964
+ const quoteBlockEndExclusive = hasUserTag ? userTagIdx : lines.length;
3965
+ const quoteHeaderIdxs = [];
3966
+ let quoteBlockFound = false;
3967
+ for (let i = 1; i < quoteBlockEndExclusive; i++) {
3968
+ if (!quoteBlockFound && lines[i].trim() === "[\u5F15\u7528]") {
3969
+ quoteBlockFound = true;
3970
+ } else if (quoteBlockFound && QUOTE_HEADER_RE.test(lines[i])) {
3971
+ quoteHeaderIdxs.push(i);
3972
+ }
3973
+ }
3974
+ let legacyUserTextStart = -1;
3975
+ if (!hasUserTag && quoteHeaderIdxs.length > 0) {
3976
+ const lastSnapshotStart = quoteHeaderIdxs.at(-1) + 1;
3977
+ let i = lines.length - 1;
3978
+ while (i >= lastSnapshotStart && lines[i].trim() === "") i -= 1;
3979
+ while (i >= lastSnapshotStart && lines[i].trim() !== "") i -= 1;
3980
+ if (i >= lastSnapshotStart) legacyUserTextStart = i + 1;
3981
+ }
3982
+ const quotes = quoteHeaderIdxs.map((headerIdx, index) => {
3983
+ const match = lines[headerIdx].match(QUOTE_HEADER_RE);
3984
+ const nextHeader = quoteHeaderIdxs[index + 1];
3985
+ const end = nextHeader ?? (legacyUserTextStart >= 0 ? legacyUserTextStart : quoteBlockEndExclusive);
3986
+ const snapshotLines = lines.slice(headerIdx + 1, end);
3987
+ while (snapshotLines.at(-1)?.trim() === "") snapshotLines.pop();
3988
+ return {
3989
+ stepNumber: Number.parseInt(match[1], 10),
3990
+ label: match[2].trim(),
3991
+ snapshot: snapshotLines.join("\n")
3992
+ };
3993
+ });
3994
+ const userTextStart = hasUserTag ? userTagIdx + 1 : legacyUserTextStart;
3995
+ const userLines = userTextStart >= 0 ? lines.slice(userTextStart) : [];
3996
+ while (userLines[0]?.trim() === "") userLines.shift();
3997
+ while (userLines.at(-1)?.trim() === "") userLines.pop();
3998
+ const fromStep = Number.parseInt(headerMatch[1], 10);
3999
+ return {
4000
+ fromStep: Number.isFinite(fromStep) ? fromStep : null,
4001
+ quotes,
4002
+ userText: userLines.join("\n")
4003
+ };
4004
+ }
4005
+
4006
+ // src/components/WhatIfUserBubble.tsx
4007
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4008
+ function WhatIfUserBubble({ parsed, onQuoteClick }) {
4009
+ const { fromStep, quotes, userText } = parsed;
4010
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-col items-end gap-2", children: [
4011
+ /* @__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: [
4012
+ /* @__PURE__ */ jsx15(RefreshCcw, { size: 10 }),
4013
+ /* @__PURE__ */ jsx15("span", { children: fromStep != null ? `\u91CD\u8DD1\u81EA step ${fromStep}` : "\u91CD\u8DD1" })
4014
+ ] }),
4015
+ quotes.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex max-w-[min(72vw,42rem)] flex-col items-stretch gap-2", children: quotes.map((quote, index) => {
4016
+ const clickable = quote.stepNumber != null && !!onQuoteClick;
4017
+ const label = quote.stepNumber != null ? `\u6B65\u9AA4${quote.stepNumber} \xB7 ${quote.label}` : quote.label;
4018
+ 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: [
4019
+ /* @__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: [
4020
+ /* @__PURE__ */ jsx15("span", { children: "\u21B3" }),
4021
+ /* @__PURE__ */ jsx15("span", { className: "truncate", children: label })
4022
+ ] }),
4023
+ 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
4024
+ ] }, `${quote.stepNumber ?? "x"}-${index}`);
4025
+ }) }),
4026
+ 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 }) })
4027
+ ] });
4028
+ }
4029
+
4030
+ // src/components/UserMessageBubble.tsx
4031
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2776
4032
  function isUserMessage(message) {
2777
4033
  return message.role === "user";
2778
4034
  }
@@ -2782,10 +4038,14 @@ function isErrorMessage(message) {
2782
4038
  var isSending = (message) => message.status === "streaming";
2783
4039
  function UserMessageBubble({ message, className }) {
2784
4040
  const text = getTextContent2(message.content).trim();
2785
- const fileParts = getFileParts(message.content);
2786
- const imageParts = getImageParts(message.content);
2787
- 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: [
2788
- imageParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx14(
4041
+ const fileParts = getFileParts2(message.content);
4042
+ const imageParts = getImageParts2(message.content);
4043
+ const whatifParsed = text && imageParts.length === 0 && fileParts.length === 0 ? parseWhatIfPrompt(text) : null;
4044
+ if (whatifParsed) {
4045
+ 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 }) }) });
4046
+ }
4047
+ 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: [
4048
+ imageParts.length > 0 && /* @__PURE__ */ jsx16("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx16(
2789
4049
  "img",
2790
4050
  {
2791
4051
  src: part.image_url.url,
@@ -2794,21 +4054,21 @@ function UserMessageBubble({ message, className }) {
2794
4054
  },
2795
4055
  part.image_url.url
2796
4056
  )) }),
2797
- fileParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs12(
4057
+ fileParts.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs14(
2798
4058
  "div",
2799
4059
  {
2800
4060
  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))]",
2801
4061
  children: [
2802
- /* @__PURE__ */ jsx14(FileText, { size: 12, className: "shrink-0" }),
2803
- /* @__PURE__ */ jsx14("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
4062
+ /* @__PURE__ */ jsx16(FileText, { size: 12, className: "shrink-0" }),
4063
+ /* @__PURE__ */ jsx16("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
2804
4064
  ]
2805
4065
  },
2806
4066
  `${part.name}-${part.data.length}`
2807
4067
  )) }),
2808
- 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 }) }),
2809
- 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: [
2810
- /* @__PURE__ */ jsx14(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
2811
- /* @__PURE__ */ jsx14("span", { children: "\u53D1\u9001\u4E2D" })
4068
+ 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 }) }),
4069
+ 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: [
4070
+ /* @__PURE__ */ jsx16(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
4071
+ /* @__PURE__ */ jsx16("span", { children: "\u53D1\u9001\u4E2D" })
2812
4072
  ] })
2813
4073
  ] }) });
2814
4074
  }
@@ -2816,12 +4076,12 @@ function ErrorMessageBlock({
2816
4076
  message,
2817
4077
  className
2818
4078
  }) {
2819
- const text = getTextContent2(message.content);
2820
- 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 }) });
4079
+ const text = chatErrorForDisplay(getTextContent2(message.content));
4080
+ 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 }) });
2821
4081
  }
2822
4082
 
2823
4083
  // src/components/MessageList.tsx
2824
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4084
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2825
4085
  function parseModeChange(message) {
2826
4086
  if (message.kind !== "mode_change" || typeof message.content !== "string") {
2827
4087
  return null;
@@ -2862,18 +4122,32 @@ function MessageList({
2862
4122
  askAnswers,
2863
4123
  onAnswer,
2864
4124
  toolCallRenderer,
4125
+ hidePlanUpdateTools = false,
2865
4126
  emptyState,
2866
4127
  className,
2867
4128
  sessionId,
2868
4129
  isViewer = false,
2869
4130
  onFollowupInteraction,
2870
4131
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2871
- onResultFeedbackSaved
4132
+ onResultFeedbackSaved,
4133
+ historyPaging
2872
4134
  }) {
4135
+ const visibleRootMessages = messages.filter((message) => {
4136
+ if ((message.loop_name ?? "root") !== "root") return false;
4137
+ if (isHiddenInternalMessage(message)) return false;
4138
+ if (message.kind === "context") return false;
4139
+ return message.role !== "tool" || getPlanningDividerKind(message) !== null;
4140
+ });
4141
+ const userMessages = visibleRootMessages.filter((message) => isUserMessage(message));
4142
+ const latestUserMessage = userMessages.at(-1);
4143
+ 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() : "";
4144
+ const latestPromptPreview = latestPromptText.length > 80 ? `${latestPromptText.slice(0, 80)}\u2026` : latestPromptText;
4145
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
2873
4146
  const renderBlocks = useMemo7(() => {
2874
4147
  const visible = messages.filter((message) => {
2875
4148
  if ((message.loop_name ?? "root") !== "root") return false;
2876
4149
  if (isHiddenInternalMessage(message)) return false;
4150
+ if (message.kind === "context") return false;
2877
4151
  if (message.kind === "compaction") return true;
2878
4152
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2879
4153
  });
@@ -2916,7 +4190,7 @@ function MessageList({
2916
4190
  blocks.push({
2917
4191
  type: "message",
2918
4192
  message,
2919
- key: message.entry_id ?? `${message.role}-${blocks.length}`
4193
+ key: message.render_id ?? message.entry_id ?? `${message.role}-${blocks.length}`
2920
4194
  });
2921
4195
  }
2922
4196
  flushAssistant();
@@ -2943,98 +4217,235 @@ function MessageList({
2943
4217
  }
2944
4218
  return blocks;
2945
4219
  }, [messages, isStreaming]);
2946
- 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: [
2947
- /* @__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: [
2948
- renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs13("div", { className: "blade-chat-empty", children: [
2949
- /* @__PURE__ */ jsx15(MessageSquare, { size: 40, strokeWidth: 1.5 }),
2950
- /* @__PURE__ */ jsx15("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
2951
- /* @__PURE__ */ jsx15("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
2952
- ] }) : renderBlocks.map((block) => {
2953
- if (block.type === "message") {
2954
- 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);
2955
- }
2956
- if (block.type === "assistant_turn") {
2957
- const blockFeedback = block.messages.map(
2958
- (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
2959
- ).find((feedback) => feedback != null);
2960
- const hasActiveFollowup = Boolean(
2961
- postChatFollowup && block.messages.some(
2962
- (message) => message.entry_id === postChatFollowup.assistant_entry_id
2963
- )
2964
- );
2965
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs13(
2966
- RenderErrorBoundary,
2967
- {
2968
- label: "\u52A9\u624B\u6D88\u606F",
2969
- details: block.key,
2970
- resetKey: getMessageResetSignature(block.messages),
2971
- children: [
2972
- /* @__PURE__ */ jsx15(
2973
- AssistantTurnBlock,
2974
- {
2975
- messages: block.messages,
2976
- isStreaming: block.isStreaming,
2977
- askAnswers,
2978
- onAnswer,
2979
- sessionStatus,
2980
- toolCallRenderer,
2981
- sessionId
2982
- }
2983
- ),
2984
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
2985
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
2986
- PostChatFollowupBlock,
2987
- {
2988
- followup: postChatFollowup,
2989
- sessionId,
2990
- onSuggestion,
2991
- isViewer,
2992
- onInteraction: onFollowupInteraction,
2993
- savedFeedback: blockFeedback,
2994
- onFeedbackSaved: onResultFeedbackSaved
2995
- }
2996
- ) : null
2997
- ]
2998
- }
2999
- ) }, block.key);
3000
- }
3001
- if (block.type === "compaction") {
3002
- return /* @__PURE__ */ jsxs13(
3003
- "div",
4220
+ return /* @__PURE__ */ jsxs15("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: [
4221
+ isStreaming ? /* @__PURE__ */ jsx17("output", { className: "sr-only", children: "\u6B63\u5728\u751F\u6210\u56DE\u590D" }) : null,
4222
+ /* @__PURE__ */ jsxs15(
4223
+ StickToBottom,
4224
+ {
4225
+ className: "h-full overflow-y-hidden",
4226
+ initial: "instant",
4227
+ resize: "instant",
4228
+ children: [
4229
+ /* @__PURE__ */ jsx17(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsxs15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: [
4230
+ historyPaging && (historyPaging.hasOlder || historyPaging.loading) ? /* @__PURE__ */ jsx17(LoadOlderSentinel, { ...historyPaging }) : null,
4231
+ isStreaming && latestUserMessage && latestPromptPreview ? /* @__PURE__ */ jsx17(
4232
+ "button",
4233
+ {
4234
+ type: "button",
4235
+ 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",
4236
+ onClick: () => Array.from(document.querySelectorAll("[data-entry-id]")).find((el) => el.dataset.entryId === latestUserMessage.entry_id)?.scrollIntoView({ behavior: "smooth", block: "start" }),
4237
+ "aria-label": "\u8DF3\u8F6C\u5230\u5F53\u524D\u63D0\u95EE",
4238
+ children: latestPromptPreview
4239
+ }
4240
+ ) : null,
4241
+ /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 flex-col", children: [
4242
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs15("div", { className: "blade-chat-empty", children: [
4243
+ /* @__PURE__ */ jsx17(MessageSquare, { size: 40, strokeWidth: 1.5 }),
4244
+ /* @__PURE__ */ jsx17("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
4245
+ /* @__PURE__ */ jsx17("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
4246
+ ] }) : renderBlocks.map((block) => {
4247
+ if (block.type === "message") {
4248
+ 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);
4249
+ }
4250
+ if (block.type === "assistant_turn") {
4251
+ const blockFeedback = block.messages.map(
4252
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
4253
+ ).find((feedback) => feedback != null);
4254
+ const hasActiveFollowup = Boolean(
4255
+ postChatFollowup && block.messages.some(
4256
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
4257
+ )
4258
+ );
4259
+ return /* @__PURE__ */ jsx17("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs15(
4260
+ RenderErrorBoundary,
4261
+ {
4262
+ label: "\u52A9\u624B\u6D88\u606F",
4263
+ details: block.key,
4264
+ resetKey: getMessageResetSignature(block.messages),
4265
+ children: [
4266
+ /* @__PURE__ */ jsx17(
4267
+ AssistantTurnBlock,
4268
+ {
4269
+ messages: block.messages,
4270
+ isStreaming: block.isStreaming,
4271
+ askAnswers,
4272
+ onAnswer,
4273
+ sessionStatus,
4274
+ toolCallRenderer,
4275
+ hidePlanUpdateTools,
4276
+ sessionId
4277
+ }
4278
+ ),
4279
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx17(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
4280
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx17(
4281
+ PostChatFollowupBlock,
4282
+ {
4283
+ followup: postChatFollowup,
4284
+ sessionId,
4285
+ onSuggestion,
4286
+ isViewer,
4287
+ onInteraction: onFollowupInteraction,
4288
+ savedFeedback: blockFeedback,
4289
+ onFeedbackSaved: onResultFeedbackSaved
4290
+ }
4291
+ ) : null
4292
+ ]
4293
+ }
4294
+ ) }, block.key);
4295
+ }
4296
+ if (block.type === "compaction") {
4297
+ return /* @__PURE__ */ jsxs15(
4298
+ "div",
4299
+ {
4300
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
4301
+ children: [
4302
+ /* @__PURE__ */ jsx17(Layers, { size: 12 }),
4303
+ /* @__PURE__ */ jsx17("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
4304
+ ]
4305
+ },
4306
+ block.key
4307
+ );
4308
+ }
4309
+ return /* @__PURE__ */ jsx17(PlanningDivider, { kind: block.kind }, block.key);
4310
+ }),
4311
+ 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
4312
+ ] })
4313
+ ] }) }),
4314
+ /* @__PURE__ */ jsx17(
4315
+ PinLatestUserMessage,
3004
4316
  {
3005
- className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3006
- children: [
3007
- /* @__PURE__ */ jsx15(Layers, { size: 12 }),
3008
- /* @__PURE__ */ jsx15("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3009
- ]
4317
+ userMessageCount: userMessages.length,
4318
+ shouldPinLatestUser,
4319
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3010
4320
  },
3011
- block.key
3012
- );
4321
+ sessionId ?? "no-session"
4322
+ ),
4323
+ /* @__PURE__ */ jsx17(ScrollToBottomButton, {})
4324
+ ]
4325
+ },
4326
+ sessionId ?? "no-session"
4327
+ )
4328
+ ] });
4329
+ }
4330
+ function LoadOlderSentinel({
4331
+ hasOlder,
4332
+ loading,
4333
+ loadOlder
4334
+ }) {
4335
+ const { contentRef, scrollRef } = useStickToBottomContext();
4336
+ const sentinelRef = useRef12(null);
4337
+ const stateRef = useRef12({ hasOlder, loading, loadOlder });
4338
+ stateRef.current = { hasOlder, loading, loadOlder };
4339
+ useEffect11(() => {
4340
+ const sentinel = sentinelRef.current;
4341
+ const scroller = scrollRef.current;
4342
+ if (!sentinel || !scroller || typeof IntersectionObserver === "undefined") return;
4343
+ let entered = false;
4344
+ let disposed = false;
4345
+ const firstVisible = () => {
4346
+ const top = scroller.getBoundingClientRect().top;
4347
+ for (const row of contentRef.current?.querySelectorAll("[data-entry-id]") ?? []) {
4348
+ const rect = row.getBoundingClientRect();
4349
+ if (rect.bottom >= top) return { id: row.dataset.entryId, offset: rect.top - top };
4350
+ }
4351
+ return null;
4352
+ };
4353
+ const load = async () => {
4354
+ if (disposed || !stateRef.current.hasOlder || stateRef.current.loading) return;
4355
+ const commitSnapshot = { anchor: null, height: scroller.scrollHeight };
4356
+ let advanced = false;
4357
+ try {
4358
+ advanced = await stateRef.current.loadOlder(() => {
4359
+ commitSnapshot.anchor = firstVisible();
4360
+ commitSnapshot.height = scroller.scrollHeight;
4361
+ });
4362
+ } catch {
4363
+ return;
4364
+ }
4365
+ if (disposed) return;
4366
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
4367
+ if (disposed) return;
4368
+ const anchor = commitSnapshot.anchor;
4369
+ const heightBefore = commitSnapshot.height;
4370
+ const anchored = anchor?.id ? Array.from(contentRef.current?.querySelectorAll("[data-entry-id]") ?? []).find((row) => row.dataset.entryId === anchor.id) : null;
4371
+ if (anchored && anchor) {
4372
+ scroller.scrollTop += anchored.getBoundingClientRect().top - scroller.getBoundingClientRect().top - anchor.offset;
4373
+ } else {
4374
+ scroller.scrollTop += scroller.scrollHeight - heightBefore;
4375
+ }
4376
+ if (!disposed && advanced && stateRef.current.hasOlder && !stateRef.current.loading && scroller.scrollHeight <= scroller.clientHeight) {
4377
+ await load();
4378
+ }
4379
+ };
4380
+ const observer = new IntersectionObserver(
4381
+ ([entry]) => {
4382
+ if (!entry?.isIntersecting) {
4383
+ entered = false;
4384
+ return;
3013
4385
  }
3014
- return /* @__PURE__ */ jsx15(PlanningDivider, { kind: block.kind }, block.key);
3015
- }),
3016
- 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
3017
- ] }) }) }),
3018
- /* @__PURE__ */ jsx15(AutoScrollOnUserSend, { userMessageCount: messages.filter((m) => isUserMessage(m)).length }),
3019
- /* @__PURE__ */ jsx15(ScrollToBottomButton, {})
3020
- ] }) });
4386
+ if (entered) return;
4387
+ entered = true;
4388
+ void load();
4389
+ },
4390
+ { root: scroller, rootMargin: "160px 0px 0px" }
4391
+ );
4392
+ observer.observe(sentinel);
4393
+ return () => {
4394
+ disposed = true;
4395
+ observer.disconnect();
4396
+ };
4397
+ }, [contentRef, scrollRef]);
4398
+ return /* @__PURE__ */ jsx17("div", { ref: sentinelRef, "data-history-sentinel": true, className: "flex h-6 items-center justify-center", children: loading ? /* @__PURE__ */ jsx17("span", { className: "text-xs text-[hsl(var(--muted-foreground))]", children: "\u6B63\u5728\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F\u2026" }) : null });
3021
4399
  }
3022
- function AutoScrollOnUserSend({ userMessageCount }) {
3023
- const { scrollToBottom } = useStickToBottomContext();
3024
- const previousCountRef = useRef7(userMessageCount);
3025
- useEffect7(() => {
3026
- if (userMessageCount > previousCountRef.current) {
4400
+ function PinLatestUserMessage({
4401
+ userMessageCount,
4402
+ shouldPinLatestUser,
4403
+ targetKey
4404
+ }) {
4405
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
4406
+ const previousCountRef = useRef12(userMessageCount);
4407
+ const spacerHeightRef = useRef12(0);
4408
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
4409
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
4410
+ const getTargetElement = useCallback7(() => {
4411
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
4412
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
4413
+ }, [contentRef]);
4414
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
4415
+ const setSpacerHeight = useCallback7(
4416
+ (height) => {
4417
+ spacerHeightRef.current = height;
4418
+ const content = contentRef.current;
4419
+ if (!content) return;
4420
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
4421
+ else content.style.removeProperty("--blade-chat-pin-spacer");
4422
+ },
4423
+ [contentRef]
4424
+ );
4425
+ useMessagePin({
4426
+ targetKey,
4427
+ pinTarget: shouldPinLatestUser,
4428
+ getScrollElement,
4429
+ getContentElement,
4430
+ getTargetElement,
4431
+ getSpacerHeight,
4432
+ setSpacerHeight,
4433
+ stopAutoScroll: stopScroll,
4434
+ scrollToBottom
4435
+ });
4436
+ useEffect11(() => {
4437
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3027
4438
  scrollToBottom("instant");
3028
4439
  }
3029
4440
  previousCountRef.current = userMessageCount;
3030
- }, [userMessageCount, scrollToBottom]);
4441
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3031
4442
  return null;
3032
4443
  }
3033
4444
  function ScrollToBottomButton() {
3034
4445
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3035
- const [visible, setVisible] = useState11(false);
3036
- const hideTimerRef = useRef7(null);
3037
- useEffect7(() => {
4446
+ const [visible, setVisible] = useState14(false);
4447
+ const hideTimerRef = useRef12(null);
4448
+ useEffect11(() => {
3038
4449
  if (isAtBottom) {
3039
4450
  if (!hideTimerRef.current) {
3040
4451
  hideTimerRef.current = setTimeout(() => {
@@ -3056,7 +4467,7 @@ function ScrollToBottomButton() {
3056
4467
  }
3057
4468
  };
3058
4469
  }, [isAtBottom]);
3059
- const handleClick = useCallback6(() => {
4470
+ const handleClick = useCallback7(() => {
3060
4471
  if (hideTimerRef.current) {
3061
4472
  clearTimeout(hideTimerRef.current);
3062
4473
  hideTimerRef.current = null;
@@ -3065,7 +4476,7 @@ function ScrollToBottomButton() {
3065
4476
  scrollToBottom();
3066
4477
  }, [scrollToBottom]);
3067
4478
  if (!visible) return null;
3068
- return /* @__PURE__ */ jsxs13(
4479
+ return /* @__PURE__ */ jsxs15(
3069
4480
  "button",
3070
4481
  {
3071
4482
  type: "button",
@@ -3073,25 +4484,25 @@ function ScrollToBottomButton() {
3073
4484
  "aria-label": "\u6EDA\u52A8\u5230\u5E95\u90E8",
3074
4485
  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))]",
3075
4486
  children: [
3076
- /* @__PURE__ */ jsx15(ChevronDown, { size: 14 }),
3077
- /* @__PURE__ */ jsx15("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
4487
+ /* @__PURE__ */ jsx17(ChevronDown, { size: 14 }),
4488
+ /* @__PURE__ */ jsx17("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
3078
4489
  ]
3079
4490
  }
3080
4491
  );
3081
4492
  }
3082
4493
  function PlanningDivider({ kind }) {
3083
- return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 py-1", children: [
3084
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
3085
- /* @__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: [
3086
- /* @__PURE__ */ jsx15(Lightbulb, { size: 12 }),
3087
- /* @__PURE__ */ jsx15("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
4494
+ return /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-3 py-1", children: [
4495
+ /* @__PURE__ */ jsx17("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
4496
+ /* @__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: [
4497
+ /* @__PURE__ */ jsx17(Lightbulb, { size: 12 }),
4498
+ /* @__PURE__ */ jsx17("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
3088
4499
  ] }),
3089
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
4500
+ /* @__PURE__ */ jsx17("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
3090
4501
  ] });
3091
4502
  }
3092
4503
 
3093
4504
  // src/components/ChatSurface.tsx
3094
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4505
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3095
4506
  function themeAttr(theme) {
3096
4507
  return theme === "dark" ? "dark" : void 0;
3097
4508
  }
@@ -3111,6 +4522,7 @@ function ChatSurface({
3111
4522
  onInputChange,
3112
4523
  onSuggestion,
3113
4524
  onSend,
4525
+ onAppend,
3114
4526
  onStop,
3115
4527
  sessionStatus,
3116
4528
  askAnswers,
@@ -3121,9 +4533,12 @@ function ChatSurface({
3121
4533
  onResultFeedbackSaved,
3122
4534
  onFollowupInteraction,
3123
4535
  beforeInput,
4536
+ showPlanUpdates = false,
4537
+ planRevealRevision = 0,
4538
+ historyPaging,
3124
4539
  banner
3125
4540
  }) {
3126
- return /* @__PURE__ */ jsxs14(
4541
+ return /* @__PURE__ */ jsxs16(
3127
4542
  "div",
3128
4543
  {
3129
4544
  "data-theme": themeAttr(theme),
@@ -3132,14 +4547,14 @@ function ChatSurface({
3132
4547
  classNames?.root
3133
4548
  ),
3134
4549
  children: [
3135
- /* @__PURE__ */ jsx16(ConnectionBanner, { connection, className: classNames?.banner }),
4550
+ /* @__PURE__ */ jsx18(ConnectionBanner, { connection, className: classNames?.banner }),
3136
4551
  banner,
3137
- errorMessage && /* @__PURE__ */ jsxs14("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
3138
- /* @__PURE__ */ jsx16(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
3139
- /* @__PURE__ */ jsx16("span", { children: errorMessage })
4552
+ errorMessage && /* @__PURE__ */ jsxs16("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
4553
+ /* @__PURE__ */ jsx18(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
4554
+ /* @__PURE__ */ jsx18("span", { className: "min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: chatErrorForDisplay2(errorMessage) })
3140
4555
  ] }),
3141
4556
  slots?.header,
3142
- /* @__PURE__ */ jsx16(
4557
+ /* @__PURE__ */ jsx18(
3143
4558
  MessageList,
3144
4559
  {
3145
4560
  messages,
@@ -3150,26 +4565,40 @@ function ChatSurface({
3150
4565
  askAnswers,
3151
4566
  onAnswer,
3152
4567
  toolCallRenderer: renderers?.toolCall,
4568
+ hidePlanUpdateTools: showPlanUpdates,
3153
4569
  emptyState: slots?.emptyState,
3154
4570
  className: classNames?.messageList,
3155
4571
  sessionId,
3156
4572
  isViewer,
3157
4573
  resultFeedbackByEntry,
3158
4574
  onResultFeedbackSaved,
3159
- onFollowupInteraction
4575
+ onFollowupInteraction,
4576
+ historyPaging
3160
4577
  }
3161
4578
  ),
4579
+ showPlanUpdates ? /* @__PURE__ */ jsx18(
4580
+ CurrentPlanPanel,
4581
+ {
4582
+ messages,
4583
+ running: isStreaming,
4584
+ revealRevision: planRevealRevision,
4585
+ sessionId,
4586
+ className: "border-t border-[hsl(var(--border))]"
4587
+ }
4588
+ ) : null,
3162
4589
  beforeInput,
3163
- /* @__PURE__ */ jsx16(
4590
+ /* @__PURE__ */ jsx18(
3164
4591
  ChatInput,
3165
4592
  {
3166
4593
  value: inputText,
3167
4594
  onValueChange: onInputChange,
3168
4595
  onSend,
4596
+ onAppend,
3169
4597
  onStop,
3170
4598
  isStreaming,
3171
4599
  isStopping,
3172
4600
  placeholder,
4601
+ queueKey: sessionId,
3173
4602
  className: classNames?.chatInput
3174
4603
  }
3175
4604
  ),
@@ -3180,13 +4609,13 @@ function ChatSurface({
3180
4609
  }
3181
4610
 
3182
4611
  // src/components/AgentChat.tsx
3183
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
4612
+ import { Fragment as Fragment4, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3184
4613
  function isUnauthorizedError(error) {
3185
4614
  return error instanceof BladeApiError && error.status === 401;
3186
4615
  }
3187
4616
  function LoginCard({ client, onLoggedIn }) {
3188
- const [loggingIn, setLoggingIn] = useState12(false);
3189
- const [loginError, setLoginError] = useState12(null);
4617
+ const [loggingIn, setLoggingIn] = useState15(false);
4618
+ const [loginError, setLoginError] = useState15(null);
3190
4619
  const handleLogin = async () => {
3191
4620
  setLoggingIn(true);
3192
4621
  setLoginError(null);
@@ -3199,11 +4628,11 @@ function LoginCard({ client, onLoggedIn }) {
3199
4628
  setLoggingIn(false);
3200
4629
  }
3201
4630
  };
3202
- 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: [
3203
- /* @__PURE__ */ jsx17(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
3204
- /* @__PURE__ */ jsx17("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
3205
- /* @__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" }),
3206
- /* @__PURE__ */ jsx17(
4631
+ 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: [
4632
+ /* @__PURE__ */ jsx19(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
4633
+ /* @__PURE__ */ jsx19("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
4634
+ /* @__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" }),
4635
+ /* @__PURE__ */ jsx19(
3207
4636
  "button",
3208
4637
  {
3209
4638
  type: "button",
@@ -3213,20 +4642,20 @@ function LoginCard({ client, onLoggedIn }) {
3213
4642
  children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
3214
4643
  }
3215
4644
  ),
3216
- loginError && /* @__PURE__ */ jsx17("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
4645
+ loginError && /* @__PURE__ */ jsx19("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
3217
4646
  ] }) });
3218
4647
  }
3219
4648
  function AgentChat(props) {
3220
4649
  const client = useBladeClient();
3221
- const [attempt, setAttempt] = useState12(0);
3222
- const [needLogin, setNeedLogin] = useState12(() => !client.hasToken());
4650
+ const [attempt, setAttempt] = useState15(0);
4651
+ const [needLogin, setNeedLogin] = useState15(() => !client.hasToken());
3223
4652
  if (needLogin) {
3224
- return /* @__PURE__ */ jsx17(
4653
+ return /* @__PURE__ */ jsx19(
3225
4654
  "div",
3226
4655
  {
3227
4656
  "data-theme": themeAttr(props.theme),
3228
4657
  className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
3229
- children: /* @__PURE__ */ jsx17(
4658
+ children: /* @__PURE__ */ jsx19(
3230
4659
  LoginCard,
3231
4660
  {
3232
4661
  client,
@@ -3239,7 +4668,7 @@ function AgentChat(props) {
3239
4668
  }
3240
4669
  );
3241
4670
  }
3242
- return /* @__PURE__ */ jsx17(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
4671
+ return /* @__PURE__ */ jsx19(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
3243
4672
  }
3244
4673
  function ChatSessionView({
3245
4674
  sessionId,
@@ -3256,17 +4685,33 @@ function ChatSessionView({
3256
4685
  onUnauthorized
3257
4686
  }) {
3258
4687
  const client = useBladeClient();
4688
+ const [planRevealRevisions, setPlanRevealRevisions] = useState15(
4689
+ () => /* @__PURE__ */ new Map()
4690
+ );
4691
+ const handleSessionConnected = useCallback8((connectedSession) => {
4692
+ return connectedSession.on("toolResult", ({ toolCall, turn, source }) => {
4693
+ if (source === "reconnect_replay" || (turn.loop_id || "root") !== "root" || toolCall.status !== "done" || !isPlanUpdateTool(toolCall) || !parsePlanUpdate(toolCall.arguments)) {
4694
+ return;
4695
+ }
4696
+ setPlanRevealRevisions((current) => {
4697
+ const next = new Map(current);
4698
+ next.set(connectedSession.sessionId, (current.get(connectedSession.sessionId) ?? 0) + 1);
4699
+ return next;
4700
+ });
4701
+ });
4702
+ }, []);
3259
4703
  const { session, state, error } = useAgentSession(sessionId, {
3260
4704
  createOptions,
3261
- onSessionCreated
4705
+ onSessionCreated,
4706
+ onSessionConnected: handleSessionConnected
3262
4707
  });
3263
4708
  const replay = useReplay(session);
3264
- const [stopRequested, setStopRequested] = useState12(false);
3265
- const [inputText, setInputText] = useState12("");
3266
- const [resultFeedback, setResultFeedback] = useState12([]);
4709
+ const [stopRequested, setStopRequested] = useState15(false);
4710
+ const [inputText, setInputText] = useState15("");
4711
+ const [resultFeedback, setResultFeedback] = useState15([]);
3267
4712
  const resolvedSessionId = session?.sessionId;
3268
4713
  const isViewer = state?.viewerRole === "viewer";
3269
- useEffect8(() => {
4714
+ useEffect12(() => {
3270
4715
  setResultFeedback([]);
3271
4716
  if (!resolvedSessionId || isViewer) return;
3272
4717
  let cancelled = false;
@@ -3295,18 +4740,18 @@ function ChatSessionView({
3295
4740
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3296
4741
  [resultFeedback]
3297
4742
  );
3298
- const handleResultFeedbackSaved = useCallback7((saved) => {
4743
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3299
4744
  setResultFeedback((current) => [
3300
4745
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3301
4746
  saved
3302
4747
  ]);
3303
4748
  }, []);
3304
- useEffect8(() => {
4749
+ useEffect12(() => {
3305
4750
  if (session) {
3306
4751
  onSessionReady?.(session);
3307
4752
  }
3308
4753
  }, [session, onSessionReady]);
3309
- useEffect8(() => {
4754
+ useEffect12(() => {
3310
4755
  if (!session) return;
3311
4756
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3312
4757
  setInputText((prev) => `${prev ? `${prev}
@@ -3322,12 +4767,12 @@ ${content}`);
3322
4767
  offInsert();
3323
4768
  };
3324
4769
  }, [session]);
3325
- useEffect8(() => {
4770
+ useEffect12(() => {
3326
4771
  if (isUnauthorizedError(error)) {
3327
4772
  onUnauthorized();
3328
4773
  }
3329
4774
  }, [error, onUnauthorized]);
3330
- useEffect8(() => {
4775
+ useEffect12(() => {
3331
4776
  if (!session || !commands) return;
3332
4777
  const unsubscribes = Object.entries(commands).map(
3333
4778
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3337,6 +4782,7 @@ ${content}`);
3337
4782
  };
3338
4783
  }, [session, commands]);
3339
4784
  const isStreaming = state?.isStreaming ?? false;
4785
+ const planRevealRevision = resolvedSessionId ? planRevealRevisions.get(resolvedSessionId) ?? 0 : 0;
3340
4786
  const isStopping = stopRequested && isStreaming;
3341
4787
  const connectError = error && !isUnauthorizedError(error) ? error.message || "\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : null;
3342
4788
  const errorMessage = connectError ?? state?.errorMessage ?? replay.error?.message ?? null;
@@ -3348,7 +4794,7 @@ ${content}`);
3348
4794
  setStopRequested(true);
3349
4795
  void session?.stop();
3350
4796
  };
3351
- return /* @__PURE__ */ jsx17(
4797
+ return /* @__PURE__ */ jsx19(
3352
4798
  ChatSurface,
3353
4799
  {
3354
4800
  theme,
@@ -3358,8 +4804,8 @@ ${content}`);
3358
4804
  slots,
3359
4805
  placeholder,
3360
4806
  connection: state?.connection ?? "connecting",
3361
- banner: /* @__PURE__ */ jsxs15(Fragment3, { children: [
3362
- /* @__PURE__ */ jsx17(
4807
+ banner: /* @__PURE__ */ jsxs17(Fragment4, { children: [
4808
+ /* @__PURE__ */ jsx19(
3363
4809
  ReplayBar,
3364
4810
  {
3365
4811
  isReplay: replay.isReplay,
@@ -3369,7 +4815,7 @@ ${content}`);
3369
4815
  onExit: () => void replay.exitToAutonomous()
3370
4816
  }
3371
4817
  ),
3372
- /* @__PURE__ */ jsx17(ReplayMismatchPrompt, { mismatch: replay.mismatch })
4818
+ /* @__PURE__ */ jsx19(ReplayMismatchPrompt, { mismatch: replay.mismatch })
3373
4819
  ] }),
3374
4820
  errorMessage,
3375
4821
  messages: state?.messages ?? [],
@@ -3377,11 +4823,25 @@ ${content}`);
3377
4823
  resultFeedbackByEntry,
3378
4824
  onResultFeedbackSaved: handleResultFeedbackSaved,
3379
4825
  isStreaming,
4826
+ showPlanUpdates: true,
4827
+ planRevealRevision,
4828
+ historyPaging: session && state ? {
4829
+ hasOlder: session.hasOlderHistory,
4830
+ loading: state.loadingOlder,
4831
+ loadOlder: async (beforeCommit) => {
4832
+ const before = session.getState().nextBefore;
4833
+ await session.loadOlderHistory({ beforeCommit });
4834
+ return before !== session.getState().nextBefore;
4835
+ }
4836
+ } : void 0,
3380
4837
  isStopping,
3381
4838
  inputText,
3382
4839
  onInputChange: setInputText,
3383
4840
  onSuggestion: setInputText,
3384
4841
  onSend: handleSend,
4842
+ onAppend: (text) => {
4843
+ void session?.queue(text);
4844
+ },
3385
4845
  onStop: handleStop,
3386
4846
  sessionStatus: state?.status ?? void 0,
3387
4847
  askAnswers: state?.askAnswers,
@@ -3398,11 +4858,11 @@ ${content}`);
3398
4858
  }
3399
4859
 
3400
4860
  // src/components/LlmChat.tsx
3401
- import { useEffect as useEffect9, useMemo as useMemo9, useState as useState14 } from "react";
4861
+ import { useEffect as useEffect13, useMemo as useMemo9, useState as useState17 } from "react";
3402
4862
 
3403
4863
  // src/components/LlmAdvancedSettings.tsx
3404
- import { useState as useState13 } from "react";
3405
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
4864
+ import { useState as useState16 } from "react";
4865
+ import { jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3406
4866
  var FIELDS = [
3407
4867
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
3408
4868
  { id: "model", label: "\u6A21\u578B", placeholder: "\u6A21\u578B\u540D\u79F0" },
@@ -3448,13 +4908,13 @@ function writeOverride(settings, baseURL, override) {
3448
4908
  }
3449
4909
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3450
4910
  const normalized = normalizeAdvanced(settings);
3451
- const [open, setOpen] = useState13(false);
3452
- const [draft, setDraft] = useState13(override);
4911
+ const [open, setOpen] = useState16(false);
4912
+ const [draft, setDraft] = useState16(override);
3453
4913
  if (!normalized) return null;
3454
4914
  const fields = FIELDS.filter((field) => normalized[field.id]);
3455
4915
  const dirty = Object.keys(override).length > 0;
3456
- return /* @__PURE__ */ jsxs16("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
3457
- /* @__PURE__ */ jsxs16(
4916
+ return /* @__PURE__ */ jsxs18("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
4917
+ /* @__PURE__ */ jsxs18(
3458
4918
  "button",
3459
4919
  {
3460
4920
  type: "button",
@@ -3464,16 +4924,16 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3464
4924
  },
3465
4925
  className: "flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3466
4926
  children: [
3467
- /* @__PURE__ */ jsx18(Settings2, { size: 13 }),
4927
+ /* @__PURE__ */ jsx20(Settings2, { size: 13 }),
3468
4928
  "\u9AD8\u7EA7\u8BBE\u7F6E",
3469
- dirty && /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
4929
+ dirty && /* @__PURE__ */ jsx20("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
3470
4930
  ]
3471
4931
  }
3472
4932
  ),
3473
- open && /* @__PURE__ */ jsxs16("div", { className: "mt-2 flex flex-col gap-2", children: [
3474
- fields.map((field) => /* @__PURE__ */ jsxs16("label", { className: "flex flex-col gap-1", children: [
3475
- /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
3476
- /* @__PURE__ */ jsx18(
4933
+ open && /* @__PURE__ */ jsxs18("div", { className: "mt-2 flex flex-col gap-2", children: [
4934
+ fields.map((field) => /* @__PURE__ */ jsxs18("label", { className: "flex flex-col gap-1", children: [
4935
+ /* @__PURE__ */ jsx20("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
4936
+ /* @__PURE__ */ jsx20(
3477
4937
  "input",
3478
4938
  {
3479
4939
  type: field.secret ? "password" : "text",
@@ -3484,9 +4944,9 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3484
4944
  }
3485
4945
  )
3486
4946
  ] }, field.id)),
3487
- 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" }),
3488
- /* @__PURE__ */ jsxs16("div", { className: "flex gap-2", children: [
3489
- /* @__PURE__ */ jsx18(
4947
+ 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" }),
4948
+ /* @__PURE__ */ jsxs18("div", { className: "flex gap-2", children: [
4949
+ /* @__PURE__ */ jsx20(
3490
4950
  "button",
3491
4951
  {
3492
4952
  type: "button",
@@ -3501,7 +4961,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3501
4961
  children: "\u4FDD\u5B58"
3502
4962
  }
3503
4963
  ),
3504
- /* @__PURE__ */ jsx18(
4964
+ /* @__PURE__ */ jsx20(
3505
4965
  "button",
3506
4966
  {
3507
4967
  type: "button",
@@ -3520,7 +4980,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3520
4980
  }
3521
4981
 
3522
4982
  // src/components/LlmChat.tsx
3523
- import { jsx as jsx19 } from "react/jsx-runtime";
4983
+ import { jsx as jsx21 } from "react/jsx-runtime";
3524
4984
  function LlmChat({
3525
4985
  classNames,
3526
4986
  renderers,
@@ -3532,11 +4992,11 @@ function LlmChat({
3532
4992
  onOverrideChange,
3533
4993
  ...options
3534
4994
  }) {
3535
- const [override, setOverride] = useState14(() => readOverride(advanced, options.baseURL));
4995
+ const [override, setOverride] = useState17(() => readOverride(advanced, options.baseURL));
3536
4996
  const effective = { ...options, ...override };
3537
4997
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3538
- const [inputText, setInputText] = useState14("");
3539
- const [stopRequested, setStopRequested] = useState14(false);
4998
+ const [inputText, setInputText] = useState17("");
4999
+ const [stopRequested, setStopRequested] = useState17(false);
3540
5000
  const handle = useMemo9(
3541
5001
  () => ({
3542
5002
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3546,10 +5006,10 @@ ${text}` : text),
3546
5006
  }),
3547
5007
  [send, reset]
3548
5008
  );
3549
- useEffect9(() => {
5009
+ useEffect13(() => {
3550
5010
  onReady?.(handle);
3551
5011
  }, [handle, onReady]);
3552
- return /* @__PURE__ */ jsx19(
5012
+ return /* @__PURE__ */ jsx21(
3553
5013
  ChatSurface,
3554
5014
  {
3555
5015
  theme,
@@ -3574,7 +5034,7 @@ ${text}` : text),
3574
5034
  setStopRequested(true);
3575
5035
  stop();
3576
5036
  },
3577
- beforeInput: advanced ? /* @__PURE__ */ jsx19(
5037
+ beforeInput: advanced ? /* @__PURE__ */ jsx21(
3578
5038
  LlmAdvancedSettingsBar,
3579
5039
  {
3580
5040
  settings: advanced,
@@ -3592,14 +5052,14 @@ ${text}` : text),
3592
5052
  }
3593
5053
 
3594
5054
  // src/components/ChatView.tsx
3595
- import { jsx as jsx20 } from "react/jsx-runtime";
5055
+ import { jsx as jsx22 } from "react/jsx-runtime";
3596
5056
  function ChatView(props) {
3597
5057
  const { mode, llm, onLlmReady, ...rest } = props;
3598
5058
  if (mode === "llm") {
3599
5059
  if (!llm) {
3600
5060
  throw new Error('ChatView: mode="llm" \u9700\u8981\u540C\u65F6\u4F20 llm={{ baseURL, model }}');
3601
5061
  }
3602
- return /* @__PURE__ */ jsx20(
5062
+ return /* @__PURE__ */ jsx22(
3603
5063
  LlmChat,
3604
5064
  {
3605
5065
  ...llm,
@@ -3612,7 +5072,232 @@ function ChatView(props) {
3612
5072
  }
3613
5073
  );
3614
5074
  }
3615
- return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
5075
+ return /* @__PURE__ */ jsx22(AgentChat, { ...rest });
5076
+ }
5077
+
5078
+ // src/components/ContextCard.tsx
5079
+ import {
5080
+ getContextDisplayState,
5081
+ getContextGroupDisplayState
5082
+ } from "@blade-hq/agent-client";
5083
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
5084
+ function ContextCard({ context, className }) {
5085
+ const display = getContextDisplayState(context);
5086
+ return /* @__PURE__ */ jsxs19(
5087
+ "details",
5088
+ {
5089
+ className: `blade-chat-context-card group/context-card rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
5090
+ children: [
5091
+ /* @__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: [
5092
+ /* @__PURE__ */ jsx23(
5093
+ Layers,
5094
+ {
5095
+ size: 15,
5096
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5097
+ "aria-hidden": "true"
5098
+ }
5099
+ ),
5100
+ /* @__PURE__ */ jsxs19("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5101
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
5102
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
5103
+ ] }),
5104
+ /* @__PURE__ */ jsx23(
5105
+ ChevronDown,
5106
+ {
5107
+ size: 14,
5108
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-card:rotate-180",
5109
+ "aria-hidden": "true"
5110
+ }
5111
+ )
5112
+ ] }),
5113
+ /* @__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 })
5114
+ ]
5115
+ }
5116
+ );
5117
+ }
5118
+ function ContextGroupCard({ contexts, className }) {
5119
+ if (contexts.length === 0) return null;
5120
+ const single = contexts.length === 1 ? getContextDisplayState(contexts[0]) : null;
5121
+ const group = single ? null : getContextGroupDisplayState(contexts);
5122
+ 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: [
5123
+ /* @__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: [
5124
+ /* @__PURE__ */ jsx23(
5125
+ Layers,
5126
+ {
5127
+ size: 15,
5128
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5129
+ "aria-hidden": "true"
5130
+ }
5131
+ ),
5132
+ /* @__PURE__ */ jsxs19("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5133
+ /* @__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` }),
5134
+ /* @__PURE__ */ jsx23("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: single ? single.summary : group?.summary })
5135
+ ] }),
5136
+ /* @__PURE__ */ jsx23(
5137
+ ChevronDown,
5138
+ {
5139
+ size: 14,
5140
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-group:rotate-180",
5141
+ "aria-hidden": "true"
5142
+ }
5143
+ )
5144
+ ] }),
5145
+ 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(
5146
+ ContextCard,
5147
+ {
5148
+ context
5149
+ },
5150
+ `${context.context_kind}:${context.context_key}`
5151
+ )) })
5152
+ ] });
5153
+ }
5154
+
5155
+ // src/components/SessionMemoryToggle.tsx
5156
+ import { useCallback as useCallback9, useEffect as useEffect14, useRef as useRef13, useState as useState18, useSyncExternalStore as useSyncExternalStore2 } from "react";
5157
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
5158
+ var saveStates = /* @__PURE__ */ new WeakMap();
5159
+ function getSaveState(client, sessionId) {
5160
+ let clientStates = saveStates.get(client);
5161
+ if (!clientStates) {
5162
+ clientStates = /* @__PURE__ */ new Map();
5163
+ saveStates.set(client, clientStates);
5164
+ }
5165
+ let state = clientStates.get(sessionId);
5166
+ if (!state) {
5167
+ state = { saving: false, listeners: /* @__PURE__ */ new Set() };
5168
+ clientStates.set(sessionId, state);
5169
+ }
5170
+ return state;
5171
+ }
5172
+ function notify(state) {
5173
+ for (const listener of state.listeners) listener();
5174
+ }
5175
+ function cleanupSaveState(client, sessionId, state) {
5176
+ if (state.saving || state.listeners.size > 0) return;
5177
+ const clientStates = saveStates.get(client);
5178
+ if (clientStates?.get(sessionId) === state) clientStates.delete(sessionId);
5179
+ }
5180
+ function SessionMemoryToggle({
5181
+ sessionId,
5182
+ enabled,
5183
+ client: clientProp,
5184
+ disabled = false,
5185
+ label = "\u5F53\u524D\u4F1A\u8BDD\u4F7F\u7528\u8BB0\u5FC6",
5186
+ className,
5187
+ labelClassName,
5188
+ inputClassName,
5189
+ onSaved,
5190
+ onError
5191
+ }) {
5192
+ const contextClient = useOptionalBladeClient();
5193
+ const client = clientProp ?? contextClient;
5194
+ if (!client) {
5195
+ throw new Error("SessionMemoryToggle \u5FC5\u987B\u5728 <BladeProvider> \u5185\u4F7F\u7528\u6216\u663E\u5F0F\u4F20\u5165 client");
5196
+ }
5197
+ const saveState = getSaveState(client, sessionId);
5198
+ const subscribe = useCallback9(
5199
+ (listener) => {
5200
+ saveState.listeners.add(listener);
5201
+ return () => {
5202
+ saveState.listeners.delete(listener);
5203
+ cleanupSaveState(client, sessionId, saveState);
5204
+ };
5205
+ },
5206
+ [client, saveState, sessionId]
5207
+ );
5208
+ const getSaving = useCallback9(() => saveState.saving, [saveState]);
5209
+ const saving = useSyncExternalStore2(
5210
+ subscribe,
5211
+ getSaving,
5212
+ getSaving
5213
+ );
5214
+ const [draftEnabled, setDraftEnabled] = useState18(enabled);
5215
+ const activeSessionIdRef = useRef13(sessionId);
5216
+ activeSessionIdRef.current = sessionId;
5217
+ useEffect14(() => {
5218
+ setDraftEnabled(enabled);
5219
+ }, [enabled, sessionId]);
5220
+ const update = useCallback9(
5221
+ (nextEnabled) => {
5222
+ const currentSaveState = getSaveState(client, sessionId);
5223
+ if (currentSaveState.saving) return;
5224
+ currentSaveState.saving = true;
5225
+ notify(currentSaveState);
5226
+ setDraftEnabled(nextEnabled);
5227
+ void client.sessions.updateSessionMemory(sessionId, nextEnabled).then(
5228
+ (updated) => {
5229
+ if (activeSessionIdRef.current === sessionId) {
5230
+ setDraftEnabled(updated.memory_enabled);
5231
+ }
5232
+ onSaved?.(sessionId, updated.memory_enabled);
5233
+ },
5234
+ (error) => {
5235
+ if (activeSessionIdRef.current === sessionId) setDraftEnabled(enabled);
5236
+ onError?.(error);
5237
+ }
5238
+ ).finally(() => {
5239
+ currentSaveState.saving = false;
5240
+ notify(currentSaveState);
5241
+ cleanupSaveState(client, sessionId, currentSaveState);
5242
+ });
5243
+ },
5244
+ [client, enabled, onError, onSaved, sessionId]
5245
+ );
5246
+ return /* @__PURE__ */ jsxs20("label", { className: cn("flex items-center justify-between", className), children: [
5247
+ /* @__PURE__ */ jsx24("span", { className: labelClassName, children: label }),
5248
+ /* @__PURE__ */ jsx24(
5249
+ "input",
5250
+ {
5251
+ type: "checkbox",
5252
+ checked: draftEnabled,
5253
+ onChange: (event) => update(event.target.checked),
5254
+ disabled: disabled || saving,
5255
+ className: inputClassName
5256
+ }
5257
+ )
5258
+ ] });
5259
+ }
5260
+
5261
+ // src/lib/agent-computer-command.ts
5262
+ var COMPUTER_LAUNCH_COMMAND_PATTERN = /(?:^|[\n;&|(]\s*)computer\s+launch(?:\s|$)/;
5263
+ function isAgentComputerCommand(command) {
5264
+ return COMPUTER_LAUNCH_COMMAND_PATTERN.test(command);
5265
+ }
5266
+ function isAgentComputerToolCall(argumentsJson) {
5267
+ if (!argumentsJson) return false;
5268
+ let command;
5269
+ try {
5270
+ const parsed = JSON.parse(argumentsJson);
5271
+ if (typeof parsed !== "object" || parsed === null) return false;
5272
+ command = parsed.command;
5273
+ } catch {
5274
+ return false;
5275
+ }
5276
+ return typeof command === "string" && isAgentComputerCommand(command);
5277
+ }
5278
+ var LAUNCH_SUCCESS_MARKER = "\u5DF2\u542F\u52A8 ";
5279
+ function resultContainsLaunchSuccessMarker(result, depth = 0) {
5280
+ if (depth > 2) return false;
5281
+ if (typeof result === "string") {
5282
+ if (result.includes(LAUNCH_SUCCESS_MARKER)) return true;
5283
+ try {
5284
+ return resultContainsLaunchSuccessMarker(JSON.parse(result), depth + 1);
5285
+ } catch {
5286
+ return false;
5287
+ }
5288
+ }
5289
+ if (typeof result === "object" && result !== null) {
5290
+ for (const value of Object.values(result)) {
5291
+ if (typeof value === "string" && value.includes(LAUNCH_SUCCESS_MARKER)) return true;
5292
+ }
5293
+ }
5294
+ return false;
5295
+ }
5296
+ function classifyAgentComputerLaunchOutcome(toolCall) {
5297
+ if (toolCall.status === "error" || toolCall.status === "cancelled") return "failed";
5298
+ if (toolCall.status !== "done") return "pending";
5299
+ if (toolCall.result === void 0 || toolCall.result === null) return "unknown";
5300
+ return resultContainsLaunchSuccessMarker(toolCall.result) ? "succeeded" : "failed";
3616
5301
  }
3617
5302
 
3618
5303
  // src/index.ts
@@ -3621,13 +5306,32 @@ export {
3621
5306
  AgentChat,
3622
5307
  BladeProvider,
3623
5308
  ChatView,
5309
+ ContextCard,
5310
+ ContextGroupCard,
5311
+ CurrentPlanPanel,
3624
5312
  LlmChat,
3625
5313
  MarkdownContent,
5314
+ MemoryRefsHint,
5315
+ PLAN_AUTO_COLLAPSE_MS,
5316
+ PlanUpdateBlock,
3626
5317
  ReplayBar,
3627
5318
  ReplayMismatchPrompt,
5319
+ SessionMemoryToggle,
5320
+ WhatIfUserBubble,
5321
+ classifyAgentComputerLaunchOutcome,
5322
+ collectMemoryRefs,
5323
+ getPlanUpdateDisplayState,
5324
+ isAgentComputerCommand,
5325
+ isAgentComputerToolCall,
5326
+ isPlanUpdateTool,
5327
+ normalizeAdjacentUrlFormatting,
5328
+ parsePlanUpdate,
5329
+ parseWhatIfPrompt,
5330
+ pickCurrentPlanStep,
3628
5331
  useAgentSession,
3629
5332
  useBladeClient,
3630
5333
  useLlmChat,
5334
+ useMessagePin,
3631
5335
  useReplay
3632
5336
  };
3633
5337
  /*! Bundled license information:
@@ -3639,29 +5343,35 @@ lucide-react/dist/esm/createLucideIcon.js:
3639
5343
  lucide-react/dist/esm/icons/arrow-right.js:
3640
5344
  lucide-react/dist/esm/icons/arrow-up-right.js:
3641
5345
  lucide-react/dist/esm/icons/arrow-up.js:
5346
+ lucide-react/dist/esm/icons/book-open.js:
3642
5347
  lucide-react/dist/esm/icons/bot.js:
3643
- lucide-react/dist/esm/icons/brain.js:
3644
5348
  lucide-react/dist/esm/icons/check.js:
3645
5349
  lucide-react/dist/esm/icons/chevron-down.js:
3646
5350
  lucide-react/dist/esm/icons/chevron-right.js:
3647
5351
  lucide-react/dist/esm/icons/circle-alert.js:
5352
+ lucide-react/dist/esm/icons/circle-dot.js:
5353
+ lucide-react/dist/esm/icons/circle.js:
3648
5354
  lucide-react/dist/esm/icons/copy.js:
3649
- lucide-react/dist/esm/icons/download.js:
5355
+ lucide-react/dist/esm/icons/earth.js:
5356
+ lucide-react/dist/esm/icons/file-pen-line.js:
3650
5357
  lucide-react/dist/esm/icons/file-text.js:
3651
- lucide-react/dist/esm/icons/file.js:
3652
- lucide-react/dist/esm/icons/film.js:
3653
5358
  lucide-react/dist/esm/icons/globe.js:
3654
5359
  lucide-react/dist/esm/icons/layers.js:
3655
5360
  lucide-react/dist/esm/icons/lightbulb.js:
5361
+ lucide-react/dist/esm/icons/list-checks.js:
3656
5362
  lucide-react/dist/esm/icons/loader-circle.js:
3657
5363
  lucide-react/dist/esm/icons/lock-keyhole.js:
3658
5364
  lucide-react/dist/esm/icons/message-square-more.js:
3659
5365
  lucide-react/dist/esm/icons/message-square.js:
3660
5366
  lucide-react/dist/esm/icons/play.js:
5367
+ lucide-react/dist/esm/icons/refresh-ccw.js:
5368
+ lucide-react/dist/esm/icons/search.js:
3661
5369
  lucide-react/dist/esm/icons/settings-2.js:
3662
5370
  lucide-react/dist/esm/icons/sparkles.js:
3663
5371
  lucide-react/dist/esm/icons/square.js:
5372
+ lucide-react/dist/esm/icons/terminal.js:
3664
5373
  lucide-react/dist/esm/icons/triangle-alert.js:
5374
+ lucide-react/dist/esm/icons/wrench.js:
3665
5375
  lucide-react/dist/esm/icons/x.js:
3666
5376
  lucide-react/dist/esm/lucide-react.js:
3667
5377
  (**