@blade-hq/agent-react 2610.0.0-beta.45 → 2610.0.0-beta.47

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/README.md CHANGED
@@ -208,6 +208,9 @@ Blade。这时把 `ChatView` 切到 `mode="llm"`,或者直接用下层的 `Llm
208
208
  <ChatView mode="llm" llm={{ baseURL: "/api/llm", model }} /> // 切成纯 LLM
209
209
  <LlmChat baseURL="/api/llm" model={model} /> // 或者直接用(LlmChatProps)
210
210
  <AgentChat sessionId={id} /> // 智能体那侧同样可以直接用(AgentChatProps)
211
+
212
+ `AgentChat` 还支持 `sessionPluginControls(sessionId)` 插槽,宿主可复用
213
+ `@blade-hq/agent-client` 的会话插件状态与激活控制。
211
214
  ```
212
215
 
213
216
  **协议就是 OpenAI 的**:`POST {baseURL}/chat/completions`,`stream: true`,读
@@ -1,4 +1,5 @@
1
1
  import type { AgentSession, CreateSessionRequest } from "@blade-hq/agent-client";
2
+ import { type ReactNode } from "react";
2
3
  import { type ChatPresentationProps } from "./ChatSurface";
3
4
  import type { FollowupInteractionEvent } from "./PostChatFollowupBlock";
