# @blade-hq/agent-react 中文文档

规范地址：https://npmcn.com/package/%40blade-hq/agent-react/

版本：2610\.0\.6
npm 元数据更新时间：2026-11-11T08:17:53.086Z
中文译文更新时间：2026-11-11T08:18:04.170Z
软件包许可：未声明

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40blade-hq/agent-react
原文对照：https://npmcn.com/package/%40blade-hq/agent-react/?lang=original
项目主页：https://github.com/blade-hq/blade-agent#readme
源码仓库：https://github.com/blade-hq/blade-agent

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：9.2 MB，79 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：1 个
- peer 依赖：react ^18\.0\.0 \|\| ^19\.0\.0，react-dom ^18\.0\.0 \|\| ^19\.0\.0
- 首次发布：2026-07-21T08:34:48.582Z
- 最新版本发布：2026-11-11T08:17:52.803Z
- 近 30 天下载量：9759（2026-10-09 至 2026-11-07，npm 官方下载统计）
- 已发布版本数：246（近 12 个月 246 个）

## 中文简介

Blade Agent React @ @ @:Blade Provider, us Assession @ ChatView @ @ @。

## 安装

```sh
npm install @blade-hq/agent-react
```

也可以使用：`pnpm add @blade-hq/agent-react`、`yarn add @blade-hq/agent-react`、`bun add @blade-hq/agent-react`

## README

# @blade-hq/agent-react

Blade Agent 的 React 绑定：`BladeProvider` + `useAgentSession` + 开箱即用的 `ChatView` 聊天界面。支持 React 18 与 19（CI 两个版本 matrix 验证）。

```bash
pnpm add @blade-hq/agent-client @blade-hq/agent-react
```

默认装到的是当前长期支持版（LTS），厂内离线交付按它开发。要跟两周一发的公网版本，改用 `@next`。

最稳妥的做法是先读目标 Server 的 `GET /api/version`，按返回的版本号在 `package.json` 里钉死——NPM 的 `latest` 不一定和目标环境跑的版本一致。

> 注意两个包都要装：`agent-client` 是运行时依赖，只装 `agent-react` 在 pnpm 下会直接报错。

## 快速开始

```tsx
import { BladeClient } from "@blade-hq/agent-client"
import { BladeProvider, ChatView } from "@blade-hq/agent-react"
import "@blade-hq/agent-react/style.full.css"

const client = new BladeClient({ baseUrl: "https://blade.example.com" })

export function App() {
  return (
    <BladeProvider client={client}>
      <ChatView />
    </BladeProvider>
  )
}
```

-   不传 `sessionId` 自动创建新会话；未登录时 `ChatView` 自己渲染登录按钮（弹窗授权，见 agent-client 的 `client.auth.login()`）。
-   同一 `BladeProvider` 下可以放多个 `ChatView`，各自独立会话、互不干扰。

## BladeProvider

只负责 client 级共享（一条 Socket 连接、一份鉴权），不含任何全局状态。

```tsx
<BladeProvider client={client}>{children}</BladeProvider>
```

Props 见 `BladeProviderProps`；在 Provider 外调用本包任何 hook 都会抛错。

## useBladeClient

```tsx
const client = useBladeClient() // 当前 Provider 的 BladeClient
```

## useAgentSession

自建 UI 的入口：连接（或创建）会话并订阅其状态，内部使用 `useSyncExternalStore`。

```tsx
const { session, state, error } = useAgentSession(sessionId, {
  createOptions,        // 不传 sessionId 时的建会话配置（UseAgentSessionOptions）
  onSessionCreated: (id) => saveSomewhere(id),
  onSessionConnected: (session) => session.on("toolResult", handleResult),
})

state?.messages     // ChatMessage[]，直接渲染
state?.isStreaming  // 是否正在流式回复
await session?.send("你好")
```

`send()` 在服务端接受消息后返回 `true`；整理中或其他冲突被拒绝时返回 `false`，内置 `ChatView` 会保留输入内容。

返回值见 `UseAgentSessionResult`。语义要点：

-   优先级：`sessionId`（连接既有会话）> `createOptions`（按配置新建）> 默认新建。
-   自动创建只发生一次（含 React StrictMode 双跑）；创建出的 id 通过 `onSessionCreated` 交还，重挂载时把它作为 `sessionId` 传回即可复用会话。
-   必须覆盖连接窗口内事件时，用 `onSessionConnected` 在历史加载和房间订阅开始前完成订阅，并返回取消订阅函数。
-   同一 `sessionId` 的多次调用复用同一 `AgentSession` 实例（引用计数）；卸载后延迟释放，路由抖动不会断连。

## SessionMemoryToggle

把当前会话是否使用记忆放进宿主自己的会话设置面板：

```tsx
const [memoryEnabled, setMemoryEnabled] = useState(session.memory_enabled !== false)

<SessionMemoryToggle
  sessionId={session.id}
  enabled={memoryEnabled}
  onSaved={(_sessionId, enabled) => setMemoryEnabled(enabled)}
  onError={(error) => toast.error(String(error))}
/>
```

组件默认使用 `BladeProvider` 的 client；已有独立 client 装配层的宿主也可以显式传 `client={client}`。同一 client、同一会话的多个开关共享保存锁，即使切换会话或同时存在多个 设置入口，也不会并发提交相互覆盖。服务端确认后的权威值通过 `onSaved` 返回。 全部属性见 `SessionMemoryToggleProps`。

## useReplay

会话回放（演示 / 彩排）：拿一个已经聊完的会话当素材，重现当时的回复和工具调用， **完全不调用模型**。

**进入回放是"派生一个新会话"，不是把当前会话切模式**：`startReplay()` 返回的是新会话 的 id，拿它去渲染（`<ChatView sessionId={newId} />` 或自己 `connect`）才进入回放。源会话 原样不动。连上之后也不会自动播——用户照着源会话的问题问，才逐句重现。

同一个 hook 覆盖两种身份——传进去的会话既可能是准备被回放的**源会话**，也可能是**回放中**的会话：

```tsx
// —— 源会话页面：提供"开始回放"入口 ——
const { session: sourceSession } = useAgentSession(sourceId)
const source = useReplay(sourceSession)

source.canReplay        // false 时 source.unsupportedReason 可直接展示
const replayId = await source.startReplay(5)   // 返回**新建的**回放会话 id，路由过去

// —— 回放会话页面：状态与控制 ——
const { session: replaySession } = useAgentSession(replayId)
const replay = useReplay(replaySession)

replay.isReplay         // 是不是在回放
replay.speed            // 当前倍速
replay.canControl       // 改回放是 owner 专属；分享出去的只读会话为 false
replay.mismatch         // 输入和录制对不上时的待决策项
await replay.setSpeed(2)
await replay.exitToAutonomous()                // 退出回放，之后真的运行
```

配套两个组件，直接接上 `useReplay()` 的返回值即可：

