@blade-hq/agent-react 2610.0.0-beta.17 → 2610.0.0-beta.19

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,6 +906,39 @@ 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
 
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" }]
940
+ ]);
941
+
700
942
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
701
943
  var FileText = createLucideIcon("FileText", [
702
944
  ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", key: "1rqfz7" }],
@@ -781,6 +1023,12 @@ var Play = createLucideIcon("Play", [
781
1023
  ["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
782
1024
  ]);
783
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
+
784
1032
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
785
1033
  var Settings2 = createLucideIcon("Settings2", [
786
1034
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -809,6 +1057,12 @@ var Square = createLucideIcon("Square", [
809
1057
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
810
1058
  ]);
811
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
+
812
1066
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
813
1067
  var TriangleAlert = createLucideIcon("TriangleAlert", [
814
1068
  [
@@ -822,6 +1076,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
822
1076
  ["path", { d: "M12 17h.01", key: "p32p05" }]
823
1077
  ]);
824
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
+
825
1090
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
826
1091
  var X = createLucideIcon("X", [
827
1092
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -829,7 +1094,7 @@ var X = createLucideIcon("X", [
829
1094
  ]);
830
1095
 
831
1096
  // src/components/AgentChat.tsx
832
- import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo8, useState as useState13 } from "react";
1097
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo8, useState as useState13 } from "react";
833
1098
 
834
1099
  // src/lib/utils.ts
835
1100
  function cn(...inputs) {
@@ -958,6 +1223,12 @@ function ReplayMismatchPrompt({ mismatch, className }) {
958
1223
 
959
1224
  // src/components/ChatInput.tsx
960
1225
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1226
+ function isImeCompositionKey(event) {
1227
+ return event.isComposing || event.keyCode === 229;
1228
+ }
1229
+ function shouldSubmitChatInput(event, menuOpen) {
1230
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1231
+ }
961
1232
  function ChatInput({
962
1233
  value,
963
1234
  onValueChange,
@@ -977,7 +1248,12 @@ function ChatInput({
977
1248
  onValueChange("");
978
1249
  };
979
1250
  const handleKeyDown = (event) => {
980
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1251
+ if (shouldSubmitChatInput({
1252
+ key: event.key,
1253
+ shiftKey: event.shiftKey,
1254
+ isComposing: event.nativeEvent.isComposing,
1255
+ keyCode: event.nativeEvent.keyCode
1256
+ }, false)) {
981
1257
  event.preventDefault();
982
1258
  void handleSend();
983
1259
  }
@@ -1027,16 +1303,16 @@ function ChatInput({
1027
1303
  }
1028
1304
 
1029
1305
  // src/components/ConnectionBanner.tsx
1030
- import { useEffect as useEffect3, useRef as useRef3, useState as useState4 } from "react";
1306
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1031
1307
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1032
1308
  var CONNECTION_NOTICE_DELAY_MS = 3e3;
1033
1309
  var CONNECTION_ERROR_DELAY_MS = 15e3;
1034
1310
  function useConnectionNoticePhase(connected) {
1035
1311
  const [phase, setPhase] = useState4("hidden");
1036
- const connectedRef = useRef3(connected);
1037
- const timersRef = useRef3([]);
1312
+ const connectedRef = useRef4(connected);
1313
+ const timersRef = useRef4([]);
1038
1314
  connectedRef.current = connected;
1039
- useEffect3(() => {
1315
+ useEffect4(() => {
1040
1316
  const clearTimers = () => {
1041
1317
  for (const timer of timersRef.current) clearTimeout(timer);
1042
1318
  timersRef.current = [];
@@ -1076,7 +1352,7 @@ function useConnectionNoticePhase(connected) {
1076
1352
  return phase;
1077
1353
  }
1078
1354
  function ConnectionBanner({ connection, className }) {
1079
- const hasConnectedRef = useRef3(connection === "connected" || connection === "reconnecting");
1355
+ const hasConnectedRef = useRef4(connection === "connected" || connection === "reconnecting");
1080
1356
  if (connection === "connected") hasConnectedRef.current = true;
1081
1357
  const connected = connection === "connected";
1082
1358
  const phase = useConnectionNoticePhase(connected);
@@ -1103,10 +1379,10 @@ function ConnectionBanner({ connection, className }) {
1103
1379
 
1104
1380
  // src/components/MessageList.tsx
1105
1381
  import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1106
- import { useCallback as useCallback6, useEffect as useEffect8, useMemo as useMemo7, useRef as useRef9, useState as useState12 } from "react";
1382
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo7, useRef as useRef11, useState as useState12 } from "react";
1107
1383
 
1108
1384
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1109
- import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
1385
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
1110
1386
  var DEFAULT_SPRING_ANIMATION = {
1111
1387
  /**
1112
1388
  * A value from 0 to 1, on how much to damp the animation.
@@ -1146,9 +1422,9 @@ var useStickToBottom = (options = {}) => {
1146
1422
  const [escapedFromLock, updateEscapedFromLock] = useState5(false);
1147
1423
  const [isAtBottom, updateIsAtBottom] = useState5(options.initial !== false);
1148
1424
  const [isNearBottom, setIsNearBottom] = useState5(false);
1149
- const optionsRef = useRef4(null);
1425
+ const optionsRef = useRef5(null);
1150
1426
  optionsRef.current = options;
1151
- const isSelecting = useCallback3(() => {
1427
+ const isSelecting = useCallback4(() => {
1152
1428
  if (!mouseDown) {
1153
1429
  return false;
1154
1430
  }
@@ -1159,11 +1435,11 @@ var useStickToBottom = (options = {}) => {
1159
1435
  const range = selection.getRangeAt(0);
1160
1436
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1161
1437
  }, []);
1162
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1438
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1163
1439
  state.isAtBottom = isAtBottom2;
1164
1440
  updateIsAtBottom(isAtBottom2);
1165
1441
  }, []);
1166
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1442
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1167
1443
  state.escapedFromLock = escapedFromLock2;
1168
1444
  updateEscapedFromLock(escapedFromLock2);
1169
1445
  }, []);
@@ -1220,7 +1496,7 @@ var useStickToBottom = (options = {}) => {
1220
1496
  }
1221
1497
  };
1222
1498
  }, []);
1223
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1499
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1224
1500
  if (typeof scrollOptions === "string") {
1225
1501
  scrollOptions = { animation: scrollOptions };
1226
1502
  }
@@ -1305,11 +1581,11 @@ var useStickToBottom = (options = {}) => {
1305
1581
  }
1306
1582
  return next();
1307
1583
  }, [setIsAtBottom, isSelecting, state]);
1308
- const stopScroll = useCallback3(() => {
1584
+ const stopScroll = useCallback4(() => {
1309
1585
  setEscapedFromLock(true);
1310
1586
  setIsAtBottom(false);
1311
1587
  }, [setEscapedFromLock, setIsAtBottom]);
1312
- const handleScroll = useCallback3(({ target }) => {
1588
+ const handleScroll = useCallback4(({ target }) => {
1313
1589
  if (target !== scrollRef.current) {
1314
1590
  return;
1315
1591
  }
@@ -1348,7 +1624,7 @@ var useStickToBottom = (options = {}) => {
1348
1624
  }
1349
1625
  }, 1);
1350
1626
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1351
- const handleWheel = useCallback3(({ target, deltaY }) => {
1627
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1352
1628
  let element = target;
1353
1629
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1354
1630
  if (!element.parentElement) {
@@ -1418,7 +1694,7 @@ var useStickToBottom = (options = {}) => {
1418
1694
  };
1419
1695
  };
1420
1696
  function useRefCallback(callback, deps) {
1421
- const result = useCallback3((ref) => {
1697
+ const result = useCallback4((ref) => {
1422
1698
  result.current = ref;
1423
1699
  return callback(ref);
1424
1700
  }, deps);
@@ -1450,11 +1726,11 @@ function mergeAnimations(...animations) {
1450
1726
 
1451
1727
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1452
1728
  import * as React from "react";
1453
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect4, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef5 } from "react";
1729
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef6 } from "react";
1454
1730
  var StickToBottomContext = createContext2(null);
1455
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect4;
1731
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect5;
1456
1732
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1457
- const customTargetScrollTop = useRef5(null);
1733
+ const customTargetScrollTop = useRef6(null);
1458
1734
  const targetScrollTop = React.useCallback((target, elements) => {
1459
1735
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1460
1736
  return get?.(target, elements) ?? target;
@@ -1530,8 +1806,13 @@ function useStickToBottomContext() {
1530
1806
  }
1531
1807
 
1532
1808
  // src/components/AssistantTurnBlock.tsx
1533
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1534
- import { useState as useState10 } from "react";
1809
+ import {
1810
+ getFileParts,
1811
+ getImageParts,
1812
+ getTextContent,
1813
+ normalizeMessageContent
1814
+ } from "@blade-hq/agent-client";
1815
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState10 } from "react";
1535
1816
 
1536
1817
  // src/components/AgentLoopBlock.tsx
1537
1818
  import { useState as useState6 } from "react";
@@ -1551,6 +1832,7 @@ var TOOL_NAME_ALIASES = {
1551
1832
  glob: "Glob",
1552
1833
  grep: "Grep",
1553
1834
  ls: "Ls",
1835
+ multi_edit: "MultiEdit",
1554
1836
  read: "Read",
1555
1837
  read_skill: "ReadSkill",
1556
1838
  web_fetch: "WebFetch",
@@ -1563,6 +1845,7 @@ var TOOL_DISPLAY_LABELS = {
1563
1845
  Read: "\u8BFB\u53D6\u6587\u4EF6",
1564
1846
  Write: "\u5199\u5165\u6587\u4EF6",
1565
1847
  Edit: "\u7F16\u8F91\u6587\u4EF6",
1848
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1566
1849
  Ls: "\u5217\u51FA\u76EE\u5F55",
1567
1850
  Glob: "\u5339\u914D\u6587\u4EF6",
1568
1851
  Grep: "\u641C\u7D22\u6587\u672C",
@@ -1576,6 +1859,16 @@ var TOOL_DISPLAY_LABELS = {
1576
1859
  ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1577
1860
  GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1578
1861
  };
1862
+ var PRIORITY_FILE_KEYS = [
1863
+ "file_path",
1864
+ "output_file",
1865
+ "output_path",
1866
+ "path",
1867
+ "filepath",
1868
+ "target_file",
1869
+ "destination",
1870
+ "filename"
1871
+ ];
1579
1872
  function safeParseJson(value) {
1580
1873
  if (!value) return null;
1581
1874
  try {
@@ -1584,6 +1877,38 @@ function safeParseJson(value) {
1584
1877
  return null;
1585
1878
  }
1586
1879
  }
1880
+ function isPlainObject(value) {
1881
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1882
+ }
1883
+ function findFileLikeValue(value, depth = 0, allowPlainString = false) {
1884
+ if (depth > 3) return null;
1885
+ if (typeof value === "string") {
1886
+ const trimmed = value.trim();
1887
+ if (!trimmed) return null;
1888
+ if (allowPlainString || trimmed.includes("/") || /\.[a-z0-9]{1,8}$/i.test(trimmed)) {
1889
+ return trimmed;
1890
+ }
1891
+ return null;
1892
+ }
1893
+ if (Array.isArray(value)) {
1894
+ for (const item of value) {
1895
+ const found = findFileLikeValue(item, depth + 1, allowPlainString);
1896
+ if (found) return found;
1897
+ }
1898
+ return null;
1899
+ }
1900
+ if (isPlainObject(value)) {
1901
+ for (const key of PRIORITY_FILE_KEYS) {
1902
+ const direct = findFileLikeValue(value[key], depth + 1, true);
1903
+ if (direct) return direct;
1904
+ }
1905
+ for (const nested of Object.values(value)) {
1906
+ const found = findFileLikeValue(nested, depth + 1, allowPlainString);
1907
+ if (found) return found;
1908
+ }
1909
+ }
1910
+ return null;
1911
+ }
1587
1912
  function getStringArgValue(args, key) {
1588
1913
  const value = args?.[key];
1589
1914
  return typeof value === "string" ? value.trim() : "";
@@ -1606,6 +1931,15 @@ function formatToolName(name) {
1606
1931
  const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1607
1932
  return TOOL_NAME_ALIASES[normalized] ?? stripped;
1608
1933
  }
1934
+ function extractToolFilePath(toolCall) {
1935
+ const formattedName = formatToolName(toolCall.name);
1936
+ if (formattedName !== "Read" && formattedName !== "Write" && formattedName !== "Edit" && formattedName !== "MultiEdit") {
1937
+ return null;
1938
+ }
1939
+ const filePath = findFileLikeValue(safeParseJson(toolCall.arguments));
1940
+ if (!filePath || filePath.split("/").pop()?.toLowerCase() === "phase.json") return null;
1941
+ return filePath;
1942
+ }
1609
1943
  function getToolDisplayLabel(toolCall) {
1610
1944
  const normalized = formatToolName(toolCall.name);
1611
1945
  const args = safeParseJson(toolCall.arguments);
@@ -1687,79 +2021,64 @@ function AgentLoopBlock({ toolCall }) {
1687
2021
  const description = parseAgentDescription(toolCall.arguments);
1688
2022
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1689
2023
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1690
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
2024
+ const hasResult = toolCall.result != null;
2025
+ const iconClass = cn(
2026
+ "size-3.5 shrink-0",
2027
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2028
+ );
2029
+ return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
1691
2030
  /* @__PURE__ */ jsxs5(
1692
- "div",
2031
+ "button",
1693
2032
  {
2033
+ type: "button",
2034
+ onClick: () => hasResult && setExpanded(!expanded),
2035
+ disabled: !hasResult,
2036
+ "aria-expanded": hasResult ? expanded : void 0,
2037
+ "data-testid": "execution-tool-intent",
1694
2038
  className: cn(
1695
- "border-l-[3px] flex items-center gap-2 px-3 py-2",
1696
- failed ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : running ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]"
2039
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2040
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2041
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1697
2042
  ),
2043
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1698
2044
  children: [
1699
- /* @__PURE__ */ jsxs5(
1700
- "button",
2045
+ 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" }),
2046
+ /* @__PURE__ */ jsxs5("span", { className: "min-w-0 truncate", children: [
2047
+ "\u5B50\u4EFB\u52A1\uFF1A",
2048
+ description
2049
+ ] }),
2050
+ hasResult ? /* @__PURE__ */ jsx6(
2051
+ ChevronRight,
1701
2052
  {
1702
- type: "button",
1703
- onClick: () => setExpanded(!expanded),
1704
- 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",
1705
- "aria-expanded": expanded,
1706
- children: [
1707
- /* @__PURE__ */ jsx6(
1708
- ChevronRight,
1709
- {
1710
- size: 11,
1711
- className: cn(
1712
- "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform",
1713
- expanded && "rotate-90"
1714
- )
1715
- }
1716
- ),
1717
- /* @__PURE__ */ jsx6(Bot, { size: 12, className: "shrink-0 text-[hsl(var(--muted-foreground))]" }),
1718
- /* @__PURE__ */ jsxs5(
1719
- "span",
1720
- {
1721
- className: cn(
1722
- "flex shrink-0 items-center gap-1 text-[10px]",
1723
- failed ? "text-[hsl(var(--muted-foreground))]" : running ? "text-blue-300" : "text-[hsl(var(--primary))]"
1724
- ),
1725
- children: [
1726
- running ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 11, className: "animate-spin" }) : failed ? /* @__PURE__ */ jsx6(X, { size: 11 }) : /* @__PURE__ */ jsx6(Check, { size: 11 }),
1727
- /* @__PURE__ */ jsx6("span", { children: running ? "\u6267\u884C\u4E2D" : failed ? "\u5DF2\u7EC8\u6B62" : "\u5B8C\u6210" })
1728
- ]
1729
- }
1730
- ),
1731
- /* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: [
1732
- "\u5B50\u667A\u80FD\u4F53\uFF1A",
1733
- description
1734
- ] })
1735
- ]
2053
+ size: 14,
2054
+ className: cn(
2055
+ "shrink-0 transition-transform duration-300",
2056
+ expanded && "rotate-90"
2057
+ ),
2058
+ "aria-hidden": "true"
1736
2059
  }
1737
- ),
1738
- 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) })
2060
+ ) : null
1739
2061
  ]
1740
2062
  }
1741
2063
  ),
1742
- expanded && toolCall.result != null && /* @__PURE__ */ jsxs5("div", { className: "ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
1743
- /* @__PURE__ */ jsx6("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
1744
- /* @__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) })
1745
- ] })
2064
+ 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
1746
2065
  ] });
1747
2066
  }
1748
2067
 
1749
2068
  // src/components/MarkdownContent.tsx
1750
2069
  import {
1751
- useEffect as useEffect5,
2070
+ useEffect as useEffect6,
1752
2071
  useMemo as useMemo5,
1753
- useRef as useRef6,
2072
+ useRef as useRef7,
1754
2073
  useState as useState7
1755
2074
  } from "react";
1756
2075
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1757
2076
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
1758
2077
  function CodeBlockPre({ children, node: _node, ...props }) {
1759
- const preRef = useRef6(null);
2078
+ const preRef = useRef7(null);
1760
2079
  const [copied, setCopied] = useState7(false);
1761
2080
  const [language, setLanguage] = useState7("");
1762
- useEffect5(() => {
2081
+ useEffect6(() => {
1763
2082
  const codeEl = preRef.current?.querySelector("code");
1764
2083
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1765
2084
  }, []);
@@ -1833,7 +2152,7 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1833
2152
  import { useState as useState9 } from "react";
1834
2153
 
1835
2154
  // src/components/AskUserQuestionBlock.tsx
1836
- import { useEffect as useEffect6, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
2155
+ import { useEffect as useEffect7, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1837
2156
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1838
2157
  var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
1839
2158
  function resizeCustomTextarea(textarea) {
@@ -1842,12 +2161,12 @@ function resizeCustomTextarea(textarea) {
1842
2161
  textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
1843
2162
  }
1844
2163
  function useAutoResizeTextarea(value) {
1845
- const textareaRef = useRef7(null);
1846
- useEffect6(() => {
2164
+ const textareaRef = useRef8(null);
2165
+ useEffect7(() => {
1847
2166
  const textarea = textareaRef.current;
1848
2167
  if (textarea?.value === value) resizeCustomTextarea(textarea);
1849
2168
  }, [value]);
1850
- useEffect6(() => {
2169
+ useEffect7(() => {
1851
2170
  const textarea = textareaRef.current;
1852
2171
  if (!textarea || typeof ResizeObserver === "undefined") return;
1853
2172
  let previousWidth = textarea.clientWidth;
@@ -1877,7 +2196,7 @@ function AskUserQuestionBlock({
1877
2196
  const [usingCustom, setUsingCustom] = useState8(/* @__PURE__ */ new Set());
1878
2197
  const [note, setNote] = useState8("");
1879
2198
  const [submitted, setSubmitted] = useState8(false);
1880
- useEffect6(() => {
2199
+ useEffect7(() => {
1881
2200
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1882
2201
  setSubmitted(false);
1883
2202
  }
@@ -2369,44 +2688,239 @@ function buildAskUserPayload(argumentsJson) {
2369
2688
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2370
2689
  function ThinkingBlock({ reasoning, isStreaming }) {
2371
2690
  const [open, setOpen] = useState10(false);
2372
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2691
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking text-xs", children: [
2373
2692
  /* @__PURE__ */ jsxs9(
2374
2693
  "button",
2375
2694
  {
2376
2695
  type: "button",
2377
2696
  onClick: () => setOpen(!open),
2378
2697
  "aria-expanded": open,
2379
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2698
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2380
2699
  children: [
2381
- /* @__PURE__ */ jsx11(Brain, { size: 12, className: "shrink-0" }),
2382
- isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u601D\u8003\u8FC7\u7A0B" }),
2383
- /* @__PURE__ */ jsxs9("span", { className: "text-[hsl(var(--muted-foreground))]/70", children: [
2384
- "\xB7 ",
2385
- new Intl.NumberFormat("zh-CN").format(reasoning.length),
2386
- " \u5B57"
2387
- ] }),
2700
+ isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u5DF2\u601D\u8003" }),
2388
2701
  /* @__PURE__ */ jsx11(
2389
- ChevronDown,
2702
+ ChevronRight,
2390
2703
  {
2391
- size: 12,
2392
- className: cn("shrink-0 transition-transform", open && "rotate-180")
2704
+ size: 14,
2705
+ className: cn(
2706
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
2707
+ open && "rotate-90 opacity-100"
2708
+ )
2393
2709
  }
2394
2710
  )
2395
2711
  ]
2396
2712
  }
2397
2713
  ),
2398
- 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 })
2714
+ open && /* @__PURE__ */ jsx11("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning })
2399
2715
  ] });
2400
2716
  }
2401
2717
  function getMessageText(message) {
2402
2718
  return getTextContent(normalizeMessageContent(message.content)).trim();
2403
2719
  }
2720
+ function hasRenderableMessageContent(message) {
2721
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
2722
+ }
2723
+ function getLastContentMessage(messages) {
2724
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
2725
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
2726
+ }
2727
+ return null;
2728
+ }
2404
2729
  function findLatestReasoningMessageIndex(messages) {
2405
2730
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2406
2731
  if (messages[index].reasoning) return index;
2407
2732
  }
2408
2733
  return -1;
2409
2734
  }
2735
+ function resolveTurnDisplayMode({
2736
+ isStreaming: _isStreaming,
2737
+ displayMode
2738
+ }) {
2739
+ return displayMode;
2740
+ }
2741
+ function formatExecutionDuration(durationMs) {
2742
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
2743
+ const minutes = Math.floor(totalSeconds / 60);
2744
+ const seconds = totalSeconds % 60;
2745
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
2746
+ }
2747
+ function getExecutionDurationMs({
2748
+ messages,
2749
+ isStreaming,
2750
+ now = Date.now()
2751
+ }) {
2752
+ const knownDuration = messages.reduce(
2753
+ (total, message) => {
2754
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
2755
+ return total + message.duration_ms;
2756
+ }
2757
+ return total + (message.tool_calls ?? []).reduce(
2758
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
2759
+ 0
2760
+ );
2761
+ },
2762
+ 0
2763
+ );
2764
+ if (!isStreaming) return knownDuration;
2765
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
2766
+ if (startedAt === void 0) return knownDuration;
2767
+ return Math.max(knownDuration, now - startedAt);
2768
+ }
2769
+ function findLastExceptionalEvent(messages) {
2770
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
2771
+ const messageStatus = messages[messageIndex].status;
2772
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
2773
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
2774
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
2775
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
2776
+ const status = toolCalls[toolIndex].status;
2777
+ if (status === "error" || status === "cancelled") {
2778
+ return { messageIndex, status };
2779
+ }
2780
+ }
2781
+ }
2782
+ return null;
2783
+ }
2784
+ function executionSummaryLabel({
2785
+ messages,
2786
+ isStreaming,
2787
+ durationMs,
2788
+ sessionStatus,
2789
+ askAnswers
2790
+ }) {
2791
+ if (isStreaming) {
2792
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
2793
+ }
2794
+ if (sessionStatus === "waiting_for_input" && messages.some(
2795
+ (message) => (message.tool_calls ?? []).some(
2796
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
2797
+ )
2798
+ )) {
2799
+ return "\u7B49\u5F85\u8F93\u5165";
2800
+ }
2801
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
2802
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
2803
+ if (lastExceptionalEvent) {
2804
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
2805
+ if (lastExceptionalEvent.status === "error") {
2806
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
2807
+ }
2808
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
2809
+ }
2810
+ return completedLabel;
2811
+ }
2812
+ function executionToolTypeLabel(toolCall) {
2813
+ switch (formatToolName(toolCall.name)) {
2814
+ case "WebSearch":
2815
+ case "WebFetch":
2816
+ return "\u7F51\u7EDC\u68C0\u7D22";
2817
+ case "Bash":
2818
+ case "BgBash":
2819
+ return "\u547D\u4EE4\u6267\u884C";
2820
+ case "Read":
2821
+ case "ReadSkill":
2822
+ return "\u5185\u5BB9\u8BFB\u53D6";
2823
+ case "Write":
2824
+ case "Edit":
2825
+ case "MultiEdit":
2826
+ return "\u6587\u4EF6\u5904\u7406";
2827
+ case "Grep":
2828
+ case "Glob":
2829
+ return "\u5185\u5BB9\u641C\u7D22";
2830
+ case "Agent":
2831
+ return "\u5B50\u4EFB\u52A1";
2832
+ case "search_skills":
2833
+ return "\u6280\u80FD\u68C0\u7D22";
2834
+ case "get_skill_content":
2835
+ return "\u8BFB\u53D6\u6280\u80FD";
2836
+ case "run_skill_tool":
2837
+ return "\u6267\u884C\u6280\u80FD";
2838
+ default:
2839
+ return toolCall.display_name?.trim() || "\u6267\u884C\u6B65\u9AA4";
2840
+ }
2841
+ }
2842
+ function executionToolIntent(toolCall) {
2843
+ const normalizedName = formatToolName(toolCall.name);
2844
+ let args = null;
2845
+ try {
2846
+ const parsed = JSON.parse(toolCall.arguments);
2847
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2848
+ } catch {
2849
+ args = null;
2850
+ }
2851
+ const getString = (key) => {
2852
+ const value = args?.[key];
2853
+ return typeof value === "string" ? value.trim() : "";
2854
+ };
2855
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || toolCall.display_name?.trim() || "";
2856
+ if (explicitIntent) return explicitIntent;
2857
+ if (normalizedName === "search_skills") return getString("query");
2858
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
2859
+ return getString("skill_name") || getString("skill");
2860
+ }
2861
+ if (normalizedName === "FinishTask") return getString("title");
2862
+ return "";
2863
+ }
2864
+ function ExecutionToolRow({ toolCall }) {
2865
+ const normalizedName = formatToolName(toolCall.name);
2866
+ const typeLabel = executionToolTypeLabel(toolCall);
2867
+ const intent = executionToolIntent(toolCall);
2868
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
2869
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
2870
+ const filePath = toolCall.status === "done" && (normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit") ? extractToolFilePath(toolCall) : null;
2871
+ const [fileOpen, setFileOpen] = useState10(false);
2872
+ const iconClass = cn(
2873
+ "size-3.5 shrink-0",
2874
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2875
+ );
2876
+ 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" ? /* @__PURE__ */ jsx11(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Grep" || normalizedName === "Glob" || normalizedName === "search_skills" ? /* @__PURE__ */ jsx11(Search, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "get_skill_content" ? /* @__PURE__ */ jsx11(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Agent" ? /* @__PURE__ */ jsx11(Bot, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Wrench, { className: iconClass, "aria-hidden": "true" });
2877
+ const rowClassName = cn(
2878
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
2879
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2880
+ );
2881
+ if (!filePath) {
2882
+ return /* @__PURE__ */ jsxs9("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
2883
+ icon,
2884
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label })
2885
+ ] });
2886
+ }
2887
+ return /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
2888
+ /* @__PURE__ */ jsxs9(
2889
+ "button",
2890
+ {
2891
+ type: "button",
2892
+ "data-testid": "execution-tool-intent",
2893
+ className: cn(rowClassName, "w-full text-left"),
2894
+ title: label,
2895
+ "aria-expanded": fileOpen,
2896
+ onClick: () => setFileOpen((open) => !open),
2897
+ children: [
2898
+ icon,
2899
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label }),
2900
+ /* @__PURE__ */ jsx11(
2901
+ ChevronRight,
2902
+ {
2903
+ size: 14,
2904
+ className: cn("shrink-0 transition-transform", fileOpen && "rotate-90"),
2905
+ "aria-hidden": "true"
2906
+ }
2907
+ )
2908
+ ]
2909
+ }
2910
+ ),
2911
+ fileOpen ? /* @__PURE__ */ jsxs9(
2912
+ "div",
2913
+ {
2914
+ className: "ml-[18px] truncate text-xs leading-[22px] text-[hsl(var(--muted-foreground))]",
2915
+ title: filePath,
2916
+ children: [
2917
+ "\u6587\u4EF6\uFF1A",
2918
+ filePath
2919
+ ]
2920
+ }
2921
+ ) : null
2922
+ ] });
2923
+ }
2410
2924
  function AssistantTurnBlock({
2411
2925
  messages,
2412
2926
  isStreaming = false,
@@ -2417,53 +2931,235 @@ function AssistantTurnBlock({
2417
2931
  sessionId
2418
2932
  }) {
2419
2933
  const hasInterrupted = messages.some((message) => message.status === "interrupted");
2420
- const hasAnyContent = messages.some(
2421
- (message) => getMessageText(message) || message.reasoning || (message.tool_calls?.length ?? 0) > 0
2934
+ const hasFailedWithoutContent = messages.some(
2935
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
2936
+ );
2937
+ const finalMessage = getLastContentMessage(messages);
2938
+ const hasExecutionProcess = messages.some(
2939
+ (message) => message.reasoning || (message.tool_calls?.length ?? 0) > 0
2422
2940
  );
2423
2941
  const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
2942
+ const hasActionableToolCall = messages.some(
2943
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
2944
+ (toolCall) => toolCall.status === "error" || toolCall.status === "cancelled"
2945
+ )
2946
+ );
2947
+ const questionToolCalls = messages.flatMap(
2948
+ (message) => (message.tool_calls ?? []).filter(
2949
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
2950
+ )
2951
+ );
2952
+ const [displayMode, setDisplayMode] = useState10(
2953
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
2954
+ );
2955
+ const userSelectedDisplayModeRef = useRef9(false);
2956
+ const wasStreamingRef = useRef9(isStreaming);
2957
+ useEffect8(() => {
2958
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
2959
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
2960
+ }
2961
+ wasStreamingRef.current = isStreaming;
2962
+ }, [hasActionableToolCall, isStreaming]);
2963
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
2964
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
2965
+ const [clock, setClock] = useState10(() => Date.now());
2966
+ const hasLiveStartTime = messages.some(
2967
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
2968
+ );
2969
+ useEffect8(() => {
2970
+ if (!isStreaming || !hasLiveStartTime) return;
2971
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
2972
+ return () => window.clearInterval(timer);
2973
+ }, [hasLiveStartTime, isStreaming]);
2974
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
2975
+ if (!hasExecutionProcess) {
2976
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2977
+ 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" }),
2978
+ 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" }),
2979
+ messages.map((message, index) => {
2980
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx11(
2981
+ "div",
2982
+ {
2983
+ className: "flex flex-col gap-3",
2984
+ children: /* @__PURE__ */ jsx11(
2985
+ AssistantMessageContent,
2986
+ {
2987
+ message,
2988
+ sessionId,
2989
+ streaming: isStreaming && index === messages.length - 1
2990
+ }
2991
+ )
2992
+ },
2993
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2994
+ ) : null;
2995
+ }),
2996
+ isStreaming && !finalMessage ? /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." }) : null
2997
+ ] });
2998
+ }
2424
2999
  return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2425
3000
  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" }),
2426
- messages.map((message, index) => {
3001
+ 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" }),
3002
+ /* @__PURE__ */ jsxs9("div", { className: "flex w-full items-start gap-2.5", children: [
3003
+ /* @__PURE__ */ jsx11(
3004
+ "span",
3005
+ {
3006
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3007
+ "aria-hidden": "true",
3008
+ children: /* @__PURE__ */ jsx11(Bot, { size: 16 })
3009
+ }
3010
+ ),
3011
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3012
+ /* @__PURE__ */ jsxs9(
3013
+ "button",
3014
+ {
3015
+ type: "button",
3016
+ onClick: () => {
3017
+ userSelectedDisplayModeRef.current = true;
3018
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
3019
+ },
3020
+ "aria-expanded": effectiveMode === "detail",
3021
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3022
+ "data-testid": "assistant-execution-summary",
3023
+ 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",
3024
+ children: [
3025
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3026
+ messages,
3027
+ isStreaming,
3028
+ durationMs: liveExecutionDurationMs,
3029
+ sessionStatus,
3030
+ askAnswers
3031
+ }) }),
3032
+ /* @__PURE__ */ jsx11(
3033
+ ChevronRight,
3034
+ {
3035
+ size: 14,
3036
+ className: cn(
3037
+ "shrink-0 transition-transform duration-300",
3038
+ effectiveMode === "detail" && "rotate-90"
3039
+ ),
3040
+ "aria-hidden": "true"
3041
+ }
3042
+ )
3043
+ ]
3044
+ }
3045
+ ),
3046
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3047
+ ] })
3048
+ ] }),
3049
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx11("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
2427
3050
  const isLast = index === messages.length - 1;
2428
3051
  const streamingThis = isStreaming && isLast;
2429
3052
  const text = getMessageText(message);
2430
- const toolCalls = message.tool_calls ?? [];
3053
+ const toolCalls = (message.tool_calls ?? []).filter(
3054
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
3055
+ );
2431
3056
  const showReasoning = !!message.reasoning && (!isStreaming || index === latestReasoningIndex);
2432
3057
  return /* @__PURE__ */ jsxs9(
2433
3058
  "div",
2434
3059
  {
2435
3060
  className: "flex flex-col gap-3",
2436
3061
  children: [
2437
- showReasoning && message.reasoning && /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }),
2438
- text && /* @__PURE__ */ jsx11("div", { className: "blade-chat-assistant-text text-[15px] leading-8 text-[hsl(var(--foreground))]", children: /* @__PURE__ */ jsx11(
2439
- MarkdownContent,
3062
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3063
+ hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx11(
3064
+ AssistantMessageContent,
2440
3065
  {
2441
- mode: streamingThis ? "streaming" : "static",
2442
- className: "blade-chat-prose",
3066
+ message,
2443
3067
  sessionId,
2444
- children: text
3068
+ streaming: streamingThis,
3069
+ compact: true
2445
3070
  }
2446
- ) }),
2447
- toolCalls.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-2", children: toolCalls.map(
2448
- (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(
2449
- ToolCallBlock,
2450
- {
2451
- toolCall,
2452
- answerData: askAnswers?.[toolCall.id],
2453
- onAnswer,
2454
- answered: sessionStatus !== "waiting_for_input",
2455
- sessionStatus,
2456
- renderer: toolCallRenderer
2457
- },
2458
- toolCall.id
2459
- )
2460
- ) })
3071
+ ) : null,
3072
+ toolCalls.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3073
+ const custom = toolCallRenderer?.(toolCall);
3074
+ 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);
3075
+ }) }) : null
2461
3076
  ]
2462
3077
  },
2463
3078
  message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2464
3079
  );
2465
- }),
2466
- isStreaming && !hasAnyContent && /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
3080
+ }) }) : null,
3081
+ finalMessage ? /* @__PURE__ */ jsx11("div", { className: "ml-10", children: /* @__PURE__ */ jsx11(
3082
+ AssistantMessageContent,
3083
+ {
3084
+ message: finalMessage,
3085
+ sessionId,
3086
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3087
+ }
3088
+ ) }) : null,
3089
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx11(
3090
+ ToolCallBlock,
3091
+ {
3092
+ toolCall,
3093
+ answerData: askAnswers?.[toolCall.id],
3094
+ onAnswer,
3095
+ answered: sessionStatus !== "waiting_for_input",
3096
+ sessionStatus,
3097
+ renderer: toolCallRenderer
3098
+ },
3099
+ toolCall.id
3100
+ ))
3101
+ ] });
3102
+ }
3103
+ function AssistantMessageContent({
3104
+ message,
3105
+ sessionId,
3106
+ streaming,
3107
+ compact = false
3108
+ }) {
3109
+ const text = getMessageText(message);
3110
+ const imageParts = getImageParts(message.content);
3111
+ const fileParts = getFileParts(message.content);
3112
+ const failed = message.status === "failed";
3113
+ 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;
3114
+ const textContent = text ? /* @__PURE__ */ jsx11(
3115
+ "div",
3116
+ {
3117
+ className: cn(
3118
+ "blade-chat-assistant-text",
3119
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3120
+ ),
3121
+ children: /* @__PURE__ */ jsx11(
3122
+ MarkdownContent,
3123
+ {
3124
+ mode: streaming ? "streaming" : "static",
3125
+ className: "blade-chat-prose",
3126
+ sessionId,
3127
+ children: text
3128
+ }
3129
+ )
3130
+ }
3131
+ ) : null;
3132
+ if (imageParts.length === 0 && fileParts.length === 0) {
3133
+ if (!failed) return textContent;
3134
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
3135
+ failedBadge,
3136
+ textContent
3137
+ ] }) : null;
3138
+ }
3139
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3", children: [
3140
+ failedBadge,
3141
+ imageParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx11(
3142
+ "img",
3143
+ {
3144
+ src: part.image_url.url,
3145
+ alt: "\u6D88\u606F\u9644\u4EF6",
3146
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3147
+ },
3148
+ part.image_url.url
3149
+ )) }) : null,
3150
+ fileParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs9(
3151
+ "div",
3152
+ {
3153
+ 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))]",
3154
+ title: part.name,
3155
+ children: [
3156
+ /* @__PURE__ */ jsx11(FileText, { size: 12, className: "shrink-0" }),
3157
+ /* @__PURE__ */ jsx11("span", { className: "max-w-56 truncate", children: part.name })
3158
+ ]
3159
+ },
3160
+ `${part.name}-${part.data.slice(0, 32)}`
3161
+ )) }) : null,
3162
+ textContent
2467
3163
  ] });
