@blade-hq/agent-react 2610.0.0-beta.5 → 2610.0.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,6 +17,9 @@ function useBladeClient() {
17
17
  }
18
18
  return client;
19
19
  }
20
+ function useOptionalBladeClient() {
21
+ return useContext(BladeClientContext);
22
+ }
20
23
 
21
24
  // src/hooks/use-agent-session.ts
22
25
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
@@ -30,11 +33,14 @@ function useAgentSession(sessionId, options = {}) {
30
33
  const connRef = useRef({
31
34
  id: null,
32
35
  session: null,
36
+ cleanup: null,
33
37
  gen: 0
34
38
  });
35
39
  const createdIdPromiseRef = useRef(null);
36
40
  const onCreatedRef = useRef(options.onSessionCreated);
37
41
  onCreatedRef.current = options.onSessionCreated;
42
+ const onConnectedRef = useRef(options.onSessionConnected);
43
+ onConnectedRef.current = options.onSessionConnected;
38
44
  const createOptionsRef = useRef(options.createOptions);
39
45
  createOptionsRef.current = options.createOptions;
40
46
  const sessionIdRef = useRef(sessionId);
@@ -42,6 +48,7 @@ function useAgentSession(sessionId, options = {}) {
42
48
  const connect = useMemo(() => {
43
49
  return (targetId) => {
44
50
  const gen = ++connRef.current.gen;
51
+ let pendingCleanup = null;
45
52
  const idPromise = targetId ? Promise.resolve(targetId) : (
46
53
  // biome-ignore lint/suspicious/noAssignInExpressions: ??= 挂 ref 是 StrictMode 下"只创建一次"的关键
47
54
  createdIdPromiseRef.current ??= client.sessions.create(createOptionsRef.current ?? {}).then((created) => {
@@ -51,16 +58,25 @@ function useAgentSession(sessionId, options = {}) {
51
58
  return id;
52
59
  })
53
60
  );
54
- idPromise.then((id) => client.hub.connect(id)).then((next) => {
61
+ idPromise.then(
62
+ (id) => client.hub.connect(id, {
63
+ setup: (next) => {
64
+ pendingCleanup = onConnectedRef.current?.(next) ?? null;
65
+ }
66
+ })
67
+ ).then((next) => {
55
68
  if (connRef.current.gen !== gen) {
69
+ pendingCleanup?.();
56
70
  next.dispose();
57
71
  return;
58
72
  }
59
73
  connRef.current.id = next.sessionId;
60
74
  connRef.current.session = next;
75
+ connRef.current.cleanup = pendingCleanup;
61
76
  setSession(next);
62
77
  setError(null);
63
78
  }).catch((err) => {
79
+ pendingCleanup?.();
64
80
  if (connRef.current.gen !== gen) return;
65
81
  createdIdPromiseRef.current = null;
66
82
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -72,8 +88,11 @@ function useAgentSession(sessionId, options = {}) {
72
88
  return () => {
73
89
  connRef.current.gen++;
74
90
  const toRelease = connRef.current.session;
91
+ const cleanup = connRef.current.cleanup;
75
92
  connRef.current.id = null;
76
93
  connRef.current.session = null;
94
+ connRef.current.cleanup = null;
95
+ cleanup?.();
77
96
  setSession(null);
78
97
  if (toRelease) setTimeout(() => toRelease.dispose(), DISPOSE_DELAY_MS);
79
98
  };
@@ -83,8 +102,11 @@ function useAgentSession(sessionId, options = {}) {
83
102
  if (connRef.current.id === null) return;
84
103
  if (sessionId === connRef.current.id) return;
85
104
  const previous = connRef.current.session;
105
+ const cleanup = connRef.current.cleanup;
86
106
  connRef.current.id = null;
87
107
  connRef.current.session = null;
108
+ connRef.current.cleanup = null;
109
+ cleanup?.();
88
110
  if (previous) setTimeout(() => previous.dispose(), DISPOSE_DELAY_MS);
89
111
  connect(sessionId);
90
112
  }, [sessionId, connect]);
@@ -245,6 +267,8 @@ function useLlmChat(options) {
245
267
  const [error, setError] = useState3(null);
246
268
  const [isStreaming, setIsStreaming] = useState3(false);
247
269
  const abortRef = useRef2(null);
270
+ const activeStartedAtRef = useRef2(null);
271
+ const assistantTimingsRef = useRef2(/* @__PURE__ */ new WeakMap());
248
272
  const generationRef = useRef2(0);
249
273
  const historyRef = useRef2([]);
250
274
  const optionsRef = useRef2(options);
@@ -263,16 +287,31 @@ function useLlmChat(options) {
263
287
  setFailedToolIds([]);
264
288
  setError(null);
265
289
  setIsStreaming(false);
290
+ activeStartedAtRef.current = null;
291
+ assistantTimingsRef.current = /* @__PURE__ */ new WeakMap();
266
292
  }, [stop]);
267
293
  const send = useCallback2(async (text) => {
268
294
  const content = text.trim();
269
295
  if (!content || abortRef.current) return false;
270
296
  const opts = optionsRef.current;
271
297
  const maxRounds = opts.maxToolRounds ?? DEFAULT_MAX_TOOL_ROUNDS;
298
+ const startedAt = Date.now();
299
+ activeStartedAtRef.current = startedAt;
272
300
  const commit = (message) => {
273
301
  historyRef.current = [...historyRef.current, message];
274
302
  setHistory(historyRef.current);
275
303
  };
304
+ const latestAssistantTiming = { current: null };
305
+ const commitAssistant = (message) => {
306
+ const timing = { startedAt };
307
+ assistantTimingsRef.current.set(message, timing);
308
+ latestAssistantTiming.current = timing;
309
+ commit(message);
310
+ return timing;
311
+ };
312
+ const finishTiming = (timing) => {
313
+ timing.durationMs = Math.max(0, Date.now() - timing.startedAt);
314
+ };
276
315
  setError(null);
277
316
  setIsStreaming(true);
278
317
  setStreamingText("");
@@ -298,8 +337,11 @@ function useLlmChat(options) {
298
337
  };
299
338
  setStreamingText(null);
300
339
  setStreamingCalls([]);
301
- commit(assistant);
302
- if (!result.toolCalls.length) return true;
340
+ const assistantTiming = commitAssistant(assistant);
341
+ if (!result.toolCalls.length) {
342
+ finishTiming(assistantTiming);
343
+ return true;
344
+ }
303
345
  const bail = (reason) => {
304
346
  for (const call of result.toolCalls) {
305
347
  commit({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ error: reason }) });
@@ -308,11 +350,13 @@ function useLlmChat(options) {
308
350
  };
309
351
  if (!opts.onToolCall) {
310
352
  bail("\u8FD9\u4E2A\u5E94\u7528\u6CA1\u6709\u63D0\u4F9B\u5DE5\u5177\u6267\u884C\u5165\u53E3");
353
+ finishTiming(assistantTiming);
311
354
  return true;
312
355
  }
313
356
  if (round >= maxRounds) {
314
357
  bail(`\u5DE5\u5177\u8C03\u7528\u5DF2\u8FBE\u4E0A\u9650 ${maxRounds} \u8F6E\uFF0C\u6CA1\u6709\u6267\u884C`);
315
358
  setError(`\u5DE5\u5177\u8C03\u7528\u8D85\u8FC7 ${maxRounds} \u8F6E\u4ECD\u672A\u7ED9\u51FA\u7ED3\u8BBA\uFF0C\u5DF2\u505C\u4E0B\u3002`);
359
+ finishTiming(assistantTiming);
316
360
  return false;
317
361
  }
318
362
  for (const call of result.toolCalls) {
@@ -337,21 +381,39 @@ function useLlmChat(options) {
337
381
  setStreamingCalls([]);
338
382
  if (controller.signal.aborted) {
339
383
  if (generation !== generationRef.current) return false;
340
- commit({ role: "assistant", content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09" });
384
+ const timing = commitAssistant({
385
+ role: "assistant",
386
+ content: partial ? `${partial}\uFF08\u5DF2\u505C\u6B62\uFF09` : "\uFF08\u5DF2\u505C\u6B62\uFF09"
387
+ });
388
+ timing.status = "interrupted";
389
+ finishTiming(timing);
341
390
  return false;
342
391
  }
343
392
  if (partial && generation === generationRef.current) {
344
- commit({ role: "assistant", content: partial });
393
+ const timing = commitAssistant({ role: "assistant", content: partial });
394
+ timing.status = "failed";
395
+ finishTiming(timing);
396
+ } else if (latestAssistantTiming.current) {
397
+ latestAssistantTiming.current.status = "failed";
398
+ finishTiming(latestAssistantTiming.current);
345
399
  }
346
400
  setError(err instanceof Error && err.message ? err.message : "\u5BF9\u8BDD\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5");
347
401
  return false;
348
402
  } finally {
349
403
  if (abortRef.current === controller) abortRef.current = null;
404
+ activeStartedAtRef.current = null;
350
405
  setIsStreaming(false);
351
406
  }
352
407
  }, []);
353
408
  const messages = useMemo2(
354
- () => toChatMessages(history, streamingText, streamingCalls, failedToolIds),
409
+ () => toChatMessages(
410
+ history,
411
+ streamingText,
412
+ streamingCalls,
413
+ failedToolIds,
414
+ assistantTimingsRef.current,
415
+ activeStartedAtRef.current
416
+ ),
355
417
  [history, streamingText, streamingCalls, failedToolIds]
356
418
  );
357
419
  return { messages, isStreaming, error, send, stop, reset };
@@ -503,7 +565,7 @@ function parseSse(raw) {
503
565
  if (!delta) return null;
504
566
  return { text: delta.content, toolCallDeltas: delta.tool_calls };
505
567
  }
506
- function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
568
+ function toChatMessages(history, streamingText, streamingCalls, failedToolIds, assistantTimings, activeStartedAt) {
507
569
  const results = /* @__PURE__ */ new Map();
508
570
  for (const msg of history) {
509
571
  if (msg.role === "tool") results.set(msg.tool_call_id, msg.content);
@@ -515,10 +577,13 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
515
577
  messages.push({ role: "user", content: msg.content, status: "completed" });
516
578
  continue;
517
579
  }
580
+ const timing = assistantTimings.get(msg);
518
581
  messages.push({
519
582
  role: "assistant",
520
583
  content: msg.content,
521
- status: "completed",
584
+ status: timing?.status ?? "completed",
585
+ ...timing ? { timestamp: new Date(timing.startedAt).toISOString() } : {},
586
+ ...timing?.durationMs === void 0 ? {} : { duration_ms: timing.durationMs },
522
587
  ...msg.tool_calls?.length ? { tool_calls: msg.tool_calls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
523
588
  });
524
589
  }
@@ -527,6 +592,7 @@ function toChatMessages(history, streamingText, streamingCalls, failedToolIds) {
527
592
  role: "assistant",
528
593
  content: streamingText,
529
594
  status: "streaming",
595
+ ...activeStartedAt === null ? {} : { timestamp: new Date(activeStartedAt).toISOString() },
530
596
  ...streamingCalls.length ? { tool_calls: streamingCalls.map((call) => toToolCallInfo(call, results, failedToolIds)) } : {}
531
597
  });
532
598
  }
@@ -544,6 +610,187 @@ function toToolCallInfo(call, results, failedToolIds) {
544
610
  };
545
611
  }
546
612
 
613
+ // src/hooks/use-message-pin.ts
614
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3 } from "react";
615
+ var DEFAULT_MARGIN_PX = 24;
616
+ var MANUAL_SCROLL_TOLERANCE_PX = 2;
617
+ function useMessagePin({
618
+ targetKey,
619
+ pinTarget,
620
+ isStreaming = true,
621
+ layoutKey,
622
+ getScrollElement,
623
+ getContentElement,
624
+ getTargetElement,
625
+ getTargetScrollTop,
626
+ getSpacerHeight,
627
+ setSpacerHeight,
628
+ stopAutoScroll,
629
+ // 不再内部调用(见下面 reposition 里的说明),但这是公开的 SDK hook 参数,
630
+ // 删掉会让已经传这个 prop 的调用方在 TS 严格类型下编译不过——留着接口
631
+ // 不动,只是这里不再用它。
632
+ // biome-ignore lint/correctness/noUnusedVariables: 保留公开 API 形状
633
+ scrollToBottom,
634
+ margin = DEFAULT_MARGIN_PX
635
+ }) {
636
+ const pinActiveRef = useRef3(false);
637
+ const frameRef = useRef3(null);
638
+ const retryTimeoutRef = useRef3(null);
639
+ const manualScrollCheckTimeoutRef = useRef3(null);
640
+ const repositionPendingRef = useRef3(false);
641
+ const pinnedTargetScrollTopRef = useRef3(null);
642
+ const appliedTargetKeyRef = useRef3(null);
643
+ const initialObservedTargetKeyRef = useRef3(pinTarget ? null : targetKey);
644
+ const observedTargetKeyRef = useRef3(initialObservedTargetKeyRef.current);
645
+ const setSpacerHeightRef = useRef3(setSpacerHeight);
646
+ setSpacerHeightRef.current = setSpacerHeight;
647
+ const release = useCallback3(() => {
648
+ if (!pinActiveRef.current) return;
649
+ pinActiveRef.current = false;
650
+ repositionPendingRef.current = false;
651
+ pinnedTargetScrollTopRef.current = null;
652
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
653
+ frameRef.current = null;
654
+ if (manualScrollCheckTimeoutRef.current != null) {
655
+ clearTimeout(manualScrollCheckTimeoutRef.current);
656
+ manualScrollCheckTimeoutRef.current = null;
657
+ }
658
+ setSpacerHeightRef.current(0);
659
+ }, []);
660
+ const reposition = useCallback3(
661
+ () => {
662
+ const scroll = getScrollElement();
663
+ const target = getTargetElement();
664
+ if (!scroll || !pinActiveRef.current) return;
665
+ const targetScrollTop = target ? Math.max(
666
+ 0,
667
+ scroll.scrollTop + target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - margin
668
+ ) : getTargetScrollTop?.(scroll);
669
+ if (targetScrollTop == null) return;
670
+ const previousSpacerHeight = getSpacerHeight();
671
+ const baseScrollHeight = scroll.scrollHeight - previousSpacerHeight;
672
+ const nextSpacerHeight = Math.max(
673
+ 0,
674
+ targetScrollTop + scroll.clientHeight - baseScrollHeight
675
+ );
676
+ setSpacerHeightRef.current(nextSpacerHeight);
677
+ pinnedTargetScrollTopRef.current = targetScrollTop;
678
+ stopAutoScroll();
679
+ scroll.scrollTop = targetScrollTop;
680
+ if (nextSpacerHeight === 0 && previousSpacerHeight > 0) {
681
+ pinActiveRef.current = false;
682
+ }
683
+ },
684
+ [
685
+ getScrollElement,
686
+ getSpacerHeight,
687
+ getTargetElement,
688
+ getTargetScrollTop,
689
+ margin,
690
+ stopAutoScroll
691
+ ]
692
+ );
693
+ const scheduleReposition = useCallback3(
694
+ () => {
695
+ if (!pinActiveRef.current) return;
696
+ repositionPendingRef.current = true;
697
+ if (frameRef.current != null) return;
698
+ frameRef.current = requestAnimationFrame(() => {
699
+ frameRef.current = null;
700
+ const shouldReposition = repositionPendingRef.current;
701
+ repositionPendingRef.current = false;
702
+ if (shouldReposition) reposition();
703
+ });
704
+ },
705
+ [reposition]
706
+ );
707
+ useEffect3(() => {
708
+ if (!targetKey) {
709
+ release();
710
+ appliedTargetKeyRef.current = null;
711
+ observedTargetKeyRef.current = null;
712
+ return;
713
+ }
714
+ if (!pinTarget) {
715
+ if (pinActiveRef.current && appliedTargetKeyRef.current != null && (appliedTargetKeyRef.current !== targetKey || !isStreaming)) {
716
+ release();
717
+ }
718
+ observedTargetKeyRef.current = targetKey;
719
+ return;
720
+ }
721
+ if (appliedTargetKeyRef.current === targetKey) {
722
+ observedTargetKeyRef.current = targetKey;
723
+ return;
724
+ }
725
+ if (observedTargetKeyRef.current === targetKey) return;
726
+ appliedTargetKeyRef.current = targetKey;
727
+ observedTargetKeyRef.current = targetKey;
728
+ pinActiveRef.current = true;
729
+ stopAutoScroll();
730
+ scheduleReposition();
731
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
732
+ retryTimeoutRef.current = window.setTimeout(() => {
733
+ retryTimeoutRef.current = null;
734
+ scheduleReposition();
735
+ }, 80);
736
+ }, [isStreaming, pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
737
+ useEffect3(() => {
738
+ if (layoutKey !== void 0) scheduleReposition();
739
+ }, [layoutKey, scheduleReposition]);
740
+ useEffect3(() => {
741
+ const scroll = getScrollElement();
742
+ if (!scroll) return;
743
+ const content = getContentElement?.();
744
+ const observer = new ResizeObserver(scheduleReposition);
745
+ observer.observe(scroll);
746
+ if (content && content !== scroll) observer.observe(content);
747
+ window.addEventListener("resize", scheduleReposition);
748
+ window.visualViewport?.addEventListener("resize", scheduleReposition);
749
+ const handleScroll = () => {
750
+ if (!pinActiveRef.current) return;
751
+ if (manualScrollCheckTimeoutRef.current != null) {
752
+ clearTimeout(manualScrollCheckTimeoutRef.current);
753
+ }
754
+ manualScrollCheckTimeoutRef.current = window.setTimeout(() => {
755
+ manualScrollCheckTimeoutRef.current = null;
756
+ if (!pinActiveRef.current || repositionPendingRef.current) return;
757
+ const targetScrollTop = pinnedTargetScrollTopRef.current;
758
+ if (targetScrollTop != null && Math.abs(scroll.scrollTop - targetScrollTop) > MANUAL_SCROLL_TOLERANCE_PX) {
759
+ release();
760
+ }
761
+ }, 0);
762
+ };
763
+ scroll.addEventListener("scroll", handleScroll, { passive: true });
764
+ return () => {
765
+ observer.disconnect();
766
+ window.removeEventListener("resize", scheduleReposition);
767
+ window.visualViewport?.removeEventListener("resize", scheduleReposition);
768
+ scroll.removeEventListener("scroll", handleScroll);
769
+ };
770
+ }, [getContentElement, getScrollElement, release, scheduleReposition]);
771
+ useEffect3(
772
+ () => () => {
773
+ if (frameRef.current != null) cancelAnimationFrame(frameRef.current);
774
+ frameRef.current = null;
775
+ if (retryTimeoutRef.current != null) clearTimeout(retryTimeoutRef.current);
776
+ retryTimeoutRef.current = null;
777
+ if (manualScrollCheckTimeoutRef.current != null) {
778
+ clearTimeout(manualScrollCheckTimeoutRef.current);
779
+ }
780
+ manualScrollCheckTimeoutRef.current = null;
781
+ repositionPendingRef.current = false;
782
+ pinnedTargetScrollTopRef.current = null;
783
+ appliedTargetKeyRef.current = null;
784
+ observedTargetKeyRef.current = initialObservedTargetKeyRef.current;
785
+ pinActiveRef.current = false;
786
+ setSpacerHeightRef.current(0);
787
+ },
788
+ []
789
+ );
790
+ const isActive = useCallback3(() => pinActiveRef.current, []);
791
+ return { release, isActive };
792
+ }
793
+
547
794
  // src/components/AgentChat.tsx
548
795
  import { BladeApiError, latestPostChatFollowup } from "@blade-hq/agent-client";
549
796
 
@@ -636,6 +883,18 @@ var ArrowUp = createLucideIcon("ArrowUp", [
636
883
  ["path", { d: "M12 19V5", key: "x0mq9r" }]
637
884
  ]);
638
885
 
886
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/book-open.js
887
+ var BookOpen = createLucideIcon("BookOpen", [
888
+ ["path", { d: "M12 7v14", key: "1akyts" }],
889
+ [
890
+ "path",
891
+ {
892
+ 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",
893
+ key: "ruj8y"
894
+ }
895
+ ]
896
+ ]);
897
+
639
898
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/bot.js
640
899
  var Bot = createLucideIcon("Bot", [
641
900
  ["path", { d: "M12 8V4H8", key: "hb8ula" }],
@@ -646,31 +905,6 @@ var Bot = createLucideIcon("Bot", [
646
905
  ["path", { d: "M9 13v2", key: "rq6x2g" }]
647
906
  ]);
648
907
 
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
908
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/check.js
675
909
  var Check = createLucideIcon("Check", [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]]);
676
910
 
@@ -691,17 +925,54 @@ var CircleAlert = createLucideIcon("CircleAlert", [
691
925
  ["line", { x1: "12", x2: "12.01", y1: "16", y2: "16", key: "4dfq90" }]
692
926
  ]);
693
927
 
928
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/circle-dot.js
929
+ var CircleDot = createLucideIcon("CircleDot", [
930
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
931
+ ["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }]
932
+ ]);
933
+
934
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/circle.js
935
+ var Circle = createLucideIcon("Circle", [
936
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
937
+ ]);
938
+
694
939
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/copy.js
695
940
  var Copy = createLucideIcon("Copy", [
696
941
  ["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2", key: "17jyea" }],
697
942
  ["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
943
  ]);
699
944
 
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" }]
945
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/earth.js
946
+ var Earth = createLucideIcon("Earth", [
947
+ ["path", { d: "M21.54 15H17a2 2 0 0 0-2 2v4.54", key: "1djwo0" }],
948
+ [
949
+ "path",
950
+ {
951
+ 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",
952
+ key: "1tzkfa"
953
+ }
954
+ ],
955
+ ["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" }],
956
+ ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }]
957
+ ]);
958
+
959
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-pen-line.js
960
+ var FilePenLine = createLucideIcon("FilePenLine", [
961
+ [
962
+ "path",
963
+ {
964
+ 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",
965
+ key: "142zxg"
966
+ }
967
+ ],
968
+ [
969
+ "path",
970
+ {
971
+ 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",
972
+ key: "2t3380"
973
+ }
974
+ ],
975
+ ["path", { d: "M8 18h1", key: "13wk12" }]
705
976
  ]);
706
977
 
707
978
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/file-text.js
@@ -713,24 +984,6 @@ var FileText = createLucideIcon("FileText", [
713
984
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
714
985
  ]);
715
986
 
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
987
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/globe.js
735
988
  var Globe = createLucideIcon("Globe", [
736
989
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
@@ -776,6 +1029,15 @@ var Lightbulb = createLucideIcon("Lightbulb", [
776
1029
  ["path", { d: "M10 22h4", key: "ceow96" }]
777
1030
  ]);
778
1031
 
1032
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/list-checks.js
1033
+ var ListChecks = createLucideIcon("ListChecks", [
1034
+ ["path", { d: "m3 17 2 2 4-4", key: "1jhpwq" }],
1035
+ ["path", { d: "m3 7 2 2 4-4", key: "1obspn" }],
1036
+ ["path", { d: "M13 6h8", key: "15sg57" }],
1037
+ ["path", { d: "M13 12h8", key: "h98zly" }],
1038
+ ["path", { d: "M13 18h8", key: "oe0vm4" }]
1039
+ ]);
1040
+
779
1041
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/loader-circle.js
780
1042
  var LoaderCircle = createLucideIcon("LoaderCircle", [
781
1043
  ["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]
@@ -806,6 +1068,20 @@ var Play = createLucideIcon("Play", [
806
1068
  ["polygon", { points: "6 3 20 12 6 21 6 3", key: "1oa8hb" }]
807
1069
  ]);
808
1070
 
1071
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/refresh-ccw.js
1072
+ var RefreshCcw = createLucideIcon("RefreshCcw", [
1073
+ ["path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8", key: "14sxne" }],
1074
+ ["path", { d: "M3 3v5h5", key: "1xhq8a" }],
1075
+ ["path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16", key: "1hlbsb" }],
1076
+ ["path", { d: "M16 16h5v5", key: "ccwih5" }]
1077
+ ]);
1078
+
1079
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/search.js
1080
+ var Search = createLucideIcon("Search", [
1081
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }],
1082
+ ["path", { d: "m21 21-4.3-4.3", key: "1qie3q" }]
1083
+ ]);
1084
+
809
1085
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/settings-2.js
810
1086
  var Settings2 = createLucideIcon("Settings2", [
811
1087
  ["path", { d: "M20 7h-9", key: "3s1dr2" }],
@@ -834,6 +1110,12 @@ var Square = createLucideIcon("Square", [
834
1110
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }]
835
1111
  ]);
836
1112
 
1113
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/terminal.js
1114
+ var Terminal = createLucideIcon("Terminal", [
1115
+ ["polyline", { points: "4 17 10 11 4 5", key: "akl6gq" }],
1116
+ ["line", { x1: "12", x2: "20", y1: "19", y2: "19", key: "q2wloq" }]
1117
+ ]);
1118
+
837
1119
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/triangle-alert.js
838
1120
  var TriangleAlert = createLucideIcon("TriangleAlert", [
839
1121
  [
@@ -847,6 +1129,17 @@ var TriangleAlert = createLucideIcon("TriangleAlert", [
847
1129
  ["path", { d: "M12 17h.01", key: "p32p05" }]
848
1130
  ]);
849
1131
 
1132
+ // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/wrench.js
1133
+ var Wrench = createLucideIcon("Wrench", [
1134
+ [
1135
+ "path",
1136
+ {
1137
+ 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",
1138
+ key: "cbrjhi"
1139
+ }
1140
+ ]
1141
+ ]);
1142
+
850
1143
  // ../../node_modules/.pnpm/lucide-react@0.468.0_react@19.2.4/node_modules/lucide-react/dist/esm/icons/x.js
851
1144
  var X = createLucideIcon("X", [
852
1145
  ["path", { d: "M18 6 6 18", key: "1bl5f8" }],
@@ -854,23 +1147,177 @@ var X = createLucideIcon("X", [
854
1147
  ]);
855
1148
 
856
1149
  // src/components/AgentChat.tsx
857
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo8, useState as useState12 } from "react";
1150
+ import { useCallback as useCallback8, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef14, useState as useState16 } from "react";
1151
+
1152
+ // src/components/SessionPluginSelector.tsx
1153
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
1154
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1155
+ function SessionPluginSelector(props) {
1156
+ return /* @__PURE__ */ jsx2(PluginSelectorState, { ...props }, props.sessionId);
1157
+ }
1158
+ function PluginSelectorState({ client, sessionId, className, side = "bottom", onChange }) {
1159
+ const [open, setOpen] = useState4(false);
1160
+ const [plugins, setPlugins] = useState4([]);
1161
+ const [loading, setLoading] = useState4(false);
1162
+ const [busy, setBusy] = useState4(false);
1163
+ const [listError, setListError] = useState4(false);
1164
+ const [mutationError, setMutationError] = useState4(false);
1165
+ const alive = useRef4(true);
1166
+ const generation = useRef4(0);
1167
+ const mutationGenerations = useRef4(/* @__PURE__ */ new Map());
1168
+ const desiredActivations = useRef4(/* @__PURE__ */ new Map());
1169
+ const pendingMutations = useRef4(/* @__PURE__ */ new Map());
1170
+ const lifetime = useRef4(0);
1171
+ const request = useRef4(null);
1172
+ useEffect4(() => {
1173
+ alive.current = true;
1174
+ setPlugins([]);
1175
+ setOpen(false);
1176
+ setLoading(false);
1177
+ setBusy(false);
1178
+ setListError(false);
1179
+ setMutationError(false);
1180
+ desiredActivations.current.clear();
1181
+ mutationGenerations.current.clear();
1182
+ pendingMutations.current.clear();
1183
+ return () => {
1184
+ alive.current = false;
1185
+ lifetime.current++;
1186
+ generation.current++;
1187
+ request.current?.abort();
1188
+ };
1189
+ }, [client]);
1190
+ const load = async () => {
1191
+ if (!alive.current) return;
1192
+ request.current?.abort();
1193
+ const controller = new AbortController();
1194
+ request.current = controller;
1195
+ const current = ++generation.current;
1196
+ setLoading(true);
1197
+ setListError(false);
1198
+ try {
1199
+ const result = await client.sessions.listSessionPlugins(sessionId, { signal: controller.signal });
1200
+ if (alive.current && current === generation.current) {
1201
+ setPlugins(result.plugins.map((item) => {
1202
+ const pending = pendingMutations.current.has(item.name);
1203
+ const desired = desiredActivations.current.get(item.name);
1204
+ return pending && desired !== void 0 ? { ...item, active: desired } : item;
1205
+ }));
1206
+ }
1207
+ } catch {
1208
+ if (alive.current && current === generation.current && !controller.signal.aborted) setListError(true);
1209
+ } finally {
1210
+ if (alive.current && current === generation.current) setLoading(false);
1211
+ }
1212
+ };
1213
+ const select = async (name, active) => {
1214
+ desiredActivations.current.set(name, active);
1215
+ const currentLifetime = lifetime.current;
1216
+ const currentMutation = (mutationGenerations.current.get(name) ?? 0) + 1;
1217
+ mutationGenerations.current.set(name, currentMutation);
1218
+ pendingMutations.current.set(name, currentMutation);
1219
+ request.current?.abort();
1220
+ generation.current++;
1221
+ setLoading(false);
1222
+ setBusy(true);
1223
+ setMutationError(false);
1224
+ setPlugins((current) => current.map((item) => item.name === name ? { ...item, active } : item));
1225
+ try {
1226
+ const { plugin } = await client.sessions.setSessionPluginActivation(sessionId, name, active);
1227
+ if (!alive.current || currentLifetime !== lifetime.current) return;
1228
+ if (currentMutation !== mutationGenerations.current.get(name)) {
1229
+ const desired = desiredActivations.current.get(name);
1230
+ if (desired !== void 0 && desired !== plugin.active) void select(name, desired);
1231
+ return;
1232
+ }
1233
+ pendingMutations.current.delete(name);
1234
+ setBusy(pendingMutations.current.size > 0);
1235
+ setPlugins((current) => current.map((item) => item.name === plugin.name ? { ...item, ...plugin } : item));
1236
+ onChange?.(plugin);
1237
+ await load();
1238
+ } catch {
1239
+ if (alive.current && currentLifetime === lifetime.current && currentMutation === mutationGenerations.current.get(name)) {
1240
+ pendingMutations.current.delete(name);
1241
+ setBusy(pendingMutations.current.size > 0);
1242
+ setMutationError(true);
1243
+ void load();
1244
+ }
1245
+ }
1246
+ };
1247
+ return /* @__PURE__ */ jsxs("div", { className: `relative text-xs ${className ?? ""}`, children: [
1248
+ /* @__PURE__ */ jsx2(
1249
+ "button",
1250
+ {
1251
+ type: "button",
1252
+ disabled: !sessionId,
1253
+ "aria-expanded": open,
1254
+ onClick: () => {
1255
+ setOpen(!open);
1256
+ if (!open) void load();
1257
+ },
1258
+ className: "rounded-full border border-[hsl(var(--border))] px-2.5 py-1",
1259
+ children: "\u4F1A\u8BDD\u63D2\u4EF6"
1260
+ }
1261
+ ),
1262
+ open ? /* @__PURE__ */ jsxs("div", { className: `absolute left-0 z-40 my-2 max-h-72 w-72 max-w-[calc(100vw-2rem)] overflow-y-auto rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--popover))] p-3 text-[hsl(var(--popover-foreground))] shadow-lg ${side === "top" ? "bottom-full" : "top-full"}`, children: [
1263
+ loading ? /* @__PURE__ */ jsx2("div", { children: "\u6B63\u5728\u83B7\u53D6\u63D2\u4EF6\u2026" }) : null,
1264
+ listError ? /* @__PURE__ */ jsx2("button", { type: "button", onClick: () => void load(), children: "\u63D2\u4EF6\u5217\u8868\u6682\u4E0D\u53EF\u7528\uFF0C\u70B9\u51FB\u91CD\u8BD5" }) : null,
1265
+ !loading && !listError && !plugins.length ? /* @__PURE__ */ jsx2("div", { children: "\u5C1A\u672A\u5B89\u88C5\u63D2\u4EF6" }) : null,
1266
+ plugins.map((plugin) => /* @__PURE__ */ jsxs("div", { className: "py-1", children: [
1267
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2", children: [
1268
+ /* @__PURE__ */ jsx2("span", { className: "min-w-0 truncate", children: plugin.name }),
1269
+ /* @__PURE__ */ jsx2(
1270
+ "input",
1271
+ {
1272
+ type: "checkbox",
1273
+ checked: plugin.active,
1274
+ disabled: busy && !plugin.active || !plugin.installed && !plugin.active,
1275
+ onChange: (event) => void select(plugin.name, event.target.checked),
1276
+ "aria-label": `${plugin.name} \u4F1A\u8BDD\u6FC0\u6D3B`
1277
+ }
1278
+ )
1279
+ ] }),
1280
+ plugin.active && plugin.status !== "valid" ? /* @__PURE__ */ jsxs("div", { className: "text-[hsl(var(--muted-foreground))]", children: [
1281
+ plugin.reason || "\u63D2\u4EF6\u6682\u4E0D\u53EF\u7528",
1282
+ /* @__PURE__ */ jsx2("button", { type: "button", disabled: busy, onClick: () => void select(plugin.name, true), className: "ml-2 underline", children: "\u91CD\u8BD5" })
1283
+ ] }) : null
1284
+ ] }, plugin.name)),
1285
+ mutationError ? /* @__PURE__ */ jsx2("div", { children: "\u64CD\u4F5C\u672A\u5B8C\u6210\uFF0C\u8BF7\u91CD\u8BD5" }) : null
1286
+ ] }) : null
1287
+ ] });
1288
+ }
858
1289
 
859
1290
  // src/lib/utils.ts
860
1291
  function cn(...inputs) {
861
1292
  return clsx(inputs);
862
1293
  }
863
1294
  async function copyToClipboard(text) {
864
- try {
865
- await navigator.clipboard.writeText(text);
866
- return true;
867
- } catch {
1295
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
1296
+ if (clipboard && typeof clipboard.writeText === "function") {
1297
+ try {
1298
+ await clipboard.writeText(text);
1299
+ return true;
1300
+ } catch {
1301
+ }
1302
+ }
1303
+ if (typeof document === "undefined" || typeof document.execCommand !== "function") {
868
1304
  return false;
869
1305
  }
1306
+ const textarea = document.createElement("textarea");
1307
+ textarea.value = text;
1308
+ textarea.style.position = "fixed";
1309
+ textarea.style.opacity = "0";
1310
+ document.body.appendChild(textarea);
1311
+ textarea.select();
1312
+ try {
1313
+ return document.execCommand("copy");
1314
+ } finally {
1315
+ document.body.removeChild(textarea);
1316
+ }
870
1317
  }
871
1318
 
872
1319
  // src/components/ReplayBar.tsx
873
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1320
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
874
1321
  var SPEED_OPTIONS = [1, 2, 5];
875
1322
  function ReplayBar({
876
1323
  isReplay,
@@ -881,7 +1328,7 @@ function ReplayBar({
881
1328
  className
882
1329
  }) {
883
1330
  if (!isReplay) return null;
884
- return /* @__PURE__ */ jsxs(
1331
+ return /* @__PURE__ */ jsxs2(
885
1332
  "div",
886
1333
  {
887
1334
  className: cn(
@@ -889,13 +1336,13 @@ function ReplayBar({
889
1336
  className
890
1337
  ),
891
1338
  children: [
892
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 font-medium text-[hsl(var(--foreground))]", children: [
893
- /* @__PURE__ */ jsx2(Play, { size: 13 }),
1339
+ /* @__PURE__ */ jsxs2("span", { className: "inline-flex items-center gap-1.5 font-medium text-[hsl(var(--foreground))]", children: [
1340
+ /* @__PURE__ */ jsx3(Play, { size: 13 }),
894
1341
  "\u56DE\u653E\u6A21\u5F0F"
895
1342
  ] }),
896
- /* @__PURE__ */ jsx2("span", { className: "text-[hsl(var(--muted-foreground))]", children: "\u6B63\u5728\u91CD\u73B0\u4E4B\u524D\u7684\u5BF9\u8BDD" }),
897
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
898
- /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-0.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-0.5", children: SPEED_OPTIONS.map((option) => /* @__PURE__ */ jsxs(
1343
+ /* @__PURE__ */ jsx3("span", { className: "text-[hsl(var(--muted-foreground))]", children: "\u6B63\u5728\u91CD\u73B0\u4E4B\u524D\u7684\u5BF9\u8BDD" }),
1344
+ /* @__PURE__ */ jsxs2("div", { className: "ml-auto flex items-center gap-2", children: [
1345
+ /* @__PURE__ */ jsx3("div", { className: "flex items-center gap-0.5 rounded-md border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-0.5", children: SPEED_OPTIONS.map((option) => /* @__PURE__ */ jsxs2(
899
1346
  "button",
900
1347
  {
901
1348
  type: "button",
@@ -914,7 +1361,7 @@ function ReplayBar({
914
1361
  },
915
1362
  option
916
1363
  )) }),
917
- /* @__PURE__ */ jsx2(
1364
+ /* @__PURE__ */ jsx3(
918
1365
  "button",
919
1366
  {
920
1367
  type: "button",
@@ -934,10 +1381,10 @@ function ReplayBar({
934
1381
  }
935
1382
 
936
1383
  // src/components/ReplayMismatchPrompt.tsx
937
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1384
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
938
1385
  function ReplayMismatchPrompt({ mismatch, className }) {
939
1386
  if (!mismatch) return null;
940
- return /* @__PURE__ */ jsxs2(
1387
+ return /* @__PURE__ */ jsxs3(
941
1388
  "div",
942
1389
  {
943
1390
  className: cn(
@@ -945,19 +1392,19 @@ function ReplayMismatchPrompt({ mismatch, className }) {
945
1392
  className
946
1393
  ),
947
1394
  children: [
948
- /* @__PURE__ */ jsx3("div", { className: "font-medium text-[hsl(var(--foreground))]", children: "\u8FD9\u53E5\u8BDD\u548C\u4E4B\u524D\u5F55\u5236\u7684\u4E0D\u4E00\u6837" }),
949
- /* @__PURE__ */ jsxs2("dl", { className: "mt-2 space-y-1 text-xs text-[hsl(var(--muted-foreground))]", children: [
950
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
951
- /* @__PURE__ */ jsx3("dt", { className: "shrink-0", children: "\u5F55\u5236\u7684\u662F" }),
952
- /* @__PURE__ */ jsx3("dd", { className: "min-w-0 break-words text-[hsl(var(--foreground))]", children: mismatch.expectedMessage || "\uFF08\u7A7A\uFF09" })
1395
+ /* @__PURE__ */ jsx4("div", { className: "font-medium text-[hsl(var(--foreground))]", children: "\u8FD9\u53E5\u8BDD\u548C\u4E4B\u524D\u5F55\u5236\u7684\u4E0D\u4E00\u6837" }),
1396
+ /* @__PURE__ */ jsxs3("dl", { className: "mt-2 space-y-1 text-xs text-[hsl(var(--muted-foreground))]", children: [
1397
+ /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
1398
+ /* @__PURE__ */ jsx4("dt", { className: "shrink-0", children: "\u5F55\u5236\u7684\u662F" }),
1399
+ /* @__PURE__ */ jsx4("dd", { className: "min-w-0 break-words text-[hsl(var(--foreground))]", children: mismatch.expectedMessage || "\uFF08\u7A7A\uFF09" })
953
1400
  ] }),
954
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
955
- /* @__PURE__ */ jsx3("dt", { className: "shrink-0", children: "\u4F60\u8F93\u5165\u7684" }),
956
- /* @__PURE__ */ jsx3("dd", { className: "min-w-0 break-words text-[hsl(var(--foreground))]", children: mismatch.actualMessage || "\uFF08\u7A7A\uFF09" })
1401
+ /* @__PURE__ */ jsxs3("div", { className: "flex gap-2", children: [
1402
+ /* @__PURE__ */ jsx4("dt", { className: "shrink-0", children: "\u4F60\u8F93\u5165\u7684" }),
1403
+ /* @__PURE__ */ jsx4("dd", { className: "min-w-0 break-words text-[hsl(var(--foreground))]", children: mismatch.actualMessage || "\uFF08\u7A7A\uFF09" })
957
1404
  ] })
958
1405
  ] }),
959
- /* @__PURE__ */ jsxs2("div", { className: "mt-3 flex flex-wrap gap-2", children: [
960
- /* @__PURE__ */ jsx3(
1406
+ /* @__PURE__ */ jsxs3("div", { className: "mt-3 flex flex-wrap gap-2", children: [
1407
+ /* @__PURE__ */ jsx4(
961
1408
  "button",
962
1409
  {
963
1410
  type: "button",
@@ -966,7 +1413,7 @@ function ReplayMismatchPrompt({ mismatch, className }) {
966
1413
  children: "\u6309\u5F55\u5236\u5185\u5BB9\u7EE7\u7EED"
967
1414
  }
968
1415
  ),
969
- /* @__PURE__ */ jsx3(
1416
+ /* @__PURE__ */ jsx4(
970
1417
  "button",
971
1418
  {
972
1419
  type: "button",
@@ -981,110 +1428,527 @@ function ReplayMismatchPrompt({ mismatch, className }) {
981
1428
  );
982
1429
  }
983
1430
 
984
- // src/components/ChatInput.tsx
985
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
986
- function ChatInput({
987
- value,
988
- onValueChange,
989
- onSend,
990
- onStop,
991
- isStreaming,
992
- isStopping = false,
993
- placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
994
- className
995
- }) {
996
- const trimmed = value.trim();
997
- const canSend = trimmed.length > 0 && !isStreaming;
998
- const handleSend = async () => {
999
- if (!canSend) return;
1000
- const accepted = await onSend(trimmed);
1001
- if (!accepted) return;
1002
- onValueChange("");
1003
- };
1004
- const handleKeyDown = (event) => {
1005
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1006
- event.preventDefault();
1007
- void handleSend();
1008
- }
1009
- };
1010
- return /* @__PURE__ */ jsx4("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: /* @__PURE__ */ jsxs3("div", { className: "blade-chat-input-inner mx-auto flex max-w-[748px] items-end gap-2 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2", children: [
1011
- /* @__PURE__ */ jsx4(
1012
- "textarea",
1013
- {
1014
- value,
1015
- onChange: (event) => onValueChange(event.target.value),
1016
- onKeyDown: handleKeyDown,
1017
- onInput: (event) => {
1018
- const el = event.currentTarget;
1019
- el.style.height = "auto";
1020
- el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
1021
- },
1022
- rows: 1,
1023
- placeholder,
1024
- "aria-label": "\u804A\u5929\u8F93\u5165",
1025
- className: "blade-chat-textarea max-h-48 min-h-[28px] flex-1 resize-none bg-transparent py-1 text-sm leading-6 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.6)]"
1026
- }
1027
- ),
1028
- isStreaming ? /* @__PURE__ */ jsx4(
1029
- "button",
1030
- {
1031
- type: "button",
1032
- onClick: onStop,
1033
- disabled: isStopping,
1034
- "aria-label": isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
1035
- title: isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D",
1036
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] transition-opacity hover:opacity-90 disabled:opacity-60",
1037
- children: isStopping ? /* @__PURE__ */ jsx4(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx4(Square, { size: 12, fill: "currentColor" })
1038
- }
1039
- ) : /* @__PURE__ */ jsx4(
1040
- "button",
1041
- {
1042
- type: "button",
1043
- onClick: handleSend,
1044
- disabled: !canSend,
1045
- "aria-label": "\u53D1\u9001\u6D88\u606F",
1046
- title: "\u53D1\u9001\u6D88\u606F",
1047
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",
1048
- children: /* @__PURE__ */ jsx4(ArrowUp, { size: 15 })
1049
- }
1050
- )
1051
- ] }) });
1052
- }
1431
+ // src/components/PlanUpdateBlock.tsx
1432
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
1053
1433
 
1054
- // src/components/ConnectionBanner.tsx
1055
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1056
- function ConnectionBanner({ connection, className }) {
1057
- if (connection === "connected" || connection === "connecting") {
1434
+ // src/components/display-utils.ts
1435
+ var TOOL_NAME_ALIASES = {
1436
+ agent: "Agent",
1437
+ ask_user_question: "AskUserQuestion",
1438
+ bash: "Bash",
1439
+ bg_bash: "BgBash",
1440
+ edit: "Edit",
1441
+ exit_plan_mode: "ExitPlanMode",
1442
+ file_edit: "Edit",
1443
+ file_read: "Read",
1444
+ file_write: "Write",
1445
+ finish_task: "FinishTask",
1446
+ glob: "Glob",
1447
+ grep: "Grep",
1448
+ kb_search: "KbSearch",
1449
+ ls: "Ls",
1450
+ multi_edit: "MultiEdit",
1451
+ read: "Read",
1452
+ read_skill: "ReadSkill",
1453
+ update_plan: "UpdatePlan",
1454
+ web_fetch: "WebFetch",
1455
+ web_search: "WebSearch",
1456
+ write: "Write"
1457
+ };
1458
+ var TOOL_DISPLAY_LABELS = {
1459
+ Bash: "\u6267\u884C\u547D\u4EE4",
1460
+ BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1461
+ Read: "\u8BFB\u53D6\u6587\u4EF6",
1462
+ Write: "\u5199\u5165\u6587\u4EF6",
1463
+ Edit: "\u7F16\u8F91\u6587\u4EF6",
1464
+ MultiEdit: "\u7F16\u8F91\u6587\u4EF6",
1465
+ Ls: "\u5217\u51FA\u76EE\u5F55",
1466
+ Glob: "\u5339\u914D\u6587\u4EF6",
1467
+ Grep: "\u641C\u7D22\u6587\u672C",
1468
+ KbSearch: "\u68C0\u7D22\u77E5\u8BC6\u5E93",
1469
+ WebSearch: "\u641C\u7D22\u7F51\u9875",
1470
+ WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1471
+ Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1472
+ AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1473
+ ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1474
+ FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1475
+ ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1476
+ ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1477
+ GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1478
+ };
1479
+ function safeParseJson(value) {
1480
+ if (!value) return null;
1481
+ try {
1482
+ return JSON.parse(value);
1483
+ } catch {
1058
1484
  return null;
1059
1485
  }
1060
- const reconnecting = connection === "reconnecting";
1061
- return /* @__PURE__ */ jsx5("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs4(
1062
- "div",
1063
- {
1064
- className: cn(
1065
- "mx-auto flex max-w-3xl items-start gap-3 rounded-2xl border px-4 py-3",
1066
- reconnecting ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1067
- ),
1068
- children: [
1069
- /* @__PURE__ */ jsx5("span", { className: "mt-0.5 shrink-0", children: reconnecting ? /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx5(TriangleAlert, { size: 14 }) }),
1070
- /* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
1071
- /* @__PURE__ */ jsx5("div", { className: "text-sm font-medium", children: reconnecting ? "\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u91CD\u8FDE\u2026" : "\u8FDE\u63A5\u5DF2\u65AD\u5F00" }),
1072
- /* @__PURE__ */ jsx5("div", { className: "text-xs opacity-80", children: "\u6D88\u606F\u540C\u6B65\u53EF\u80FD\u4F1A\u5EF6\u8FDF\uFF0C\u7CFB\u7EDF\u4F1A\u7EE7\u7EED\u81EA\u52A8\u91CD\u8BD5" })
1073
- ] })
1074
- ]
1075
- }
1076
- ) });
1077
1486
  }
1078
-
1079
- // src/components/MessageList.tsx
1080
- import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1081
- import { useCallback as useCallback6, useEffect as useEffect7, useMemo as useMemo7, useRef as useRef7, useState as useState11 } from "react";
1082
-
1083
- // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1084
- import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
1085
- var DEFAULT_SPRING_ANIMATION = {
1086
- /**
1087
- * A value from 0 to 1, on how much to damp the animation.
1487
+ function getStringArgValue(args, key) {
1488
+ const value = args?.[key];
1489
+ return typeof value === "string" ? value.trim() : "";
1490
+ }
1491
+ var SKILL_ENTRY_FILE_NAMES = /* @__PURE__ */ new Set(["skill.md", "command.md"]);
1492
+ var NON_SKILL_DIR_NAMES = /* @__PURE__ */ new Set([".", "..", ".agent", ".agents", ".claude", "skill_data", "skills"]);
1493
+ function getSkillNameFromFilePath(filePath) {
1494
+ if (!filePath) return null;
1495
+ const segments = filePath.split(/[\\/]+/).filter(Boolean);
1496
+ const fileName = segments.pop();
1497
+ if (!fileName || !SKILL_ENTRY_FILE_NAMES.has(fileName.toLowerCase())) return null;
1498
+ const dirName = segments.pop();
1499
+ if (!dirName || NON_SKILL_DIR_NAMES.has(dirName.toLowerCase())) return null;
1500
+ return dirName;
1501
+ }
1502
+ function formatToolName(name) {
1503
+ const trimmed = name.trim();
1504
+ if (!trimmed) return name;
1505
+ const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1506
+ const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1507
+ return TOOL_NAME_ALIASES[normalized] ?? stripped;
1508
+ }
1509
+ function getToolDisplayLabel(toolCall) {
1510
+ const normalized = formatToolName(toolCall.name);
1511
+ const args = safeParseJson(toolCall.arguments);
1512
+ const displayName = toolCall.display_name?.trim() ?? "";
1513
+ const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1514
+ const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1515
+ if (metaDisplayName) {
1516
+ return metaDisplayName;
1517
+ }
1518
+ const description = getStringArgValue(args, "description");
1519
+ if (normalized === "BgBash") {
1520
+ return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1521
+ }
1522
+ if (normalized === "ReadSkill") {
1523
+ const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1524
+ return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1525
+ }
1526
+ if (normalized === "Read") {
1527
+ const skillName = getSkillNameFromFilePath(
1528
+ getStringArgValue(args, "file_path") || getStringArgValue(args, "path")
1529
+ );
1530
+ if (skillName) return `\u8BFB\u53D6\u6280\u80FD\u300C${skillName}\u300D`;
1531
+ }
1532
+ if (normalized === "FinishTask") {
1533
+ const title = getStringArgValue(args, "title");
1534
+ return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1535
+ }
1536
+ return description || baseLabel;
1537
+ }
1538
+ function getToolTone(status) {
1539
+ if (status === "error" || status === "cancelled") return "red";
1540
+ if (status === "awaiting_answer") return "amber";
1541
+ if (status === "pending") return "blue";
1542
+ return "emerald";
1543
+ }
1544
+ function getToolStatusLabel(status) {
1545
+ if (status === "pending") return "\u8FD0\u884C\u4E2D";
1546
+ if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1547
+ if (status === "error") return "\u9519\u8BEF";
1548
+ if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1549
+ return "\u5B8C\u6210";
1550
+ }
1551
+ function formatToolDuration(ms) {
1552
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
1553
+ const seconds = ms / 1e3;
1554
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
1555
+ const minutes = Math.floor(seconds / 60);
1556
+ const remainingSeconds = Math.round(seconds % 60);
1557
+ return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
1558
+ }
1559
+ function formatToolArgs(args) {
1560
+ try {
1561
+ return JSON.stringify(JSON.parse(args), null, 2);
1562
+ } catch {
1563
+ return args;
1564
+ }
1565
+ }
1566
+ var RESULT_PREVIEW_LIMIT = 4e3;
1567
+ function formatToolResult(result) {
1568
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1569
+ if (text == null) return "";
1570
+ if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1571
+ return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1572
+ \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
1573
+ }
1574
+
1575
+ // src/components/PlanUpdateBlock.tsx
1576
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1577
+ var PLAN_STEP_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
1578
+ var PLAN_AUTO_COLLAPSE_MS = 5e3;
1579
+ function isPlanUpdateTool(toolCall) {
1580
+ return formatToolName(toolCall.name) === "UpdatePlan";
1581
+ }
1582
+ function getPlanUpdateDisplayState(messages) {
1583
+ let current = null;
1584
+ let latestAttempt = null;
1585
+ let latestAttemptStreaming = false;
1586
+ for (const message of messages) {
1587
+ if ((message.loop_name ?? "root") !== "root") continue;
1588
+ for (const toolCall of message.tool_calls ?? []) {
1589
+ if (!isPlanUpdateTool(toolCall)) continue;
1590
+ latestAttempt = toolCall;
1591
+ latestAttemptStreaming = message.status === "streaming";
1592
+ if (toolCall.status === "done" && parsePlanUpdate(toolCall.arguments)) current = toolCall;
1593
+ }
1594
+ }
1595
+ return {
1596
+ current,
1597
+ updating: latestAttempt?.status === "pending" && latestAttemptStreaming
1598
+ };
1599
+ }
1600
+ function parsePlanUpdate(argumentsJson) {
1601
+ try {
1602
+ const raw = JSON.parse(argumentsJson);
1603
+ if (!raw || typeof raw !== "object") return null;
1604
+ const candidate = raw;
1605
+ if (!Array.isArray(candidate.plan)) return null;
1606
+ const plan = candidate.plan.map((item) => {
1607
+ if (!item || typeof item !== "object") return null;
1608
+ const step = item.step;
1609
+ const status = item.status;
1610
+ if (typeof step !== "string" || !step.trim() || typeof status !== "string" || !PLAN_STEP_STATUSES.has(status)) {
1611
+ return null;
1612
+ }
1613
+ return { step: step.trim(), status };
1614
+ });
1615
+ if (plan.some((item) => item === null)) return null;
1616
+ if (plan.filter((item) => item?.status === "in_progress").length > 1) return null;
1617
+ return { plan };
1618
+ } catch {
1619
+ return null;
1620
+ }
1621
+ }
1622
+ function pickCurrentPlanStep(plan) {
1623
+ return plan.find((item) => item.status === "in_progress") ?? plan.find((item) => item.status === "pending") ?? plan[plan.length - 1] ?? null;
1624
+ }
1625
+ function PlanStepIcon({
1626
+ status,
1627
+ size = 17,
1628
+ running = false
1629
+ }) {
1630
+ if (status === "completed") {
1631
+ return /* @__PURE__ */ jsx5(Check, { size, strokeWidth: 2, className: "shrink-0 text-emerald-500" });
1632
+ }
1633
+ if (status === "in_progress") {
1634
+ return running ? /* @__PURE__ */ jsx5(LoaderCircle, { size, className: "shrink-0 animate-spin text-[hsl(var(--muted-foreground))]" }) : /* @__PURE__ */ jsx5(CircleDot, { size, className: "shrink-0 text-amber-500" });
1635
+ }
1636
+ return /* @__PURE__ */ jsx5(Circle, { size, className: "shrink-0 text-[hsl(var(--muted-foreground))]/60" });
1637
+ }
1638
+ function PlanUpdateBlock({
1639
+ toolCall,
1640
+ running = false,
1641
+ autoReveal = false
1642
+ }) {
1643
+ const updateKey = `${toolCall.id}:${toolCall.arguments}`;
1644
+ const revealKey = autoReveal ? updateKey : null;
1645
+ const [collapsed, setCollapsed] = useState5(!autoReveal);
1646
+ const collapseTimerRef = useRef5(null);
1647
+ const data = parsePlanUpdate(toolCall.arguments);
1648
+ useEffect5(() => {
1649
+ if (!revealKey) return;
1650
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1651
+ setCollapsed(false);
1652
+ collapseTimerRef.current = setTimeout(() => {
1653
+ setCollapsed(true);
1654
+ collapseTimerRef.current = null;
1655
+ }, PLAN_AUTO_COLLAPSE_MS);
1656
+ }, [revealKey]);
1657
+ useEffect5(
1658
+ () => () => {
1659
+ if (collapseTimerRef.current) clearTimeout(collapseTimerRef.current);
1660
+ },
1661
+ []
1662
+ );
1663
+ if (!data) return null;
1664
+ const completed = data.plan.filter((item) => item.status === "completed").length;
1665
+ const currentStep = pickCurrentPlanStep(data.plan);
1666
+ const pausedAtCurrentStep = !running && currentStep?.status === "in_progress";
1667
+ return /* @__PURE__ */ jsxs4("section", { className: "overflow-hidden", children: [
1668
+ /* @__PURE__ */ jsxs4(
1669
+ "button",
1670
+ {
1671
+ type: "button",
1672
+ "aria-expanded": !collapsed,
1673
+ onClick: () => {
1674
+ if (collapseTimerRef.current) {
1675
+ clearTimeout(collapseTimerRef.current);
1676
+ collapseTimerRef.current = null;
1677
+ }
1678
+ setCollapsed((value) => !value);
1679
+ },
1680
+ className: cn(
1681
+ "flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[hsl(var(--muted)/0.3)]",
1682
+ !collapsed && "border-b border-[hsl(var(--border))]"
1683
+ ),
1684
+ children: [
1685
+ collapsed && currentStep ? /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-xs text-[hsl(var(--foreground))]", children: [
1686
+ /* @__PURE__ */ jsx5(PlanStepIcon, { status: currentStep.status, size: 14, running }),
1687
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: currentStep.step }),
1688
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[11px] text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1689
+ ] }) : /* @__PURE__ */ jsxs4("span", { className: "flex min-w-0 flex-1 items-center gap-1.5 text-[11px] text-[hsl(var(--muted-foreground))]", children: [
1690
+ /* @__PURE__ */ jsx5(ListChecks, { size: 14, className: "shrink-0", "aria-hidden": "true" }),
1691
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: "\u4EFB\u52A1\u8FDB\u5EA6" }),
1692
+ pausedAtCurrentStep ? /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-amber-500", children: "\u5DF2\u6682\u505C" }) : null
1693
+ ] }),
1694
+ /* @__PURE__ */ jsxs4("span", { className: "shrink-0 text-[11px] tabular-nums text-[hsl(var(--muted-foreground))]", children: [
1695
+ completed,
1696
+ "/",
1697
+ data.plan.length
1698
+ ] }),
1699
+ /* @__PURE__ */ jsx5(
1700
+ ChevronDown,
1701
+ {
1702
+ size: 14,
1703
+ className: cn(
1704
+ "shrink-0 text-[hsl(var(--muted-foreground))] transition-transform duration-300 ease-out motion-reduce:transition-none",
1705
+ !collapsed && "rotate-180"
1706
+ )
1707
+ }
1708
+ )
1709
+ ]
1710
+ }
1711
+ ),
1712
+ /* @__PURE__ */ jsx5(
1713
+ "div",
1714
+ {
1715
+ "aria-hidden": collapsed,
1716
+ className: cn(
1717
+ "grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none",
1718
+ collapsed ? "grid-rows-[0fr] opacity-0" : "grid-rows-[1fr] opacity-100"
1719
+ ),
1720
+ children: /* @__PURE__ */ jsx5("div", { className: "min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx5("div", { className: "flex max-h-40 flex-col gap-0.5 overflow-y-auto px-3 py-2", children: data.plan.length === 0 ? /* @__PURE__ */ jsx5("span", { className: "text-xs text-[hsl(var(--muted-foreground))]", children: "\u6682\u65E0\u4EFB\u52A1\u6B65\u9AA4" }) : data.plan.map((item, index) => /* @__PURE__ */ jsxs4("div", { className: "flex items-start gap-2 py-0.5", children: [
1721
+ /* @__PURE__ */ jsx5("span", { className: "mt-[3px] flex shrink-0", children: /* @__PURE__ */ jsx5(PlanStepIcon, { status: item.status, size: 14, running }) }),
1722
+ /* @__PURE__ */ jsx5(
1723
+ "span",
1724
+ {
1725
+ className: cn(
1726
+ "min-w-0 flex-1 break-words text-[13px] leading-5",
1727
+ item.status === "completed" ? "text-[hsl(var(--muted-foreground))]" : item.status === "in_progress" ? "font-medium text-[hsl(var(--foreground))]" : "text-[hsl(var(--muted-foreground))]"
1728
+ ),
1729
+ children: item.step
1730
+ }
1731
+ )
1732
+ ] }, `${index}-${item.step}`)) }) })
1733
+ }
1734
+ )
1735
+ ] });
1736
+ }
1737
+ function CurrentPlanPanel({
1738
+ messages,
1739
+ running = false,
1740
+ revealRevision = 0,
1741
+ sessionId,
1742
+ className
1743
+ }) {
1744
+ const { current, updating } = getPlanUpdateDisplayState(messages);
1745
+ const revealBaselinesRef = useRef5(/* @__PURE__ */ new Map([[sessionId, revealRevision]]));
1746
+ const autoReveal = (revealBaselinesRef.current.get(sessionId) ?? 0) !== revealRevision;
1747
+ useEffect5(() => {
1748
+ if (!current) return;
1749
+ revealBaselinesRef.current.set(sessionId, revealRevision);
1750
+ }, [current, revealRevision, sessionId]);
1751
+ if (!current && !updating) return null;
1752
+ return /* @__PURE__ */ jsxs4("div", { className: cn("blade-chat-plan mx-auto w-full max-w-[748px] px-4", className), children: [
1753
+ updating ? /* @__PURE__ */ jsxs4("div", { className: "mb-2 flex items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
1754
+ /* @__PURE__ */ jsx5(LoaderCircle, { size: 14, className: "shrink-0 animate-spin" }),
1755
+ /* @__PURE__ */ jsx5("span", { children: "\u6B63\u5728\u66F4\u65B0\u4EFB\u52A1\u8FDB\u5EA6\u2026" })
1756
+ ] }) : null,
1757
+ current ? /* @__PURE__ */ jsx5(
1758
+ PlanUpdateBlock,
1759
+ {
1760
+ toolCall: current,
1761
+ running,
1762
+ autoReveal
1763
+ },
1764
+ sessionId ?? "current-session"
1765
+ ) : null
1766
+ ] });
1767
+ }
1768
+
1769
+ // src/components/ChatSurface.tsx
1770
+ import { chatErrorForDisplay as chatErrorForDisplay2 } from "@blade-hq/agent-client";
1771
+
1772
+ // src/components/ChatInput.tsx
1773
+ import { useState as useState6 } from "react";
1774
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1775
+ function isImeCompositionKey(event) {
1776
+ return event.isComposing || event.keyCode === 229;
1777
+ }
1778
+ function shouldSubmitChatInput(event, menuOpen) {
1779
+ return event.key === "Enter" && !event.shiftKey && !menuOpen && !isImeCompositionKey(event);
1780
+ }
1781
+ function ChatInput({
1782
+ value,
1783
+ onValueChange,
1784
+ onSend,
1785
+ onAppend,
1786
+ onStop,
1787
+ isStreaming,
1788
+ isStopping = false,
1789
+ placeholder = "\u8F93\u5165\u6D88\u606F\u2026",
1790
+ className,
1791
+ queueKey
1792
+ }) {
1793
+ const trimmed = value.trim();
1794
+ const [sendMode, setSendMode] = useState6("queue");
1795
+ void queueKey;
1796
+ const canSend = trimmed.length > 0 && (!isStreaming || !isStopping && sendMode === "queue" && !!onAppend);
1797
+ const handleSend = async () => {
1798
+ if (!canSend) return;
1799
+ if (isStreaming && sendMode === "queue") {
1800
+ if (onAppend) onAppend(trimmed);
1801
+ onValueChange("");
1802
+ return;
1803
+ }
1804
+ if (isStreaming && sendMode === "direct") {
1805
+ if (!onAppend) return;
1806
+ onAppend(trimmed);
1807
+ onValueChange("");
1808
+ return;
1809
+ }
1810
+ const accepted = await onSend(trimmed);
1811
+ if (!accepted) return;
1812
+ onValueChange("");
1813
+ };
1814
+ const handleKeyDown = (event) => {
1815
+ if (shouldSubmitChatInput({
1816
+ key: event.key,
1817
+ shiftKey: event.shiftKey,
1818
+ isComposing: event.nativeEvent.isComposing,
1819
+ keyCode: event.nativeEvent.keyCode
1820
+ }, false)) {
1821
+ event.preventDefault();
1822
+ void handleSend();
1823
+ }
1824
+ };
1825
+ return /* @__PURE__ */ jsxs5("div", { className: cn("blade-chat-input border-t border-[hsl(var(--border))] py-3", className), children: [
1826
+ /* @__PURE__ */ jsx6("div", { className: "mx-auto mb-2 flex max-w-[748px] items-center justify-between px-1 text-xs text-[hsl(var(--muted-foreground))]", children: /* @__PURE__ */ jsxs5("fieldset", { className: "flex items-center gap-1 rounded-md border border-[hsl(var(--border))] p-0.5", children: [
1827
+ /* @__PURE__ */ jsx6("legend", { className: "sr-only", children: "\u53D1\u9001\u65B9\u5F0F" }),
1828
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setSendMode("direct"), "aria-pressed": sendMode === "direct", disabled: isStreaming && !onAppend, className: `rounded px-2 py-1 ${sendMode === "direct" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u76F4\u63A5\u63D2\u5165" }),
1829
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => setSendMode("queue"), "aria-pressed": sendMode === "queue", className: `rounded px-2 py-1 ${sendMode === "queue" ? "bg-[hsl(var(--accent))] text-[hsl(var(--foreground))]" : ""}`, children: "\u6392\u961F\u6267\u884C" })
1830
+ ] }) }),
1831
+ /* @__PURE__ */ jsxs5("div", { className: "blade-chat-input-inner mx-auto flex max-w-[748px] items-end gap-2 rounded-2xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] px-3 py-2", children: [
1832
+ /* @__PURE__ */ jsx6(
1833
+ "textarea",
1834
+ {
1835
+ value,
1836
+ onChange: (event) => onValueChange(event.target.value),
1837
+ onKeyDown: handleKeyDown,
1838
+ onInput: (event) => {
1839
+ const el = event.currentTarget;
1840
+ el.style.height = "auto";
1841
+ el.style.height = `${Math.min(el.scrollHeight, 192)}px`;
1842
+ },
1843
+ rows: 1,
1844
+ placeholder,
1845
+ "aria-label": "\u804A\u5929\u8F93\u5165",
1846
+ className: "blade-chat-textarea max-h-48 min-h-[28px] flex-1 resize-none bg-transparent py-1 text-sm leading-6 text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.6)]"
1847
+ }
1848
+ ),
1849
+ isStreaming ? /* @__PURE__ */ jsxs5(Fragment, { children: [
1850
+ sendMode === "queue" ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: handleSend, disabled: !canSend, "aria-label": "\u52A0\u5165\u6392\u961F", title: "\u52A0\u5165\u6392\u961F", className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] disabled:opacity-40", children: /* @__PURE__ */ jsx6(ArrowUp, { size: 15 }) }) : null,
1851
+ /* @__PURE__ */ jsx6("button", { type: "button", onClick: onStop, disabled: isStopping, "aria-label": isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D", title: isStopping ? "\u6B63\u5728\u505C\u6B62" : "\u505C\u6B62\u56DE\u590D", className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--muted))] text-[hsl(var(--foreground))] transition-opacity hover:opacity-90 disabled:opacity-60", children: isStopping ? /* @__PURE__ */ jsx6(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx6(Square, { size: 12, fill: "currentColor" }) })
1852
+ ] }) : /* @__PURE__ */ jsx6(
1853
+ "button",
1854
+ {
1855
+ type: "button",
1856
+ onClick: handleSend,
1857
+ disabled: !canSend,
1858
+ "aria-label": "\u53D1\u9001\u6D88\u606F",
1859
+ title: "\u53D1\u9001\u6D88\u606F",
1860
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[hsl(var(--primary))] text-[hsl(var(--primary-foreground))] transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",
1861
+ children: /* @__PURE__ */ jsx6(ArrowUp, { size: 15 })
1862
+ }
1863
+ )
1864
+ ] })
1865
+ ] });
1866
+ }
1867
+
1868
+ // src/components/ConnectionBanner.tsx
1869
+ import { useEffect as useEffect6, useRef as useRef6, useState as useState7 } from "react";
1870
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1871
+ var CONNECTION_NOTICE_DELAY_MS = 3e3;
1872
+ var CONNECTION_ERROR_DELAY_MS = 15e3;
1873
+ function useConnectionNoticePhase(connected) {
1874
+ const [phase, setPhase] = useState7("hidden");
1875
+ const connectedRef = useRef6(connected);
1876
+ const timersRef = useRef6([]);
1877
+ connectedRef.current = connected;
1878
+ useEffect6(() => {
1879
+ const clearTimers = () => {
1880
+ for (const timer of timersRef.current) clearTimeout(timer);
1881
+ timersRef.current = [];
1882
+ };
1883
+ const startGracePeriod = () => {
1884
+ clearTimers();
1885
+ setPhase("hidden");
1886
+ timersRef.current = [
1887
+ setTimeout(() => setPhase("recovering"), CONNECTION_NOTICE_DELAY_MS),
1888
+ setTimeout(() => setPhase("failed"), CONNECTION_ERROR_DELAY_MS)
1889
+ ];
1890
+ };
1891
+ if (connected) {
1892
+ clearTimers();
1893
+ setPhase("hidden");
1894
+ } else {
1895
+ startGracePeriod();
1896
+ }
1897
+ const handleForeground = () => {
1898
+ if (!connectedRef.current) startGracePeriod();
1899
+ };
1900
+ const handleVisibilityChange = () => {
1901
+ if (document.visibilityState === "visible") handleForeground();
1902
+ };
1903
+ window.addEventListener("blade:app-active", handleForeground);
1904
+ window.addEventListener("focus", handleForeground);
1905
+ window.addEventListener("pageshow", handleForeground);
1906
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1907
+ return () => {
1908
+ clearTimers();
1909
+ window.removeEventListener("blade:app-active", handleForeground);
1910
+ window.removeEventListener("focus", handleForeground);
1911
+ window.removeEventListener("pageshow", handleForeground);
1912
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1913
+ };
1914
+ }, [connected]);
1915
+ return phase;
1916
+ }
1917
+ function ConnectionBanner({ connection, className }) {
1918
+ const hasConnectedRef = useRef6(connection === "connected" || connection === "reconnecting");
1919
+ if (connection === "connected") hasConnectedRef.current = true;
1920
+ const connected = connection === "connected";
1921
+ const phase = useConnectionNoticePhase(connected);
1922
+ if (connected || phase === "hidden") return null;
1923
+ const recovering = phase === "recovering";
1924
+ const firstConnection = !hasConnectedRef.current;
1925
+ return /* @__PURE__ */ jsx7("div", { className: cn("blade-chat-banner bg-[hsl(var(--background))] px-5 pt-3", className), children: /* @__PURE__ */ jsxs6(
1926
+ "div",
1927
+ {
1928
+ className: cn(
1929
+ "mx-auto flex max-w-3xl items-start gap-3 rounded-2xl border px-4 py-3",
1930
+ recovering ? "border-amber-500/25 bg-amber-500/10 text-amber-100" : "border-rose-500/25 bg-rose-500/10 text-rose-100"
1931
+ ),
1932
+ children: [
1933
+ /* @__PURE__ */ jsx7("span", { className: "mt-0.5 shrink-0", children: recovering ? /* @__PURE__ */ jsx7(LoaderCircle, { size: 14, className: "animate-spin" }) : /* @__PURE__ */ jsx7(TriangleAlert, { size: 14 }) }),
1934
+ /* @__PURE__ */ jsxs6("div", { className: "min-w-0", children: [
1935
+ /* @__PURE__ */ jsx7("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" }),
1936
+ /* @__PURE__ */ jsx7("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" })
1937
+ ] })
1938
+ ]
1939
+ }
1940
+ ) });
1941
+ }
1942
+
1943
+ // src/components/MessageList.tsx
1944
+ import { isHiddenInternalMessage } from "@blade-hq/agent-client";
1945
+ import { useCallback as useCallback7, useEffect as useEffect12, useMemo as useMemo7, useRef as useRef13, useState as useState15 } from "react";
1946
+
1947
+ // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/useStickToBottom.js
1948
+ import { useCallback as useCallback4, useMemo as useMemo3, useRef as useRef7, useState as useState8 } from "react";
1949
+ var DEFAULT_SPRING_ANIMATION = {
1950
+ /**
1951
+ * A value from 0 to 1, on how much to damp the animation.
1088
1952
  * 0 means no damping, 1 means full damping.
1089
1953
  *
1090
1954
  * @default 0.7
@@ -1118,12 +1982,12 @@ globalThis.document?.addEventListener("click", () => {
1118
1982
  mouseDown = false;
1119
1983
  });
1120
1984
  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);
1985
+ const [escapedFromLock, updateEscapedFromLock] = useState8(false);
1986
+ const [isAtBottom, updateIsAtBottom] = useState8(options.initial !== false);
1987
+ const [isNearBottom, setIsNearBottom] = useState8(false);
1988
+ const optionsRef = useRef7(null);
1125
1989
  optionsRef.current = options;
1126
- const isSelecting = useCallback3(() => {
1990
+ const isSelecting = useCallback4(() => {
1127
1991
  if (!mouseDown) {
1128
1992
  return false;
1129
1993
  }
@@ -1134,11 +1998,11 @@ var useStickToBottom = (options = {}) => {
1134
1998
  const range = selection.getRangeAt(0);
1135
1999
  return range.commonAncestorContainer.contains(scrollRef.current) || scrollRef.current?.contains(range.commonAncestorContainer);
1136
2000
  }, []);
1137
- const setIsAtBottom = useCallback3((isAtBottom2) => {
2001
+ const setIsAtBottom = useCallback4((isAtBottom2) => {
1138
2002
  state.isAtBottom = isAtBottom2;
1139
2003
  updateIsAtBottom(isAtBottom2);
1140
2004
  }, []);
1141
- const setEscapedFromLock = useCallback3((escapedFromLock2) => {
2005
+ const setEscapedFromLock = useCallback4((escapedFromLock2) => {
1142
2006
  state.escapedFromLock = escapedFromLock2;
1143
2007
  updateEscapedFromLock(escapedFromLock2);
1144
2008
  }, []);
@@ -1195,7 +2059,7 @@ var useStickToBottom = (options = {}) => {
1195
2059
  }
1196
2060
  };
1197
2061
  }, []);
1198
- const scrollToBottom = useCallback3((scrollOptions = {}) => {
2062
+ const scrollToBottom = useCallback4((scrollOptions = {}) => {
1199
2063
  if (typeof scrollOptions === "string") {
1200
2064
  scrollOptions = { animation: scrollOptions };
1201
2065
  }
@@ -1280,11 +2144,11 @@ var useStickToBottom = (options = {}) => {
1280
2144
  }
1281
2145
  return next();
1282
2146
  }, [setIsAtBottom, isSelecting, state]);
1283
- const stopScroll = useCallback3(() => {
2147
+ const stopScroll = useCallback4(() => {
1284
2148
  setEscapedFromLock(true);
1285
2149
  setIsAtBottom(false);
1286
2150
  }, [setEscapedFromLock, setIsAtBottom]);
1287
- const handleScroll = useCallback3(({ target }) => {
2151
+ const handleScroll = useCallback4(({ target }) => {
1288
2152
  if (target !== scrollRef.current) {
1289
2153
  return;
1290
2154
  }
@@ -1323,7 +2187,7 @@ var useStickToBottom = (options = {}) => {
1323
2187
  }
1324
2188
  }, 1);
1325
2189
  }, [setEscapedFromLock, setIsAtBottom, isSelecting, state]);
1326
- const handleWheel = useCallback3(({ target, deltaY }) => {
2190
+ const handleWheel = useCallback4(({ target, deltaY }) => {
1327
2191
  let element = target;
1328
2192
  while (!["scroll", "auto"].includes(getComputedStyle(element).overflow)) {
1329
2193
  if (!element.parentElement) {
@@ -1393,7 +2257,7 @@ var useStickToBottom = (options = {}) => {
1393
2257
  };
1394
2258
  };
1395
2259
  function useRefCallback(callback, deps) {
1396
- const result = useCallback3((ref) => {
2260
+ const result = useCallback4((ref) => {
1397
2261
  result.current = ref;
1398
2262
  return callback(ref);
1399
2263
  }, deps);
@@ -1425,11 +2289,11 @@ function mergeAnimations(...animations) {
1425
2289
 
1426
2290
  // ../../node_modules/.pnpm/use-stick-to-bottom@1.1.3_react@19.2.4/node_modules/use-stick-to-bottom/dist/StickToBottom.js
1427
2291
  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";
2292
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect7, useImperativeHandle, useLayoutEffect, useMemo as useMemo4, useRef as useRef8 } from "react";
1429
2293
  var StickToBottomContext = createContext2(null);
1430
- var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
2294
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect7;
1431
2295
  function StickToBottom({ instance, children, resize, initial, mass, damping, stiffness, targetScrollTop: currentTargetScrollTop, contextRef, ...props }) {
1432
- const customTargetScrollTop = useRef4(null);
2296
+ const customTargetScrollTop = useRef8(null);
1433
2297
  const targetScrollTop = React.useCallback((target, elements) => {
1434
2298
  const get = context?.targetScrollTop ?? currentTargetScrollTop;
1435
2299
  return get?.(target, elements) ?? target;
@@ -1469,169 +2333,53 @@ function StickToBottom({ instance, children, resize, initial, mass, damping, sti
1469
2333
  useImperativeHandle(contextRef, () => context, [context]);
1470
2334
  useIsomorphicLayoutEffect(() => {
1471
2335
  if (!scrollRef.current) {
1472
- return;
1473
- }
1474
- if (getComputedStyle(scrollRef.current).overflow === "visible") {
1475
- scrollRef.current.style.overflow = "auto";
1476
- }
1477
- }, []);
1478
- return React.createElement(
1479
- StickToBottomContext.Provider,
1480
- { value: context },
1481
- React.createElement("div", { ...props }, typeof children === "function" ? children(context) : children)
1482
- );
1483
- }
1484
- (function(StickToBottom2) {
1485
- function Content({ children, scrollClassName, ...props }) {
1486
- const context = useStickToBottomContext();
1487
- return React.createElement(
1488
- "div",
1489
- { ref: context.scrollRef, style: {
1490
- height: "100%",
1491
- width: "100%",
1492
- scrollbarGutter: "stable both-edges"
1493
- }, className: scrollClassName },
1494
- React.createElement("div", { ...props, ref: context.contentRef }, typeof children === "function" ? children(context) : children)
1495
- );
1496
- }
1497
- StickToBottom2.Content = Content;
1498
- })(StickToBottom || (StickToBottom = {}));
1499
- function useStickToBottomContext() {
1500
- const context = useContext2(StickToBottomContext);
1501
- if (!context) {
1502
- throw new Error("use-stick-to-bottom component context must be used within a StickToBottom component");
1503
- }
1504
- return context;
1505
- }
1506
-
1507
- // src/components/AssistantTurnBlock.tsx
1508
- import { getTextContent, normalizeMessageContent } from "@blade-hq/agent-client";
1509
- import { useState as useState9 } from "react";
1510
-
1511
- // src/components/AgentLoopBlock.tsx
1512
- import { useState as useState5 } from "react";
1513
-
1514
- // src/components/display-utils.ts
1515
- var TOOL_NAME_ALIASES = {
1516
- agent: "Agent",
1517
- ask_user_question: "AskUserQuestion",
1518
- bash: "Bash",
1519
- bg_bash: "BgBash",
1520
- edit: "Edit",
1521
- exit_plan_mode: "ExitPlanMode",
1522
- file_edit: "Edit",
1523
- file_read: "Read",
1524
- file_write: "Write",
1525
- finish_task: "FinishTask",
1526
- glob: "Glob",
1527
- grep: "Grep",
1528
- ls: "Ls",
1529
- read: "Read",
1530
- read_skill: "ReadSkill",
1531
- web_fetch: "WebFetch",
1532
- web_search: "WebSearch",
1533
- write: "Write"
1534
- };
1535
- var TOOL_DISPLAY_LABELS = {
1536
- Bash: "\u6267\u884C\u547D\u4EE4",
1537
- BgBash: "\u540E\u53F0\u6267\u884C\u547D\u4EE4",
1538
- Read: "\u8BFB\u53D6\u6587\u4EF6",
1539
- Write: "\u5199\u5165\u6587\u4EF6",
1540
- Edit: "\u7F16\u8F91\u6587\u4EF6",
1541
- Ls: "\u5217\u51FA\u76EE\u5F55",
1542
- Glob: "\u5339\u914D\u6587\u4EF6",
1543
- Grep: "\u641C\u7D22\u6587\u672C",
1544
- WebSearch: "\u641C\u7D22\u7F51\u9875",
1545
- WebFetch: "\u6574\u7406\u7F51\u9875\u5185\u5BB9",
1546
- Agent: "\u6D3E\u751F\u5B50\u667A\u80FD\u4F53",
1547
- AskUserQuestion: "\u5411\u7528\u6237\u63D0\u95EE",
1548
- ReadSkill: "\u8BFB\u53D6\u6280\u80FD",
1549
- FinishTask: "\u4EFB\u52A1\u5B8C\u6210",
1550
- ExitPlanMode: "\u63D0\u4EA4\u8BA1\u5212",
1551
- ListSessions: "\u5217\u51FA\u5386\u53F2\u4F1A\u8BDD",
1552
- GetSessionHistory: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2"
1553
- };
1554
- function safeParseJson(value) {
1555
- if (!value) return null;
1556
- try {
1557
- return JSON.parse(value);
1558
- } catch {
1559
- return null;
1560
- }
1561
- }
1562
- function getStringArgValue(args, key) {
1563
- const value = args?.[key];
1564
- return typeof value === "string" ? value.trim() : "";
1565
- }
1566
- function formatToolName(name) {
1567
- const trimmed = name.trim();
1568
- if (!trimmed) return name;
1569
- const stripped = trimmed.split(":").pop()?.split("/").pop()?.split(".").pop()?.trim() || trimmed;
1570
- const normalized = stripped.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
1571
- return TOOL_NAME_ALIASES[normalized] ?? stripped;
1572
- }
1573
- function getToolDisplayLabel(toolCall) {
1574
- const normalized = formatToolName(toolCall.name);
1575
- const args = safeParseJson(toolCall.arguments);
1576
- const displayName = toolCall.display_name?.trim() ?? "";
1577
- const baseLabel = displayName || TOOL_DISPLAY_LABELS[normalized] || normalized;
1578
- const metaDisplayName = getStringArgValue(args, "_meta_display_name");
1579
- if (metaDisplayName) {
1580
- return metaDisplayName;
1581
- }
1582
- const description = getStringArgValue(args, "description");
1583
- if (normalized === "BgBash") {
1584
- return description ? `\u540E\u53F0\u6267\u884C\uFF1A${description}` : "\u540E\u53F0\u6267\u884C\u547D\u4EE4";
1585
- }
1586
- if (normalized === "ReadSkill") {
1587
- const skillName = getStringArgValue(args, "skill") || getStringArgValue(args, "skill_name");
1588
- return skillName ? `${baseLabel}\u300C${skillName}\u300D` : baseLabel;
1589
- }
1590
- if (normalized === "FinishTask") {
1591
- const title = getStringArgValue(args, "title");
1592
- return title ? `${baseLabel}\uFF1A${title}` : baseLabel;
1593
- }
1594
- return description || baseLabel;
1595
- }
1596
- function getToolTone(status) {
1597
- if (status === "error" || status === "cancelled") return "red";
1598
- if (status === "awaiting_answer") return "amber";
1599
- if (status === "pending") return "blue";
1600
- return "emerald";
1601
- }
1602
- function getToolStatusLabel(status) {
1603
- if (status === "pending") return "\u8FD0\u884C\u4E2D";
1604
- if (status === "awaiting_answer") return "\u7B49\u5F85\u56DE\u7B54";
1605
- if (status === "error") return "\u9519\u8BEF";
1606
- if (status === "cancelled") return "\u5DF2\u53D6\u6D88";
1607
- return "\u5B8C\u6210";
1608
- }
1609
- function formatToolDuration(ms) {
1610
- if (ms < 1e3) return `${Math.round(ms)}ms`;
1611
- const seconds = ms / 1e3;
1612
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
1613
- const minutes = Math.floor(seconds / 60);
1614
- const remainingSeconds = Math.round(seconds % 60);
1615
- return remainingSeconds > 0 ? `${minutes}m${remainingSeconds}s` : `${minutes}m`;
2336
+ return;
2337
+ }
2338
+ if (getComputedStyle(scrollRef.current).overflow === "visible") {
2339
+ scrollRef.current.style.overflow = "auto";
2340
+ }
2341
+ }, []);
2342
+ return React.createElement(
2343
+ StickToBottomContext.Provider,
2344
+ { value: context },
2345
+ React.createElement("div", { ...props }, typeof children === "function" ? children(context) : children)
2346
+ );
1616
2347
  }
1617
- function formatToolArgs(args) {
1618
- try {
1619
- return JSON.stringify(JSON.parse(args), null, 2);
1620
- } catch {
1621
- return args;
2348
+ (function(StickToBottom2) {
2349
+ function Content({ children, scrollClassName, ...props }) {
2350
+ const context = useStickToBottomContext();
2351
+ return React.createElement(
2352
+ "div",
2353
+ { ref: context.scrollRef, style: {
2354
+ height: "100%",
2355
+ width: "100%",
2356
+ scrollbarGutter: "stable both-edges"
2357
+ }, className: scrollClassName },
2358
+ React.createElement("div", { ...props, ref: context.contentRef }, typeof children === "function" ? children(context) : children)
2359
+ );
1622
2360
  }
1623
- }
1624
- var RESULT_PREVIEW_LIMIT = 4e3;
1625
- function formatToolResult(result) {
1626
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1627
- if (text == null) return "";
1628
- if (text.length <= RESULT_PREVIEW_LIMIT) return text;
1629
- return `${text.slice(0, RESULT_PREVIEW_LIMIT)}
1630
- \u2026\uFF08\u7ED3\u679C\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD\uFF09`;
2361
+ StickToBottom2.Content = Content;
2362
+ })(StickToBottom || (StickToBottom = {}));
2363
+ function useStickToBottomContext() {
2364
+ const context = useContext2(StickToBottomContext);
2365
+ if (!context) {
2366
+ throw new Error("use-stick-to-bottom component context must be used within a StickToBottom component");
2367
+ }
2368
+ return context;
1631
2369
  }
1632
2370
 
2371
+ // src/components/AssistantTurnBlock.tsx
2372
+ import {
2373
+ getFileParts,
2374
+ getImageParts,
2375
+ getTextContent,
2376
+ normalizeMessageContent
2377
+ } from "@blade-hq/agent-client";
2378
+ import { useEffect as useEffect10, useRef as useRef11, useState as useState13 } from "react";
2379
+
1633
2380
  // src/components/AgentLoopBlock.tsx
1634
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2381
+ import { useState as useState9 } from "react";
2382
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1635
2383
  function parseAgentDescription(argumentsJson) {
1636
2384
  try {
1637
2385
  const parsed = JSON.parse(argumentsJson);
@@ -1641,83 +2389,81 @@ function parseAgentDescription(argumentsJson) {
1641
2389
  }
1642
2390
  }
1643
2391
  function AgentLoopBlock({ toolCall }) {
1644
- const [expanded, setExpanded] = useState5(false);
2392
+ const [expanded, setExpanded] = useState9(false);
1645
2393
  const description = parseAgentDescription(toolCall.arguments);
1646
2394
  const running = toolCall.status === "pending" || toolCall.status === "awaiting_answer";
1647
2395
  const failed = toolCall.status === "error" || toolCall.status === "cancelled";
1648
- return /* @__PURE__ */ jsxs5("div", { className: "blade-chat-agent-loop ml-4 text-xs", children: [
1649
- /* @__PURE__ */ jsxs5(
1650
- "div",
2396
+ const hasResult = toolCall.result != null;
2397
+ const iconClass = cn(
2398
+ "size-3.5 shrink-0",
2399
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
2400
+ );
2401
+ return /* @__PURE__ */ jsxs7("div", { className: "blade-chat-agent-loop text-xs leading-[22px]", children: [
2402
+ /* @__PURE__ */ jsxs7(
2403
+ "button",
1651
2404
  {
2405
+ type: "button",
2406
+ onClick: () => hasResult && setExpanded(!expanded),
2407
+ disabled: !hasResult,
2408
+ "aria-expanded": hasResult ? expanded : void 0,
2409
+ "data-testid": "execution-tool-intent",
1652
2410
  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))]"
2411
+ "flex min-w-0 items-center gap-1 py-1.5 text-left",
2412
+ hasResult && "cursor-pointer hover:text-[hsl(var(--foreground))]",
2413
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
1655
2414
  ),
2415
+ title: `\u5B50\u4EFB\u52A1\uFF1A${description}`,
1656
2416
  children: [
1657
- /* @__PURE__ */ jsxs5(
1658
- "button",
2417
+ running ? /* @__PURE__ */ jsx8(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx8(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx8(X, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx8(Bot, { className: iconClass, "aria-hidden": "true" }),
2418
+ /* @__PURE__ */ jsxs7("span", { className: "min-w-0 truncate", children: [
2419
+ "\u5B50\u4EFB\u52A1\uFF1A",
2420
+ description
2421
+ ] }),
2422
+ hasResult ? /* @__PURE__ */ jsx8(
2423
+ ChevronRight,
1659
2424
  {
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
- ]
2425
+ size: 14,
2426
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
2427
+ className: cn(
2428
+ "shrink-0 transition-transform",
2429
+ expanded && "rotate-90"
2430
+ ),
2431
+ "aria-hidden": "true"
1694
2432
  }
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) })
2433
+ ) : null
1697
2434
  ]
1698
2435
  }
1699
2436
  ),
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
- ] })
2437
+ expanded && hasResult ? /* @__PURE__ */ jsx8("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
2438
  ] });
1705
2439
  }
1706
2440
 
1707
2441
  // src/components/MarkdownContent.tsx
1708
2442
  import {
1709
- useEffect as useEffect4,
2443
+ useEffect as useEffect8,
1710
2444
  useMemo as useMemo5,
1711
- useRef as useRef5,
1712
- useState as useState6
2445
+ useRef as useRef9,
2446
+ useState as useState10
1713
2447
  } from "react";
1714
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
2448
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1715
2449
  var SYSTEM_REMINDER_RE = /<system-reminder>[\s\S]*?<\/system-reminder>/gi;
2450
+ function normalizeAdjacentUrlFormatting(value) {
2451
+ const protectedSegments = [];
2452
+ const protectedValue = value.replace(/(`{1,3}[\s\S]*?`{1,3}|\[[^\]]*\]\([^)]*\))/g, (segment) => {
2453
+ const index = protectedSegments.push(segment) - 1;
2454
+ return `blade-url-protected-${index}-marker`;
2455
+ });
2456
+ const normalized = protectedValue.replace(
2457
+ /(\*\*|__|~~|\*|_)(https?:\/\/[^\s<>]+?)\1(?=[\s。,、!?;:,.!?;:]|$)/g,
2458
+ (_, marker, url) => `${marker}[${url}](<${url}>)${marker}`
2459
+ );
2460
+ return normalized.replace(/blade-url-protected-(\d+)-marker/g, (_, index) => protectedSegments[Number(index)]);
2461
+ }
1716
2462
  function CodeBlockPre({ children, node: _node, ...props }) {
1717
- const preRef = useRef5(null);
1718
- const [copied, setCopied] = useState6(false);
1719
- const [language, setLanguage] = useState6("");
1720
- useEffect4(() => {
2463
+ const preRef = useRef9(null);
2464
+ const [copied, setCopied] = useState10(false);
2465
+ const [language, setLanguage] = useState10("");
2466
+ useEffect8(() => {
1721
2467
  const codeEl = preRef.current?.querySelector("code");
1722
2468
  setLanguage(codeEl?.className.match(/language-(\S+)/)?.[1] ?? "");
1723
2469
  }, []);
@@ -1728,10 +2474,10 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1728
2474
  setTimeout(() => setCopied(false), 2e3);
1729
2475
  }
1730
2476
  };
1731
- return /* @__PURE__ */ jsxs6("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
1732
- /* @__PURE__ */ jsxs6("div", { className: "blade-chat-codeblock-header flex h-[34px] items-center justify-between border-b border-[hsl(var(--border))] bg-[hsl(var(--muted))/0.5] pl-3.5 pr-1.5", children: [
1733
- /* @__PURE__ */ jsx7("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
1734
- /* @__PURE__ */ jsxs6(
2477
+ return /* @__PURE__ */ jsxs8("div", { className: "blade-chat-codeblock not-prose my-3 overflow-hidden rounded-xl border border-[hsl(var(--border))]", children: [
2478
+ /* @__PURE__ */ jsxs8("div", { className: "blade-chat-codeblock-header flex h-[34px] items-center justify-between border-b border-[hsl(var(--border))] bg-[hsl(var(--muted))/0.5] pl-3.5 pr-1.5", children: [
2479
+ /* @__PURE__ */ jsx9("span", { className: "font-mono text-[12px] text-[hsl(var(--muted-foreground))]", children: language || "code" }),
2480
+ /* @__PURE__ */ jsxs8(
1735
2481
  "button",
1736
2482
  {
1737
2483
  type: "button",
@@ -1741,13 +2487,13 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1741
2487
  copied ? "text-[hsl(var(--primary))]" : "text-[hsl(var(--muted-foreground))] hover:bg-[hsl(var(--accent))] hover:text-[hsl(var(--foreground))]"
1742
2488
  ),
1743
2489
  children: [
1744
- copied ? /* @__PURE__ */ jsx7(Check, { size: 12 }) : /* @__PURE__ */ jsx7(Copy, { size: 12 }),
1745
- /* @__PURE__ */ jsx7("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
2490
+ copied ? /* @__PURE__ */ jsx9(Check, { size: 12 }) : /* @__PURE__ */ jsx9(Copy, { size: 12 }),
2491
+ /* @__PURE__ */ jsx9("span", { children: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236" })
1746
2492
  ]
1747
2493
  }
1748
2494
  )
1749
2495
  ] }),
1750
- /* @__PURE__ */ jsx7(
2496
+ /* @__PURE__ */ jsx9(
1751
2497
  "pre",
1752
2498
  {
1753
2499
  ref: preRef,
@@ -1759,7 +2505,7 @@ function CodeBlockPre({ children, node: _node, ...props }) {
1759
2505
  ] });
1760
2506
  }
1761
2507
  function ExternalAnchor({ node: _node, children, ...props }) {
1762
- return /* @__PURE__ */ jsx7("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
2508
+ return /* @__PURE__ */ jsx9("a", { ...props, target: "_blank", rel: "noopener noreferrer", children });
1763
2509
  }
1764
2510
  var MARKDOWN_COMPONENTS = {
1765
2511
  pre: CodeBlockPre,
@@ -1767,9 +2513,9 @@ var MARKDOWN_COMPONENTS = {
1767
2513
  };
1768
2514
  function MarkdownContent({ children, className, mode, sessionId }) {
1769
2515
  const resolvedChildren = useMemo5(() => {
1770
- return children.replace(SYSTEM_REMINDER_RE, "");
2516
+ return normalizeAdjacentUrlFormatting(children.replace(SYSTEM_REMINDER_RE, ""));
1771
2517
  }, [children]);
1772
- return /* @__PURE__ */ jsx7(
2518
+ return /* @__PURE__ */ jsx9(
1773
2519
  _r,
1774
2520
  {
1775
2521
  className: cn("blade-chat-markdown break-words", className),
@@ -1782,17 +2528,46 @@ function MarkdownContent({ children, className, mode, sessionId }) {
1782
2528
  }
1783
2529
 
1784
2530
  // src/components/Shimmer.tsx
1785
- import { jsx as jsx8 } from "react/jsx-runtime";
2531
+ import { jsx as jsx10 } from "react/jsx-runtime";
1786
2532
  function Shimmer({ children = "\u6B63\u5728\u601D\u8003...", className }) {
1787
- return /* @__PURE__ */ jsx8("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
2533
+ return /* @__PURE__ */ jsx10("span", { className: cn("blade-shimmer-text text-sm font-medium", className), children });
1788
2534
  }
1789
2535
 
1790
2536
  // src/components/ToolCallBlock.tsx
1791
- import { useState as useState8 } from "react";
2537
+ import { useState as useState12 } from "react";
1792
2538
 
1793
2539
  // src/components/AskUserQuestionBlock.tsx
1794
- import { useEffect as useEffect5, useMemo as useMemo6, useState as useState7 } from "react";
1795
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
2540
+ import { useEffect as useEffect9, useMemo as useMemo6, useRef as useRef10, useState as useState11 } from "react";
2541
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2542
+ var CUSTOM_TEXTAREA_MAX_HEIGHT = 160;
2543
+ function resizeCustomTextarea(textarea) {
2544
+ textarea.style.height = "auto";
2545
+ textarea.style.height = `${Math.min(textarea.scrollHeight, CUSTOM_TEXTAREA_MAX_HEIGHT)}px`;
2546
+ textarea.style.overflowY = textarea.scrollHeight > CUSTOM_TEXTAREA_MAX_HEIGHT ? "auto" : "hidden";
2547
+ }
2548
+ function useAutoResizeTextarea(value) {
2549
+ const textareaRef = useRef10(null);
2550
+ useEffect9(() => {
2551
+ const textarea = textareaRef.current;
2552
+ if (textarea?.value === value) resizeCustomTextarea(textarea);
2553
+ }, [value]);
2554
+ useEffect9(() => {
2555
+ const textarea = textareaRef.current;
2556
+ if (!textarea || typeof ResizeObserver === "undefined") return;
2557
+ let previousWidth = textarea.clientWidth;
2558
+ const observer = new ResizeObserver(([entry]) => {
2559
+ if (!entry || entry.contentRect.width === previousWidth) return;
2560
+ previousWidth = entry.contentRect.width;
2561
+ resizeCustomTextarea(textarea);
2562
+ });
2563
+ observer.observe(textarea);
2564
+ return () => observer.disconnect();
2565
+ }, []);
2566
+ return textareaRef;
2567
+ }
2568
+ function indentAnswerContinuationLines(answer) {
2569
+ return answer.replaceAll("\n", "\n ");
2570
+ }
1796
2571
  function AskUserQuestionBlock({
1797
2572
  data,
1798
2573
  answered,
@@ -1801,18 +2576,19 @@ function AskUserQuestionBlock({
1801
2576
  answerData,
1802
2577
  onAnswer
1803
2578
  }) {
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(() => {
2579
+ const [selections, setSelections] = useState11(/* @__PURE__ */ new Map());
2580
+ const [customTexts, setCustomTexts] = useState11(/* @__PURE__ */ new Map());
2581
+ const [usingCustom, setUsingCustom] = useState11(/* @__PURE__ */ new Set());
2582
+ const [note, setNote] = useState11("");
2583
+ const [submitted, setSubmitted] = useState11(false);
2584
+ useEffect9(() => {
1809
2585
  if (sessionStatus === "failed" || sessionStatus === "interrupted") {
1810
2586
  setSubmitted(false);
1811
2587
  }
1812
2588
  }, [sessionStatus]);
1813
2589
  const displayAnswerState = useMemo6(() => {
1814
2590
  if (!(answered && answerData)) {
1815
- return { selections, customTexts, usingCustom };
2591
+ return { selections, customTexts, usingCustom, note };
1816
2592
  }
1817
2593
  const nextSelections = /* @__PURE__ */ new Map();
1818
2594
  const nextCustomTexts = /* @__PURE__ */ new Map();
@@ -1828,9 +2604,10 @@ function AskUserQuestionBlock({
1828
2604
  return {
1829
2605
  selections: nextSelections,
1830
2606
  customTexts: nextCustomTexts,
1831
- usingCustom: nextUsingCustom
2607
+ usingCustom: nextUsingCustom,
2608
+ note: answerData.note ?? ""
1832
2609
  };
1833
- }, [answerData, answered, customTexts, selections, usingCustom]);
2610
+ }, [answerData, answered, customTexts, note, selections, usingCustom]);
1834
2611
  const toggleOption = (qIdx, optIdx, multi) => {
1835
2612
  if (answered || submitted) return;
1836
2613
  setSelections((prev) => {
@@ -1878,6 +2655,7 @@ function AskUserQuestionBlock({
1878
2655
  const allAnswered = data.questions.every((_, i) => getAnswer(i) !== null);
1879
2656
  const handleSubmit = () => {
1880
2657
  if (answered || submitted || !allAnswered || !onAnswer) return;
2658
+ const trimmedNote = note.trim();
1881
2659
  const nextAnswerData = {
1882
2660
  selections: Object.fromEntries(
1883
2661
  Array.from(selections.entries()).map(([qIdx, optionIndexes]) => [
@@ -1887,15 +2665,21 @@ function AskUserQuestionBlock({
1887
2665
  ),
1888
2666
  custom: Object.fromEntries(
1889
2667
  Array.from(usingCustom).map((qIdx) => [qIdx, (customTexts.get(qIdx) ?? "").trim()]).filter(([, text2]) => text2.length > 0)
1890
- )
2668
+ ),
2669
+ ...trimmedNote ? { note: trimmedNote } : {}
1891
2670
  };
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")}`;
2671
+ const parts = data.questions.map(
2672
+ (q, i) => `- ${q.question} -> ${indentAnswerContinuationLines(getAnswer(i) ?? "")}`
2673
+ );
2674
+ const text = [
2675
+ `\u5173\u4E8E\u9700\u8981\u786E\u8BA4\u7684\u95EE\u9898\uFF0C\u7528\u6237\u7684\u56DE\u7B54\u5982\u4E0B\uFF1A
2676
+ ${parts.join("\n")}`,
2677
+ trimmedNote ? `\u8865\u5145\u8BF4\u660E\uFF1A${indentAnswerContinuationLines(trimmedNote)}` : ""
2678
+ ].filter(Boolean).join("\n");
1895
2679
  setSubmitted(true);
1896
2680
  onAnswer(text, toolCallId, nextAnswerData);
1897
2681
  };
1898
- return /* @__PURE__ */ jsxs7(
2682
+ return /* @__PURE__ */ jsxs9(
1899
2683
  "div",
1900
2684
  {
1901
2685
  className: cn(
@@ -1903,12 +2687,12 @@ ${parts.join("\n")}`;
1903
2687
  answered ? "max-w-2xl space-y-3 p-3 text-xs text-[hsl(var(--muted-foreground))] opacity-80" : "max-w-lg space-y-5 p-4 text-sm"
1904
2688
  ),
1905
2689
  children: [
1906
- data.source_loop?.description && /* @__PURE__ */ jsxs7("div", { className: "rounded-lg bg-[hsl(var(--muted)/0.35)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
2690
+ data.source_loop?.description && /* @__PURE__ */ jsxs9("div", { className: "rounded-lg bg-[hsl(var(--muted)/0.35)] px-3 py-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
1907
2691
  "\u5B50\u667A\u80FD\u4F53\u300C",
1908
2692
  data.source_loop.description,
1909
2693
  "\u300D\u5728\u7B49\u5F85\u4F60\u7684\u56DE\u7B54"
1910
2694
  ] }),
1911
- data.questions.map((q, qIdx) => /* @__PURE__ */ jsx9(
2695
+ data.questions.map((q, qIdx) => /* @__PURE__ */ jsx11(
1912
2696
  QuestionCard,
1913
2697
  {
1914
2698
  question: q,
@@ -1923,7 +2707,16 @@ ${parts.join("\n")}`;
1923
2707
  },
1924
2708
  q.question
1925
2709
  )),
1926
- !answered && !submitted && onAnswer && /* @__PURE__ */ jsx9(
2710
+ /* @__PURE__ */ jsx11(
2711
+ NoteField,
2712
+ {
2713
+ answered,
2714
+ submitted,
2715
+ note: displayAnswerState.note,
2716
+ onChange: setNote
2717
+ }
2718
+ ),
2719
+ !answered && !submitted && onAnswer && /* @__PURE__ */ jsx11(
1927
2720
  "button",
1928
2721
  {
1929
2722
  type: "button",
@@ -1933,14 +2726,14 @@ ${parts.join("\n")}`;
1933
2726
  children: allAnswered ? "\u786E\u8BA4" : "\u8BF7\u5148\u9009\u62E9\u4E00\u4E2A\u9009\u9879"
1934
2727
  }
1935
2728
  ),
1936
- submitted && !answered && /* @__PURE__ */ jsxs7(
2729
+ submitted && !answered && /* @__PURE__ */ jsxs9(
1937
2730
  "button",
1938
2731
  {
1939
2732
  type: "button",
1940
2733
  disabled: true,
1941
2734
  className: "flex w-full items-center justify-center gap-2 rounded-lg bg-[hsl(var(--primary))] px-4 py-2 text-xs font-semibold text-[hsl(var(--primary-foreground))] opacity-80",
1942
2735
  children: [
1943
- /* @__PURE__ */ jsx9(LoaderCircle, { size: 14, className: "animate-spin" }),
2736
+ /* @__PURE__ */ jsx11(LoaderCircle, { size: 14, className: "animate-spin" }),
1944
2737
  "\u786E\u8BA4\u4E2D"
1945
2738
  ]
1946
2739
  }
@@ -1961,30 +2754,31 @@ function QuestionCard({
1961
2754
  onCustomChange
1962
2755
  }) {
1963
2756
  const multi = question.multiSelect ?? false;
1964
- return /* @__PURE__ */ jsxs7("div", { children: [
1965
- /* @__PURE__ */ jsxs7("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
1966
- /* @__PURE__ */ jsx9(
2757
+ const customTextareaRef = useAutoResizeTextarea(customText);
2758
+ return /* @__PURE__ */ jsxs9("div", { children: [
2759
+ /* @__PURE__ */ jsxs9("div", { className: cn("flex items-start gap-2", answered ? "mb-2" : "mb-3"), children: [
2760
+ /* @__PURE__ */ jsx11(
1967
2761
  MessageSquareMore,
1968
2762
  {
1969
2763
  size: answered ? 12 : 13,
1970
2764
  className: "mt-0.5 shrink-0 text-[hsl(var(--primary))]"
1971
2765
  }
1972
2766
  ),
1973
- /* @__PURE__ */ jsx9(
2767
+ /* @__PURE__ */ jsx11(
1974
2768
  "div",
1975
2769
  {
1976
2770
  className: cn(
1977
2771
  "min-w-0 flex-1 font-medium text-[hsl(var(--foreground))]",
1978
2772
  answered ? "text-xs" : "text-sm"
1979
2773
  ),
1980
- children: /* @__PURE__ */ jsx9(MarkdownContent, { className: "blade-chat-prose", children: question.question })
2774
+ children: /* @__PURE__ */ jsx11(MarkdownContent, { className: "blade-chat-prose", children: question.question })
1981
2775
  }
1982
2776
  )
1983
2777
  ] }),
1984
- /* @__PURE__ */ jsxs7("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
2778
+ /* @__PURE__ */ jsxs9("div", { className: cn("flex flex-col pl-5", answered ? "gap-1" : "gap-1.5"), children: [
1985
2779
  question.options.map((opt, optIdx) => {
1986
2780
  const isSel = selected.has(optIdx);
1987
- return /* @__PURE__ */ jsxs7(
2781
+ return /* @__PURE__ */ jsxs9(
1988
2782
  "button",
1989
2783
  {
1990
2784
  type: "button",
@@ -1998,14 +2792,14 @@ function QuestionCard({
1998
2792
  answered && "cursor-default opacity-70"
1999
2793
  ),
2000
2794
  children: [
2001
- multi && /* @__PURE__ */ jsx9(
2795
+ multi && /* @__PURE__ */ jsx11(
2002
2796
  "div",
2003
2797
  {
2004
2798
  className: cn(
2005
2799
  "mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
2006
2800
  isSel && !answered ? "border-[hsl(var(--primary-foreground)/0.6)] bg-[hsl(var(--primary-foreground)/0.2)]" : isSel ? "border-[hsl(var(--primary)/0.45)] bg-[hsl(var(--primary)/0.12)]" : "border-[hsl(var(--border))]"
2007
2801
  ),
2008
- children: isSel && /* @__PURE__ */ jsx9(
2802
+ children: isSel && /* @__PURE__ */ jsx11(
2009
2803
  Check,
2010
2804
  {
2011
2805
  size: 9,
@@ -2014,9 +2808,9 @@ function QuestionCard({
2014
2808
  )
2015
2809
  }
2016
2810
  ),
2017
- /* @__PURE__ */ jsxs7("div", { className: "min-w-0", children: [
2018
- /* @__PURE__ */ jsx9("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2019
- opt.description && /* @__PURE__ */ jsx9(
2811
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0", children: [
2812
+ /* @__PURE__ */ jsx11("div", { className: cn("font-medium", answered ? "text-xs" : "text-[13px]"), children: opt.label }),
2813
+ opt.description && /* @__PURE__ */ jsx11(
2020
2814
  "div",
2021
2815
  {
2022
2816
  className: cn(
@@ -2033,29 +2827,30 @@ function QuestionCard({
2033
2827
  opt.label
2034
2828
  );
2035
2829
  }),
2036
- answered && !isCustom ? null : /* @__PURE__ */ jsxs7(
2830
+ answered && !isCustom ? null : /* @__PURE__ */ jsxs9(
2037
2831
  "div",
2038
2832
  {
2039
2833
  className: cn(
2040
- "flex items-center gap-2 rounded-lg border transition-all",
2834
+ "flex items-start gap-2 rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2041
2835
  answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2042
2836
  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
2837
  answered && "cursor-default opacity-70"
2044
2838
  ),
2045
2839
  children: [
2046
- /* @__PURE__ */ jsx9("span", { className: "shrink-0 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2047
- /* @__PURE__ */ jsx9(
2048
- "input",
2840
+ /* @__PURE__ */ jsx11("span", { className: "shrink-0 pt-1 text-xs text-[hsl(var(--muted-foreground))]", children: "\u5176\u4ED6\uFF1A" }),
2841
+ /* @__PURE__ */ jsx11(
2842
+ "textarea",
2049
2843
  {
2050
- type: "text",
2844
+ ref: customTextareaRef,
2845
+ rows: 2,
2051
2846
  value: customText,
2052
- disabled: answered,
2847
+ readOnly: answered,
2053
2848
  onChange: (e) => onCustomChange(qIdx, e.target.value),
2054
2849
  onFocus: () => onCustomFocus(qIdx),
2055
2850
  "aria-label": "\u81EA\u5B9A\u4E49\u56DE\u7B54",
2056
2851
  placeholder: "\u8F93\u5165\u4F60\u7684\u7B54\u6848...",
2057
2852
  className: cn(
2058
- "min-w-0 flex-1 bg-transparent text-[hsl(var(--foreground))] outline-none placeholder:text-[hsl(var(--muted-foreground)/0.5)]",
2853
+ "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
2854
  answered ? "text-xs" : "text-sm"
2060
2855
  )
2061
2856
  }
@@ -2066,6 +2861,49 @@ function QuestionCard({
2066
2861
  ] })
2067
2862
  ] });
2068
2863
  }
2864
+ function NoteField({
2865
+ answered,
2866
+ submitted,
2867
+ note,
2868
+ onChange
2869
+ }) {
2870
+ const textareaRef = useAutoResizeTextarea(note);
2871
+ const readOnly = answered || submitted;
2872
+ if (answered && !note.trim()) return null;
2873
+ return /* @__PURE__ */ jsxs9(
2874
+ "label",
2875
+ {
2876
+ className: cn(
2877
+ "block rounded-lg border transition-all focus-within:ring-2 focus-within:ring-[hsl(var(--ring)/0.35)]",
2878
+ answered ? "px-2.5 py-1.5" : "px-3 py-2.5",
2879
+ 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))]",
2880
+ readOnly && "cursor-default opacity-70"
2881
+ ),
2882
+ children: [
2883
+ /* @__PURE__ */ jsx11("span", { className: "mb-1.5 block text-xs text-[hsl(var(--muted-foreground))]", children: "\u8865\u5145\u8BF4\u660E\uFF08\u53EF\u9009\uFF09" }),
2884
+ /* @__PURE__ */ jsx11(
2885
+ "textarea",
2886
+ {
2887
+ ref: textareaRef,
2888
+ rows: 2,
2889
+ value: note,
2890
+ readOnly,
2891
+ onChange: (event) => {
2892
+ if (readOnly) return;
2893
+ onChange(event.target.value);
2894
+ },
2895
+ "aria-label": "\u8865\u5145\u8BF4\u660E",
2896
+ placeholder: "\u9009\u5B8C\u8FD8\u53EF\u4EE5\u518D\u8BB2\u4E24\u53E5\uFF0C\u7A7A\u7740\u5C31\u5F53\u6CA1\u6709",
2897
+ className: cn(
2898
+ "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)]",
2899
+ answered ? "text-xs" : "text-sm"
2900
+ )
2901
+ }
2902
+ )
2903
+ ]
2904
+ }
2905
+ );
2906
+ }
2069
2907
  function parseAskUserQuestion(toolResult) {
2070
2908
  if (!toolResult) return null;
2071
2909
  try {
@@ -2088,6 +2926,26 @@ function parseAskUserQuestion(toolResult) {
2088
2926
  }
2089
2927
  return null;
2090
2928
  }
2929
+ function parseAskUserQuestionError(toolResult) {
2930
+ if (!toolResult) return null;
2931
+ try {
2932
+ const parsed = JSON.parse(toolResult);
2933
+ let detail = null;
2934
+ if (typeof parsed.error === "string") detail = parsed.error;
2935
+ if (parsed.error && typeof parsed.error === "object") {
2936
+ const message = parsed.error.message;
2937
+ if (typeof message === "string") detail = message;
2938
+ }
2939
+ if (!detail && typeof parsed.message === "string") detail = parsed.message;
2940
+ if (!detail) return null;
2941
+ return {
2942
+ message: parsed.error_code === "invalid_questions" ? "\u63D0\u95EE\u5185\u5BB9\u4E0D\u5B8C\u6574\uFF0C\u5F53\u524D\u6CA1\u6709\u7B49\u5F85\u4F60\u56DE\u7B54\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" : "\u8FD9\u6B21\u63D0\u95EE\u6CA1\u6709\u6210\u529F\uFF0C\u5F53\u524D\u6CA1\u6709\u7B49\u5F85\u4F60\u56DE\u7B54\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002",
2943
+ detail
2944
+ };
2945
+ } catch {
2946
+ return null;
2947
+ }
2948
+ }
2091
2949
  function normalizeQuestionItem(value) {
2092
2950
  if (!value || typeof value !== "object") return null;
2093
2951
  const item = value;
@@ -2112,13 +2970,14 @@ function normalizeOptionItem(value) {
2112
2970
  }
2113
2971
 
2114
2972
  // src/components/ToolCallBlock.tsx
2115
- import { Fragment, jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
2973
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2116
2974
  function resolveAskQuestionState({
2117
2975
  toolStatus,
2118
2976
  hasAnswerData,
2119
- fallbackAnswered
2977
+ fallbackAnswered,
2978
+ fallbackAwaiting
2120
2979
  }) {
2121
- const awaitingAnswer = !hasAnswerData && toolStatus === "awaiting_answer";
2980
+ const awaitingAnswer = !hasAnswerData && (toolStatus === "awaiting_answer" || toolStatus === "pending" && fallbackAwaiting === true);
2122
2981
  return {
2123
2982
  awaitingAnswer,
2124
2983
  answered: hasAnswerData || !awaitingAnswer && (Boolean(fallbackAnswered) || toolStatus === "done" || toolStatus === "cancelled" || toolStatus === "error")
@@ -2130,14 +2989,15 @@ function ToolCallBlock({
2130
2989
  answered,
2131
2990
  answerData,
2132
2991
  sessionStatus,
2992
+ isActiveQuestion,
2133
2993
  renderer
2134
2994
  }) {
2135
- const [expanded, setExpanded] = useState8(false);
2995
+ const [expanded, setExpanded] = useState12(false);
2136
2996
  const normalizedName = formatToolName(toolCall.name);
2137
2997
  if (renderer) {
2138
2998
  const custom = renderer(toolCall);
2139
2999
  if (custom !== null && custom !== void 0) {
2140
- return /* @__PURE__ */ jsx10(Fragment, { children: custom });
3000
+ return /* @__PURE__ */ jsx12(Fragment2, { children: custom });
2141
3001
  }
2142
3002
  }
2143
3003
  if (normalizedName === "AskUserQuestion") {
@@ -2145,11 +3005,12 @@ function ToolCallBlock({
2145
3005
  const questionState = resolveAskQuestionState({
2146
3006
  toolStatus: toolCall.status,
2147
3007
  hasAnswerData: Boolean(answerData),
2148
- fallbackAnswered: answered
3008
+ fallbackAnswered: answered,
3009
+ fallbackAwaiting: isActiveQuestion === true && (sessionStatus === "paused" || sessionStatus === "waiting_for_input")
2149
3010
  });
2150
3011
  const canAnswer = questionState.awaitingAnswer && Boolean(onAnswer);
2151
3012
  if (askData) {
2152
- return /* @__PURE__ */ jsx10(
3013
+ return /* @__PURE__ */ jsx12(
2153
3014
  AskUserQuestionBlock,
2154
3015
  {
2155
3016
  data: askData,
@@ -2162,24 +3023,31 @@ function ToolCallBlock({
2162
3023
  );
2163
3024
  }
2164
3025
  if (toolCall.status === "pending") {
2165
- return /* @__PURE__ */ jsxs8("div", { className: "ml-4 flex max-w-lg items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-4 text-sm text-[hsl(var(--muted-foreground))]", children: [
2166
- /* @__PURE__ */ jsx10(LoaderCircle, { size: 14, className: "animate-spin" }),
2167
- /* @__PURE__ */ jsx10("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
3026
+ return /* @__PURE__ */ jsxs10("div", { className: "ml-4 flex max-w-lg items-center gap-2 rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] p-4 text-sm text-[hsl(var(--muted-foreground))]", children: [
3027
+ /* @__PURE__ */ jsx12(LoaderCircle, { size: 14, className: "animate-spin" }),
3028
+ /* @__PURE__ */ jsx12("span", { children: "\u6B63\u5728\u51C6\u5907\u95EE\u9898\u2026" })
2168
3029
  ] });
2169
3030
  }
2170
- return /* @__PURE__ */ jsxs8("div", { className: "ml-4 max-w-lg rounded-xl border border-amber-500/35 bg-amber-500/10 p-4 text-sm text-[hsl(var(--foreground))]", children: [
2171
- /* @__PURE__ */ jsx10("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
2172
- /* @__PURE__ */ jsx10("div", { className: "mt-1 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: "\u6536\u5230\u7684\u4EA4\u4E92\u6570\u636E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" })
3031
+ const errorDetail = parseAskUserQuestionError(
3032
+ typeof toolCall.result === "string" ? toolCall.result : null
3033
+ );
3034
+ return /* @__PURE__ */ jsxs10("div", { className: "ml-4 max-w-lg rounded-xl border border-amber-500/35 bg-amber-500/10 p-4 text-sm text-[hsl(var(--foreground))]", children: [
3035
+ /* @__PURE__ */ jsx12("div", { className: "font-semibold", children: "\u9009\u62E9\u9898\u5185\u5BB9\u6682\u65F6\u65E0\u6CD5\u663E\u793A" }),
3036
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: errorDetail?.message ?? "\u6536\u5230\u7684\u4EA4\u4E92\u6570\u636E\u4E0D\u5B8C\u6574\u3002\u8BF7\u8BA9\u667A\u80FD\u4F53\u91CD\u65B0\u63D0\u95EE\u3002" }),
3037
+ errorDetail?.detail ? /* @__PURE__ */ jsxs10("details", { className: "mt-2 text-xs text-[hsl(var(--muted-foreground))]", children: [
3038
+ /* @__PURE__ */ jsx12("summary", { className: "cursor-pointer", children: "\u67E5\u770B\u5177\u4F53\u539F\u56E0" }),
3039
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 break-words font-mono", children: errorDetail.detail })
3040
+ ] }) : null
2173
3041
  ] });
2174
3042
  }
2175
3043
  const tone = getToolTone(toolCall.status);
2176
3044
  const displayName = getToolDisplayLabel(toolCall);
2177
3045
  const toneClass = tone === "red" ? "border-l-[hsl(var(--muted-foreground)/0.5)]" : tone === "amber" ? "border-l-amber-400" : tone === "blue" ? "border-l-blue-500" : "border-l-[hsl(var(--primary))]";
2178
- const statusIcon = toolCall.status === "pending" ? /* @__PURE__ */ jsx10(LoaderCircle, { size: 11, className: "animate-spin" }) : toolCall.status === "awaiting_answer" ? /* @__PURE__ */ jsx10(MessageSquareMore, { size: 11 }) : toolCall.status === "cancelled" || toolCall.status === "error" ? /* @__PURE__ */ jsx10(X, { size: 11 }) : /* @__PURE__ */ jsx10(Check, { size: 11 });
3046
+ const statusIcon = toolCall.status === "pending" ? /* @__PURE__ */ jsx12(LoaderCircle, { size: 11, className: "animate-spin" }) : toolCall.status === "awaiting_answer" ? /* @__PURE__ */ jsx12(MessageSquareMore, { size: 11 }) : toolCall.status === "cancelled" || toolCall.status === "error" ? /* @__PURE__ */ jsx12(X, { size: 11 }) : /* @__PURE__ */ jsx12(Check, { size: 11 });
2179
3047
  const statusTextClass = tone === "red" ? "text-[hsl(var(--muted-foreground))]" : tone === "amber" ? "text-amber-300" : tone === "blue" ? "text-blue-300" : "text-[hsl(var(--primary))]";
2180
- return /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool ml-4 text-xs", children: [
2181
- /* @__PURE__ */ jsxs8("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
2182
- /* @__PURE__ */ jsxs8(
3048
+ return /* @__PURE__ */ jsxs10("div", { className: "blade-chat-tool ml-4 text-xs", children: [
3049
+ /* @__PURE__ */ jsxs10("div", { className: cn("border-l-[3px] flex items-center gap-2 px-3 py-2", toneClass), children: [
3050
+ /* @__PURE__ */ jsxs10(
2183
3051
  "button",
2184
3052
  {
2185
3053
  type: "button",
@@ -2187,7 +3055,7 @@ function ToolCallBlock({
2187
3055
  className: "flex min-w-0 flex-1 items-center gap-2 text-left transition-colors hover:bg-white/3 focus-visible:ring-1 focus-visible:ring-[hsl(var(--ring))] focus:outline-none",
2188
3056
  "aria-expanded": expanded,
2189
3057
  children: [
2190
- /* @__PURE__ */ jsx10(
3058
+ /* @__PURE__ */ jsx12(
2191
3059
  ChevronRight,
2192
3060
  {
2193
3061
  size: 11,
@@ -2197,24 +3065,24 @@ function ToolCallBlock({
2197
3065
  )
2198
3066
  }
2199
3067
  ),
2200
- /* @__PURE__ */ jsxs8("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
3068
+ /* @__PURE__ */ jsxs10("span", { className: cn("flex shrink-0 items-center gap-1 text-[10px]", statusTextClass), children: [
2201
3069
  statusIcon,
2202
- /* @__PURE__ */ jsx10("span", { children: getToolStatusLabel(toolCall.status) })
3070
+ /* @__PURE__ */ jsx12("span", { children: getToolStatusLabel(toolCall.status) })
2203
3071
  ] }),
2204
- /* @__PURE__ */ jsx10("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
3072
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 flex-1 truncate font-medium text-[hsl(var(--foreground))]", children: displayName })
2205
3073
  ]
2206
3074
  }
2207
3075
  ),
2208
- typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx10("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
3076
+ typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 && /* @__PURE__ */ jsx12("span", { className: "shrink-0 font-mono text-[10px] text-[hsl(var(--muted-foreground))]", children: formatToolDuration(toolCall.duration_ms) })
2209
3077
  ] }),
2210
- expanded && /* @__PURE__ */ jsxs8("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
2211
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
2212
- /* @__PURE__ */ jsx10("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
2213
- /* @__PURE__ */ jsx10("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
2214
- /* @__PURE__ */ jsx10("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolArgs(toolCall.arguments) }),
2215
- toolCall.result != null && /* @__PURE__ */ jsxs8(Fragment, { children: [
2216
- /* @__PURE__ */ jsx10("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
2217
- /* @__PURE__ */ jsx10("pre", { className: "max-h-[400px] overflow-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolResult(toolCall.result) })
3078
+ expanded && /* @__PURE__ */ jsxs10("div", { className: "blade-chat-tool-detail ml-4 mt-1 rounded-xl bg-[hsl(var(--card))] px-3 py-3", children: [
3079
+ /* @__PURE__ */ jsx12("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u5DE5\u5177" }),
3080
+ /* @__PURE__ */ jsx12("div", { className: "mb-3 font-mono text-[11px] text-[hsl(var(--foreground))]", children: normalizedName }),
3081
+ /* @__PURE__ */ jsx12("div", { className: "mb-1 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u53C2\u6570" }),
3082
+ /* @__PURE__ */ jsx12("pre", { className: "overflow-x-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolArgs(toolCall.arguments) }),
3083
+ toolCall.result != null && /* @__PURE__ */ jsxs10(Fragment2, { children: [
3084
+ /* @__PURE__ */ jsx12("div", { className: "mb-1 mt-3 text-[10px] uppercase tracking-wider text-[hsl(var(--muted-foreground))]", children: "\u7ED3\u679C" }),
3085
+ /* @__PURE__ */ jsx12("pre", { className: "max-h-[400px] overflow-auto whitespace-pre-wrap rounded-md bg-[hsl(var(--muted))] p-2 font-mono text-[11px] text-[hsl(var(--foreground))]", children: formatToolResult(toolCall.result) })
2218
3086
  ] })
2219
3087
  ] })
2220
3088
  ] });
@@ -2232,47 +3100,235 @@ function buildAskUserPayload(argumentsJson) {
2232
3100
  }
2233
3101
 
2234
3102
  // src/components/AssistantTurnBlock.tsx
2235
- import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
3103
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
2236
3104
  function ThinkingBlock({ reasoning, isStreaming }) {
2237
- const [open, setOpen] = useState9(false);
2238
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-thinking ml-4 text-sm", children: [
2239
- /* @__PURE__ */ jsxs9(
3105
+ const [open, setOpen] = useState13(false);
3106
+ if (!isStreaming) return null;
3107
+ return /* @__PURE__ */ jsxs11("div", { className: "blade-chat-thinking text-xs", children: [
3108
+ /* @__PURE__ */ jsxs11(
2240
3109
  "button",
2241
3110
  {
2242
3111
  type: "button",
2243
3112
  onClick: () => setOpen(!open),
2244
3113
  "aria-expanded": open,
2245
- className: "inline-flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3114
+ className: "group/thinking inline-flex items-center gap-1 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
2246
3115
  children: [
2247
- /* @__PURE__ */ jsx11(Brain, { size: 12, className: "shrink-0" }),
2248
- isStreaming ? /* @__PURE__ */ jsx11(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }) : /* @__PURE__ */ jsx11("span", { children: "\u601D\u8003\u8FC7\u7A0B" }),
2249
- /* @__PURE__ */ jsxs9("span", { className: "text-[hsl(var(--muted-foreground))]/70", children: [
2250
- "\xB7 ",
2251
- new Intl.NumberFormat("zh-CN").format(reasoning.length),
2252
- " \u5B57"
2253
- ] }),
2254
- /* @__PURE__ */ jsx11(
2255
- ChevronDown,
3116
+ /* @__PURE__ */ jsx13(Shimmer, { className: "text-xs", children: "\u6B63\u5728\u601D\u8003" }),
3117
+ /* @__PURE__ */ jsx13(
3118
+ ChevronRight,
2256
3119
  {
2257
- size: 12,
2258
- className: cn("shrink-0 transition-transform", open && "rotate-180")
3120
+ size: 14,
3121
+ className: cn(
3122
+ "shrink-0 opacity-0 transition-[opacity,transform] group-hover/thinking:opacity-100",
3123
+ open && "rotate-90 opacity-100"
3124
+ )
2259
3125
  }
2260
3126
  )
2261
3127
  ]
2262
3128
  }
2263
3129
  ),
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 })
3130
+ open ? /* @__PURE__ */ jsx13("div", { className: "mt-1.5 whitespace-pre-wrap text-xs leading-[22px] text-[hsl(var(--muted-foreground))]", children: reasoning }) : null
2265
3131
  ] });
2266
3132
  }
2267
3133
  function getMessageText(message) {
2268
3134
  return getTextContent(normalizeMessageContent(message.content)).trim();
2269
3135
  }
3136
+ function hasRenderableMessageContent(message) {
3137
+ return Boolean(getMessageText(message)) || getImageParts(message.content).length > 0 || getFileParts(message.content).length > 0;
3138
+ }
3139
+ function getLastContentMessage(messages) {
3140
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
3141
+ if (hasRenderableMessageContent(messages[index])) return messages[index];
3142
+ }
3143
+ return null;
3144
+ }
3145
+ function getOrderedMessageParts(message, toolCalls) {
3146
+ const blocks = message.blocks ?? [];
3147
+ if (!blocks.some((block) => block.type === "text") || !blocks.some((block) => block.type === "tool_use")) return [];
3148
+ const toolsById = new Map(toolCalls.map((toolCall) => [toolCall.id, toolCall]));
3149
+ const seenToolIds = /* @__PURE__ */ new Set();
3150
+ const parts = [];
3151
+ for (const [index, block] of blocks.entries()) {
3152
+ if (block.type === "text" && block.content != null && block.content !== "") {
3153
+ const content = Array.isArray(block.content) ? block.content : String(block.content);
3154
+ parts.push({ type: "text", key: `text-${index}`, content });
3155
+ }
3156
+ if (block.type !== "tool_use" || !block.tool_call_id) continue;
3157
+ const toolCall = toolsById.get(block.tool_call_id);
3158
+ if (!toolCall) continue;
3159
+ seenToolIds.add(toolCall.id);
3160
+ const previous = parts[parts.length - 1];
3161
+ if (previous?.type === "tools") previous.toolCalls.push(toolCall);
3162
+ else parts.push({ type: "tools", key: `tools-${index}`, toolCalls: [toolCall] });
3163
+ }
3164
+ const missingTools = toolCalls.filter((toolCall) => !seenToolIds.has(toolCall.id));
3165
+ if (seenToolIds.size === 0) return [];
3166
+ if (missingTools.length > 0) {
3167
+ parts.push({ type: "tools", key: "tools-missing", toolCalls: missingTools });
3168
+ }
3169
+ return parts;
3170
+ }
2270
3171
  function findLatestReasoningMessageIndex(messages) {
2271
3172
  for (let index = messages.length - 1; index >= 0; index -= 1) {
2272
3173
  if (messages[index].reasoning) return index;
2273
3174
  }
2274
3175
  return -1;
2275
3176
  }
3177
+ function resolveTurnDisplayMode({
3178
+ isStreaming: _isStreaming,
3179
+ displayMode
3180
+ }) {
3181
+ return displayMode;
3182
+ }
3183
+ function formatExecutionDuration(durationMs) {
3184
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1e3));
3185
+ const minutes = Math.floor(totalSeconds / 60);
3186
+ const seconds = totalSeconds % 60;
3187
+ return minutes > 0 ? `${minutes}\u5206${seconds}\u79D2` : `${seconds}\u79D2`;
3188
+ }
3189
+ function getExecutionDurationMs({
3190
+ messages,
3191
+ isStreaming,
3192
+ now = Date.now()
3193
+ }) {
3194
+ const knownDuration = messages.reduce(
3195
+ (total, message) => {
3196
+ if (typeof message.duration_ms === "number" && message.duration_ms > 0) {
3197
+ return total + message.duration_ms;
3198
+ }
3199
+ return total + (message.tool_calls ?? []).reduce(
3200
+ (toolTotal, toolCall) => toolTotal + (typeof toolCall.duration_ms === "number" && toolCall.duration_ms > 0 ? toolCall.duration_ms : 0),
3201
+ 0
3202
+ );
3203
+ },
3204
+ 0
3205
+ );
3206
+ if (!isStreaming) return knownDuration;
3207
+ const startedAt = messages.map((message) => message.timestamp ? Date.parse(message.timestamp) : Number.NaN).filter((value) => Number.isFinite(value)).sort((a, b) => a - b)[0];
3208
+ if (startedAt === void 0) return knownDuration;
3209
+ return Math.max(knownDuration, now - startedAt);
3210
+ }
3211
+ function findLastExceptionalEvent(messages) {
3212
+ for (let messageIndex = messages.length - 1; messageIndex >= 0; messageIndex -= 1) {
3213
+ const messageStatus = messages[messageIndex].status;
3214
+ if (messageStatus === "failed") return { messageIndex, status: "error" };
3215
+ if (messageStatus === "interrupted") return { messageIndex, status: "cancelled" };
3216
+ const toolCalls = messages[messageIndex].tool_calls ?? [];
3217
+ for (let toolIndex = toolCalls.length - 1; toolIndex >= 0; toolIndex -= 1) {
3218
+ const status = toolCalls[toolIndex].status;
3219
+ if (status === "error" || status === "cancelled") {
3220
+ return { messageIndex, status };
3221
+ }
3222
+ }
3223
+ }
3224
+ return null;
3225
+ }
3226
+ function executionSummaryLabel({
3227
+ messages,
3228
+ isStreaming,
3229
+ durationMs,
3230
+ sessionStatus,
3231
+ askAnswers
3232
+ }) {
3233
+ if (isStreaming) {
3234
+ return durationMs > 0 ? `\u6B63\u5728\u6267\u884C ${formatExecutionDuration(durationMs)}` : "\u6B63\u5728\u6267\u884C";
3235
+ }
3236
+ if (sessionStatus === "waiting_for_input" && messages.some(
3237
+ (message) => (message.tool_calls ?? []).some(
3238
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion" && toolCall.status === "awaiting_answer" && !askAnswers?.[toolCall.id]
3239
+ )
3240
+ )) {
3241
+ return "\u7B49\u5F85\u8F93\u5165";
3242
+ }
3243
+ const completedLabel = durationMs > 0 ? `\u6267\u884C\u5B8C\u6210 ${formatExecutionDuration(durationMs)}` : "\u6267\u884C\u5B8C\u6210";
3244
+ const lastExceptionalEvent = findLastExceptionalEvent(messages);
3245
+ if (lastExceptionalEvent) {
3246
+ const recovered = messages.slice(lastExceptionalEvent.messageIndex + 1).some(hasRenderableMessageContent);
3247
+ if (lastExceptionalEvent.status === "error") {
3248
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u5931\u8D25` : "\u6267\u884C\u5931\u8D25";
3249
+ }
3250
+ return recovered ? `${completedLabel} \xB7 \u90E8\u5206\u6B65\u9AA4\u672A\u5B8C\u6210` : "\u6267\u884C\u5DF2\u4E2D\u65AD";
3251
+ }
3252
+ return completedLabel;
3253
+ }
3254
+ function businessToolDisplayName(toolCall) {
3255
+ const displayName = toolCall.display_name?.trim() ?? "";
3256
+ if (!displayName) return "";
3257
+ const rawName = toolCall.name.trim();
3258
+ return displayName !== rawName && formatToolName(displayName) !== formatToolName(rawName) ? displayName : "";
3259
+ }
3260
+ function executionToolTypeLabel(toolCall) {
3261
+ switch (formatToolName(toolCall.name)) {
3262
+ case "WebSearch":
3263
+ case "WebFetch":
3264
+ return "\u7F51\u7EDC\u68C0\u7D22";
3265
+ case "Bash":
3266
+ case "BgBash":
3267
+ return "\u547D\u4EE4\u6267\u884C";
3268
+ case "Read":
3269
+ case "ReadSkill":
3270
+ return "\u5185\u5BB9\u8BFB\u53D6";
3271
+ case "Write":
3272
+ case "Edit":
3273
+ case "MultiEdit":
3274
+ return "\u6587\u4EF6\u5904\u7406";
3275
+ case "Grep":
3276
+ case "Glob":
3277
+ return "\u5185\u5BB9\u641C\u7D22";
3278
+ case "Agent":
3279
+ return "\u5B50\u4EFB\u52A1";
3280
+ case "search_skills":
3281
+ return "\u6280\u80FD\u68C0\u7D22";
3282
+ case "get_skill_content":
3283
+ return "\u8BFB\u53D6\u6280\u80FD";
3284
+ case "run_skill_tool":
3285
+ return "\u6267\u884C\u6280\u80FD";
3286
+ default:
3287
+ return businessToolDisplayName(toolCall) || "\u6267\u884C\u6B65\u9AA4";
3288
+ }
3289
+ }
3290
+ function executionToolIntent(toolCall) {
3291
+ const normalizedName = formatToolName(toolCall.name);
3292
+ let args = null;
3293
+ try {
3294
+ const parsed = JSON.parse(toolCall.arguments);
3295
+ args = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
3296
+ } catch {
3297
+ args = null;
3298
+ }
3299
+ const getString = (key) => {
3300
+ const value = args?.[key];
3301
+ return typeof value === "string" ? value.trim() : "";
3302
+ };
3303
+ const explicitIntent = getString("description") || getString("_meta_display_name") || getString("display_name") || "";
3304
+ if (explicitIntent) return explicitIntent;
3305
+ if (normalizedName === "search_skills") return getString("query");
3306
+ if (normalizedName === "get_skill_content" || normalizedName === "ReadSkill") {
3307
+ return getString("skill_name") || getString("skill");
3308
+ }
3309
+ if (normalizedName === "FinishTask") return getString("title");
3310
+ return "";
3311
+ }
3312
+ function ExecutionToolRow({ toolCall }) {
3313
+ const normalizedName = formatToolName(toolCall.name);
3314
+ const typeLabel = executionToolTypeLabel(toolCall);
3315
+ const intent = executionToolIntent(toolCall);
3316
+ const label = intent ? `${typeLabel}\uFF1A${intent}` : typeLabel;
3317
+ const failed = toolCall.status === "error" || toolCall.status === "cancelled";
3318
+ const iconClass = cn(
3319
+ "size-3.5 shrink-0",
3320
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3321
+ );
3322
+ const icon = toolCall.status === "pending" ? /* @__PURE__ */ jsx13(LoaderCircle, { className: cn(iconClass, "animate-spin"), "aria-hidden": "true" }) : toolCall.status === "error" ? /* @__PURE__ */ jsx13(CircleAlert, { className: iconClass, "aria-hidden": "true" }) : toolCall.status === "cancelled" ? /* @__PURE__ */ jsx13(X, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "WebSearch" || normalizedName === "WebFetch" ? /* @__PURE__ */ jsx13(Earth, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Bash" || normalizedName === "BgBash" ? /* @__PURE__ */ jsx13(Terminal, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Write" || normalizedName === "Edit" || normalizedName === "MultiEdit" ? /* @__PURE__ */ jsx13(FilePenLine, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Read" || normalizedName === "ReadSkill" || normalizedName === "get_skill_content" ? /* @__PURE__ */ jsx13(BookOpen, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Grep" || normalizedName === "Glob" || normalizedName === "search_skills" ? /* @__PURE__ */ jsx13(Search, { className: iconClass, "aria-hidden": "true" }) : normalizedName === "Agent" ? /* @__PURE__ */ jsx13(Bot, { className: iconClass, "aria-hidden": "true" }) : /* @__PURE__ */ jsx13(Wrench, { className: iconClass, "aria-hidden": "true" });
3323
+ const rowClassName = cn(
3324
+ "flex min-w-0 items-center gap-1 py-1.5 text-xs leading-[22px]",
3325
+ failed ? "text-red-500" : "text-[hsl(var(--muted-foreground))]"
3326
+ );
3327
+ return /* @__PURE__ */ jsxs11("div", { "data-testid": "execution-tool-intent", className: rowClassName, title: label, children: [
3328
+ icon,
3329
+ /* @__PURE__ */ jsx13("span", { className: "min-w-0 truncate", children: label })
3330
+ ] });
3331
+ }
2276
3332
  function AssistantTurnBlock({
2277
3333
  messages,
2278
3334
  isStreaming = false,
@@ -2280,62 +3336,319 @@ function AssistantTurnBlock({
2280
3336
  onAnswer,
2281
3337
  sessionStatus,
2282
3338
  toolCallRenderer,
3339
+ hidePlanUpdateTools = false,
2283
3340
  sessionId
2284
3341
  }) {
2285
- const hasInterrupted = messages.some((message) => message.status === "interrupted");
2286
- const hasAnyContent = messages.some(
2287
- (message) => getMessageText(message) || message.reasoning || (message.tool_calls?.length ?? 0) > 0
2288
- );
2289
- const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
2290
- return /* @__PURE__ */ jsxs9("div", { className: "blade-chat-assistant-turn flex flex-col gap-3", children: [
2291
- hasInterrupted && /* @__PURE__ */ jsx11("div", { className: "ml-4 w-fit rounded-full border border-amber-500/30 bg-amber-500/10 px-2.5 py-1 text-[10px] font-medium uppercase tracking-[0.12em] text-amber-300", children: "\u5DF2\u4E2D\u65AD" }),
2292
- messages.map((message, index) => {
2293
- const isLast = index === messages.length - 1;
2294
- const streamingThis = isStreaming && isLast;
2295
- const text = getMessageText(message);
2296
- const toolCalls = message.tool_calls ?? [];
2297
- const showReasoning = !!message.reasoning && (!isStreaming || index === latestReasoningIndex);
2298
- return /* @__PURE__ */ jsxs9(
2299
- "div",
3342
+ const shouldHideToolCall = (message, toolCall) => {
3343
+ if (!hidePlanUpdateTools || !isPlanUpdateTool(toolCall) || parsePlanUpdate(toolCall.arguments) === null) {
3344
+ return false;
3345
+ }
3346
+ return toolCall.status === "done" || toolCall.status === "pending" && message.status === "streaming";
3347
+ };
3348
+ const hasInterrupted = messages.some((message) => message.status === "interrupted");
3349
+ const hasFailedWithoutContent = messages.some(
3350
+ (message) => message.status === "failed" && !hasRenderableMessageContent(message)
3351
+ );
3352
+ const finalMessage = getLastContentMessage(messages);
3353
+ const turnToolCalls = messages.flatMap((message) => message.tool_calls ?? []);
3354
+ const finalOrderedParts = finalMessage ? getOrderedMessageParts(
3355
+ finalMessage,
3356
+ (finalMessage.tool_calls ?? []).filter(
3357
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(finalMessage, toolCall)
3358
+ )
3359
+ ) : [];
3360
+ const hasExecutionProcess = messages.some(
3361
+ (message) => message.reasoning || (message.tool_calls ?? []).some((toolCall) => !shouldHideToolCall(message, toolCall))
3362
+ );
3363
+ const latestReasoningIndex = isStreaming ? findLatestReasoningMessageIndex(messages) : -1;
3364
+ const hasActionableToolCall = messages.some(
3365
+ (message) => message.status === "failed" || message.status === "interrupted" || (message.tool_calls ?? []).some(
3366
+ (toolCall) => !shouldHideToolCall(message, toolCall) && (toolCall.status === "error" || toolCall.status === "cancelled")
3367
+ )
3368
+ );
3369
+ const questionToolCalls = messages.flatMap(
3370
+ (message) => (message.tool_calls ?? []).filter(
3371
+ (toolCall) => formatToolName(toolCall.name) === "AskUserQuestion"
3372
+ )
3373
+ );
3374
+ const activeQuestionId = questionToolCalls.filter((toolCall) => toolCall.status === "pending").at(-1)?.id;
3375
+ const [displayMode, setDisplayMode] = useState13(
3376
+ () => isStreaming || hasActionableToolCall ? "detail" : "compact"
3377
+ );
3378
+ const userSelectedDisplayModeRef = useRef11(false);
3379
+ const wasStreamingRef = useRef11(isStreaming);
3380
+ useEffect10(() => {
3381
+ if (wasStreamingRef.current && !isStreaming && !userSelectedDisplayModeRef.current) {
3382
+ setDisplayMode(hasActionableToolCall ? "detail" : "compact");
3383
+ }
3384
+ wasStreamingRef.current = isStreaming;
3385
+ }, [hasActionableToolCall, isStreaming]);
3386
+ const effectiveMode = resolveTurnDisplayMode({ isStreaming, displayMode });
3387
+ const executionDurationMs = getExecutionDurationMs({ messages, isStreaming });
3388
+ const [clock, setClock] = useState13(() => Date.now());
3389
+ const hasLiveStartTime = messages.some(
3390
+ (message) => message.timestamp != null && Number.isFinite(Date.parse(message.timestamp))
3391
+ );
3392
+ useEffect10(() => {
3393
+ if (!isStreaming || !hasLiveStartTime) return;
3394
+ const timer = window.setInterval(() => setClock(Date.now()), 1e3);
3395
+ return () => window.clearInterval(timer);
3396
+ }, [hasLiveStartTime, isStreaming]);
3397
+ const liveExecutionDurationMs = isStreaming ? getExecutionDurationMs({ messages, isStreaming, now: clock }) : executionDurationMs;
3398
+ const memoryRefs = collectMemoryRefs(messages);
3399
+ if (!hasExecutionProcess) {
3400
+ return /* @__PURE__ */ jsxs11(
3401
+ "div",
3402
+ {
3403
+ "aria-busy": isStreaming || void 0,
3404
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3405
+ children: [
3406
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx13(MemoryRefsHint, { refs: memoryRefs }) : null,
3407
+ hasInterrupted && /* @__PURE__ */ jsx13("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" }),
3408
+ hasFailedWithoutContent && /* @__PURE__ */ jsx13("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" }),
3409
+ messages.map((message, index) => {
3410
+ return hasRenderableMessageContent(message) ? /* @__PURE__ */ jsx13(
3411
+ "div",
3412
+ {
3413
+ className: "flex flex-col gap-3",
3414
+ children: /* @__PURE__ */ jsx13(
3415
+ AssistantMessageContent,
3416
+ {
3417
+ message,
3418
+ sessionId,
3419
+ streaming: isStreaming && index === messages.length - 1
3420
+ }
3421
+ )
3422
+ },
3423
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3424
+ ) : null;
3425
+ })
3426
+ ]
3427
+ }
3428
+ );
3429
+ }
3430
+ return /* @__PURE__ */ jsxs11(
3431
+ "div",
3432
+ {
3433
+ "aria-busy": isStreaming || void 0,
3434
+ className: "blade-chat-assistant-turn flex flex-col gap-3",
3435
+ children: [
3436
+ memoryRefs.length > 0 ? /* @__PURE__ */ jsx13(MemoryRefsHint, { refs: memoryRefs }) : null,
3437
+ hasInterrupted && /* @__PURE__ */ jsx13("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" }),
3438
+ hasFailedWithoutContent && /* @__PURE__ */ jsx13("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" }),
3439
+ /* @__PURE__ */ jsxs11("div", { className: "flex w-full items-start gap-2.5", children: [
3440
+ /* @__PURE__ */ jsx13(
3441
+ "span",
3442
+ {
3443
+ className: "grid size-[30px] shrink-0 place-items-center rounded-full bg-[hsl(var(--muted)/0.55)] text-[hsl(var(--foreground))]",
3444
+ "aria-hidden": "true",
3445
+ children: /* @__PURE__ */ jsx13(Bot, { size: 16 })
3446
+ }
3447
+ ),
3448
+ /* @__PURE__ */ jsxs11("div", { className: "min-w-0 flex-1 pt-0.5", children: [
3449
+ /* @__PURE__ */ jsxs11(
3450
+ "button",
3451
+ {
3452
+ type: "button",
3453
+ onClick: () => {
3454
+ userSelectedDisplayModeRef.current = true;
3455
+ setDisplayMode(displayMode === "detail" ? "compact" : "detail");
3456
+ },
3457
+ "aria-expanded": effectiveMode === "detail",
3458
+ "aria-label": effectiveMode === "detail" ? "\u6536\u8D77\u6267\u884C\u8FC7\u7A0B" : "\u5C55\u5F00\u6267\u884C\u8FC7\u7A0B",
3459
+ "data-testid": "assistant-execution-summary",
3460
+ 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",
3461
+ children: [
3462
+ /* @__PURE__ */ jsx13("span", { className: "min-w-0 truncate", children: executionSummaryLabel({
3463
+ messages,
3464
+ isStreaming,
3465
+ durationMs: liveExecutionDurationMs,
3466
+ sessionStatus,
3467
+ askAnswers
3468
+ }) }),
3469
+ /* @__PURE__ */ jsx13(
3470
+ ChevronRight,
3471
+ {
3472
+ size: 14,
3473
+ style: { transitionDuration: "260ms", transitionTimingFunction: "cubic-bezier(0.25, 0.1, 0.25, 1)" },
3474
+ className: cn(
3475
+ "shrink-0 transition-transform",
3476
+ effectiveMode === "detail" && "rotate-90"
3477
+ ),
3478
+ "aria-hidden": "true"
3479
+ }
3480
+ )
3481
+ ]
3482
+ }
3483
+ ),
3484
+ /* @__PURE__ */ jsx13("div", { className: "mt-3 h-px w-full bg-[hsl(var(--border)/0.75)]" })
3485
+ ] })
3486
+ ] }),
3487
+ effectiveMode === "detail" ? /* @__PURE__ */ jsx13("div", { className: "ml-10 flex flex-col gap-3 pt-1", children: messages.map((message, index) => {
3488
+ const isLast = index === messages.length - 1;
3489
+ const streamingThis = isStreaming && isLast;
3490
+ const text = getMessageText(message);
3491
+ const toolCalls = (message.tool_calls ?? []).filter(
3492
+ (toolCall) => formatToolName(toolCall.name) !== "AskUserQuestion" && !shouldHideToolCall(message, toolCall)
3493
+ );
3494
+ const orderedParts = getOrderedMessageParts(message, toolCalls);
3495
+ const showReasoning = !!message.reasoning && isStreaming && index === latestReasoningIndex;
3496
+ return /* @__PURE__ */ jsxs11(
3497
+ "div",
3498
+ {
3499
+ className: "flex flex-col gap-3",
3500
+ children: [
3501
+ showReasoning && message.reasoning ? /* @__PURE__ */ jsx13(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }) : null,
3502
+ orderedParts.length > 0 ? orderedParts.map(
3503
+ (part) => part.type === "text" ? /* @__PURE__ */ jsx13(
3504
+ AssistantMessageContent,
3505
+ {
3506
+ message: { ...message, content: part.content, tool_calls: turnToolCalls },
3507
+ sessionId,
3508
+ streaming: streamingThis,
3509
+ compact: true
3510
+ },
3511
+ part.key
3512
+ ) : /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-0.5", children: part.toolCalls.map((toolCall) => {
3513
+ const custom = toolCallRenderer?.(toolCall);
3514
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx13("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx13(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx13(ExecutionToolRow, { toolCall }, toolCall.id);
3515
+ }) }, part.key)
3516
+ ) : hasRenderableMessageContent(message) && message !== finalMessage ? /* @__PURE__ */ jsx13(
3517
+ AssistantMessageContent,
3518
+ {
3519
+ message,
3520
+ sessionId,
3521
+ streaming: streamingThis,
3522
+ compact: true
3523
+ }
3524
+ ) : null,
3525
+ orderedParts.length === 0 && toolCalls.length > 0 ? /* @__PURE__ */ jsx13("div", { className: "flex flex-col gap-0.5", children: toolCalls.map((toolCall) => {
3526
+ const custom = toolCallRenderer?.(toolCall);
3527
+ return custom !== null && custom !== void 0 ? /* @__PURE__ */ jsx13("div", { children: custom }, toolCall.id) : formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx13(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx13(ExecutionToolRow, { toolCall }, toolCall.id);
3528
+ }) }) : null
3529
+ ]
3530
+ },
3531
+ message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
3532
+ );
3533
+ }) }) : null,
3534
+ finalMessage && (effectiveMode === "compact" || finalOrderedParts.length === 0) ? /* @__PURE__ */ jsx13("div", { className: "ml-10", children: /* @__PURE__ */ jsx13(
3535
+ AssistantMessageContent,
3536
+ {
3537
+ message: finalMessage,
3538
+ sessionId,
3539
+ streaming: isStreaming && finalMessage === messages[messages.length - 1]
3540
+ }
3541
+ ) }) : null,
3542
+ questionToolCalls.map((toolCall) => /* @__PURE__ */ jsx13(
3543
+ ToolCallBlock,
3544
+ {
3545
+ toolCall,
3546
+ answerData: askAnswers?.[toolCall.id],
3547
+ onAnswer,
3548
+ answered: sessionStatus !== "waiting_for_input",
3549
+ sessionStatus,
3550
+ isActiveQuestion: toolCall.id === activeQuestionId,
3551
+ renderer: toolCallRenderer
3552
+ },
3553
+ toolCall.id
3554
+ ))
3555
+ ]
3556
+ }
3557
+ );
3558
+ }
3559
+ function collectMemoryRefs(messages) {
3560
+ const refs = /* @__PURE__ */ new Map();
3561
+ for (const message of messages) {
3562
+ for (const ref of message.memory_refs ?? []) if (!refs.has(ref.id)) refs.set(ref.id, ref);
3563
+ }
3564
+ return [...refs.values()];
3565
+ }
3566
+ function MemoryRefsHint({ refs }) {
3567
+ const [expanded, setExpanded] = useState13(false);
3568
+ const label = refs.some((ref) => ref.skill_name) ? "\u53C2\u8003\u4E86\u8BE5\u6280\u80FD\u7684\u5386\u53F2\u7ECF\u9A8C" : "\u53C2\u8003\u4E86\u5386\u53F2\u7ECF\u9A8C";
3569
+ return /* @__PURE__ */ jsxs11("div", { className: "blade-chat-memory-refs ml-1 w-full max-w-[680px]", children: [
3570
+ /* @__PURE__ */ jsxs11("button", { type: "button", onClick: () => setExpanded((value) => !value), className: "inline-flex h-8 items-center gap-1.5 rounded-lg border border-[hsl(var(--primary)/0.22)] bg-[hsl(var(--primary)/0.07)] px-3 text-xs font-medium text-[hsl(var(--primary))]", children: [
3571
+ /* @__PURE__ */ jsx13(BookOpen, { size: 12 }),
3572
+ /* @__PURE__ */ jsxs11("span", { children: [
3573
+ label,
3574
+ "\uFF08",
3575
+ refs.length,
3576
+ "\uFF09"
3577
+ ] }),
3578
+ /* @__PURE__ */ jsx13(ChevronRight, { size: 10, className: cn("transition-transform", expanded && "rotate-90") })
3579
+ ] }),
3580
+ expanded ? /* @__PURE__ */ jsx13("div", { className: "mt-2 flex flex-col gap-2 rounded-xl border border-[hsl(var(--border)/0.8)] bg-[hsl(var(--muted)/0.28)] p-2.5", children: refs.map((ref) => /* @__PURE__ */ jsxs11("div", { className: "rounded-lg border border-[hsl(var(--border)/0.55)] bg-[hsl(var(--background)/0.72)] px-3 py-2.5 text-xs", children: [
3581
+ /* @__PURE__ */ jsx13("p", { className: "line-clamp-2 break-words leading-5", children: ref.content_preview }),
3582
+ ref.skill_name ? /* @__PURE__ */ jsx13("span", { className: "mt-1 inline-flex text-[10px] text-[hsl(var(--primary))]", children: ref.skill_name }) : null
3583
+ ] }, ref.id)) }) : null
3584
+ ] });
3585
+ }
3586
+ function AssistantMessageContent({
3587
+ message,
3588
+ sessionId,
3589
+ streaming,
3590
+ compact = false
3591
+ }) {
3592
+ const text = getMessageText(message);
3593
+ const imageParts = getImageParts(message.content);
3594
+ const fileParts = getFileParts(message.content);
3595
+ const failed = message.status === "failed";
3596
+ const failedBadge = failed ? /* @__PURE__ */ jsx13("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;
3597
+ const textContent = text ? /* @__PURE__ */ jsx13(
3598
+ "div",
3599
+ {
3600
+ className: cn(
3601
+ "blade-chat-assistant-text",
3602
+ compact ? "text-xs leading-[22px] text-[hsl(var(--foreground))]" : "text-[15px] leading-8 text-[hsl(var(--foreground))]"
3603
+ ),
3604
+ children: /* @__PURE__ */ jsx13(
3605
+ MarkdownContent,
2300
3606
  {
2301
- className: "flex flex-col gap-3",
2302
- children: [
2303
- showReasoning && message.reasoning && /* @__PURE__ */ jsx11(ThinkingBlock, { reasoning: message.reasoning, isStreaming: streamingThis && !text }),
2304
- text && /* @__PURE__ */ jsx11("div", { className: "blade-chat-assistant-text text-[15px] leading-8 text-[hsl(var(--foreground))]", children: /* @__PURE__ */ jsx11(
2305
- MarkdownContent,
2306
- {
2307
- mode: streamingThis ? "streaming" : "static",
2308
- className: "blade-chat-prose",
2309
- sessionId,
2310
- children: text
2311
- }
2312
- ) }),
2313
- toolCalls.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-col gap-2", children: toolCalls.map(
2314
- (toolCall) => formatToolName(toolCall.name) === "Agent" ? /* @__PURE__ */ jsx11(AgentLoopBlock, { toolCall }, toolCall.id) : /* @__PURE__ */ jsx11(
2315
- ToolCallBlock,
2316
- {
2317
- toolCall,
2318
- answerData: askAnswers?.[toolCall.id],
2319
- onAnswer,
2320
- answered: sessionStatus !== "waiting_for_input",
2321
- sessionStatus,
2322
- renderer: toolCallRenderer
2323
- },
2324
- toolCall.id
2325
- )
2326
- ) })
2327
- ]
2328
- },
2329
- message.entry_id ?? `${message.timestamp ?? "assistant"}-${index}`
2330
- );
2331
- }),
2332
- isStreaming && !hasAnyContent && /* @__PURE__ */ jsx11(Shimmer, { className: "ml-4", children: "\u6B63\u5728\u751F\u6210..." })
3607
+ mode: streaming ? "streaming" : "static",
3608
+ className: "blade-chat-prose",
3609
+ sessionId,
3610
+ children: text
3611
+ }
3612
+ )
3613
+ }
3614
+ ) : null;
3615
+ if (imageParts.length === 0 && fileParts.length === 0) {
3616
+ if (!failed) return textContent;
3617
+ return failedBadge || textContent ? /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-2", children: [
3618
+ failedBadge,
3619
+ textContent
3620
+ ] }) : null;
3621
+ }
3622
+ return /* @__PURE__ */ jsxs11("div", { className: "flex flex-col gap-3", children: [
3623
+ failedBadge,
3624
+ imageParts.length > 0 ? /* @__PURE__ */ jsx13("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx13(
3625
+ "img",
3626
+ {
3627
+ src: part.image_url.url,
3628
+ alt: "\u6D88\u606F\u9644\u4EF6",
3629
+ className: "max-h-72 rounded-xl border border-[hsl(var(--border))] object-cover"
3630
+ },
3631
+ part.image_url.url
3632
+ )) }) : null,
3633
+ fileParts.length > 0 ? /* @__PURE__ */ jsx13("div", { className: "flex flex-wrap gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs11(
3634
+ "div",
3635
+ {
3636
+ 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))]",
3637
+ title: part.name,
3638
+ children: [
3639
+ /* @__PURE__ */ jsx13(FileText, { size: 12, className: "shrink-0" }),
3640
+ /* @__PURE__ */ jsx13("span", { className: "max-w-56 truncate", children: part.name })
3641
+ ]
3642
+ },
3643
+ `${part.name}-${part.data.slice(0, 32)}`
3644
+ )) }) : null,
3645
+ textContent
2333
3646
  ] });
2334
3647
  }
2335
3648
 
2336
3649
  // src/components/RenderErrorBoundary.tsx
2337
3650
  import { Component } from "react";
2338
- import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
3651
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
2339
3652
  function getFirstComponentName(componentStack) {
2340
3653
  const match = componentStack.match(/\n\s+at\s+([^\s(]+)/);
2341
3654
  return match?.[1] ?? null;
@@ -2368,26 +3681,26 @@ var RenderErrorBoundary = class extends Component {
2368
3681
  return children;
2369
3682
  }
2370
3683
  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: [
3684
+ return /* @__PURE__ */ jsx14("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__ */ jsxs12("div", { className: "flex items-start gap-2", children: [
3685
+ /* @__PURE__ */ jsx14(TriangleAlert, { className: "mt-0.5 h-4 w-4 shrink-0 text-amber-300" }),
3686
+ /* @__PURE__ */ jsxs12("div", { className: "min-w-0 flex-1", children: [
3687
+ /* @__PURE__ */ jsxs12("div", { className: "font-medium", children: [
2375
3688
  label,
2376
3689
  "\u6E32\u67D3\u5931\u8D25"
2377
3690
  ] }),
2378
- /* @__PURE__ */ jsxs10("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
3691
+ /* @__PURE__ */ jsxs12("div", { className: "mt-1 break-words text-xs leading-5 text-amber-100/75", children: [
2379
3692
  componentName ? `\u7EC4\u4EF6\uFF1A${componentName}\u3002` : null,
2380
3693
  error.message || "\u53D1\u751F\u4E86\u672A\u9884\u671F\u7684\u6E32\u67D3\u9519\u8BEF\u3002"
2381
3694
  ] }),
2382
- details ? /* @__PURE__ */ jsx12("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
3695
+ details ? /* @__PURE__ */ jsx14("div", { className: "mt-1 truncate text-xs text-amber-100/55", children: details }) : null
2383
3696
  ] })
2384
3697
  ] }) });
2385
3698
  }
2386
3699
  };
2387
3700
 
2388
3701
  // 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";
3702
+ import { useCallback as useCallback6, useEffect as useEffect11, useRef as useRef12, useState as useState14 } from "react";
3703
+ import { Fragment as Fragment3, jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
2391
3704
  function emitInteraction(callback, event) {
2392
3705
  try {
2393
3706
  callback?.(event);
@@ -2397,9 +3710,6 @@ function emitInteraction(callback, event) {
2397
3710
  function basename(path) {
2398
3711
  return path.split(/[\\/]/).filter(Boolean).pop() || path;
2399
3712
  }
2400
- function isVideo(path) {
2401
- return /\.(?:mp4|mov|webm|mkv|avi|m4v)$/i.test(path);
2402
- }
2403
3713
  function ArtifactCard({
2404
3714
  artifact,
2405
3715
  sessionId,
@@ -2409,10 +3719,10 @@ function ArtifactCard({
2409
3719
  onArtifactOpened
2410
3720
  }) {
2411
3721
  const client = useBladeClient();
2412
- const [downloading, setDownloading] = useState10(false);
3722
+ const [downloading, setDownloading] = useState14(false);
2413
3723
  const name = artifact.label || basename(artifact.target);
2414
3724
  if (artifact.kind === "link") {
2415
- return /* @__PURE__ */ jsxs11(
3725
+ return /* @__PURE__ */ jsxs13(
2416
3726
  "a",
2417
3727
  {
2418
3728
  href: artifact.target,
@@ -2423,51 +3733,55 @@ function ArtifactCard({
2423
3733
  ${artifact.target}`,
2424
3734
  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
3735
  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" })
3736
+ /* @__PURE__ */ jsx15(Globe, { size: 15, className: "shrink-0 text-[hsl(var(--primary))]" }),
3737
+ /* @__PURE__ */ jsx15("span", { className: "min-w-0 flex-1 truncate font-medium", children: name }),
3738
+ /* @__PURE__ */ jsx15(ArrowUpRight, { size: 13, className: "absolute right-2 opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" })
2429
3739
  ]
2430
3740
  }
2431
3741
  );
2432
3742
  }
2433
- const Icon2 = isVideo(artifact.target) ? Film : File;
2434
- return /* @__PURE__ */ jsxs11(
2435
- "button",
3743
+ const fileName = basename(artifact.target);
3744
+ const downloadUrl = sessionId ? client.buildAuthedUrl(
3745
+ `/api/sessions/${encodeURIComponent(sessionId)}/files/${encodeURIComponent(artifact.target)}`
3746
+ ) : void 0;
3747
+ const handleDownload = async (event) => {
3748
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
3749
+ event.preventDefault();
3750
+ if (!sessionId || downloading) return;
3751
+ setDownloading(true);
3752
+ emitInteraction(onInteraction, {
3753
+ type: "artifact_download_started",
3754
+ sessionId,
3755
+ assistantEntryId,
3756
+ artifactIndex,
3757
+ artifactKind: "file"
3758
+ });
3759
+ try {
3760
+ await client.sessions.downloadFile(sessionId, artifact.target, fileName);
3761
+ emitInteraction(onInteraction, {
3762
+ type: "artifact_download_succeeded",
3763
+ sessionId,
3764
+ assistantEntryId,
3765
+ artifactIndex,
3766
+ artifactKind: "file"
3767
+ });
3768
+ } catch {
3769
+ } finally {
3770
+ setDownloading(false);
3771
+ }
3772
+ };
3773
+ return /* @__PURE__ */ jsx15(
3774
+ "a",
2436
3775
  {
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
- ]
3776
+ href: downloadUrl,
3777
+ download: fileName,
3778
+ onClick: handleDownload,
3779
+ title: fileName,
3780
+ "aria-label": `\u4E0B\u8F7D\u6587\u4EF6\uFF1A${fileName}`,
3781
+ "aria-disabled": !sessionId || void 0,
3782
+ "aria-busy": downloading || void 0,
3783
+ 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",
3784
+ children: fileName
2471
3785
  }
2472
3786
  );
2473
3787
  }
@@ -2484,17 +3798,17 @@ function feedbackReasonLabel(reason) {
2484
3798
  }
2485
3799
  function HistoricalResultFeedback({ feedback }) {
2486
3800
  const label = feedbackReasonLabel(feedback.reason);
2487
- return /* @__PURE__ */ jsxs11(
3801
+ return /* @__PURE__ */ jsxs13(
2488
3802
  "section",
2489
3803
  {
2490
3804
  "aria-label": "\u5386\u53F2\u7ED3\u679C\u53CD\u9988",
2491
3805
  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
3806
  children: [
2493
- /* @__PURE__ */ jsxs11("span", { children: [
3807
+ /* @__PURE__ */ jsxs13("span", { children: [
2494
3808
  "\u4F60\u5BF9\u6B64\u8F6E\u7ED3\u679C\u7684\u8BC4\u4EF7\uFF1A",
2495
3809
  feedback.helpful ? "\u6709\u5E2E\u52A9" : "\u6CA1\u5E2E\u52A9"
2496
3810
  ] }),
2497
- label ? /* @__PURE__ */ jsxs11("span", { children: [
3811
+ label ? /* @__PURE__ */ jsxs13("span", { children: [
2498
3812
  " \xB7 ",
2499
3813
  label
2500
3814
  ] }) : null
@@ -2511,15 +3825,15 @@ function ResultFeedback({
2511
3825
  onFeedbackSaved
2512
3826
  }) {
2513
3827
  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);
3828
+ const [saved, setSaved] = useState14(savedFeedback ?? null);
3829
+ const [helpful, setHelpful] = useState14(savedFeedback?.helpful ?? null);
3830
+ const [reason, setReason] = useState14(savedFeedback?.reason ?? null);
3831
+ const [saving, setSaving] = useState14(false);
3832
+ const [saveError, setSaveError] = useState14(false);
3833
+ const reportedShown = useRef12(false);
3834
+ const latestChoice = useRef12(null);
2521
3835
  const eligible = followup.feedback_eligible === true && Boolean(sessionId) && !isViewer;
2522
- useEffect6(() => {
3836
+ useEffect11(() => {
2523
3837
  if (!eligible || reportedShown.current) return;
2524
3838
  reportedShown.current = true;
2525
3839
  emitInteraction(onInteraction, {
@@ -2528,13 +3842,13 @@ function ResultFeedback({
2528
3842
  assistantEntryId: followup.assistant_entry_id
2529
3843
  });
2530
3844
  }, [eligible, followup.assistant_entry_id, onInteraction, sessionId]);
2531
- useEffect6(() => {
3845
+ useEffect11(() => {
2532
3846
  if (!savedFeedback || latestChoice.current) return;
2533
3847
  setSaved(savedFeedback);
2534
3848
  setHelpful(savedFeedback.helpful);
2535
3849
  setReason(savedFeedback.reason);
2536
3850
  }, [savedFeedback]);
2537
- const submit = useCallback5(
3851
+ const submit = useCallback6(
2538
3852
  async (nextHelpful, nextReason) => {
2539
3853
  if (!sessionId) return;
2540
3854
  const choice = { helpful: nextHelpful, reason: nextReason };
@@ -2569,15 +3883,15 @@ function ResultFeedback({
2569
3883
  [client, followup.assistant_entry_id, onFeedbackSaved, onInteraction, sessionId]
2570
3884
  );
2571
3885
  if (!eligible) return null;
2572
- return /* @__PURE__ */ jsxs11(
3886
+ return /* @__PURE__ */ jsxs13(
2573
3887
  "section",
2574
3888
  {
2575
3889
  "aria-label": "\u7ED3\u679C\u53CD\u9988",
2576
3890
  className: "flex flex-col gap-2 border-t border-[hsl(var(--border))] pt-3",
2577
3891
  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(
3892
+ /* @__PURE__ */ jsx15("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u76EE\u524D\u7684\u6574\u4F53\u7ED3\u679C\u6709\u5E2E\u52A9\u5417\uFF1F" }),
3893
+ /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap gap-1.5", children: [
3894
+ /* @__PURE__ */ jsx15(
2581
3895
  "button",
2582
3896
  {
2583
3897
  type: "button",
@@ -2588,7 +3902,7 @@ function ResultFeedback({
2588
3902
  children: "\u6709\u5E2E\u52A9"
2589
3903
  }
2590
3904
  ),
2591
- /* @__PURE__ */ jsx13(
3905
+ /* @__PURE__ */ jsx15(
2592
3906
  "button",
2593
3907
  {
2594
3908
  type: "button",
@@ -2600,7 +3914,7 @@ function ResultFeedback({
2600
3914
  }
2601
3915
  )
2602
3916
  ] }),
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(
3917
+ helpful === false ? /* @__PURE__ */ jsx15("div", { className: "flex flex-wrap gap-1.5", "aria-label": "\u6CA1\u5E2E\u52A9\u7684\u4E3B\u8981\u539F\u56E0", children: FEEDBACK_REASONS.map((item) => /* @__PURE__ */ jsx15(
2604
3918
  "button",
2605
3919
  {
2606
3920
  type: "button",
@@ -2612,9 +3926,9 @@ function ResultFeedback({
2612
3926
  },
2613
3927
  item.value
2614
3928
  )) }) : 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(
3929
+ saveError ? /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 text-xs text-[hsl(var(--destructive))]", children: [
3930
+ /* @__PURE__ */ jsx15("span", { children: "\u53CD\u9988\u6682\u672A\u4FDD\u5B58\uFF0C\u53EF\u91CD\u8BD5" }),
3931
+ /* @__PURE__ */ jsx15(
2618
3932
  "button",
2619
3933
  {
2620
3934
  type: "button",
@@ -2626,7 +3940,7 @@ function ResultFeedback({
2626
3940
  children: "\u91CD\u8BD5"
2627
3941
  }
2628
3942
  )
2629
- ] }) : saved ? /* @__PURE__ */ jsx13("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
3943
+ ] }) : saved ? /* @__PURE__ */ jsx15("div", { className: "text-[11px] text-[hsl(var(--muted-foreground))]", children: "\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u968F\u65F6\u4FEE\u6539" }) : null
2630
3944
  ]
2631
3945
  }
2632
3946
  );
@@ -2640,14 +3954,14 @@ function PostChatFollowupBlock({
2640
3954
  savedFeedback,
2641
3955
  onFeedbackSaved
2642
3956
  }) {
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());
3957
+ const [expanded, setExpanded] = useState14(false);
3958
+ const adopted = useRef12(/* @__PURE__ */ new Set());
3959
+ const reportedSuggestions = useRef12(false);
3960
+ const reportedArtifacts = useRef12(/* @__PURE__ */ new Set());
3961
+ const openedArtifacts = useRef12(/* @__PURE__ */ new Set());
2648
3962
  const artifacts = followup.final_artifacts ?? [];
2649
3963
  const visibleArtifacts = expanded ? artifacts : artifacts.slice(0, 3);
2650
- useEffect6(() => {
3964
+ useEffect11(() => {
2651
3965
  if (!reportedSuggestions.current && followup.suggestions.length > 0) {
2652
3966
  reportedSuggestions.current = true;
2653
3967
  emitInteraction(onInteraction, {
@@ -2677,7 +3991,7 @@ function PostChatFollowupBlock({
2677
3991
  sessionId,
2678
3992
  visibleArtifacts
2679
3993
  ]);
2680
- const reportArtifactOpened = useCallback5(
3994
+ const reportArtifactOpened = useCallback6(
2681
3995
  (artifactIndex, artifactKind) => {
2682
3996
  if (openedArtifacts.current.has(artifactIndex)) return;
2683
3997
  openedArtifacts.current.add(artifactIndex);
@@ -2693,15 +4007,15 @@ function PostChatFollowupBlock({
2693
4007
  );
2694
4008
  if (!followup.recaption && artifacts.length === 0 && followup.suggestions.length === 0 && !followup.feedback_eligible)
2695
4009
  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 }),
4010
+ return /* @__PURE__ */ jsxs13("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: [
4011
+ followup.recaption || artifacts.length > 0 ? /* @__PURE__ */ jsxs13("section", { "aria-label": "\u672C\u8F6E\u5C0F\u7ED3", className: "flex flex-col gap-2", children: [
4012
+ /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-1.5 text-xs font-medium text-[hsl(var(--muted-foreground))]", children: [
4013
+ /* @__PURE__ */ jsx15(Sparkles, { size: 14 }),
2700
4014
  "\u672C\u8F6E\u5C0F\u7ED3"
2701
4015
  ] }),
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(
4016
+ followup.recaption ? /* @__PURE__ */ jsx15("p", { className: "text-[13px] leading-5", children: followup.recaption }) : null,
4017
+ artifacts.length > 0 ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
4018
+ /* @__PURE__ */ jsx15("div", { className: "grid max-w-full grid-cols-3 gap-1.5", children: visibleArtifacts.map((artifact, artifactIndex) => /* @__PURE__ */ jsx15(
2705
4019
  ArtifactCard,
2706
4020
  {
2707
4021
  artifact,
@@ -2713,7 +4027,7 @@ function PostChatFollowupBlock({
2713
4027
  },
2714
4028
  `${artifact.kind}:${artifactIndex}`
2715
4029
  )) }),
2716
- artifacts.length > 3 ? /* @__PURE__ */ jsxs11(
4030
+ artifacts.length > 3 ? /* @__PURE__ */ jsxs13(
2717
4031
  "button",
2718
4032
  {
2719
4033
  type: "button",
@@ -2722,15 +4036,15 @@ function PostChatFollowupBlock({
2722
4036
  className: "flex w-fit items-center gap-0.5 text-[11px] text-[hsl(var(--muted-foreground))]",
2723
4037
  children: [
2724
4038
  expanded ? "\u6536\u8D77" : `\u5C55\u5F00 ${artifacts.length - 3} \u4E2A`,
2725
- /* @__PURE__ */ jsx13(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
4039
+ /* @__PURE__ */ jsx15(ChevronDown, { size: 13, className: expanded ? "rotate-180" : void 0 })
2726
4040
  ]
2727
4041
  }
2728
4042
  ) : null
2729
4043
  ] }) : null
2730
4044
  ] }) : 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(
4045
+ followup.suggestions.length > 0 ? /* @__PURE__ */ jsxs13("section", { "aria-label": "\u4E0B\u4E00\u6B65\u5EFA\u8BAE", className: "flex flex-col gap-1.5", children: [
4046
+ /* @__PURE__ */ jsx15("div", { className: "text-xs font-medium text-[hsl(var(--muted-foreground))]", children: "\u4E0B\u4E00\u6B65\u53EF\u4EE5" }),
4047
+ followup.suggestions.map((suggestion, suggestionIndex) => /* @__PURE__ */ jsxs13(
2734
4048
  "button",
2735
4049
  {
2736
4050
  type: "button",
@@ -2749,14 +4063,14 @@ function PostChatFollowupBlock({
2749
4063
  },
2750
4064
  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
4065
  children: [
2752
- /* @__PURE__ */ jsx13("span", { children: suggestion }),
2753
- /* @__PURE__ */ jsx13(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
4066
+ /* @__PURE__ */ jsx15("span", { children: suggestion }),
4067
+ /* @__PURE__ */ jsx15(ArrowRight, { size: 14, className: "ml-auto shrink-0" })
2754
4068
  ]
2755
4069
  },
2756
4070
  suggestion
2757
4071
  ))
2758
4072
  ] }) : null,
2759
- /* @__PURE__ */ jsx13(
4073
+ /* @__PURE__ */ jsx15(
2760
4074
  ResultFeedback,
2761
4075
  {
2762
4076
  followup,
@@ -2771,8 +4085,91 @@ function PostChatFollowupBlock({
2771
4085
  }
2772
4086
 
2773
4087
  // 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";
4088
+ import {
4089
+ chatErrorForDisplay,
4090
+ getFileParts as getFileParts2,
4091
+ getImageParts as getImageParts2,
4092
+ getTextContent as getTextContent2
4093
+ } from "@blade-hq/agent-client";
4094
+
4095
+ // src/lib/whatif-prompt.ts
4096
+ var HEADER_RE = /^以下消息和 step 产物标记为 deprecated_by_rerun,请基于最新用户假设从 step(\d+) 开始完整重新推演,不要复用旧结论。$/;
4097
+ var QUOTE_HEADER_RE = /^\[步骤(\d+)\s*·\s*(.+?)\]$/;
4098
+ var USER_INPUT_TAG = "[\u7528\u6237\u8F93\u5165]";
4099
+ function parseWhatIfPrompt(text) {
4100
+ const lines = text.replace(/\r\n/g, "\n").trimEnd().split("\n");
4101
+ const headerMatch = lines[0]?.match(HEADER_RE);
4102
+ if (!headerMatch) return null;
4103
+ const userTagIdx = lines.indexOf(USER_INPUT_TAG);
4104
+ const hasUserTag = userTagIdx >= 0;
4105
+ const quoteBlockEndExclusive = hasUserTag ? userTagIdx : lines.length;
4106
+ const quoteHeaderIdxs = [];
4107
+ let quoteBlockFound = false;
4108
+ for (let i = 1; i < quoteBlockEndExclusive; i++) {
4109
+ if (!quoteBlockFound && lines[i].trim() === "[\u5F15\u7528]") {
4110
+ quoteBlockFound = true;
4111
+ } else if (quoteBlockFound && QUOTE_HEADER_RE.test(lines[i])) {
4112
+ quoteHeaderIdxs.push(i);
4113
+ }
4114
+ }
4115
+ let legacyUserTextStart = -1;
4116
+ if (!hasUserTag && quoteHeaderIdxs.length > 0) {
4117
+ const lastSnapshotStart = quoteHeaderIdxs.at(-1) + 1;
4118
+ let i = lines.length - 1;
4119
+ while (i >= lastSnapshotStart && lines[i].trim() === "") i -= 1;
4120
+ while (i >= lastSnapshotStart && lines[i].trim() !== "") i -= 1;
4121
+ if (i >= lastSnapshotStart) legacyUserTextStart = i + 1;
4122
+ }
4123
+ const quotes = quoteHeaderIdxs.map((headerIdx, index) => {
4124
+ const match = lines[headerIdx].match(QUOTE_HEADER_RE);
4125
+ const nextHeader = quoteHeaderIdxs[index + 1];
4126
+ const end = nextHeader ?? (legacyUserTextStart >= 0 ? legacyUserTextStart : quoteBlockEndExclusive);
4127
+ const snapshotLines = lines.slice(headerIdx + 1, end);
4128
+ while (snapshotLines.at(-1)?.trim() === "") snapshotLines.pop();
4129
+ return {
4130
+ stepNumber: Number.parseInt(match[1], 10),
4131
+ label: match[2].trim(),
4132
+ snapshot: snapshotLines.join("\n")
4133
+ };
4134
+ });
4135
+ const userTextStart = hasUserTag ? userTagIdx + 1 : legacyUserTextStart;
4136
+ const userLines = userTextStart >= 0 ? lines.slice(userTextStart) : [];
4137
+ while (userLines[0]?.trim() === "") userLines.shift();
4138
+ while (userLines.at(-1)?.trim() === "") userLines.pop();
4139
+ const fromStep = Number.parseInt(headerMatch[1], 10);
4140
+ return {
4141
+ fromStep: Number.isFinite(fromStep) ? fromStep : null,
4142
+ quotes,
4143
+ userText: userLines.join("\n")
4144
+ };
4145
+ }
4146
+
4147
+ // src/components/WhatIfUserBubble.tsx
4148
+ import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4149
+ function WhatIfUserBubble({ parsed, onQuoteClick }) {
4150
+ const { fromStep, quotes, userText } = parsed;
4151
+ return /* @__PURE__ */ jsxs14("div", { className: "flex flex-col items-end gap-2", children: [
4152
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-1.5 rounded-full border border-[hsl(var(--border))] bg-[hsl(var(--muted)/0.5)] px-2.5 py-0.5 text-[10px] text-[hsl(var(--muted-foreground))]", children: [
4153
+ /* @__PURE__ */ jsx16(RefreshCcw, { size: 10 }),
4154
+ /* @__PURE__ */ jsx16("span", { children: fromStep != null ? `\u91CD\u8DD1\u81EA step ${fromStep}` : "\u91CD\u8DD1" })
4155
+ ] }),
4156
+ quotes.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex max-w-[min(72vw,42rem)] flex-col items-stretch gap-2", children: quotes.map((quote, index) => {
4157
+ const clickable = quote.stepNumber != null && !!onQuoteClick;
4158
+ const label = quote.stepNumber != null ? `\u6B65\u9AA4${quote.stepNumber} \xB7 ${quote.label}` : quote.label;
4159
+ return /* @__PURE__ */ jsxs14("div", { className: "rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card)/0.8)] px-3 py-2 text-left", children: [
4160
+ /* @__PURE__ */ jsxs14("button", { type: "button", disabled: !clickable, onClick: () => clickable && onQuoteClick(quote.stepNumber), className: "inline-flex max-w-full items-center gap-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))] hover:text-[hsl(var(--foreground))] disabled:cursor-default", title: clickable ? "\u8DF3\u8F6C\u5230\u5BF9\u5E94\u6B65\u9AA4\u5361\u7247" : void 0, children: [
4161
+ /* @__PURE__ */ jsx16("span", { children: "\u21B3" }),
4162
+ /* @__PURE__ */ jsx16("span", { className: "truncate", children: label })
4163
+ ] }),
4164
+ quote.snapshot ? /* @__PURE__ */ jsx16("div", { className: "mt-1 border-l-2 border-[hsl(var(--accent-foreground)/0.35)] pl-2 text-xs leading-relaxed text-[hsl(var(--foreground)/0.8)]", children: /* @__PURE__ */ jsx16(MarkdownContent, { className: "blade-chat-prose", children: quote.snapshot }) }) : null
4165
+ ] }, `${quote.stepNumber ?? "x"}-${index}`);
4166
+ }) }),
4167
+ userText && /* @__PURE__ */ jsx16("div", { className: "rounded-2xl border border-[hsl(var(--user-msg-border))] bg-[hsl(var(--user-msg-bg))] px-4 py-2.5 text-sm leading-relaxed text-[hsl(var(--user-msg-fg))]", children: /* @__PURE__ */ jsx16(MarkdownContent, { className: "blade-chat-prose", children: userText }) })
4168
+ ] });
4169
+ }
4170
+
4171
+ // src/components/UserMessageBubble.tsx
4172
+ import { jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
2776
4173
  function isUserMessage(message) {
2777
4174
  return message.role === "user";
2778
4175
  }
@@ -2782,10 +4179,14 @@ function isErrorMessage(message) {
2782
4179
  var isSending = (message) => message.status === "streaming";
2783
4180
  function UserMessageBubble({ message, className }) {
2784
4181
  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(
4182
+ const fileParts = getFileParts2(message.content);
4183
+ const imageParts = getImageParts2(message.content);
4184
+ const whatifParsed = text && imageParts.length === 0 && fileParts.length === 0 ? parseWhatIfPrompt(text) : null;
4185
+ if (whatifParsed) {
4186
+ return /* @__PURE__ */ jsx17("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsx17("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: /* @__PURE__ */ jsx17(WhatIfUserBubble, { parsed: whatifParsed }) }) });
4187
+ }
4188
+ return /* @__PURE__ */ jsx17("div", { className: cn("blade-chat-user-row flex justify-end", className), children: /* @__PURE__ */ jsxs15("div", { className: "blade-chat-user-col flex max-w-[72%] flex-col items-end gap-3", children: [
4189
+ imageParts.length > 0 && /* @__PURE__ */ jsx17("div", { className: "grid gap-2", children: imageParts.map((part) => /* @__PURE__ */ jsx17(
2789
4190
  "img",
2790
4191
  {
2791
4192
  src: part.image_url.url,
@@ -2794,21 +4195,21 @@ function UserMessageBubble({ message, className }) {
2794
4195
  },
2795
4196
  part.image_url.url
2796
4197
  )) }),
2797
- fileParts.length > 0 && /* @__PURE__ */ jsx14("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs12(
4198
+ fileParts.length > 0 && /* @__PURE__ */ jsx17("div", { className: "flex flex-col items-end gap-1.5", children: fileParts.map((part) => /* @__PURE__ */ jsxs15(
2798
4199
  "div",
2799
4200
  {
2800
4201
  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
4202
  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 })
4203
+ /* @__PURE__ */ jsx17(FileText, { size: 12, className: "shrink-0" }),
4204
+ /* @__PURE__ */ jsx17("span", { className: "max-w-56 truncate", title: part.name, children: part.name })
2804
4205
  ]
2805
4206
  },
2806
4207
  `${part.name}-${part.data.length}`
2807
4208
  )) }),
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" })
4209
+ text && /* @__PURE__ */ jsx17("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__ */ jsx17(MarkdownContent, { className: "blade-chat-prose", children: text }) }),
4210
+ text && isSending(message) && /* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-1 pr-1 text-[11px] font-medium text-[hsl(var(--muted-foreground))/0.85]", children: [
4211
+ /* @__PURE__ */ jsx17(LoaderCircle, { size: 11, className: "animate-spin", "aria-hidden": "true" }),
4212
+ /* @__PURE__ */ jsx17("span", { children: "\u53D1\u9001\u4E2D" })
2812
4213
  ] })
2813
4214
  ] }) });
2814
4215
  }
@@ -2816,12 +4217,12 @@ function ErrorMessageBlock({
2816
4217
  message,
2817
4218
  className
2818
4219
  }) {
2819
- const text = getTextContent2(message.content);
2820
- return /* @__PURE__ */ jsx14("div", { className: cn("blade-chat-error-row flex justify-center", className), children: /* @__PURE__ */ jsx14("div", { className: "blade-chat-error-block max-w-[85%] border-l-[3px] border-[hsl(var(--border))] px-4 py-1 text-sm leading-7 text-[hsl(var(--muted-foreground))]", children: text }) });
4220
+ const text = chatErrorForDisplay(getTextContent2(message.content));
4221
+ return /* @__PURE__ */ jsx17("div", { className: cn("blade-chat-error-row flex min-w-0 justify-start", className), children: /* @__PURE__ */ jsx17("div", { className: "blade-chat-error-block min-w-0 max-w-full whitespace-pre-wrap break-words border-l-[3px] border-[hsl(var(--border))] px-3 py-1 text-left text-sm leading-7 text-[hsl(var(--muted-foreground))] [overflow-wrap:anywhere]", children: text }) });
2821
4222
  }
2822
4223
 
2823
4224
  // src/components/MessageList.tsx
2824
- import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
4225
+ import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
2825
4226
  function parseModeChange(message) {
2826
4227
  if (message.kind !== "mode_change" || typeof message.content !== "string") {
2827
4228
  return null;
@@ -2862,18 +4263,30 @@ function MessageList({
2862
4263
  askAnswers,
2863
4264
  onAnswer,
2864
4265
  toolCallRenderer,
4266
+ hidePlanUpdateTools = false,
2865
4267
  emptyState,
2866
4268
  className,
2867
4269
  sessionId,
2868
4270
  isViewer = false,
2869
4271
  onFollowupInteraction,
2870
4272
  resultFeedbackByEntry = /* @__PURE__ */ new Map(),
2871
- onResultFeedbackSaved
4273
+ onResultFeedbackSaved,
4274
+ historyPaging
2872
4275
  }) {
4276
+ const visibleRootMessages = messages.filter((message) => {
4277
+ if ((message.loop_name ?? "root") !== "root") return false;
4278
+ if (isHiddenInternalMessage(message)) return false;
4279
+ if (message.kind === "context") return false;
4280
+ return message.role !== "tool" || getPlanningDividerKind(message) !== null;
4281
+ });
4282
+ const userMessages = visibleRootMessages.filter((message) => isUserMessage(message));
4283
+ const latestUserMessage = userMessages.at(-1);
4284
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null ? isStreaming : latestUserMessage.entry_id.startsWith("local-user-"));
2873
4285
  const renderBlocks = useMemo7(() => {
2874
4286
  const visible = messages.filter((message) => {
2875
4287
  if ((message.loop_name ?? "root") !== "root") return false;
2876
4288
  if (isHiddenInternalMessage(message)) return false;
4289
+ if (message.kind === "context") return false;
2877
4290
  if (message.kind === "compaction") return true;
2878
4291
  return message.role !== "tool" || getPlanningDividerKind(message) !== null;
2879
4292
  });
@@ -2916,7 +4329,7 @@ function MessageList({
2916
4329
  blocks.push({
2917
4330
  type: "message",
2918
4331
  message,
2919
- key: message.entry_id ?? `${message.role}-${blocks.length}`
4332
+ key: message.render_id ?? message.entry_id ?? `${message.role}-${blocks.length}`
2920
4333
  });
2921
4334
  }
2922
4335
  flushAssistant();
@@ -2943,98 +4356,228 @@ function MessageList({
2943
4356
  }
2944
4357
  return blocks;
2945
4358
  }, [messages, isStreaming]);
2946
- return /* @__PURE__ */ jsx15("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: /* @__PURE__ */ jsxs13(StickToBottom, { className: "h-full overflow-y-hidden", initial: "instant", resize: "instant", children: [
2947
- /* @__PURE__ */ jsx15(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsx15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: /* @__PURE__ */ jsxs13("div", { className: "flex min-w-0 flex-col", children: [
2948
- renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs13("div", { className: "blade-chat-empty", children: [
2949
- /* @__PURE__ */ jsx15(MessageSquare, { size: 40, strokeWidth: 1.5 }),
2950
- /* @__PURE__ */ jsx15("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
2951
- /* @__PURE__ */ jsx15("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
2952
- ] }) : renderBlocks.map((block) => {
2953
- if (block.type === "message") {
2954
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx15(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx15(ErrorMessageBlock, { message: block.message }) : null }, block.key);
2955
- }
2956
- if (block.type === "assistant_turn") {
2957
- const blockFeedback = block.messages.map(
2958
- (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
2959
- ).find((feedback) => feedback != null);
2960
- const hasActiveFollowup = Boolean(
2961
- postChatFollowup && block.messages.some(
2962
- (message) => message.entry_id === postChatFollowup.assistant_entry_id
2963
- )
2964
- );
2965
- return /* @__PURE__ */ jsx15("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs13(
2966
- RenderErrorBoundary,
2967
- {
2968
- label: "\u52A9\u624B\u6D88\u606F",
2969
- details: block.key,
2970
- resetKey: getMessageResetSignature(block.messages),
2971
- children: [
2972
- /* @__PURE__ */ jsx15(
2973
- AssistantTurnBlock,
2974
- {
2975
- messages: block.messages,
2976
- isStreaming: block.isStreaming,
2977
- askAnswers,
2978
- onAnswer,
2979
- sessionStatus,
2980
- toolCallRenderer,
2981
- sessionId
2982
- }
2983
- ),
2984
- blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx15(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
2985
- hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx15(
2986
- PostChatFollowupBlock,
2987
- {
2988
- followup: postChatFollowup,
2989
- sessionId,
2990
- onSuggestion,
2991
- isViewer,
2992
- onInteraction: onFollowupInteraction,
2993
- savedFeedback: blockFeedback,
2994
- onFeedbackSaved: onResultFeedbackSaved
2995
- }
2996
- ) : null
2997
- ]
2998
- }
2999
- ) }, block.key);
3000
- }
3001
- if (block.type === "compaction") {
3002
- return /* @__PURE__ */ jsxs13(
3003
- "div",
4359
+ return /* @__PURE__ */ jsxs16("div", { className: cn("blade-chat-messages relative min-h-0 flex-1", className), children: [
4360
+ isStreaming ? /* @__PURE__ */ jsx18("output", { className: "sr-only", children: "\u6B63\u5728\u751F\u6210\u56DE\u590D" }) : null,
4361
+ /* @__PURE__ */ jsxs16(
4362
+ StickToBottom,
4363
+ {
4364
+ className: "h-full overflow-y-hidden",
4365
+ initial: "instant",
4366
+ resize: "instant",
4367
+ children: [
4368
+ /* @__PURE__ */ jsx18(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsxs16("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: [
4369
+ historyPaging && (historyPaging.hasOlder || historyPaging.loading) ? /* @__PURE__ */ jsx18(LoadOlderSentinel, { ...historyPaging }) : null,
4370
+ /* @__PURE__ */ jsxs16("div", { className: "flex min-w-0 flex-col", children: [
4371
+ renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs16("div", { className: "blade-chat-empty", children: [
4372
+ /* @__PURE__ */ jsx18(MessageSquare, { size: 40, strokeWidth: 1.5 }),
4373
+ /* @__PURE__ */ jsx18("span", { className: "text-base font-medium", children: "\u5F00\u59CB\u5BF9\u8BDD" }),
4374
+ /* @__PURE__ */ jsx18("span", { className: "text-sm opacity-60", children: "\u5728\u4E0B\u65B9\u8F93\u5165\u6D88\u606F\u5F00\u59CB\u804A\u5929" })
4375
+ ] }) : renderBlocks.map((block) => {
4376
+ if (block.type === "message") {
4377
+ return /* @__PURE__ */ jsx18("div", { "data-entry-id": block.message.entry_id, children: isUserMessage(block.message) ? /* @__PURE__ */ jsx18(UserMessageBubble, { message: block.message }) : isErrorMessage(block.message) ? /* @__PURE__ */ jsx18(ErrorMessageBlock, { message: block.message }) : null }, block.key);
4378
+ }
4379
+ if (block.type === "assistant_turn") {
4380
+ const blockFeedback = block.messages.map(
4381
+ (message) => message.entry_id ? resultFeedbackByEntry.get(message.entry_id) : void 0
4382
+ ).find((feedback) => feedback != null);
4383
+ const hasActiveFollowup = Boolean(
4384
+ postChatFollowup && block.messages.some(
4385
+ (message) => message.entry_id === postChatFollowup.assistant_entry_id
4386
+ )
4387
+ );
4388
+ return /* @__PURE__ */ jsx18("div", { "data-entry-id": block.messages[0]?.entry_id, children: /* @__PURE__ */ jsxs16(
4389
+ RenderErrorBoundary,
4390
+ {
4391
+ label: "\u52A9\u624B\u6D88\u606F",
4392
+ details: block.key,
4393
+ resetKey: getMessageResetSignature(block.messages),
4394
+ children: [
4395
+ /* @__PURE__ */ jsx18(
4396
+ AssistantTurnBlock,
4397
+ {
4398
+ messages: block.messages,
4399
+ isStreaming: block.isStreaming,
4400
+ askAnswers,
4401
+ onAnswer,
4402
+ sessionStatus,
4403
+ toolCallRenderer,
4404
+ hidePlanUpdateTools,
4405
+ sessionId
4406
+ }
4407
+ ),
4408
+ blockFeedback && !hasActiveFollowup ? /* @__PURE__ */ jsx18(HistoricalResultFeedback, { feedback: blockFeedback }) : null,
4409
+ hasActiveFollowup && postChatFollowup ? /* @__PURE__ */ jsx18(
4410
+ PostChatFollowupBlock,
4411
+ {
4412
+ followup: postChatFollowup,
4413
+ sessionId,
4414
+ onSuggestion,
4415
+ isViewer,
4416
+ onInteraction: onFollowupInteraction,
4417
+ savedFeedback: blockFeedback,
4418
+ onFeedbackSaved: onResultFeedbackSaved
4419
+ }
4420
+ ) : null
4421
+ ]
4422
+ }
4423
+ ) }, block.key);
4424
+ }
4425
+ if (block.type === "compaction") {
4426
+ return /* @__PURE__ */ jsxs16(
4427
+ "div",
4428
+ {
4429
+ className: "flex items-center gap-2 text-xs text-[hsl(var(--muted-foreground))]",
4430
+ children: [
4431
+ /* @__PURE__ */ jsx18(Layers, { size: 12 }),
4432
+ /* @__PURE__ */ jsx18("span", { children: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29" })
4433
+ ]
4434
+ },
4435
+ block.key
4436
+ );
4437
+ }
4438
+ return /* @__PURE__ */ jsx18(PlanningDivider, { kind: block.kind }, block.key);
4439
+ }),
4440
+ sessionStatus === "interrupted" && !isStreaming ? /* @__PURE__ */ jsx18("div", { className: "flex", children: /* @__PURE__ */ jsx18("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
4441
+ ] })
4442
+ ] }) }),
4443
+ /* @__PURE__ */ jsx18(
4444
+ PinLatestUserMessage,
3004
4445
  {
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
- ]
4446
+ userMessageCount: userMessages.length,
4447
+ shouldPinLatestUser,
4448
+ isStreaming,
4449
+ targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
3010
4450
  },
3011
- block.key
3012
- );
4451
+ sessionId ?? "no-session"
4452
+ ),
4453
+ /* @__PURE__ */ jsx18(ScrollToBottomButton, {})
4454
+ ]
4455
+ },
4456
+ sessionId ?? "no-session"
4457
+ )
4458
+ ] });
4459
+ }
4460
+ function LoadOlderSentinel({
4461
+ hasOlder,
4462
+ loading,
4463
+ loadOlder
4464
+ }) {
4465
+ const { contentRef, scrollRef } = useStickToBottomContext();
4466
+ const sentinelRef = useRef13(null);
4467
+ const stateRef = useRef13({ hasOlder, loading, loadOlder });
4468
+ stateRef.current = { hasOlder, loading, loadOlder };
4469
+ useEffect12(() => {
4470
+ const sentinel = sentinelRef.current;
4471
+ const scroller = scrollRef.current;
4472
+ if (!sentinel || !scroller || typeof IntersectionObserver === "undefined") return;
4473
+ let entered = false;
4474
+ let disposed = false;
4475
+ const firstVisible = () => {
4476
+ const top = scroller.getBoundingClientRect().top;
4477
+ for (const row of contentRef.current?.querySelectorAll("[data-entry-id]") ?? []) {
4478
+ const rect = row.getBoundingClientRect();
4479
+ if (rect.bottom >= top) return { id: row.dataset.entryId, offset: rect.top - top };
4480
+ }
4481
+ return null;
4482
+ };
4483
+ const load = async () => {
4484
+ if (disposed || !stateRef.current.hasOlder || stateRef.current.loading) return;
4485
+ const commitSnapshot = { anchor: null, height: scroller.scrollHeight };
4486
+ let advanced = false;
4487
+ try {
4488
+ advanced = await stateRef.current.loadOlder(() => {
4489
+ commitSnapshot.anchor = firstVisible();
4490
+ commitSnapshot.height = scroller.scrollHeight;
4491
+ });
4492
+ } catch {
4493
+ return;
4494
+ }
4495
+ if (disposed) return;
4496
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
4497
+ if (disposed) return;
4498
+ const anchor = commitSnapshot.anchor;
4499
+ const heightBefore = commitSnapshot.height;
4500
+ const anchored = anchor?.id ? Array.from(contentRef.current?.querySelectorAll("[data-entry-id]") ?? []).find((row) => row.dataset.entryId === anchor.id) : null;
4501
+ if (anchored && anchor) {
4502
+ scroller.scrollTop += anchored.getBoundingClientRect().top - scroller.getBoundingClientRect().top - anchor.offset;
4503
+ } else {
4504
+ scroller.scrollTop += scroller.scrollHeight - heightBefore;
4505
+ }
4506
+ if (!disposed && advanced && stateRef.current.hasOlder && !stateRef.current.loading && scroller.scrollHeight <= scroller.clientHeight) {
4507
+ await load();
4508
+ }
4509
+ };
4510
+ const observer = new IntersectionObserver(
4511
+ ([entry]) => {
4512
+ if (!entry?.isIntersecting) {
4513
+ entered = false;
4514
+ return;
3013
4515
  }
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
- ] }) });
4516
+ if (entered) return;
4517
+ entered = true;
4518
+ void load();
4519
+ },
4520
+ { root: scroller, rootMargin: "160px 0px 0px" }
4521
+ );
4522
+ observer.observe(sentinel);
4523
+ return () => {
4524
+ disposed = true;
4525
+ observer.disconnect();
4526
+ };
4527
+ }, [contentRef, scrollRef]);
4528
+ return /* @__PURE__ */ jsx18("div", { ref: sentinelRef, "data-history-sentinel": true, className: "flex h-6 items-center justify-center", children: loading ? /* @__PURE__ */ jsx18("span", { className: "text-xs text-[hsl(var(--muted-foreground))]", children: "\u6B63\u5728\u52A0\u8F7D\u66F4\u65E9\u6D88\u606F\u2026" }) : null });
3021
4529
  }
3022
- function AutoScrollOnUserSend({ userMessageCount }) {
3023
- const { scrollToBottom } = useStickToBottomContext();
3024
- const previousCountRef = useRef7(userMessageCount);
3025
- useEffect7(() => {
3026
- if (userMessageCount > previousCountRef.current) {
4530
+ function PinLatestUserMessage({
4531
+ userMessageCount,
4532
+ shouldPinLatestUser,
4533
+ isStreaming,
4534
+ targetKey
4535
+ }) {
4536
+ const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
4537
+ const previousCountRef = useRef13(userMessageCount);
4538
+ const spacerHeightRef = useRef13(0);
4539
+ const getScrollElement = useCallback7(() => scrollRef.current, [scrollRef]);
4540
+ const getContentElement = useCallback7(() => contentRef.current, [contentRef]);
4541
+ const getTargetElement = useCallback7(() => {
4542
+ const rows = contentRef.current?.querySelectorAll(".blade-chat-user-row");
4543
+ return rows?.item((rows?.length ?? 0) - 1) ?? null;
4544
+ }, [contentRef]);
4545
+ const getSpacerHeight = useCallback7(() => spacerHeightRef.current, []);
4546
+ const setSpacerHeight = useCallback7(
4547
+ (height) => {
4548
+ spacerHeightRef.current = height;
4549
+ const content = contentRef.current;
4550
+ if (!content) return;
4551
+ if (height > 0) content.style.setProperty("--blade-chat-pin-spacer", `${height}px`);
4552
+ else content.style.removeProperty("--blade-chat-pin-spacer");
4553
+ },
4554
+ [contentRef]
4555
+ );
4556
+ useMessagePin({
4557
+ targetKey,
4558
+ pinTarget: shouldPinLatestUser,
4559
+ isStreaming,
4560
+ getScrollElement,
4561
+ getContentElement,
4562
+ getTargetElement,
4563
+ getSpacerHeight,
4564
+ setSpacerHeight,
4565
+ stopAutoScroll: stopScroll,
4566
+ scrollToBottom
4567
+ });
4568
+ useEffect12(() => {
4569
+ if (userMessageCount > previousCountRef.current && !shouldPinLatestUser) {
3027
4570
  scrollToBottom("instant");
3028
4571
  }
3029
4572
  previousCountRef.current = userMessageCount;
3030
- }, [userMessageCount, scrollToBottom]);
4573
+ }, [scrollToBottom, shouldPinLatestUser, userMessageCount]);
3031
4574
  return null;
3032
4575
  }
3033
4576
  function ScrollToBottomButton() {
3034
4577
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
3035
- const [visible, setVisible] = useState11(false);
3036
- const hideTimerRef = useRef7(null);
3037
- useEffect7(() => {
4578
+ const [visible, setVisible] = useState15(false);
4579
+ const hideTimerRef = useRef13(null);
4580
+ useEffect12(() => {
3038
4581
  if (isAtBottom) {
3039
4582
  if (!hideTimerRef.current) {
3040
4583
  hideTimerRef.current = setTimeout(() => {
@@ -3056,7 +4599,7 @@ function ScrollToBottomButton() {
3056
4599
  }
3057
4600
  };
3058
4601
  }, [isAtBottom]);
3059
- const handleClick = useCallback6(() => {
4602
+ const handleClick = useCallback7(() => {
3060
4603
  if (hideTimerRef.current) {
3061
4604
  clearTimeout(hideTimerRef.current);
3062
4605
  hideTimerRef.current = null;
@@ -3065,7 +4608,7 @@ function ScrollToBottomButton() {
3065
4608
  scrollToBottom();
3066
4609
  }, [scrollToBottom]);
3067
4610
  if (!visible) return null;
3068
- return /* @__PURE__ */ jsxs13(
4611
+ return /* @__PURE__ */ jsxs16(
3069
4612
  "button",
3070
4613
  {
3071
4614
  type: "button",
@@ -3073,25 +4616,25 @@ function ScrollToBottomButton() {
3073
4616
  "aria-label": "\u6EDA\u52A8\u5230\u5E95\u90E8",
3074
4617
  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
4618
  children: [
3076
- /* @__PURE__ */ jsx15(ChevronDown, { size: 14 }),
3077
- /* @__PURE__ */ jsx15("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
4619
+ /* @__PURE__ */ jsx18(ChevronDown, { size: 14 }),
4620
+ /* @__PURE__ */ jsx18("span", { className: "blade-chat-scroll-bottom-label", children: "\u6EDA\u52A8\u5230\u5E95\u90E8" })
3078
4621
  ]
3079
4622
  }
3080
4623
  );
3081
4624
  }
3082
4625
  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" })
4626
+ return /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-3 py-1", children: [
4627
+ /* @__PURE__ */ jsx18("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" }),
4628
+ /* @__PURE__ */ jsxs16("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: [
4629
+ /* @__PURE__ */ jsx18(Lightbulb, { size: 12 }),
4630
+ /* @__PURE__ */ jsx18("span", { children: kind === "enter" ? "\u8FDB\u5165\u89C4\u5212\u6A21\u5F0F" : "\u89C4\u5212\u5B8C\u6210" })
3088
4631
  ] }),
3089
- /* @__PURE__ */ jsx15("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
4632
+ /* @__PURE__ */ jsx18("div", { className: "h-px flex-1 bg-gradient-to-r from-transparent via-amber-400/40 to-transparent" })
3090
4633
  ] });
3091
4634
  }
3092
4635
 
3093
4636
  // src/components/ChatSurface.tsx
3094
- import { jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
4637
+ import { jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
3095
4638
  function themeAttr(theme) {
3096
4639
  return theme === "dark" ? "dark" : void 0;
3097
4640
  }
@@ -3111,6 +4654,7 @@ function ChatSurface({
3111
4654
  onInputChange,
3112
4655
  onSuggestion,
3113
4656
  onSend,
4657
+ onAppend,
3114
4658
  onStop,
3115
4659
  sessionStatus,
3116
4660
  askAnswers,
@@ -3121,9 +4665,12 @@ function ChatSurface({
3121
4665
  onResultFeedbackSaved,
3122
4666
  onFollowupInteraction,
3123
4667
  beforeInput,
4668
+ showPlanUpdates = false,
4669
+ planRevealRevision = 0,
4670
+ historyPaging,
3124
4671
  banner
3125
4672
  }) {
3126
- return /* @__PURE__ */ jsxs14(
4673
+ return /* @__PURE__ */ jsxs17(
3127
4674
  "div",
3128
4675
  {
3129
4676
  "data-theme": themeAttr(theme),
@@ -3132,14 +4679,14 @@ function ChatSurface({
3132
4679
  classNames?.root
3133
4680
  ),
3134
4681
  children: [
3135
- /* @__PURE__ */ jsx16(ConnectionBanner, { connection, className: classNames?.banner }),
4682
+ /* @__PURE__ */ jsx19(ConnectionBanner, { connection, className: classNames?.banner }),
3136
4683
  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 })
4684
+ errorMessage && /* @__PURE__ */ jsxs17("div", { className: "blade-chat-error-bar flex items-start gap-2 border-b px-4 py-3 text-sm", children: [
4685
+ /* @__PURE__ */ jsx19(CircleAlert, { size: 16, className: "mt-0.5 shrink-0" }),
4686
+ /* @__PURE__ */ jsx19("span", { className: "min-w-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: chatErrorForDisplay2(errorMessage) })
3140
4687
  ] }),
3141
4688
  slots?.header,
3142
- /* @__PURE__ */ jsx16(
4689
+ /* @__PURE__ */ jsx19(
3143
4690
  MessageList,
3144
4691
  {
3145
4692
  messages,
@@ -3150,26 +4697,40 @@ function ChatSurface({
3150
4697
  askAnswers,
3151
4698
  onAnswer,
3152
4699
  toolCallRenderer: renderers?.toolCall,
4700
+ hidePlanUpdateTools: showPlanUpdates,
3153
4701
  emptyState: slots?.emptyState,
3154
4702
  className: classNames?.messageList,
3155
4703
  sessionId,
3156
4704
  isViewer,
3157
4705
  resultFeedbackByEntry,
3158
4706
  onResultFeedbackSaved,
3159
- onFollowupInteraction
4707
+ onFollowupInteraction,
4708
+ historyPaging
3160
4709
  }
3161
4710
  ),
4711
+ showPlanUpdates ? /* @__PURE__ */ jsx19(
4712
+ CurrentPlanPanel,
4713
+ {
4714
+ messages,
4715
+ running: isStreaming,
4716
+ revealRevision: planRevealRevision,
4717
+ sessionId,
4718
+ className: "border-t border-[hsl(var(--border))]"
4719
+ }
4720
+ ) : null,
3162
4721
  beforeInput,
3163
- /* @__PURE__ */ jsx16(
4722
+ /* @__PURE__ */ jsx19(
3164
4723
  ChatInput,
3165
4724
  {
3166
4725
  value: inputText,
3167
4726
  onValueChange: onInputChange,
3168
4727
  onSend,
4728
+ onAppend,
3169
4729
  onStop,
3170
4730
  isStreaming,
3171
4731
  isStopping,
3172
4732
  placeholder,
4733
+ queueKey: sessionId,
3173
4734
  className: classNames?.chatInput
3174
4735
  }
3175
4736
  ),
@@ -3180,13 +4741,13 @@ function ChatSurface({
3180
4741
  }
3181
4742
 
3182
4743
  // src/components/AgentChat.tsx
3183
- import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs15 } from "react/jsx-runtime";
4744
+ import { Fragment as Fragment4, jsx as jsx20, jsxs as jsxs18 } from "react/jsx-runtime";
3184
4745
  function isUnauthorizedError(error) {
3185
4746
  return error instanceof BladeApiError && error.status === 401;
3186
4747
  }
3187
4748
  function LoginCard({ client, onLoggedIn }) {
3188
- const [loggingIn, setLoggingIn] = useState12(false);
3189
- const [loginError, setLoginError] = useState12(null);
4749
+ const [loggingIn, setLoggingIn] = useState16(false);
4750
+ const [loginError, setLoginError] = useState16(null);
3190
4751
  const handleLogin = async () => {
3191
4752
  setLoggingIn(true);
3192
4753
  setLoginError(null);
@@ -3199,11 +4760,11 @@ function LoginCard({ client, onLoggedIn }) {
3199
4760
  setLoggingIn(false);
3200
4761
  }
3201
4762
  };
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(
4763
+ return /* @__PURE__ */ jsx20("div", { className: "blade-chat-login flex flex-1 items-center justify-center p-6", children: /* @__PURE__ */ jsxs18("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: [
4764
+ /* @__PURE__ */ jsx20(LockKeyhole, { size: 28, className: "text-[hsl(var(--muted-foreground))]" }),
4765
+ /* @__PURE__ */ jsx20("div", { className: "text-base font-medium text-[hsl(var(--foreground))]", children: "\u9700\u8981\u767B\u5F55\u540E\u4F7F\u7528" }),
4766
+ /* @__PURE__ */ jsx20("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" }),
4767
+ /* @__PURE__ */ jsx20(
3207
4768
  "button",
3208
4769
  {
3209
4770
  type: "button",
@@ -3213,20 +4774,20 @@ function LoginCard({ client, onLoggedIn }) {
3213
4774
  children: loggingIn ? "\u767B\u5F55\u4E2D\u2026" : "\u767B\u5F55"
3214
4775
  }
3215
4776
  ),
3216
- loginError && /* @__PURE__ */ jsx17("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
4777
+ loginError && /* @__PURE__ */ jsx20("div", { className: "text-xs text-[hsl(var(--destructive))]", children: loginError })
3217
4778
  ] }) });
3218
4779
  }
3219
4780
  function AgentChat(props) {
3220
4781
  const client = useBladeClient();
3221
- const [attempt, setAttempt] = useState12(0);
3222
- const [needLogin, setNeedLogin] = useState12(() => !client.hasToken());
4782
+ const [attempt, setAttempt] = useState16(0);
4783
+ const [needLogin, setNeedLogin] = useState16(() => !client.hasToken());
3223
4784
  if (needLogin) {
3224
- return /* @__PURE__ */ jsx17(
4785
+ return /* @__PURE__ */ jsx20(
3225
4786
  "div",
3226
4787
  {
3227
4788
  "data-theme": themeAttr(props.theme),
3228
4789
  className: cn("blade-chat flex min-h-0 flex-1 flex-col", props.classNames?.root),
3229
- children: /* @__PURE__ */ jsx17(
4790
+ children: /* @__PURE__ */ jsx20(
3230
4791
  LoginCard,
3231
4792
  {
3232
4793
  client,
@@ -3239,7 +4800,7 @@ function AgentChat(props) {
3239
4800
  }
3240
4801
  );
3241
4802
  }
3242
- return /* @__PURE__ */ jsx17(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
4803
+ return /* @__PURE__ */ jsx20(ChatSessionView, { ...props, onUnauthorized: () => setNeedLogin(true) }, attempt);
3243
4804
  }
3244
4805
  function ChatSessionView({
3245
4806
  sessionId,
@@ -3253,20 +4814,43 @@ function ChatSessionView({
3253
4814
  placeholder,
3254
4815
  theme,
3255
4816
  onFollowupInteraction,
4817
+ sessionPluginControls,
3256
4818
  onUnauthorized
3257
4819
  }) {
3258
4820
  const client = useBladeClient();
4821
+ const [planRevealRevisions, setPlanRevealRevisions] = useState16(
4822
+ () => /* @__PURE__ */ new Map()
4823
+ );
4824
+ const handleSessionConnected = useCallback8((connectedSession) => {
4825
+ return connectedSession.on("toolResult", ({ toolCall, turn, source }) => {
4826
+ if (source === "reconnect_replay" || (turn.loop_id || "root") !== "root" || toolCall.status !== "done" || !isPlanUpdateTool(toolCall) || !parsePlanUpdate(toolCall.arguments)) {
4827
+ return;
4828
+ }
4829
+ setPlanRevealRevisions((current) => {
4830
+ const next = new Map(current);
4831
+ next.set(connectedSession.sessionId, (current.get(connectedSession.sessionId) ?? 0) + 1);
4832
+ return next;
4833
+ });
4834
+ });
4835
+ }, []);
3259
4836
  const { session, state, error } = useAgentSession(sessionId, {
3260
4837
  createOptions,
3261
- onSessionCreated
4838
+ onSessionCreated,
4839
+ onSessionConnected: handleSessionConnected
3262
4840
  });
3263
4841
  const replay = useReplay(session);
3264
- const [stopRequested, setStopRequested] = useState12(false);
3265
- const [inputText, setInputText] = useState12("");
3266
- const [resultFeedback, setResultFeedback] = useState12([]);
4842
+ const [stopRequested, setStopRequested] = useState16(false);
4843
+ const [inputText, setInputText] = useState16("");
4844
+ const [resultFeedback, setResultFeedback] = useState16([]);
3267
4845
  const resolvedSessionId = session?.sessionId;
3268
4846
  const isViewer = state?.viewerRole === "viewer";
3269
- useEffect8(() => {
4847
+ const onSessionReadyRef = useRef14(onSessionReady);
4848
+ const readySessionRef = useRef14(null);
4849
+ const hasOnSessionReady = onSessionReady !== void 0;
4850
+ useEffect13(() => {
4851
+ onSessionReadyRef.current = onSessionReady;
4852
+ }, [onSessionReady]);
4853
+ useEffect13(() => {
3270
4854
  setResultFeedback([]);
3271
4855
  if (!resolvedSessionId || isViewer) return;
3272
4856
  let cancelled = false;
@@ -3295,18 +4879,19 @@ function ChatSessionView({
3295
4879
  () => new Map(resultFeedback.map((item) => [item.assistant_entry_id, item])),
3296
4880
  [resultFeedback]
3297
4881
  );
3298
- const handleResultFeedbackSaved = useCallback7((saved) => {
4882
+ const handleResultFeedbackSaved = useCallback8((saved) => {
3299
4883
  setResultFeedback((current) => [
3300
4884
  ...current.filter((item) => item.assistant_entry_id !== saved.assistant_entry_id),
3301
4885
  saved
3302
4886
  ]);
3303
4887
  }, []);
3304
- useEffect8(() => {
3305
- if (session) {
3306
- onSessionReady?.(session);
3307
- }
3308
- }, [session, onSessionReady]);
3309
- useEffect8(() => {
4888
+ useEffect13(() => {
4889
+ const handler = onSessionReadyRef.current;
4890
+ if (!session || !hasOnSessionReady || !handler || readySessionRef.current === session) return;
4891
+ readySessionRef.current = session;
4892
+ handler(session);
4893
+ }, [session, hasOnSessionReady]);
4894
+ useEffect13(() => {
3310
4895
  if (!session) return;
3311
4896
  const offAttach = session.on("attachRequested", ({ label, content }) => {
3312
4897
  setInputText((prev) => `${prev ? `${prev}
@@ -3322,12 +4907,12 @@ ${content}`);
3322
4907
  offInsert();
3323
4908
  };
3324
4909
  }, [session]);
3325
- useEffect8(() => {
4910
+ useEffect13(() => {
3326
4911
  if (isUnauthorizedError(error)) {
3327
4912
  onUnauthorized();
3328
4913
  }
3329
4914
  }, [error, onUnauthorized]);
3330
- useEffect8(() => {
4915
+ useEffect13(() => {
3331
4916
  if (!session || !commands) return;
3332
4917
  const unsubscribes = Object.entries(commands).map(
3333
4918
  ([action, handler]) => session.onCommand(action, (payload) => handler(payload))
@@ -3337,6 +4922,7 @@ ${content}`);
3337
4922
  };
3338
4923
  }, [session, commands]);
3339
4924
  const isStreaming = state?.isStreaming ?? false;
4925
+ const planRevealRevision = resolvedSessionId ? planRevealRevisions.get(resolvedSessionId) ?? 0 : 0;
3340
4926
  const isStopping = stopRequested && isStreaming;
3341
4927
  const connectError = error && !isUnauthorizedError(error) ? error.message || "\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" : null;
3342
4928
  const errorMessage = connectError ?? state?.errorMessage ?? replay.error?.message ?? null;
@@ -3348,7 +4934,7 @@ ${content}`);
3348
4934
  setStopRequested(true);
3349
4935
  void session?.stop();
3350
4936
  };
3351
- return /* @__PURE__ */ jsx17(
4937
+ return /* @__PURE__ */ jsx20(
3352
4938
  ChatSurface,
3353
4939
  {
3354
4940
  theme,
@@ -3358,8 +4944,9 @@ ${content}`);
3358
4944
  slots,
3359
4945
  placeholder,
3360
4946
  connection: state?.connection ?? "connecting",
3361
- banner: /* @__PURE__ */ jsxs15(Fragment3, { children: [
3362
- /* @__PURE__ */ jsx17(
4947
+ banner: /* @__PURE__ */ jsxs18(Fragment4, { children: [
4948
+ resolvedSessionId && !isViewer && (sessionPluginControls ? sessionPluginControls(resolvedSessionId) : /* @__PURE__ */ jsx20(SessionPluginSelector, { client, sessionId: resolvedSessionId })),
4949
+ /* @__PURE__ */ jsx20(
3363
4950
  ReplayBar,
3364
4951
  {
3365
4952
  isReplay: replay.isReplay,
@@ -3369,7 +4956,7 @@ ${content}`);
3369
4956
  onExit: () => void replay.exitToAutonomous()
3370
4957
  }
3371
4958
  ),
3372
- /* @__PURE__ */ jsx17(ReplayMismatchPrompt, { mismatch: replay.mismatch })
4959
+ /* @__PURE__ */ jsx20(ReplayMismatchPrompt, { mismatch: replay.mismatch })
3373
4960
  ] }),
3374
4961
  errorMessage,
3375
4962
  messages: state?.messages ?? [],
@@ -3377,11 +4964,25 @@ ${content}`);
3377
4964
  resultFeedbackByEntry,
3378
4965
  onResultFeedbackSaved: handleResultFeedbackSaved,
3379
4966
  isStreaming,
4967
+ showPlanUpdates: true,
4968
+ planRevealRevision,
4969
+ historyPaging: session && state ? {
4970
+ hasOlder: session.hasOlderHistory,
4971
+ loading: state.loadingOlder,
4972
+ loadOlder: async (beforeCommit) => {
4973
+ const before = session.getState().nextBefore;
4974
+ await session.loadOlderHistory({ beforeCommit });
4975
+ return before !== session.getState().nextBefore;
4976
+ }
4977
+ } : void 0,
3380
4978
  isStopping,
3381
4979
  inputText,
3382
4980
  onInputChange: setInputText,
3383
4981
  onSuggestion: setInputText,
3384
4982
  onSend: handleSend,
4983
+ onAppend: (text) => {
4984
+ void session?.queue(text);
4985
+ },
3385
4986
  onStop: handleStop,
3386
4987
  sessionStatus: state?.status ?? void 0,
3387
4988
  askAnswers: state?.askAnswers,
@@ -3398,11 +4999,11 @@ ${content}`);
3398
4999
  }
3399
5000
 
3400
5001
  // src/components/LlmChat.tsx
3401
- import { useEffect as useEffect9, useMemo as useMemo9, useState as useState14 } from "react";
5002
+ import { useEffect as useEffect14, useMemo as useMemo9, useState as useState18 } from "react";
3402
5003
 
3403
5004
  // src/components/LlmAdvancedSettings.tsx
3404
- import { useState as useState13 } from "react";
3405
- import { jsx as jsx18, jsxs as jsxs16 } from "react/jsx-runtime";
5005
+ import { useState as useState17 } from "react";
5006
+ import { jsx as jsx21, jsxs as jsxs19 } from "react/jsx-runtime";
3406
5007
  var FIELDS = [
3407
5008
  { id: "baseURL", label: "\u6A21\u578B\u670D\u52A1\u5730\u5740", placeholder: "http://\u5185\u7F51\u5730\u5740/v1" },
3408
5009
  { id: "model", label: "\u6A21\u578B", placeholder: "\u6A21\u578B\u540D\u79F0" },
@@ -3448,13 +5049,13 @@ function writeOverride(settings, baseURL, override) {
3448
5049
  }
3449
5050
  function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3450
5051
  const normalized = normalizeAdvanced(settings);
3451
- const [open, setOpen] = useState13(false);
3452
- const [draft, setDraft] = useState13(override);
5052
+ const [open, setOpen] = useState17(false);
5053
+ const [draft, setDraft] = useState17(override);
3453
5054
  if (!normalized) return null;
3454
5055
  const fields = FIELDS.filter((field) => normalized[field.id]);
3455
5056
  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(
5057
+ return /* @__PURE__ */ jsxs19("div", { className: "blade-chat-advanced border-t border-[hsl(var(--border))] px-4 py-2 text-xs", children: [
5058
+ /* @__PURE__ */ jsxs19(
3458
5059
  "button",
3459
5060
  {
3460
5061
  type: "button",
@@ -3464,16 +5065,16 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3464
5065
  },
3465
5066
  className: "flex items-center gap-1.5 text-[hsl(var(--muted-foreground))] transition-colors hover:text-[hsl(var(--foreground))]",
3466
5067
  children: [
3467
- /* @__PURE__ */ jsx18(Settings2, { size: 13 }),
5068
+ /* @__PURE__ */ jsx21(Settings2, { size: 13 }),
3468
5069
  "\u9AD8\u7EA7\u8BBE\u7F6E",
3469
- dirty && /* @__PURE__ */ jsx18("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
5070
+ dirty && /* @__PURE__ */ jsx21("span", { className: "text-[hsl(var(--primary))]", children: "\uFF08\u5DF2\u81EA\u5B9A\u4E49\uFF09" })
3470
5071
  ]
3471
5072
  }
3472
5073
  ),
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(
5074
+ open && /* @__PURE__ */ jsxs19("div", { className: "mt-2 flex flex-col gap-2", children: [
5075
+ fields.map((field) => /* @__PURE__ */ jsxs19("label", { className: "flex flex-col gap-1", children: [
5076
+ /* @__PURE__ */ jsx21("span", { className: "text-[hsl(var(--muted-foreground))]", children: field.label }),
5077
+ /* @__PURE__ */ jsx21(
3477
5078
  "input",
3478
5079
  {
3479
5080
  type: field.secret ? "password" : "text",
@@ -3484,9 +5085,9 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3484
5085
  }
3485
5086
  )
3486
5087
  ] }, 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(
5088
+ normalized.apiKey && /* @__PURE__ */ jsx21("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" }),
5089
+ /* @__PURE__ */ jsxs19("div", { className: "flex gap-2", children: [
5090
+ /* @__PURE__ */ jsx21(
3490
5091
  "button",
3491
5092
  {
3492
5093
  type: "button",
@@ -3501,7 +5102,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3501
5102
  children: "\u4FDD\u5B58"
3502
5103
  }
3503
5104
  ),
3504
- /* @__PURE__ */ jsx18(
5105
+ /* @__PURE__ */ jsx21(
3505
5106
  "button",
3506
5107
  {
3507
5108
  type: "button",
@@ -3520,7 +5121,7 @@ function LlmAdvancedSettingsBar({ settings, defaults, override, onChange }) {
3520
5121
  }
3521
5122
 
3522
5123
  // src/components/LlmChat.tsx
3523
- import { jsx as jsx19 } from "react/jsx-runtime";
5124
+ import { jsx as jsx22 } from "react/jsx-runtime";
3524
5125
  function LlmChat({
3525
5126
  classNames,
3526
5127
  renderers,
@@ -3532,11 +5133,11 @@ function LlmChat({
3532
5133
  onOverrideChange,
3533
5134
  ...options
3534
5135
  }) {
3535
- const [override, setOverride] = useState14(() => readOverride(advanced, options.baseURL));
5136
+ const [override, setOverride] = useState18(() => readOverride(advanced, options.baseURL));
3536
5137
  const effective = { ...options, ...override };
3537
5138
  const { messages, isStreaming, error, send, stop, reset } = useLlmChat(effective);
3538
- const [inputText, setInputText] = useState14("");
3539
- const [stopRequested, setStopRequested] = useState14(false);
5139
+ const [inputText, setInputText] = useState18("");
5140
+ const [stopRequested, setStopRequested] = useState18(false);
3540
5141
  const handle = useMemo9(
3541
5142
  () => ({
3542
5143
  insertText: (text) => setInputText((prev) => prev ? `${prev}
@@ -3546,10 +5147,10 @@ ${text}` : text),
3546
5147
  }),
3547
5148
  [send, reset]
3548
5149
  );
3549
- useEffect9(() => {
5150
+ useEffect14(() => {
3550
5151
  onReady?.(handle);
3551
5152
  }, [handle, onReady]);
3552
- return /* @__PURE__ */ jsx19(
5153
+ return /* @__PURE__ */ jsx22(
3553
5154
  ChatSurface,
3554
5155
  {
3555
5156
  theme,
@@ -3574,7 +5175,7 @@ ${text}` : text),
3574
5175
  setStopRequested(true);
3575
5176
  stop();
3576
5177
  },
3577
- beforeInput: advanced ? /* @__PURE__ */ jsx19(
5178
+ beforeInput: advanced ? /* @__PURE__ */ jsx22(
3578
5179
  LlmAdvancedSettingsBar,
3579
5180
  {
3580
5181
  settings: advanced,
@@ -3592,14 +5193,14 @@ ${text}` : text),
3592
5193
  }
3593
5194
 
3594
5195
  // src/components/ChatView.tsx
3595
- import { jsx as jsx20 } from "react/jsx-runtime";
5196
+ import { jsx as jsx23 } from "react/jsx-runtime";
3596
5197
  function ChatView(props) {
3597
5198
  const { mode, llm, onLlmReady, ...rest } = props;
3598
5199
  if (mode === "llm") {
3599
5200
  if (!llm) {
3600
5201
  throw new Error('ChatView: mode="llm" \u9700\u8981\u540C\u65F6\u4F20 llm={{ baseURL, model }}');
3601
5202
  }
3602
- return /* @__PURE__ */ jsx20(
5203
+ return /* @__PURE__ */ jsx23(
3603
5204
  LlmChat,
3604
5205
  {
3605
5206
  ...llm,
@@ -3612,7 +5213,232 @@ function ChatView(props) {
3612
5213
  }
3613
5214
  );
3614
5215
  }
3615
- return /* @__PURE__ */ jsx20(AgentChat, { ...rest });
5216
+ return /* @__PURE__ */ jsx23(AgentChat, { ...rest });
5217
+ }
5218
+
5219
+ // src/components/ContextCard.tsx
5220
+ import {
5221
+ getContextDisplayState,
5222
+ getContextGroupDisplayState
5223
+ } from "@blade-hq/agent-client";
5224
+ import { jsx as jsx24, jsxs as jsxs20 } from "react/jsx-runtime";
5225
+ function ContextCard({ context, className }) {
5226
+ const display = getContextDisplayState(context);
5227
+ return /* @__PURE__ */ jsxs20(
5228
+ "details",
5229
+ {
5230
+ className: `blade-chat-context-card group/context-card rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`,
5231
+ children: [
5232
+ /* @__PURE__ */ jsxs20("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: [
5233
+ /* @__PURE__ */ jsx24(
5234
+ Layers,
5235
+ {
5236
+ size: 15,
5237
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5238
+ "aria-hidden": "true"
5239
+ }
5240
+ ),
5241
+ /* @__PURE__ */ jsxs20("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5242
+ /* @__PURE__ */ jsx24("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: display.title }),
5243
+ /* @__PURE__ */ jsx24("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: display.summary })
5244
+ ] }),
5245
+ /* @__PURE__ */ jsx24(
5246
+ ChevronDown,
5247
+ {
5248
+ size: 14,
5249
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-card:rotate-180",
5250
+ "aria-hidden": "true"
5251
+ }
5252
+ )
5253
+ ] }),
5254
+ /* @__PURE__ */ jsx24("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 })
5255
+ ]
5256
+ }
5257
+ );
5258
+ }
5259
+ function ContextGroupCard({ contexts, className }) {
5260
+ if (contexts.length === 0) return null;
5261
+ const single = contexts.length === 1 ? getContextDisplayState(contexts[0]) : null;
5262
+ const group = single ? null : getContextGroupDisplayState(contexts);
5263
+ return /* @__PURE__ */ jsxs20("details", { className: `blade-chat-context-card group/context-group rounded-xl border border-[hsl(var(--border))] bg-[hsl(var(--card))] text-sm ${className ?? ""}`, children: [
5264
+ /* @__PURE__ */ jsxs20("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: [
5265
+ /* @__PURE__ */ jsx24(
5266
+ Layers,
5267
+ {
5268
+ size: 15,
5269
+ className: "blade-chat-context-icon shrink-0 text-[hsl(var(--muted-foreground))]",
5270
+ "aria-hidden": "true"
5271
+ }
5272
+ ),
5273
+ /* @__PURE__ */ jsxs20("span", { className: "blade-chat-context-copy min-w-0 flex-1", children: [
5274
+ /* @__PURE__ */ jsx24("span", { className: "blade-chat-context-title block font-medium text-[hsl(var(--foreground))]", children: single ? single.title : `${group?.title} \xB7 ${group?.count} \u9879` }),
5275
+ /* @__PURE__ */ jsx24("span", { className: "blade-chat-context-status block truncate text-xs text-[hsl(var(--muted-foreground))]", children: single ? single.summary : group?.summary })
5276
+ ] }),
5277
+ /* @__PURE__ */ jsx24(
5278
+ ChevronDown,
5279
+ {
5280
+ size: 14,
5281
+ className: "blade-chat-context-chevron shrink-0 text-[hsl(var(--muted-foreground))] transition-transform group-open/context-group:rotate-180",
5282
+ "aria-hidden": "true"
5283
+ }
5284
+ )
5285
+ ] }),
5286
+ single ? /* @__PURE__ */ jsx24("div", { className: "blade-chat-context-detail border-t border-[hsl(var(--border))] px-3 py-2.5 text-xs leading-5 text-[hsl(var(--muted-foreground))]", children: single.detail }) : /* @__PURE__ */ jsx24("div", { className: "blade-chat-context-group-items flex flex-col gap-1.5 border-t border-[hsl(var(--border))] p-2", children: contexts.map((context) => /* @__PURE__ */ jsx24(
5287
+ ContextCard,
5288
+ {
5289
+ context
5290
+ },
5291
+ `${context.context_kind}:${context.context_key}`
5292
+ )) })
5293
+ ] });
5294
+ }
5295
+
5296
+ // src/components/SessionMemoryToggle.tsx
5297
+ import { useCallback as useCallback9, useEffect as useEffect15, useRef as useRef15, useState as useState19, useSyncExternalStore as useSyncExternalStore2 } from "react";
5298
+ import { jsx as jsx25, jsxs as jsxs21 } from "react/jsx-runtime";
5299
+ var saveStates = /* @__PURE__ */ new WeakMap();
5300
+ function getSaveState(client, sessionId) {
5301
+ let clientStates = saveStates.get(client);
5302
+ if (!clientStates) {
5303
+ clientStates = /* @__PURE__ */ new Map();
5304
+ saveStates.set(client, clientStates);
5305
+ }
5306
+ let state = clientStates.get(sessionId);
5307
+ if (!state) {
5308
+ state = { saving: false, listeners: /* @__PURE__ */ new Set() };
5309
+ clientStates.set(sessionId, state);
5310
+ }
5311
+ return state;
5312
+ }
5313
+ function notify(state) {
5314
+ for (const listener of state.listeners) listener();
5315
+ }
5316
+ function cleanupSaveState(client, sessionId, state) {
5317
+ if (state.saving || state.listeners.size > 0) return;
5318
+ const clientStates = saveStates.get(client);
5319
+ if (clientStates?.get(sessionId) === state) clientStates.delete(sessionId);
5320
+ }
5321
+ function SessionMemoryToggle({
5322
+ sessionId,
5323
+ enabled,
5324
+ client: clientProp,
5325
+ disabled = false,
5326
+ label = "\u5F53\u524D\u4F1A\u8BDD\u4F7F\u7528\u8BB0\u5FC6",
5327
+ className,
5328
+ labelClassName,
5329
+ inputClassName,
5330
+ onSaved,
5331
+ onError
5332
+ }) {
5333
+ const contextClient = useOptionalBladeClient();
5334
+ const client = clientProp ?? contextClient;
5335
+ if (!client) {
5336
+ throw new Error("SessionMemoryToggle \u5FC5\u987B\u5728 <BladeProvider> \u5185\u4F7F\u7528\u6216\u663E\u5F0F\u4F20\u5165 client");
5337
+ }
5338
+ const saveState = getSaveState(client, sessionId);
5339
+ const subscribe = useCallback9(
5340
+ (listener) => {
5341
+ saveState.listeners.add(listener);
5342
+ return () => {
5343
+ saveState.listeners.delete(listener);
5344
+ cleanupSaveState(client, sessionId, saveState);
5345
+ };
5346
+ },
5347
+ [client, saveState, sessionId]
5348
+ );
5349
+ const getSaving = useCallback9(() => saveState.saving, [saveState]);
5350
+ const saving = useSyncExternalStore2(
5351
+ subscribe,
5352
+ getSaving,
5353
+ getSaving
5354
+ );
5355
+ const [draftEnabled, setDraftEnabled] = useState19(enabled);
5356
+ const activeSessionIdRef = useRef15(sessionId);
5357
+ activeSessionIdRef.current = sessionId;
5358
+ useEffect15(() => {
5359
+ setDraftEnabled(enabled);
5360
+ }, [enabled, sessionId]);
5361
+ const update = useCallback9(
5362
+ (nextEnabled) => {
5363
+ const currentSaveState = getSaveState(client, sessionId);
5364
+ if (currentSaveState.saving) return;
5365
+ currentSaveState.saving = true;
5366
+ notify(currentSaveState);
5367
+ setDraftEnabled(nextEnabled);
5368
+ void client.sessions.updateSessionMemory(sessionId, nextEnabled).then(
5369
+ (updated) => {
5370
+ if (activeSessionIdRef.current === sessionId) {
5371
+ setDraftEnabled(updated.memory_enabled);
5372
+ }
5373
+ onSaved?.(sessionId, updated.memory_enabled);
5374
+ },
5375
+ (error) => {
5376
+ if (activeSessionIdRef.current === sessionId) setDraftEnabled(enabled);
5377
+ onError?.(error);
5378
+ }
5379
+ ).finally(() => {
5380
+ currentSaveState.saving = false;
5381
+ notify(currentSaveState);
5382
+ cleanupSaveState(client, sessionId, currentSaveState);
5383
+ });
5384
+ },
5385
+ [client, enabled, onError, onSaved, sessionId]
5386
+ );
5387
+ return /* @__PURE__ */ jsxs21("label", { className: cn("flex items-center justify-between", className), children: [
5388
+ /* @__PURE__ */ jsx25("span", { className: labelClassName, children: label }),
5389
+ /* @__PURE__ */ jsx25(
5390
+ "input",
5391
+ {
5392
+ type: "checkbox",
5393
+ checked: draftEnabled,
5394
+ onChange: (event) => update(event.target.checked),
5395
+ disabled: disabled || saving,
5396
+ className: inputClassName
5397
+ }
5398
+ )
5399
+ ] });
5400
+ }
5401
+
5402
+ // src/lib/agent-computer-command.ts
5403
+ var COMPUTER_LAUNCH_COMMAND_PATTERN = /(?:^|[\n;&|(]\s*)computer\s+launch(?:\s|$)/;
5404
+ function isAgentComputerCommand(command) {
5405
+ return COMPUTER_LAUNCH_COMMAND_PATTERN.test(command);
5406
+ }
5407
+ function isAgentComputerToolCall(argumentsJson) {
5408
+ if (!argumentsJson) return false;
5409
+ let command;
5410
+ try {
5411
+ const parsed = JSON.parse(argumentsJson);
5412
+ if (typeof parsed !== "object" || parsed === null) return false;
5413
+ command = parsed.command;
5414
+ } catch {
5415
+ return false;
5416
+ }
5417
+ return typeof command === "string" && isAgentComputerCommand(command);
5418
+ }
5419
+ var LAUNCH_SUCCESS_MARKER = "\u5DF2\u542F\u52A8 ";
5420
+ function resultContainsLaunchSuccessMarker(result, depth = 0) {
5421
+ if (depth > 2) return false;
5422
+ if (typeof result === "string") {
5423
+ if (result.includes(LAUNCH_SUCCESS_MARKER)) return true;
5424
+ try {
5425
+ return resultContainsLaunchSuccessMarker(JSON.parse(result), depth + 1);
5426
+ } catch {
5427
+ return false;
5428
+ }
5429
+ }
5430
+ if (typeof result === "object" && result !== null) {
5431
+ for (const value of Object.values(result)) {
5432
+ if (typeof value === "string" && value.includes(LAUNCH_SUCCESS_MARKER)) return true;
5433
+ }
5434
+ }
5435
+ return false;
5436
+ }
5437
+ function classifyAgentComputerLaunchOutcome(toolCall) {
5438
+ if (toolCall.status === "error" || toolCall.status === "cancelled") return "failed";
5439
+ if (toolCall.status !== "done") return "pending";
5440
+ if (toolCall.result === void 0 || toolCall.result === null) return "unknown";
5441
+ return resultContainsLaunchSuccessMarker(toolCall.result) ? "succeeded" : "failed";
3616
5442
  }
3617
5443
 
3618
5444
  // src/index.ts
@@ -3621,13 +5447,33 @@ export {
3621
5447
  AgentChat,
3622
5448
  BladeProvider,
3623
5449
  ChatView,
5450
+ ContextCard,
5451
+ ContextGroupCard,
5452
+ CurrentPlanPanel,
3624
5453
  LlmChat,
3625
5454
  MarkdownContent,
5455
+ MemoryRefsHint,
5456
+ PLAN_AUTO_COLLAPSE_MS,
5457
+ PlanUpdateBlock,
3626
5458
  ReplayBar,
3627
5459
  ReplayMismatchPrompt,
5460
+ SessionMemoryToggle,
5461
+ SessionPluginSelector,
5462
+ WhatIfUserBubble,
5463
+ classifyAgentComputerLaunchOutcome,
5464
+ collectMemoryRefs,
5465
+ getPlanUpdateDisplayState,
5466
+ isAgentComputerCommand,
5467
+ isAgentComputerToolCall,
5468
+ isPlanUpdateTool,
5469
+ normalizeAdjacentUrlFormatting,
5470
+ parsePlanUpdate,
5471
+ parseWhatIfPrompt,
5472
+ pickCurrentPlanStep,
3628
5473
  useAgentSession,
3629
5474
  useBladeClient,
3630
5475
  useLlmChat,
5476
+ useMessagePin,
3631
5477
  useReplay
3632
5478
  };
3633
5479
  /*! Bundled license information:
@@ -3639,29 +5485,35 @@ lucide-react/dist/esm/createLucideIcon.js:
3639
5485
  lucide-react/dist/esm/icons/arrow-right.js:
3640
5486
  lucide-react/dist/esm/icons/arrow-up-right.js:
3641
5487
  lucide-react/dist/esm/icons/arrow-up.js:
5488
+ lucide-react/dist/esm/icons/book-open.js:
3642
5489
  lucide-react/dist/esm/icons/bot.js:
3643
- lucide-react/dist/esm/icons/brain.js:
3644
5490
  lucide-react/dist/esm/icons/check.js:
3645
5491
  lucide-react/dist/esm/icons/chevron-down.js:
3646
5492
  lucide-react/dist/esm/icons/chevron-right.js:
3647
5493
  lucide-react/dist/esm/icons/circle-alert.js:
5494
+ lucide-react/dist/esm/icons/circle-dot.js:
5495
+ lucide-react/dist/esm/icons/circle.js:
3648
5496
  lucide-react/dist/esm/icons/copy.js:
3649
- lucide-react/dist/esm/icons/download.js:
5497
+ lucide-react/dist/esm/icons/earth.js:
5498
+ lucide-react/dist/esm/icons/file-pen-line.js:
3650
5499
  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
5500
  lucide-react/dist/esm/icons/globe.js:
3654
5501
  lucide-react/dist/esm/icons/layers.js:
3655
5502
  lucide-react/dist/esm/icons/lightbulb.js:
5503
+ lucide-react/dist/esm/icons/list-checks.js:
3656
5504
  lucide-react/dist/esm/icons/loader-circle.js:
3657
5505
  lucide-react/dist/esm/icons/lock-keyhole.js:
3658
5506
  lucide-react/dist/esm/icons/message-square-more.js:
3659
5507
  lucide-react/dist/esm/icons/message-square.js:
3660
5508
  lucide-react/dist/esm/icons/play.js:
5509
+ lucide-react/dist/esm/icons/refresh-ccw.js:
5510
+ lucide-react/dist/esm/icons/search.js:
3661
5511
  lucide-react/dist/esm/icons/settings-2.js:
3662
5512
  lucide-react/dist/esm/icons/sparkles.js:
3663
5513
  lucide-react/dist/esm/icons/square.js:
5514
+ lucide-react/dist/esm/icons/terminal.js:
3664
5515
  lucide-react/dist/esm/icons/triangle-alert.js:
5516
+ lucide-react/dist/esm/icons/wrench.js:
3665
5517
  lucide-react/dist/esm/icons/x.js:
3666
5518
  lucide-react/dist/esm/lucide-react.js:
3667
5519
  (**