@blade-hq/agent-react 2610.0.0-beta.2 → 2610.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -245,6 +245,8 @@ function useLlmChat(options) {
245
245
  const [error, setError] = useState3(null);
246
246
  const [isStreaming, setIsStreaming] = useState3(false);
247
247
  const abortRef = useRef2(null);
248
+ const activeStartedAtRef = useRef2(null);
249
+ const assistantTimingsRef = useRef2(/* @__PURE__ */ new WeakMap());
248
250
  const generationRef = useRef2(0);
249
251
  const historyRef = useRef2([]);
250
252
  const optionsRef = useRef2(options);
@@ -263,16 +265,31 @@ function useLlmChat(options) {
263
265
  setFailedToolIds([]);
264
266
  setError(null);
265
267
  setIsStreaming(false);
268
+ activeStartedAtRef.current = null;
269
+ assistantTimingsRef.current = /* @__PURE__ */ new WeakMap();
266
270
  }, [stop]);
267
271
  const send = useCallback2(async (text) => {
268
272
  const content = text.trim();
269
273
  if (!content || abortRef.current) return false;
270
274
  const opts = optionsRef.current;
271
275
  const maxRounds = opts.maxToolRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
276
+ const startedAt = Date.now();
277
+ activeStartedAtRef.current = startedAt;
272
278
  const commit = (message) => {
273
279
  historyRef.current = [...historyRef.current, message];
274
280
  setHistory(historyRef.current);
275
281
  };
282
+ const latestAssistantTiming = { current: null };
283
+ const commitAssistant = (message) => {
284
+ const timing = { startedAt };
285
+ assistantTimingsRef.current.set(message, timing);
286
+ latestAssistantTiming.current = timing;
287
+ commit(message);
288
+ return timing;
289
+ };
290
+ const finishTiming = (timing) => {
291
+ timing.durationMs = Math.max(0, Date.now() - timing.startedAt);
292
+ };
276
293
  setError(null);
277
294
  setIsStreaming(true);
278
295
  setStreamingText("");
@@ -298,8 +315,11 @@ function useLlmChat(options) {
298
315
  };
299
316
  setStreamingText(null);
300
317
  setStreamingCalls([]);
301
- commit(assistant);
302
- if (!result.toolCalls.length) return true;
318
+ const assistantTiming = commitAssistant(assistant);
319
+ if (!result.toolCalls.length) {
320
+ finishTiming(assistantTiming);
321
+ return true;
322
+ }
303
323
  const bail = (reason) => {
304
324
  for (const call of result.toolCalls) {
305
325
  commit({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: reason }) });
@@ -308,11 +328,13 @@ function useLlmChat(options) {
308
328
  };
309
329
  if (!opts.onToolCall) {
310
330
  bail("\u8FD9\u4E2A\u5E94\u7528\u6CA1\u6709\u63D0\u4F9B\u5DE5\u5177\u6267\u884C\u5165\u53E3");
331
+ finishTiming(assistantTiming);
311
332
  return true;
312
333
  }
313
334
  if (round >= maxRounds) {
314
335
  bail(`\u5DE5\u5177\u8C03\u7528\u5DF2\u8FBE\u4E0A\u9650 ${maxRounds} \u8F6E\uFF0C\u6CA1\u6709\u6267\u884C`);
315
336
  setError(`\u5DE5\u5177\u8C03\u7528\u8D85\u8FC7 ${maxRounds} \u8F6E\u4ECD\u672A\u7ED9\u51FA\u7ED3\u8BBA\uFF0C\u5DF2\u505C\u4E0B\u3002`);
337
+ finishTiming(assistantTiming);
316
338
  return false;
317
339
  }
318
340
  for (const call of result.toolCalls) {
@@ -337,21 +359,39 @@ function useLlmChat(options) {
337
359
  setStreamingCalls([]);
338
360
  if (controller.signal.aborted) {
339
361
  if (generation !== generationRef.current) return false;
340
- commit({ role: "assistant", content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09" });
362
+ const timing = commitAssistant({
363
+ role: "assistant",
364
+ content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09"
365
+ });
366
+ timing.status = "interrupted";
367
+ finishTiming(timing);
341
368
  return false;
342
369
  }
343
370
  if (partial && generation === generationRef.current) {
344
- commit({ role: "assistant", content: partial });
371
+ const timing = commitAssistant({ role: "assistant", content: partial });
372
+ timing.status = "failed";
373
+ finishTiming(timing);
374
+ } else if (latestAssistantTiming.current) {
375
+ latestAssistantTiming.current.status = "failed";
376
+ finishTiming(latestAssistantTiming.current);
345
377
  }
346
378
  setError(err instanceof Error && err.message ? err.message : "\u5BF9\u8BDD\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
347
379
  return false;
348
380
  } finally {
349
381
  if (abortRef.current === controller) abortRef.current = null;
382
+ activeStartedAtRef.current = null;
350
383
  setIsStreaming(false);
351
384
  }
352
385
  }, []);
353
386
  const messages = useMemo2(
354
- () => toChatMessages(history, streamingText, streamingCalls, failedToolIds),
387
+ () => toChatMessages(
388
+ history,
389
+ streamingText,
390
+ streamingCalls,
391
+ failedToolIds,
392
+ assistantTimingsRef.current,
393
+ activeStartedAtRef.current
394
+ ),
355
395
  [history, streamingText, streamingCalls, failedToolIds]
356
396
  );
357
397
  return { messages, isStreaming, error, send, stop, reset };
@@ -503,7 +543,7 @@ function parseSse(raw) {
503
543
  if (!delta) return null;
504
544
  return { text: delta.content, toolCallDeltas: delta.tool_calls };
505
545
  }
506
- function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
546
+ function toChatMessages(history, streamingText, streamingCalls, failedToolIds, assistantTimings, activeStartedAt) {
507
547
  const results = /* @__PURE__ */ new Map();
508
548
  for (const msg of history) {
509
549
  if (msg.role === "tool") results.set(msg.tool_call_id, msg.content);
@@ -515,10 +555,13 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
515
555
  messages.push({ role: "user", content: msg.content, status: "completed" });
516
556
  continue;
517
557
  }
558
+ const timing = assistantTimings.get(msg);
518
559
  messages.push({
519
560
  role: "assistant",
520
561
  content: msg.content,
521
- status: "completed",
562
+ status: timing?.status ?? "completed",
563
+ ...timing ? { timestamp: new Date(timing.startedAt).toISOString() } : {},
564
+ ...timing?.durationMs === void 0 ? {} : { duration_ms: timing.durationMs },
522
565
  ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
523
566
  });
524
567
  }
@@ -527,6 +570,7 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
527
570
  role: "assistant",
528
571
  content: streamingText,
529
572
  status: "streaming",
573
+ ...activeStartedAt === null ? {} : { timestamp: new Date(activeStartedAt).toISOString() },
530
574
  ...streamingCalls.length ? { tool_calls: streamingCalls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
531
575
  });
532
576
  }
@@ -544,6 +588,184 @@ function toToolCallInfo(call, results, failedToolIds) {
544
588
  };
545
589
  }
546
590
 
591
+ // src/hooks/use-message-pin.ts
592
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3 } from "react";
593
+ var DEFAULT_MARGIN_PX = 24;
594
+ var MANUAL_SCROLL_TOLERANCE_PX = 2;
595
+ function useMessagePin({
596
+ targetKey,
597
+ pinTarget,
598
+ layoutKey,
599
+ getScrollElement,
600
+ getContentElement,
601
+ getTargetElement,
602
+ getTargetScrollTop,
603
+ getSpacerHeight,
604
+ setSpacerHeight,
605
+ stopAutoScroll,
606
+ scrollToBottom,
607
+ margin = DEFAULT_MARGIN_PX
608
+ }) {
609
+ const pinActiveRef = useRef3(false);
610
+ const frameRef = useRef3(null);
611
+ const retryTimeoutRef = useRef3(null);
612
+ const manualScrollCheckTimeoutRef = useRef3(null);
613
+ const repositionPendingRef = useRef3(false);
614
+ const pinnedTargetScrollTopRef = useRef3(null);
615
+ const appliedTargetKeyRef = useRef3(null);
616
+ const initialObservedTargetKeyRef = useRef3(pinTarget ? null : targetKey);
617
+ const observedTargetKeyRef = useRef3(initialObservedTargetKeyRef.current);
618
+ const setSpacerHeightRef = useRef3(setSpacerHeight);
619
+ setSpacerHeightRef.current = setSpacerHeight;
620
+ const release = useCallback3(() => {
621
+ if (!pinActiveRef.current) return;
622
+ pinActiveRef.current = false;
623
+ repositionPendingRef.current = false;
624
+ pinnedTargetScrollTopRef.current = null;
625
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
626
+ frameRef.current = null;
627
+ if (manualScrollCheckTimeoutRef.current != null) {
628
+ clearTimeout(manualScrollCheckTimeoutRef.current);
629
+ manualScrollCheckTimeoutRef.current = null;
630
+ }
631
+ setSpacerHeightRef.current(0);
632
+ }, []);
633
+ const reposition = useCallback3(
634
+ () => {
635
+ const scroll = getScrollElement();
636
+ const target = getTargetElement();
637
+ if (!scroll || !pinActiveRef.current) return;
638
+ const targetScrollTop = target ? Math.max(
639
+ 0,
640
+ scroll.scrollTop + target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - margin
641
+ ) : getTargetScrollTop?.(scroll);
642
+ if (targetScrollTop == null) return;
643
+ const previousSpacerHeight = getSpacerHeight();
644
+ const baseScrollHeight = scroll.scrollHeight - previousSpacerHeight;
645
+ const nextSpacerHeight = Math.max(
646
+ 0,
647
+ targetScrollTop + scroll.clientHeight - baseScrollHeight
648
+ );
649
+ setSpacerHeightRef.current(nextSpacerHeight);
650
+ pinnedTargetScrollTopRef.current = targetScrollTop;
651
+ stopAutoScroll();
652
+ scroll.scrollTop = targetScrollTop;
653
+ if (nextSpacerHeight === 0 && previousSpacerHeight > 0) {
654
+ pinActiveRef.current = false;
655
+ scrollToBottom();
656
+ }
657
+ },
658
+ [
659
+ getScrollElement,
660
+ getSpacerHeight,
661
+ getTargetElement,
662
+ getTargetScrollTop,
663
+ margin,
664
+ scrollToBottom,
665
+ stopAutoScroll
666
+ ]
667
+ );
668
+ const scheduleReposition = useCallback3(
669
+ () => {
670
+ if (!pinActiveRef.current) return;
671
+ repositionPendingRef.current = true;
672
+ if (frameRef.current != null) return;
673
+ frameRef.current = requestAnimationFrame(() => {
674
+ frameRef.current = null;
675
+ const shouldReposition = repositionPendingRef.current;
676
+ repositionPendingRef.current = false;
677
+ if (shouldReposition) reposition();
678
+ });
679
+ },
680
+ [reposition]
681
+ );
682
+ useEffect3(() => {
683
+ if (!targetKey) {
684
+ release();
685
+ appliedTargetKeyRef.current = null;
686
+ observedTargetKeyRef.current = null;
687
+ return;
688
+ }
689
+ if (!pinTarget) {
690
+ if (pinActiveRef.current && appliedTargetKeyRef.current != null && appliedTargetKeyRef.current !== targetKey) {
691
+ release();
692
+ }
693
+ observedTargetKeyRef.current = targetKey;
694
+ return;
695
+ }
696
+ if (appliedTargetKeyRef.current === targetKey) {
697
+ observedTargetKeyRef.current = targetKey;
698
+ return;
699
+ }
700
+ if (observedTargetKeyRef.current === targetKey) return;
701
+ appliedTargetKeyRef.current = targetKey;
702
+ observedTargetKeyRef.current = targetKey;
703
+ pinActiveRef.current = true;
704
+ stopAutoScroll();
705
+ scheduleReposition();
706
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
707
+ retryTimeoutRef.current = window.setTimeout(() => {
708
+ retryTimeoutRef.current = null;
709
+ scheduleReposition();
710
+ }, 80);
711
+ }, [pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
712
+ useEffect3(() => {
713
+ if (layoutKey !== void 0) scheduleReposition();
714
+ }, [layoutKey, scheduleReposition]);
715
+ useEffect3(() => {
716
+ const scroll = getScrollElement();
717
+ if (!scroll) return;
718
+ const content = getContentElement?.();
719
+ const observer = new ResizeObserver(scheduleReposition);
720
+ observer.observe(scroll);
721
+ if (content && content !== scroll) observer.observe(content);
722
+ window.addEventListener("resize", scheduleReposition);
723
+ window.visualViewport?.addEventListener("resize", scheduleReposition);
724
+ const handleScroll = () => {
725
+ if (!pinActiveRef.current) return;
726
+ if (manualScrollCheckTimeoutRef.current != null) {
727
+ clearTimeout(manualScrollCheckTimeoutRef.current);
728
+ }
729
+ manualScrollCheckTimeoutRef.current = window.setTimeout(() => {
730
+ manualScrollCheckTimeoutRef.current = null;
731
+ if (!pinActiveRef.current || repositionPendingRef.current) return;
732
+ const targetScrollTop = pinnedTargetScrollTopRef.current;
733
+ if (targetScrollTop != null && Math.abs(scroll.scrollTop - targetScrollTop) > MANUAL_SCROLL_TOLERANCE_PX) {
734
+ release();
735
+ }
736
+ }, 0);
737
+ };
738
+ scroll.addEventListener("scroll", handleScroll, { passive: true });
739
+ return () => {
740
+ observer.disconnect();
741
+ window.removeEventListener("resize", scheduleReposition);
742
+ window.visualViewport?.removeEventListener("resize", scheduleReposition);
743
+ scroll.removeEventListener("scroll", handleScroll);
744
+ };
745
+ }, [getContentElement, getScrollElement, release, scheduleReposition]);
746
+ useEffect3(
747
+ () => () => {
748
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
749
+ frameRef.current = null;
750
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
751
+ retryTimeoutRef.current = null;
752
+ if (manualScrollCheckTimeoutRef.current != null) {
753
+ clearTimeout(manualScrollCheckTimeoutRef.current);
754
+ }
755
+ manualScrollCheckTimeoutRef.current = null;
756
+ repositionPendingRef.current = false;
757
+ pinnedTargetScrollTopRef.current = null;
758
+ appliedTargetKeyRef.current = null;
759
+ observedTargetKeyRef.current = initialObservedTargetKeyRef.current;
760
+ pinActiveRef.current = false;
761
+ setSpacerHeightRef.current(0);
762
+ },
763
+ []
764
+ );
765
+ const isActive = useCallback3(() => pinActiveRef.current, []);
766
+ return { release, isActive };
767
+ }
768
+
547
769
  // src/components/AgentChat.tsx
548
770
  import { BladeApiError, latestPostChatFollowup } from "@blade-hq/agent-client";
549
771
 
@@ -636,6 +858,18 @@ var ArrowUp = createLucideIcon("ArrowUp", [
636
858
  ["path", { d: "M12 19V5", key: "x0mq9r" }]
637
859
  ]);
638
860
 
861
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/book-open.js
862
+ var BookOpen = createLucideIcon("BookOpen", [
863
+ ["path", { d: "M12 7v14", key: "1akyts" }],
864
+ [
865
+ "path",
866
+ {
867
+ d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",
868
+ key: "ruj8y"
869
+ }
870
+ ]
871
+ ]);
872
+
639
873
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/bot.js
640
874
  var Bot = createLucideIcon("Bot", [
641
875
  ["path", { d: "M12 8V4H8", key: "hb8ula" }],
@@ -646,31 +880,6 @@ var Bot = createLucideIcon("Bot", [
646
880
  ["path", { d: "M9 13v2", key: "rq6x2g" }]
647
881
  ]);
648
882
 
649
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/brain.js
650
- var Brain = createLucideIcon("Brain", [
651
- [
652
- "path",
653
- {
654
- d: "M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z",
655
- key: "l5xja"
656
- }
657
- ],
658
- [
659
- "path",
660
- {
661
- d: "M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z",
662
- key: "ep3f8r"
663
- }
664
- ],
665
- ["path", { d: "M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4", key: "1p4c4q" }],
666
- ["path", { d: "M17.599 6.5a3 3 0 0 0 .399-1.375", key: "tmeiqw" }],
667
- ["path", { d: "M6.003 5.125A3 3 0 0 0 6.401 6.5", key: "105sqy" }],
668
- ["path", { d: "M3.477 10.896a4 4 0 0 1 .585-.396", key: "ql3yin" }],
669
- ["path", { d: "M19.938 10.5a4 4 0 0 1 .585.396", key: "1qfode" }],
670
- ["path", { d: "M6 18a4 4 0 0 1-1.967-.516", key: "2e4loj" }],
671
- ["path", { d: "M19.967 17.484A4 4 0 0 1 18 18", key: "159ez6" }]
672
- ]);
673
-
674
883
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/check.js
675
884
  var Check = createLucideIcon("Check", [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]);
676
885
 
@@ -697,11 +906,37 @@ var Copy = createLucideIcon("Copy", [
697
906
  ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
698
907
  ]);
699
908
 
700
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/download.js
701
- var Download = createLucideIcon("Download", [
702
- ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
703
- ["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }],
704
- ["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }]
909
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/earth.js
910
+ var Earth = createLucideIcon("Earth", [
911
+ ["path", { d: "M21.54 15H17a2 2 0 0 0-2 2v4.54", key: "1djwo0" }],
912
+ [
913
+ "path",
914
+ {
915
+ d: "M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",
916
+ key: "1tzkfa"
917
+ }
918
+ ],
919
+ ["path", { d: "M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05", key: "14pb5j" }],
920
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
921
+ ]);
922
+
923
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-pen-line.js
924
+ var FilePenLine = createLucideIcon("FilePenLine", [
925
+ [
926
+ "path",
927
+ {
928
+ d: "m18 5-2.414-2.414A2 2 0 0 0 14.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2",
929
+ key: "142zxg"
930
+ }
931
+ ],
932
+ [
933
+ "path",
934
+ {
935
+ d: "M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z",
936
+ key: "2t3380"
937
+ }
938
+ ],
939
+ ["path", { d: "M8 18h1", key: "13wk12" }]
705
940
  ]);
706
941
 
707
942
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
@@ -713,24 +948,6 @@ var FileText = createLucideIcon("FileText", [
713
948
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
714
949
  ]);
715
950
 
716
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file.js
717
- var File = createLucideIcon("File", [
718
- ["path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z", key: "1rqfz7" }],
719
- ["path", { d: "M14 2v4a2 2 0 0 0 2 2h4", key: "tnqrlb" }]
720
- ]);
721
-
722
- // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/film.js
723
- var Film = createLucideIcon("Film", [
724
- ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
725
- ["path", { d: "M7 3v18", key: "bbkbws" }],
726
- ["path", { d: "M3 7.5h4", key: "zfgn84" }],
727
- ["path", { d: "M3 12h18", key: "1i2n21" }],
728
- ["path", { d: "M3 16.5h4", key: "1230mu" }],
729
- ["path", { d: "M17 3v18", key: "in4fa5" }],
730
- ["path", { d: "M17 7.5h4", key: "myr1c1" }],
731
- ["path", { d: "M17 16.5h4", key: "go4c1d" }]
732
- ]);
733
-
734
951
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/globe.js
735
952
  var Globe = createLucideIcon("Globe", [
736
953
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
@@ -806,6 +1023,12 @@ var Play = createLucideIcon("Play", [
806
1023
  ["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
807
1024
  ]);
808
1025
 
1026
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/search.js
1027
+ var Search = createLucideIcon("Search", [
1028
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
1029
+ ["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
1030
+ ]);
1031
+
809
1032
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
810
1033
  var Settings2 = createLucideIcon("Settings2", [
811
1034
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -834,6 +1057,12 @@ var Square = createLucideIcon("Square", [
834
1057
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
835
1058
  ]);
836
1059
 
1060
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/terminal.js
1061
+ var Terminal = createLucideIcon("Terminal", [
1062
+ ["polyline", { points: "4 17 10 11 4 5", key: "akl6gq" }],
1063
+ ["line", { x1: "12", x2: "20", y1: "19", y2: "19", key: "q2wloq" }]
1064
+ ]);
1065
+
837
1066
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
838
1067
  var TriangleAlert = createLucideIcon("TriangleAlert", [
839
1068
  [
@@ -847,6 +1076,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
847
1076
  ["path", { d: "M12 17h.01", key: "p32p05" }]
848
1077
  ]);
849
1078
 
1079
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/wrench.js
1080
+ var Wrench = createLucideIcon("Wrench", [
1081
+ [
1082
+ "path",
1083
+ {
1084
+ d: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z",
1085
+ key: "cbrjhi"
1086
+ }
1087
+ ]
1088
+ ]);
1089
+
850
1090
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
851
1091
  var X = createLucideIcon("X", [
852
1092
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -854,7 +1094,7 @@ var X = createLucideIcon("X", [
854
1094
  ]);
855
1095
 
856
1096
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState12 } from "react";
1097
+ import { useCallback as useCallback8, useEffect as useEffect11, useMemo as useMemo8, useState as useState13 } from "react";
858
1098
 
859
1099
  // src/lib/utils.ts
860
1100
  function cn(...inputs) {
@@ -983,6 +1223,12 @@ function ReplayMismatchPrompt({ mismatch, className }) {
983
1223
 
984
1224
  // src/components/ChatInput.tsx
985
1225
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1226
+ function isImeCompositionKey(event) {
1227
+ return event.isComposing || event.keyCode === 229;
1228
+ }
1229
+ function shouldSubmitChatInput(event, menuOpen) {
1230
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1231
+ }
986
1232
  function ChatInput({
987
1233
  value,
988
1234
  onValueChange,
@@ -1002,7 +1248,12 @@ function ChatInput({
1002
1248
  onValueChange("");
1003
1249
  };
1004
1250
  const handleKeyDown = (event) => {
1005
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1251
+ if (shouldSubmitChatInput({
1252
+ key: event.key,
1253
+ shiftKey: event.shiftKey,
1254
+ isComposing: event.nativeEvent.isComposing,
1255
+ keyCode: event.nativeEvent.keyCode
1256
+ }, false)) {
1006
1257
  event.preventDefault();
1007
1258
  void handleSend();
1008
1259
  }
@@ -1052,24 +1303,74 @@ function ChatInput({
1052
1303
  }
1053
1304
 
1054
1305
  // src/components/ConnectionBanner.tsx
1306
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1055
1307
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1308
+ var CONNECTION_NOTICE_DELAY_MS = 3e3;
1309
+ var CONNECTION_ERROR_DELAY_MS = 15e3;
1310
+ function useConnectionNoticePhase(connected) {
1311
+ const [phase, setPhase] = useState4("hidden");
1312
+ const connectedRef = useRef4(connected);
1313
+ const timersRef = useRef4([]);
1314
+ connectedRef.current = connected;
1315
+ useEffect4(() => {
1316
+ const clearTimers = () => {
1317
+ for (const timer of timersRef.current) clearTimeout(timer);
1318
+ timersRef.current = [];
1319
+ };
1320
+ const startGracePeriod = () => {
1321
+ clearTimers();
1322
+ setPhase("hidden");
1323
+ timersRef.current = [
1324
+ setTimeout(() => setPhase("recovering"), CONNECTION_NOTICE_DELAY_MS),
1325
+ setTimeout(() => setPhase("failed"), CONNECTION_ERROR_DELAY_MS)
1326
+ ];
1327
+ };
1328
+ if (connected) {
1329
+ clearTimers();
1330
+ setPhase("hidden");
1331
+ } else {
1332
+ startGracePeriod();
1333
+ }
1334
+ const handleForeground = () => {
1335
+ if (!connectedRef.current) startGracePeriod();
1336
+ };
1337
+ const handleVisibilityChange = () => {
1338
+ if (document.visibilityState === "visible") handleForeground();
1339
+ };
1340
+ window.addEventListener("blade:app-active", handleForeground);
1341
+ window.addEventListener("focus", handleForeground);
1342
+ window.addEventListener("pageshow", handleForeground);
1343
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1344
+ return () => {
1345
+ clearTimers();
1346
+ window.removeEventListener("blade:app-active", handleForeground);
1347
+ window.removeEventListener("focus", handleForeground);
1348
+ window.removeEventListener("pageshow", handleForeground);
1349
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1350
+ };
1351
+ }, [connected]);
1352
+ return phase;
1353
+ }
1056
1354
  function ConnectionBanner({ connection, className }) {
1057
- if (connection === "connected" || connection === "connecting") {
1058
- return null;
1059
- }
1060
- const reconnecting = connection === "reconnecting";
1355
+ const hasConnectedRef = useRef4(connection === "connected" || connection === "reconnecting");
1356
+ if (connection === "connected") hasConnectedRef.current = true;
1357
+ const connected = connection === "connected";
1358
+ const phase = useConnectionNoticePhase(connected);
1359
+ if (connected || phase === "hidden") return null;
1360
+ const recovering = phase === "recovering";
1361
+ const firstConnection = !hasConnectedRef.current;
1061
1362
  return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1062
1363
  "div",
1063
1364
  {
1064
1365
  className: cn(
1065
1366
  "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"
1367
+ recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1067
1368
  ),
1068
1369
  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 }) }),
1370
+ /* @__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
1371
  /* @__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" })
1372
+ /* @__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" }),
1373
+ /* @__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
1374
  ] })
1074
1375
  ]
1075
1376
  }
@@ -1078,10 +1379,10 @@ function ConnectionBanner({ connection, className }) {
1078
1379
 
1079
1380
  // src/components/MessageList.tsx
1080
1381
  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";
1382
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo7, useRef as useRef11, useState as useState12 } from "react";
1082
1383
 
1083
1384
  // ../../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";
1385
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef5, useState as useState5 } from "react";
1085
1386
  var DEFAULT_SPRING_ANIMATION = {
1086
1387
  /**
1087
1388
  * A value from 0 to 1, on how much to damp the animation.
@@ -1118,12 +1419,12 @@ globalThis.document?.addEventListener("click", () => {
1118
1419
  mouseDown = false;
1119
1420
  });
1120
1421
  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);
1422
+ const [escapedFromLock, updateEscapedFromLock] = useState5(false);
1423
+ const [isAtBottom, updateIsAtBottom] = useState5(options.initial !== false);
1424
+ const [isNearBottom, setIsNearBottom] = useState5(false);
1425
+ const optionsRef = useRef5(null);
1125
1426
  optionsRef.current = options;
1126
- const isSelecting = useCallback3(() => {
1427
+ const isSelecting = useCallback4(() => {
1127
1428
  if (!mouseDown) {
1128
1429
  return false;
1129
1430
  }
@@ -1134,11 +1435,11 @@ var useStickToBottom = (options = {}) => {
1134
1435
  const range = selection.getRangeAt(0);
1135
1436
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1136
1437
  }, []);
1137
- const setIsAtBottom = useCallback3((isAtBottom2) => {
1438
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1138
1439
  state.isAtBottom = isAtBottom2;
1139
1440
  updateIsAtBottom(isAtBottom2);
1140
1441
  }, []);
1141
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
1442
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1142
1443
  state.escapedFromLock = escapedFromLock2;
1143
1444
  updateEscapedFromLock(escapedFromLock2);
1144
1445
  }, []);
@@ -1195,7 +1496,7 @@ var useStickToBottom = (options = {}) => {
1195
1496
  }
1196
1497
  };
1197
1498
  }, []);
1198
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
1499
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1199
1500
  if (typeof scrollOptions === "string") {
1200
1501
  scrollOptions = { animation: scrollOptions };
1201
1502
  }
@@ -1280,11 +1581,11 @@ var useStickToBottom = (options = {}) => {
1280
1581
  }
1281
1582
  return next();
1282
1583
  }, [setIsAtBottom, isSelecting, state]);
1283
- const stopScroll = useCallback3(() => {
1584
+ const stopScroll = useCallback4(() => {
1284
1585
  setEscapedFromLock(true);
1285
1586
  setIsAtBottom(false);
1286
1587
  }, [setEscapedFromLock, setIsAtBottom]);
1287
- const handleScroll = useCallback3(({ target }) => {
1588
+ const handleScroll = useCallback4(({ target }) => {
1288
1589
  if (target !== scrollRef.current) {
1289
1590
  return;
1290
1591
  }
@@ -1323,7 +1624,7 @@ var useStickToBottom = (options = {}) => {
1323
1624
  }
1324
1625
  }, 1);
1325
1626
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1326
- const handleWheel = useCallback3(({ target, deltaY }) => {
1627
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1327
1628
  let element = target;
1328
1629
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1329
1630
  if (!element.parentElement) {
@@ -1393,7 +1694,7 @@ var useStickToBottom = (options = {}) => {
1393
1694
  };
1394
1695
  };
1395
1696
  function useRefCallback(callback, deps) {
1396
- const result = useCallback3((ref) => {
1697
+ const result = useCallback4((ref) => {
1397
1698
  result.current = ref;
1398
1699
  return callback(ref);
1399
1700
  }, deps);
@@ -1425,11 +1726,11 @@ function mergeAnimations(...animations) {
1425
1726
 
1426
1727
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1427
1728
  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";
1729
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect5, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef6 } from "react";
1429
1730
  var StickToBottomContext = createContext2(null);
1430
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
1731
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect5;
1431
1732
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1432
- const customTargetScrollTop = useRef4(null);
1733
+ const customTargetScrollTop = useRef6(null);
1433
1734
  const targetScrollTop = React.useCallback((target, elements) => {
1434
1735
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1435
1736
  return get?.(target, elements) ?? target;
@@ -1505,11 +1806,16 @@ function useStickToBottomContext() {
1505
1806
  }
1506
1807
 
1507
1808
  // src/components/AssistantTurnBlock.tsx
1508
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1509
- import { useState as useState9 } from "react";
1809
+ import {
1810
+ getFileParts,
1811
+ getImageParts,
1812
+ getTextContent,
1813
+ normalizeMessageContent
1814
+ } from "@blade-hq/agent-client";
1815
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState10 } from "react";
1510
1816
 
1511
1817
  // src/components/AgentLoopBlock.tsx
1512
- import { useState as useState5 } from "react";
1818
+ import { useState as useState6 } from "react";
1513
1819
 
1514
1820
  // src/components/display-utils.ts
1515
1821
  var TOOL_NAME_ALIASES = {
@@ -1525,7 +1831,9 @@ var TOOL_NAME_ALIASES = {
1525
1831
  finish_task: "FinishTask",
1526
1832
  glob: "Glob",
1527
1833
  grep: "Grep",
1834
+ kb_search: "KbSearch",
1528
1835
  ls: "Ls",
1836
+ multi_edit: "MultiEdit",
1529
1837
  read: "Read",
1530
1838
  read_skill: "ReadSkill",
1531
1839
  web_fetch: "WebFetch",
@@ -1538,9 +1846,11 @@ var TOOL_DISPLAY_LABELS = {
1538
1846
  Read: "\u8BFB\u53D6\u6587\u4EF6",
1539
1847
  Write: "\u5199\u5165\u6587\u4EF6",
1540
1848
  Edit: "\u7F16\u8F91\u6587\u4EF6",
1849
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1541
1850
  Ls: "\u5217\u51FA\u76EE\u5F55",
1542
1851
  Glob: "\u5339\u914D\u6587\u4EF6",
1543
1852
  Grep: "\u641C\u7D22\u6587\u672C",
1853
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1544
1854
  WebSearch: "\u641C\u7D22\u7F51\u9875",
1545
1855
  WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1546
1856
  Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
@@ -1551,6 +1861,16 @@ var TOOL_DISPLAY_LABELS = {
1551
1861
  ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1552
1862
  GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1553
1863
  };
1864
+ var PRIORITY_FILE_KEYS = [
1865
+ "file_path",
1866
+ "output_file",
1867
+ "output_path",
1868
+ "path",
1869
+ "filepath",
1870
+ "target_file",
1871
+ "destination",
1872
+ "filename"
1873
+ ];
1554
1874
  function safeParseJson(value) {
1555
1875
  if (!value) return null;
1556
1876
  try {
@@ -1559,10 +1879,53 @@ function safeParseJson(value) {
1559
1879
  return null;
1560
1880
  }
1561
1881
  }
1882
+ function isPlainObject(value) {
1883
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1884
+ }
1885
+ function findFileLikeValue(value, depth = 0, allowPlainString = false) {
1886
+ if (depth > 3) return null;
1887
+ if (typeof value === "string") {
1888
+ const trimmed = value.trim();
1889
+ if (!trimmed) return null;
1890
+ if (allowPlainString || trimmed.includes("/") || /\.[a-z0-9]{1,8}$/i.test(trimmed)) {
1891
+ return trimmed;
1892
+ }
1893
+ return null;
1894
+ }
1895
+ if (Array.isArray(value)) {
1896
+ for (const item of value) {
1897
+ const found = findFileLikeValue(item, depth + 1, allowPlainString);
1898
+ if (found) return found;
1899
+ }
1900
+ return null;
1901
+ }
1902
+ if (isPlainObject(value)) {
1903
+ for (const key of PRIORITY_FILE_KEYS) {
1904
+ const direct = findFileLikeValue(value[key], depth + 1, true);
1905
+ if (direct) return direct;
1906
+ }
1907
+ for (const nested of Object.values(value)) {
1908
+ const found = findFileLikeValue(nested, depth + 1, allowPlainString);
1909
+ if (found) return found;
1910
+ }
1911
+ }
1912
+ return null;
1913
+ }
1562
1914
  function getStringArgValue(args, key) {
1563
1915
  const value = args?.[key];
1564
1916
  return typeof value === "string" ? value.trim() : "";
1565
1917
  }
1918
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1919
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1920
+ function getSkillNameFromFilePath(filePath) {
1921
+ if (!filePath) return null;
1922
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1923
+ const fileName = segments.pop();
1924
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1925
+ const dirName = segments.pop();
1926
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1927
+ return dirName;
1928
+ }
1566
1929
  function formatToolName(name) {
1567
1930
  const trimmed = name.trim();
1568
1931
  if (!trimmed) return name;
@@ -1570,6 +1933,15 @@ function formatToolName(name) {
1570
1933
  const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1571
1934
  return TOOL_NAME_ALIASES[normalized] ?? stripped;
1572
1935
  }
1936
+ function extractToolFilePath(toolCall) {
1937
+ const formattedName = formatToolName(toolCall.name);
1938
+ if (formattedName !== "Read" && formattedName !== "Write" && formattedName !== "Edit" && formattedName !== "MultiEdit") {
1939
+ return null;
1940
+ }
1941
+ const filePath = findFileLikeValue(safeParseJson(toolCall.arguments));
1942
+ if (!filePath || filePath.split("/").pop()?.toLowerCase() === "phase.json") return null;
1943
+ return filePath;
1944
+ }
1573
1945
  function getToolDisplayLabel(toolCall) {
1574
1946
  const normalized = formatToolName(toolCall.name);
1575
1947
  const args = safeParseJson(toolCall.arguments);
@@ -1587,6 +1959,12 @@ function getToolDisplayLabel(toolCall) {
1587
1959
  const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1588
1960
  return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1589
1961
  }
1962
+ if (normalized === "Read") {
1963
+ const skillName = getSkillNameFromFilePath(
1964
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1965
+ );
1966
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1967
+ }
1590
1968
  if (normalized === "FinishTask") {
1591
1969
  const title = getStringArgValue(args, "title");
1592
1970
  return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
@@ -1641,83 +2019,68 @@ function parseAgentDescription(argumentsJson) {
1641
2019
  }
1642
2020
  }
1643
2021
  function AgentLoopBlock({ toolCall }) {
1644
- const [expanded, setExpanded] = useState5(false);
2022
+ const [expanded, setExpanded] = useState6(false);
1645
2023
  const description = parseAgentDescription(toolCall.arguments);
1646
2024
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1647
2025
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1648
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
2026
+ const hasResult = toolCall.result != null;
2027
+ const iconClass = cn(
2028
+ "size-3.5 shrink-0",
2029
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2030
+ );
2031
+ return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
1649
2032
  /* @__PURE__ */ jsxs5(
1650
- "div",
2033
+ "button",
1651
2034
  {
2035
+ type: "button",
2036
+ onClick: () => hasResult && setExpanded(!expanded),
2037
+ disabled: !hasResult,
2038
+ "aria-expanded": hasResult ? expanded : void 0,
2039
+ "data-testid": "execution-tool-intent",
1652
2040
  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))]"
2041
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2042
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2043
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1655
2044
  ),
2045
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1656
2046
  children: [
1657
- /* @__PURE__ */ jsxs5(
1658
- "button",
2047
+ 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" }),
2048
+ /* @__PURE__ */ jsxs5("span", { className: "min-w-0 truncate", children: [
2049
+ "\u5B50\u4EFB\u52A1\uFF1A",
2050
+ description
2051
+ ] }),
2052
+ hasResult ? /* @__PURE__ */ jsx6(
2053
+ ChevronRight,
1659
2054
  {
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
- ]
2055
+ size: 14,
2056
+ className: cn(
2057
+ "shrink-0 transition-transform duration-300",
2058
+ expanded && "rotate-90"
2059
+ ),
2060
+ "aria-hidden": "true"
1694
2061
  }
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) })
2062
+ ) : null
1697
2063
  ]
1698
2064
  }
1699
2065
  ),
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
- ] })
2066
+ 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
2067
  ] });
1705
2068
  }
1706
2069
 
1707
2070
  // src/components/MarkdownContent.tsx
1708
2071
  import {
1709
- useEffect as useEffect4,
2072
+ useEffect as useEffect6,
1710
2073
  useMemo as useMemo5,
1711
- useRef as useRef5,
1712
- useState as useState6
2074
+ useRef as useRef7,
2075
+ useState as useState7
1713
2076
  } from "react";
1714
2077
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1715
2078
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
1716
2079
  function CodeBlockPre({ children, node: _node, ...props }) {
1717
- const preRef = useRef5(null);
1718
- const [copied, setCopied] = useState6(false);
1719
- const [language, setLanguage] = useState6("");
1720
- useEffect4(() => {
2080
+ const preRef = useRef7(null);
2081
+ const [copied, setCopied] = useState7(false);
2082
+ const [language, setLanguage] = useState7("");
2083
+ useEffect6(() => {
1721
2084
  const codeEl = preRef.current?.querySelector("code");
1722
2085
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1723
2086
  }, []);
@@ -1788,11 +2151,40 @@ function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1788
2151
  }
1789
2152
 
1790
2153
  // src/components/ToolCallBlock.tsx
1791
- import { useState as useState8 } from "react";
2154
+ import { useState as useState9 } from "react";
1792
2155
 
1793
2156
  // src/components/AskUserQuestionBlock.tsx
1794
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState7 } from "react";
2157
+ import { useEffect as useEffect7, useMemo as useMemo6, useRef as useRef8, useState as useState8 } from "react";
1795
2158
  import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2159
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2160
+ function resizeCustomTextarea(textarea) {
2161
+ textarea.style.height = "auto";
2162
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2163
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2164
+ }
2165
+ function useAutoResizeTextarea(value) {
2166
+ const textareaRef = useRef8(null);
2167
+ useEffect7(() => {
2168
+ const textarea = textareaRef.current;
2169
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2170
+ }, [value]);
2171
+ useEffect7(() => {
2172
+ const textarea = textareaRef.current;
2173
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2174
+ let previousWidth = textarea.clientWidth;
2175
+ const observer = new ResizeObserver(([entry]) => {
2176
+ if (!entry || entry.contentRect.width === previousWidth) return;
2177
+ previousWidth = entry.contentRect.width;
2178
+ resizeCustomTextarea(textarea);
2179
+ });
2180
+ observer.observe(textarea);
2181
+ return () => observer.disconnect();
2182
+ }, []);
2183
+ return textareaRef;
2184
+ }
2185
+ function indentAnswerContinuationLines(answer) {
2186
+ return answer.replaceAll("\n", "\n ");
2187
+ }
1796
2188
  function AskUserQuestionBlock({
1797
2189
  data,
1798
2190
  answered,
@@ -1801,18 +2193,19 @@ function AskUserQuestionBlock({
1801
2193
  answerData,
1802
2194
  onAnswer
1803
2195
  }) {
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(() => {
2196
+ const [selections, setSelections] = useState8(/* @__PURE__ */ new Map());
2197
+ const [customTexts, setCustomTexts] = useState8(/* @__PURE__ */ new Map());
2198
+ const [usingCustom, setUsingCustom] = useState8(/* @__PURE__ */ new Set());
2199
+ const [note, setNote] = useState8("");
2200
+ const [submitted, setSubmitted] = useState8(false);
2201
+ useEffect7(() => {
1809
2202
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1810
2203
  setSubmitted(false);
1811
2204
  }
1812
2205
  }, [sessionStatus]);
1813
2206
  const displayAnswerState = useMemo6(() => {
1814
2207
  if (!(answered && answerData)) {
1815
- return { selections, customTexts, usingCustom };
2208
+ return { selections, customTexts, usingCustom, note };
1816
2209
  }
1817
2210
  const nextSelections = /* @__PURE__ */ new Map();
1818
2211
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1828,9 +2221,10 @@ function AskUserQuestionBlock({
1828
2221
  return {
1829
2222
  selections: nextSelections,
1830
2223
  customTexts: nextCustomTexts,
1831
- usingCustom: nextUsingCustom
2224
+ usingCustom: nextUsingCustom,
2225
+ note: answerData.note ?? ""
1832
2226
  };
1833
- }, [answerData, answered, customTexts, selections, usingCustom]);
2227
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1834
2228
  const toggleOption = (qIdx, optIdx, multi) => {
1835
2229
  if (answered || submitted) return;
1836
2230
  setSelections((prev) => {
@@ -1878,6 +2272,7 @@ function AskUserQuestionBlock({
1878
2272
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1879
2273
  const handleSubmit = () => {
1880
2274
  if (answered || submitted || !allAnswered || !onAnswer) return;
2275
+ const trimmedNote = note.trim();
1881
2276
  const nextAnswerData = {
1882
2277
  selections: Object.fromEntries(
1883
2278
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1887,11 +2282,17 @@ function AskUserQuestionBlock({
1887
2282
  ),
1888
2283
  custom: Object.fromEntries(
1889
2284
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1890
- )
2285
+ ),
2286
+ ...trimmedNote ? { note: trimmedNote } : {}
1891
2287
  };
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")}`;
2288
+ const parts = data.questions.map(
2289
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2290
+ );
2291
+ const text = [
2292
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2293
+ ${parts.join("\n")}`,
2294
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2295
+ ].filter(Boolean).join("\n");
1895
2296
  setSubmitted(true);
1896
2297
  onAnswer(text, toolCallId, nextAnswerData);
1897
2298
  };
@@ -1923,6 +2324,15 @@ ${parts.join("\n")}`;
1923
2324
  },
1924
2325
  q.question
1925
2326
  )),
2327
+ /* @__PURE__ */ jsx9(
2328
+ NoteField,
2329
+ {
2330
+ answered,
2331
+ submitted,
2332
+ note: displayAnswerState.note,
2333
+ onChange: setNote
2334
+ }
2335
+ ),
1926
2336
  !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
1927
2337
  "button",
1928
2338
  {
@@ -1961,6 +2371,7 @@ function QuestionCard({
1961
2371
  onCustomChange
1962
2372
  }) {
1963
2373
  const multi = question.multiSelect ?? false;
2374
+ const customTextareaRef = useAutoResizeTextarea(customText);
1964
2375
  return /* @__PURE__ */ jsxs7("div", { children: [
1965
2376
  /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
1966
2377
  /* @__PURE__ */ jsx9(
@@ -2037,25 +2448,26 @@ function QuestionCard({
2037
2448
  "div",
2038
2449
  {
2039
2450
  className: cn(
2040
- "flex items-center gap-2 rounded-lg border transition-all",
2451
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2041
2452
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2042
2453
  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
2454
  answered && "cursor-default opacity-70"
2044
2455
  ),
2045
2456
  children: [
2046
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2457
+ /* @__PURE__ */ jsx9("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2047
2458
  /* @__PURE__ */ jsx9(
2048
- "input",
2459
+ "textarea",
2049
2460
  {
2050
- type: "text",
2461
+ ref: customTextareaRef,
2462
+ rows: 2,
2051
2463
  value: customText,
2052
- disabled: answered,
2464
+ readOnly: answered,
2053
2465
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2054
2466
  onFocus: () => onCustomFocus(qIdx),
2055
2467
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2056
2468
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2057
2469
  className: cn(
2058
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2470
+ "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
2471
  answered ? "text-xs" : "text-sm"
2060
2472
  )
2061
2473
  }
@@ -2066,6 +2478,49 @@ function QuestionCard({
2066
2478
  ] })
2067
2479
  ] });
2068
2480
  }
2481
+ function NoteField({
2482
+ answered,
2483
+ submitted,
2484
+ note,
2485
+ onChange
2486
+ }) {
2487
+ const textareaRef = useAutoResizeTextarea(note);
2488
+ const readOnly = answered || submitted;
2489
+ if (answered && !note.trim()) return null;
2490
+ return /* @__PURE__ */ jsxs7(
2491
+ "label",
2492
+ {
2493
+ className: cn(
2494
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2495
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2496
+ 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))]",
2497
+ readOnly && "cursor-default opacity-70"
2498
+ ),
2499
+ children: [
2500
+ /* @__PURE__ */ jsx9("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2501
+ /* @__PURE__ */ jsx9(
2502
+ "textarea",
2503
+ {
2504
+ ref: textareaRef,
2505
+ rows: 2,
2506
+ value: note,
2507
+ readOnly,
2508
+ onChange: (event) => {
2509
+ if (readOnly) return;
2510
+ onChange(event.target.value);
2511
+ },
2512
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2513
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2514
+ className: cn(
2515
+ "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)]",
2516
+ answered ? "text-xs" : "text-sm"
2517
+ )
2518
+ }
2519
+ )
2520
+ ]
2521
+ }
2522
+ );
2523
+ }
2069
2524
  function parseAskUserQuestion(toolResult) {
2070
2525
  if (!toolResult) return null;
2071
2526
  try {
@@ -2132,7 +2587,7 @@ function ToolCallBlock({
2132
2587
  sessionStatus,
2133
2588
  renderer
2134
2589
  }) {
2135
- const [expanded, setExpanded] = useState8(false);
2590
+ const [expanded, setExpanded] = useState9(false);
2136
2591
  const normalizedName = formatToolName(toolCall.name);
2137
2592
  if (renderer) {
2138
2593
  const custom = renderer(toolCall);
@@ -2234,45 +2689,240 @@ function buildAskUserPayload(argumentsJson) {
2234
2689
  // src/components/AssistantTurnBlock.tsx
2235
2690
  import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2236
2691
  function ThinkingBlock({ reasoning, isStreaming }) {
2237
- const [open, setOpen] = useState9(false);
2238
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2692
+ const [open, setOpen] = useState10(false);
2693
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking text-xs", children: [
2239
2694
  /* @__PURE__ */ jsxs9(
2240
2695
  "button",
2241
2696
  {
2242
2697
  type: "button",
2243
2698
  onClick: () => setOpen(!open),
2244
2699
  "aria-expanded": open,
2245
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2700
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2246
2701
  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
- ] }),
2702
+ isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u5DF2\u601D\u8003" }),
2254
2703
  /* @__PURE__ */ jsx11(
2255
- ChevronDown,
2704
+ ChevronRight,
2256
2705
  {
2257
- size: 12,
2258
- className: cn("shrink-0 transition-transform", open && "rotate-180")
2706
+ size: 14,
2707
+ className: cn(
2708
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
2709
+ open && "rotate-90 opacity-100"
2710
+ )
2259
2711
  }
2260
2712
  )
2261
2713
  ]
2262
2714
  }
2263
2715
  ),
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 })
2716
+ open && /* @__PURE__ */ jsx11("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning })
2265
2717
  ] });
2266
2718
  }
2267
2719
  function getMessageText(message) {
2268
2720
  return getTextContent(normalizeMessageContent(message.content)).trim();
2269
2721
  }
2722
+ function hasRenderableMessageContent(message) {
2723
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
2724
+ }
2725
+ function getLastContentMessage(messages) {
2726
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
2727
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
2728
+ }
2729
+ return null;
2730
+ }
2270
2731
  function findLatestReasoningMessageIndex(messages) {
2271
2732
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2272
2733
  if (messages[index].reasoning) return index;
2273
2734
  }
2274
2735
  return -1;
2275
2736
  }
2737
+ function resolveTurnDisplayMode({
2738
+ isStreaming: _isStreaming,
2739
+ displayMode
2740
+ }) {
2741
+ return displayMode;
2742
+ }
2743
+ function formatExecutionDuration(durationMs) {
2744
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
2745
+ const minutes = Math.floor(totalSeconds / 60);
2746
+ const seconds = totalSeconds % 60;
2747
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
2748
+ }
2749
+ function getExecutionDurationMs({
2750
+ messages,
2751
+ isStreaming,
2752
+ now = Date.now()
2753
+ }) {
2754
+ const knownDuration = messages.reduce(
2755
+ (total, message) => {
2756
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
2757
+ return total + message.duration_ms;
2758
+ }
2759
+ return total + (message.tool_calls ?? []).reduce(
2760
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
2761
+ 0
2762
+ );
2763
+ },
2764
+ 0
2765
+ );
2766
+ if (!isStreaming) return knownDuration;
2767
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
2768
+ if (startedAt === void 0) return knownDuration;
2769
+ return Math.max(knownDuration, now - startedAt);
2770
+ }
2771
+ function findLastExceptionalEvent(messages) {
2772
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
2773
+ const messageStatus = messages[messageIndex].status;
2774
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
2775
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
2776
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
2777
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
2778
+ const status = toolCalls[toolIndex].status;
2779
+ if (status === "error" || status === "cancelled") {
2780
+ return { messageIndex, status };
2781
+ }
2782
+ }
2783
+ }
2784
+ return null;
2785
+ }
2786
+ function executionSummaryLabel({
2787
+ messages,
2788
+ isStreaming,
2789
+ durationMs,
2790
+ sessionStatus,
2791
+ askAnswers
2792
+ }) {
2793
+ if (isStreaming) {
2794
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
2795
+ }
2796
+ if (sessionStatus === "waiting_for_input" && messages.some(
2797
+ (message) => (message.tool_calls ?? []).some(
2798
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
2799
+ )
2800
+ )) {
2801
+ return "\u7B49\u5F85\u8F93\u5165";
2802
+ }
2803
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
2804
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
2805
+ if (lastExceptionalEvent) {
2806
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
2807
+ if (lastExceptionalEvent.status === "error") {
2808
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
2809
+ }
2810
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
2811
+ }
2812
+ return completedLabel;
2813
+ }
2814
+ function executionToolTypeLabel(toolCall) {
2815
+ switch (formatToolName(toolCall.name)) {
2816
+ case "WebSearch":
2817
+ case "WebFetch":
2818
+ return "\u7F51\u7EDC\u68C0\u7D22";
2819
+ case "Bash":
2820
+ case "BgBash":
2821
+ return "\u547D\u4EE4\u6267\u884C";
2822
+ case "Read":
2823
+ case "ReadSkill":
2824
+ return "\u5185\u5BB9\u8BFB\u53D6";
2825
+ case "Write":
2826
+ case "Edit":
2827
+ case "MultiEdit":
2828
+ return "\u6587\u4EF6\u5904\u7406";
2829
+ case "Grep":
2830
+ case "Glob":
2831
+ return "\u5185\u5BB9\u641C\u7D22";
2832
+ case "Agent":
2833
+ return "\u5B50\u4EFB\u52A1";
2834
+ case "search_skills":
2835
+ return "\u6280\u80FD\u68C0\u7D22";
2836
+ case "get_skill_content":
2837
+ return "\u8BFB\u53D6\u6280\u80FD";
2838
+ case "run_skill_tool":
2839
+ return "\u6267\u884C\u6280\u80FD";
2840
+ default:
2841
+ return toolCall.display_name?.trim() || "\u6267\u884C\u6B65\u9AA4";
2842
+ }
2843
+ }
2844
+ function executionToolIntent(toolCall) {
2845
+ const normalizedName = formatToolName(toolCall.name);
2846
+ let args = null;
2847
+ try {
2848
+ const parsed = JSON.parse(toolCall.arguments);
2849
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
2850
+ } catch {
2851
+ args = null;
2852
+ }
2853
+ const getString = (key) => {
2854
+ const value = args?.[key];
2855
+ return typeof value === "string" ? value.trim() : "";
2856
+ };
2857
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || toolCall.display_name?.trim() || "";
2858
+ if (explicitIntent) return explicitIntent;
2859
+ if (normalizedName === "search_skills") return getString("query");
2860
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
2861
+ return getString("skill_name") || getString("skill");
2862
+ }
2863
+ if (normalizedName === "FinishTask") return getString("title");
2864
+ return "";
2865
+ }
2866
+ function ExecutionToolRow({ toolCall }) {
2867
+ const normalizedName = formatToolName(toolCall.name);
2868
+ const typeLabel = executionToolTypeLabel(toolCall);
2869
+ const intent = executionToolIntent(toolCall);
2870
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
2871
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
2872
+ const filePath = toolCall.status === "done" && (normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit") ? extractToolFilePath(toolCall) : null;
2873
+ const [fileOpen, setFileOpen] = useState10(false);
2874
+ const iconClass = cn(
2875
+ "size-3.5 shrink-0",
2876
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2877
+ );
2878
+ const icon = toolCall.status === "pending" ? /* @__PURE__ */ jsx11(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx11(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx11(X, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "WebSearch" || normalizedName === "WebFetch" ? /* @__PURE__ */ jsx11(Earth, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Bash" || normalizedName === "BgBash" ? /* @__PURE__ */ jsx11(Terminal, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit" ? /* @__PURE__ */ jsx11(FilePenLine, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Read" || normalizedName === "ReadSkill" ? /* @__PURE__ */ jsx11(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Grep" || normalizedName === "Glob" || normalizedName === "search_skills" ? /* @__PURE__ */ jsx11(Search, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "get_skill_content" ? /* @__PURE__ */ jsx11(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Agent" ? /* @__PURE__ */ jsx11(Bot, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx11(Wrench, { className: iconClass, "aria-hidden": "true" });
2879
+ const rowClassName = cn(
2880
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
2881
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2882
+ );
2883
+ if (!filePath) {
2884
+ return /* @__PURE__ */ jsxs9("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
2885
+ icon,
2886
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label })
2887
+ ] });
2888
+ }
2889
+ return /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
2890
+ /* @__PURE__ */ jsxs9(
2891
+ "button",
2892
+ {
2893
+ type: "button",
2894
+ "data-testid": "execution-tool-intent",
2895
+ className: cn(rowClassName, "w-full text-left"),
2896
+ title: label,
2897
+ "aria-expanded": fileOpen,
2898
+ onClick: () => setFileOpen((open) => !open),
2899
+ children: [
2900
+ icon,
2901
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: label }),
2902
+ /* @__PURE__ */ jsx11(
2903
+ ChevronRight,
2904
+ {
2905
+ size: 14,
2906
+ className: cn("shrink-0 transition-transform", fileOpen && "rotate-90"),
2907
+ "aria-hidden": "true"
2908
+ }
2909
+ )
2910
+ ]
2911
+ }
2912
+ ),
2913
+ fileOpen ? /* @__PURE__ */ jsxs9(
2914
+ "div",
2915
+ {
2916
+ className: "ml-[18px] truncate text-xs leading-[22px] text-[hsl(var(--muted-foreground))]",
2917
+ title: filePath,
2918
+ children: [
2919
+ "\u6587\u4EF6\uFF1A",
2920
+ filePath
2921
+ ]
2922
+ }
2923
+ ) : null
2924
+ ] });
2925
+ }
2276
2926
  function AssistantTurnBlock({
2277
2927
  messages,
2278
2928
  isStreaming = false,
@@ -2283,59 +2933,281 @@ function AssistantTurnBlock({
2283
2933
  sessionId
2284
2934
  }) {
2285
2935
  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
2936
+ const hasFailedWithoutContent = messages.some(
2937
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
2938
+ );
2939
+ const finalMessage = getLastContentMessage(messages);
2940
+ const hasExecutionProcess = messages.some(
2941
+ (message) => message.reasoning || (message.tool_calls?.length ?? 0) > 0
2288
2942
  );
2289
2943
  const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
2944
+ const hasActionableToolCall = messages.some(
2945
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
2946
+ (toolCall) => toolCall.status === "error" || toolCall.status === "cancelled"
2947
+ )
2948
+ );
2949
+ const questionToolCalls = messages.flatMap(
2950
+ (message) => (message.tool_calls ?? []).filter(
2951
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
2952
+ )
2953
+ );
2954
+ const [displayMode, setDisplayMode] = useState10(
2955
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
2956
+ );
2957
+ const userSelectedDisplayModeRef = useRef9(false);
2958
+ const wasStreamingRef = useRef9(isStreaming);
2959
+ useEffect8(() => {
2960
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
2961
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
2962
+ }
2963
+ wasStreamingRef.current = isStreaming;
2964
+ }, [hasActionableToolCall, isStreaming]);
2965
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
2966
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
2967
+ const [clock, setClock] = useState10(() => Date.now());
2968
+ const hasLiveStartTime = messages.some(
2969
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
2970
+ );
2971
+ useEffect8(() => {
2972
+ if (!isStreaming || !hasLiveStartTime) return;
2973
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
2974
+ return () => window.clearInterval(timer);
2975
+ }, [hasLiveStartTime, isStreaming]);
2976
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
2977
+ if (!hasExecutionProcess) {
2978
+ return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2979
+ 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" }),
2980
+ 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" }),
2981
+ messages.map((message, index) => {
2982
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx11(
2983
+ "div",
2984
+ {
2985
+ className: "flex flex-col gap-3",
2986
+ children: /* @__PURE__ */ jsx11(
2987
+ AssistantMessageContent,
2988
+ {
2989
+ message,
2990
+ sessionId,
2991
+ streaming: isStreaming && index === messages.length - 1
2992
+ }
2993
+ )
2994
+ },
2995
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2996
+ ) : null;
2997
+ }),
2998
+ isStreaming && !finalMessage ? /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." }) : null
2999
+ ] });
3000
+ }
2290
3001
  return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2291
3002
  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) => {
3003
+ 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" }),
3004
+ /* @__PURE__ */ jsxs9("div", { className: "flex w-full items-start gap-2.5", children: [
3005
+ /* @__PURE__ */ jsx11(
3006
+ "span",
3007
+ {
3008
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3009
+ "aria-hidden": "true",
3010
+ children: /* @__PURE__ */ jsx11(Bot, { size: 16 })
3011
+ }
3012
+ ),
3013
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3014
+ /* @__PURE__ */ jsxs9(
3015
+ "button",
3016
+ {
3017
+ type: "button",
3018
+ onClick: () => {
3019
+ userSelectedDisplayModeRef.current = true;
3020
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
3021
+ },
3022
+ "aria-expanded": effectiveMode === "detail",
3023
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3024
+ "data-testid": "assistant-execution-summary",
3025
+ 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",
3026
+ children: [
3027
+ /* @__PURE__ */ jsx11("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3028
+ messages,
3029
+ isStreaming,
3030
+ durationMs: liveExecutionDurationMs,
3031
+ sessionStatus,
3032
+ askAnswers
3033
+ }) }),
3034
+ /* @__PURE__ */ jsx11(
3035
+ ChevronRight,
3036
+ {
3037
+ size: 14,
3038
+ className: cn(
3039
+ "shrink-0 transition-transform duration-300",
3040
+ effectiveMode === "detail" && "rotate-90"
3041
+ ),
3042
+ "aria-hidden": "true"
3043
+ }
3044
+ )
3045
+ ]
3046
+ }
3047
+ ),
3048
+ /* @__PURE__ */ jsx11("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3049
+ ] })
3050
+ ] }),
3051
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx11("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
2293
3052
  const isLast = index === messages.length - 1;
2294
3053
  const streamingThis = isStreaming && isLast;
2295
3054
  const text = getMessageText(message);
2296
- const toolCalls = message.tool_calls ?? [];
3055
+ const toolCalls = (message.tool_calls ?? []).filter(
3056
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion"
3057
+ );
2297
3058
  const showReasoning = !!message.reasoning && (!isStreaming || index === latestReasoningIndex);
2298
3059
  return /* @__PURE__ */ jsxs9(
2299
3060
  "div",
2300
3061
  {
2301
3062
  className: "flex flex-col gap-3",
2302
3063
  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,
3064
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3065
+ hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx11(
3066
+ AssistantMessageContent,
2306
3067
  {
2307
- mode: streamingThis ? "streaming" : "static",
2308
- className: "blade-chat-prose",
3068
+ message,
2309
3069
  sessionId,
2310
- children: text
3070
+ streaming: streamingThis,
3071
+ compact: true
2311
3072
  }
2312
- ) }),
2313
- toolCalls.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-2", children: toolCalls.map(
2314
- (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(
2315
- ToolCallBlock,
2316
- {
2317
- toolCall,
2318
- answerData: askAnswers?.[toolCall.id],
2319
- onAnswer,
2320
- answered: sessionStatus !== "waiting_for_input",
2321
- sessionStatus,
2322
- renderer: toolCallRenderer
2323
- },
2324
- toolCall.id
2325
- )
2326
- ) })
3073
+ ) : null,
3074
+ toolCalls.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3075
+ const custom = toolCallRenderer?.(toolCall);
3076
+ 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);
3077
+ }) }) : null
2327
3078
  ]
