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

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,20 @@ 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/refresh-ccw.js
1027
+ var RefreshCcw = createLucideIcon("RefreshCcw", [
1028
+ ["path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8", key: "14sxne" }],
1029
+ ["path", { d: "M3 3v5h5", key: "1xhq8a" }],
1030
+ ["path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16", key: "1hlbsb" }],
1031
+ ["path", { d: "M16 16h5v5", key: "ccwih5" }]
1032
+ ]);
1033
+
1034
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/search.js
1035
+ var Search = createLucideIcon("Search", [
1036
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
1037
+ ["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
1038
+ ]);
1039
+
809
1040
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
810
1041
  var Settings2 = createLucideIcon("Settings2", [
811
1042
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -834,6 +1065,12 @@ var Square = createLucideIcon("Square", [
834
1065
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
835
1066
  ]);
836
1067
 
1068
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/terminal.js
1069
+ var Terminal = createLucideIcon("Terminal", [
1070
+ ["polyline", { points: "4 17 10 11 4 5", key: "akl6gq" }],
1071
+ ["line", { x1: "12", x2: "20", y1: "19", y2: "19", key: "q2wloq" }]
1072
+ ]);
1073
+
837
1074
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
838
1075
  var TriangleAlert = createLucideIcon("TriangleAlert", [
839
1076
  [
@@ -847,6 +1084,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
847
1084
  ["path", { d: "M12 17h.01", key: "p32p05" }]
848
1085
  ]);
849
1086
 
1087
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/wrench.js
1088
+ var Wrench = createLucideIcon("Wrench", [
1089
+ [
1090
+ "path",
1091
+ {
1092
+ 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",
1093
+ key: "cbrjhi"
1094
+ }
1095
+ ]
1096
+ ]);
1097
+
850
1098
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
851
1099
  var X = createLucideIcon("X", [
852
1100
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -854,7 +1102,7 @@ var X = createLucideIcon("X", [
854
1102
  ]);
855
1103
 
856
1104
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState12 } from "react";
1105
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo8, useState as useState13 } from "react";
858
1106
 
859
1107
  // src/lib/utils.ts
860
1108
  function cn(...inputs) {
@@ -981,8 +1229,17 @@ function ReplayMismatchPrompt({ mismatch, className }) {
981
1229
  );
982
1230
  }
983
1231
 
1232
+ // src/components/ChatSurface.tsx
1233
+ import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1234
+
984
1235
  // src/components/ChatInput.tsx
985
1236
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1237
+ function isImeCompositionKey(event) {
1238
+ return event.isComposing || event.keyCode === 229;
1239
+ }
1240
+ function shouldSubmitChatInput(event, menuOpen) {
1241
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1242
+ }
986
1243
  function ChatInput({
987
1244
  value,
988
1245
  onValueChange,
@@ -1002,7 +1259,12 @@ function ChatInput({
1002
1259
  onValueChange("");
1003
1260
  };
1004
1261
  const handleKeyDown = (event) => {
1005
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1262
+ if (shouldSubmitChatInput({
1263
+ key: event.key,
1264
+ shiftKey: event.shiftKey,
1265
+ isComposing: event.nativeEvent.isComposing,
1266
+ keyCode: event.nativeEvent.keyCode
1267
+ }, false)) {
1006
1268
  event.preventDefault();
1007
1269
  void handleSend();
1008
1270
  }
@@ -1052,24 +1314,74 @@ function ChatInput({
1052
1314
  }
1053
1315
 
1054
1316
  // src/components/ConnectionBanner.tsx
1317
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1055
1318
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1319
+ var CONNECTION_NOTICE_DELAY_MS = 3e3;
1320
+ var CONNECTION_ERROR_DELAY_MS = 15e3;
1321
+ function useConnectionNoticePhase(connected) {
1322
+ const [phase, setPhase] = useState4("hidden");
1323
+ const connectedRef = useRef4(connected);
1324
+ const timersRef = useRef4([]);
1325
+ connectedRef.current = connected;
1326
+ useEffect4(() => {
1327
+ const clearTimers = () => {
1328
+ for (const timer of timersRef.current) clearTimeout(timer);
1329
+ timersRef.current = [];
1330
+ };
1331
+ const startGracePeriod = () => {
1332
+ clearTimers();
1333
+ setPhase("hidden");
1334
+ timersRef.current = [
1335
+ setTimeout(() => setPhase("recovering"), CONNECTION_NOTICE_DELAY_MS),
1336
+ setTimeout(() => setPhase("failed"), CONNECTION_ERROR_DELAY_MS)
1337
+ ];
1338
+ };
1339
+ if (connected) {
1340
+ clearTimers();
1341
+ setPhase("hidden");
1342
+ } else {
1343
+ startGracePeriod();
1344
+ }
1345
+ const handleForeground = () => {
1346
+ if (!connectedRef.current) startGracePeriod();
1347
+ };
1348
+ const handleVisibilityChange = () => {
1349
+ if (document.visibilityState === "visible") handleForeground();
1350
+ };
1351
+ window.addEventListener("blade:app-active", handleForeground);
1352
+ window.addEventListener("focus", handleForeground);
1353
+ window.addEventListener("pageshow", handleForeground);
1354
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1355
+ return () => {
1356
+ clearTimers();
1357
+ window.removeEventListener("blade:app-active", handleForeground);
1358
+ window.removeEventListener("focus", handleForeground);
1359
+ window.removeEventListener("pageshow", handleForeground);
1360
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1361
+ };
1362
+ }, [connected]);
1363
+ return phase;
1364
+ }
1056
1365
  function ConnectionBanner({ connection, className }) {
1057
- if (connection === "connected" || connection === "connecting") {
1058
- return null;
1059
- }
1060
- const reconnecting = connection === "reconnecting";
1366
+ const hasConnectedRef = useRef4(connection === "connected" || connection === "reconnecting");
1367
+ if (connection === "connected") hasConnectedRef.current = true;
1368
+ const connected = connection === "connected";
1369
+ const phase = useConnectionNoticePhase(connected);
1370
+ if (connected || phase === "hidden") return null;
1371
+ const recovering = phase === "recovering";
1372
+ const firstConnection = !hasConnectedRef.current;
1061
1373
  return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1062
1374
  "div",
1063
1375
  {
1064
1376
  className: cn(
1065
1377
  "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"
1378
+ recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1067
1379
  ),
1068
1380
  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 }) }),
1381
+ /* @__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
1382
  /* @__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" })
1383
+ /* @__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" }),
1384
+ /* @__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
1385
  ] })
1074
1386
  ]
1075
1387
  }
@@ -1078,10 +1390,10 @@ function ConnectionBanner({ connection, className }) {
1078
1390
 
1079
1391
  // src/components/MessageList.tsx
1080
1392
  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";
1393
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo7, useRef as useRef11, useState as useState12 } from "react";
1082
1394
 
1083
1395
  // ../../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";
1396
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
1085
1397
  var DEFAULT_SPRING_ANIMATION = {
1086
1398
  /**
1087
1399
  * A value from 0 to 1, on how much to damp the animation.
@@ -1118,12 +1430,12 @@ globalThis.document?.addEventListener("click", () => {
1118
1430
  mouseDown = false;
1119
1431
  });
1120
1432
  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);
1433
+ const [escapedFromLock, updateEscapedFromLock] = useState5(false);
1434
+ const [isAtBottom, updateIsAtBottom] = useState5(options.initial !== false);
1435
+ const [isNearBottom, setIsNearBottom] = useState5(false);
1436
+ const optionsRef = useRef5(null);
1125
1437
  optionsRef.current = options;
1126
- const isSelecting = useCallback3(() => {
1438
+ const isSelecting = useCallback4(() => {
1127
1439
  if (!mouseDown) {
1128
1440
  return false;
1129
1441
  }
@@ -1134,11 +1446,11 @@ var useStickToBottom = (options = {}) => {
1134
1446
  const range = selection.getRangeAt(0);
1135
1447
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1136
1448
  }, []);
1137
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1449
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1138
1450
  state.isAtBottom = isAtBottom2;
1139
1451
  updateIsAtBottom(isAtBottom2);
1140
1452
  }, []);
1141
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1453
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1142
1454
  state.escapedFromLock = escapedFromLock2;
1143
1455
  updateEscapedFromLock(escapedFromLock2);
1144
1456
  }, []);
@@ -1195,7 +1507,7 @@ var useStickToBottom = (options = {}) => {
1195
1507
  }
1196
1508
  };
1197
1509
  }, []);
1198
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1510
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1199
1511
  if (typeof scrollOptions === "string") {
1200
1512
  scrollOptions = { animation: scrollOptions };
1201
1513
  }
@@ -1280,11 +1592,11 @@ var useStickToBottom = (options = {}) => {
1280
1592
  }
1281
1593
  return next();
1282
1594
  }, [setIsAtBottom, isSelecting, state]);
1283
- const stopScroll = useCallback3(() => {
1595
+ const stopScroll = useCallback4(() => {
1284
1596
  setEscapedFromLock(true);
1285
1597
  setIsAtBottom(false);
1286
1598
  }, [setEscapedFromLock, setIsAtBottom]);
1287
- const handleScroll = useCallback3(({ target }) => {
1599
+ const handleScroll = useCallback4(({ target }) => {
1288
1600
  if (target !== scrollRef.current) {
1289
1601
  return;
1290
1602
  }
@@ -1323,7 +1635,7 @@ var useStickToBottom = (options = {}) => {
1323
1635
  }
1324
1636
  }, 1);
1325
1637
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1326
- const handleWheel = useCallback3(({ target, deltaY }) => {
1638
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1327
1639
  let element = target;
1328
1640
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1329
1641
  if (!element.parentElement) {
@@ -1393,7 +1705,7 @@ var useStickToBottom = (options = {}) => {
1393
1705
  };
1394
1706
  };
1395
1707
  function useRefCallback(callback, deps) {
1396
- const result = useCallback3((ref) => {
1708
+ const result = useCallback4((ref) => {
1397
1709
  result.current = ref;
1398
1710
  return callback(ref);
1399
1711
  }, deps);
@@ -1425,11 +1737,11 @@ function mergeAnimations(...animations) {
1425
1737
 
1426
1738
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1427
1739
  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";
1740
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef6 } from "react";
1429
1741
  var StickToBottomContext = createContext2(null);
1430
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
1742
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect5;
1431
1743
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1432
- const customTargetScrollTop = useRef4(null);
1744
+ const customTargetScrollTop = useRef6(null);
1433
1745
  const targetScrollTop = React.useCallback((target, elements) => {
1434
1746
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1435
1747
  return get?.(target, elements) ?? target;
@@ -1505,11 +1817,16 @@ function useStickToBottomContext() {
1505
1817
  }
1506
1818
 
1507
1819
  // src/components/AssistantTurnBlock.tsx
1508
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1509
- import { useState as useState9 } from "react";
1820
+ import {
1821
+ getFileParts,
1822
+ getImageParts,
1823
+ getTextContent,
1824
+ normalizeMessageContent
1825
+ } from "@blade-hq/agent-client";
1826
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState10 } from "react";
1510
1827
 
1511
1828
  // src/components/AgentLoopBlock.tsx
1512
- import { useState as useState5 } from "react";
1829
+ import { useState as useState6 } from "react";
1513
1830
 
1514
1831
  // src/components/display-utils.ts
1515
1832
  var TOOL_NAME_ALIASES = {
@@ -1525,7 +1842,9 @@ var TOOL_NAME_ALIASES = {
1525
1842
  finish_task: "FinishTask",
1526
1843
  glob: "Glob",
1527
1844
  grep: "Grep",
1845
+ kb_search: "KbSearch",
1528
1846
  ls: "Ls",
1847
+ multi_edit: "MultiEdit",
1529
1848
  read: "Read",
1530
1849
  read_skill: "ReadSkill",
1531
1850
  web_fetch: "WebFetch",
@@ -1538,9 +1857,11 @@ var TOOL_DISPLAY_LABELS = {
1538
1857
  Read: "\u8BFB\u53D6\u6587\u4EF6",
1539
1858
  Write: "\u5199\u5165\u6587\u4EF6",
1540
1859
  Edit: "\u7F16\u8F91\u6587\u4EF6",
1860
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1541
1861
  Ls: "\u5217\u51FA\u76EE\u5F55",
1542
1862
  Glob: "\u5339\u914D\u6587\u4EF6",
1543
1863
  Grep: "\u641C\u7D22\u6587\u672C",
1864
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1544
1865
  WebSearch: "\u641C\u7D22\u7F51\u9875",
1545
1866
  WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1546
1867
  Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
@@ -1563,6 +1884,17 @@ function getStringArgValue(args, key) {
1563
1884
  const value = args?.[key];
1564
1885
  return typeof value === "string" ? value.trim() : "";
1565
1886
  }
1887
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1888
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1889
+ function getSkillNameFromFilePath(filePath) {
1890
+ if (!filePath) return null;
1891
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1892
+ const fileName = segments.pop();
1893
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1894
+ const dirName = segments.pop();
1895
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1896
+ return dirName;
1897
+ }
1566
1898
  function formatToolName(name) {
1567
1899
  const trimmed = name.trim();
1568
1900
  if (!trimmed) return name;
@@ -1587,6 +1919,12 @@ function getToolDisplayLabel(toolCall) {
1587
1919
  const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1588
1920
  return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1589
1921
  }
1922
+ if (normalized === "Read") {
1923
+ const skillName = getSkillNameFromFilePath(
1924
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1925
+ );
1926
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1927
+ }
1590
1928
  if (normalized === "FinishTask") {
1591
1929
  const title = getStringArgValue(args, "title");
1592
1930
  return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
@@ -1641,83 +1979,80 @@ function parseAgentDescription(argumentsJson) {
1641
1979
  }
1642
1980
  }
1643
1981
  function AgentLoopBlock({ toolCall }) {
1644
- const [expanded, setExpanded] = useState5(false);
1982
+ const [expanded, setExpanded] = useState6(false);
1645
1983
  const description = parseAgentDescription(toolCall.arguments);
1646
1984
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1647
1985
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1648
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
1986
+ const hasResult = toolCall.result != null;
1987
+ const iconClass = cn(
1988
+ "size-3.5 shrink-0",
1989
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1990
+ );
1991
+ return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
1649
1992
  /* @__PURE__ */ jsxs5(
1650
- "div",
1993
+ "button",
1651
1994
  {
1995
+ type: "button",
1996
+ onClick: () => hasResult && setExpanded(!expanded),
1997
+ disabled: !hasResult,
1998
+ "aria-expanded": hasResult ? expanded : void 0,
1999
+ "data-testid": "execution-tool-intent",
1652
2000
  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))]"
2001
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2002
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2003
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1655
2004
  ),
2005
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1656
2006
  children: [
1657
- /* @__PURE__ */ jsxs5(
1658
- "button",
2007
+ 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" }),
2008
+ /* @__PURE__ */ jsxs5("span", { className: "min-w-0 truncate", children: [
2009
+ "\u5B50\u4EFB\u52A1\uFF1A",
2010
+ description
2011
+ ] }),
2012
+ hasResult ? /* @__PURE__ */ jsx6(
2013
+ ChevronRight,
1659
2014
  {
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
- ]
2015
+ size: 14,
2016
+ className: cn(
2017
+ "shrink-0 transition-transform duration-300",
2018
+ expanded && "rotate-90"
2019
+ ),
2020
+ "aria-hidden": "true"
1694
2021
  }
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) })
2022
+ ) : null
1697
2023
  ]
1698
2024
  }
1699
2025
  ),
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
- ] })
2026
+ 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
2027
  ] });