```tsx
<ReplayBar
  isReplay={replay.isReplay}
  speed={replay.speed}
  canControl={replay.canControl}
  onSpeedChange={(s) => void replay.setSpeed(s)}
  onExit={() => void replay.exitToAutonomous()}
/>
<ReplayMismatchPrompt mismatch={replay.mismatch} />
```

返回值见 `UseReplayResult`，冲突项见 `ReplayMismatch`，组件属性见 `ReplayBarProps` / `ReplayMismatchPromptProps`。

只读身份（分享链接打开的会话）仍然会显示「回放模式」——这是会话事实，藏起来会让分享出去的 回放看着像真实运行；但倍速和退出会被禁用，因为服务端的 `PATCH` 是 owner 专属。

**`ChatView` 已经自带这一整套**——用它打开回放会话，状态条、倍速、退出、冲突卡片都在， 不用自己接。下面这些 API 是给自建 UI 用的。

**注册即接管**：底层的 `replayMismatch` 只交给最先注册的处理器。调用 `useReplay()`（或用 `ChatView`）就意味着由它接管冲突；两者都不用则维持 SDK 默认行为——静默转为真实运行。

回放状态本身住在 `SessionState.replay` 里，本 hook 只是它的 React 绑定。非 React 宿主 （Vue、Svelte、自建 UI）直接订阅快照即可，不需要这个包，用法见 [agent-client 的会话回放章节](https://npmcn.com/package/%40blade-hq/agent-client/README.md#%E4%BC%9A%E8%AF%9D%E5%9B%9E%E6%94%BE%E6%BC%94%E7%A4%BA--%E5%BD%A9%E6%8E%92)。

## ChatView

完整聊天界面：消息列表（Markdown、代码高亮、工具调用、提问卡片）+ 输入框 + 连接状态条 + 登录引导。

`ChatView` 固定使用普通展示模式。产品内置 Web 的精简/开发者模式不属于 SDK 公共能力， 因此没有 `renderMode` 等切换属性。

```tsx
<ChatView
  sessionId={id}                       // 可选；不传自动建会话
  createOptions={{ intent: "..." }}    // 自动建会话时的配置
  onSessionCreated={(id) => ...}       // 自动创建的会话 id 交还调用方
  onSessionReady={(session) => ...}    // 会话实例就绪（页面协作高级用法）
  commands={{ "map.highlight": (data) => map.highlight(data) }}  // 智能体 → 页面指令
  onFollowupInteraction={(event) => analytics.capture(event.type, event)} // 厂商无关交互回调
  placeholder="输入消息..."
  classNames={{ root: "...", banner: "...", messages: "...", input: "..." }}  // ChatViewClassNames
  renderers={{ toolCall: (info) => <MyToolCard info={info} /> }}              // ChatViewRenderers
  slots={{ header: <MyHeader />, emptyState: <MyEmpty />, footer: <MyTip /> }} // ChatViewSlots
/>
```

全部 props 见 `ChatViewProps`。样式说明：

智能体调用 `update_plan` 新增或更新任务时，`ChatView` 会在输入框上方自动展开最新任务进度， 5 秒后平滑收起；连续更新会重新计时，用户手动展开或收起后以用户选择为准。自建聊天布局可直接 使用 `CurrentPlanPanel` / `PlanUpdateBlock`，或用 `getPlanUpdateDisplayState`、 `parsePlanUpdate` 和 `pickCurrentPlanStep` 复用同一套判定。

后台任务自动唤醒会话时，`ChatView` 显示简短的 `BackgroundWakeNotice`，说明助手为何继续处理。卡片由明确的后台来源标记生成，流式和历史加载一致；自定义消息列表可以复用该组件及 agent-client 的 `isBackgroundWakeMessage`，无需解析内部提示文本。

`ChatView` 的普通展示模式不显示只供诊断的项目说明、可用能力和当前工作环境。需要自建诊断界面时，可直接使用独立的 `ContextCard`，属性见 `ContextCardProps`；状态到文案的映射来自 agent-client 的 `getContextDisplayState`，不需要接入方重复判断。一次输入通常会同时注入十几项上下文，把渲染序列里连续的几项用 `ContextGroupCard`（属性见 `ContextGroupCardProps`）折成一行更合适；哪几项算一组由 agent-client 的 `groupAdjacentContextRuns` 判定，折叠行文案来自 `getContextGroupDisplayState` / `ContextGroupDisplayState`。底层数据类型为 `ContextProjectionData` / `ContextProjectionFields`，展示结果为 `ContextDisplayState`，并保留 `ContextAction`、`ContextSourceInfo` 和 `contextProjectionData` 给 headless 消费方。

记忆引用提示可通过 `MemoryRefsHint` 展示，`collectMemoryRefs` 用于从一轮消息中聚合引用，保证宿主自定义消息布局与 SDK 默认界面保持一致。

MCP App 卡片（`tool_ui` block）在默认消息流里自动渲染，无需配置；自建消息布局时可用 `ToolUiCardView` 渲染单个卡片，并传入 `sessionId` 启用共享 AppBridge。卡片的实例、内容解析与终态（archived 留档不重放）判定来自 agent-client 的共享层（`collectInlineToolUiCards` / `resolveToolUiCardContent` 等）。preview 卡片不进消息流，由 `toolPreview` 事件推给宿主自建面板。 文件引用卡片（`resource-file`）只带 `sourcePath`，`ToolUiCardView` 在 `BladeProvider` 下由父页面带鉴权读取当前文件，再放入无令牌、无同源权限的 iframe 渲染；同一路径的新卡片不会自动重载当前页面，用户点击卡片上方的“刷新”按钮才重新读取。自建面板也应按路径去重、首次打开时读取，并保留用户操作中的页面直到手动刷新。

`onFollowupInteraction` 使用 `FollowupInteractionEvent`，覆盖下一步建议展示/采纳、成果展示/打开/下载以及结果评分。SDK 不内置分析服务；宿主回调抛错也不会中断用户点击或下载。 文件成果以文件名为文本的标准下载链接展示；Vue / 纯 HTML 使用的 `<blade-chat>` 与 `ChatView` 行为一致。

-   必须引入一份样式，按宿主有没有 Tailwind 二选一：
    -   **`style.full.css`**（默认选它）：CSS 变量、容器查询、Markdown 排版等兜底 + 编译好的 Tailwind 产物，自包含，宿主没装 Tailwind 也是完整视觉。
    -   **`style.css`**：只有兜底那部分（Tailwind 类表达不了的规则）。宿主自己装了 Tailwind、且 `content` 配了扫描 `node_modules/@blade-hq/agent-react` 时用它，可以少一份重复 CSS。选错不会报错，只是布局、配色、工具块和思考块会退化成裸 HTML——组件的视觉本来就只定义一次，都在它的 Tailwind 类上。
    -   两份都用同一套层序 `@layer properties, theme, base, components, utilities, blade-chat-overrides;`（在 `src/scoped-preflight.css` 里声明一次；`properties` 是 Tailwind 的 `--tw-*` 初始值层，必须排在最前）。兜底规则在 `components` 层；Tailwind 工具类在 `utilities` 层；SDK 里那几条**必须压过自己工具类**的规则（窄容器里放宽用户消息、移动端聚焦不缩放、Markdown 与代码块排版）在最后一层 `blade-chat-overrides`（名字带 SDK 前缀，避免宿主已有的同名层把全局层序定到别处）——放进 `components` 会被同元素的工具类盖掉，等于没写。**宿主写的普通 CSS 不分层，按层序赢过这里所有层，与加载顺序无关，也不需要 `!important`**：这是「接入方覆盖必胜」的实现方式，改 `src/style.css` 时不要把规则挪出这两层。
    -   两份都不含 Tailwind preflight 这类全局 reset，引入后不会影响宿主页面自己的排版；组件需要的那点重置限定在 `.blade-chat` 子树内（见 `src/scoped-preflight.css`）。Shadow DOM 版（`<blade-chat>`）不受此限，它注入的是带 preflight 的完整产物。
-   颜色走 CSS 变量，可整体换肤；深色页面要在根元素上写 `data-theme="dark"`，否则拿到的是默认的浅色变量。
-   `renderers.toolCall` 返回 `null` 时回落到默认工具卡片。
-   **内边距按容器宽度伸缩**，嵌进窄侧栏（300–400px）时会自动收窄，不看浏览器视口。代价是聊天容器需要有确定宽度——放在 flex/grid 里被拉伸，或显式给宽度都可以；放进 `width: fit-content`、`inline-flex` 这类由内容决定宽度的父容器会塌缩。

## 纯 LLM 模式（不建会话）

有些应用只要「一问一答」——分类、改写、总结，不需要智能体、不需要会话、也不需要用户登录 Blade。这时把 `ChatView` 切到 `mode="llm"`，或者直接用下层的 `LlmChat`，界面完全一样：

```tsx
<ChatView />                                                  // 默认：智能体模式，行为不变
<ChatView mode="llm" llm={{ baseURL: "/api/llm", model }} />   // 切成纯 LLM
<LlmChat baseURL="/api/llm" model={model} />                   // 或者直接用（LlmChatProps）
<AgentChat sessionId={id} />                                   // 智能体那侧同样可以直接用（AgentChatProps）

`AgentChat` 还支持 `sessionPluginControls(sessionId)` 插槽，宿主可复用
`@blade-hq/agent-client` 的会话插件状态与激活控制。
```

**协议就是 OpenAI 的**：`POST {baseURL}/chat/completions`，`stream: true`，读 `choices[0].delta`。任何 OpenAI 兼容实现（Blade 网关、vLLM、Ollama、OpenAI 本身、你自己的 透传代理）都能直接接，不用为这个组件专门写后端。

**密钥默认不进浏览器**：把 `baseURL` 指向自己后端的一个路径，后端补上 `Authorization` 再原样 转发即可（协议是标准的，这个代理就是纯转发）。确实要前端直连时才传 `apiKey`——只适合受控内网、 演示，或密钥本身就属于当前用户的场景。

**模型服务地址和模型名别写死**，用 `client.models.list()` 拿，各部署端口不一样：

```tsx
const { baseUrl, defaultServiceModel, models } = await client.models.list()

// 后端透传（推荐）：baseURL 指向自己的路径，模型名仍然要用 defaultServiceModel
<ChatView mode="llm" llm={{ baseURL: "/api/llm", model: defaultServiceModel }} />

// 浏览器直连：仅当 baseUrl 对浏览器也可达时
<ChatView mode="llm" llm={{ baseURL: baseUrl, model: defaultServiceModel, apiKey }} />

// 让用户选模型时，过滤掉不在这个服务上的
models.filter((m) => m.serviceModelId)
```

**模型名要用 `defaultServiceModel` / `serviceModelId`，不是 `default` / `id`。** 后者是平台内部 标识，平台接了多个模型服务时形如 `provider-xxx::deepseek-v4-flash`，发给模型服务只会得到 「模型不存在」。`baseUrl` 同理是服务端视角的地址，平台常配成 `http://127.0.0.1:30000/v1`， 那是给自己后端转发用的，不能直接丢给浏览器。

**工具调用走 OpenAI 标准的多轮流程**，不需要任何私有扩展：声明 `tools`，在 `onToolCall` （收到 `LlmToolCall`）里执行——通常就是调你自己应用的同源接口——返回值会作为 `role: "tool"` 消息回喂给模型，直到它给出最终回答或达到 `maxToolRounds`（默认 4）。 每一轮工具调用都会渲染成和智能体模式一样的工具卡片。

```tsx
<LlmChat
  baseURL="/api/llm"
  model={model}
  system="你是订单助手"
  tools={[{ type: "function", function: { name: "query_orders", parameters: schema } }]}  // ChatCompletionTool
  onToolCall={async ({ name, arguments: args }) => {
    if (name !== "query_orders") throw new Error(`没有这个函数：${name}`)
    // 参数是模型生成的不可信输入：先校验类型，再用 URLSearchParams 拼，
    // 直接塞进查询串的话，一个 `李航&limit=all` 就能多带出参数
    const { owner } = JSON.parse(args || "{}")
    if (typeof owner !== "string") throw new Error("owner 必须是字符串")
    const query = new URLSearchParams({ owner })
    const resp = await fetch(`/api/orders?${query}`)
    if (!resp.ok) throw new Error(`订单接口返回 ${resp.status}`)
    return resp.json()
  }}
  onReady={(handle) => setChat(handle)}   // LlmChatHandle：insertText / send / reset
/>
```

### 让用户自己改模型服务地址

传 `advanced` 就在输入框上方多一行「高级设置」（默认收起），用户可以改模型服务地址和模型名； 改过之后会显示「已自定义」，也能一键恢复默认。不传就一个节点都不渲染。

```tsx
<LlmChat baseURL="/api/llm" model={model} advanced />
<LlmChat baseURL="/api/llm" model={model} advanced={{ apiKey: true, storage: "memory" }} />
<ChatView mode="llm" llm={{ baseURL: "/api/llm", model, advanced: true }} />
```

`LlmAdvancedSettings` 可细调：`baseURL` / `model` 各自能不能改（默认都能）、`apiKey` 要不要 出现（**默认不出现**，打开意味着密钥存在浏览器里，只适合受控场景）、`storage` 是记住选择 （`local`，默认）还是只在本次有效（`memory`）、`storageKey` 自定义存储键。用户改了会回调 `onOverrideChange`（`LlmOverride`），宿主想自己也记一份时用。

其余可选项见 `LlmChatOptions`：`headers`、`historyTurns`（默认 12 轮）、`temperature`、 `extraBody`、`fetchImpl`。只要状态机、界面自己写的，用 `useLlmChat`（返回 `UseLlmChatResult`：`messages` / `isStreaming` / `error` / `send` / `stop` / `reset`）。

纯 LLM 模式不做会话持久化、文件上传、提问卡片、记忆和检查点——那些是智能体协议的能力， 需要就用默认模式。

## MarkdownContent

单独渲染一段智能体消息 Markdown（含代码复制按钮、XSS 消毒；HTML 经 sanitize 白名单处理，不接受任意 raw HTML）：

```tsx
<MarkdownContent content={markdownText} />
```

```tsx
<MarkdownContent sessionId={sessionId}>{markdownText}</MarkdownContent>
```

`normalizeAdjacentUrlFormatting` 可在复用其他 Markdown 渲染器前，保护紧贴行内格式标记的 URL。

## Canonical 引用消息

`ChatView` 会自动识别 ship-attack 会话中的 `[引用]` / `[用户输入]` canonical 消息，并按原顺序展示引用来源、快照和用户输入。自建消息列表时可用 `parseWhatIfPrompt` 解析同一格式，再交给 `WhatIfUserBubble` 渲染； `onQuoteClick` 可接入宿主的步骤跳转。

```tsx
const parsed = parseWhatIfPrompt(messageText)
return parsed ? (
  <WhatIfUserBubble parsed={parsed} onQuoteClick={jumpToStep} />
) : null
```

## isAgentComputerCommand / isAgentComputerToolCall / classifyAgentComputerLaunchOutcome

判断智能体是否在沙盒里启动了通用 GUI 应用镜像（`computer launch`）——后端 blade-agent 通过 `computer` CLI 驱动一块虚拟屏幕运行任意 GUI 软件，这些函数用于自行接入方判断要不要展示对应的实时画面入口（比如自建一个类似「电脑镜像」的标签页）。当前 SDK 侧还没有内置这块 UI，只导出判定规则，避免各接入方重新猜一遍匹配逻辑。

`isAgentComputerCommand`/`isAgentComputerToolCall` 只回答"这条命令是不是一次 launch 尝试"，不回答"这次 launch 最终成不成功"——待处理、已失败的 launch 也会命中。要判断真实结果（比如决定要不要展示画面入口），用 `classifyAgentComputerLaunchOutcome`：它会解析工具调用的 `status`/`result`，返回 `"pending" | "succeeded" | "failed" | "unknown"`。`"unknown"` 表示 `status` 已经是终态但 `result` 字段本身缺失（常见于会话整理/compaction 之后的历史投影，归档时不会重新序列化完整 result），跟"result 回来了、里面确实没有成功标记"的 `"failed"` 是两种不同性质的证据——接入方要按自己的场景决定怎么处理（展示类场景可以偏宽松，当作足够展示；自动跳转/替用户做决定的场景应该偏保守，当作跟 `"failed"` 一样处理）。

```ts
import {
  isAgentComputerCommand,
  isAgentComputerToolCall,
  classifyAgentComputerLaunchOutcome,
} from "@blade-hq/agent-react"

isAgentComputerCommand("computer launch --exec /opt/apps/foo/bar") // true
isAgentComputerCommand("computer status") // false（只认 launch，不认查询类子命令）

isAgentComputerToolCall(toolCall.arguments) // 工具调用的 JSON 参数版本

classifyAgentComputerLaunchOutcome(toolCall) // "pending" | "succeeded" | "failed" | "unknown"
```

## 选择模型

SDK 的 ChatView / `<blade-chat>` 不内置模型选择器（默认用后端配置的模型）。指定模型有两种方式：

-   **建会话时指定**（React）：`<ChatView createOptions={{ model: "..." }} />`
-   **`<blade-chat>` 属性**（纯 HTML / Vue）：`<blade-chat base-url="..." model="..."></blade-chat>`
-   **自建 UI 时逐条覆盖**：`chat.send(text, { model: "..." })`

可用模型清单由后端提供，接入方对照后端 Swagger（`<baseUrl>/docs`）自行获取。

## 样式定制

默认是浅色主题。四个层次，按需选用：

1.  **深浅主题**：`<ChatView theme="dark" />` / `<blade-chat theme="dark">` 切内置深色，不传或 `"light"` 为浅色。宿主已经在祖先元素上写了 `data-theme="dark"` 时不用传，`ChatView` 会跟着变。`<blade-chat>` 渲染在 Shadow DOM 内，页面上的 `data-theme` 对它无效，只认 `theme` 属性。
    
2.  **CSS 变量换肤**（React 与 `<blade-chat>` 通用）：所有颜色走 `--primary`、`--background`、`--muted` 等变量（完整清单见下面的「CSS 变量清单」，默认值在 style.css 开头）。页面直接覆盖即可穿透 Shadow DOM：
    

```css
blade-chat { --primary: 262 83% 58%; height: 640px; }
```

3.  **classNames props**（React）：`ChatView` 的 `classNames` 把自定义 class（含你自己构建的 Tailwind 工具类）挂到各个区块上，见 `ChatViewClassNames`。SDK 内部用 `cn`（clsx + tailwind-merge）拼接，你传的类在最后一个参数，所以 `classNames={{ chatInput: "p-0" }}` 会直接盖掉组件内部的 `py-3`，与 CSS 加载顺序无关：

```tsx
<ChatView
  sessionId={id}
  classNames={{
    root: "h-[640px] rounded-2xl border",
    userBubble: "bg-sky-100 text-sky-900",
    assistantText: "text-[15px] leading-7",
    chatInput: "px-2",
    inputInner: "bg-white shadow-sm",
  }}
/>
```

4.  **内嵌 `<style>` 与 `::part`（纯 HTML / Vue）**：`<blade-chat>` 的直接 `<style>` 子元素会被注入 Shadow DOM、排在内置样式之后，可用 `.blade-chat-*` 类名做任意深度定制；每个主要区块同时带一个与锚点同名的 `part`（`blade-chat-user-bubble` → `part="user-bubble"`），页面外层用 `::part()` 也能选到它：

```html
<blade-chat base-url="...">
  <style>
    .blade-chat-user-bubble { background: #eef2ff; }
    .blade-chat-thinking { font-size: 15px; }
  </style>
</blade-chat>
```

```css
blade-chat::part(user-bubble) { background: #eef2ff; }
blade-chat::part(input-toolbar) { gap: 12px; }
```

### classNames 覆盖哪些区块

区块与锚点一一对应（键名 = 锚点名去掉 `blade-chat-` 前缀）：

| `classNames` 键 | 锚点 | 落点 |
| --- | --- | --- |
| `root` | `.blade-chat` | 聊天根容器 |
| `banner` | `.blade-chat-banner` | 连接状态条 |
| `errorBar` | `.blade-chat-error-bar` | 顶部出错条 |
| `messageList` | `.blade-chat-messages` | 消息区 |
| `messagesScroll` | `.blade-chat-messages-scroll` | 消息区滚动内容（内边距） |
| `messagesContent` | `.blade-chat-messages-content` | 消息内容列（居中、最大宽度） |
| `empty` | `.blade-chat-empty` | 空状态 |
| `scrollToBottom` | `.blade-chat-scroll-bottom` | 「滚动到底部」按钮 |
| `userMessage` | `.blade-chat-user-row` | 用户消息行 |
| `userBubble` | `.blade-chat-user-bubble` | 用户气泡 |
| `fileCard` | `.blade-chat-file-card` | 消息里的文件附件 |
| `errorMessage` | `.blade-chat-error-row` | 消息流里的出错块 |
| `assistantTurn` | `.blade-chat-assistant-turn` | 一轮助手回复 |
| `thinking` | `.blade-chat-thinking` | 思考过程 |
| `assistantText` | `.blade-chat-assistant-text` | 助手正文 |
| `memoryRefs` | `.blade-chat-memory-refs` | 记忆引用 |
| `toolRow` | `.blade-chat-tool-row` | 执行过程里的工具行（智能体模式下工具调用都走这条路径） |
| `agentLoop` | `.blade-chat-agent-loop` | 子智能体行 |
| `askCard` | `.blade-chat-ask-card` | 提问卡 |
| `askOption` | `.blade-chat-ask-option` | 提问卡的每个选项 |
| `planPanel` | `.blade-chat-plan` | 输入框上方的计划面板 |
| `queuePanel` | `.blade-chat-queue` | 会话队列面板 |
| `followup` | `.blade-chat-followup` | 本轮小结与跟进区 |
| `followupAction` | `.blade-chat-followup-action` | 跟进建议按钮 |
| `chatInput` | `.blade-chat-input` | 输入区外层 |
| `inputInner` | `.blade-chat-input-inner` | 输入框卡片（边框、圆角、底色） |
| `textarea` | `.blade-chat-textarea` | 输入框 |
| `inputToolbar` | `.blade-chat-input-toolbar` | 输入框右侧按钮组 |

同一批锚点也是普通 CSS 的抓手：锚点只负责「可被选中」，视觉规则不在它身上，所以对你的锚点写 `.blade-chat-user-bubble { ... }` 不会被 SDK 的样式盖掉（SDK 的规则都在 `@layer components` / `@layer blade-chat-overrides`，Tailwind 工具类在 `@layer utilities`，你的样式不分层，按 CSS 层序本来就赢）。

### 锚点清单

每个可见区块的根元素都固定带一个 `blade-chat-*` 类，构成公开契约（`src/style.css` 顶部的登记块是唯一来源，构建会校验它和组件源码、三份 CSS 产物一致）：

| 区块 | 锚点 |
| --- | --- |
| 聊天根容器 | `.blade-chat` |
| 登录引导 / 连接状态条 / 出错条 / 回放条 | `.blade-chat-login`、`.blade-chat-banner`、`.blade-chat-error-bar`、`.blade-chat-replay-bar` |
| 消息区 | `.blade-chat-messages`、`.blade-chat-messages-scroll`、`.blade-chat-messages-content`、`.blade-chat-empty`、`.blade-chat-scroll-bottom`、`.blade-chat-scroll-bottom-label` |
| 用户消息 | `.blade-chat-user-row`、`.blade-chat-user-col`、`.blade-chat-user-bubble`、`.blade-chat-file-card`（+ `-icon` / `-name`） |
| 助手回复 | `.blade-chat-assistant-turn`、`.blade-chat-assistant-text`、`.blade-chat-thinking`、`.blade-chat-memory-refs`、`.blade-chat-render-error` |
| 工具调用 | `.blade-chat-tool-row`（执行过程里的工具行）、`.blade-chat-agent-loop`（子智能体行） |
| 提问卡 | `.blade-chat-ask-card`、`.blade-chat-ask-option` |
| 计划与队列 | `.blade-chat-plan`、`.blade-chat-plan-block`、`.blade-chat-queue`、`.blade-chat-queue-item`、`.blade-chat-queue-notice` |
| 跟进区 | `.blade-chat-followup`、`.blade-chat-followup-action` |
| 输入区 | `.blade-chat-input`、`.blade-chat-input-inner`、`.blade-chat-textarea`、`.blade-chat-input-toolbar` |
| 上下文卡片 | `.blade-chat-context-card`、`.blade-chat-context-summary`、`.blade-chat-context-icon`、`.blade-chat-context-copy`、`.blade-chat-context-title`、`.blade-chat-context-status`、`.blade-chat-context-chevron`、`.blade-chat-context-detail`、`.blade-chat-context-group-items` |
| Markdown 与代码块 | `.blade-chat-markdown`、`.blade-chat-prose`、`.blade-chat-codeblock`、`.blade-chat-codeblock-header`、`.blade-chat-codeblock-body` |
| 其他 | `.blade-chat-host`（`<blade-chat>` 内的挂载点）、`.blade-chat-advanced`、`.blade-chat-mcp-app`、`.blade-chat-error-row`、`.blade-chat-error-block` |

### part 清单（`<blade-chat>` 的 Shadow DOM 专用）

Shadow DOM 把上面的类名挡在页面之外，所以**主要区块**额外带一个与锚点同名的 `part`（去掉 `blade-chat-` 前缀；根容器的锚点是 `blade-chat`，约定用 `root`），页面外层用 `::part()` 选：

```css
blade-chat::part(user-bubble) { background: #eef2ff; }
blade-chat::part(input-toolbar) { gap: 12px; }
```

清单（`src` 里每一处 `part` 都由构建校验，必须能对回锚点清单）：

| 分组 | part |
| --- | --- |
| 容器与状态 | `root`、`banner`、`error-bar`、`replay-bar`、`mcp-app` |
| 消息区 | `messages`、`messages-scroll`、`messages-content`、`empty`、`scroll-bottom` |
| 用户消息 | `user-row`、`user-bubble`、`file-card`、`error-row` |
| 助手回复 | `assistant-turn`、`assistant-text`、`thinking`、`memory-refs` |
| 工具与提问 | `tool-row`、`agent-loop`、`ask-card`、`ask-option` |
| 计划与队列 | `plan`、`plan-block`、`queue` |
| 跟进区 | `followup`、`followup-action` |
| 输入区 | `input`、`input-inner`、`textarea`、`input-toolbar` |

外围区块（`.blade-chat-context-*`、`.blade-chat-codeblock-*` 等）只有类名锚点，React 下可以直接选；`<blade-chat>` 下要改它们得用第 4 层的内嵌 `<style>`（那段样式就注入在 Shadow DOM 里）。

### CSS 变量清单

颜色是 `hsl()` 的三个分量（形如 `223 100% 54%`），几何是长度值。宿主盖掉这些变量即可整体换肤（React 下写在 `.blade-chat` 或祖先元素上；`<blade-chat>` 下写在元素上，变量能穿透 Shadow DOM）：

| 分组 | 变量 |
| --- | --- |
| 基础色 | `--background`、`--foreground`、`--card`、`--card-foreground`、`--popover`、`--popover-foreground` |
| 语义色 | `--primary`、`--primary-foreground`、`--muted`、`--muted-foreground`、`--accent`、`--accent-foreground`、`--destructive`、`--border`、`--ring` |
| 用户消息专用 | `--user-msg-bg`、`--user-msg-fg`、`--user-msg-border` |
| 布局（SDK 按容器宽度推导） | `--blade-chat-gutter`（消息区与输入区的左右留白）、`--blade-chat-turn-gap`（轮次间距）。两个变量声明在 `.blade-chat` 上，**设在祖先元素上不生效**（元素上的声明永远赢过继承值）。React 下直接对 `.blade-chat` 写规则，或用 `classNames.root` 传一个设该变量的工具类（如 `"[--blade-chat-gutter:2rem]"`）；`<blade-chat>` 下用第 4 层的内嵌 `<style>` |
| 布局（接入方按需覆盖） | `--blade-chat-queue-gutter`（队列面板左右留白，默认跟随 gutter）、`--blade-chat-pin-spacer`（消息顶置的底部补白，组件自己写）、`--blade-chat-input-top-border`（输入区上边框，组件自己写） |

深色主题的变量挂在 `[data-theme="dark"]` 上，由 `theme` prop / 属性落到聊天根节点，整棵子树继承。

上面这些是**会改变 SDK 组件外观**的变量。另外随主题一起声明的还有 `--radius`、`--input`、`--destructive-foreground`：`agent-react` 的默认组件不消费它们（圆角走 Tailwind 自己的 `--radius-*`），宿主在聊天容器里自绘 UI 时可以直接用，改动它们不会影响 SDK 自带的界面。

### 区块级渲染器（renderers）

`classNames` 换不掉整个区块时用 `renderers`：每个渲染器拿到一个描述该区块的对象，返回 `ReactNode`；返回 `null` 走 SDK 默认渲染。

```tsx
<ChatView
  sessionId={id}
  renderers={{
    toolCall: (toolCall) => <MyToolCard toolCall={toolCall} />,
    userMessage: ({ message }) => <MyBubble message={message} />,
    assistantText: ({ message, streaming, compact }) => (compact ? null : <MyMarkdown text={message.content} />),
    askUserQuestion: ({ data, answered, onAnswer }) => <MyQuestions data={data} answered={answered} onAnswer={onAnswer} />,
    planUpdate: ({ plan, running }) => <MyPlanSteps plan={plan} running={running} />,
  }}
/>
```

-   `toolCall`：`(toolCall: ToolCallInfo) => ReactNode | null`，见 `ToolCallRenderer`。
-   `userMessage`：`UserMessageRendererProps`——只替换用户消息气泡，消息行（`.blade-chat-user-row`）与图片/文件附件仍由 SDK 渲染；消息顶置依赖那个行容器，替换掉它新消息就不会被顶到视口顶部。
-   `assistantText`：`AssistantTextRendererProps`——只替换助手正文，思考块、工具卡、附件照旧。
-   `askUserQuestion`：`AskUserQuestionRendererProps`——`data` 是 `AskUserQuestionData`，`onAnswer` 与默认卡片同一个回调契约；照常作答才算完成这次提问。**提问工具优先走这个渲染器**，只有没配它时才回落到 `toolCall`（避免 `toolCall` 先拿到提问、而它没有作答回调）。
-   `planUpdate`：`PlanUpdateRendererProps`——`plan` 是最近一次成功 `update_plan` 的解析结果；还没有解析出计划时渲染器不会被调用，回落到默认面板的「正在更新任务进度…」。

## 自定义消息列表滚动

`ChatView`、`AgentChat` 和 `LlmChat` 已经处理好用户发言顶置，不用额外配置。自建消息列表时可用 `useMessagePin` 复用同一套触发、补白、视口 resize 和手动滚动接管逻辑；调用方只需提供滚动容器、目标消息与 spacer 的读写函数。完整参数见 [public-api.md](https://npmcn.com/package/%40blade-hq/agent-react/public-api.md#usemessagepin-function)。

## 打字机式显示节流

`ChatView`、`AgentChat` 和 `LlmChat` 已经内置这套节流，不用额外配置。服务端到达速率天然不均匀（一次推 5 个字、一次推 20 个字），直接原样渲染会显得忽快忽慢；`useTypewriterReveal(targetText, isLive, resetKey)` 按估算到达速度、留一个很小的固定滞后量匀速播放，返回 `{ displayedText, isRevealing, flushNow }`——自建消息列表时可以直接复用。`isLive` 传 `false`（历史加载 / 回放）时永远整段直接显示；`resetKey`（比如消息的 `entry_id`）变化时播放状态从头开始；`flushNow()` 用于用户点了停止时立即整段显示已到达内容。完整参数见 [public-api.md](https://npmcn.com/package/%40blade-hq/agent-react/public-api.md#usetypewriterreveal-function)。

## 长会话历史导航（useTurnNavigation / TurnNavRail）

`AgentChat` 已经内置这条轨道，不用额外配置：右侧刻度轨列出**整条会话**的用户发言（目录来自服务端 `/turn-index`，所以窗口外的旧发言也点得到），点已加载的发言直接滚动并短暂高亮，点窗口外的发言发 一次定位请求（服务端返回「从它到最新」的窗口）后滚动到目标。

自建聊天界面时用 `useTurnNavigation` 复用同一套时序（滚动 / 加载态 / 高亮）：宿主提供 `TurnNavigationSource`（目录怎么来、定位怎么发、刷新信号）与 `getLoadedEntryElement` / `revealEntry` / `beforeSeek` 三个列表侧回调（合起来就是 `TurnNavigationOptions`），返回 `TurnNavigationResult`（`items`、`loadingEntryId`、 `error`、`failedEntryId`、`activeEntryId`、`selectEntry`）。`TurnNavRail` 是配套的刻度轨组件 （`TurnNavRailProps`），样式与交互可直接用或照抄；定位高亮的类名是 `TURN_NAV_HIGHLIGHT_CLASS`。 目录刷新信号用 `latestUserSpeechEntryId(turns)` 计算——只认最新一条用户发言，往前翻页补进的更早 发言不该作废正在进行的定位；没有目录接口的会话（静态历史 / 分享页）用 `turnNavItemsFromMessages` 把已加载的窗口转成兜底条目。

## 纯静态 HTML / Vue？

不需要本包——用后端托管的单文件产物 `<script src=".../sdk/blade-agent.js">` + `<blade-chat>` 自定义元素，行为与 `ChatView` 一致；详见 agent-client README 与接入文档。

## 附录：连接器与插件选择

-   **连接器面板**：`ConnectorPanel`、`ConnectorPanelProps`、`PluginConnectorList`、`PluginConnectorListProps`、`ConfiguringPlugin`
-   **插件行与触发器**：`PluginConnectorItem`、`PluginConnectorSelection`、`selectionFromItem`、`triggerSummary`、`TriggerSummary`、`TRIGGER_ICON_LIMIT`、`mergeConnectorItems`、`catalogToConnectorItems`、`filterConnectorItems`
-   **触发器上的头像堆叠**：`PluginTriggerStack`、`PluginTriggerStackProps`（前三枚插件头像 + `+N`，宿主自己画触发器时用它，不要另写一份折叠）
-   **启用时序**：`useSessionPluginActivation`、`UseSessionPluginActivationResult`

## 附录：re-export 自 @blade-hq/agent-client

为方便单包引入，本包 re-export 了 agent-client 的全部公开声明（`BladeClient`、`AgentSession`、`SessionState`、协议类型等），文档一律见 [agent-client 的 README](https://npmcn.com/package/%40blade-hq/agent-client/README.md)。完整签名见 [public-api.md](https://npmcn.com/package/%40blade-hq/agent-react/public-api.md)。

本包自有声明：`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`useMessagePin`、`UseMessagePinOptions`、`useTypewriterReveal`、`UseTypewriterRevealResult`、`useReplay`、`UseReplayResult`、`ReplayMismatch`、`ReplayBar`、`ReplayBarProps`、`ReplayMismatchPrompt`、`ReplayMismatchPromptProps`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`FollowupInteractionEvent`、`ToolCallRenderer`、`CurrentPlanPanel`、`PlanUpdateBlock`、`PlanUpdateData`、`PlanUpdateDisplayState`、`PlanStepStatus`、`getPlanUpdateDisplayState`、`isPlanUpdateTool`、`parsePlanUpdate`、`pickCurrentPlanStep`、`PLAN_AUTO_COLLAPSE_MS`、`MarkdownContent`、`MarkdownContentProps`、`MarkdownRenderer`、`MarkdownRendererProps`、`MarkdownComponents`、`AllowedTags`、`DEFAULT_ALLOWED_TAGS`、`CodeBlockOverrides`、`LinkSafetyConfig`、`RawHtmlPlugin`、`parseWhatIfPrompt`、`ParsedWhatIfPrompt`、`WhatIfQuote`、`WhatIfUserBubble`、`WhatIfUserBubbleProps`、`SessionQueuePanel`、`SessionQueuePanelProps`、`isQueuePanelVisible`、`ExternalAnchor`、`stripSystemReminders`。

## 会话消息队列

智能体会话的等待消息由服务端持久化，`ChatView` / `AgentChat` 已在输入框上方渲染队列区域（空队列不占位，条目数超过三条可折叠）。输入框不再有「直接插入 / 排队执行」开关：会话空闲时提交直接开始处理，运行中提交默认入队；暂停时在面板上显示原因和「继续执行」。面板数据来自 `useAgentSession` 状态里的 `queue`（`SessionQueueSnapshot`），可编辑/删除/立即补充的判定来自 agent-client 的 `canEditQueuedMessage` / `canCancelQueuedMessage` / `canDeliverQueuedMessage`，文案用 `queuedMessageStatusLabel` / `queuePauseReasonLabel`。

自建布局可直接使用 `<SessionQueuePanel>`，属性类型为 `SessionQueuePanelProps`：传 `snapshot`、`canDeliver`、可选的 `pendingItemId`（提交中的条目禁用操作）与 `notice`（冲突/失败说明），以及 `onResume` / `onCancel` / `onEdit` / `onMove` / `onReorder` / `onDeliver` 回调。上移/下移与拖动排序最终都提交当前 pending 条目的完整 id 顺序。冲突时队列由服务端快照刷新，`ChatView` 会把未保存的正文放回输入框，不会静默丢掉用户编辑的内容。

面板比输入框窄一圈、贴在它上方，所以宽度必须跟输入框一致：用可选属性 `contentWidthClassName` 传入输入框内层的宽度类（不传则按本包输入框的 `max-w-[748px]`）。左右内边距同理——默认值对应本包输入框的 `--blade-chat-gutter`；如果你给输入框根节点换了左右 padding（例如改成 `px-10`），必须用 `rootClassName` 在面板根节点重设 `--blade-chat-queue-gutter` 成同一个值，否则面板会跟输入框错开一圈、在两者相接的缝上露出来。

输入框的 `mergesWithPanelAbove` 为 true 时上两角改直角，跟面板拼成一条完整轮廓；否则输入框的上角圆弧会从较窄的面板两侧露出来，形成两个尖角。**这个值要取面板上报的状态，不要在宿主侧用 `isQueuePanelVisible` 复算**：面板有本地编辑态（编辑到一半、那条等待消息被服务端拿走时仍要留在原地保住草稿，见 `detachedEdit`），这时 snapshot 里没有条目、宿主复算会得到 false，输入框就会在面板还显示着的时候把上角恢复成圆角。用 `onVisibleChange` 拿到面板的真实占位状态，再传给输入框或 `ChatSurface` 的 `queuePanelVisible`。本包的 `AgentChat` 已按这条接好，自建布局照做即可。

拼轮廓的前提是两者**真的相邻**。中间夹着别的内容时（本包的 MCP 暂存内容、内置 Web 的 `composerBanner`），面板下两角与输入框上两角都要圆起来——直角是"被对方接住"的表示，露出直角却中间空着一块，看起来像被削掉一角。面板的 `mergesWithInputBelow` 与输入框的 `mergesWithPanelAbove` 要取同一个判据；`ChatSurface` 会用 context 把这一位传给面板，自建布局直接传即可。

`PollingBackoff` 从 agent-client 重导出，供自定义只读轮询计算退避时间；它不发送或重放请求。用法见 [agent-client 轮询说明](https://npmcn.com/package/%40blade-hq/agent-client/README.md#%E5%8F%AA%E8%AF%BB%E8%BD%AE%E8%AF%A2%E9%80%80%E9%81%BF)。

### 会话插件

默认 `AgentChat` / 智能体模式 `ChatView` 自带会话插件选择器，展开时查询 BH，选择按当前会话保存。准备失败会保留勾选并提供重试。独立嵌入可使用 `<SessionPluginSelector client={client} sessionId={id} />`；`sessionPluginControls` 可覆盖默认控件。组件参数类型为 `SessionPluginSelectorProps`。

列表项用插件声明的业务名称与随包本地图标展示（`sessionPluginLabel(plugin)` 与 `<SessionPluginIcon client={client} sessionId={id} plugin={plugin} />`，两者都可单独复用，参数类型为 `SessionPluginLabelProps` / `SessionPluginIconProps`）；没有声明或图标不可用时回退技术 ID 与通用占位。授权、缓存、调用一律仍用 `plugin.name`。

插件的账号、token 等配置由包内 JSON Schema 声明，选择器就地提供填写入口：首次激活缺配置的插件会直接弹出表单（`<SessionPluginConfigDialog client={client} sessionId={id} plugin={plugin} />`），已配置的插件在行内提供「配置」按钮。表单本体是 `<SessionPluginConfigForm>`，参数与提交类型为 `SessionPluginConfigFormProps` / `SessionPluginConfigDraft`，也可单独嵌入；`SessionPluginConfigDialogProps` 是对话框的参数类型。

表单只呈现它确定能呈现的 JSON Schema 构造（字符串/数值/布尔/null、对象与数组、枚举、`properties` 内的 `writeOnly`，以及用同一字段的 `const` 固定的 `oneOf` 分支）；遇到无法安全呈现的写法直接说明原因，不降级成手写 JSON、不静默丢字段。`writeOnly` 字段永不回显：留空表示保持不变，输入表示修改，显式「清除」才会删除。

### 连接器面板

首页与对话输入框共用同一个连接器面板：`<ConnectorPanel client={client} sessionId={id} />`， 参数类型为 `ConnectorPanelProps`。面板本身只管插件分区，电脑与外部账号由宿主通过 `renderComputerSection` / `renderExternalAccounts` / `renderFooterActions` 注入——那些入口 要跳宿主自己的路由，SDK 不硬编码内部路径。`sessionId` 传 `null` 表示"首页"：此时开关只记录 本次输入的待选（`PluginConnectorSelection`、`onPendingPluginsChange`），不激活任何东西、 不建会话、不启沙盒；`pendingDisabledReason` 用于说明该模式下为什么用不了待选。

插件行的渲染交给 `<PluginConnectorList>`（`PluginConnectorListProps`），它只负责样式与排版： "该显示成什么"全部来自 `PluginConnectorItem`、`mergeConnectorItems`、`catalogToConnectorItems`、 `filterConnectorItems`，判定不各写一份。`sessionPluginLabel` 决定 一行读起来是什么名字。

启用时序由 `useSessionPluginActivation` 收口（返回值类型 `UseSessionPluginActivationResult`）： **先准备配置、再提交激活**。缺必填配置时开关不落到已启用、立刻弹表单，保存由服务端确认配置 完整才提交；取消、校验失败、读取失败都保持关闭，不会先写 `active=true` 再补偿。慢准备/慢激活 期间开关仍可点，语义是取消这次意图而不是再排一次队。配置弹窗由 `ConfiguringPlugin` 描述。

触发器要在不打开面板时就能显示"选了哪几个、几个"：用 `selectionFromItem` 把一行转成 `PluginConnectorSelection`，再用 `triggerSummary` 折成 `TriggerSummary`（最多 `TRIGGER_ICON_LIMIT` 个图标，其余折成数量；没有图标 token 的项不占图标位但仍计数）。

### MCP Apps

`McpAppCard` 使用官方 AppBridge 渲染归档卡片，内置 Web 消费同一组件。在 `BladeProvider` 下传入会话 ID 与来源 ID，也可直接提供 `BladeClient`。`McpAppMessage` 表示 App 提供的文本；`McpAppMessageContext` 将其交给当前对话的待发送区。用户可在 `ChatView` 查看、移除并确认发送。`update-model-context` 会把 App 提交的当前状态经 `sessions.updateMcpAppContext` 存为会话上下文：只落库、不追加聊天、不启动 agent turn，正在运行的一轮保持启动时版本，下一个新的运行才读取；冲突（409）时重读最新 revision 后重提同一份状态一次。

### 子智能体卡片

`AgentTaskCard` 提供共享的可折叠子智能体卡片，`AgentTaskCardProps` 接受任务 `title`、`status`、最新动作 `activity` 和详情 `children`。`AgentTaskStatus` 包含 `running`、`waiting_input`、`completed`、`failed`、`cancelled`；进入 `waiting_input` 时自动展开，提示图标持续轻缓闪动，并遵守系统减少动态效果设置。回答表单由宿主通过 `children` 提供，回答后应更新状态以停止提示。

### 对话分支选择器

`SessionBranchPicker`（属性类型 `SessionBranchPickerProps`）提供与内置 Web 共用的 Git 风格对话树、消息预览、切换中禁用和错误提示，可放进宿主的弹窗或侧栏。`tree` 使用 `client.sessions.getSessionCheckpoints(sessionId)` 的返回值；`disabled` 应来自该会话的运行状态或只读权限，不能仅依据当前标签页是否收到流式消息。

```tsx
<SessionBranchPicker
  tree={checkpoints}
  disabled={isRunning || readOnly}
  onCheckout={async (checkpointId, position) => {
    const result = await client.sessions.checkoutSession(
      sessionId, checkpointId, position, "conversation",
    )
    await refreshConversation()
    setDraft(result.position === "before" ? result.content : "")
    setCheckpoints(await client.sessions.getSessionCheckpoints(sessionId))
  }}
/>
```

示例中的刷新消息、设置草稿与保存检查点状态由宿主实现。消息圆点回到发送前（`before`），虚线圆点切到分支末尾（`turn_end`）。宿主弹窗应明确说明：这些操作只修改对话记录，不修改、删除或恢复工作目录中的文件；请求使用 `conversation` 模式，不使用文件恢复模式。

### 会话图片

`MessageImage` 是聊天组件共用的图片组件，属性类型为 `MessageImageProps`。它支持原生 `<img>` 属性，并从 `BladeProvider` 获取客户端；也可显式传入 `client`。会话资源使用带鉴权的独立请求，公开分享图片使用分享令牌授权，在接近可视区域时加载预览，卸载后回收对象 URL。传入 `original` 可用于原图预览。普通外部图片 URL 仍直接由浏览器加载。

```tsx
<MessageImage src={imageUrl} alt="附件图片" width={width} height={height} />
```

### 对话执行耗时

`getChatRun(messages)` 取最新一次根对话运行区间；`getChatRunDuration(messages, now)` 返回结束后的后端总耗时，或运行中从区间起点到校准服务器时间的毫秒数。这个区间包含工具、子任务和结束前的收尾，不重复累加并行任务。

`useLiveExecutionDuration` 在起点就绪后用单调时钟逐秒更新，停止时清理计时器。自建聊天界面可复用这组函数；模型和工具各自的性能样本仍分别保留。

## 常见问题

### @blade-hq/agent-react 是什么？

Blade Agent React @ @ @:Blade Provider, us Assession @ ChatView @ @ @。

### 如何安装 @blade-hq/agent-react？

在项目目录运行 npm install @blade-hq/agent-react，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：react（^18\.0\.0 \|\| ^19\.0\.0）、react-dom（^18\.0\.0 \|\| ^19\.0\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @blade-hq/agent-react 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @blade-hq/agent-react 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @blade-hq/agent-react 可以商用吗？

@blade-hq/agent-react 使用 未声明 许可。package\.json 没有声明许可证。没有许可证的代码默认保留全部权利，商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括，不构成法律意见，请以许可原文为准。

### @blade-hq/agent-react 的下载量有多少？

根据 npm 官方下载统计，@blade-hq/agent-react 在 2026/10/09 – 2026/11/07 这 30 天内被下载 9,759 次，日均约 325 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @blade-hq/agent-react 还在维护吗？

最新版本 2610\.0\.6 发布于 2026/11/11（今天）；至今共发布 246 个版本，其中过去 12 个月发布了 246 个；首次发布于 2026/07/21。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

