@blade-hq/agent-react 2610.0.0-beta.3 → 2610.0.0-beta.30

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
@@ -245,6 +245,8 @@ function useLlmChat(options) {
245
245
  const [error, setError] = useState3(null);
246
246
  const [isStreaming, setIsStreaming] = useState3(false);
247
247
  const abortRef = useRef2(null);
248
+ const activeStartedAtRef = useRef2(null);
249
+ const assistantTimingsRef = useRef2(/* @__PURE__ */ new WeakMap());
248
250
  const generationRef = useRef2(0);
249
251
  const historyRef = useRef2([]);
250
252
  const optionsRef = useRef2(options);
@@ -263,16 +265,31 @@ function useLlmChat(options) {
263
265
  setFailedToolIds([]);
264
266
  setError(null);
265
267
  setIsStreaming(false);
268
+ activeStartedAtRef.current = null;
269
+ assistantTimingsRef.current = /* @__PURE__ */ new WeakMap();
266
270
  }, [stop]);
267
271
  const send = useCallback2(async (text) => {
268
272
  const content = text.trim();
269
273
  if (!content || abortRef.current) return false;
270
274
  const opts = optionsRef.current;
271
275
  const maxRounds = opts.maxToolRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
276
+ const startedAt = Date.now();
277
+ activeStartedAtRef.current = startedAt;
272
278
  const commit = (message) => {
273
279
  historyRef.current = [...historyRef.current, message];
274
280
  setHistory(historyRef.current);
275
281
  };
282
+ const latestAssistantTiming = { current: null };
283
+ const commitAssistant = (message) => {
284
+ const timing = { startedAt };
285
+ assistantTimingsRef.current.set(message, timing);
286
+ latestAssistantTiming.current = timing;
287
+ commit(message);
288
+ return timing;
289
+ };
290
+ const finishTiming = (timing) => {
291
+ timing.durationMs = Math.max(0, Date.now() - timing.startedAt);
292
+ };
276
293
  setError(null);
277
294
  setIsStreaming(true);
278
295
  setStreamingText("");
@@ -298,8 +315,11 @@ function useLlmChat(options) {
298
315
  };
299
316
  setStreamingText(null);
300
317
  setStreamingCalls([]);
301
- commit(assistant);
302
- if (!result.toolCalls.length) return true;
318
+ const assistantTiming = commitAssistant(assistant);
319
+ if (!result.toolCalls.length) {
320
+ finishTiming(assistantTiming);
321
+ return true;
322
+ }
303
323
  const bail = (reason) => {
304
324
  for (const call of result.toolCalls) {
305
325
  commit({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: reason }) });
@@ -308,11 +328,13 @@ function useLlmChat(options) {
308
328
  };
309
329
  if (!opts.onToolCall) {
310
330
  bail("\u8FD9\u4E2A\u5E94\u7528\u6CA1\u6709\u63D0\u4F9B\u5DE5\u5177\u6267\u884C\u5165\u53E3");
331
+ finishTiming(assistantTiming);
311
332
  return true;
312
333
  }
313
334
  if (round >= maxRounds) {
314
335
  bail(`\u5DE5\u5177\u8C03\u7528\u5DF2\u8FBE\u4E0A\u9650 ${maxRounds} \u8F6E\uFF0C\u6CA1\u6709\u6267\u884C`);
315
336
  setError(`\u5DE5\u5177\u8C03\u7528\u8D85\u8FC7 ${maxRounds} \u8F6E\u4ECD\u672A\u7ED9\u51FA\u7ED3\u8BBA\uFF0C\u5DF2\u505C\u4E0B\u3002`);
337
+ finishTiming(assistantTiming);
316
338
  return false;
317
339
  }
318
340
  for (const call of result.toolCalls) {
@@ -337,21 +359,39 @@ function useLlmChat(options) {
337
359
  setStreamingCalls([]);
338
360
  if (controller.signal.aborted) {
339
361
  if (generation !== generationRef.current) return false;
340
- commit({ role: "assistant", content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09" });
362
+ const timing = commitAssistant({
363
+ role: "assistant",
364
+ content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09"
365
+ });
366
+ timing.status = "interrupted";
367
+ finishTiming(timing);
341
368
  return false;
342
369
  }
343
370
  if (partial && generation === generationRef.current) {
344
- commit({ role: "assistant", content: partial });
371
+ const timing = commitAssistant({ role: "assistant", content: partial });
372
+ timing.status = "failed";
373
+ finishTiming(timing);
374
+ } else if (latestAssistantTiming.current) {
375
+ latestAssistantTiming.current.status = "failed";
376
+ finishTiming(latestAssistantTiming.current);
345
377
  }
346
378
  setError(err instanceof Error && err.message ? err.message : "\u5BF9\u8BDD\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
347
379
  return false;
348
380
  } finally {
349
381
  if (abortRef.current === controller) abortRef.current = null;
382
+ activeStartedAtRef.current = null;
350
383
  setIsStreaming(false);
351
384
  }
352
385
  }, []);
353
386
  const messages = useMemo2(
354
- () => toChatMessages(history, streamingText, streamingCalls, failedToolIds),
387
+ () => toChatMessages(
388
+ history,
389
+ streamingText,
390
+ streamingCalls,
391
+ failedToolIds,
392
+ assistantTimingsRef.current,
393
+ activeStartedAtRef.current
394
+ ),
355
395
  [history, streamingText, streamingCalls, failedToolIds]
356
396
  );
357
397
  return { messages, isStreaming, error, send, stop, reset };
@@ -503,7 +543,7 @@ function parseSse(raw) {
503
543
  if (!delta) return null;
504
544
  return { text: delta.content, toolCallDeltas: delta.tool_calls };
505
545
  }
506
- function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
546
+ function toChatMessages(history, streamingText, streamingCalls, failedToolIds, assistantTimings, activeStartedAt) {
507
547
  const results = /* @__PURE__ */ new Map();
508
548
  for (const msg of history) {
509
549
  if (msg.role === "tool") results.set(msg.tool_call_id, msg.content);
@@ -515,10 +555,13 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
515
555
  messages.push({ role: "user", content: msg.content, status: "completed" });
516
556
  continue;
517
557
  }
558
+ const timing = assistantTimings.get(msg);
518
559
  messages.push({
519
560
  role: "assistant",
520
561
  content: msg.content,
521
- status: "completed",
562
+ status: timing?.status ?? "completed",
563
+ ...timing ? { timestamp: new Date(timing.startedAt).toISOString() } : {},
564
+ ...timing?.durationMs === void 0 ? {} : { duration_ms: timing.durationMs },
522
565
  ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
523
566
  });
524
567
  }
@@ -527,6 +570,7 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
527
570
  role: "assistant",
528
571
  content: streamingText,
529
572
  status: "streaming",
573
+ ...activeStartedAt === null ? {} : { timestamp: new Date(activeStartedAt).toISOString() },
530
574
  ...streamingCalls.length ? { tool_calls: streamingCalls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
531
575
  });
532
576
  }
@@ -544,6 +588,184 @@ function toToolCallInfo(call, results, failedToolIds) {
544
588
  };
545
589
  }
546
590
 
591
+ // src/hooks/use-message-pin.ts
592
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3 } from "react";
593
+ var DEFAULT_MARGIN_PX = 24;
594
+ var MANUAL_SCROLL_TOLERANCE_PX = 2;
595
+ function useMessagePin({
596
+ targetKey,
597
+ pinTarget,
598
+ layoutKey,
599
+ getScrollElement,
600
+ getContentElement,
601
+ getTargetElement,
602
+ getTargetScrollTop,
603
+ getSpacerHeight,
604
+ setSpacerHeight,
605
+ stopAutoScroll,
606
+ scrollToBottom,
607
+ margin = DEFAULT_MARGIN_PX
608
+ }) {
609
+ const pinActiveRef = useRef3(false);
610
+ const frameRef = useRef3(null);
611
+ const retryTimeoutRef = useRef3(null);
612
+ const manualScrollCheckTimeoutRef = useRef3(null);
613
+ const repositionPendingRef = useRef3(false);
614
+ const pinnedTargetScrollTopRef = useRef3(null);
615
+ const appliedTargetKeyRef = useRef3(null);
616
+ const initialObservedTargetKeyRef = useRef3(pinTarget ? null : targetKey);
617
+ const observedTargetKeyRef = useRef3(initialObservedTargetKeyRef.current);
618
+ const setSpacerHeightRef = useRef3(setSpacerHeight);
619
+ setSpacerHeightRef.current = setSpacerHeight;
620
+ const release = useCallback3(() => {
621
+ if (!pinActiveRef.current) return;
622
+ pinActiveRef.current = false;
623
+ repositionPendingRef.current = false;
624
+ pinnedTargetScrollTopRef.current = null;
625
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
626
+ frameRef.current = null;
627
+ if (manualScrollCheckTimeoutRef.current != null) {
628
+ clearTimeout(manualScrollCheckTimeoutRef.current);
629
+ manualScrollCheckTimeoutRef.current = null;
630
+ }
631
+ setSpacerHeightRef.current(0);
632
+ }, []);
633
+ const reposition = useCallback3(
634
+ () => {
635
+ const scroll = getScrollElement();
636
+ const target = getTargetElement();
637
+ if (!scroll || !pinActiveRef.current) return;
638
+ const targetScrollTop = target ? Math.max(
639
+ 0,
640
+ scroll.scrollTop + target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - margin
641
+ ) : getTargetScrollTop?.(scroll);
642
+ if (targetScrollTop == null) return;
643
+ const previousSpacerHeight = getSpacerHeight();
644
+ const baseScrollHeight = scroll.scrollHeight - previousSpacerHeight;
645
+ const nextSpacerHeight = Math.max(
646
+ 0,
647
+ targetScrollTop + scroll.clientHeight - baseScrollHeight
648
+ );
649
+ setSpacerHeightRef.current(nextSpacerHeight);
650
+ pinnedTargetScrollTopRef.current = targetScrollTop;
651
+ stopAutoScroll();
652
+ scroll.scrollTop = targetScrollTop;
653
+ if (nextSpacerHeight === 0 && previousSpacerHeight > 0) {
654
+ pinActiveRef.current = false;
655
+ scrollToBottom();
656
+ }
657
+ },
658
+ [
659
+ getScrollElement,
660
+ getSpacerHeight,
661
+ getTargetElement,
662
+ getTargetScrollTop,
663
+ margin,
664
+ scrollToBottom,
665
+ stopAutoScroll
666
+ ]
667
+ );
668
+ const scheduleReposition = useCallback3(
669
+ () => {
670
+ if (!pinActiveRef.current) return;
671
+ repositionPendingRef.current = true;
672
+ if (frameRef.current != null) return;
673
+ frameRef.current = requestAnimationFrame(() => {
674
+ frameRef.current = null;
675
+ const shouldReposition = repositionPendingRef.current;
676
+ repositionPendingRef.current = false;
677
+ if (shouldReposition) reposition();
678
+ });
679
+ },
680
+ [reposition]
681
+ );
682
+ useEffect3(() => {
683
+ if (!targetKey) {
684
+ release();
685
+ appliedTargetKeyRef.current = null;
686
+ observedTargetKeyRef.current = null;
687
+ return;
688
+ }
689
+ if (!pinTarget) {
690
+ if (pinActiveRef.current && appliedTargetKeyRef.current != null && appliedTargetKeyRef.current !== targetKey) {
691
+ release();
692
+ }
693
+ observedTargetKeyRef.current = targetKey;
694
+ return;
695
+ }
696
+ if (appliedTargetKeyRef.current === targetKey) {
697
+ observedTargetKeyRef.current = targetKey;
698
+ return;
699
+ }
700
+ if (observedTargetKeyRef.current === targetKey) return;
701
+ appliedTargetKeyRef.current = targetKey;
702
+ observedTargetKeyRef.current = targetKey;
703
+ pinActiveRef.current = true;
704
+ stopAutoScroll();
705
+ scheduleReposition();
706
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
707
+ retryTimeoutRef.current = window.setTimeout(() => {
708
+ retryTimeoutRef.current = null;
709
+ scheduleReposition();
710
+ }, 80);
711
+ }, [pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
712
+ useEffect3(() => {
713
+ if (layoutKey !== void 0) scheduleReposition();
714
+ }, [layoutKey, scheduleReposition]);
715
+ useEffect3(() => {
716
+ const scroll = getScrollElement();
717
+ if (!scroll) return;
718
+ const content = getContentElement?.();
719
+ const observer = new ResizeObserver(scheduleReposition);
720
+ observer.observe(scroll);
721
+ if (content && content !== scroll) observer.observe(content);
722
+ window.addEventListener("resize", scheduleReposition);
723
+ window.visualViewport?.addEventListener("resize", scheduleReposition);
724
+ const handleScroll = () => {
725
+ if (!pinActiveRef.current) return;
726
+ if (manualScrollCheckTimeoutRef.current != null) {
727
+ clearTimeout(manualScrollCheckTimeoutRef.current);
728
+ }
729
+ manualScrollCheckTimeoutRef.current = window.setTimeout(() => {
730
+ manualScrollCheckTimeoutRef.current = null;
731
+ if (!pinActiveRef.current || repositionPendingRef.current) return;
732
+ const targetScrollTop = pinnedTargetScrollTopRef.current;
733
+ if (targetScrollTop != null && Math.abs(scroll.scrollTop - targetScrollTop) > MANUAL_SCROLL_TOLERANCE_PX) {
734
+ release();
735
+ }
736
+ }, 0);
737
+ };
738
+ scroll.addEventListener("scroll", handleScroll, { passive: true });
739
+ return () => {
740
+ observer.disconnect();
741
+ window.removeEventListener("resize", scheduleReposition);
742
+ window.visualViewport?.removeEventListener("resize", scheduleReposition);
743
+ scroll.removeEventListener("scroll", handleScroll);
744
+ };
745
+ }, [getContentElement, getScrollElement, release, scheduleReposition]);
746
+ useEffect3(
747
+ () => () => {
748
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
749
+ frameRef.current = null;
750
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
751
+ retryTimeoutRef.current = null;
752
+ if (manualScrollCheckTimeoutRef.current != null) {
753
+ clearTimeout(manualScrollCheckTimeoutRef.current);
754
+ }
755
+ manualScrollCheckTimeoutRef.current = null;
756
+ repositionPendingRef.current = false;
757
+ pinnedTargetScrollTopRef.current = null;
758
+ appliedTargetKeyRef.current = null;
759
+ observedTargetKeyRef.current = initialObservedTargetKeyRef.current;
760
+ pinActiveRef.current = false;
761
+ setSpacerHeightRef.current(0);
762
+ },
763
+ []
764
+ );
765
+ const isActive = useCallback3(() => pinActiveRef.current, []);
766
+ return { release, isActive };
767
+ }
768
+
547
769
  // src/components/AgentChat.tsx
548
770
  import { BladeApiError, latestPostChatFollowup } from "@blade-hq/agent-client";
549
771
 
@@ -636,6 +858,18 @@ var ArrowUp = createLucideIcon("ArrowUp", [
636
858
  ["path", { d: "M12 19V5", key: "x0mq9r" }]
637
859
  ]);
638
860
 
861
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/book-open.js
862
+ var BookOpen = createLucideIcon("BookOpen", [
863
+ ["path", { d: "M12 7v14", key: "1akyts" }],
864
+ [
865
+ "path",
866
+ {
867
+ 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",
868
+ key: "ruj8y"
869
+ }
870
+ ]
871
+ ]);
872
+
639
873
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/bot.js
640
874
  var Bot = createLucideIcon("Bot", [
641
875
  ["path", { d: "M12 8V4H8", key: "hb8ula" }],
@@ -646,31 +880,6 @@ var Bot = createLucideIcon("Bot", [
646
880
  ["path", { d: "M9 13v2", key: "rq6x2g" }]
647
881
  ]);
648
882
 
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
883
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/check.js
675
884
  var Check = createLucideIcon("Check", [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]);
676
885
 
@@ -697,11 +906,37 @@ var Copy = createLucideIcon("Copy", [
697
906
  ["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
907
  ]);
699
908
 
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" }]
909
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/earth.js
910
+ var Earth = createLucideIcon("Earth", [
911
+ ["path", { d: "M21.54 15H17a2 2 0 0 0-2 2v4.54", key: "1djwo0" }],
912
+ [
913
+ "path",
914
+ {
915
+ 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",
916
+ key: "1tzkfa"
917
+ }
918
+ ],
919
+ ["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" }],
920
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
921
+ ]);
922
+
923
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-pen-line.js
924
+ var FilePenLine = createLucideIcon("FilePenLine", [
925
+ [
926
+ "path",
927
+ {
928
+ 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",
929
+ key: "142zxg"
930
+ }
931
+ ],
932
+ [
933
+ "path",
934
+ {
935
+ 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",
936
+ key: "2t3380"
937
+ }
938
+ ],
939
+ ["path", { d: "M8 18h1", key: "13wk12" }]
705
940
  ]);
706
941
 
707
942
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
@@ -713,24 +948,6 @@ var FileText = createLucideIcon("FileText", [
713
948
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
714
949
  ]);
715
950
 
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
951
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/globe.js
735
952
  var Globe = createLucideIcon("Globe", [
736
953
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
@@ -806,6 +1023,12 @@ var Play = createLucideIcon("Play", [
806
1023
  ["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
807
1024
  ]);
808
1025
 
1026
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/search.js
1027
+ var Search = createLucideIcon("Search", [
1028
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
1029
+ ["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
1030
+ ]);
1031
+
809
1032
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
810
1033
  var Settings2 = createLucideIcon("Settings2", [
811
1034
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -834,6 +1057,12 @@ var Square = createLucideIcon("Square", [
834
1057
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
835
1058
  ]);
836
1059
 
1060
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/terminal.js
1061
+ var Terminal = createLucideIcon("Terminal", [
1062
+ ["polyline", { points: "4 17 10 11 4 5", key: "akl6gq" }],
1063
+ ["line", { x1: "12", x2: "20", y1: "19", y2: "19", key: "q2wloq" }]
1064
+ ]);
1065
+
837
1066
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
838
1067
  var TriangleAlert = createLucideIcon("TriangleAlert", [
839
1068
  [
@@ -847,6 +1076,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
847
1076
  ["path", { d: "M12 17h.01", key: "p32p05" }]
848
1077
  ]);
849
1078
 
1079
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/wrench.js
1080
+ var Wrench = createLucideIcon("Wrench", [
1081
+ [
1082
+ "path",
1083
+ {
1084
+ 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",
1085
+ key: "cbrjhi"
1086
+ }
1087
+ ]
1088
+ ]);
1089
+
850
1090
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
851
1091
  var X = createLucideIcon("X", [
852
1092
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -854,7 +1094,7 @@ var X = createLucideIcon("X", [
854
1094
  ]);
855
1095
 
856
1096
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState12 } from "react";
1097
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo8, useState as useState13 } from "react";
858
1098
 
859
1099
  // src/lib/utils.ts
860
1100
  function cn(...inputs) {
@@ -981,8 +1221,17 @@ function ReplayMismatchPrompt({ mismatch, className }) {
981
1221
  );
982
1222
  }
983
1223
 
1224
+ // src/components/ChatSurface.tsx
1225
+ import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1226
+
984
1227
  // src/components/ChatInput.tsx
985
1228
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1229
+ function isImeCompositionKey(event) {
1230
+ return event.isComposing || event.keyCode === 229;
1231
+ }
1232
+ function shouldSubmitChatInput(event, menuOpen) {
1233
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1234
+ }
986
1235
  function ChatInput({
987
1236
  value,
988
1237
  onValueChange,
@@ -1002,7 +1251,12 @@ function ChatInput({
1002
1251
  onValueChange("");
1003
1252
  };
1004
1253
  const handleKeyDown = (event) => {
1005
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1254
+ if (shouldSubmitChatInput({
1255
+ key: event.key,
1256
+ shiftKey: event.shiftKey,
1257
+ isComposing: event.nativeEvent.isComposing,
1258
+ keyCode: event.nativeEvent.keyCode
1259
+ }, false)) {
1006
1260
  event.preventDefault();
1007
1261
  void handleSend();
1008
1262
  }
@@ -1052,24 +1306,74 @@ function ChatInput({
1052
1306
  }
1053
1307
 
1054
1308
  // src/components/ConnectionBanner.tsx
1309
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1055
1310
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1311
+ var CONNECTION_NOTICE_DELAY_MS = 3e3;
1312
+ var CONNECTION_ERROR_DELAY_MS = 15e3;
1313
+ function useConnectionNoticePhase(connected) {
1314
+ const [phase, setPhase] = useState4("hidden");
1315
+ const connectedRef = useRef4(connected);
1316
+ const timersRef = useRef4([]);
1317
+ connectedRef.current = connected;
1318
+ useEffect4(() => {
1319
+ const clearTimers = () => {
1320
+ for (const timer of timersRef.current) clearTimeout(timer);
1321
+ timersRef.current = [];
1322
+ };
1323
+ const startGracePeriod = () => {
1324
+ clearTimers();
1325
+ setPhase("hidden");
1326
+ timersRef.current = [
1327
+ setTimeout(() => setPhase("recovering"), CONNECTION_NOTICE_DELAY_MS),
1328
+ setTimeout(() => setPhase("failed"), CONNECTION_ERROR_DELAY_MS)
1329
+ ];
1330
+ };
1331
+ if (connected) {
1332
+ clearTimers();
1333
+ setPhase("hidden");
1334
+ } else {
1335
+ startGracePeriod();
1336
+ }
1337
+ const handleForeground = () => {
1338
+ if (!connectedRef.current) startGracePeriod();
1339
+ };
1340
+ const handleVisibilityChange = () => {
1341
+ if (document.visibilityState === "visible") handleForeground();
1342
+ };
1343
+ window.addEventListener("blade:app-active", handleForeground);
1344
+ window.addEventListener("focus", handleForeground);
1345
+ window.addEventListener("pageshow", handleForeground);
1346
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1347
+ return () => {
1348
+ clearTimers();
1349
+ window.removeEventListener("blade:app-active", handleForeground);
1350
+ window.removeEventListener("focus", handleForeground);
1351
+ window.removeEventListener("pageshow", handleForeground);
1352
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1353
+ };
1354
+ }, [connected]);
1355
+ return phase;
1356
+ }
1056
1357
  function ConnectionBanner({ connection, className }) {
1057
- if (connection === "connected" || connection === "connecting") {
1058
- return null;
1059
- }
1060
- const reconnecting = connection === "reconnecting";
1358
+ const hasConnectedRef = useRef4(connection === "connected" || connection === "reconnecting");
1359
+ if (connection === "connected") hasConnectedRef.current = true;
1360
+ const connected = connection === "connected";
1361
+ const phase = useConnectionNoticePhase(connected);
1362
+ if (connected || phase === "hidden") return null;
1363
+ const recovering = phase === "recovering";
1364
+ const firstConnection = !hasConnectedRef.current;
1061
1365
  return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1062
1366
  "div",
1063
1367
  {
1064
1368
  className: cn(
1065
1369
  "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"
1370
+ recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1067
1371
  ),
1068
1372
  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 }) }),
1373
+ /* @__PURE__ */ jsx5("span", { className: "mt-0.5 shrink-0", children: recovering ? /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx5(TriangleAlert, { size: 14 }) }),
1070
1374
  /* @__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" })
1375
+ /* @__PURE__ */ jsx5("div", { className: "text-sm font-medium", children: recovering ? firstConnection ? "\u6B63\u5728\u8FDE\u63A5\u2026" : "\u6B63\u5728\u6062\u590D\u8FDE\u63A5\u2026" : "\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5" }),
1376
+ /* @__PURE__ */ jsx5("div", { className: "text-xs opacity-80", children: recovering ? "\u6062\u590D\u540E\u4F1A\u81EA\u52A8\u540C\u6B65\u6700\u65B0\u6D88\u606F\uFF0C\u8BF7\u7A0D\u5019" : "\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216\u670D\u52A1\u72B6\u6001\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
1073
1377
  ] })
1074
1378
  ]
1075
1379
  }
@@ -1078,10 +1382,10 @@ function ConnectionBanner({ connection, className }) {
1078
1382
 
1079
1383
  // src/components/MessageList.tsx
1080
1384
  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";
1385
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo7, useRef as useRef11, useState as useState12 } from "react";
1082
1386
 
1083
1387
  // ../../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";
1388
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
1085
1389
  var DEFAULT_SPRING_ANIMATION = {
1086
1390
  /**
1087
1391
  * A value from 0 to 1, on how much to damp the animation.
@@ -1118,12 +1422,12 @@ globalThis.document?.addEventListener("click", () => {
1118
1422
  mouseDown = false;
1119
1423
  });
1120
1424
  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);
1425
+ const [escapedFromLock, updateEscapedFromLock] = useState5(false);
1426
+ const [isAtBottom, updateIsAtBottom] = useState5(options.initial !== false);
1427
+ const [isNearBottom, setIsNearBottom] = useState5(false);
1428
+ const optionsRef = useRef5(null);
1125
1429
  optionsRef.current = options;
1126
- const isSelecting = useCallback3(() => {
1430
+ const isSelecting = useCallback4(() => {
1127
1431
  if (!mouseDown) {
1128
1432
  return false;
1129
1433
  }
@@ -1134,11 +1438,11 @@ var useStickToBottom = (options = {}) => {
1134
1438
  const range = selection.getRangeAt(0);
1135
1439
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1136
1440
  }, []);
1137
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1441
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1138
1442
  state.isAtBottom = isAtBottom2;
1139
1443
  updateIsAtBottom(isAtBottom2);
1140
1444
  }, []);
1141
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1445
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1142
1446
  state.escapedFromLock = escapedFromLock2;
1143
1447
  updateEscapedFromLock(escapedFromLock2);
1144
1448
  }, []);
@@ -1195,7 +1499,7 @@ var useStickToBottom = (options = {}) => {
1195
1499
  }
1196
1500
  };
1197
1501
  }, []);
1198
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1502
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1199
1503
  if (typeof scrollOptions === "string") {
1200
1504
  scrollOptions = { animation: scrollOptions };
1201
1505
  }
@@ -1280,11 +1584,11 @@ var useStickToBottom = (options = {}) => {
1280
1584
  }
1281
1585
  return next();
1282
1586
  }, [setIsAtBottom, isSelecting, state]);
1283
- const stopScroll = useCallback3(() => {
1587
+ const stopScroll = useCallback4(() => {
1284
1588
  setEscapedFromLock(true);
1285
1589
  setIsAtBottom(false);
1286
1590
  }, [setEscapedFromLock, setIsAtBottom]);
1287
- const handleScroll = useCallback3(({ target }) => {
1591
+ const handleScroll = useCallback4(({ target }) => {
1288
1592
  if (target !== scrollRef.current) {
1289
1593
  return;
1290
1594
  }
@@ -1323,7 +1627,7 @@ var useStickToBottom = (options = {}) => {
1323
1627
  }
1324
1628
  }, 1);
1325
1629
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1326
- const handleWheel = useCallback3(({ target, deltaY }) => {
1630
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1327
1631
  let element = target;
1328
1632
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1329
1633
  if (!element.parentElement) {
@@ -1393,7 +1697,7 @@ var useStickToBottom = (options = {}) => {
1393
1697
  };
1394
1698
  };
1395
1699
  function useRefCallback(callback, deps) {
1396
- const result = useCallback3((ref) => {
1700
+ const result = useCallback4((ref) => {
1397
1701
  result.current = ref;
1398
1702
  return callback(ref);
1399
1703
  }, deps);
@@ -1425,11 +1729,11 @@ function mergeAnimations(...animations) {
1425
1729
 
1426
1730
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1427
1731
  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";
1732
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef6 } from "react";
1429
1733
  var StickToBottomContext = createContext2(null);
1430
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
1734
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect5;
1431
1735
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1432
- const customTargetScrollTop = useRef4(null);
1736
+ const customTargetScrollTop = useRef6(null);
1433
1737
  const targetScrollTop = React.useCallback((target, elements) => {
1434
1738
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1435
1739
  return get?.(target, elements) ?? target;
@@ -1505,11 +1809,16 @@ function useStickToBottomContext() {
1505
1809
  }
1506
1810
 
1507
1811
  // src/components/AssistantTurnBlock.tsx
1508
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1509
- import { useState as useState9 } from "react";
1812
+ import {
1813
+ getFileParts,
1814
+ getImageParts,
1815
+ getTextContent,
1816
+ normalizeMessageContent
1817
+ } from "@blade-hq/agent-client";
1818
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState10 } from "react";
1510
1819
 
1511
1820
  // src/components/AgentLoopBlock.tsx
1512
- import { useState as useState5 } from "react";
1821
+ import { useState as useState6 } from "react";
1513
1822
 
1514
1823
  // src/components/display-utils.ts
1515
1824
  var TOOL_NAME_ALIASES = {
@@ -1525,7 +1834,9 @@ var TOOL_NAME_ALIASES = {
1525
1834
  finish_task: "FinishTask",
1526
1835
  glob: "Glob",
1527
1836
  grep: "Grep",
1837
+ kb_search: "KbSearch",
1528
1838
  ls: "Ls",
1839
+ multi_edit: "MultiEdit",
1529
1840
  read: "Read",
1530
1841
  read_skill: "ReadSkill",
1531
1842
  web_fetch: "WebFetch",
@@ -1538,9 +1849,11 @@ var TOOL_DISPLAY_LABELS = {
1538
1849
  Read: "\u8BFB\u53D6\u6587\u4EF6",
1539
1850
  Write: "\u5199\u5165\u6587\u4EF6",
1540
1851
  Edit: "\u7F16\u8F91\u6587\u4EF6",
1852
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1541
1853
  Ls: "\u5217\u51FA\u76EE\u5F55",
1542
1854
  Glob: "\u5339\u914D\u6587\u4EF6",
1543
1855
  Grep: "\u641C\u7D22\u6587\u672C",
1856
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1544
1857
  WebSearch: "\u641C\u7D22\u7F51\u9875",
1545
1858
  WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1546
1859
  Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
@@ -1563,6 +1876,17 @@ function getStringArgValue(args, key) {
1563
1876
  const value = args?.[key];
1564
1877
  return typeof value === "string" ? value.trim() : "";
1565
1878
  }
1879
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1880
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1881
+ function getSkillNameFromFilePath(filePath) {
1882
+ if (!filePath) return null;
1883
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1884
+ const fileName = segments.pop();
1885
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1886
+ const dirName = segments.pop();
1887
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1888
+ return dirName;
1889
+ }
1566
1890
  function formatToolName(name) {
1567
1891
  const trimmed = name.trim();
1568
1892
  if (!trimmed) return name;
@@ -1587,6 +1911,12 @@ function getToolDisplayLabel(toolCall) {
1587
1911
  const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1588
1912
  return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1589
1913
  }
1914
+ if (normalized === "Read") {
1915
+ const skillName = getSkillNameFromFilePath(
1916
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1917
+ );
1918
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1919
+ }
1590
1920
  if (normalized === "FinishTask") {
1591
1921
  const title = getStringArgValue(args, "title");
1592
1922
  return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
@@ -1641,83 +1971,68 @@ function parseAgentDescription(argumentsJson) {
1641
1971
  }
1642
1972
  }
1643
1973
  function AgentLoopBlock({ toolCall }) {
1644
- const [expanded, setExpanded] = useState5(false);
1974
+ const [expanded, setExpanded] = useState6(false);
1645
1975
  const description = parseAgentDescription(toolCall.arguments);
1646
1976
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1647
1977
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1648
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
1978
+ const hasResult = toolCall.result != null;
1979
+ const iconClass = cn(
1980
+ "size-3.5 shrink-0",
1981
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1982
+ );
1983
+ return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
1649
1984
  /* @__PURE__ */ jsxs5(
1650
- "div",
1985
+ "button",
1651
1986
  {
1987
+ type: "button",
1988
+ onClick: () => hasResult && setExpanded(!expanded),
1989
+ disabled: !hasResult,
1990
+ "aria-expanded": hasResult ? expanded : void 0,
1991
+ "data-testid": "execution-tool-intent",
1652
1992
  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))]"
1993
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
1994
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
1995
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1655
1996
  ),
1997
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1656
1998
  children: [
1657
- /* @__PURE__ */ jsxs5(
1658
- "button",
1999
+ running ? /* @__PURE__ */ jsx6(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx6(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx6(X, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx6(Bot, { className: iconClass, "aria-hidden": "true" }),
2000
+ /* @__PURE__ */ jsxs5("span", { className: "min-w-0 truncate", children: [
2001
+ "\u5B50\u4EFB\u52A1\uFF1A",
2002
+ description
2003
+ ] }),
2004
+ hasResult ? /* @__PURE__ */ jsx6(
2005
+ ChevronRight,
1659
2006
  {
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
- ]
2007
+ size: 14,
2008
+ className: cn(
2009
+ "shrink-0 transition-transform duration-300",
2010
+ expanded && "rotate-90"
2011
+ ),
2012
+ "aria-hidden": "true"
1694
2013
  }
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) })
2014
+ ) : null
1697
2015
  ]
1698
2016
  }
1699
2017
  ),
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
- ] })
2018
+ expanded && hasResult ? /* @__PURE__ */ jsx6("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
2019
  ] });
1705
2020
  }
1706
2021
 
1707
2022
  // src/components/MarkdownContent.tsx
1708
2023
  import {
1709
- useEffect as useEffect4,
2024
+ useEffect as useEffect6,
1710
2025
  useMemo as useMemo5,
1711
- useRef as useRef5,
1712
- useState as useState6
2026
+ useRef as useRef7,
2027
+ useState as useState7
1713
2028
  } from "react";
1714
2029
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1715
2030
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
1716
2031
  function CodeBlockPre({ children, node: _node, ...props }) {
1717
- const preRef = useRef5(null);
1718
- const [copied, setCopied] = useState6(false);
1719
- const [language, setLanguage] = useState6("");
1720
- useEffect4(() => {
2032
+ const preRef = useRef7(null);
2033
+ const [copied, setCopied] = useState7(false);
2034
+ const [language, setLanguage] = useState7("");
2035
+ useEffect6(() => {
1721
2036
  const codeEl = preRef.current?.querySelector("code");
1722
2037
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1723
2038
  }, []);
@@ -1788,11 +2103,40 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1788
2103
  }
1789
2104
 
1790
2105
  // src/components/ToolCallBlock.tsx
1791
- import { useState as useState8 } from "react";
2106
+ import { useState as useState9 } from "react";
1792
2107
 
1793
2108
  // src/components/AskUserQuestionBlock.tsx
1794
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState7 } from "react";
2109
+ import { useEffect as useEffect7, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1795
2110
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2111
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2112
+ function resizeCustomTextarea(textarea) {
2113
+ textarea.style.height = "auto";
2114
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2115
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2116
+ }
2117
+ function useAutoResizeTextarea(value) {
2118
+ const textareaRef = useRef8(null);
2119
+ useEffect7(() => {
2120
+ const textarea = textareaRef.current;
2121
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2122
+ }, [value]);
2123
+ useEffect7(() => {
2124
+ const textarea = textareaRef.current;
2125
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2126
+ let previousWidth = textarea.clientWidth;
2127
+ const observer = new ResizeObserver(([entry]) => {
2128
+ if (!entry || entry.contentRect.width === previousWidth) return;
2129
+ previousWidth = entry.contentRect.width;
2130
+ resizeCustomTextarea(textarea);
2131
+ });
2132
+ observer.observe(textarea);
2133
+ return () => observer.disconnect();
2134
+ }, []);
2135
+ return textareaRef;
2136
+ }
2137
+ function indentAnswerContinuationLines(answer) {
2138
+ return answer.replaceAll("\n", "\n ");
2139
+ }
1796
2140
  function AskUserQuestionBlock({
1797
2141
  data,
1798
2142
  answered,
@@ -1801,18 +2145,19 @@ function AskUserQuestionBlock({
1801
2145
  answerData,
1802
2146
  onAnswer
1803
2147
  }) {
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(() => {
2148
+ const [selections, setSelections] = useState8(/* @__PURE__ */ new Map());
2149
+ const [customTexts, setCustomTexts] = useState8(/* @__PURE__ */ new Map());
2150
+ const [usingCustom, setUsingCustom] = useState8(/* @__PURE__ */ new Set());
2151
+ const [note, setNote] = useState8("");
2152
+ const [submitted, setSubmitted] = useState8(false);
2153
+ useEffect7(() => {
1809
2154
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1810
2155
  setSubmitted(false);
1811
2156
  }
1812
2157
  }, [sessionStatus]);
1813
2158
  const displayAnswerState = useMemo6(() => {
1814
2159
  if (!(answered && answerData)) {
1815
- return { selections, customTexts, usingCustom };
2160
+ return { selections, customTexts, usingCustom, note };
1816
2161
  }
1817
2162
  const nextSelections = /* @__PURE__ */ new Map();
1818
2163
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1828,9 +2173,10 @@ function AskUserQuestionBlock({
1828
2173
  return {
1829
2174
  selections: nextSelections,
1830
2175
  customTexts: nextCustomTexts,
1831
- usingCustom: nextUsingCustom
2176
+ usingCustom: nextUsingCustom,
2177
+ note: answerData.note ?? ""
1832
2178
  };
1833
- }, [answerData, answered, customTexts, selections, usingCustom]);
2179
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1834
2180
  const toggleOption = (qIdx, optIdx, multi) => {
1835
2181
  if (answered || submitted) return;
1836
2182
  setSelections((prev) => {
@@ -1878,6 +2224,7 @@ function AskUserQuestionBlock({
1878
2224
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1879
2225
  const handleSubmit = () => {
1880
2226
  if (answered || submitted || !allAnswered || !onAnswer) return;
2227
+ const trimmedNote = note.trim();
1881
2228
  const nextAnswerData = {
1882
2229
  selections: Object.fromEntries(
1883
2230
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1887,11 +2234,17 @@ function AskUserQuestionBlock({
1887
2234
  ),
1888
2235
  custom: Object.fromEntries(
1889
2236
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1890
- )
2237
+ ),
2238
+ ...trimmedNote ? { note: trimmedNote } : {}
1891
2239
  };
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")}`;
2240
+ const parts = data.questions.map(
2241
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2242
+ );
2243
+ const text = [
2244
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2245
+ ${parts.join("\n")}`,
2246
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2247
+ ].filter(Boolean).join("\n");
1895
2248
  setSubmitted(true);
1896
2249
  onAnswer(text, toolCallId, nextAnswerData);
1897
2250
  };
@@ -1923,6 +2276,15 @@ ${parts.join("\n")}`;
1923
2276
  },
1924
2277
  q.question
1925
2278
  )),
2279
+ /* @__PURE__ */ jsx9(
2280
+ NoteField,
2281
+ {
2282
+ answered,
2283
+ submitted,
2284
+ note: displayAnswerState.note,
2285
+ onChange: setNote
2286
+ }
2287
+ ),
1926
2288
  !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
1927
2289
  "button",
1928
2290
  {
@@ -1961,6 +2323,7 @@ function QuestionCard({
1961
2323
  onCustomChange
1962
2324
  }) {
1963
2325
  const multi = question.multiSelect ?? false;
2326
+ const customTextareaRef = useAutoResizeTextarea(customText);
1964
2327
  return /* @__PURE__ */ jsxs7("div", { children: [
1965
2328
  /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
1966
2329
  /* @__PURE__ */ jsx9(
@@ -2037,25 +2400,26 @@ function QuestionCard({
2037
2400
  "div",
2038
2401
  {
2039
2402
  className: cn(
2040
- "flex items-center gap-2 rounded-lg border transition-all",
2403
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2041
2404
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2042
2405
  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
2406
  answered && "cursor-default opacity-70"
2044
2407
  ),
2045
2408
  children: [
2046
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2409
+ /* @__PURE__ */ jsx9("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2047
2410
  /* @__PURE__ */ jsx9(
2048
- "input",
2411
+ "textarea",
2049
2412
  {
2050
- type: "text",
2413
+ ref: customTextareaRef,
2414
+ rows: 2,
2051
2415
  value: customText,
2052
- disabled: answered,
2416
+ readOnly: answered,
2053
2417
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2054
2418
  onFocus: () => onCustomFocus(qIdx),
2055
2419
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2056
2420
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2057
2421
  className: cn(
2058
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2422
+ "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
2423
  answered ? "text-xs" : "text-sm"
2060
2424
  )
2061
2425
  }
@@ -2066,6 +2430,49 @@ function QuestionCard({
2066
2430
  ] })
2067
2431
  ] });
2068
2432
  }
2433
+ function NoteField({
2434
+ answered,
2435
+ submitted,
2436
+ note,
2437
+ onChange
2438
+ }) {
2439
+ const textareaRef = useAutoResizeTextarea(note);
2440
+ const readOnly = answered || submitted;
2441
+ if (answered && !note.trim()) return null;
2442
+ return /* @__PURE__ */ jsxs7(
2443
+ "label",
2444
+ {
2445
+ className: cn(
2446
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2447
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2448
+ 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))]",
2449
+ readOnly && "cursor-default opacity-70"
2450
+ ),
2451
+ children: [
2452
+ /* @__PURE__ */ jsx9("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2453
+ /* @__PURE__ */ jsx9(
2454
+ "textarea",
2455
+ {
2456
+ ref: textareaRef,
2457
+ rows: 2,
2458
+ value: note,
2459
+ readOnly,
2460
+ onChange: (event) => {
2461
+ if (readOnly) return;
2462
+ onChange(event.target.value);
2463
+ },
2464
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2465
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2466
+ className: cn(
2467
+ "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)]",
2468
+ answered ? "text-xs" : "text-sm"
2469
+ )
2470
+ }
2471
+ )
2472
+ ]
2473
+ }
2474
+ );
2475
+ }
2069
2476
  function parseAskUserQuestion(toolResult) {
2070
2477
  if (!toolResult) return null;
2071
2478
  try {
@@ -2088,6 +2495,26 @@ function parseAskUserQuestion(toolResult) {
2088
2495
  }
2089
2496
  return null;
2090
2497
  }
2498
+ function parseAskUserQuestionError(toolResult) {
2499
+ if (!toolResult) return null;
2500
+ try {
2501
+ const parsed = JSON.parse(toolResult);
2502
+ let detail = null;
2503
+ if (typeof parsed.error === "string") detail = parsed.error;
2504
+ if (parsed.error && typeof parsed.error === "object") {
2505
+ const message = parsed.error.message;
2506
+ if (typeof message === "string") detail = message;
2507
+ }
2508
+ if (!detail && typeof parsed.message === "string") detail = parsed.message;
2509
+ if (!detail) return null;
2510
+ return {
2511
+ 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",
2512
+ detail
2513
+ };
2514
+ } catch {
2515
+ return null;
2516
+ }
2517
+ }
2091
2518
  function normalizeQuestionItem(value) {
2092
2519
  if (!value || typeof value !== "object") return null;
2093
2520
  const item = value;
@@ -2116,9 +2543,10 @@ import { Fragment, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2116
2543
  function resolveAskQuestionState({
2117
2544
  toolStatus,
2118
2545
  hasAnswerData,
2119
- fallbackAnswered
2546
+ fallbackAnswered,
2547
+ fallbackAwaiting
2120
2548
  }) {
2121
- const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
2549
+ const awaitingAnswer = !hasAnswerData && (toolStatus === "awaiting_answer" || toolStatus === "pending" && fallbackAwaiting === true);
2122
2550
  return {
2123
2551
  awaitingAnswer,
2124
2552
  answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
@@ -2130,9 +2558,10 @@ function ToolCallBlock({
2130
2558
  answered,
2131
2559
  answerData,
2132
2560
  sessionStatus,
2561
+ isActiveQuestion,
2133
2562
  renderer
2134
2563
  }) {
2135
- const [expanded, setExpanded] = useState8(false);
2564
+ const [expanded, setExpanded] = useState9(false);
2136
2565
  const normalizedName = formatToolName(toolCall.name);
2137
2566
  if (renderer) {
2138
2567
  const custom = renderer(toolCall);
@@ -2145,7 +2574,8 @@ function ToolCallBlock({
2145
2574
  const questionState = resolveAskQuestionState({
2146
2575
  toolStatus: toolCall.status,
2147
2576
  hasAnswerData: Boolean(answerData),
2148
- fallbackAnswered: answered
2577
+ fallbackAnswered: answered,
2578
+ fallbackAwaiting: isActiveQuestion === true && (sessionStatus === "paused" || sessionStatus === "waiting_for_input")
2149
2579
  });
2150
2580
  const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
2151
2581
  if (askData) {
@@ -2167,9 +2597,16 @@ function ToolCallBlock({
2167
2597
  /* @__PURE__ */ jsx10("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2168
2598
  ] });
2169
2599
  }
2600
+ const errorDetail = parseAskUserQuestionError(
2601
+ typeof toolCall.result === "string" ? toolCall.result : null
2602
+ );
2170
2603
  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
2604
  /* @__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" })
2605
+ /* @__PURE__ */ jsx10("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" }),
2606
+ errorDetail?.detail ? /* @__PURE__ */ jsxs8("details", { className: "mt-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2607
+ /* @__PURE__ */ jsx10("summary", { className: "cursor-pointer", children: "\u67E5\u770B\u5177\u4F53\u539F\u56E0" }),
2608
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-words font-mono", children: errorDetail.detail })
2609
+ ] }) : null
2173
2610
  ] });
2174
2611
  }
2175
2612
  const tone = getToolTone(toolCall.status);
@@ -2234,45 +2671,233 @@ function buildAskUserPayload(argumentsJson) {
2234
2671
  // src/components/AssistantTurnBlock.tsx
2235
2672
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2236
2673
  function ThinkingBlock({ reasoning, isStreaming }) {
2237
- const [open, setOpen] = useState9(false);
2238
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2674
+ const [open, setOpen] = useState10(false);
2675
+ if (!isStreaming) return null;
2676
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking text-xs", children: [
2239
2677
  /* @__PURE__ */ jsxs9(
2240
2678
  "button",
2241
2679
  {
2242
2680
  type: "button",
2243
2681
  onClick: () => setOpen(!open),
2244
2682
  "aria-expanded": open,
2245
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2683
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2246
2684
  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
- ] }),
2685
+ /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }),
2254
2686
  /* @__PURE__ */ jsx11(
2255
- ChevronDown,
2687
+ ChevronRight,
2256
2688
  {
2257
- size: 12,
2258
- className: cn("shrink-0 transition-transform", open && "rotate-180")
2689
+ size: 14,
2690
+ className: cn(
2691
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
2692
+ open && "rotate-90 opacity-100"
2693
+ )
2259
2694
  }
2260
2695
  )
2261
2696
  ]
2262
2697
  }
2263
2698
  ),
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 })
2699
+ open ? /* @__PURE__ */ jsx11("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning }) : null
2265
2700
  ] });
2266
2701
  }
2267
2702
  function getMessageText(message) {
2268
2703
  return getTextContent(normalizeMessageContent(message.content)).trim();
2269
2704
  }
2705
+ function hasRenderableMessageContent(message) {
2706
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
2707
+ }
2708
+ function getLastContentMessage(messages) {
2709
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
2710
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
2711
+ }
2712
+ return null;
2713
+ }
2714
+ function getOrderedMessageParts(message, toolCalls) {
2715
+ const blocks = message.blocks ?? [];
2716
+ if (!blocks.some((block) => block.type === "text") || !blocks.some((block) => block.type === "tool_use")) return [];
2717
+ const toolsById = new Map(toolCalls.map((toolCall) => [toolCall.id, toolCall]));
2718
+ const seenToolIds = /* @__PURE__ */ new Set();
2719
+ const parts = [];
2720
+ for (const [index, block] of blocks.entries()) {
2721
+ if (block.type === "text" && block.content != null && block.content !== "") {
2722
+ const content = Array.isArray(block.content) ? block.content : String(block.content);
2723
+ parts.push({ type: "text", key: `text-${index}`, content });
2724
+ }
2725
+ if (block.type !== "tool_use" || !block.tool_call_id) continue;
2726
+ const toolCall = toolsById.get(block.tool_call_id);
2727
+ if (!toolCall) continue;
2728
+ seenToolIds.add(toolCall.id);
2729
+ const previous = parts[parts.length - 1];
2730
+ if (previous?.type === "tools") previous.toolCalls.push(toolCall);
2731
+ else parts.push({ type: "tools", key: `tools-${index}`, toolCalls: [toolCall] });
2732
+ }
2733
+ const missingTools = toolCalls.filter((toolCall) => !seenToolIds.has(toolCall.id));
2734
+ if (seenToolIds.size === 0) return [];
2735
+ if (missingTools.length > 0) {
2736
+ parts.push({ type: "tools", key: "tools-missing", toolCalls: missingTools });
2737
+ }
2738
+ return parts;
2739
+ }
2270
2740
  function findLatestReasoningMessageIndex(messages) {
2271
2741
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2272
2742
  if (messages[index].reasoning) return index;
2273
2743
  }
2274
2744
  return -1;
2275
2745
  }
2746
+ function resolveTurnDisplayMode({
2747
+ isStreaming: _isStreaming,
2748
+ displayMode
2749
+ }) {
2750
+ return displayMode;
2751
+ }
2752
+ function formatExecutionDuration(durationMs) {
2753
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
2754
+ const minutes = Math.floor(totalSeconds / 60);
2755
+ const seconds = totalSeconds % 60;
2756
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
2757
+ }
2758
+ function getExecutionDurationMs({
2759
+ messages,
2760
+ isStreaming,
2761
+ now = Date.now()
2762
+ }) {
2763
+ const knownDuration = messages.reduce(
2764
+ (total, message) => {
2765
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
2766
+ return total + message.duration_ms;
2767
+ }
2768
+ return total + (message.tool_calls ?? []).reduce(
2769
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
2770
+ 0
2771
+ );
2772
+ },
2773
+ 0
2774
+ );
2775
+ if (!isStreaming) return knownDuration;
2776
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
2777
+ if (startedAt === void 0) return knownDuration;
2778
+ return Math.max(knownDuration, now - startedAt);
2779
+ }
2780
+ function findLastExceptionalEvent(messages) {
2781
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
2782
+ const messageStatus = messages[messageIndex].status;
2783
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
2784
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
2785
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
2786
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
2787
+ const status = toolCalls[toolIndex].status;
2788
+ if (status === "error" || status === "cancelled") {
2789
+ return { messageIndex, status };
2790
+ }
2791
+ }
2792
+ }
2793
+ return null;
2794
+ }
2795
+ function executionSummaryLabel({
2796
+ messages,
2797
+ isStreaming,
2798
+ durationMs,
2799
+ sessionStatus,
2800
+ askAnswers
2801
+ }) {
2802
+ if (isStreaming) {
2803
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
2804
+ }
2805
+ if (sessionStatus === "waiting_for_input" && messages.some(
2806
+ (message) => (message.tool_calls ?? []).some(
2807
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
2808
+ )
2809
+ )) {
2810
+ return "\u7B49\u5F85\u8F93\u5165";
2811
+ }
2812
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
2813
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
2814
+ if (lastExceptionalEvent) {
2815
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
2816
+ if (lastExceptionalEvent.status === "error") {
2817
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
2818
+ }
2819
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
2820
+ }
2821
+ return completedLabel;
2822
+ }
2823
+ function businessToolDisplayName(toolCall) {
2824
+ const displayName = toolCall.display_name?.trim() ?? "";
2825
+ if (!displayName) return "";
2826
+ const rawName = toolCall.name.trim();
2827
+ return displayName !== rawName && formatToolName(displayName) !== formatToolName(rawName) ? displayName : "";
2828
+ }
2829
+ function executionToolTypeLabel(toolCall) {
2830
+ switch (formatToolName(toolCall.name)) {
2831
+ case "WebSearch":
2832
+ case "WebFetch":
2833
+ return "\u7F51\u7EDC\u68C0\u7D22";
2834
+ case "Bash":
2835
+ case "BgBash":
2836
+ return "\u547D\u4EE4\u6267\u884C";
2837
+ case "Read":
2838
+ case "ReadSkill":
2839
+ return "\u5185\u5BB9\u8BFB\u53D6";
2840
+ case "Write":
2841
+ case "Edit":
2842
+ case "MultiEdit":
2843
+ return "\u6587\u4EF6\u5904\u7406";
2844
+ case "Grep":
2845
+ case "Glob":
2846
+ return "\u5185\u5BB9\u641C\u7D22";
2847
+ case "Agent":
2848
+ return "\u5B50\u4EFB\u52A1";
2849
+ case "search_skills":
2850
+ return "\u6280\u80FD\u68C0\u7D22";
2851
+ case "get_skill_content":
2852
+ return "\u8BFB\u53D6\u6280\u80FD";
2853
+ case "run_skill_tool":
2854
+ return "\u6267\u884C\u6280\u80FD";
2855
+ default:
2856
+ return businessToolDisplayName(toolCall) || "\u6267\u884C\u6B65\u9AA4";
2857
+ }
2858
+ }
2859
+ function executionToolIntent(toolCall) {
2860
+ const normalizedName = formatToolName(toolCall.name);
2861
+ let args = null;
2862
+ try {
2863
+ const parsed = JSON.parse(toolCall.arguments);
2864
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2865
+ } catch {
2866
+ args = null;
2867
+ }
2868
+ const getString = (key) => {
2869
+ const value = args?.[key];
2870
+ return typeof value === "string" ? value.trim() : "";
2871
+ };
2872
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || "";
2873
+ if (explicitIntent) return explicitIntent;
2874
+ if (normalizedName === "search_skills") return getString("query");
2875
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
2876
+ return getString("skill_name") || getString("skill");
2877
+ }
2878
+ if (normalizedName === "FinishTask") return getString("title");
2879
+ return "";
2880
+ }
2881
+ function ExecutionToolRow({ toolCall }) {
2882
+ const normalizedName = formatToolName(toolCall.name);
2883
+ const typeLabel = executionToolTypeLabel(toolCall);
2884
+ const intent = executionToolIntent(toolCall);
2885
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
2886
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
2887
+ const iconClass = cn(
2888
+ "size-3.5 shrink-0",
2889
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2890
+ );
2891
+ const icon = toolCall.status === "pending" ? /* @__PURE__ */ jsx11(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx11(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx11(X, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "WebSearch" || normalizedName === "WebFetch" ? /* @__PURE__ */ jsx11(Earth, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Bash" || normalizedName === "BgBash" ? /* @__PURE__ */ jsx11(Terminal, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit" ? /* @__PURE__ */ jsx11(FilePenLine, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Read" || normalizedName === "ReadSkill" || normalizedName === "get_skill_content" ? /* @__PURE__ */ jsx11(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Grep" || normalizedName === "Glob" || normalizedName === "search_skills" ? /* @__PURE__ */ jsx11(Search, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Agent" ? /* @__PURE__ */ jsx11(Bot, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Wrench, { className: iconClass, "aria-hidden": "true" });
2892
+ const rowClassName = cn(
2893
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
2894
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2895
+ );
2896
+ return /* @__PURE__ */ jsxs9("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
2897
+ icon,
2898
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label })
2899
+ ] });
2900
+ }
2276
2901
  function AssistantTurnBlock({
2277
2902
  messages,
2278
2903
  isStreaming = false,
@@ -2283,53 +2908,302 @@ function AssistantTurnBlock({
2283
2908
  sessionId
2284
2909
  }) {
2285
2910
  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
2911
+ const hasFailedWithoutContent = messages.some(
2912
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
2913
+ );
2914
+ const finalMessage = getLastContentMessage(messages);
2915
+ const turnToolCalls = messages.flatMap((message) => message.tool_calls ?? []);
2916
+ const finalOrderedParts = finalMessage ? getOrderedMessageParts(
2917
+ finalMessage,
2918
+ (finalMessage.tool_calls ?? []).filter(
2919
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
2920
+ )
2921
+ ) : [];
2922
+ const hasExecutionProcess = messages.some(
2923
+ (message) => message.reasoning || (message.tool_calls?.length ?? 0) > 0
2288
2924
  );
2289
2925
  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",
2300
- {
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,
2926
+ const hasActionableToolCall = messages.some(
2927
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
2928
+ (toolCall) => toolCall.status === "error" || toolCall.status === "cancelled"
2929
+ )
2930
+ );
2931
+ const questionToolCalls = messages.flatMap(
2932
+ (message) => (message.tool_calls ?? []).filter(
2933
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
2934
+ )
2935
+ );
2936
+ const activeQuestionId = questionToolCalls.filter((toolCall) => toolCall.status === "pending").at(-1)?.id;
2937
+ const [displayMode, setDisplayMode] = useState10(
2938
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
2939
+ );
2940
+ const userSelectedDisplayModeRef = useRef9(false);
2941
+ const wasStreamingRef = useRef9(isStreaming);
2942
+ useEffect8(() => {
2943
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
2944
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
2945
+ }
2946
+ wasStreamingRef.current = isStreaming;
2947
+ }, [hasActionableToolCall, isStreaming]);
2948
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
2949
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
2950
+ const [clock, setClock] = useState10(() => Date.now());
2951
+ const hasLiveStartTime = messages.some(
2952
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
2953
+ );
2954
+ useEffect8(() => {
2955
+ if (!isStreaming || !hasLiveStartTime) return;
2956
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
2957
+ return () => window.clearInterval(timer);
2958
+ }, [hasLiveStartTime, isStreaming]);
2959
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
2960
+ const memoryRefs = collectMemoryRefs(messages);
2961
+ if (!hasExecutionProcess) {
2962
+ return /* @__PURE__ */ jsxs9(
2963
+ "div",
2964
+ {
2965
+ "aria-busy": isStreaming || void 0,
2966
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
2967
+ children: [
2968
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx11(MemoryRefsHint, { refs: memoryRefs }) : null,
2969
+ 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" }),
2970
+ hasFailedWithoutContent && /* @__PURE__ */ jsx11("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" }),
2971
+ messages.map((message, index) => {
2972
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx11(
2973
+ "div",
2306
2974
  {
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
2975
+ className: "flex flex-col gap-3",
2976
+ children: /* @__PURE__ */ jsx11(
2977
+ AssistantMessageContent,
2978
+ {
2979
+ message,
2980
+ sessionId,
2981
+ streaming: isStreaming && index === messages.length - 1
2982
+ }
2983
+ )
2984
+ },
2985
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2986
+ ) : null;
2987
+ })
2988
+ ]
2989
+ }
2990
+ );
2991
+ }
2992
+ return /* @__PURE__ */ jsxs9(
2993
+ "div",
2994
+ {
2995
+ "aria-busy": isStreaming || void 0,
2996
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
2997
+ children: [
2998
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx11(MemoryRefsHint, { refs: memoryRefs }) : null,
2999
+ 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" }),
3000
+ hasFailedWithoutContent && /* @__PURE__ */ jsx11("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" }),
3001
+ /* @__PURE__ */ jsxs9("div", { className: "flex w-full items-start gap-2.5", children: [
3002
+ /* @__PURE__ */ jsx11(
3003
+ "span",
3004
+ {
3005
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3006
+ "aria-hidden": "true",
3007
+ children: /* @__PURE__ */ jsx11(Bot, { size: 16 })
3008
+ }
3009
+ ),
3010
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3011
+ /* @__PURE__ */ jsxs9(
3012
+ "button",
3013
+ {
3014
+ type: "button",
3015
+ onClick: () => {
3016
+ userSelectedDisplayModeRef.current = true;
3017
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
2323
3018
  },
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..." })
3019
+ "aria-expanded": effectiveMode === "detail",
3020
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3021
+ "data-testid": "assistant-execution-summary",
3022
+ 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",
3023
+ children: [
3024
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3025
+ messages,
3026
+ isStreaming,
3027
+ durationMs: liveExecutionDurationMs,
3028
+ sessionStatus,
3029
+ askAnswers
3030
+ }) }),
3031
+ /* @__PURE__ */ jsx11(
3032
+ ChevronRight,
3033
+ {
3034
+ size: 14,
3035
+ className: cn(
3036
+ "shrink-0 transition-transform duration-300",
3037
+ effectiveMode === "detail" && "rotate-90"
3038
+ ),
3039
+ "aria-hidden": "true"
3040
+ }
3041
+ )
3042
+ ]
3043
+ }
3044
+ ),
3045
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3046
+ ] })
3047
+ ] }),
3048
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx11("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
3049
+ const isLast = index === messages.length - 1;
3050
+ const streamingThis = isStreaming && isLast;
3051
+ const text = getMessageText(message);
3052
+ const toolCalls = (message.tool_calls ?? []).filter(
3053
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
3054
+ );
3055
+ const orderedParts = getOrderedMessageParts(message, toolCalls);
3056
+ const showReasoning = !!message.reasoning && isStreaming && index === latestReasoningIndex;
3057
+ return /* @__PURE__ */ jsxs9(
3058
+ "div",
3059
+ {
3060
+ className: "flex flex-col gap-3",
3061
+ children: [
3062
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3063
+ orderedParts.length > 0 ? orderedParts.map(
3064
+ (part) => part.type === "text" ? /* @__PURE__ */ jsx11(
3065
+ AssistantMessageContent,
3066
+ {
3067
+ message: { ...message, content: part.content, tool_calls: turnToolCalls },
3068
+ sessionId,
3069
+ streaming: streamingThis,
3070
+ compact: true
3071
+ },
3072
+ part.key
3073
+ ) : /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: part.toolCalls.map((toolCall) => {
3074
+ const custom = toolCallRenderer?.(toolCall);
3075
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx11("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(ExecutionToolRow, { toolCall }, toolCall.id);
3076
+ }) }, part.key)
3077
+ ) : hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx11(
3078
+ AssistantMessageContent,
3079
+ {
3080
+ message,
3081
+ sessionId,
3082
+ streaming: streamingThis,
3083
+ compact: true
3084
+ }
3085
+ ) : null,
3086
+ orderedParts.length === 0 && toolCalls.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3087
+ const custom = toolCallRenderer?.(toolCall);
3088
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx11("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(ExecutionToolRow, { toolCall }, toolCall.id);
3089
+ }) }) : null
3090
+ ]
3091
+ },
3092
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3093
+ );
3094
+ }) }) : null,
3095
+ finalMessage && (effectiveMode === "compact" || finalOrderedParts.length === 0) ? /* @__PURE__ */ jsx11("div", { className: "ml-10", children: /* @__PURE__ */ jsx11(
3096
+ AssistantMessageContent,
3097
+ {
3098
+ message: finalMessage,
3099
+ sessionId,
3100
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3101
+ }
3102
+ ) }) : null,
3103
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx11(
3104
+ ToolCallBlock,
3105
+ {
3106
+ toolCall,
3107
+ answerData: askAnswers?.[toolCall.id],
3108
+ onAnswer,
3109
+ answered: sessionStatus !== "waiting_for_input",
3110
+ sessionStatus,
3111
+ isActiveQuestion: toolCall.id === activeQuestionId,
3112
+ renderer: toolCallRenderer
3113
+ },
3114
+ toolCall.id
3115
+ ))
3116
+ ]
3117
+ }
3118
+ );
3119
+ }
3120
+ function collectMemoryRefs(messages) {
3121
+ const refs = /* @__PURE__ */ new Map();
3122
+ for (const message of messages) {
3123
+ for (const ref of message.memory_refs ?? []) if (!refs.has(ref.id)) refs.set(ref.id, ref);
3124
+ }
3125
+ return [...refs.values()];
3126
+ }
3127
+ function MemoryRefsHint({ refs }) {
3128
+ const [expanded, setExpanded] = useState10(false);
3129
+ 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";
3130
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-memory-refs ml-1 w-full max-w-[680px]", children: [
3131
+ /* @__PURE__ */ jsxs9("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: [
3132
+ /* @__PURE__ */ jsx11(BookOpen, { size: 12 }),
3133
+ /* @__PURE__ */ jsxs9("span", { children: [
3134
+ label,
3135
+ "\uFF08",
3136
+ refs.length,
3137
+ "\uFF09"
3138
+ ] }),
3139
+ /* @__PURE__ */ jsx11(ChevronRight, { size: 10, className: cn("transition-transform", expanded && "rotate-90") })
3140
+ ] }),
3141
+ expanded ? /* @__PURE__ */ jsx11("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__ */ jsxs9("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: [
3142
+ /* @__PURE__ */ jsx11("p", { className: "line-clamp-2 break-words leading-5", children: ref.content_preview }),
3143
+ ref.skill_name ? /* @__PURE__ */ jsx11("span", { className: "mt-1 inline-flex text-[10px] text-[hsl(var(--primary))]", children: ref.skill_name }) : null
3144
+ ] }, ref.id)) }) : null
3145
+ ] });
3146
+ }
3147
+ function AssistantMessageContent({
3148
+ message,
3149
+ sessionId,
3150
+ streaming,
3151
+ compact = false
3152
+ }) {
3153
+ const text = getMessageText(message);
3154
+ const imageParts = getImageParts(message.content);
3155
+ const fileParts = getFileParts(message.content);
3156
+ const failed = message.status === "failed";
3157
+ const failedBadge = failed ? /* @__PURE__ */ jsx11("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;
3158
+ const textContent = text ? /* @__PURE__ */ jsx11(
3159
+ "div",
3160
+ {
3161
+ className: cn(
3162
+ "blade-chat-assistant-text",
3163
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3164
+ ),
3165
+ children: /* @__PURE__ */ jsx11(
3166
+ MarkdownContent,
3167
+ {
3168
+ mode: streaming ? "streaming" : "static",
3169
+ className: "blade-chat-prose",
3170
+ sessionId,
3171
+ children: text
3172
+ }
3173
+ )
3174
+ }
3175
+ ) : null;
3176
+ if (imageParts.length === 0 && fileParts.length === 0) {
3177
+ if (!failed) return textContent;
3178
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
3179
+ failedBadge,
3180
+ textContent
3181
+ ] }) : null;
3182
+ }
3183
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3", children: [
3184
+ failedBadge,
3185
+ imageParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx11(
3186
+ "img",
3187
+ {
3188
+ src: part.image_url.url,
3189
+ alt: "\u6D88\u606F\u9644\u4EF6",
3190
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3191
+ },
3192
+ part.image_url.url
3193
+ )) }) : null,
3194
+ fileParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs9(
3195
+ "div",
3196
+ {
3197
+ 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))]",
3198
+ title: part.name,
3199
+ children: [
3200
+ /* @__PURE__ */ jsx11(FileText, { size: 12, className: "shrink-0" }),
3201
+ /* @__PURE__ */ jsx11("span", { className: "max-w-56 truncate", children: part.name })
3202
+ ]
3203
+ },
3204
+ `${part.name}-${part.data.slice(0, 32)}`
3205
+ )) }) : null,
3206
+ textContent
2333
3207
  ] });