1705
2028
  }
1706
2029
 
1707
2030
  // src/components/MarkdownContent.tsx
1708
2031
  import {
1709
- useEffect as useEffect4,
2032
+ useEffect as useEffect6,
1710
2033
  useMemo as useMemo5,
1711
- useRef as useRef5,
1712
- useState as useState6
2034
+ useRef as useRef7,
2035
+ useState as useState7
1713
2036
  } from "react";
1714
2037
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1715
2038
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
2039
+ function normalizeAdjacentUrlFormatting(value) {
2040
+ const protectedSegments = [];
2041
+ const protectedValue = value.replace(/(`{1,3}[\s\S]*?`{1,3}|\[[^\]]*\]\([^)]*\))/g, (segment) => {
2042
+ const index = protectedSegments.push(segment) - 1;
2043
+ return `blade-url-protected-${index}-marker`;
2044
+ });
2045
+ const normalized = protectedValue.replace(
2046
+ /(\*\*|__|~~|\*|_)(https?:\/\/[^\s<>]+?)\1(?=[\s。,、!?;:,.!?;:]|$)/g,
2047
+ (_, marker, url) => `${marker}[${url}](<${url}>)${marker}`
2048
+ );
2049
+ return normalized.replace(/blade-url-protected-(\d+)-marker/g, (_, index) => protectedSegments[Number(index)]);
2050
+ }
1716
2051
  function CodeBlockPre({ children, node: _node, ...props }) {
1717
- const preRef = useRef5(null);
1718
- const [copied, setCopied] = useState6(false);
1719
- const [language, setLanguage] = useState6("");
1720
- useEffect4(() => {
2052
+ const preRef = useRef7(null);
2053
+ const [copied, setCopied] = useState7(false);
2054
+ const [language, setLanguage] = useState7("");
2055
+ useEffect6(() => {
1721
2056
  const codeEl = preRef.current?.querySelector("code");
1722
2057
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1723
2058
  }, []);
@@ -1767,7 +2102,7 @@ var MARKDOWN_COMPONENTS = {
1767
2102
  };
1768
2103
  function MarkdownContent({ children, className, mode, sessionId }) {
1769
2104
  const resolvedChildren = useMemo5(() => {
1770
- return children.replace(SYSTEM_REMINDER_RE, "");
2105
+ return normalizeAdjacentUrlFormatting(children.replace(SYSTEM_REMINDER_RE, ""));
1771
2106
  }, [children]);
1772
2107
  return /* @__PURE__ */ jsx7(
1773
2108
  _r,
@@ -1788,11 +2123,40 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1788
2123
  }
1789
2124
 
1790
2125
  // src/components/ToolCallBlock.tsx
1791
- import { useState as useState8 } from "react";
2126
+ import { useState as useState9 } from "react";
1792
2127
 
1793
2128
  // src/components/AskUserQuestionBlock.tsx
1794
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState7 } from "react";
2129
+ import { useEffect as useEffect7, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1795
2130
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2131
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2132
+ function resizeCustomTextarea(textarea) {
2133
+ textarea.style.height = "auto";
2134
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2135
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2136
+ }
2137
+ function useAutoResizeTextarea(value) {
2138
+ const textareaRef = useRef8(null);
2139
+ useEffect7(() => {
2140
+ const textarea = textareaRef.current;
2141
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2142
+ }, [value]);
2143
+ useEffect7(() => {
2144
+ const textarea = textareaRef.current;
2145
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2146
+ let previousWidth = textarea.clientWidth;
2147
+ const observer = new ResizeObserver(([entry]) => {
2148
+ if (!entry || entry.contentRect.width === previousWidth) return;
2149
+ previousWidth = entry.contentRect.width;
2150
+ resizeCustomTextarea(textarea);
2151
+ });
2152
+ observer.observe(textarea);
2153
+ return () => observer.disconnect();
2154
+ }, []);
2155
+ return textareaRef;
2156
+ }
2157
+ function indentAnswerContinuationLines(answer) {
2158
+ return answer.replaceAll("\n", "\n ");
2159
+ }
1796
2160
  function AskUserQuestionBlock({
1797
2161
  data,
1798
2162
  answered,
@@ -1801,18 +2165,19 @@ function AskUserQuestionBlock({
1801
2165
  answerData,
1802
2166
  onAnswer
1803
2167
  }) {
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(() => {
2168
+ const [selections, setSelections] = useState8(/* @__PURE__ */ new Map());
2169
+ const [customTexts, setCustomTexts] = useState8(/* @__PURE__ */ new Map());
2170
+ const [usingCustom, setUsingCustom] = useState8(/* @__PURE__ */ new Set());
2171
+ const [note, setNote] = useState8("");
2172
+ const [submitted, setSubmitted] = useState8(false);
2173
+ useEffect7(() => {
1809
2174
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1810
2175
  setSubmitted(false);
1811
2176
  }
1812
2177
  }, [sessionStatus]);
1813
2178
  const displayAnswerState = useMemo6(() => {
1814
2179
  if (!(answered && answerData)) {
1815
- return { selections, customTexts, usingCustom };
2180
+ return { selections, customTexts, usingCustom, note };
1816
2181
  }
1817
2182
  const nextSelections = /* @__PURE__ */ new Map();
1818
2183
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1828,9 +2193,10 @@ function AskUserQuestionBlock({
1828
2193
  return {
1829
2194
  selections: nextSelections,
1830
2195
  customTexts: nextCustomTexts,
1831
- usingCustom: nextUsingCustom
2196
+ usingCustom: nextUsingCustom,
2197
+ note: answerData.note ?? ""
1832
2198
  };
1833
- }, [answerData, answered, customTexts, selections, usingCustom]);
2199
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1834
2200
  const toggleOption = (qIdx, optIdx, multi) => {
1835
2201
  if (answered || submitted) return;
1836
2202
  setSelections((prev) => {
@@ -1878,6 +2244,7 @@ function AskUserQuestionBlock({
1878
2244
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1879
2245
  const handleSubmit = () => {
1880
2246
  if (answered || submitted || !allAnswered || !onAnswer) return;
2247
+ const trimmedNote = note.trim();
1881
2248
  const nextAnswerData = {
1882
2249
  selections: Object.fromEntries(
1883
2250
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1887,11 +2254,17 @@ function AskUserQuestionBlock({
1887
2254
  ),
1888
2255
  custom: Object.fromEntries(
1889
2256
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1890
- )
2257
+ ),
2258
+ ...trimmedNote ? { note: trimmedNote } : {}
1891
2259
  };
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")}`;
2260
+ const parts = data.questions.map(
2261
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2262
+ );
2263
+ const text = [
2264
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2265
+ ${parts.join("\n")}`,
2266
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2267
+ ].filter(Boolean).join("\n");
1895
2268
  setSubmitted(true);
1896
2269
  onAnswer(text, toolCallId, nextAnswerData);
1897
2270
  };
@@ -1923,6 +2296,15 @@ ${parts.join("\n")}`;
1923
2296
  },
1924
2297
  q.question
1925
2298
  )),
2299
+ /* @__PURE__ */ jsx9(
2300
+ NoteField,
2301
+ {
2302
+ answered,
2303
+ submitted,
2304
+ note: displayAnswerState.note,
2305
+ onChange: setNote
2306
+ }
2307
+ ),
1926
2308
  !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
1927
2309
  "button",
1928
2310
  {
@@ -1961,6 +2343,7 @@ function QuestionCard({
1961
2343
  onCustomChange
1962
2344
  }) {
1963
2345
  const multi = question.multiSelect ?? false;
2346
+ const customTextareaRef = useAutoResizeTextarea(customText);
1964
2347
  return /* @__PURE__ */ jsxs7("div", { children: [
1965
2348
  /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
1966
2349
  /* @__PURE__ */ jsx9(
@@ -2037,25 +2420,26 @@ function QuestionCard({
2037
2420
  "div",
2038
2421
  {
2039
2422
  className: cn(
2040
- "flex items-center gap-2 rounded-lg border transition-all",
2423
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2041
2424
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2042
2425
  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
2426
  answered && "cursor-default opacity-70"
2044
2427
  ),
2045
2428
  children: [
2046
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2429
+ /* @__PURE__ */ jsx9("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2047
2430
  /* @__PURE__ */ jsx9(
2048
- "input",
2431
+ "textarea",
2049
2432
  {
2050
- type: "text",
2433
+ ref: customTextareaRef,
2434
+ rows: 2,
2051
2435
  value: customText,
2052
- disabled: answered,
2436
+ readOnly: answered,
2053
2437
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2054
2438
  onFocus: () => onCustomFocus(qIdx),
2055
2439
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2056
2440
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2057
2441
  className: cn(
2058
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2442
+ "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
2443
  answered ? "text-xs" : "text-sm"
2060
2444
  )
2061
2445
  }
@@ -2066,6 +2450,49 @@ function QuestionCard({
2066
2450
  ] })
2067
2451
  ] });
2068
2452
  }
2453
+ function NoteField({
2454
+ answered,
2455
+ submitted,
2456
+ note,
2457
+ onChange
2458
+ }) {
2459
+ const textareaRef = useAutoResizeTextarea(note);
2460
+ const readOnly = answered || submitted;
2461
+ if (answered && !note.trim()) return null;
2462
+ return /* @__PURE__ */ jsxs7(
2463
+ "label",
2464
+ {
2465
+ className: cn(
2466
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2467
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2468
+ 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))]",
2469
+ readOnly && "cursor-default opacity-70"
2470
+ ),
2471
+ children: [
2472
+ /* @__PURE__ */ jsx9("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2473
+ /* @__PURE__ */ jsx9(
2474
+ "textarea",
2475
+ {
2476
+ ref: textareaRef,
2477
+ rows: 2,
2478
+ value: note,
2479
+ readOnly,
2480
+ onChange: (event) => {
2481
+ if (readOnly) return;
2482
+ onChange(event.target.value);
2483
+ },
2484
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2485
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2486
+ className: cn(
2487
+ "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)]",
2488
+ answered ? "text-xs" : "text-sm"
2489
+ )
2490
+ }
2491
+ )
2492
+ ]
2493
+ }
2494
+ );
2495
+ }
2069
2496
  function parseAskUserQuestion(toolResult) {
2070
2497
  if (!toolResult) return null;
2071
2498
  try {
@@ -2088,6 +2515,26 @@ function parseAskUserQuestion(toolResult) {
2088
2515
  }
2089
2516
  return null;
2090
2517
  }
2518
+ function parseAskUserQuestionError(toolResult) {
2519
+ if (!toolResult) return null;
2520
+ try {
2521
+ const parsed = JSON.parse(toolResult);
2522
+ let detail = null;
2523
+ if (typeof parsed.error === "string") detail = parsed.error;
2524
+ if (parsed.error && typeof parsed.error === "object") {
2525
+ const message = parsed.error.message;
2526
+ if (typeof message === "string") detail = message;
2527
+ }
2528
+ if (!detail && typeof parsed.message === "string") detail = parsed.message;
2529
+ if (!detail) return null;
2530
+ return {
2531
+ 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",
2532
+ detail
2533
+ };
2534
+ } catch {
2535
+ return null;
2536
+ }
2537
+ }
2091
2538
  function normalizeQuestionItem(value) {
2092
2539
  if (!value || typeof value !== "object") return null;
2093
2540
  const item = value;
@@ -2116,9 +2563,10 @@ import { Fragment, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2116
2563
  function resolveAskQuestionState({
2117
2564
  toolStatus,
2118
2565
  hasAnswerData,
2119
- fallbackAnswered
2566
+ fallbackAnswered,
2567
+ fallbackAwaiting
2120
2568
  }) {
2121
- const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
2569
+ const awaitingAnswer = !hasAnswerData && (toolStatus === "awaiting_answer" || toolStatus === "pending" && fallbackAwaiting === true);
2122
2570
  return {
2123
2571
  awaitingAnswer,
2124
2572
  answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
@@ -2130,9 +2578,10 @@ function ToolCallBlock({
2130
2578
  answered,
2131
2579
  answerData,
2132
2580
  sessionStatus,
2581
+ isActiveQuestion,
2133
2582
  renderer
2134
2583
  }) {
2135
- const [expanded, setExpanded] = useState8(false);
2584
+ const [expanded, setExpanded] = useState9(false);
2136
2585
  const normalizedName = formatToolName(toolCall.name);
2137
2586
  if (renderer) {
2138
2587
  const custom = renderer(toolCall);
@@ -2145,7 +2594,8 @@ function ToolCallBlock({
2145
2594
  const questionState = resolveAskQuestionState({
2146
2595
  toolStatus: toolCall.status,
2147
2596
  hasAnswerData: Boolean(answerData),
2148
- fallbackAnswered: answered
2597
+ fallbackAnswered: answered,
2598
+ fallbackAwaiting: isActiveQuestion === true && (sessionStatus === "paused" || sessionStatus === "waiting_for_input")
2149
2599
  });
2150
2600
  const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
2151
2601
  if (askData) {
@@ -2167,9 +2617,16 @@ function ToolCallBlock({
2167
2617
  /* @__PURE__ */ jsx10("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2168
2618
  ] });
2169
2619
  }
2620
+ const errorDetail = parseAskUserQuestionError(
2621
+ typeof toolCall.result === "string" ? toolCall.result : null
2622
+ );
2170
2623
  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
2624
  /* @__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" })
2625
+ /* @__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" }),
2626
+ errorDetail?.detail ? /* @__PURE__ */ jsxs8("details", { className: "mt-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2627
+ /* @__PURE__ */ jsx10("summary", { className: "cursor-pointer", children: "\u67E5\u770B\u5177\u4F53\u539F\u56E0" }),
2628
+ /* @__PURE__ */ jsx10("div", { className: "mt-1 break-words font-mono", children: errorDetail.detail })
2629
+ ] }) : null
2173
2630
  ] });
2174
2631
  }
2175
2632
  const tone = getToolTone(toolCall.status);
@@ -2234,45 +2691,233 @@ function buildAskUserPayload(argumentsJson) {
2234
2691
  // src/components/AssistantTurnBlock.tsx
2235
2692
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2236
2693
  function ThinkingBlock({ reasoning, isStreaming }) {
2237
- const [open, setOpen] = useState9(false);
2238
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2694
+ const [open, setOpen] = useState10(false);
2695
+ if (!isStreaming) return null;
2696
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking text-xs", children: [
2239
2697
  /* @__PURE__ */ jsxs9(
2240
2698
  "button",
2241
2699
  {
2242
2700
  type: "button",
2243
2701
  onClick: () => setOpen(!open),
2244
2702
  "aria-expanded": open,
2245
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2703
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2246
2704
  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
- ] }),
2705
+ /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }),
2254
2706
  /* @__PURE__ */ jsx11(
2255
- ChevronDown,
2707
+ ChevronRight,
2256
2708
  {
2257
- size: 12,
2258
- className: cn("shrink-0 transition-transform", open && "rotate-180")
2709
+ size: 14,
2710
+ className: cn(
2711
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
2712
+ open && "rotate-90 opacity-100"
2713
+ )
2259
2714
  }
2260
2715
  )
2261
2716
  ]
2262
2717
  }
2263
2718
  ),
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 })
2719
+ open ? /* @__PURE__ */ jsx11("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning }) : null
2265
2720
  ] });
2266
2721
  }
2267
2722
  function getMessageText(message) {
2268
2723
  return getTextContent(normalizeMessageContent(message.content)).trim();
2269
2724
  }
2725
+ function hasRenderableMessageContent(message) {
2726
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
2727
+ }
2728
+ function getLastContentMessage(messages) {
2729
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
2730
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
2731
+ }
2732
+ return null;
2733
+ }
2734
+ function getOrderedMessageParts(message, toolCalls) {
2735
+ const blocks = message.blocks ?? [];
2736
+ if (!blocks.some((block) => block.type === "text") || !blocks.some((block) => block.type === "tool_use")) return [];
2737
+ const toolsById = new Map(toolCalls.map((toolCall) => [toolCall.id, toolCall]));
2738
+ const seenToolIds = /* @__PURE__ */ new Set();
2739
+ const parts = [];
2740
+ for (const [index, block] of blocks.entries()) {
2741
+ if (block.type === "text" && block.content != null && block.content !== "") {
2742
+ const content = Array.isArray(block.content) ? block.content : String(block.content);
2743
+ parts.push({ type: "text", key: `text-${index}`, content });
2744
+ }
2745
+ if (block.type !== "tool_use" || !block.tool_call_id) continue;
2746
+ const toolCall = toolsById.get(block.tool_call_id);
2747
+ if (!toolCall) continue;
2748
+ seenToolIds.add(toolCall.id);
2749
+ const previous = parts[parts.length - 1];
2750
+ if (previous?.type === "tools") previous.toolCalls.push(toolCall);
2751
+ else parts.push({ type: "tools", key: `tools-${index}`, toolCalls: [toolCall] });
2752
+ }
2753
+ const missingTools = toolCalls.filter((toolCall) => !seenToolIds.has(toolCall.id));
2754
+ if (seenToolIds.size === 0) return [];
2755
+ if (missingTools.length > 0) {
2756
+ parts.push({ type: "tools", key: "tools-missing", toolCalls: missingTools });
2757
+ }
2758
+ return parts;
2759
+ }
2270
2760
  function findLatestReasoningMessageIndex(messages) {
2271
2761
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2272
2762
  if (messages[index].reasoning) return index;
2273
2763
  }
2274
2764
  return -1;
2275
2765
  }
2766
+ function resolveTurnDisplayMode({
2767
+ isStreaming: _isStreaming,
2768
+ displayMode
2769
+ }) {
2770
+ return displayMode;
2771
+ }
2772
+ function formatExecutionDuration(durationMs) {
2773
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
2774
+ const minutes = Math.floor(totalSeconds / 60);
2775
+ const seconds = totalSeconds % 60;
2776
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
2777
+ }
2778
+ function getExecutionDurationMs({
2779
+ messages,
2780
+ isStreaming,
2781
+ now = Date.now()
2782
+ }) {
2783
+ const knownDuration = messages.reduce(
2784
+ (total, message) => {
2785
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
2786
+ return total + message.duration_ms;
2787
+ }
2788
+ return total + (message.tool_calls ?? []).reduce(
2789
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
2790
+ 0
2791
+ );
2792
+ },
2793
+ 0
2794
+ );
2795
+ if (!isStreaming) return knownDuration;
2796
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
2797
+ if (startedAt === void 0) return knownDuration;
2798
+ return Math.max(knownDuration, now - startedAt);
2799
+ }
2800
+ function findLastExceptionalEvent(messages) {
2801
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
2802
+ const messageStatus = messages[messageIndex].status;
2803
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
2804
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
2805
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
2806
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
2807
+ const status = toolCalls[toolIndex].status;
2808
+ if (status === "error" || status === "cancelled") {
2809
+ return { messageIndex, status };
2810
+ }
2811
+ }
2812
+ }
2813
+ return null;
2814
+ }
2815
+ function executionSummaryLabel({
2816
+ messages,
2817
+ isStreaming,
2818
+ durationMs,
2819
+ sessionStatus,
2820
+ askAnswers
2821
+ }) {
2822
+ if (isStreaming) {
2823
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
2824
+ }
2825
+ if (sessionStatus === "waiting_for_input" && messages.some(
2826
+ (message) => (message.tool_calls ?? []).some(
2827
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
2828
+ )
2829
+ )) {
2830
+ return "\u7B49\u5F85\u8F93\u5165";
2831
+ }
2832
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
2833
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
2834
+ if (lastExceptionalEvent) {
2835
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
2836
+ if (lastExceptionalEvent.status === "error") {
2837
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
2838
+ }
2839
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
2840
+ }
2841
+ return completedLabel;
2842
+ }
2843
+ function businessToolDisplayName(toolCall) {
2844
+ const displayName = toolCall.display_name?.trim() ?? "";
2845
+ if (!displayName) return "";
2846
+ const rawName = toolCall.name.trim();
2847
+ return displayName !== rawName && formatToolName(displayName) !== formatToolName(rawName) ? displayName : "";
2848
+ }
2849
+ function executionToolTypeLabel(toolCall) {
2850
+ switch (formatToolName(toolCall.name)) {
2851
+ case "WebSearch":
2852
+ case "WebFetch":
2853
+ return "\u7F51\u7EDC\u68C0\u7D22";
2854
+ case "Bash":
2855
+ case "BgBash":
2856
+ return "\u547D\u4EE4\u6267\u884C";
2857
+ case "Read":
2858
+ case "ReadSkill":
2859
+ return "\u5185\u5BB9\u8BFB\u53D6";
2860
+ case "Write":
2861
+ case "Edit":
2862
+ case "MultiEdit":
2863
+ return "\u6587\u4EF6\u5904\u7406";
2864
+ case "Grep":
2865
+ case "Glob":
2866
+ return "\u5185\u5BB9\u641C\u7D22";
2867
+ case "Agent":
2868
+ return "\u5B50\u4EFB\u52A1";
2869
+ case "search_skills":
2870
+ return "\u6280\u80FD\u68C0\u7D22";
2871
+ case "get_skill_content":
2872
+ return "\u8BFB\u53D6\u6280\u80FD";
2873
+ case "run_skill_tool":
2874
+ return "\u6267\u884C\u6280\u80FD";
2875
+ default:
2876
+ return businessToolDisplayName(toolCall) || "\u6267\u884C\u6B65\u9AA4";
2877
+ }
2878
+ }
2879
+ function executionToolIntent(toolCall) {
2880
+ const normalizedName = formatToolName(toolCall.name);
2881
+ let args = null;
2882
+ try {
2883
+ const parsed = JSON.parse(toolCall.arguments);
2884
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2885
+ } catch {
2886
+ args = null;
2887
+ }
2888
+ const getString = (key) => {
2889
+ const value = args?.[key];
2890
+ return typeof value === "string" ? value.trim() : "";
2891
+ };
2892
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || "";
2893
+ if (explicitIntent) return explicitIntent;
2894
+ if (normalizedName === "search_skills") return getString("query");
2895
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
2896
+ return getString("skill_name") || getString("skill");
2897
+ }
2898
+ if (normalizedName === "FinishTask") return getString("title");
2899
+ return "";
2900
+ }
2901
+ function ExecutionToolRow({ toolCall }) {
2902
+ const normalizedName = formatToolName(toolCall.name);
2903
+ const typeLabel = executionToolTypeLabel(toolCall);
2904
+ const intent = executionToolIntent(toolCall);
2905
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
2906
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
2907
+ const iconClass = cn(
2908
+ "size-3.5 shrink-0",
2909
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2910
+ );
2911
+ 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" });
2912
+ const rowClassName = cn(
2913
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
2914
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2915
+ );
2916
+ return /* @__PURE__ */ jsxs9("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
2917
+ icon,
2918
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label })
2919
+ ] });
2920
+ }
2276
2921
  function AssistantTurnBlock({
2277
2922
  messages,
2278
2923
  isStreaming = false,
@@ -2283,53 +2928,302 @@ function AssistantTurnBlock({
2283
2928
  sessionId
2284
2929
  }) {
2285
2930
  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
2931
+ const hasFailedWithoutContent = messages.some(
2932
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
2933
+ );
2934
+ const finalMessage = getLastContentMessage(messages);
2935
+ const turnToolCalls = messages.flatMap((message) => message.tool_calls ?? []);
2936
+ const finalOrderedParts = finalMessage ? getOrderedMessageParts(
2937
+ finalMessage,
2938
+ (finalMessage.tool_calls ?? []).filter(
2939
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
2940
+ )
2941
+ ) : [];
2942
+ const hasExecutionProcess = messages.some(
2943
+ (message) => message.reasoning || (message.tool_calls?.length ?? 0) > 0
2288
2944
  );
2289
2945
  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,
2946
+ const hasActionableToolCall = messages.some(
2947
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
2948
+ (toolCall) => toolCall.status === "error" || toolCall.status === "cancelled"
2949
+ )
2950
+ );
2951
+ const questionToolCalls = messages.flatMap(
2952
+ (message) => (message.tool_calls ?? []).filter(
2953
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
2954
+ )
2955
+ );
2956
+ const activeQuestionId = questionToolCalls.filter((toolCall) => toolCall.status === "pending").at(-1)?.id;
2957
+ const [displayMode, setDisplayMode] = useState10(
2958
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
2959
+ );
2960
+ const userSelectedDisplayModeRef = useRef9(false);
2961
+ const wasStreamingRef = useRef9(isStreaming);
2962
+ useEffect8(() => {
2963
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
2964
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
2965
+ }
2966
+ wasStreamingRef.current = isStreaming;
2967
+ }, [hasActionableToolCall, isStreaming]);
2968
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
2969
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
2970
+ const [clock, setClock] = useState10(() => Date.now());
2971
+ const hasLiveStartTime = messages.some(
2972
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
2973
+ );
2974
+ useEffect8(() => {
2975
+ if (!isStreaming || !hasLiveStartTime) return;
2976
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
2977
+ return () => window.clearInterval(timer);
2978
+ }, [hasLiveStartTime, isStreaming]);
2979
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
2980
+ const memoryRefs = collectMemoryRefs(messages);
2981
+ if (!hasExecutionProcess) {
2982
+ return /* @__PURE__ */ jsxs9(
2983
+ "div",
2984
+ {
2985
+ "aria-busy": isStreaming || void 0,
2986
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
2987
+ children: [
2988
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx11(MemoryRefsHint, { refs: memoryRefs }) : null,
2989
+ 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" }),
2990
+ 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" }),
2991
+ messages.map((message, index) => {
2992
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx11(
2993
+ "div",
2306
2994
  {
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
2995
+ className: "flex flex-col gap-3",
2996
+ children: /* @__PURE__ */ jsx11(
2997
+ AssistantMessageContent,
2998
+ {
2999
+ message,
3000
+ sessionId,
3001
+ streaming: isStreaming && index === messages.length - 1
3002
+ }
3003
+ )
3004
+ },
3005
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3006
+ ) : null;
3007
+ })
3008
+ ]
3009
+ }
3010
+ );
3011
+ }
3012
+ return /* @__PURE__ */ jsxs9(
3013
+ "div",
3014
+ {
3015
+ "aria-busy": isStreaming || void 0,
3016
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3017
+ children: [
3018
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx11(MemoryRefsHint, { refs: memoryRefs }) : null,
3019
+ 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" }),
3020
+ 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" }),
3021
+ /* @__PURE__ */ jsxs9("div", { className: "flex w-full items-start gap-2.5", children: [
3022
+ /* @__PURE__ */ jsx11(
3023
+ "span",
3024
+ {
3025
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3026
+ "aria-hidden": "true",
3027
+ children: /* @__PURE__ */ jsx11(Bot, { size: 16 })
3028
+ }
3029
+ ),
3030
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3031
+ /* @__PURE__ */ jsxs9(
3032
+ "button",
3033
+ {
3034
+ type: "button",
3035
+ onClick: () => {
3036
+ userSelectedDisplayModeRef.current = true;
3037
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
2323
3038
  },
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..." })
3039
+ "aria-expanded": effectiveMode === "detail",
3040
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3041
+ "data-testid": "assistant-execution-summary",
3042
+ 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",
3043
+ children: [
3044
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3045
+ messages,
3046
+ isStreaming,
3047
+ durationMs: liveExecutionDurationMs,
3048
+ sessionStatus,
3049
+ askAnswers
3050
+ }) }),
3051
+ /* @__PURE__ */ jsx11(
3052
+ ChevronRight,
3053
+ {
3054
+ size: 14,
3055
+ className: cn(
3056
+ "shrink-0 transition-transform duration-300",
3057
+ effectiveMode === "detail" && "rotate-90"
3058
+ ),
3059
+ "aria-hidden": "true"
3060
+ }
3061
+ )
3062
+ ]
3063
+ }
3064
+ ),
3065
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3066
+ ] })
3067
+ ] }),
3068
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx11("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
3069
+ const isLast = index === messages.length - 1;
3070
+ const streamingThis = isStreaming && isLast;
3071
+ const text = getMessageText(message);
3072
+ const toolCalls = (message.tool_calls ?? []).filter(
3073
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
3074
+ );
3075
+ const orderedParts = getOrderedMessageParts(message, toolCalls);
3076
+ const showReasoning = !!message.reasoning && isStreaming && index === latestReasoningIndex;
3077
+ return /* @__PURE__ */ jsxs9(
3078
+ "div",
3079
+ {
3080
+ className: "flex flex-col gap-3",
3081
+ children: [
3082
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3083
+ orderedParts.length > 0 ? orderedParts.map(
3084
+ (part) => part.type === "text" ? /* @__PURE__ */ jsx11(
3085
+ AssistantMessageContent,
3086
+ {
3087
+ message: { ...message, content: part.content, tool_calls: turnToolCalls },
3088
+ sessionId,
3089
+ streaming: streamingThis,
3090
+ compact: true
3091
+ },
3092
+ part.key
3093
+ ) : /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: part.toolCalls.map((toolCall) => {
3094
+ const custom = toolCallRenderer?.(toolCall);
3095
+ 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);
3096
+ }) }, part.key)
3097
+ ) : hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx11(
3098
+ AssistantMessageContent,
3099
+ {
3100
+ message,
3101
+ sessionId,
3102
+ streaming: streamingThis,
3103
+ compact: true
3104
+ }
3105
+ ) : null,
3106
+ orderedParts.length === 0 && toolCalls.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3107
+ const custom = toolCallRenderer?.(toolCall);
3108
+ 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);
3109
+ }) }) : null
3110
+ ]
3111
+ },
3112
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3113
+ );
3114
+ }) }) : null,
3115
+ finalMessage && (effectiveMode === "compact" || finalOrderedParts.length === 0) ? /* @__PURE__ */ jsx11("div", { className: "ml-10", children: /* @__PURE__ */ jsx11(
3116
+ AssistantMessageContent,
3117
+ {
3118
+ message: finalMessage,
3119
+ sessionId,
3120
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3121
+ }
3122
+ ) }) : null,
3123
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx11(
3124
+ ToolCallBlock,
3125
+ {
3126
+ toolCall,
3127
+ answerData: askAnswers?.[toolCall.id],
3128
+ onAnswer,
3129
+ answered: sessionStatus !== "waiting_for_input",
3130
+ sessionStatus,
3131
+ isActiveQuestion: toolCall.id === activeQuestionId,
3132
+ renderer: toolCallRenderer
3133
+ },
3134
+ toolCall.id
3135
+ ))
3136
+ ]
3137
+ }
3138
+ );
3139
+ }
3140
+ function collectMemoryRefs(messages) {
3141
+ const refs = /* @__PURE__ */ new Map();
3142
+ for (const message of messages) {
3143
+ for (const ref of message.memory_refs ?? []) if (!refs.has(ref.id)) refs.set(ref.id, ref);
3144
+ }
3145
+ return [...refs.values()];
3146
+ }
3147
+ function MemoryRefsHint({ refs }) {
3148
+ const [expanded, setExpanded] = useState10(false);
3149
+ 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";
3150
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-memory-refs ml-1 w-full max-w-[680px]", children: [
3151
+ /* @__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: [
3152
+ /* @__PURE__ */ jsx11(BookOpen, { size: 12 }),
3153
+ /* @__PURE__ */ jsxs9("span", { children: [
3154
+ label,
3155
+ "\uFF08",
3156
+ refs.length,
3157
+ "\uFF09"
3158
+ ] }),
3159
+ /* @__PURE__ */ jsx11(ChevronRight, { size: 10, className: cn("transition-transform", expanded && "rotate-90") })
3160
+ ] }),
3161
+ 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: [
3162
+ /* @__PURE__ */ jsx11("p", { className: "line-clamp-2 break-words leading-5", children: ref.content_preview }),
3163
+ ref.skill_name ? /* @__PURE__ */ jsx11("span", { className: "mt-1 inline-flex text-[10px] text-[hsl(var(--primary))]", children: ref.skill_name }) : null
3164
+ ] }, ref.id)) }) : null
3165
+ ] });
3166
+ }
3167
+ function AssistantMessageContent({
3168
+ message,
3169
+ sessionId,
3170
+ streaming,
3171
+ compact = false
3172
+ }) {
3173
+ const text = getMessageText(message);
3174
+ const imageParts = getImageParts(message.content);
3175
+ const fileParts = getFileParts(message.content);
3176
+ const failed = message.status === "failed";
3177
+ 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;
3178
+ const textContent = text ? /* @__PURE__ */ jsx11(
3179
+ "div",
3180
+ {
3181
+ className: cn(
3182
+ "blade-chat-assistant-text",
3183
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3184
+ ),
3185
+ children: /* @__PURE__ */ jsx11(
3186
+ MarkdownContent,
3187
+ {
3188
+ mode: streaming ? "streaming" : "static",
3189
+ className: "blade-chat-prose",
3190
+ sessionId,
3191
+ children: text
3192
+ }
3193
+ )
3194
+ }
3195
+ ) : null;
3196
+ if (imageParts.length === 0 && fileParts.length === 0) {
3197
+ if (!failed) return textContent;
3198
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
3199
+ failedBadge,
3200
+ textContent
3201
+ ] }) : null;
3202
+ }
3203
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3", children: [
3204
+ failedBadge,
3205
+ imageParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx11(
3206
+ "img",
3207
+ {
3208
+ src: part.image_url.url,
3209
+ alt: "\u6D88\u606F\u9644\u4EF6",
3210
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3211
+ },
3212
+ part.image_url.url
3213
+ )) }) : null,
3214
+ fileParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs9(
3215
+ "div",
3216
+ {
3217
+ 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))]",
3218
+ title: part.name,
3219
+ children: [
3220
+ /* @__PURE__ */ jsx11(FileText, { size: 12, className: "shrink-0" }),
3221
+ /* @__PURE__ */ jsx11("span", { className: "max-w-56 truncate", children: part.name })
3222
+ ]
3223
+ },
3224
+ `${part.name}-${part.data.slice(0, 32)}`
3225
+ )) }) : null,
3226
+ textContent
2333
3227
  ] });