2328
3079
  },
2329
3080
  message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2330
3081
  );
2331
- }),
2332
- isStreaming && !hasAnyContent && /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
3082
+ }) }) : null,
3083
+ finalMessage ? /* @__PURE__ */ jsx11("div", { className: "ml-10", children: /* @__PURE__ */ jsx11(
3084
+ AssistantMessageContent,
3085
+ {
3086
+ message: finalMessage,
3087
+ sessionId,
3088
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3089
+ }
3090
+ ) }) : null,
3091
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx11(
3092
+ ToolCallBlock,
3093
+ {
3094
+ toolCall,
3095
+ answerData: askAnswers?.[toolCall.id],
3096
+ onAnswer,
3097
+ answered: sessionStatus !== "waiting_for_input",
3098
+ sessionStatus,
3099
+ renderer: toolCallRenderer
3100
+ },
3101
+ toolCall.id
3102
+ ))
2333
3103
  ] });
2334
3104
  }
3105
+ function AssistantMessageContent({
3106
+ message,
3107
+ sessionId,
3108
+ streaming,
3109
+ compact = false
3110
+ }) {
3111
+ const text = getMessageText(message);
3112
+ const imageParts = getImageParts(message.content);
3113
+ const fileParts = getFileParts(message.content);
3114
+ const failed = message.status === "failed";
3115
+ 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;
3116
+ const textContent = text ? /* @__PURE__ */ jsx11(
3117
+ "div",
3118
+ {
3119
+ className: cn(
3120
+ "blade-chat-assistant-text",
3121
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3122
+ ),
3123
+ children: /* @__PURE__ */ jsx11(
3124
+ MarkdownContent,
3125
+ {
3126
+ mode: streaming ? "streaming" : "static",
3127
+ className: "blade-chat-prose",
3128
+ sessionId,
3129
+ children: text
3130
+ }
3131
+ )
3132
+ }
3133
+ ) : null;
3134
+ if (imageParts.length === 0 && fileParts.length === 0) {
3135
+ if (!failed) return textContent;
3136
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-2", children: [
3137
+ failedBadge,
3138
+ textContent
3139
+ ] }) : null;
3140
+ }
3141
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-3", children: [
3142
+ failedBadge,
3143
+ imageParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx11(
3144
+ "img",
3145
+ {
3146
+ src: part.image_url.url,
3147
+ alt: "\u6D88\u606F\u9644\u4EF6",
3148
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3149
+ },
3150
+ part.image_url.url
3151
+ )) }) : null,
3152
+ fileParts.length > 0 ? /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs9(
3153
+ "div",
3154
+ {
3155
+ 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))]",
3156
+ title: part.name,
3157
+ children: [
3158
+ /* @__PURE__ */ jsx11(FileText, { size: 12, className: "shrink-0" }),
3159
+ /* @__PURE__ */ jsx11("span", { className: "max-w-56 truncate", children: part.name })
3160
+ ]
3161
+ },
3162
+ `${part.name}-${part.data.slice(0, 32)}`
3163
+ )) }) : null,
3164
+ textContent
3165
+ ] });
3166
+ }
3167
+
3168
+ // src/components/ContextCard.tsx
3169
+ import {
3170
+ getContextDisplayState
3171
+ } from "@blade-hq/agent-client";
3172
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3173
+ function ContextCard({ context, className }) {
3174
+ const display = getContextDisplayState(context);
3175
+ return /* @__PURE__ */ jsxs10(
3176
+ "details",
3177
+ {
3178
+ className: `blade-chat-context-card group rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
3179
+ children: [
3180
+ /* @__PURE__ */ jsxs10("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: [
3181
+ /* @__PURE__ */ jsx12(
3182
+ Layers,
3183
+ {
3184
+ size: 15,
3185
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
3186
+ "aria-hidden": "true"
3187
+ }
3188
+ ),
3189
+ /* @__PURE__ */ jsxs10("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
3190
+ /* @__PURE__ */ jsx12("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
3191
+ /* @__PURE__ */ jsx12("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
3192
+ ] }),
3193
+ /* @__PURE__ */ jsx12(
3194
+ ChevronDown,
3195
+ {
3196
+ size: 14,
3197
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open:rotate-180",
3198
+ "aria-hidden": "true"
3199
+ }
3200
+ )
3201
+ ] }),
3202
+ /* @__PURE__ */ jsx12("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 })
3203
+ ]
3204
+ }
3205
+ );
3206
+ }
2335
3207
 
2336
3208
  // src/components/RenderErrorBoundary.tsx
2337
3209
  import { Component } from "react";
2338
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3210
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2339
3211
  function getFirstComponentName(componentStack) {
2340
3212
  const match = componentStack.match(/\n\s+at\s+([^\s(]+)/);
2341
3213
  return match?.[1] ?? null;
@@ -2368,26 +3240,26 @@ var RenderErrorBoundary = class extends Component {
2368
3240
  return children;
2369
3241
  }
2370
3242
  const componentName = getFirstComponentName(componentStack);
2371
- return /* @__PURE__ */ jsx12("div", { className: "blade-chat-render-error rounded-xl border border-amber-500/30 bg-amber-500/8 px-4 py-3 text-sm text-amber-100", children: /* @__PURE__ */ jsxs10("div", { className: "flex items-start gap-2", children: [
2372
- /* @__PURE__ */ jsx12(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
2373
- /* @__PURE__ */ jsxs10("div", { className: "min-w-0 flex-1", children: [
2374
- /* @__PURE__ */ jsxs10("div", { className: "font-medium", children: [
3243
+ return /* @__PURE__ */ jsx13("div", { className: "blade-chat-render-error rounded-xl border border-amber-500/30 bg-amber-500/8 px-4 py-3 text-sm text-amber-100", children: /* @__PURE__ */ jsxs11("div", { className: "flex items-start gap-2", children: [
3244
+ /* @__PURE__ */ jsx13(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
3245
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1", children: [
3246
+ /* @__PURE__ */ jsxs11("div", { className: "font-medium", children: [
2375
3247
  label,
2376
3248
  "\u6E32\u67D3\u5931\u8D25"
2377
3249
  ] }),
2378
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
3250
+ /* @__PURE__ */ jsxs11("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
2379
3251
  componentName ? `\u7EC4\u4EF6\uFF1A${componentName}\u3002` : null,
2380
3252
  error.message || "\u53D1\u751F\u4E86\u672A\u9884\u671F\u7684\u6E32\u67D3\u9519\u8BEF\u3002"
2381
3253
  ] }),
2382
- details ? /* @__PURE__ */ jsx12("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
3254
+ details ? /* @__PURE__ */ jsx13("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
2383
3255
  ] })
2384
3256
  ] }) });
2385
3257
  }
2386
3258
  };
2387
3259
 
2388
3260
  // src/components/PostChatFollowupBlock.tsx
2389
- import { useCallback as useCallback5, useEffect as useEffect6, useRef as useRef6, useState as useState10 } from "react";
2390
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3261
+ import { useCallback as useCallback6, useEffect as useEffect9, useRef as useRef10, useState as useState11 } from "react";
3262
+ import { Fragment as Fragment2, jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2391
3263
  function emitInteraction(callback, event) {
2392
3264
  try {
2393
3265
  callback?.(event);
@@ -2397,9 +3269,6 @@ function emitInteraction(callback, event) {
2397
3269
  function basename(path) {
2398
3270
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2399
3271
  }
2400
- function isVideo(path) {
2401
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2402
- }
2403
3272
  function ArtifactCard({
2404
3273
  artifact,
2405
3274
  sessionId,
@@ -2409,10 +3278,10 @@ function ArtifactCard({
2409
3278
  onArtifactOpened
2410
3279
  }) {
2411
3280
  const client = useBladeClient();
2412
- const [downloading, setDownloading] = useState10(false);
3281
+ const [downloading, setDownloading] = useState11(false);
2413
3282
  const name = artifact.label || basename(artifact.target);
2414
3283
  if (artifact.kind === "link") {
2415
- return /* @__PURE__ */ jsxs11(
3284
+ return /* @__PURE__ */ jsxs12(
2416
3285
  "a",
2417
3286
  {
2418
3287
  href: artifact.target,
@@ -2423,51 +3292,55 @@ function ArtifactCard({
2423
3292
  ${artifact.target}`,
2424
3293
  className: "group relative flex min-w-0 items-center gap-1.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-2 py-1.5 text-xs text-[hsl(var(--card-foreground))] hover:bg-[hsl(var(--accent))]",
2425
3294
  children: [
2426
- /* @__PURE__ */ jsx13(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
2427
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
2428
- /* @__PURE__ */ jsx13(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
3295
+ /* @__PURE__ */ jsx14(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
3296
+ /* @__PURE__ */ jsx14("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
3297
+ /* @__PURE__ */ jsx14(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2429
3298
  ]
2430
3299
  }
2431
3300
  );
2432
3301
  }
2433
- const Icon2 = isVideo(artifact.target) ? Film : File;
2434
- return /* @__PURE__ */ jsxs11(
2435
- "button",
3302
+ const fileName = basename(artifact.target);
3303
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3304
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3305
+ ) : void 0;
3306
+ const handleDownload = async (event) => {
3307
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3308
+ event.preventDefault();
3309
+ if (!sessionId || downloading) return;
3310
+ setDownloading(true);
3311
+ emitInteraction(onInteraction, {
3312
+ type: "artifact_download_started",
3313
+ sessionId,
3314
+ assistantEntryId,
3315
+ artifactIndex,
3316
+ artifactKind: "file"
3317
+ });
3318
+ try {
3319
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3320
+ emitInteraction(onInteraction, {
3321
+ type: "artifact_download_succeeded",
3322
+ sessionId,
3323
+ assistantEntryId,
3324
+ artifactIndex,
3325
+ artifactKind: "file"
3326
+ });
3327
+ } catch {
3328
+ } finally {
3329
+ setDownloading(false);
3330
+ }
3331
+ };
3332
+ return /* @__PURE__ */ jsx14(
3333
+ "a",
2436
3334
  {
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
- ]
3335
+ href: downloadUrl,
3336
+ download: fileName,
3337
+ onClick: handleDownload,
3338
+ title: fileName,
3339
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3340
+ "aria-disabled": !sessionId || void 0,
3341
+ "aria-busy": downloading || void 0,
3342
+ 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",
3343
+ children: fileName
2471
3344
  }
2472
3345
  );
2473
3346
  }
@@ -2484,17 +3357,17 @@ function feedbackReasonLabel(reason) {
2484
3357
  }
2485
3358
  function HistoricalResultFeedback({ feedback }) {
2486
3359
  const label = feedbackReasonLabel(feedback.reason);
2487
- return /* @__PURE__ */ jsxs11(
3360
+ return /* @__PURE__ */ jsxs12(
2488
3361
  "section",
2489
3362
  {
2490
3363
  "aria-label": "\u5386\u53F2\u7ED3\u679C\u53CD\u9988",
2491
3364
  className: "mt-3 w-fit max-w-full rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.2)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]",
2492
3365
  children: [
2493
- /* @__PURE__ */ jsxs11("span", { children: [
3366
+ /* @__PURE__ */ jsxs12("span", { children: [
2494
3367
  "\u4F60\u5BF9\u6B64\u8F6E\u7ED3\u679C\u7684\u8BC4\u4EF7\uFF1A",
2495
3368
  feedback.helpful ? "\u6709\u5E2E\u52A9" : "\u6CA1\u5E2E\u52A9"
2496
3369
  ] }),
2497
- label ? /* @__PURE__ */ jsxs11("span", { children: [
3370
+ label ? /* @__PURE__ */ jsxs12("span", { children: [
2498
3371
  " \xB7 ",
2499
3372
  label
2500
3373
  ] }) : null
@@ -2511,15 +3384,15 @@ function ResultFeedback({
2511
3384
  onFeedbackSaved
2512
3385
  }) {
2513
3386
  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);
3387
+ const [saved, setSaved] = useState11(savedFeedback ?? null);
3388
+ const [helpful, setHelpful] = useState11(savedFeedback?.helpful ?? null);
3389
+ const [reason, setReason] = useState11(savedFeedback?.reason ?? null);
3390
+ const [saving, setSaving] = useState11(false);
3391
+ const [saveError, setSaveError] = useState11(false);
3392
+ const reportedShown = useRef10(false);
3393
+ const latestChoice = useRef10(null);
2521
3394
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2522
- useEffect6(() => {
3395
+ useEffect9(() => {
2523
3396
  if (!eligible || reportedShown.current) return;
2524
3397
  reportedShown.current = true;
2525
3398
  emitInteraction(onInteraction, {
@@ -2528,13 +3401,13 @@ function ResultFeedback({
2528
3401
  assistantEntryId: followup.assistant_entry_id
2529
3402
  });
2530
3403
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2531
- useEffect6(() => {
3404
+ useEffect9(() => {
2532
3405
  if (!savedFeedback || latestChoice.current) return;
2533
3406
  setSaved(savedFeedback);
2534
3407
  setHelpful(savedFeedback.helpful);
2535
3408
  setReason(savedFeedback.reason);
2536
3409
  }, [savedFeedback]);
2537
- const submit = useCallback5(
3410
+ const submit = useCallback6(
2538
3411
  async (nextHelpful, nextReason) => {
2539
3412
  if (!sessionId) return;
2540
3413
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2569,15 +3442,15 @@ function ResultFeedback({
2569
3442
  [client, followup.assistant_entry_id, onFeedbackSaved, onInteraction, sessionId]
2570
3443
  );
2571
3444
  if (!eligible) return null;
2572
- return /* @__PURE__ */ jsxs11(
3445
+ return /* @__PURE__ */ jsxs12(
2573
3446
  "section",
2574
3447
  {
2575
3448
  "aria-label": "\u7ED3\u679C\u53CD\u9988",
2576
3449
  className: "flex flex-col gap-2 border-t border-[hsl(var(--border))] pt-3",
2577
3450
  children: [
2578
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u76EE\u524D\u7684\u6574\u4F53\u7ED3\u679C\u6709\u5E2E\u52A9\u5417\uFF1F" }),
2579
- /* @__PURE__ */ jsxs11("div", { className: "flex flex-wrap gap-1.5", children: [
2580
- /* @__PURE__ */ jsx13(
3451
+ /* @__PURE__ */ jsx14("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u76EE\u524D\u7684\u6574\u4F53\u7ED3\u679C\u6709\u5E2E\u52A9\u5417\uFF1F" }),
3452
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap gap-1.5", children: [
3453
+ /* @__PURE__ */ jsx14(
2581
3454
  "button",
2582
3455
  {
2583
3456
  type: "button",
@@ -2588,7 +3461,7 @@ function ResultFeedback({
2588
3461
  children: "\u6709\u5E2E\u52A9"
2589
3462
  }
2590
3463
  ),
2591
- /* @__PURE__ */ jsx13(
3464
+ /* @__PURE__ */ jsx14(
2592
3465
  "button",
2593
3466
  {
2594
3467
  type: "button",
@@ -2600,7 +3473,7 @@ function ResultFeedback({
2600
3473
  }
2601
3474
  )
2602
3475
  ] }),
2603
- helpful === false ? /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-1.5", "aria-label": "\u6CA1\u5E2E\u52A9\u7684\u4E3B\u8981\u539F\u56E0", children: FEEDBACK_REASONS.map((item) => /* @__PURE__ */ jsx13(
3476
+ helpful === false ? /* @__PURE__ */ jsx14("div", { className: "flex flex-wrap gap-1.5", "aria-label": "\u6CA1\u5E2E\u52A9\u7684\u4E3B\u8981\u539F\u56E0", children: FEEDBACK_REASONS.map((item) => /* @__PURE__ */ jsx14(
2604
3477
  "button",
2605
3478
  {
2606
3479
  type: "button",
@@ -2612,9 +3485,9 @@ function ResultFeedback({
2612
3485
  },
2613
3486
  item.value
2614
3487
  )) }) : null,
2615
- saveError ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
2616
- /* @__PURE__ */ jsx13("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
2617
- /* @__PURE__ */ jsx13(
3488
+ saveError ? /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
3489
+ /* @__PURE__ */ jsx14("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
3490
+ /* @__PURE__ */ jsx14(
2618
3491
  "button",
2619
3492
  {
2620
3493
  type: "button",
@@ -2626,7 +3499,7 @@ function ResultFeedback({
2626
3499
  children: "\u91CD\u8BD5"
2627
3500
  }
2628
3501
  )
2629
- ] }) : saved ? /* @__PURE__ */ jsx13("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
3502
+ ] }) : saved ? /* @__PURE__ */ jsx14("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
2630
3503
  ]
2631
3504
  }
2632
3505
  );
@@ -2640,14 +3513,14 @@ function PostChatFollowupBlock({
2640
3513
  savedFeedback,
2641
3514
  onFeedbackSaved
2642
3515
  }) {
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());
3516
+ const [expanded, setExpanded] = useState11(false);
3517
+ const adopted = useRef10(/* @__PURE__ */ new Set());
3518
+ const reportedSuggestions = useRef10(false);
3519
+ const reportedArtifacts = useRef10(/* @__PURE__ */ new Set());
3520
+ const openedArtifacts = useRef10(/* @__PURE__ */ new Set());
2648
3521
  const artifacts = followup.final_artifacts ?? [];
2649
3522
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2650
- useEffect6(() => {
3523
+ useEffect9(() => {
2651
3524
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2652
3525
  reportedSuggestions.current = true;
2653
3526
  emitInteraction(onInteraction, {
@@ -2677,7 +3550,7 @@ function PostChatFollowupBlock({
2677
3550
  sessionId,
2678
3551
  visibleArtifacts
2679
3552
  ]);
2680
- const reportArtifactOpened = useCallback5(
3553
+ const reportArtifactOpened = useCallback6(
2681
3554
  (artifactIndex, artifactKind) => {
2682
3555
  if (openedArtifacts.current.has(artifactIndex)) return;
2683
3556
  openedArtifacts.current.add(artifactIndex);
@@ -2693,15 +3566,15 @@ function PostChatFollowupBlock({
2693
3566
  );
2694
3567
  if (!followup.recaption && artifacts.length === 0 && followup.suggestions.length === 0 && !followup.feedback_eligible)
2695
3568
  return null;
2696
- return /* @__PURE__ */ jsxs11("div", { className: "mt-3 flex w-fit max-w-full flex-col gap-3 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.28)] p-3 sm:max-w-[680px]", children: [
2697
- followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
2698
- /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
2699
- /* @__PURE__ */ jsx13(Sparkles, { size: 14 }),
3569
+ return /* @__PURE__ */ jsxs12("div", { className: "mt-3 flex w-fit max-w-full flex-col gap-3 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.28)] p-3 sm:max-w-[680px]", children: [
3570
+ followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
3571
+ /* @__PURE__ */ jsxs12("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
3572
+ /* @__PURE__ */ jsx14(Sparkles, { size: 14 }),
2700
3573
  "\u672C\u8F6E\u5C0F\u7ED3"
2701
3574
  ] }),
2702
- followup.recaption ? /* @__PURE__ */ jsx13("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
2703
- artifacts.length > 0 ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
2704
- /* @__PURE__ */ jsx13("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx13(
3575
+ followup.recaption ? /* @__PURE__ */ jsx14("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
3576
+ artifacts.length > 0 ? /* @__PURE__ */ jsxs12(Fragment2, { children: [
3577
+ /* @__PURE__ */ jsx14("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx14(
2705
3578
  ArtifactCard,
2706
3579
  {
2707
3580
  artifact,
@@ -2713,7 +3586,7 @@ function PostChatFollowupBlock({
2713
3586
  },
2714
3587
  `${artifact.kind}:${artifactIndex}`
2715
3588
  )) }),
2716
- artifacts.length > 3 ? /* @__PURE__ */ jsxs11(
3589
+ artifacts.length > 3 ? /* @__PURE__ */ jsxs12(
2717
3590
  "button",
2718
3591
  {
2719
3592
  type: "button",
@@ -2722,15 +3595,15 @@ function PostChatFollowupBlock({
2722
3595
  className: "flex w-fit items-center gap-0.5 text-[11px] text-[hsl(var(--muted-foreground))]",
2723
3596
  children: [
2724
3597
  expanded ? "\u6536\u8D77" : `\u5C55\u5F00 ${artifacts.length - 3} \u4E2A`,
2725
- /* @__PURE__ */ jsx13(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
3598
+ /* @__PURE__ */ jsx14(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
2726
3599
  ]
2727
3600
  }
2728
3601
  ) : null
2729
3602
  ] }) : null
2730
3603
  ] }) : null,
2731
- followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs11("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
2732
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
2733
- followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs11(
3604
+ followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs12("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
3605
+ /* @__PURE__ */ jsx14("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
3606
+ followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs12(
2734
3607
  "button",
2735
3608
  {
2736
3609
  type: "button",
@@ -2749,14 +3622,14 @@ function PostChatFollowupBlock({
2749
3622
  },
2750
3623
  className: "group flex items-center gap-2 rounded-xl bg-[hsl(var(--muted)/0.62)] px-3 py-2 text-left text-[13px] disabled:cursor-default disabled:opacity-60",
2751
3624
  children: [
2752
- /* @__PURE__ */ jsx13("span", { children: suggestion }),
2753
- /* @__PURE__ */ jsx13(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
3625
+ /* @__PURE__ */ jsx14("span", { children: suggestion }),
3626
+ /* @__PURE__ */ jsx14(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
2754
3627
  ]
2755
3628
  },
2756
3629
  suggestion
2757
3630
  ))
2758
3631
  ] }) : null,
2759
- /* @__PURE__ */ jsx13(
3632
+ /* @__PURE__ */ jsx14(
2760
3633
  ResultFeedback,
2761
3634
  {
2762
3635
  followup,
@@ -2771,8 +3644,8 @@ function PostChatFollowupBlock({
2771
3644
  }
2772
3645
 
2773
3646
  // src/components/UserMessageBubble.tsx
2774
- import { getFileParts, getImageParts, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
2775
- import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3647
+ import { getFileParts as getFileParts2, getImageParts as getImageParts2, getTextContent as getTextContent2 } from "@blade-hq/agent-client";
3648
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2776
3649
  function isUserMessage(message) {
2777
3650
  return message.role === "user";
2778
3651
  }
@@ -2782,10 +3655,10 @@ function isErrorMessage(message) {
2782
3655
  var isSending = (message) => message.status === "streaming";
2783
3656
  function UserMessageBubble({ message, className }) {
2784
3657
  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(
3658
+ const fileParts = getFileParts2(message.content);
3659
+ const imageParts = getImageParts2(message.content);
3660
+ 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: [
3661
+ imageParts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx15(
2789
3662
  "img",
2790
3663
  {
2791
3664
  src: part.image_url.url,
@@ -2794,21 +3667,21 @@ function UserMessageBubble({ message, className }) {
2794
3667
  },
2795
3668
  part.image_url.url
2796
3669
  )) }),
2797
- fileParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs12(
3670
+ fileParts.length > 0 && /* @__PURE__ */ jsx15("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs13(
2798
3671
  "div",
2799
3672
  {
2800
3673
  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
3674
  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 })
3675
+ /* @__PURE__ */ jsx15(FileText, { size: 12, className: "shrink-0" }),
3676
+ /* @__PURE__ */ jsx15("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
2804
3677
  ]
2805
3678
  },
2806
3679
  `${part.name}-${part.data.length}`
2807
3680
  )) }),
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" })
3681
+ 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 }) }),
3682
+ 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: [
3683
+ /* @__PURE__ */ jsx15(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
3684
+ /* @__PURE__ */ jsx15("span", { children: "\u53D1\u9001\u4E2D" })
2812
3685
  ] })
2813
3686
  ] }) });
2814
3687
  }
@@ -2817,11 +3690,11 @@ function ErrorMessageBlock({
2817
3690
  className
2818
3691
  }) {
2819
3692
  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 }) });
3693
+ return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-error-row flex justify-center", className), children: /* @__PURE__ */ jsx15("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 }) });
2821
3694
  }
2822
3695
 
2823
3696
  // src/components/MessageList.tsx
2824
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
3697
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
2825
3698
  function parseModeChange(message) {
2826
3699
  if (message.kind !== "mode_change" || typeof message.content !== "string") {
2827
3700
  return null;
@@ -2870,10 +3743,14 @@ function MessageList({
2870
3743
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2871
3744
  onResultFeedbackSaved
2872
3745
  }) {
3746
+ const userMessages = messages.filter((message) => isUserMessage(message));
3747
+ const latestUserMessage = userMessages.at(-1);
3748
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
2873
3749
  const renderBlocks = useMemo7(() => {
2874
3750
  const visible = messages.filter((message) => {
2875
3751
  if ((message.loop_name ?? "root") !== "root") return false;
2876
3752
  if (isHiddenInternalMessage(message)) return false;
3753
+ if (message.kind === "context") return Boolean(message.context);
2877
3754
  if (message.kind === "compaction") return true;
2878
3755
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2879
3756
  });
@@ -2908,6 +3785,15 @@ function MessageList({
2908
3785
  blocks.push({ type: "compaction", key: message.entry_id ?? `compaction-${blocks.length}` });
2909
3786
  continue;
2910
3787
  }
3788
+ if (message.kind === "context" && message.context) {
3789
+ flushAssistant();
3790
+ blocks.push({
3791
+ type: "context",
3792
+ message,
3793
+ key: message.entry_id ?? `context-${blocks.length}`
3794
+ });
3795
+ continue;
3796
+ }
2911
3797
  if (message.role === "assistant") {
2912
3798
  assistantBuffer.push(message);
2913
3799
  continue;
@@ -2916,7 +3802,7 @@ function MessageList({
2916
3802
  blocks.push({
2917
3803
  type: "message",
2918
3804
  message,
2919
- key: message.entry_id ?? `${message.role}-${blocks.length}`
3805
+ key: message.render_id ?? message.entry_id ?? `${message.role}-${blocks.length}`
2920
3806
  });
2921
3807
  }
2922
3808
  flushAssistant();
@@ -2943,98 +3829,151 @@ function MessageList({
2943
3829
  }
2944
3830
  return blocks;
2945
3831
  }, [messages, isStreaming]);
2946
- return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs13(StickToBottom, { className: "h-full overflow-y-hidden", initial: "instant", resize: "instant", children: [
2947
- /* @__PURE__ */ jsx15(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsx15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs13("div", { className: "flex min-w-0 flex-col", children: [
2948
- renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs13("div", { className: "blade-chat-empty", children: [
2949
- /* @__PURE__ */ jsx15(MessageSquare, { size: 40, strokeWidth: 1.5 }),
2950
- /* @__PURE__ */ jsx15("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
2951
- /* @__PURE__ */ jsx15("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
2952
- ] }) : renderBlocks.map((block) => {
2953
- if (block.type === "message") {
2954
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx15(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx15(ErrorMessageBlock, { message: block.message }) : null }, block.key);
2955
- }
2956
- if (block.type === "assistant_turn") {
2957
- const blockFeedback = block.messages.map(
2958
- (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
2959
- ).find((feedback) => feedback != null);
2960
- const hasActiveFollowup = Boolean(
2961
- postChatFollowup && block.messages.some(
2962
- (message) => message.entry_id === postChatFollowup.assistant_entry_id
2963
- )
2964
- );
2965
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs13(
2966
- RenderErrorBoundary,
2967
- {
2968
- label: "\u52A9\u624B\u6D88\u606F",
2969
- details: block.key,
2970
- resetKey: getMessageResetSignature(block.messages),
2971
- children: [
2972
- /* @__PURE__ */ jsx15(
2973
- AssistantTurnBlock,
2974
- {
2975
- messages: block.messages,
2976
- isStreaming: block.isStreaming,
2977
- askAnswers,
2978
- onAnswer,
2979
- sessionStatus,
2980
- toolCallRenderer,
2981
- sessionId
2982
- }
2983
- ),
2984
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
2985
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
2986
- PostChatFollowupBlock,
2987
- {
2988
- followup: postChatFollowup,
2989
- sessionId,
2990
- onSuggestion,
2991
- isViewer,
2992
- onInteraction: onFollowupInteraction,
2993
- savedFeedback: blockFeedback,
2994
- onFeedbackSaved: onResultFeedbackSaved
2995
- }
2996
- ) : null
2997
- ]
3832
+ return /* @__PURE__ */ jsx16("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs14(
3833
+ StickToBottom,
3834
+ {
3835
+ className: "h-full overflow-y-hidden",
3836
+ initial: "instant",
3837
+ resize: "instant",
3838
+ children: [
3839
+ /* @__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: [
3840
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs14("div", { className: "blade-chat-empty", children: [
3841
+ /* @__PURE__ */ jsx16(MessageSquare, { size: 40, strokeWidth: 1.5 }),
3842
+ /* @__PURE__ */ jsx16("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
3843
+ /* @__PURE__ */ jsx16("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
3844
+ ] }) : renderBlocks.map((block) => {
3845
+ if (block.type === "message") {
3846
+ return /* @__PURE__ */ 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);
2998
3847
  }
2999
- ) }, block.key);
3000
- }
3001
- if (block.type === "compaction") {
3002
- return /* @__PURE__ */ jsxs13(
3003
- "div",
3004
- {
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
- ]
3010
- },
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
- ] }) });
3848
+ if (block.type === "assistant_turn") {
3849
+ const blockFeedback = block.messages.map(
3850
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
3851
+ ).find((feedback) => feedback != null);
3852
+ const hasActiveFollowup = Boolean(
3853
+ postChatFollowup && block.messages.some(
3854
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
3855
+ )
3856
+ );
3857
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs14(
3858
+ RenderErrorBoundary,
3859
+ {
3860
+ label: "\u52A9\u624B\u6D88\u606F",
3861
+ details: block.key,
3862
+ resetKey: getMessageResetSignature(block.messages),
3863
+ children: [
3864
+ /* @__PURE__ */ jsx16(
3865
+ AssistantTurnBlock,
3866
+ {
3867
+ messages: block.messages,
3868
+ isStreaming: block.isStreaming,
3869
+ askAnswers,
3870
+ onAnswer,
3871
+ sessionStatus,
3872
+ toolCallRenderer,
3873
+ sessionId
3874
+ }
3875
+ ),
3876
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx16(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
3877
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx16(
3878
+ PostChatFollowupBlock,
3879
+ {
3880
+ followup: postChatFollowup,
3881
+ sessionId,
3882
+ onSuggestion,
3883
+ isViewer,
3884
+ onInteraction: onFollowupInteraction,
3885
+ savedFeedback: blockFeedback,
3886
+ onFeedbackSaved: onResultFeedbackSaved
3887
+ }
3888
+ ) : null
3889
+ ]
3890
+ }
3891
+ ) }, block.key);
3892
+ }
3893
+ if (block.type === "context") {
3894
+ return /* @__PURE__ */ jsx16("div", { "data-entry-id": block.message.entry_id, children: /* @__PURE__ */ jsx16(ContextCard, { context: block.message.context }) }, block.key);
3895
+ }
3896
+ if (block.type === "compaction") {
3897
+ return /* @__PURE__ */ jsxs14(
3898
+ "div",
3899
+ {
3900
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
3901
+ children: [
3902
+ /* @__PURE__ */ jsx16(Layers, { size: 12 }),
3903
+ /* @__PURE__ */ jsx16("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
3904
+ ]
3905
+ },
3906
+ block.key
3907
+ );
3908
+ }
3909
+ return /* @__PURE__ */ jsx16(PlanningDivider, { kind: block.kind }, block.key);
3910
+ }),
3911
+ 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
3912
+ ] }) }) }),
3913
+ /* @__PURE__ */ jsx16(
3914
+ PinLatestUserMessage,
3915
+ {
3916
+ userMessageCount: userMessages.length,
3917
+ shouldPinLatestUser,
3918
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3919
+ },
3920
+ sessionId ?? "no-session"
3921
+ ),
3922
+ /* @__PURE__ */ jsx16(ScrollToBottomButton, {})
3923
+ ]
3924
+ },
3925
+ sessionId ?? "no-session"
3926
+ ) });
3021
3927
  }
3022
- function AutoScrollOnUserSend({ userMessageCount }) {
3023
- const { scrollToBottom } = useStickToBottomContext();
3024
- const previousCountRef = useRef7(userMessageCount);
3025
- useEffect7(() => {
3026
- if (userMessageCount > previousCountRef.current) {
3928
+ function PinLatestUserMessage({
3929
+ userMessageCount,
3930
+ shouldPinLatestUser,
3931
+ targetKey
3932
+ }) {
3933
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
3934
+ const previousCountRef = useRef11(userMessageCount);
3935
+ const spacerHeightRef = useRef11(0);
3936
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
3937
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
3938
+ const getTargetElement = useCallback7(() => {
3939
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
3940
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
3941
+ }, [contentRef]);
3942
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
3943
+ const setSpacerHeight = useCallback7(
3944
+ (height) => {
3945
+ spacerHeightRef.current = height;
3946
+ const content = contentRef.current;
3947
+ if (!content) return;
3948
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
3949
+ else content.style.removeProperty("--blade-chat-pin-spacer");
3950
+ },
3951
+ [contentRef]
3952
+ );
3953
+ useMessagePin({
3954
+ targetKey,
3955
+ pinTarget: shouldPinLatestUser,
3956
+ getScrollElement,
3957
+ getContentElement,
3958
+ getTargetElement,
3959
+ getSpacerHeight,
3960
+ setSpacerHeight,
3961
+ stopAutoScroll: stopScroll,
3962
+ scrollToBottom
3963
+ });
3964
+ useEffect10(() => {
3965
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3027
3966
  scrollToBottom("instant");
3028
3967
  }
3029
3968
  previousCountRef.current = userMessageCount;
3030
- }, [userMessageCount, scrollToBottom]);
3969
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3031
3970
  return null;
3032
3971
  }
3033
3972
  function ScrollToBottomButton() {
3034
3973
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3035
- const [visible, setVisible] = useState11(false);
3036
- const hideTimerRef = useRef7(null);
3037
- useEffect7(() => {
3974
+ const [visible, setVisible] = useState12(false);
3975
+ const hideTimerRef = useRef11(null);
3976
+ useEffect10(() => {
3038
3977
  if (isAtBottom) {
3039
3978
  if (!hideTimerRef.current) {
3040
3979
  hideTimerRef.current = setTimeout(() => {
@@ -3056,7 +3995,7 @@ function ScrollToBottomButton() {
3056
3995
  }
3057
3996
  };
3058
3997
  }, [isAtBottom]);
3059
- const handleClick = useCallback6(() => {
3998
+ const handleClick = useCallback7(() => {
3060
3999
  if (hideTimerRef.current) {
3061
4000
  clearTimeout(hideTimerRef.current);
3062
4001
  hideTimerRef.current = null;
@@ -3065,7 +4004,7 @@ function ScrollToBottomButton() {
3065
4004
  scrollToBottom();
3066
4005
  }, [scrollToBottom]);
3067
4006
  if (!visible) return null;
3068
- return /* @__PURE__ */ jsxs13(
4007
+ return /* @__PURE__ */ jsxs14(
3069
4008
  "button",
3070
4009
  {
3071
4010
  type: "button",
@@ -3073,25 +4012,25 @@ function ScrollToBottomButton() {
3073
4012
  "aria-label": "\u6EDA\u52A8\u5230\u5E95\u90E8",
3074
4013
  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
4014
  children: [
3076
- /* @__PURE__ */ jsx15(ChevronDown, { size: 14 }),
3077
- /* @__PURE__ */ jsx15("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
4015
+ /* @__PURE__ */ jsx16(ChevronDown, { size: 14 }),
4016
+ /* @__PURE__ */ jsx16("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
3078
4017
  ]
3079
4018
  }
3080
4019
  );
3081
4020
  }
3082
4021
  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" })
4022
+ return /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-3 py-1", children: [
4023
+ /* @__PURE__ */ jsx16("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
4024
+ /* @__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: [
4025
+ /* @__PURE__ */ jsx16(Lightbulb, { size: 12 }),
4026
+ /* @__PURE__ */ jsx16("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
3088
4027
  ] }),
3089
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
4028
+ /* @__PURE__ */ jsx16("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
3090
4029
  ] });
3091
4030
  }
3092
4031
 
3093
4032
  // src/components/ChatSurface.tsx
3094
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4033
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
3095
4034
  function themeAttr(theme) {
3096
4035
  return theme === "dark" ? "dark" : void 0;
3097
4036
  }
@@ -3123,7 +4062,7 @@ function ChatSurface({
3123
4062
  beforeInput,
3124
4063
  banner
3125
4064
  }) {
3126
- return /* @__PURE__ */ jsxs14(
4065
+ return /* @__PURE__ */ jsxs15(
3127
4066
  "div",
3128
4067
  {
3129
4068
  "data-theme": themeAttr(theme),
@@ -3132,14 +4071,14 @@ function ChatSurface({
3132
4071
  classNames?.root
3133
4072
  ),
3134
4073
  children: [
3135
- /* @__PURE__ */ jsx16(ConnectionBanner, { connection, className: classNames?.banner }),
4074
+ /* @__PURE__ */ jsx17(ConnectionBanner, { connection, className: classNames?.banner }),
3136
4075
  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 })
4076
+ errorMessage && /* @__PURE__ */ jsxs15("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
4077
+ /* @__PURE__ */ jsx17(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
4078
+ /* @__PURE__ */ jsx17("span", { children: errorMessage })
3140
4079
  ] }),
3141
4080
  slots?.header,
3142
- /* @__PURE__ */ jsx16(
4081
+ /* @__PURE__ */ jsx17(
3143
4082
  MessageList,
3144
4083
  {
3145
4084
  messages,
@@ -3160,7 +4099,7 @@ function ChatSurface({
3160
4099
  }
3161
4100
  ),
3162
4101
  beforeInput,
3163
- /* @__PURE__ */ jsx16(
4102
+ /* @__PURE__ */ jsx17(
3164
4103
  ChatInput,
3165
4104
  {
3166
4105
  value: inputText,
@@ -3180,13 +4119,13 @@ function ChatSurface({
3180
4119
  }
3181
4120
 
3182
4121
  // src/components/AgentChat.tsx
3183
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
4122
+ import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
3184
4123
  function isUnauthorizedError(error) {
3185
4124
  return error instanceof BladeApiError && error.status === 401;
3186
4125
  }
3187
4126
  function LoginCard({ client, onLoggedIn }) {
3188
- const [loggingIn, setLoggingIn] = useState12(false);
3189
- const [loginError, setLoginError] = useState12(null);
4127
+ const [loggingIn, setLoggingIn] = useState13(false);
4128
+ const [loginError, setLoginError] = useState13(null);
3190
4129
  const handleLogin = async () => {
3191
4130
  setLoggingIn(true);
3192
4131
  setLoginError(null);
@@ -3199,11 +4138,11 @@ function LoginCard({ client, onLoggedIn }) {
3199
4138
  setLoggingIn(false);
3200
4139
  }
3201
4140
  };
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(
4141
+ 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: [
4142
+ /* @__PURE__ */ jsx18(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
4143
+ /* @__PURE__ */ jsx18("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
4144
+ /* @__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" }),
4145
+ /* @__PURE__ */ jsx18(
3207
4146
  "button",
3208
4147
  {
3209
4148
  type: "button",
@@ -3213,20 +4152,20 @@ function LoginCard({ client, onLoggedIn }) {
3213
4152
  children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
3214
4153
  }
3215
4154
  ),
3216
- loginError && /* @__PURE__ */ jsx17("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
4155
+ loginError && /* @__PURE__ */ jsx18("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
3217
4156
  ] }) });
3218
4157
  }
3219
4158
  function AgentChat(props) {
3220
4159
  const client = useBladeClient();
3221
- const [attempt, setAttempt] = useState12(0);
3222
- const [needLogin, setNeedLogin] = useState12(() => !client.hasToken());
4160
+ const [attempt, setAttempt] = useState13(0);
4161
+ const [needLogin, setNeedLogin] = useState13(() => !client.hasToken());
3223
4162
  if (needLogin) {
3224
- return /* @__PURE__ */ jsx17(
4163
+ return /* @__PURE__ */ jsx18(
3225
4164
  "div",
3226
4165
  {
3227
4166
  "data-theme": themeAttr(props.theme),
3228
4167
  className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
3229
- children: /* @__PURE__ */ jsx17(
4168
+ children: /* @__PURE__ */ jsx18(
3230
4169
  LoginCard,
3231
4170
  {
3232
4171
  client,
@@ -3239,7 +4178,7 @@ function AgentChat(props) {
3239
4178
  }
3240
4179
  );
3241
4180
  }
3242
- return /* @__PURE__ */ jsx17(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
4181
+ return /* @__PURE__ */ jsx18(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
3243
4182
  }
3244
4183
  function ChatSessionView({
3245
4184
  sessionId,
@@ -3261,12 +4200,12 @@ function ChatSessionView({
3261
4200
  onSessionCreated
3262
4201
  });
3263
4202
  const replay = useReplay(session);
3264
- const [stopRequested, setStopRequested] = useState12(false);
3265
- const [inputText, setInputText] = useState12("");
3266
- const [resultFeedback, setResultFeedback] = useState12([]);
4203
+ const [stopRequested, setStopRequested] = useState13(false);
4204
+ const [inputText, setInputText] = useState13("");
4205
+ const [resultFeedback, setResultFeedback] = useState13([]);
3267
4206
  const resolvedSessionId = session?.sessionId;
3268
4207
  const isViewer = state?.viewerRole === "viewer";
3269
- useEffect8(() => {
4208
+ useEffect11(() => {
3270
4209
  setResultFeedback([]);
3271
4210
  if (!resolvedSessionId || isViewer) return;
3272
4211
  let cancelled = false;
@@ -3295,18 +4234,18 @@ function ChatSessionView({
3295
4234
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3296
4235
  [resultFeedback]
3297
4236
  );
3298
- const handleResultFeedbackSaved = useCallback7((saved) => {
4237
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3299
4238
  setResultFeedback((current) => [
3300
4239
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3301
4240
  saved
3302
4241
  ]);
3303
4242
  }, []);
3304
- useEffect8(() => {
4243
+ useEffect11(() => {
3305
4244
  if (session) {
3306
4245
  onSessionReady?.(session);
3307
4246
  }
3308
4247
  }, [session, onSessionReady]);
3309
- useEffect8(() => {
4248
+ useEffect11(() => {
3310
4249
  if (!session) return;
3311
4250
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3312
4251
  setInputText((prev) => `${prev ? `${prev}
@@ -3322,12 +4261,12 @@ ${content}`);
3322
4261
  offInsert();
3323
4262
  };
3324
4263
  }, [session]);
3325
- useEffect8(() => {
4264
+ useEffect11(() => {
3326
4265
  if (isUnauthorizedError(error)) {
3327
4266
  onUnauthorized();
3328
4267
  }
3329
4268
  }, [error, onUnauthorized]);
3330
- useEffect8(() => {
4269
+ useEffect11(() => {
3331
4270
  if (!session || !commands) return;
3332
4271
  const unsubscribes = Object.entries(commands).map(
3333
4272
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3348,7 +4287,7 @@ ${content}`);
3348
4287
  setStopRequested(true);
3349
4288
  void session?.stop();
3350
4289
  };
3351
- return /* @__PURE__ */ jsx17(
4290
+ return /* @__PURE__ */ jsx18(
3352
4291
  ChatSurface,
3353
4292
  {
3354
4293
  theme,
@@ -3358,8 +4297,8 @@ ${content}`);
3358
4297
  slots,
3359
4298
  placeholder,
3360
4299
  connection: state?.connection ?? "connecting",
3361
- banner: /* @__PURE__ */ jsxs15(Fragment3, { children: [
3362
- /* @__PURE__ */ jsx17(
4300
+ banner: /* @__PURE__ */ jsxs16(Fragment3, { children: [
4301
+ /* @__PURE__ */ jsx18(
3363
4302
  ReplayBar,
3364
4303
  {
3365
4304
  isReplay: replay.isReplay,
@@ -3369,7 +4308,7 @@ ${content}`);
3369
4308
  onExit: () => void replay.exitToAutonomous()
3370
4309
  }
3371
4310
  ),
3372
- /* @__PURE__ */ jsx17(ReplayMismatchPrompt, { mismatch: replay.mismatch })
4311
+ /* @__PURE__ */ jsx18(ReplayMismatchPrompt, { mismatch: replay.mismatch })
3373
4312
  ] }),
3374
4313
  errorMessage,
3375
4314
  messages: state?.messages ?? [],
@@ -3398,11 +4337,11 @@ ${content}`);
3398
4337
  }
3399
4338
 
3400
4339
  // src/components/LlmChat.tsx
3401
- import { useEffect as useEffect9, useMemo as useMemo9, useState as useState14 } from "react";
4340
+ import { useEffect as useEffect12, useMemo as useMemo9, useState as useState15 } from "react";
3402
4341
 
3403
4342
  // src/components/LlmAdvancedSettings.tsx
3404
- import { useState as useState13 } from "react";
3405
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
4343
+ import { useState as useState14 } from "react";
4344
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3406
4345
  var FIELDS = [
3407
4346
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
3408
4347
  { id: "model", label: "\u6A21\u578B", placeholder: "\u6A21\u578B\u540D\u79F0" },
@@ -3448,13 +4387,13 @@ function writeOverride(settings, baseURL, override) {
3448
4387
  }
3449
4388
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3450
4389
  const normalized = normalizeAdvanced(settings);
3451
- const [open, setOpen] = useState13(false);
3452
- const [draft, setDraft] = useState13(override);
4390
+ const [open, setOpen] = useState14(false);
4391
+ const [draft, setDraft] = useState14(override);
3453
4392
  if (!normalized) return null;
3454
4393
  const fields = FIELDS.filter((field) => normalized[field.id]);
3455
4394
  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(
4395
+ return /* @__PURE__ */ jsxs17("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
4396
+ /* @__PURE__ */ jsxs17(
3458
4397
  "button",
3459
4398
  {
3460
4399
  type: "button",
@@ -3464,16 +4403,16 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3464
4403
  },
3465
4404
  className: "flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3466
4405
  children: [
3467
- /* @__PURE__ */ jsx18(Settings2, { size: 13 }),
4406
+ /* @__PURE__ */ jsx19(Settings2, { size: 13 }),
3468
4407
  "\u9AD8\u7EA7\u8BBE\u7F6E",
3469
- dirty && /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
4408
+ dirty && /* @__PURE__ */ jsx19("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
3470
4409
  ]
3471
4410
  }
3472
4411
  ),
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(
4412
+ open && /* @__PURE__ */ jsxs17("div", { className: "mt-2 flex flex-col gap-2", children: [
4413
+ fields.map((field) => /* @__PURE__ */ jsxs17("label", { className: "flex flex-col gap-1", children: [
4414
+ /* @__PURE__ */ jsx19("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
4415
+ /* @__PURE__ */ jsx19(
3477
4416
  "input",
3478
4417
  {
3479
4418
  type: field.secret ? "password" : "text",
@@ -3484,9 +4423,9 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3484
4423
  }
3485
4424
  )
3486
4425
  ] }, 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(
4426
+ 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" }),
4427
+ /* @__PURE__ */ jsxs17("div", { className: "flex gap-2", children: [
4428
+ /* @__PURE__ */ jsx19(
3490
4429
  "button",
3491
4430
  {
3492
4431
  type: "button",
@@ -3501,7 +4440,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3501
4440
  children: "\u4FDD\u5B58"
3502
4441
  }
3503
4442
  ),
3504
- /* @__PURE__ */ jsx18(
4443
+ /* @__PURE__ */ jsx19(
3505
4444
  "button",
3506
4445
  {
3507
4446
  type: "button",
@@ -3520,7 +4459,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3520
4459
  }
3521
4460
 
3522
4461
  // src/components/LlmChat.tsx
3523
- import { jsx as jsx19 } from "react/jsx-runtime";
4462
+ import { jsx as jsx20 } from "react/jsx-runtime";
3524
4463
  function LlmChat({
3525
4464
  classNames,
3526
4465
  renderers,
@@ -3532,11 +4471,11 @@ function LlmChat({
3532
4471
  onOverrideChange,
3533
4472
  ...options
3534
4473
  }) {
3535
- const [override, setOverride] = useState14(() => readOverride(advanced, options.baseURL));
4474
+ const [override, setOverride] = useState15(() => readOverride(advanced, options.baseURL));
3536
4475
  const effective = { ...options, ...override };
3537
4476
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3538
- const [inputText, setInputText] = useState14("");
3539
- const [stopRequested, setStopRequested] = useState14(false);
4477
+ const [inputText, setInputText] = useState15("");
4478
+ const [stopRequested, setStopRequested] = useState15(false);
3540
4479
  const handle = useMemo9(
3541
4480
  () => ({
3542
4481
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3546,10 +4485,10 @@ ${text}` : text),
3546
4485
  }),
3547
4486
  [send, reset]
3548
4487
  );
3549
- useEffect9(() => {
4488
+ useEffect12(() => {
3550
4489
  onReady?.(handle);
3551
4490
  }, [handle, onReady]);
3552
- return /* @__PURE__ */ jsx19(
4491
+ return /* @__PURE__ */ jsx20(
3553
4492
  ChatSurface,
3554
4493
  {
3555
4494
  theme,
@@ -3574,7 +4513,7 @@ ${text}` : text),
3574
4513
  setStopRequested(true);
3575
4514
  stop();
3576
4515
  },
3577
- beforeInput: advanced ? /* @__PURE__ */ jsx19(
4516
+ beforeInput: advanced ? /* @__PURE__ */ jsx20(
3578
4517
  LlmAdvancedSettingsBar,
3579
4518
  {
3580
4519
  settings: advanced,
@@ -3592,14 +4531,14 @@ ${text}` : text),
3592
4531
  }
3593
4532
 
3594
4533
  // src/components/ChatView.tsx
3595
- import { jsx as jsx20 } from "react/jsx-runtime";
4534
+ import { jsx as jsx21 } from "react/jsx-runtime";
3596
4535
  function ChatView(props) {
3597
4536
  const { mode, llm, onLlmReady, ...rest } = props;
3598
4537
  if (mode === "llm") {
3599
4538
  if (!llm) {
3600
4539
  throw new Error('ChatView: mode="llm" \u9700\u8981\u540C\u65F6\u4F20 llm={{ baseURL, model }}');
3601
4540
  }
3602
- return /* @__PURE__ */ jsx20(
4541
+ return /* @__PURE__ */ jsx21(
3603
4542
  LlmChat,
3604
4543
  {
3605
4544
  ...llm,
@@ -3612,7 +4551,7 @@ function ChatView(props) {
3612
4551
  }
3613
4552
  );
3614
4553
  }
3615
- return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
4554
+ return /* @__PURE__ */ jsx21(AgentChat, { ...rest });
3616
4555
  }
3617
4556
 
3618
4557
  // src/index.ts
@@ -3621,6 +4560,7 @@ export {
3621
4560
  AgentChat,
3622
4561
  BladeProvider,
3623
4562
  ChatView,
4563
+ ContextCard,
3624
4564
  LlmChat,
3625
4565
  MarkdownContent,
3626
4566
  ReplayBar,
@@ -3628,6 +4568,7 @@ export {
3628
4568
  useAgentSession,
3629
4569
  useBladeClient,
3630
4570
  useLlmChat,
4571
+ useMessagePin,
3631
4572
  useReplay
3632
4573
  };
3633
4574
  /*! Bundled license information:
@@ -3639,17 +4580,16 @@ lucide-react/dist/esm/createLucideIcon.js:
3639
4580
  lucide-react/dist/esm/icons/arrow-right.js:
3640
4581
  lucide-react/dist/esm/icons/arrow-up-right.js:
3641
4582
  lucide-react/dist/esm/icons/arrow-up.js:
4583
+ lucide-react/dist/esm/icons/book-open.js:
3642
4584
  lucide-react/dist/esm/icons/bot.js:
3643
- lucide-react/dist/esm/icons/brain.js:
3644
4585
  lucide-react/dist/esm/icons/check.js:
3645
4586
  lucide-react/dist/esm/icons/chevron-down.js:
3646
4587
  lucide-react/dist/esm/icons/chevron-right.js:
3647
4588
  lucide-react/dist/esm/icons/circle-alert.js:
3648
4589
  lucide-react/dist/esm/icons/copy.js:
3649
- lucide-react/dist/esm/icons/download.js:
4590
+ lucide-react/dist/esm/icons/earth.js:
4591
+ lucide-react/dist/esm/icons/file-pen-line.js:
3650
4592
  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
4593
  lucide-react/dist/esm/icons/globe.js:
3654
4594
  lucide-react/dist/esm/icons/layers.js:
3655
4595
  lucide-react/dist/esm/icons/lightbulb.js:
@@ -3658,10 +4598,13 @@ lucide-react/dist/esm/icons/lock-keyhole.js:
3658
4598
  lucide-react/dist/esm/icons/message-square-more.js:
3659
4599
  lucide-react/dist/esm/icons/message-square.js:
3660
4600
  lucide-react/dist/esm/icons/play.js:
4601
+ lucide-react/dist/esm/icons/search.js:
3661
4602
  lucide-react/dist/esm/icons/settings-2.js:
3662
4603
  lucide-react/dist/esm/icons/sparkles.js:
3663
4604
  lucide-react/dist/esm/icons/square.js:
4605
+ lucide-react/dist/esm/icons/terminal.js:
3664
4606
  lucide-react/dist/esm/icons/triangle-alert.js:
4607
+ lucide-react/dist/esm/icons/wrench.js:
3665
4608
  lucide-react/dist/esm/icons/x.js:
3666
4609
  lucide-react/dist/esm/lucide-react.js:
3667
4610
  (**