2468
3164
  }
2469
3165
 
@@ -2560,7 +3256,7 @@ var RenderErrorBoundary = class extends Component {
2560
3256
  };
2561
3257
 
2562
3258
  // src/components/PostChatFollowupBlock.tsx
2563
- import { useCallback as useCallback5, useEffect as useEffect7, useRef as useRef8, useState as useState11 } from "react";
3259
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef10, useState as useState11 } from "react";
2564
3260
  import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2565
3261
  function emitInteraction(callback, event) {
2566
3262
  try {
@@ -2691,10 +3387,10 @@ function ResultFeedback({
2691
3387
  const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
2692
3388
  const [saving, setSaving] = useState11(false);
2693
3389
  const [saveError, setSaveError] = useState11(false);
2694
- const reportedShown = useRef8(false);
2695
- const latestChoice = useRef8(null);
3390
+ const reportedShown = useRef10(false);
3391
+ const latestChoice = useRef10(null);
2696
3392
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2697
- useEffect7(() => {
3393
+ useEffect9(() => {
2698
3394
  if (!eligible || reportedShown.current) return;
2699
3395
  reportedShown.current = true;
2700
3396
  emitInteraction(onInteraction, {
@@ -2703,13 +3399,13 @@ function ResultFeedback({
2703
3399
  assistantEntryId: followup.assistant_entry_id
2704
3400
  });
2705
3401
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2706
- useEffect7(() => {
3402
+ useEffect9(() => {
2707
3403
  if (!savedFeedback || latestChoice.current) return;
2708
3404
  setSaved(savedFeedback);
2709
3405
  setHelpful(savedFeedback.helpful);
2710
3406
  setReason(savedFeedback.reason);
2711
3407
  }, [savedFeedback]);
2712
- const submit = useCallback5(
3408
+ const submit = useCallback6(
2713
3409
  async (nextHelpful, nextReason) => {
2714
3410
  if (!sessionId) return;
2715
3411
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2816,13 +3512,13 @@ function PostChatFollowupBlock({
2816
3512
  onFeedbackSaved
2817
3513
  }) {
2818
3514
  const [expanded, setExpanded] = useState11(false);
2819
- const adopted = useRef8(/* @__PURE__ */ new Set());
2820
- const reportedSuggestions = useRef8(false);
2821
- const reportedArtifacts = useRef8(/* @__PURE__ */ new Set());
2822
- const openedArtifacts = useRef8(/* @__PURE__ */ new Set());
3515
+ const adopted = useRef10(/* @__PURE__ */ new Set());
3516
+ const reportedSuggestions = useRef10(false);
3517
+ const reportedArtifacts = useRef10(/* @__PURE__ */ new Set());
3518
+ const openedArtifacts = useRef10(/* @__PURE__ */ new Set());
2823
3519
  const artifacts = followup.final_artifacts ?? [];
2824
3520
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2825
- useEffect7(() => {
3521
+ useEffect9(() => {
2826
3522
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2827
3523
  reportedSuggestions.current = true;
2828
3524
  emitInteraction(onInteraction, {
@@ -2852,7 +3548,7 @@ function PostChatFollowupBlock({
2852
3548
  sessionId,
2853
3549
  visibleArtifacts
2854
3550
  ]);
2855
- const reportArtifactOpened = useCallback5(
3551
+ const reportArtifactOpened = useCallback6(
2856
3552
  (artifactIndex, artifactKind) => {
2857
3553
  if (openedArtifacts.current.has(artifactIndex)) return;
2858
3554
  openedArtifacts.current.add(artifactIndex);
@@ -2946,7 +3642,7 @@ function PostChatFollowupBlock({
2946
3642
  }
2947
3643
 
2948
3644
  // src/components/UserMessageBubble.tsx
2949
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
3645
+ import { getFileParts as getFileParts2, getImageParts as getImageParts2, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
2950
3646
  import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2951
3647
  function isUserMessage(message) {
2952
3648
  return message.role === "user";
@@ -2957,8 +3653,8 @@ function isErrorMessage(message) {
2957
3653
  var isSending = (message) => message.status === "streaming";
2958
3654
  function UserMessageBubble({ message, className }) {
2959
3655
  const text = getTextContent2(message.content).trim();
2960
- const fileParts = getFileParts(message.content);
2961
- const imageParts = getImageParts(message.content);
3656
+ const fileParts = getFileParts2(message.content);
3657
+ const imageParts = getImageParts2(message.content);
2962
3658
  return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs13("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
2963
3659
  imageParts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx15(
2964
3660
  "img",
@@ -3045,6 +3741,9 @@ function MessageList({
3045
3741
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
3046
3742
  onResultFeedbackSaved
3047
3743
  }) {
3744
+ const userMessages = messages.filter((message) => isUserMessage(message));
3745
+ const latestUserMessage = userMessages.at(-1);
3746
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
3048
3747
  const renderBlocks = useMemo7(() => {
3049
3748
  const visible = messages.filter((message) => {
3050
3749
  if ((message.loop_name ?? "root") !== "root") return false;
@@ -3128,101 +3827,151 @@ function MessageList({
3128
3827
  }
3129
3828
  return blocks;
3130
3829
  }, [messages, isStreaming]);
3131
- return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs14(StickToBottom, { className: "h-full overflow-y-hidden", initial: "instant", resize: "instant", children: [
3132
- /* @__PURE__ */ jsx16(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsx16("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 flex-col", children: [
3133
- renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs14("div", { className: "blade-chat-empty", children: [
3134
- /* @__PURE__ */ jsx16(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3135
- /* @__PURE__ */ jsx16("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3136
- /* @__PURE__ */ jsx16("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3137
- ] }) : renderBlocks.map((block) => {
3138
- if (block.type === "message") {
3139
- return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx16(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx16(ErrorMessageBlock, { message: block.message }) : null }, block.key);
3140
- }
3141
- if (block.type === "assistant_turn") {
3142
- const blockFeedback = block.messages.map(
3143
- (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3144
- ).find((feedback) => feedback != null);
3145
- const hasActiveFollowup = Boolean(
3146
- postChatFollowup && block.messages.some(
3147
- (message) => message.entry_id === postChatFollowup.assistant_entry_id
3148
- )
3149
- );
3150
- return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs14(
3151
- RenderErrorBoundary,
3152
- {
3153
- label: "\u52A9\u624B\u6D88\u606F",
3154
- details: block.key,
3155
- resetKey: getMessageResetSignature(block.messages),
3156
- children: [
3157
- /* @__PURE__ */ jsx16(
3158
- AssistantTurnBlock,
3159
- {
3160
- messages: block.messages,
3161
- isStreaming: block.isStreaming,
3162
- askAnswers,
3163
- onAnswer,
3164
- sessionStatus,
3165
- toolCallRenderer,
3166
- sessionId
3167
- }
3168
- ),
3169
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx16(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3170
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx16(
3171
- PostChatFollowupBlock,
3172
- {
3173
- followup: postChatFollowup,
3174
- sessionId,
3175
- onSuggestion,
3176
- isViewer,
3177
- onInteraction: onFollowupInteraction,
3178
- savedFeedback: blockFeedback,
3179
- onFeedbackSaved: onResultFeedbackSaved
3180
- }
3181
- ) : null
3182
- ]
3830
+ return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs14(
3831
+ StickToBottom,
3832
+ {
3833
+ className: "h-full overflow-y-hidden",
3834
+ initial: "instant",
3835
+ resize: "instant",
3836
+ children: [
3837
+ /* @__PURE__ */ jsx16(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsx16("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs14("div", { className: "flex min-w-0 flex-col", children: [
3838
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs14("div", { className: "blade-chat-empty", children: [
3839
+ /* @__PURE__ */ jsx16(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3840
+ /* @__PURE__ */ jsx16("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3841
+ /* @__PURE__ */ jsx16("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3842
+ ] }) : renderBlocks.map((block) => {
3843
+ if (block.type === "message") {
3844
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx16(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx16(ErrorMessageBlock, { message: block.message }) : null }, block.key);
3183
3845
  }
3184
- ) }, block.key);
3185
- }
3186
- if (block.type === "context") {
3187
- return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.message.entry_id, children: /* @__PURE__ */ jsx16(ContextCard, { context: block.message.context }) }, block.key);
3188
- }
3189
- if (block.type === "compaction") {
3190
- return /* @__PURE__ */ jsxs14(
3191
- "div",
3192
- {
3193
- className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3194
- children: [
3195
- /* @__PURE__ */ jsx16(Layers, { size: 12 }),
3196
- /* @__PURE__ */ jsx16("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3197
- ]
3198
- },
3199
- block.key
3200
- );
3201
- }
3202
- return /* @__PURE__ */ jsx16(PlanningDivider, { kind: block.kind }, block.key);
3203
- }),
3204
- sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx16("div", { className: "flex", children: /* @__PURE__ */ jsx16("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
3205
- ] }) }) }),
3206
- /* @__PURE__ */ jsx16(AutoScrollOnUserSend, { userMessageCount: messages.filter((m) => isUserMessage(m)).length }),
3207
- /* @__PURE__ */ jsx16(ScrollToBottomButton, {})
3208
- ] }) });
3846
+ if (block.type === "assistant_turn") {
3847
+ const blockFeedback = block.messages.map(
3848
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3849
+ ).find((feedback) => feedback != null);
3850
+ const hasActiveFollowup = Boolean(
3851
+ postChatFollowup && block.messages.some(
3852
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
3853
+ )
3854
+ );
3855
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs14(
3856
+ RenderErrorBoundary,
3857
+ {
3858
+ label: "\u52A9\u624B\u6D88\u606F",
3859
+ details: block.key,
3860
+ resetKey: getMessageResetSignature(block.messages),
3861
+ children: [
3862
+ /* @__PURE__ */ jsx16(
3863
+ AssistantTurnBlock,
3864
+ {
3865
+ messages: block.messages,
3866
+ isStreaming: block.isStreaming,
3867
+ askAnswers,
3868
+ onAnswer,
3869
+ sessionStatus,
3870
+ toolCallRenderer,
3871
+ sessionId
3872
+ }
3873
+ ),
3874
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx16(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3875
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx16(
3876
+ PostChatFollowupBlock,
3877
+ {
3878
+ followup: postChatFollowup,
3879
+ sessionId,
3880
+ onSuggestion,
3881
+ isViewer,
3882
+ onInteraction: onFollowupInteraction,
3883
+ savedFeedback: blockFeedback,
3884
+ onFeedbackSaved: onResultFeedbackSaved
3885
+ }
3886
+ ) : null
3887
+ ]
3888
+ }
3889
+ ) }, block.key);
3890
+ }
3891
+ if (block.type === "context") {
3892
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.message.entry_id, children: /* @__PURE__ */ jsx16(ContextCard, { context: block.message.context }) }, block.key);
3893
+ }
3894
+ if (block.type === "compaction") {
3895
+ return /* @__PURE__ */ jsxs14(
3896
+ "div",
3897
+ {
3898
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3899
+ children: [
3900
+ /* @__PURE__ */ jsx16(Layers, { size: 12 }),
3901
+ /* @__PURE__ */ jsx16("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3902
+ ]
3903
+ },
3904
+ block.key
3905
+ );
3906
+ }
3907
+ return /* @__PURE__ */ jsx16(PlanningDivider, { kind: block.kind }, block.key);
3908
+ }),
3909
+ sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx16("div", { className: "flex", children: /* @__PURE__ */ jsx16("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
3910
+ ] }) }) }),
3911
+ /* @__PURE__ */ jsx16(
3912
+ PinLatestUserMessage,
3913
+ {
3914
+ userMessageCount: userMessages.length,
3915
+ shouldPinLatestUser,
3916
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3917
+ },
3918
+ sessionId ?? "no-session"
3919
+ ),
3920
+ /* @__PURE__ */ jsx16(ScrollToBottomButton, {})
3921
+ ]
3922
+ },
3923
+ sessionId ?? "no-session"
3924
+ ) });
3209
3925
  }
3210
- function AutoScrollOnUserSend({ userMessageCount }) {
3211
- const { scrollToBottom } = useStickToBottomContext();
3212
- const previousCountRef = useRef9(userMessageCount);
3213
- useEffect8(() => {
3214
- 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) {
3215
3964
  scrollToBottom("instant");
3216
3965
  }
3217
3966
  previousCountRef.current = userMessageCount;
3218
- }, [userMessageCount, scrollToBottom]);
3967
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3219
3968
  return null;
3220
3969
  }
3221
3970
  function ScrollToBottomButton() {
3222
3971
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3223
3972
  const [visible, setVisible] = useState12(false);
3224
- const hideTimerRef = useRef9(null);
3225
- useEffect8(() => {
3973
+ const hideTimerRef = useRef11(null);
3974
+ useEffect10(() => {
3226
3975
  if (isAtBottom) {
3227
3976
  if (!hideTimerRef.current) {
3228
3977
  hideTimerRef.current = setTimeout(() => {
@@ -3244,7 +3993,7 @@ function ScrollToBottomButton() {
3244
3993
  }
3245
3994
  };
3246
3995
  }, [isAtBottom]);
3247
- const handleClick = useCallback6(() => {
3996
+ const handleClick = useCallback7(() => {
3248
3997
  if (hideTimerRef.current) {
3249
3998
  clearTimeout(hideTimerRef.current);
3250
3999
  hideTimerRef.current = null;
@@ -3454,7 +4203,7 @@ function ChatSessionView({
3454
4203
  const [resultFeedback, setResultFeedback] = useState13([]);
3455
4204
  const resolvedSessionId = session?.sessionId;
3456
4205
  const isViewer = state?.viewerRole === "viewer";
3457
- useEffect9(() => {
4206
+ useEffect11(() => {
3458
4207
  setResultFeedback([]);
3459
4208
  if (!resolvedSessionId || isViewer) return;
3460
4209
  let cancelled = false;
@@ -3483,18 +4232,18 @@ function ChatSessionView({
3483
4232
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3484
4233
  [resultFeedback]
3485
4234
  );
3486
- const handleResultFeedbackSaved = useCallback7((saved) => {
4235
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3487
4236
  setResultFeedback((current) => [
3488
4237
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3489
4238
  saved
3490
4239
  ]);
3491
4240
  }, []);
3492
- useEffect9(() => {
4241
+ useEffect11(() => {
3493
4242
  if (session) {
3494
4243
  onSessionReady?.(session);
3495
4244
  }
3496
4245
  }, [session, onSessionReady]);
3497
- useEffect9(() => {
4246
+ useEffect11(() => {
3498
4247
  if (!session) return;
3499
4248
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3500
4249
  setInputText((prev) => `${prev ? `${prev}
@@ -3510,12 +4259,12 @@ ${content}`);
3510
4259
  offInsert();
3511
4260
  };
3512
4261
  }, [session]);
3513
- useEffect9(() => {
4262
+ useEffect11(() => {
3514
4263
  if (isUnauthorizedError(error)) {
3515
4264
  onUnauthorized();
3516
4265
  }
3517
4266
  }, [error, onUnauthorized]);
3518
- useEffect9(() => {
4267
+ useEffect11(() => {
3519
4268
  if (!session || !commands) return;
3520
4269
  const unsubscribes = Object.entries(commands).map(
3521
4270
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3586,7 +4335,7 @@ ${content}`);
3586
4335
  }
3587
4336
 
3588
4337
  // src/components/LlmChat.tsx
3589
- import { useEffect as useEffect10, useMemo as useMemo9, useState as useState15 } from "react";
4338
+ import { useEffect as useEffect12, useMemo as useMemo9, useState as useState15 } from "react";
3590
4339
 
3591
4340
  // src/components/LlmAdvancedSettings.tsx
3592
4341
  import { useState as useState14 } from "react";
@@ -3734,7 +4483,7 @@ ${text}` : text),
3734
4483
  }),
3735
4484
  [send, reset]
3736
4485
  );
3737
- useEffect10(() => {
4486
+ useEffect12(() => {
3738
4487
  onReady?.(handle);
3739
4488
  }, [handle, onReady]);
3740
4489
  return /* @__PURE__ */ jsx20(
@@ -3817,6 +4566,7 @@ export {
3817
4566
  useAgentSession,
3818
4567
  useBladeClient,
3819
4568
  useLlmChat,
4569
+ useMessagePin,
3820
4570
  useReplay
3821
4571
  };
3822
4572
  /*! Bundled license information:
@@ -3828,13 +4578,15 @@ lucide-react/dist/esm/createLucideIcon.js:
3828
4578
  lucide-react/dist/esm/icons/arrow-right.js:
3829
4579
  lucide-react/dist/esm/icons/arrow-up-right.js:
3830
4580
  lucide-react/dist/esm/icons/arrow-up.js:
4581
+ lucide-react/dist/esm/icons/book-open.js:
3831
4582
  lucide-react/dist/esm/icons/bot.js:
3832
- lucide-react/dist/esm/icons/brain.js:
3833
4583
  lucide-react/dist/esm/icons/check.js:
3834
4584
  lucide-react/dist/esm/icons/chevron-down.js:
3835
4585
  lucide-react/dist/esm/icons/chevron-right.js:
3836
4586
  lucide-react/dist/esm/icons/circle-alert.js:
3837
4587
  lucide-react/dist/esm/icons/copy.js:
4588
+ lucide-react/dist/esm/icons/earth.js:
4589
+ lucide-react/dist/esm/icons/file-pen-line.js:
3838
4590
  lucide-react/dist/esm/icons/file-text.js:
3839
4591
  lucide-react/dist/esm/icons/globe.js:
3840
4592
  lucide-react/dist/esm/icons/layers.js:
@@ -3844,10 +4596,13 @@ lucide-react/dist/esm/icons/lock-keyhole.js:
3844
4596
  lucide-react/dist/esm/icons/message-square-more.js:
3845
4597
  lucide-react/dist/esm/icons/message-square.js:
3846
4598
  lucide-react/dist/esm/icons/play.js:
4599
+ lucide-react/dist/esm/icons/search.js:
3847
4600
  lucide-react/dist/esm/icons/settings-2.js:
3848
4601
  lucide-react/dist/esm/icons/sparkles.js:
3849
4602
  lucide-react/dist/esm/icons/square.js:
4603
+ lucide-react/dist/esm/icons/terminal.js:
3850
4604
  lucide-react/dist/esm/icons/triangle-alert.js:
4605
+ lucide-react/dist/esm/icons/wrench.js:
3851
4606
  lucide-react/dist/esm/icons/x.js:
3852
4607
  lucide-react/dist/esm/lucide-react.js:
3853
4608
  (**