2334
3208
  }
2335
3209
 
@@ -2386,7 +3260,7 @@ var RenderErrorBoundary = class extends Component {
2386
3260
  };
2387
3261
 
2388
3262
  // src/components/PostChatFollowupBlock.tsx
2389
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState10 } from "react";
3263
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef10, useState as useState11 } from "react";
2390
3264
  import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2391
3265
  function emitInteraction(callback, event) {
2392
3266
  try {
@@ -2397,9 +3271,6 @@ function emitInteraction(callback, event) {
2397
3271
  function basename(path) {
2398
3272
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2399
3273
  }
2400
- function isVideo(path) {
2401
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2402
- }
2403
3274
  function ArtifactCard({
2404
3275
  artifact,
2405
3276
  sessionId,
@@ -2409,7 +3280,7 @@ function ArtifactCard({
2409
3280
  onArtifactOpened
2410
3281
  }) {
2411
3282
  const client = useBladeClient();
2412
- const [downloading, setDownloading] = useState10(false);
3283
+ const [downloading, setDownloading] = useState11(false);
2413
3284
  const name = artifact.label || basename(artifact.target);
2414
3285
  if (artifact.kind === "link") {
2415
3286
  return /* @__PURE__ */ jsxs11(
@@ -2430,44 +3301,48 @@ ${artifact.target}`,
2430
3301
  }
2431
3302
  );
2432
3303
  }
2433
- const Icon2 = isVideo(artifact.target) ? Film : File;
2434
- return /* @__PURE__ */ jsxs11(
2435
- "button",
3304
+ const fileName = basename(artifact.target);
3305
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3306
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3307
+ ) : void 0;
3308
+ const handleDownload = async (event) => {
3309
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3310
+ event.preventDefault();
3311
+ if (!sessionId || downloading) return;
3312
+ setDownloading(true);
3313
+ emitInteraction(onInteraction, {
3314
+ type: "artifact_download_started",
3315
+ sessionId,
3316
+ assistantEntryId,
3317
+ artifactIndex,
3318
+ artifactKind: "file"
3319
+ });
3320
+ try {
3321
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3322
+ emitInteraction(onInteraction, {
3323
+ type: "artifact_download_succeeded",
3324
+ sessionId,
3325
+ assistantEntryId,
3326
+ artifactIndex,
3327
+ artifactKind: "file"
3328
+ });
3329
+ } catch {
3330
+ } finally {
3331
+ setDownloading(false);
3332
+ }
3333
+ };
3334
+ return /* @__PURE__ */ jsx13(
3335
+ "a",
2436
3336
  {
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
- ]
3337
+ href: downloadUrl,
3338
+ download: fileName,
3339
+ onClick: handleDownload,
3340
+ title: fileName,
3341
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3342
+ "aria-disabled": !sessionId || void 0,
3343
+ "aria-busy": downloading || void 0,
3344
+ 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",
3345
+ children: fileName
2471
3346
  }
2472
3347
  );
2473
3348
  }
@@ -2511,15 +3386,15 @@ function ResultFeedback({
2511
3386
  onFeedbackSaved
2512
3387
  }) {
2513
3388
  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);
3389
+ const [saved, setSaved] = useState11(savedFeedback ?? null);
3390
+ const [helpful, setHelpful] = useState11(savedFeedback?.helpful ?? null);
3391
+ const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
3392
+ const [saving, setSaving] = useState11(false);
3393
+ const [saveError, setSaveError] = useState11(false);
3394
+ const reportedShown = useRef10(false);
3395
+ const latestChoice = useRef10(null);
2521
3396
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2522
- useEffect6(() => {
3397
+ useEffect9(() => {
2523
3398
  if (!eligible || reportedShown.current) return;
2524
3399
  reportedShown.current = true;
2525
3400
  emitInteraction(onInteraction, {
@@ -2528,13 +3403,13 @@ function ResultFeedback({
2528
3403
  assistantEntryId: followup.assistant_entry_id
2529
3404
  });
2530
3405
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2531
- useEffect6(() => {
3406
+ useEffect9(() => {
2532
3407
  if (!savedFeedback || latestChoice.current) return;
2533
3408
  setSaved(savedFeedback);
2534
3409
  setHelpful(savedFeedback.helpful);
2535
3410
  setReason(savedFeedback.reason);
2536
3411
  }, [savedFeedback]);
2537
- const submit = useCallback5(
3412
+ const submit = useCallback6(
2538
3413
  async (nextHelpful, nextReason) => {
2539
3414
  if (!sessionId) return;
2540
3415
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2640,14 +3515,14 @@ function PostChatFollowupBlock({
2640
3515
  savedFeedback,
2641
3516
  onFeedbackSaved
2642
3517
  }) {
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());
3518
+ const [expanded, setExpanded] = useState11(false);
3519
+ const adopted = useRef10(/* @__PURE__ */ new Set());
3520
+ const reportedSuggestions = useRef10(false);
3521
+ const reportedArtifacts = useRef10(/* @__PURE__ */ new Set());
3522
+ const openedArtifacts = useRef10(/* @__PURE__ */ new Set());
2648
3523
  const artifacts = followup.final_artifacts ?? [];
2649
3524
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2650
- useEffect6(() => {
3525
+ useEffect9(() => {
2651
3526
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2652
3527
  reportedSuggestions.current = true;
2653
3528
  emitInteraction(onInteraction, {
@@ -2677,7 +3552,7 @@ function PostChatFollowupBlock({
2677
3552
  sessionId,
2678
3553
  visibleArtifacts
2679
3554
  ]);
2680
- const reportArtifactOpened = useCallback5(
3555
+ const reportArtifactOpened = useCallback6(
2681
3556
  (artifactIndex, artifactKind) => {
2682
3557
  if (openedArtifacts.current.has(artifactIndex)) return;
2683
3558
  openedArtifacts.current.add(artifactIndex);
@@ -2771,7 +3646,12 @@ function PostChatFollowupBlock({
2771
3646
  }
2772
3647
 
2773
3648
  // src/components/UserMessageBubble.tsx
2774
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
3649
+ import {
3650
+ chatErrorForDisplay,
3651
+ getFileParts as getFileParts2,
3652
+ getImageParts as getImageParts2,
3653
+ getTextContent as getTextContent2
3654
+ } from "@blade-hq/agent-client";
2775
3655
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2776
3656
  function isUserMessage(message) {
2777
3657
  return message.role === "user";
@@ -2782,8 +3662,8 @@ function isErrorMessage(message) {
2782
3662
  var isSending = (message) => message.status === "streaming";
2783
3663
  function UserMessageBubble({ message, className }) {
2784
3664
  const text = getTextContent2(message.content).trim();
2785
- const fileParts = getFileParts(message.content);
2786
- const imageParts = getImageParts(message.content);
3665
+ const fileParts = getFileParts2(message.content);
3666
+ const imageParts = getImageParts2(message.content);
2787
3667
  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
3668
  imageParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx14(
2789
3669
  "img",
@@ -2816,8 +3696,8 @@ function ErrorMessageBlock({
2816
3696
  message,
2817
3697
  className
2818
3698
  }) {
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 }) });
3699
+ const text = chatErrorForDisplay(getTextContent2(message.content));
3700
+ return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat-error-row flex min-w-0 justify-start", className), children: /* @__PURE__ */ jsx14("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
3701
  }
2822
3702
 
2823
3703
  // src/components/MessageList.tsx
@@ -2870,10 +3750,14 @@ function MessageList({
2870
3750
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2871
3751
  onResultFeedbackSaved
2872
3752
  }) {
3753
+ const userMessages = messages.filter((message) => isUserMessage(message));
3754
+ const latestUserMessage = userMessages.at(-1);
3755
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
2873
3756
  const renderBlocks = useMemo7(() => {
2874
3757
  const visible = messages.filter((message) => {
2875
3758
  if ((message.loop_name ?? "root") !== "root") return false;
2876
3759
  if (isHiddenInternalMessage(message)) return false;
3760
+ if (message.kind === "context") return false;
2877
3761
  if (message.kind === "compaction") return true;
2878
3762
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2879
3763
  });
@@ -2916,7 +3800,7 @@ function MessageList({
2916
3800
  blocks.push({
2917
3801
  type: "message",
2918
3802
  message,
2919
- key: message.entry_id ?? `${message.role}-${blocks.length}`
3803
+ key: message.render_id ?? message.entry_id ?? `${message.role}-${blocks.length}`
2920
3804
  });
2921
3805
  }
2922
3806
  flushAssistant();
@@ -2943,98 +3827,151 @@ function MessageList({
2943
3827
  }
2944
3828
  return blocks;
2945
3829
  }, [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,
3830
+ return /* @__PURE__ */ jsxs13("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: [
3831
+ isStreaming ? /* @__PURE__ */ jsx15("output", { className: "sr-only", children: "\u6B63\u5728\u751F\u6210\u56DE\u590D" }) : null,
3832
+ /* @__PURE__ */ jsxs13(
3833
+ StickToBottom,
3834
+ {
3835
+ className: "h-full overflow-y-hidden",
3836
+ initial: "instant",
3837
+ resize: "instant",
3838
+ children: [
3839
+ /* @__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: [
3840
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs13("div", { className: "blade-chat-empty", children: [
3841
+ /* @__PURE__ */ jsx15(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3842
+ /* @__PURE__ */ jsx15("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3843
+ /* @__PURE__ */ jsx15("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3844
+ ] }) : renderBlocks.map((block) => {
3845
+ if (block.type === "message") {
3846
+ 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);
3847
+ }
3848
+ if (block.type === "assistant_turn") {
3849
+ const blockFeedback = block.messages.map(
3850
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3851
+ ).find((feedback) => feedback != null);
3852
+ const hasActiveFollowup = Boolean(
3853
+ postChatFollowup && block.messages.some(
3854
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
3855
+ )
3856
+ );
3857
+ return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs13(
3858
+ RenderErrorBoundary,
2974
3859
  {
2975
- messages: block.messages,
2976
- isStreaming: block.isStreaming,
2977
- askAnswers,
2978
- onAnswer,
2979
- sessionStatus,
2980
- toolCallRenderer,
2981
- sessionId
3860
+ label: "\u52A9\u624B\u6D88\u606F",
3861
+ details: block.key,
3862
+ resetKey: getMessageResetSignature(block.messages),
3863
+ children: [
3864
+ /* @__PURE__ */ jsx15(
3865
+ AssistantTurnBlock,
3866
+ {
3867
+ messages: block.messages,
3868
+ isStreaming: block.isStreaming,
3869
+ askAnswers,
3870
+ onAnswer,
3871
+ sessionStatus,
3872
+ toolCallRenderer,
3873
+ sessionId
3874
+ }
3875
+ ),
3876
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3877
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
3878
+ PostChatFollowupBlock,
3879
+ {
3880
+ followup: postChatFollowup,
3881
+ sessionId,
3882
+ onSuggestion,
3883
+ isViewer,
3884
+ onInteraction: onFollowupInteraction,
3885
+ savedFeedback: blockFeedback,
3886
+ onFeedbackSaved: onResultFeedbackSaved
3887
+ }
3888
+ ) : null
3889
+ ]
2982
3890
  }
2983
- ),
2984
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
2985
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
2986
- PostChatFollowupBlock,
3891
+ ) }, block.key);
3892
+ }
3893
+ if (block.type === "compaction") {
3894
+ return /* @__PURE__ */ jsxs13(
3895
+ "div",
2987
3896
  {
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",
3897
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3898
+ children: [
3899
+ /* @__PURE__ */ jsx15(Layers, { size: 12 }),
3900
+ /* @__PURE__ */ jsx15("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3901
+ ]
3902
+ },
3903
+ block.key
3904
+ );
3905
+ }
3906
+ return /* @__PURE__ */ jsx15(PlanningDivider, { kind: block.kind }, block.key);
3907
+ }),
3908
+ 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
3909
+ ] }) }) }),
3910
+ /* @__PURE__ */ jsx15(
3911
+ PinLatestUserMessage,
3004
3912
  {
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
- ]
3913
+ userMessageCount: userMessages.length,
3914
+ shouldPinLatestUser,
3915
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3010
3916
  },
3011
- block.key
3012
- );
3013
- }
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
- ] }) });
3917
+ sessionId ?? "no-session"
3918
+ ),
3919
+ /* @__PURE__ */ jsx15(ScrollToBottomButton, {})
3920
+ ]
3921
+ },
3922
+ sessionId ?? "no-session"
3923
+ )
3924
+ ] });
3021
3925
  }
3022
- function AutoScrollOnUserSend({ userMessageCount }) {
3023
- const { scrollToBottom } = useStickToBottomContext();
3024
- const previousCountRef = useRef7(userMessageCount);
3025
- useEffect7(() => {
3026
- if (userMessageCount > previousCountRef.current) {
3926
+ function PinLatestUserMessage({
3927
+ userMessageCount,
3928
+ shouldPinLatestUser,
3929
+ targetKey
3930
+ }) {
3931
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
3932
+ const previousCountRef = useRef11(userMessageCount);
3933
+ const spacerHeightRef = useRef11(0);
3934
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
3935
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
3936
+ const getTargetElement = useCallback7(() => {
3937
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
3938
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
3939
+ }, [contentRef]);
3940
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
3941
+ const setSpacerHeight = useCallback7(
3942
+ (height) => {
3943
+ spacerHeightRef.current = height;
3944
+ const content = contentRef.current;
3945
+ if (!content) return;
3946
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
3947
+ else content.style.removeProperty("--blade-chat-pin-spacer");
3948
+ },
3949
+ [contentRef]
3950
+ );
3951
+ useMessagePin({
3952
+ targetKey,
3953
+ pinTarget: shouldPinLatestUser,
3954
+ getScrollElement,
3955
+ getContentElement,
3956
+ getTargetElement,
3957
+ getSpacerHeight,
3958
+ setSpacerHeight,
3959
+ stopAutoScroll: stopScroll,
3960
+ scrollToBottom
3961
+ });
3962
+ useEffect10(() => {
3963
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3027
3964
  scrollToBottom("instant");
3028
3965
  }
3029
3966
  previousCountRef.current = userMessageCount;
3030
- }, [userMessageCount, scrollToBottom]);
3967
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3031
3968
  return null;
3032
3969
  }
3033
3970
  function ScrollToBottomButton() {
3034
3971
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3035
- const [visible, setVisible] = useState11(false);
3036
- const hideTimerRef = useRef7(null);
3037
- useEffect7(() => {
3972
+ const [visible, setVisible] = useState12(false);
3973
+ const hideTimerRef = useRef11(null);
3974
+ useEffect10(() => {
3038
3975
  if (isAtBottom) {
3039
3976
  if (!hideTimerRef.current) {
3040
3977
  hideTimerRef.current = setTimeout(() => {
@@ -3056,7 +3993,7 @@ function ScrollToBottomButton() {
3056
3993
  }
3057
3994
  };
3058
3995
  }, [isAtBottom]);
3059
- const handleClick = useCallback6(() => {
3996
+ const handleClick = useCallback7(() => {
3060
3997
  if (hideTimerRef.current) {
3061
3998
  clearTimeout(hideTimerRef.current);
3062
3999
  hideTimerRef.current = null;
@@ -3136,7 +4073,7 @@ function ChatSurface({
3136
4073
  banner,
3137
4074
  errorMessage && /* @__PURE__ */ jsxs14("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
3138
4075
  /* @__PURE__ */ jsx16(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
3139
- /* @__PURE__ */ jsx16("span", { children: errorMessage })
4076
+ /* @__PURE__ */ jsx16("span", { className: "min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: chatErrorForDisplay2(errorMessage) })
3140
4077
  ] }),
3141
4078
  slots?.header,
3142
4079
  /* @__PURE__ */ jsx16(
@@ -3185,8 +4122,8 @@ function isUnauthorizedError(error) {
3185
4122
  return error instanceof BladeApiError && error.status === 401;
3186
4123
  }
3187
4124
  function LoginCard({ client, onLoggedIn }) {
3188
- const [loggingIn, setLoggingIn] = useState12(false);
3189
- const [loginError, setLoginError] = useState12(null);
4125
+ const [loggingIn, setLoggingIn] = useState13(false);
4126
+ const [loginError, setLoginError] = useState13(null);
3190
4127
  const handleLogin = async () => {
3191
4128
  setLoggingIn(true);
3192
4129
  setLoginError(null);
@@ -3218,8 +4155,8 @@ function LoginCard({ client, onLoggedIn }) {
3218
4155
  }
3219
4156
  function AgentChat(props) {
3220
4157
  const client = useBladeClient();
3221
- const [attempt, setAttempt] = useState12(0);
3222
- const [needLogin, setNeedLogin] = useState12(() => !client.hasToken());
4158
+ const [attempt, setAttempt] = useState13(0);
4159
+ const [needLogin, setNeedLogin] = useState13(() => !client.hasToken());
3223
4160
  if (needLogin) {
3224
4161
  return /* @__PURE__ */ jsx17(
3225
4162
  "div",
@@ -3261,12 +4198,12 @@ function ChatSessionView({
3261
4198
  onSessionCreated
3262
4199
  });
3263
4200
  const replay = useReplay(session);
3264
- const [stopRequested, setStopRequested] = useState12(false);
3265
- const [inputText, setInputText] = useState12("");
3266
- const [resultFeedback, setResultFeedback] = useState12([]);
4201
+ const [stopRequested, setStopRequested] = useState13(false);
4202
+ const [inputText, setInputText] = useState13("");
4203
+ const [resultFeedback, setResultFeedback] = useState13([]);
3267
4204
  const resolvedSessionId = session?.sessionId;
3268
4205
  const isViewer = state?.viewerRole === "viewer";
3269
- useEffect8(() => {
4206
+ useEffect11(() => {
3270
4207
  setResultFeedback([]);
3271
4208
  if (!resolvedSessionId || isViewer) return;
3272
4209
  let cancelled = false;
@@ -3295,18 +4232,18 @@ function ChatSessionView({
3295
4232
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3296
4233
  [resultFeedback]
3297
4234
  );
3298
- const handleResultFeedbackSaved = useCallback7((saved) => {
4235
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3299
4236
  setResultFeedback((current) => [
3300
4237
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3301
4238
  saved
3302
4239
  ]);
3303
4240
  }, []);
3304
- useEffect8(() => {
4241
+ useEffect11(() => {
3305
4242
  if (session) {
3306
4243
  onSessionReady?.(session);
3307
4244
  }
3308
4245
  }, [session, onSessionReady]);
3309
- useEffect8(() => {
4246
+ useEffect11(() => {
3310
4247
  if (!session) return;
3311
4248
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3312
4249
  setInputText((prev) => `${prev ? `${prev}
@@ -3322,12 +4259,12 @@ ${content}`);
3322
4259
  offInsert();
3323
4260
  };
3324
4261
  }, [session]);
3325
- useEffect8(() => {
4262
+ useEffect11(() => {
3326
4263
  if (isUnauthorizedError(error)) {
3327
4264
  onUnauthorized();
3328
4265
  }
3329
4266
  }, [error, onUnauthorized]);
3330
- useEffect8(() => {
4267
+ useEffect11(() => {
3331
4268
  if (!session || !commands) return;
3332
4269
  const unsubscribes = Object.entries(commands).map(
3333
4270
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3398,10 +4335,10 @@ ${content}`);
3398
4335
  }
3399
4336
 
3400
4337
  // src/components/LlmChat.tsx
3401
- import { useEffect as useEffect9, useMemo as useMemo9, useState as useState14 } from "react";
4338
+ import { useEffect as useEffect12, useMemo as useMemo9, useState as useState15 } from "react";
3402
4339
 
3403
4340
  // src/components/LlmAdvancedSettings.tsx
3404
- import { useState as useState13 } from "react";
4341
+ import { useState as useState14 } from "react";
3405
4342
  import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3406
4343
  var FIELDS = [
3407
4344
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
@@ -3448,8 +4385,8 @@ function writeOverride(settings, baseURL, override) {
3448
4385
  }
3449
4386
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3450
4387
  const normalized = normalizeAdvanced(settings);
3451
- const [open, setOpen] = useState13(false);
3452
- const [draft, setDraft] = useState13(override);
4388
+ const [open, setOpen] = useState14(false);
4389
+ const [draft, setDraft] = useState14(override);
3453
4390
  if (!normalized) return null;
3454
4391
  const fields = FIELDS.filter((field) => normalized[field.id]);
3455
4392
  const dirty = Object.keys(override).length > 0;
@@ -3532,11 +4469,11 @@ function LlmChat({
3532
4469
  onOverrideChange,
3533
4470
  ...options
3534
4471
  }) {
3535
- const [override, setOverride] = useState14(() => readOverride(advanced, options.baseURL));
4472
+ const [override, setOverride] = useState15(() => readOverride(advanced, options.baseURL));
3536
4473
  const effective = { ...options, ...override };
3537
4474
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3538
- const [inputText, setInputText] = useState14("");
3539
- const [stopRequested, setStopRequested] = useState14(false);
4475
+ const [inputText, setInputText] = useState15("");
4476
+ const [stopRequested, setStopRequested] = useState15(false);
3540
4477
  const handle = useMemo9(
3541
4478
  () => ({
3542
4479
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3546,7 +4483,7 @@ ${text}` : text),
3546
4483
  }),
3547
4484
  [send, reset]
3548
4485
  );
3549
- useEffect9(() => {
4486
+ useEffect12(() => {
3550
4487
  onReady?.(handle);
3551
4488
  }, [handle, onReady]);
3552
4489
  return /* @__PURE__ */ jsx19(
@@ -3615,19 +4552,108 @@ function ChatView(props) {
3615
4552
  return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
3616
4553
  }
3617
4554
 
4555
+ // src/components/ContextCard.tsx
4556
+ import {
4557
+ getContextDisplayState
4558
+ } from "@blade-hq/agent-client";
4559
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
4560
+ function ContextCard({ context, className }) {
4561
+ const display = getContextDisplayState(context);
4562
+ return /* @__PURE__ */ jsxs17(
4563
+ "details",
4564
+ {
4565
+ className: `blade-chat-context-card group rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
4566
+ children: [
4567
+ /* @__PURE__ */ jsxs17("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: [
4568
+ /* @__PURE__ */ jsx21(
4569
+ Layers,
4570
+ {
4571
+ size: 15,
4572
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
4573
+ "aria-hidden": "true"
4574
+ }
4575
+ ),
4576
+ /* @__PURE__ */ jsxs17("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
4577
+ /* @__PURE__ */ jsx21("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
4578
+ /* @__PURE__ */ jsx21("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
4579
+ ] }),
4580
+ /* @__PURE__ */ jsx21(
4581
+ ChevronDown,
4582
+ {
4583
+ size: 14,
4584
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open:rotate-180",
4585
+ "aria-hidden": "true"
4586
+ }
4587
+ )
4588
+ ] }),
4589
+ /* @__PURE__ */ jsx21("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 })
4590
+ ]
4591
+ }
4592
+ );
4593
+ }
4594
+
4595
+ // src/lib/agent-computer-command.ts
4596
+ var COMPUTER_LAUNCH_COMMAND_PATTERN = /(?:^|[\n;&|(]\s*)computer\s+launch(?:\s|$)/;
4597
+ function isAgentComputerCommand(command) {
4598
+ return COMPUTER_LAUNCH_COMMAND_PATTERN.test(command);
4599
+ }
4600
+ function isAgentComputerToolCall(argumentsJson) {
4601
+ if (!argumentsJson) return false;
4602
+ let command;
4603
+ try {
4604
+ const parsed = JSON.parse(argumentsJson);
4605
+ if (typeof parsed !== "object" || parsed === null) return false;
4606
+ command = parsed.command;
4607
+ } catch {
4608
+ return false;
4609
+ }
4610
+ return typeof command === "string" && isAgentComputerCommand(command);
4611
+ }
4612
+ var LAUNCH_SUCCESS_MARKER = "\u5DF2\u542F\u52A8 ";
4613
+ function resultContainsLaunchSuccessMarker(result, depth = 0) {
4614
+ if (depth > 2) return false;
4615
+ if (typeof result === "string") {
4616
+ if (result.includes(LAUNCH_SUCCESS_MARKER)) return true;
4617
+ try {
4618
+ return resultContainsLaunchSuccessMarker(JSON.parse(result), depth + 1);
4619
+ } catch {
4620
+ return false;
4621
+ }
4622
+ }
4623
+ if (typeof result === "object" && result !== null) {
4624
+ for (const value of Object.values(result)) {
4625
+ if (typeof value === "string" && value.includes(LAUNCH_SUCCESS_MARKER)) return true;
4626
+ }
4627
+ }
4628
+ return false;
4629
+ }
4630
+ function classifyAgentComputerLaunchOutcome(toolCall) {
4631
+ if (toolCall.status === "error" || toolCall.status === "cancelled") return "failed";
4632
+ if (toolCall.status !== "done") return "pending";
4633
+ if (toolCall.result === void 0 || toolCall.result === null) return "unknown";
4634
+ return resultContainsLaunchSuccessMarker(toolCall.result) ? "succeeded" : "failed";
4635
+ }
4636
+
3618
4637
  // src/index.ts
3619
4638
  export * from "@blade-hq/agent-client";
3620
4639
  export {
3621
4640
  AgentChat,
3622
4641
  BladeProvider,
3623
4642
  ChatView,
4643
+ ContextCard,
3624
4644
  LlmChat,
3625
4645
  MarkdownContent,
4646
+ MemoryRefsHint,
3626
4647
  ReplayBar,
3627
4648
  ReplayMismatchPrompt,
4649
+ classifyAgentComputerLaunchOutcome,
4650
+ collectMemoryRefs,
4651
+ isAgentComputerCommand,
4652
+ isAgentComputerToolCall,
3628
4653
  useAgentSession,
3629
4654
  useBladeClient,
3630
4655
  useLlmChat,
4656
+ useMessagePin,
3631
4657
  useReplay
3632
4658
  };
3633
4659
  /*! Bundled license information:
@@ -3639,17 +4665,16 @@ lucide-react/dist/esm/createLucideIcon.js:
3639
4665
  lucide-react/dist/esm/icons/arrow-right.js:
3640
4666
  lucide-react/dist/esm/icons/arrow-up-right.js:
3641
4667
  lucide-react/dist/esm/icons/arrow-up.js:
4668
+ lucide-react/dist/esm/icons/book-open.js:
3642
4669
  lucide-react/dist/esm/icons/bot.js:
3643
- lucide-react/dist/esm/icons/brain.js:
3644
4670
  lucide-react/dist/esm/icons/check.js:
3645
4671
  lucide-react/dist/esm/icons/chevron-down.js:
3646
4672
  lucide-react/dist/esm/icons/chevron-right.js:
3647
4673
  lucide-react/dist/esm/icons/circle-alert.js:
3648
4674
  lucide-react/dist/esm/icons/copy.js:
3649
- lucide-react/dist/esm/icons/download.js:
4675
+ lucide-react/dist/esm/icons/earth.js:
4676
+ lucide-react/dist/esm/icons/file-pen-line.js:
3650
4677
  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
4678
  lucide-react/dist/esm/icons/globe.js:
3654
4679
  lucide-react/dist/esm/icons/layers.js:
3655
4680
  lucide-react/dist/esm/icons/lightbulb.js:
@@ -3658,10 +4683,13 @@ lucide-react/dist/esm/icons/lock-keyhole.js:
3658
4683
  lucide-react/dist/esm/icons/message-square-more.js:
3659
4684
  lucide-react/dist/esm/icons/message-square.js:
3660
4685
  lucide-react/dist/esm/icons/play.js:
4686
+ lucide-react/dist/esm/icons/search.js:
3661
4687
  lucide-react/dist/esm/icons/settings-2.js:
3662
4688
  lucide-react/dist/esm/icons/sparkles.js:
3663
4689
  lucide-react/dist/esm/icons/square.js:
4690
+ lucide-react/dist/esm/icons/terminal.js:
3664
4691
  lucide-react/dist/esm/icons/triangle-alert.js:
4692
+ lucide-react/dist/esm/icons/wrench.js:
3665
4693
  lucide-react/dist/esm/icons/x.js:
3666
4694
  lucide-react/dist/esm/lucide-react.js:
3667
4695
  (**