2334
3228
  }
2335
3229
 
@@ -2386,7 +3280,7 @@ var RenderErrorBoundary = class extends Component {
2386
3280
  };
2387
3281
 
2388
3282
  // src/components/PostChatFollowupBlock.tsx
2389
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState10 } from "react";
3283
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef10, useState as useState11 } from "react";
2390
3284
  import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2391
3285
  function emitInteraction(callback, event) {
2392
3286
  try {
@@ -2397,9 +3291,6 @@ function emitInteraction(callback, event) {
2397
3291
  function basename(path) {
2398
3292
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2399
3293
  }
2400
- function isVideo(path) {
2401
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2402
- }
2403
3294
  function ArtifactCard({
2404
3295
  artifact,
2405
3296
  sessionId,
@@ -2409,7 +3300,7 @@ function ArtifactCard({
2409
3300
  onArtifactOpened
2410
3301
  }) {
2411
3302
  const client = useBladeClient();
2412
- const [downloading, setDownloading] = useState10(false);
3303
+ const [downloading, setDownloading] = useState11(false);
2413
3304
  const name = artifact.label || basename(artifact.target);
2414
3305
  if (artifact.kind === "link") {
2415
3306
  return /* @__PURE__ */ jsxs11(
@@ -2430,44 +3321,48 @@ ${artifact.target}`,
2430
3321
  }
2431
3322
  );
2432
3323
  }
2433
- const Icon2 = isVideo(artifact.target) ? Film : File;
2434
- return /* @__PURE__ */ jsxs11(
2435
- "button",
3324
+ const fileName = basename(artifact.target);
3325
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3326
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3327
+ ) : void 0;
3328
+ const handleDownload = async (event) => {
3329
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3330
+ event.preventDefault();
3331
+ if (!sessionId || downloading) return;
3332
+ setDownloading(true);
3333
+ emitInteraction(onInteraction, {
3334
+ type: "artifact_download_started",
3335
+ sessionId,
3336
+ assistantEntryId,
3337
+ artifactIndex,
3338
+ artifactKind: "file"
3339
+ });
3340
+ try {
3341
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3342
+ emitInteraction(onInteraction, {
3343
+ type: "artifact_download_succeeded",
3344
+ sessionId,
3345
+ assistantEntryId,
3346
+ artifactIndex,
3347
+ artifactKind: "file"
3348
+ });
3349
+ } catch {
3350
+ } finally {
3351
+ setDownloading(false);
3352
+ }
3353
+ };
3354
+ return /* @__PURE__ */ jsx13(
3355
+ "a",
2436
3356
  {
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
- ]
3357
+ href: downloadUrl,
3358
+ download: fileName,
3359
+ onClick: handleDownload,
3360
+ title: fileName,
3361
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3362
+ "aria-disabled": !sessionId || void 0,
3363
+ "aria-busy": downloading || void 0,
3364
+ 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",
3365
+ children: fileName
2471
3366
  }
2472
3367
  );
2473
3368
  }
@@ -2511,15 +3406,15 @@ function ResultFeedback({
2511
3406
  onFeedbackSaved
2512
3407
  }) {
2513
3408
  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);
3409
+ const [saved, setSaved] = useState11(savedFeedback ?? null);
3410
+ const [helpful, setHelpful] = useState11(savedFeedback?.helpful ?? null);
3411
+ const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
3412
+ const [saving, setSaving] = useState11(false);
3413
+ const [saveError, setSaveError] = useState11(false);
3414
+ const reportedShown = useRef10(false);
3415
+ const latestChoice = useRef10(null);
2521
3416
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2522
- useEffect6(() => {
3417
+ useEffect9(() => {
2523
3418
  if (!eligible || reportedShown.current) return;
2524
3419
  reportedShown.current = true;
2525
3420
  emitInteraction(onInteraction, {
@@ -2528,13 +3423,13 @@ function ResultFeedback({
2528
3423
  assistantEntryId: followup.assistant_entry_id
2529
3424
  });
2530
3425
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2531
- useEffect6(() => {
3426
+ useEffect9(() => {
2532
3427
  if (!savedFeedback || latestChoice.current) return;
2533
3428
  setSaved(savedFeedback);
2534
3429
  setHelpful(savedFeedback.helpful);
2535
3430
  setReason(savedFeedback.reason);
2536
3431
  }, [savedFeedback]);
2537
- const submit = useCallback5(
3432
+ const submit = useCallback6(
2538
3433
  async (nextHelpful, nextReason) => {
2539
3434
  if (!sessionId) return;
2540
3435
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2640,14 +3535,14 @@ function PostChatFollowupBlock({
2640
3535
  savedFeedback,
2641
3536
  onFeedbackSaved
2642
3537
  }) {
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());
3538
+ const [expanded, setExpanded] = useState11(false);
3539
+ const adopted = useRef10(/* @__PURE__ */ new Set());
3540
+ const reportedSuggestions = useRef10(false);
3541
+ const reportedArtifacts = useRef10(/* @__PURE__ */ new Set());
3542
+ const openedArtifacts = useRef10(/* @__PURE__ */ new Set());
2648
3543
  const artifacts = followup.final_artifacts ?? [];
2649
3544
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2650
- useEffect6(() => {
3545
+ useEffect9(() => {
2651
3546
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2652
3547
  reportedSuggestions.current = true;
2653
3548
  emitInteraction(onInteraction, {
@@ -2677,7 +3572,7 @@ function PostChatFollowupBlock({
2677
3572
  sessionId,
2678
3573
  visibleArtifacts
2679
3574
  ]);
2680
- const reportArtifactOpened = useCallback5(
3575
+ const reportArtifactOpened = useCallback6(
2681
3576
  (artifactIndex, artifactKind) => {
2682
3577
  if (openedArtifacts.current.has(artifactIndex)) return;
2683
3578
  openedArtifacts.current.add(artifactIndex);
@@ -2771,8 +3666,91 @@ function PostChatFollowupBlock({
2771
3666
  }
2772
3667
 
2773
3668
  // src/components/UserMessageBubble.tsx
2774
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
3669
+ import {
3670
+ chatErrorForDisplay,
3671
+ getFileParts as getFileParts2,
3672
+ getImageParts as getImageParts2,
3673
+ getTextContent as getTextContent2
3674
+ } from "@blade-hq/agent-client";
3675
+
3676
+ // src/lib/whatif-prompt.ts
3677
+ var HEADER_RE = /^以下消息和 step 产物标记为 deprecated_by_rerun,请基于最新用户假设从 step(\d+) 开始完整重新推演,不要复用旧结论。$/;
3678
+ var QUOTE_HEADER_RE = /^\[步骤(\d+)\s*·\s*(.+?)\]$/;
3679
+ var USER_INPUT_TAG = "[\u7528\u6237\u8F93\u5165]";
3680
+ function parseWhatIfPrompt(text) {
3681
+ const lines = text.replace(/\r\n/g, "\n").trimEnd().split("\n");
3682
+ const headerMatch = lines[0]?.match(HEADER_RE);
3683
+ if (!headerMatch) return null;
3684
+ const userTagIdx = lines.indexOf(USER_INPUT_TAG);
3685
+ const hasUserTag = userTagIdx >= 0;
3686
+ const quoteBlockEndExclusive = hasUserTag ? userTagIdx : lines.length;
3687
+ const quoteHeaderIdxs = [];
3688
+ let quoteBlockFound = false;
3689
+ for (let i = 1; i < quoteBlockEndExclusive; i++) {
3690
+ if (!quoteBlockFound && lines[i].trim() === "[\u5F15\u7528]") {
3691
+ quoteBlockFound = true;
3692
+ } else if (quoteBlockFound && QUOTE_HEADER_RE.test(lines[i])) {
3693
+ quoteHeaderIdxs.push(i);
3694
+ }
3695
+ }
3696
+ let legacyUserTextStart = -1;
3697
+ if (!hasUserTag && quoteHeaderIdxs.length > 0) {
3698
+ const lastSnapshotStart = quoteHeaderIdxs.at(-1) + 1;
3699
+ let i = lines.length - 1;
3700
+ while (i >= lastSnapshotStart && lines[i].trim() === "") i -= 1;
3701
+ while (i >= lastSnapshotStart && lines[i].trim() !== "") i -= 1;
3702
+ if (i >= lastSnapshotStart) legacyUserTextStart = i + 1;
3703
+ }
3704
+ const quotes = quoteHeaderIdxs.map((headerIdx, index) => {
3705
+ const match = lines[headerIdx].match(QUOTE_HEADER_RE);
3706
+ const nextHeader = quoteHeaderIdxs[index + 1];
3707
+ const end = nextHeader ?? (legacyUserTextStart >= 0 ? legacyUserTextStart : quoteBlockEndExclusive);
3708
+ const snapshotLines = lines.slice(headerIdx + 1, end);
3709
+ while (snapshotLines.at(-1)?.trim() === "") snapshotLines.pop();
3710
+ return {
3711
+ stepNumber: Number.parseInt(match[1], 10),
3712
+ label: match[2].trim(),
3713
+ snapshot: snapshotLines.join("\n")
3714
+ };
3715
+ });
3716
+ const userTextStart = hasUserTag ? userTagIdx + 1 : legacyUserTextStart;
3717
+ const userLines = userTextStart >= 0 ? lines.slice(userTextStart) : [];
3718
+ while (userLines[0]?.trim() === "") userLines.shift();
3719
+ while (userLines.at(-1)?.trim() === "") userLines.pop();
3720
+ const fromStep = Number.parseInt(headerMatch[1], 10);
3721
+ return {
3722
+ fromStep: Number.isFinite(fromStep) ? fromStep : null,
3723
+ quotes,
3724
+ userText: userLines.join("\n")
3725
+ };
3726
+ }
3727
+
3728
+ // src/components/WhatIfUserBubble.tsx
2775
3729
  import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3730
+ function WhatIfUserBubble({ parsed, onQuoteClick }) {
3731
+ const { fromStep, quotes, userText } = parsed;
3732
+ return /* @__PURE__ */ jsxs12("div", { className: "flex flex-col items-end gap-2", children: [
3733
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1.5 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.5)] px-2.5 py-0.5 text-[10px] text-[hsl(var(--muted-foreground))]", children: [
3734
+ /* @__PURE__ */ jsx14(RefreshCcw, { size: 10 }),
3735
+ /* @__PURE__ */ jsx14("span", { children: fromStep != null ? `\u91CD\u8DD1\u81EA step ${fromStep}` : "\u91CD\u8DD1" })
3736
+ ] }),
3737
+ quotes.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex max-w-[min(72vw,42rem)] flex-col items-stretch gap-2", children: quotes.map((quote, index) => {
3738
+ const clickable = quote.stepNumber != null && !!onQuoteClick;
3739
+ const label = quote.stepNumber != null ? `\u6B65\u9AA4${quote.stepNumber} \xB7 ${quote.label}` : quote.label;
3740
+ return /* @__PURE__ */ jsxs12("div", { className: "rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card)/0.8)] px-3 py-2 text-left", children: [
3741
+ /* @__PURE__ */ jsxs12("button", { type: "button", disabled: !clickable, onClick: () => clickable && onQuoteClick(quote.stepNumber), className: "inline-flex max-w-full items-center gap-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] disabled:cursor-default", title: clickable ? "\u8DF3\u8F6C\u5230\u5BF9\u5E94\u6B65\u9AA4\u5361\u7247" : void 0, children: [
3742
+ /* @__PURE__ */ jsx14("span", { children: "\u21B3" }),
3743
+ /* @__PURE__ */ jsx14("span", { className: "truncate", children: label })
3744
+ ] }),
3745
+ quote.snapshot ? /* @__PURE__ */ jsx14("div", { className: "mt-1 border-l-2 border-[hsl(var(--accent-foreground)/0.35)] pl-2 text-xs leading-relaxed text-[hsl(var(--foreground)/0.8)]", children: /* @__PURE__ */ jsx14(MarkdownContent, { className: "blade-chat-prose", children: quote.snapshot }) }) : null
3746
+ ] }, `${quote.stepNumber ?? "x"}-${index}`);
3747
+ }) }),
3748
+ userText && /* @__PURE__ */ jsx14("div", { className: "rounded-2xl border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-4 py-2.5 text-sm leading-relaxed text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx14(MarkdownContent, { className: "blade-chat-prose", children: userText }) })
3749
+ ] });
3750
+ }
3751
+
3752
+ // src/components/UserMessageBubble.tsx
3753
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2776
3754
  function isUserMessage(message) {
2777
3755
  return message.role === "user";
2778
3756
  }
@@ -2782,10 +3760,14 @@ function isErrorMessage(message) {
2782
3760
  var isSending = (message) => message.status === "streaming";
2783
3761
  function UserMessageBubble({ message, className }) {
2784
3762
  const text = getTextContent2(message.content).trim();
2785
- const fileParts = getFileParts(message.content);
2786
- const imageParts = getImageParts(message.content);
2787
- return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs12("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
2788
- imageParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx14(
3763
+ const fileParts = getFileParts2(message.content);
3764
+ const imageParts = getImageParts2(message.content);
3765
+ const whatifParsed = text && imageParts.length === 0 && fileParts.length === 0 ? parseWhatIfPrompt(text) : null;
3766
+ if (whatifParsed) {
3767
+ return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsx15("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: /* @__PURE__ */ jsx15(WhatIfUserBubble, { parsed: whatifParsed }) }) });
3768
+ }
3769
+ 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: [
3770
+ imageParts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx15(
2789
3771
  "img",
2790
3772
  {
2791
3773
  src: part.image_url.url,
@@ -2794,21 +3776,21 @@ function UserMessageBubble({ message, className }) {
2794
3776
  },
2795
3777
  part.image_url.url
2796
3778
  )) }),
2797
- fileParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs12(
3779
+ fileParts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs13(
2798
3780
  "div",
2799
3781
  {
2800
3782
  className: "flex items-center gap-1.5 rounded-lg border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--muted)/0.3)] px-2.5 py-1.5 text-xs text-[hsl(var(--muted-foreground))]",
2801
3783
  children: [
2802
- /* @__PURE__ */ jsx14(FileText, { size: 12, className: "shrink-0" }),
2803
- /* @__PURE__ */ jsx14("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
3784
+ /* @__PURE__ */ jsx15(FileText, { size: 12, className: "shrink-0" }),
3785
+ /* @__PURE__ */ jsx15("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
2804
3786
  ]
2805
3787
  },
2806
3788
  `${part.name}-${part.data.length}`
2807
3789
  )) }),
2808
- text && /* @__PURE__ */ jsx14("div", { className: "blade-chat-user-bubble max-w-full rounded-[20px] rounded-br-[6px] border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-[18px] py-[13px] text-sm leading-[1.65] text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx14(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
2809
- text && isSending(message) && /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
2810
- /* @__PURE__ */ jsx14(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
2811
- /* @__PURE__ */ jsx14("span", { children: "\u53D1\u9001\u4E2D" })
3790
+ text && /* @__PURE__ */ jsx15("div", { className: "blade-chat-user-bubble max-w-full rounded-[20px] rounded-br-[6px] border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-[18px] py-[13px] text-sm leading-[1.65] text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx15(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
3791
+ text && isSending(message) && /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
3792
+ /* @__PURE__ */ jsx15(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
3793
+ /* @__PURE__ */ jsx15("span", { children: "\u53D1\u9001\u4E2D" })
2812
3794
  ] })
2813
3795
  ] }) });
2814
3796
  }
@@ -2816,12 +3798,12 @@ function ErrorMessageBlock({
2816
3798
  message,
2817
3799
  className
2818
3800
  }) {
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 }) });
3801
+ const text = chatErrorForDisplay(getTextContent2(message.content));
3802
+ return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-error-row flex min-w-0 justify-start", className), children: /* @__PURE__ */ jsx15("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
3803
  }
2822
3804
 
2823
3805
  // src/components/MessageList.tsx
2824
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
3806
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2825
3807
  function parseModeChange(message) {
2826
3808
  if (message.kind !== "mode_change" || typeof message.content !== "string") {
2827
3809
  return null;
@@ -2870,10 +3852,14 @@ function MessageList({
2870
3852
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2871
3853
  onResultFeedbackSaved
2872
3854
  }) {
3855
+ const userMessages = messages.filter((message) => isUserMessage(message));
3856
+ const latestUserMessage = userMessages.at(-1);
3857
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
2873
3858
  const renderBlocks = useMemo7(() => {
2874
3859
  const visible = messages.filter((message) => {
2875
3860
  if ((message.loop_name ?? "root") !== "root") return false;
2876
3861
  if (isHiddenInternalMessage(message)) return false;
3862
+ if (message.kind === "context") return false;
2877
3863
  if (message.kind === "compaction") return true;
2878
3864
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2879
3865
  });
@@ -2916,7 +3902,7 @@ function MessageList({
2916
3902
  blocks.push({
2917
3903
  type: "message",
2918
3904
  message,
2919
- key: message.entry_id ?? `${message.role}-${blocks.length}`
3905
+ key: message.render_id ?? message.entry_id ?? `${message.role}-${blocks.length}`
2920
3906
  });
2921
3907
  }
2922
3908
  flushAssistant();
@@ -2943,98 +3929,151 @@ function MessageList({
2943
3929
  }
2944
3930
  return blocks;
2945
3931
  }, [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,
3932
+ return /* @__PURE__ */ jsxs14("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: [
3933
+ isStreaming ? /* @__PURE__ */ jsx16("output", { className: "sr-only", children: "\u6B63\u5728\u751F\u6210\u56DE\u590D" }) : null,
3934
+ /* @__PURE__ */ jsxs14(
3935
+ StickToBottom,
3936
+ {
3937
+ className: "h-full overflow-y-hidden",
3938
+ initial: "instant",
3939
+ resize: "instant",
3940
+ children: [
3941
+ /* @__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: [
3942
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs14("div", { className: "blade-chat-empty", children: [
3943
+ /* @__PURE__ */ jsx16(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3944
+ /* @__PURE__ */ jsx16("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3945
+ /* @__PURE__ */ jsx16("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3946
+ ] }) : renderBlocks.map((block) => {
3947
+ if (block.type === "message") {
3948
+ 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);
3949
+ }
3950
+ if (block.type === "assistant_turn") {
3951
+ const blockFeedback = block.messages.map(
3952
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3953
+ ).find((feedback) => feedback != null);
3954
+ const hasActiveFollowup = Boolean(
3955
+ postChatFollowup && block.messages.some(
3956
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
3957
+ )
3958
+ );
3959
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs14(
3960
+ RenderErrorBoundary,
2974
3961
  {
2975
- messages: block.messages,
2976
- isStreaming: block.isStreaming,
2977
- askAnswers,
2978
- onAnswer,
2979
- sessionStatus,
2980
- toolCallRenderer,
2981
- sessionId
3962
+ label: "\u52A9\u624B\u6D88\u606F",
3963
+ details: block.key,
3964
+ resetKey: getMessageResetSignature(block.messages),
3965
+ children: [
3966
+ /* @__PURE__ */ jsx16(
3967
+ AssistantTurnBlock,
3968
+ {
3969
+ messages: block.messages,
3970
+ isStreaming: block.isStreaming,
3971
+ askAnswers,
3972
+ onAnswer,
3973
+ sessionStatus,
3974
+ toolCallRenderer,
3975
+ sessionId
3976
+ }
3977
+ ),
3978
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx16(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3979
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx16(
3980
+ PostChatFollowupBlock,
3981
+ {
3982
+ followup: postChatFollowup,
3983
+ sessionId,
3984
+ onSuggestion,
3985
+ isViewer,
3986
+ onInteraction: onFollowupInteraction,
3987
+ savedFeedback: blockFeedback,
3988
+ onFeedbackSaved: onResultFeedbackSaved
3989
+ }
3990
+ ) : null
3991
+ ]
2982
3992
  }
2983
- ),
2984
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
2985
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
2986
- PostChatFollowupBlock,
3993
+ ) }, block.key);
3994
+ }
3995
+ if (block.type === "compaction") {
3996
+ return /* @__PURE__ */ jsxs14(
3997
+ "div",
2987
3998
  {
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",
3999
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
4000
+ children: [
4001
+ /* @__PURE__ */ jsx16(Layers, { size: 12 }),
4002
+ /* @__PURE__ */ jsx16("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
4003
+ ]
4004
+ },
4005
+ block.key
4006
+ );
4007
+ }
4008
+ return /* @__PURE__ */ jsx16(PlanningDivider, { kind: block.kind }, block.key);
4009
+ }),
4010
+ 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
4011
+ ] }) }) }),
4012
+ /* @__PURE__ */ jsx16(
4013
+ PinLatestUserMessage,
3004
4014
  {
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
- ]
4015
+ userMessageCount: userMessages.length,
4016
+ shouldPinLatestUser,
4017
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3010
4018
  },
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
- ] }) });
4019
+ sessionId ?? "no-session"
4020
+ ),
4021
+ /* @__PURE__ */ jsx16(ScrollToBottomButton, {})
4022
+ ]
4023
+ },
4024
+ sessionId ?? "no-session"
4025
+ )
4026
+ ] });
3021
4027
  }
3022
- function AutoScrollOnUserSend({ userMessageCount }) {
3023
- const { scrollToBottom } = useStickToBottomContext();
3024
- const previousCountRef = useRef7(userMessageCount);
3025
- useEffect7(() => {
3026
- if (userMessageCount > previousCountRef.current) {
4028
+ function PinLatestUserMessage({
4029
+ userMessageCount,
4030
+ shouldPinLatestUser,
4031
+ targetKey
4032
+ }) {
4033
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
4034
+ const previousCountRef = useRef11(userMessageCount);
4035
+ const spacerHeightRef = useRef11(0);
4036
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
4037
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
4038
+ const getTargetElement = useCallback7(() => {
4039
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
4040
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
4041
+ }, [contentRef]);
4042
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
4043
+ const setSpacerHeight = useCallback7(
4044
+ (height) => {
4045
+ spacerHeightRef.current = height;
4046
+ const content = contentRef.current;
4047
+ if (!content) return;
4048
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
4049
+ else content.style.removeProperty("--blade-chat-pin-spacer");
4050
+ },
4051
+ [contentRef]
4052
+ );
4053
+ useMessagePin({
4054
+ targetKey,
4055
+ pinTarget: shouldPinLatestUser,
4056
+ getScrollElement,
4057
+ getContentElement,
4058
+ getTargetElement,
4059
+ getSpacerHeight,
4060
+ setSpacerHeight,
4061
+ stopAutoScroll: stopScroll,
4062
+ scrollToBottom
4063
+ });
4064
+ useEffect10(() => {
4065
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3027
4066
  scrollToBottom("instant");
3028
4067
  }
3029
4068
  previousCountRef.current = userMessageCount;
3030
- }, [userMessageCount, scrollToBottom]);
4069
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3031
4070
  return null;
3032
4071
  }
3033
4072
  function ScrollToBottomButton() {
3034
4073
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3035
- const [visible, setVisible] = useState11(false);
3036
- const hideTimerRef = useRef7(null);
3037
- useEffect7(() => {
4074
+ const [visible, setVisible] = useState12(false);
4075
+ const hideTimerRef = useRef11(null);
4076
+ useEffect10(() => {
3038
4077
  if (isAtBottom) {
3039
4078
  if (!hideTimerRef.current) {
3040
4079
  hideTimerRef.current = setTimeout(() => {
@@ -3056,7 +4095,7 @@ function ScrollToBottomButton() {
3056
4095
  }
3057
4096
  };
3058
4097
  }, [isAtBottom]);
3059
- const handleClick = useCallback6(() => {
4098
+ const handleClick = useCallback7(() => {
3060
4099
  if (hideTimerRef.current) {
3061
4100
  clearTimeout(hideTimerRef.current);
3062
4101
  hideTimerRef.current = null;
@@ -3065,7 +4104,7 @@ function ScrollToBottomButton() {
3065
4104
  scrollToBottom();
3066
4105
  }, [scrollToBottom]);
3067
4106
  if (!visible) return null;
3068
- return /* @__PURE__ */ jsxs13(
4107
+ return /* @__PURE__ */ jsxs14(
3069
4108
  "button",
3070
4109
  {
3071
4110
  type: "button",
@@ -3073,25 +4112,25 @@ function ScrollToBottomButton() {
3073
4112
  "aria-label": "\u6EDA\u52A8\u5230\u5E95\u90E8",
3074
4113
  className: "blade-chat-scroll-bottom absolute bottom-4 right-4 flex items-center gap-1 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-1.5 text-xs text-[hsl(var(--muted-foreground))] shadow-lg transition-colors hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]",
3075
4114
  children: [
3076
- /* @__PURE__ */ jsx15(ChevronDown, { size: 14 }),
3077
- /* @__PURE__ */ jsx15("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
4115
+ /* @__PURE__ */ jsx16(ChevronDown, { size: 14 }),
4116
+ /* @__PURE__ */ jsx16("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
3078
4117
  ]
3079
4118
  }
3080
4119
  );
3081
4120
  }
3082
4121
  function PlanningDivider({ kind }) {
3083
- return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-3 py-1", children: [
3084
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
3085
- /* @__PURE__ */ jsxs13("div", { className: "inline-flex items-center gap-1.5 rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-300", children: [
3086
- /* @__PURE__ */ jsx15(Lightbulb, { size: 12 }),
3087
- /* @__PURE__ */ jsx15("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
4122
+ return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-3 py-1", children: [
4123
+ /* @__PURE__ */ jsx16("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
4124
+ /* @__PURE__ */ jsxs14("div", { className: "inline-flex items-center gap-1.5 rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-300", children: [
4125
+ /* @__PURE__ */ jsx16(Lightbulb, { size: 12 }),
4126
+ /* @__PURE__ */ jsx16("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
3088
4127
  ] }),
3089
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
4128
+ /* @__PURE__ */ jsx16("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
3090
4129
  ] });
3091
4130
  }
3092
4131
 
3093
4132
  // src/components/ChatSurface.tsx
3094
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4133
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
3095
4134
  function themeAttr(theme) {
3096
4135
  return theme === "dark" ? "dark" : void 0;
3097
4136
  }
@@ -3123,7 +4162,7 @@ function ChatSurface({
3123
4162
  beforeInput,
3124
4163
  banner
3125
4164
  }) {
3126
- return /* @__PURE__ */ jsxs14(
4165
+ return /* @__PURE__ */ jsxs15(
3127
4166
  "div",
3128
4167
  {
3129
4168
  "data-theme": themeAttr(theme),
@@ -3132,14 +4171,14 @@ function ChatSurface({
3132
4171
  classNames?.root
3133
4172
  ),
3134
4173
  children: [
3135
- /* @__PURE__ */ jsx16(ConnectionBanner, { connection, className: classNames?.banner }),
4174
+ /* @__PURE__ */ jsx17(ConnectionBanner, { connection, className: classNames?.banner }),
3136
4175
  banner,
3137
- errorMessage && /* @__PURE__ */ jsxs14("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
3138
- /* @__PURE__ */ jsx16(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
3139
- /* @__PURE__ */ jsx16("span", { children: errorMessage })
4176
+ errorMessage && /* @__PURE__ */ jsxs15("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
4177
+ /* @__PURE__ */ jsx17(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
4178
+ /* @__PURE__ */ jsx17("span", { className: "min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: chatErrorForDisplay2(errorMessage) })
3140
4179
  ] }),
3141
4180
  slots?.header,
3142
- /* @__PURE__ */ jsx16(
4181
+ /* @__PURE__ */ jsx17(
3143
4182
  MessageList,
3144
4183
  {
3145
4184
  messages,
@@ -3160,7 +4199,7 @@ function ChatSurface({
3160
4199
  }
3161
4200
  ),
3162
4201
  beforeInput,
3163
- /* @__PURE__ */ jsx16(
4202
+ /* @__PURE__ */ jsx17(
3164
4203
  ChatInput,
3165
4204
  {
3166
4205
  value: inputText,
@@ -3180,13 +4219,13 @@ function ChatSurface({
3180
4219
  }
3181
4220
 
3182
4221
  // src/components/AgentChat.tsx
3183
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
4222
+ import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3184
4223
  function isUnauthorizedError(error) {
3185
4224
  return error instanceof BladeApiError && error.status === 401;
3186
4225
  }
3187
4226
  function LoginCard({ client, onLoggedIn }) {
3188
- const [loggingIn, setLoggingIn] = useState12(false);
3189
- const [loginError, setLoginError] = useState12(null);
4227
+ const [loggingIn, setLoggingIn] = useState13(false);
4228
+ const [loginError, setLoginError] = useState13(null);
3190
4229
  const handleLogin = async () => {
3191
4230
  setLoggingIn(true);
3192
4231
  setLoginError(null);
@@ -3199,11 +4238,11 @@ function LoginCard({ client, onLoggedIn }) {
3199
4238
  setLoggingIn(false);
3200
4239
  }
3201
4240
  };
3202
- return /* @__PURE__ */ jsx17("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs15("div", { className: "flex w-full max-w-sm flex-col items-center gap-4 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-6 py-8 text-center", children: [
3203
- /* @__PURE__ */ jsx17(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
3204
- /* @__PURE__ */ jsx17("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
3205
- /* @__PURE__ */ jsx17("div", { className: "text-sm text-[hsl(var(--muted-foreground))]", children: "\u767B\u5F55\u540E\u5373\u53EF\u4E0E\u667A\u80FD\u4F53\u5BF9\u8BDD\uFF0C\u4F60\u7684\u4F1A\u8BDD\u5185\u5BB9\u4EC5\u81EA\u5DF1\u53EF\u89C1\u3002" }),
3206
- /* @__PURE__ */ jsx17(
4241
+ return /* @__PURE__ */ jsx18("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs16("div", { className: "flex w-full max-w-sm flex-col items-center gap-4 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-6 py-8 text-center", children: [
4242
+ /* @__PURE__ */ jsx18(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
4243
+ /* @__PURE__ */ jsx18("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
4244
+ /* @__PURE__ */ jsx18("div", { className: "text-sm text-[hsl(var(--muted-foreground))]", children: "\u767B\u5F55\u540E\u5373\u53EF\u4E0E\u667A\u80FD\u4F53\u5BF9\u8BDD\uFF0C\u4F60\u7684\u4F1A\u8BDD\u5185\u5BB9\u4EC5\u81EA\u5DF1\u53EF\u89C1\u3002" }),
4245
+ /* @__PURE__ */ jsx18(
3207
4246
  "button",
3208
4247
  {
3209
4248
  type: "button",
@@ -3213,20 +4252,20 @@ function LoginCard({ client, onLoggedIn }) {
3213
4252
  children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
3214
4253
  }
3215
4254
  ),
3216
- loginError && /* @__PURE__ */ jsx17("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
4255
+ loginError && /* @__PURE__ */ jsx18("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
3217
4256
  ] }) });
3218
4257
  }
3219
4258
  function AgentChat(props) {
3220
4259
  const client = useBladeClient();
3221
- const [attempt, setAttempt] = useState12(0);
3222
- const [needLogin, setNeedLogin] = useState12(() => !client.hasToken());
4260
+ const [attempt, setAttempt] = useState13(0);
4261
+ const [needLogin, setNeedLogin] = useState13(() => !client.hasToken());
3223
4262
  if (needLogin) {
3224
- return /* @__PURE__ */ jsx17(
4263
+ return /* @__PURE__ */ jsx18(
3225
4264
  "div",
3226
4265
  {
3227
4266
  "data-theme": themeAttr(props.theme),
3228
4267
  className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
3229
- children: /* @__PURE__ */ jsx17(
4268
+ children: /* @__PURE__ */ jsx18(
3230
4269
  LoginCard,
3231
4270
  {
3232
4271
  client,
@@ -3239,7 +4278,7 @@ function AgentChat(props) {
3239
4278
  }
3240
4279
  );
3241
4280
  }
3242
- return /* @__PURE__ */ jsx17(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
4281
+ return /* @__PURE__ */ jsx18(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
3243
4282
  }
3244
4283
  function ChatSessionView({
3245
4284
  sessionId,
@@ -3261,12 +4300,12 @@ function ChatSessionView({
3261
4300
  onSessionCreated
3262
4301
  });
3263
4302
  const replay = useReplay(session);
3264
- const [stopRequested, setStopRequested] = useState12(false);
3265
- const [inputText, setInputText] = useState12("");
3266
- const [resultFeedback, setResultFeedback] = useState12([]);
4303
+ const [stopRequested, setStopRequested] = useState13(false);
4304
+ const [inputText, setInputText] = useState13("");
4305
+ const [resultFeedback, setResultFeedback] = useState13([]);
3267
4306
  const resolvedSessionId = session?.sessionId;
3268
4307
  const isViewer = state?.viewerRole === "viewer";
3269
- useEffect8(() => {
4308
+ useEffect11(() => {
3270
4309
  setResultFeedback([]);
3271
4310
  if (!resolvedSessionId || isViewer) return;
3272
4311
  let cancelled = false;
@@ -3295,18 +4334,18 @@ function ChatSessionView({
3295
4334
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3296
4335
  [resultFeedback]
3297
4336
  );
3298
- const handleResultFeedbackSaved = useCallback7((saved) => {
4337
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3299
4338
  setResultFeedback((current) => [
3300
4339
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3301
4340
  saved
3302
4341
  ]);
3303
4342
  }, []);
3304
- useEffect8(() => {
4343
+ useEffect11(() => {
3305
4344
  if (session) {
3306
4345
  onSessionReady?.(session);
3307
4346
  }
3308
4347
  }, [session, onSessionReady]);
3309
- useEffect8(() => {
4348
+ useEffect11(() => {
3310
4349
  if (!session) return;
3311
4350
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3312
4351
  setInputText((prev) => `${prev ? `${prev}
@@ -3322,12 +4361,12 @@ ${content}`);
3322
4361
  offInsert();
3323
4362
  };
3324
4363
  }, [session]);
3325
- useEffect8(() => {
4364
+ useEffect11(() => {
3326
4365
  if (isUnauthorizedError(error)) {
3327
4366
  onUnauthorized();
3328
4367
  }
3329
4368
  }, [error, onUnauthorized]);
3330
- useEffect8(() => {
4369
+ useEffect11(() => {
3331
4370
  if (!session || !commands) return;
3332
4371
  const unsubscribes = Object.entries(commands).map(
3333
4372
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3348,7 +4387,7 @@ ${content}`);
3348
4387
  setStopRequested(true);
3349
4388
  void session?.stop();
3350
4389
  };
3351
- return /* @__PURE__ */ jsx17(
4390
+ return /* @__PURE__ */ jsx18(
3352
4391
  ChatSurface,
3353
4392
  {
3354
4393
  theme,
@@ -3358,8 +4397,8 @@ ${content}`);
3358
4397
  slots,
3359
4398
  placeholder,
3360
4399
  connection: state?.connection ?? "connecting",
3361
- banner: /* @__PURE__ */ jsxs15(Fragment3, { children: [
3362
- /* @__PURE__ */ jsx17(
4400
+ banner: /* @__PURE__ */ jsxs16(Fragment3, { children: [
4401
+ /* @__PURE__ */ jsx18(
3363
4402
  ReplayBar,
3364
4403
  {
3365
4404
  isReplay: replay.isReplay,
@@ -3369,7 +4408,7 @@ ${content}`);
3369
4408
  onExit: () => void replay.exitToAutonomous()
3370
4409
  }
3371
4410
  ),
3372
- /* @__PURE__ */ jsx17(ReplayMismatchPrompt, { mismatch: replay.mismatch })
4411
+ /* @__PURE__ */ jsx18(ReplayMismatchPrompt, { mismatch: replay.mismatch })
3373
4412
  ] }),
3374
4413
  errorMessage,
3375
4414
  messages: state?.messages ?? [],
@@ -3398,11 +4437,11 @@ ${content}`);
3398
4437
  }
3399
4438
 
3400
4439
  // src/components/LlmChat.tsx
3401
- import { useEffect as useEffect9, useMemo as useMemo9, useState as useState14 } from "react";
4440
+ import { useEffect as useEffect12, useMemo as useMemo9, useState as useState15 } from "react";
3402
4441
 
3403
4442
  // src/components/LlmAdvancedSettings.tsx
3404
- import { useState as useState13 } from "react";
3405
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
4443
+ import { useState as useState14 } from "react";
4444
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3406
4445
  var FIELDS = [
3407
4446
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
3408
4447
  { id: "model", label: "\u6A21\u578B", placeholder: "\u6A21\u578B\u540D\u79F0" },
@@ -3448,13 +4487,13 @@ function writeOverride(settings, baseURL, override) {
3448
4487
  }
3449
4488
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3450
4489
  const normalized = normalizeAdvanced(settings);
3451
- const [open, setOpen] = useState13(false);
3452
- const [draft, setDraft] = useState13(override);
4490
+ const [open, setOpen] = useState14(false);
4491
+ const [draft, setDraft] = useState14(override);
3453
4492
  if (!normalized) return null;
3454
4493
  const fields = FIELDS.filter((field) => normalized[field.id]);
3455
4494
  const dirty = Object.keys(override).length > 0;
3456
- return /* @__PURE__ */ jsxs16("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
3457
- /* @__PURE__ */ jsxs16(
4495
+ return /* @__PURE__ */ jsxs17("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
4496
+ /* @__PURE__ */ jsxs17(
3458
4497
  "button",
3459
4498
  {
3460
4499
  type: "button",
@@ -3464,16 +4503,16 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3464
4503
  },
3465
4504
  className: "flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3466
4505
  children: [
3467
- /* @__PURE__ */ jsx18(Settings2, { size: 13 }),
4506
+ /* @__PURE__ */ jsx19(Settings2, { size: 13 }),
3468
4507
  "\u9AD8\u7EA7\u8BBE\u7F6E",
3469
- dirty && /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
4508
+ dirty && /* @__PURE__ */ jsx19("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
3470
4509
  ]
3471
4510
  }
3472
4511
  ),
3473
- open && /* @__PURE__ */ jsxs16("div", { className: "mt-2 flex flex-col gap-2", children: [
3474
- fields.map((field) => /* @__PURE__ */ jsxs16("label", { className: "flex flex-col gap-1", children: [
3475
- /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
3476
- /* @__PURE__ */ jsx18(
4512
+ open && /* @__PURE__ */ jsxs17("div", { className: "mt-2 flex flex-col gap-2", children: [
4513
+ fields.map((field) => /* @__PURE__ */ jsxs17("label", { className: "flex flex-col gap-1", children: [
4514
+ /* @__PURE__ */ jsx19("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
4515
+ /* @__PURE__ */ jsx19(
3477
4516
  "input",
3478
4517
  {
3479
4518
  type: field.secret ? "password" : "text",
@@ -3484,9 +4523,9 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3484
4523
  }
3485
4524
  )
3486
4525
  ] }, field.id)),
3487
- normalized.apiKey && /* @__PURE__ */ jsx18("p", { className: "text-[hsl(var(--muted-foreground))]", children: "\u5BC6\u94A5\u4F1A\u5B58\u5728\u8FD9\u53F0\u6D4F\u89C8\u5668\u91CC\u3002\u53EA\u5728\u4F60\u4FE1\u5F97\u8FC7\u8FD9\u53F0\u673A\u5668\u65F6\u586B\u3002" }),
3488
- /* @__PURE__ */ jsxs16("div", { className: "flex gap-2", children: [
3489
- /* @__PURE__ */ jsx18(
4526
+ normalized.apiKey && /* @__PURE__ */ jsx19("p", { className: "text-[hsl(var(--muted-foreground))]", children: "\u5BC6\u94A5\u4F1A\u5B58\u5728\u8FD9\u53F0\u6D4F\u89C8\u5668\u91CC\u3002\u53EA\u5728\u4F60\u4FE1\u5F97\u8FC7\u8FD9\u53F0\u673A\u5668\u65F6\u586B\u3002" }),
4527
+ /* @__PURE__ */ jsxs17("div", { className: "flex gap-2", children: [
4528
+ /* @__PURE__ */ jsx19(
3490
4529
  "button",
3491
4530
  {
3492
4531
  type: "button",
@@ -3501,7 +4540,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3501
4540
  children: "\u4FDD\u5B58"
3502
4541
  }
3503
4542
  ),
3504
- /* @__PURE__ */ jsx18(
4543
+ /* @__PURE__ */ jsx19(
3505
4544
  "button",
3506
4545
  {
3507
4546
  type: "button",
@@ -3520,7 +4559,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3520
4559
  }
3521
4560
 
3522
4561
  // src/components/LlmChat.tsx
3523
- import { jsx as jsx19 } from "react/jsx-runtime";
4562
+ import { jsx as jsx20 } from "react/jsx-runtime";
3524
4563
  function LlmChat({
3525
4564
  classNames,
3526
4565
  renderers,
@@ -3532,11 +4571,11 @@ function LlmChat({
3532
4571
  onOverrideChange,
3533
4572
  ...options
3534
4573
  }) {
3535
- const [override, setOverride] = useState14(() => readOverride(advanced, options.baseURL));
4574
+ const [override, setOverride] = useState15(() => readOverride(advanced, options.baseURL));
3536
4575
  const effective = { ...options, ...override };
3537
4576
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3538
- const [inputText, setInputText] = useState14("");
3539
- const [stopRequested, setStopRequested] = useState14(false);
4577
+ const [inputText, setInputText] = useState15("");
4578
+ const [stopRequested, setStopRequested] = useState15(false);
3540
4579
  const handle = useMemo9(
3541
4580
  () => ({
3542
4581
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3546,10 +4585,10 @@ ${text}` : text),
3546
4585
  }),
3547
4586
  [send, reset]
3548
4587
  );
3549
- useEffect9(() => {
4588
+ useEffect12(() => {
3550
4589
  onReady?.(handle);
3551
4590
  }, [handle, onReady]);
3552
- return /* @__PURE__ */ jsx19(
4591
+ return /* @__PURE__ */ jsx20(
3553
4592
  ChatSurface,
3554
4593
  {
3555
4594
  theme,
@@ -3574,7 +4613,7 @@ ${text}` : text),
3574
4613
  setStopRequested(true);
3575
4614
  stop();
3576
4615
  },
3577
- beforeInput: advanced ? /* @__PURE__ */ jsx19(
4616
+ beforeInput: advanced ? /* @__PURE__ */ jsx20(
3578
4617
  LlmAdvancedSettingsBar,
3579
4618
  {
3580
4619
  settings: advanced,
@@ -3592,14 +4631,14 @@ ${text}` : text),
3592
4631
  }
3593
4632
 
3594
4633
  // src/components/ChatView.tsx
3595
- import { jsx as jsx20 } from "react/jsx-runtime";
4634
+ import { jsx as jsx21 } from "react/jsx-runtime";
3596
4635
  function ChatView(props) {
3597
4636
  const { mode, llm, onLlmReady, ...rest } = props;
3598
4637
  if (mode === "llm") {
3599
4638
  if (!llm) {
3600
4639
  throw new Error('ChatView: mode="llm" \u9700\u8981\u540C\u65F6\u4F20 llm={{ baseURL, model }}');
3601
4640
  }
3602
- return /* @__PURE__ */ jsx20(
4641
+ return /* @__PURE__ */ jsx21(
3603
4642
  LlmChat,
3604
4643
  {
3605
4644
  ...llm,
@@ -3612,7 +4651,126 @@ function ChatView(props) {
3612
4651
  }
3613
4652
  );
3614
4653
  }
3615
- return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
4654
+ return /* @__PURE__ */ jsx21(AgentChat, { ...rest });
4655
+ }
4656
+
4657
+ // src/components/ContextCard.tsx
4658
+ import {
4659
+ getContextDisplayState,
4660
+ getContextGroupDisplayState
4661
+ } from "@blade-hq/agent-client";
4662
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
4663
+ function ContextCard({ context, className }) {
4664
+ const display = getContextDisplayState(context);
4665
+ return /* @__PURE__ */ jsxs18(
4666
+ "details",
4667
+ {
4668
+ className: `blade-chat-context-card group/context-card rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
4669
+ children: [
4670
+ /* @__PURE__ */ jsxs18("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: [
4671
+ /* @__PURE__ */ jsx22(
4672
+ Layers,
4673
+ {
4674
+ size: 15,
4675
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
4676
+ "aria-hidden": "true"
4677
+ }
4678
+ ),
4679
+ /* @__PURE__ */ jsxs18("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
4680
+ /* @__PURE__ */ jsx22("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
4681
+ /* @__PURE__ */ jsx22("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
4682
+ ] }),
4683
+ /* @__PURE__ */ jsx22(
4684
+ ChevronDown,
4685
+ {
4686
+ size: 14,
4687
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-card:rotate-180",
4688
+ "aria-hidden": "true"
4689
+ }
4690
+ )
4691
+ ] }),
4692
+ /* @__PURE__ */ jsx22("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 })
4693
+ ]
4694
+ }
4695
+ );
4696
+ }
4697
+ function ContextGroupCard({ contexts, className }) {
4698
+ if (contexts.length === 0) return null;
4699
+ const single = contexts.length === 1 ? getContextDisplayState(contexts[0]) : null;
4700
+ const group = single ? null : getContextGroupDisplayState(contexts);
4701
+ return /* @__PURE__ */ jsxs18("details", { className: `blade-chat-context-card group/context-group rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`, children: [
4702
+ /* @__PURE__ */ jsxs18("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: [
4703
+ /* @__PURE__ */ jsx22(
4704
+ Layers,
4705
+ {
4706
+ size: 15,
4707
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
4708
+ "aria-hidden": "true"
4709
+ }
4710
+ ),
4711
+ /* @__PURE__ */ jsxs18("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
4712
+ /* @__PURE__ */ jsx22("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: single ? single.title : `${group?.title} \xB7 ${group?.count} \u9879` }),
4713
+ /* @__PURE__ */ jsx22("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: single ? single.summary : group?.summary })
4714
+ ] }),
4715
+ /* @__PURE__ */ jsx22(
4716
+ ChevronDown,
4717
+ {
4718
+ size: 14,
4719
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-group:rotate-180",
4720
+ "aria-hidden": "true"
4721
+ }
4722
+ )
4723
+ ] }),
4724
+ single ? /* @__PURE__ */ jsx22("div", { className: "blade-chat-context-detail border-t border-[hsl(var(--border))] px-3 py-2.5 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: single.detail }) : /* @__PURE__ */ jsx22("div", { className: "blade-chat-context-group-items flex flex-col gap-1.5 border-t border-[hsl(var(--border))] p-2", children: contexts.map((context) => /* @__PURE__ */ jsx22(
4725
+ ContextCard,
4726
+ {
4727
+ context
4728
+ },
4729
+ `${context.context_kind}:${context.context_key}`
4730
+ )) })
4731
+ ] });
4732
+ }
4733
+
4734
+ // src/lib/agent-computer-command.ts
4735
+ var COMPUTER_LAUNCH_COMMAND_PATTERN = /(?:^|[\n;&|(]\s*)computer\s+launch(?:\s|$)/;
4736
+ function isAgentComputerCommand(command) {
4737
+ return COMPUTER_LAUNCH_COMMAND_PATTERN.test(command);
4738
+ }
4739
+ function isAgentComputerToolCall(argumentsJson) {
4740
+ if (!argumentsJson) return false;
4741
+ let command;
4742
+ try {
4743
+ const parsed = JSON.parse(argumentsJson);
4744
+ if (typeof parsed !== "object" || parsed === null) return false;
4745
+ command = parsed.command;
4746
+ } catch {
4747
+ return false;
4748
+ }
4749
+ return typeof command === "string" && isAgentComputerCommand(command);
4750
+ }
4751
+ var LAUNCH_SUCCESS_MARKER = "\u5DF2\u542F\u52A8 ";
4752
+ function resultContainsLaunchSuccessMarker(result, depth = 0) {
4753
+ if (depth > 2) return false;
4754
+ if (typeof result === "string") {
4755
+ if (result.includes(LAUNCH_SUCCESS_MARKER)) return true;
4756
+ try {
4757
+ return resultContainsLaunchSuccessMarker(JSON.parse(result), depth + 1);
4758
+ } catch {
4759
+ return false;
4760
+ }
4761
+ }
4762
+ if (typeof result === "object" && result !== null) {
4763
+ for (const value of Object.values(result)) {
4764
+ if (typeof value === "string" && value.includes(LAUNCH_SUCCESS_MARKER)) return true;
4765
+ }
4766
+ }
4767
+ return false;
4768
+ }
4769
+ function classifyAgentComputerLaunchOutcome(toolCall) {
4770
+ if (toolCall.status === "error" || toolCall.status === "cancelled") return "failed";
4771
+ if (toolCall.status !== "done") return "pending";
4772
+ if (toolCall.result === void 0 || toolCall.result === null) return "unknown";
4773
+ return resultContainsLaunchSuccessMarker(toolCall.result) ? "succeeded" : "failed";
3616
4774
  }
3617
4775
 
3618
4776
  // src/index.ts
@@ -3621,13 +4779,24 @@ export {
3621
4779
  AgentChat,
3622
4780
  BladeProvider,
3623
4781
  ChatView,
4782
+ ContextCard,
4783
+ ContextGroupCard,
3624
4784
  LlmChat,
3625
4785
  MarkdownContent,
4786
+ MemoryRefsHint,
3626
4787
  ReplayBar,
3627
4788
  ReplayMismatchPrompt,
4789
+ WhatIfUserBubble,
4790
+ classifyAgentComputerLaunchOutcome,
4791
+ collectMemoryRefs,
4792
+ isAgentComputerCommand,
4793
+ isAgentComputerToolCall,
4794
+ normalizeAdjacentUrlFormatting,
4795
+ parseWhatIfPrompt,
3628
4796
  useAgentSession,
3629
4797
  useBladeClient,
3630
4798
  useLlmChat,
4799
+ useMessagePin,
3631
4800
  useReplay
3632
4801
  };
3633
4802
  /*! Bundled license information:
@@ -3639,17 +4808,16 @@ lucide-react/dist/esm/createLucideIcon.js:
3639
4808
  lucide-react/dist/esm/icons/arrow-right.js:
3640
4809
  lucide-react/dist/esm/icons/arrow-up-right.js:
3641
4810
  lucide-react/dist/esm/icons/arrow-up.js:
4811
+ lucide-react/dist/esm/icons/book-open.js:
3642
4812
  lucide-react/dist/esm/icons/bot.js:
3643
- lucide-react/dist/esm/icons/brain.js:
3644
4813
  lucide-react/dist/esm/icons/check.js:
3645
4814
  lucide-react/dist/esm/icons/chevron-down.js:
3646
4815
  lucide-react/dist/esm/icons/chevron-right.js:
3647
4816
  lucide-react/dist/esm/icons/circle-alert.js:
3648
4817
  lucide-react/dist/esm/icons/copy.js:
3649
- lucide-react/dist/esm/icons/download.js:
4818
+ lucide-react/dist/esm/icons/earth.js:
4819
+ lucide-react/dist/esm/icons/file-pen-line.js:
3650
4820
  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
4821
  lucide-react/dist/esm/icons/globe.js:
3654
4822
  lucide-react/dist/esm/icons/layers.js:
3655
4823
  lucide-react/dist/esm/icons/lightbulb.js:
@@ -3658,10 +4826,14 @@ lucide-react/dist/esm/icons/lock-keyhole.js:
3658
4826
  lucide-react/dist/esm/icons/message-square-more.js:
3659
4827
  lucide-react/dist/esm/icons/message-square.js:
3660
4828
  lucide-react/dist/esm/icons/play.js:
4829
+ lucide-react/dist/esm/icons/refresh-ccw.js:
4830
+ lucide-react/dist/esm/icons/search.js:
3661
4831
  lucide-react/dist/esm/icons/settings-2.js:
3662
4832
  lucide-react/dist/esm/icons/sparkles.js:
3663
4833
  lucide-react/dist/esm/icons/square.js:
4834
+ lucide-react/dist/esm/icons/terminal.js:
3664
4835
  lucide-react/dist/esm/icons/triangle-alert.js:
4836
+ lucide-react/dist/esm/icons/wrench.js:
3665
4837
  lucide-react/dist/esm/icons/x.js:
3666
4838
  lucide-react/dist/esm/lucide-react.js:
3667
4839
  (**