4
5
  export interface AgentChatProps extends ChatPresentationProps {
@@ -13,6 +14,8 @@ export interface AgentChatProps extends ChatPresentationProps {
13
14
  onSessionReady?: (session: AgentSession) => void;
14
15
  /** 智能体下发给宿主页面的指令处理器(action → handler)。 */
15
16
  commands?: Record<string, (data: unknown) => void>;
17
+ /** 会话插件状态与激活控件;由宿主注入以复用共享会话 API。 */
18
+ sessionPluginControls?: (sessionId: string) => ReactNode;
16
19
  }
17
20
  /**
18
21
  * 智能体模式的聊天界面:连接(或新建)一个 Blade 会话,渲染消息流与输入框。
@@ -1,6 +1,12 @@
1
1
  export interface UseMessagePinOptions {
2
2
  targetKey: string | null;
3
3
  pinTarget: boolean;
4
+ /**
5
+ * 省略时按旧行为处理(视为一直流式中,只靠 targetKey 变化释放)——
6
+ * useMessagePin 是导出的公开 hook,加这个字段前如果直接改成必填,
7
+ * 所有已经在调用这个 hook 的外部使用方都会编译不过。
8
+ */
9
+ isStreaming?: boolean;
4
10
  layoutKey?: string;
5
11
  getScrollElement: () => HTMLElement | null;
6
12
  getContentElement?: () => HTMLElement | null;
@@ -16,7 +22,7 @@ export interface UseMessagePinOptions {
16
22
  * 用户发言顶置的共享状态机。SDK 和内置轻量列表只提供 DOM/虚拟列表适配器,
17
23
  * 触发、调度、resize、手动接管和跟随交还的决策只在这里维护。
18
24
  */
19
- export declare function useMessagePin({ targetKey, pinTarget, layoutKey, getScrollElement, getContentElement, getTargetElement, getTargetScrollTop, getSpacerHeight, setSpacerHeight, stopAutoScroll, scrollToBottom, margin, }: UseMessagePinOptions): {
25
+ export declare function useMessagePin({ targetKey, pinTarget, isStreaming, layoutKey, getScrollElement, getContentElement, getTargetElement, getTargetScrollTop, getSpacerHeight, setSpacerHeight, stopAutoScroll, scrollToBottom, margin, }: UseMessagePinOptions): {
20
26
  release: () => void;
21
27
  isActive: () => boolean;
22
28
  };
package/dist/index.js CHANGED
@@ -617,6 +617,7 @@ var MANUAL_SCROLL_TOLERANCE_PX = 2;
617
617
  function useMessagePin({
618
618
  targetKey,
619
619
  pinTarget,
620
+ isStreaming = true,
620
621
  layoutKey,
621
622
  getScrollElement,
622
623
  getContentElement,
@@ -625,6 +626,10 @@ function useMessagePin({
625
626
  getSpacerHeight,
626
627
  setSpacerHeight,
627
628
  stopAutoScroll,
629
+ // 不再内部调用(见下面 reposition 里的说明),但这是公开的 SDK hook 参数,
630
+ // 删掉会让已经传这个 prop 的调用方在 TS 严格类型下编译不过——留着接口
631
+ // 不动,只是这里不再用它。
632
+ // biome-ignore lint/correctness/noUnusedVariables: 保留公开 API 形状
628
633
  scrollToBottom,
629
634
  margin = DEFAULT_MARGIN_PX
630
635
  }) {
@@ -674,7 +679,6 @@ function useMessagePin({
674
679
  scroll.scrollTop = targetScrollTop;
675
680
  if (nextSpacerHeight === 0 && previousSpacerHeight > 0) {
676
681
  pinActiveRef.current = false;
677
- scrollToBottom();
678
682
  }
679
683
  },
680
684
  [
@@ -683,7 +687,6 @@ function useMessagePin({
683
687
  getTargetElement,
684
688
  getTargetScrollTop,
685
689
  margin,
686
- scrollToBottom,
687
690
  stopAutoScroll
688
691
  ]
689
692
  );
@@ -709,7 +712,7 @@ function useMessagePin({
709
712
  return;
710
713
  }
711
714
  if (!pinTarget) {
712
- if (pinActiveRef.current && appliedTargetKeyRef.current != null && appliedTargetKeyRef.current !== targetKey) {
715
+ if (pinActiveRef.current && appliedTargetKeyRef.current != null && (appliedTargetKeyRef.current !== targetKey || !isStreaming)) {
713
716
  release();
714
717
  }
715
718
  observedTargetKeyRef.current = targetKey;
@@ -730,7 +733,7 @@ function useMessagePin({
730
733
  retryTimeoutRef.current = null;
731
734
  scheduleReposition();
732
735
  }, 80);
733
- }, [pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
736
+ }, [isStreaming, pinTarget, release, scheduleReposition, stopAutoScroll, targetKey]);
734
737
  useEffect3(() => {
735
738
  if (layoutKey !== void 0) scheduleReposition();
736
739
  }, [layoutKey, scheduleReposition]);
@@ -4140,9 +4143,7 @@ function MessageList({
4140
4143
  });
4141
4144
  const userMessages = visibleRootMessages.filter((message) => isUserMessage(message));
4142
4145
  const latestUserMessage = userMessages.at(-1);
4143
- const latestPromptText = latestUserMessage ? (typeof latestUserMessage.content === "string" ? latestUserMessage.content : latestUserMessage.content.filter((part) => part.type === "text").map((part) => part.text).join("")).replace(/\s+/g, " ").trim() : "";
4144
- const latestPromptPreview = latestPromptText.length > 80 ? `${latestPromptText.slice(0, 80)}\u2026` : latestPromptText;
4145
- const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null || latestUserMessage.entry_id.startsWith("local-user-"));
4146
+ const shouldPinLatestUser = latestUserMessage != null && (latestUserMessage.entry_id == null ? isStreaming : latestUserMessage.entry_id.startsWith("local-user-"));
4146
4147
  const renderBlocks = useMemo7(() => {
4147
4148
  const visible = messages.filter((message) => {
4148
4149
  if ((message.loop_name ?? "root") !== "root") return false;
@@ -4228,16 +4229,6 @@ function MessageList({
4228
4229
  children: [
4229
4230
  /* @__PURE__ */ jsx17(StickToBottom.Content, { className: "blade-chat-messages-scroll", children: /* @__PURE__ */ jsxs15("div", { className: "blade-chat-messages-content mx-auto max-w-[748px]", children: [
4230
4231
  historyPaging && (historyPaging.hasOlder || historyPaging.loading) ? /* @__PURE__ */ jsx17(LoadOlderSentinel, { ...historyPaging }) : null,
4231
- isStreaming && latestUserMessage && latestPromptPreview ? /* @__PURE__ */ jsx17(
4232
- "button",
4233
- {
4234
- type: "button",
4235
- className: "sticky top-0 z-20 mb-4 w-full truncate rounded-2xl border border-[hsl(var(--primary)/0.2)] bg-[hsl(var(--background)/0.92)] px-4 py-3 text-left text-sm font-medium shadow-sm backdrop-blur",
4236
- onClick: () => Array.from(document.querySelectorAll("[data-entry-id]")).find((el) => el.dataset.entryId === latestUserMessage.entry_id)?.scrollIntoView({ behavior: "smooth", block: "start" }),
4237
- "aria-label": "\u8DF3\u8F6C\u5230\u5F53\u524D\u63D0\u95EE",
4238
- children: latestPromptPreview
4239
- }
4240
- ) : null,
4241
4232
  /* @__PURE__ */ jsxs15("div", { className: "flex min-w-0 flex-col", children: [
4242
4233
  renderBlocks.length === 0 ? emptyState ?? /* @__PURE__ */ jsxs15("div", { className: "blade-chat-empty", children: [
4243
4234
  /* @__PURE__ */ jsx17(MessageSquare, { size: 40, strokeWidth: 1.5 }),
@@ -4316,6 +4307,7 @@ function MessageList({
4316
4307
  {
4317
4308
  userMessageCount: userMessages.length,
4318
4309
  shouldPinLatestUser,
4310
+ isStreaming,
4319
4311
  targetKey: latestUserMessage?.render_id ?? latestUserMessage?.entry_id ?? (latestUserMessage ? `user:${userMessages.length}` : null)
4320
4312
  },
4321
4313
  sessionId ?? "no-session"
@@ -4400,6 +4392,7 @@ function LoadOlderSentinel({
4400
4392
  function PinLatestUserMessage({
4401
4393
  userMessageCount,
4402
4394
  shouldPinLatestUser,
4395
+ isStreaming,
4403
4396
  targetKey
4404
4397
  }) {
4405
4398
  const { contentRef, scrollRef, scrollToBottom, stopScroll } = useStickToBottomContext();
@@ -4425,6 +4418,7 @@ function PinLatestUserMessage({
4425
4418
  useMessagePin({
4426
4419
  targetKey,
4427
4420
  pinTarget: shouldPinLatestUser,
4421
+ isStreaming,
4428
4422
  getScrollElement,
4429
4423
  getContentElement,
4430
4424
  getTargetElement,
@@ -4682,6 +4676,7 @@ function ChatSessionView({
4682
4676
  placeholder,
4683
4677
  theme,
4684
4678
  onFollowupInteraction,
4679
+ sessionPluginControls,
4685
4680
  onUnauthorized
4686
4681
  }) {
4687
4682
  const client = useBladeClient();
@@ -4805,6 +4800,7 @@ ${content}`);
4805
4800
  placeholder,
4806
4801
  connection: state?.connection ?? "connecting",
4807
4802
  banner: /* @__PURE__ */ jsxs17(Fragment4, { children: [
4803
+ resolvedSessionId && !isViewer && sessionPluginControls?.(resolvedSessionId),
4808
4804
  /* @__PURE__ */ jsx19(
4809
4805
  ReplayBar,
4810
4806
  {