@blade-hq/agent-react 2610.0.0-beta.8 → 2610.0.0-beta.80

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.
Files changed (70) hide show
  1. package/README.md +313 -8
  2. package/dist/chunk-G3PMV62Z.js +36 -0
  3. package/dist/components/AgentChat.d.ts +3 -0
  4. package/dist/components/AgentLoopBlock.d.ts +3 -5
  5. package/dist/components/AgentTaskCard.d.ts +12 -0
  6. package/dist/components/AskUserQuestionBlock.d.ts +12 -1
  7. package/dist/components/AssistantTurnBlock.d.ts +26 -8
  8. package/dist/components/ChatInput.d.ts +21 -1
  9. package/dist/components/ChatSurface.d.ts +133 -2
  10. package/dist/components/ChatView.d.ts +1 -1
  11. package/dist/components/ConnectorPanel.d.ts +41 -0
  12. package/dist/components/ContextCard.d.ts +20 -0
  13. package/dist/components/MarkdownContent.d.ts +17 -0
  14. package/dist/components/McpAppCard.d.ts +14 -0
  15. package/dist/components/MessageList.d.ts +38 -4
  16. package/dist/components/PlanUpdateBlock.d.ts +32 -0
  17. package/dist/components/PluginConnectorList.d.ts +47 -0
  18. package/dist/components/PluginTriggerStack.d.ts +22 -0
  19. package/dist/components/PostChatFollowupBlock.d.ts +5 -1
  20. package/dist/components/SessionMemoryToggle.d.ts +19 -0
  21. package/dist/components/SessionPluginConfigDialog.d.ts +10 -0
  22. package/dist/components/SessionPluginConfigForm.d.ts +14 -0
  23. package/dist/components/SessionPluginIcon.d.ts +30 -0
  24. package/dist/components/SessionPluginSelector.d.ts +10 -0
  25. package/dist/components/SessionQueuePanel.d.ts +74 -0
  26. package/dist/components/ToolCallBlock.d.ts +9 -3
  27. package/dist/components/ToolUiCardView.d.ts +9 -0
  28. package/dist/components/TurnNavRail.d.ts +23 -0
  29. package/dist/components/UserMessageBubble.d.ts +7 -3
  30. package/dist/components/WhatIfUserBubble.d.ts +7 -0
  31. package/dist/components/connector-panel-types.d.ts +37 -0
  32. package/dist/components/display-utils.d.ts +10 -0
  33. package/dist/components/markdown/heal-incomplete.d.ts +7 -0
  34. package/dist/components/markdown/incremental.d.ts +73 -0
  35. package/dist/components/markdown/index.d.ts +18 -0
  36. package/dist/components/markdown/parse.d.ts +19 -0
  37. package/dist/components/markdown/render.d.ts +79 -0
  38. package/dist/components/markdown/sanitize.d.ts +50 -0
  39. package/dist/components/markdown/types.d.ts +53 -0
  40. package/dist/components/plugin-config-draft.d.ts +13 -0
  41. package/dist/components/plugin-config-schema.d.ts +6 -0
  42. package/dist/components/plugin-connector.d.ts +40 -0
  43. package/dist/components/queue-panel-adjacency.d.ts +1 -0
  44. package/dist/components/use-session-plugin-activation.d.ts +40 -0
  45. package/dist/context.d.ts +1 -0
  46. package/dist/embed/blade-chat-element.d.ts +4 -3
  47. package/dist/embed/entry.d.ts +49 -0
  48. package/dist/hooks/use-agent-session.d.ts +5 -0
  49. package/dist/hooks/use-message-pin.d.ts +28 -0
  50. package/dist/hooks/use-turn-navigation.d.ts +117 -0
  51. package/dist/hooks/use-typewriter-reveal.d.ts +32 -0
  52. package/dist/index.d.ts +53 -1
  53. package/dist/index.js +58278 -2242
  54. package/dist/index.js.map +1 -1
  55. package/dist/lib/agent-computer-command.d.ts +34 -0
  56. package/dist/lib/random-id.d.ts +10 -0
  57. package/dist/lib/utils.d.ts +17 -2
  58. package/dist/lib/whatif-prompt.d.ts +18 -0
  59. package/dist/style.css +506 -401
  60. package/dist/style.full.css +517 -405
  61. package/dist/webapi-W3IB5DO4.js +2481 -0
  62. package/dist/webapi-W3IB5DO4.js.map +1 -0
  63. package/package.json +2 -2
  64. package/public-api.md +1749 -65
  65. package/dist/chunk-ZXXLY4RM.js +0 -28255
  66. package/dist/chunk-ZXXLY4RM.js.map +0 -1
  67. package/dist/highlighted-body-B3W2YXNL-YD7FAP6V.js +0 -27
  68. package/dist/highlighted-body-B3W2YXNL-YD7FAP6V.js.map +0 -1
  69. package/dist/mermaid-3ZIDBTTL-N7YKJ3SM.js +0 -8
  70. /package/dist/{mermaid-3ZIDBTTL-N7YKJ3SM.js.map → chunk-G3PMV62Z.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  import type { AskUserAnswerData, ToolCallInfo } from "@blade-hq/agent-client";
2
2
  import { type ReactNode } from "react";
3
+ import type { ChatViewClassNames, ChatViewRenderers } from "./ChatSurface";
3
4
  /** 接入方自定义渲染器:返回 null 时回落到默认渲染。 */
4
5
  export type ToolCallRenderer = (info: ToolCallInfo) => ReactNode | null;
5
6
  interface Props {
@@ -8,7 +9,11 @@ interface Props {
8
9
  answered?: boolean;
9
10
  answerData?: AskUserAnswerData;
10
11
  sessionStatus?: string;
11
- renderer?: ToolCallRenderer;
12
+ isActiveQuestion?: boolean;
13
+ /** 各可见区块的 className 落点;键与元素上的 `blade-chat-*` 锚点一一对应。 */
14
+ classNames?: ChatViewClassNames;
15
+ /** 区块级渲染器;返回 null 走默认渲染。 */
16
+ renderers?: ChatViewRenderers;
12
17
  }
13
18
  /**
14
19
  * 判断提问卡片该等待作答还是展示成已作答,只认工具状态。
@@ -19,10 +24,11 @@ interface Props {
19
24
  * 会同时把已经回答过的旧提问重新变成可作答(历史加载不会带回 answerData),
20
25
  * 而且会盖住投影层的错误——这个 bug 就是这么藏住的。
21
26
  */
22
- export declare function resolveAskQuestionState({ toolStatus, hasAnswerData, fallbackAnswered, }: {
27
+ export declare function resolveAskQuestionState({ toolStatus, hasAnswerData, fallbackAnswered, fallbackAwaiting, }: {
23
28
  toolStatus: ToolCallInfo["status"];
24
29
  hasAnswerData: boolean;
25
30
  fallbackAnswered?: boolean;
31
+ fallbackAwaiting?: boolean;
26
32
  }): {
27
33
  awaitingAnswer: boolean;
28
34
  answered: boolean;
@@ -31,5 +37,5 @@ export declare function resolveAskQuestionState({ toolStatus, hasAnswerData, fal
31
37
  * 极简工具调用块:状态 + 展示名一行,点开后是参数/结果的 <pre> 折叠展示。
32
38
  * AskUserQuestion 特殊处理为提问选项卡片。
33
39
  */
34
- export declare function ToolCallBlock({ toolCall, onAnswer, answered, answerData, sessionStatus, renderer, }: Props): import("react/jsx-runtime").JSX.Element;
40
+ export declare function ToolCallBlock({ toolCall, onAnswer, answered, answerData, sessionStatus, isActiveQuestion, classNames, renderers, }: Props): import("react/jsx-runtime").JSX.Element;
35
41
  export {};
@@ -0,0 +1,9 @@
1
+ import { type ToolUiCard } from "@blade-hq/agent-client";
2
+ /**
3
+ * MCP Apps 使用 SDK/Web 共享的 AppBridge;恢复只消费留档,不重读资源。
4
+ * 普通资源卡片仍使用 agent-client 的共享内容判定。
5
+ */
6
+ export declare function ToolUiCardView({ card, sessionId }: {
7
+ card: ToolUiCard;
8
+ sessionId?: string;
9
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,23 @@
1
+ import type { TurnNavItem } from "../hooks/use-turn-navigation";
2
+ export interface TurnNavRailProps {
3
+ /** 当前会话全部用户发言的目录;少于两条时不渲染(一条没有导航的意义)。 */
4
+ items: TurnNavItem[];
5
+ /** 正在高亮的那条发言(视口所在轮,或刚定位到的目标)。 */
6
+ activeTurnId: string | null;
7
+ /** 正在请求定位的目标;null 表示没有在途定位。 */
8
+ loadingTurnId?: string | null;
9
+ /** 定位失败提示。 */
10
+ error?: string | null;
11
+ /** 定位失败的那条发言;重试按钮就再点它一次。 */
12
+ failedTurnId?: string | null;
13
+ /** 点击/重试某条发言。 */
14
+ onSelectTurn: (entryId: string) => void;
15
+ }
16
+ /**
17
+ * 用户发言的刻度轨:右侧一列刻度,悬停/聚焦展开目录列表。
18
+ *
19
+ * 只负责显示和点击,请求、滚动与高亮全在 `useTurnNavigation` 里——内置 Web 那份
20
+ * rail(apps/web 的 TurnNavRail.tsx)有同样的语义,样式与交互各自实现。
21
+ * 类名走 `blade-turn-nav-*`:宿主没装 Tailwind 时 style.css 提供等效样式。
22
+ */
23
+ export declare function TurnNavRail({ items, activeTurnId, loadingTurnId, error, failedTurnId, onSelectTurn, }: TurnNavRailProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,4 +1,5 @@
1
1
  import type { ChatMessage } from "@blade-hq/agent-client";
2
+ import type { ChatViewClassNames, ChatViewRenderers } from "./ChatSurface";
2
3
  export type UserChatMessage = ChatMessage & {
3
4
  role: "user";
4
5
  };
@@ -9,10 +10,13 @@ export declare function isUserMessage(message: ChatMessage): message is UserChat
9
10
  export declare function isErrorMessage(message: ChatMessage): message is ErrorChatMessage;
10
11
  interface UserMessageProps {
11
12
  message: UserChatMessage;
12
- className?: string;
13
+ /** 各可见区块的 className 落点;键与元素上的 `blade-chat-*` 锚点一一对应。 */
14
+ classNames?: ChatViewClassNames;
15
+ /** 区块级渲染器;userMessage 只替换气泡本身,行与附件仍由 SDK 渲染。 */
16
+ renderers?: ChatViewRenderers;
13
17
  }
14
- /** 用户消息气泡:文本 + 图片/文件附件展示(不含第一方的预览弹窗与 what-if 徽章)。 */
15
- export declare function UserMessageBubble({ message, className }: UserMessageProps): import("react/jsx-runtime").JSX.Element;
18
+ /** 用户消息气泡:文本、canonical 引用消息与图片/文件附件。 */
19
+ export declare function UserMessageBubble({ message, classNames, renderers }: UserMessageProps): import("react/jsx-runtime").JSX.Element;
16
20
  /** 错误消息:居中红色提示条。 */
17
21
  export declare function ErrorMessageBlock({ message, className, }: {
18
22
  message: ErrorChatMessage;
@@ -0,0 +1,7 @@
1
+ import type { ParsedWhatIfPrompt } from "../lib/whatif-prompt";
2
+ export interface WhatIfUserBubbleProps {
3
+ parsed: ParsedWhatIfPrompt;
4
+ onQuoteClick?: (stepNumber: number) => void;
5
+ }
6
+ /** SDK 与内置 Web 共用的 canonical 引用消息渲染。 */
7
+ export declare function WhatIfUserBubble({ parsed, onQuoteClick }: WhatIfUserBubbleProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import type { PluginConnectorItem } from "./plugin-connector";
2
+ /**
3
+ * 首页选中的一个待选插件。
4
+ *
5
+ * 带上展示名和图标是为了两件事:后续的配置弹窗与失败提示能一直用业务名(只传技术
6
+ * ID 的话,用户在面板里看到「示例登录」、在弹窗标题里看到 `demo-login`,等于要他认
7
+ * 两遍),以及不用打开面板就能在触发器上看到自己选了什么。
8
+ *
9
+ * 技术 ID 仍是路径、授权、缓存与调用的权威。
10
+ */
11
+ export interface PluginConnectorSelection {
12
+ name: string;
13
+ displayName: string | null;
14
+ icon: string | null;
15
+ iconSource: PluginConnectorItem["iconSource"];
16
+ }
17
+ /** 面板一行 → 待选条目。 */
18
+ export declare function selectionFromItem(item: PluginConnectorItem): PluginConnectorSelection;
19
+ /** 触发器上最多直接显示几个插件头像,其余折成数量。 */
20
+ export declare const TRIGGER_ICON_LIMIT = 3;
21
+ /** 触发器上的头像堆叠内容:前三个 + 折起来的数量。 */
22
+ export interface TriggerSummary {
23
+ /** 前 TRIGGER_ICON_LIMIT 个选中项,**不管有没有图标 token**。 */
24
+ visible: Array<Pick<PluginConnectorSelection, "name" | "displayName" | "icon" | "iconSource">>;
25
+ /** 超出 TRIGGER_ICON_LIMIT 的数量;0 表示不显示 `+N`。 */
26
+ overflow: number;
27
+ /** 已选/已启用的总数,用于无障碍标签与提示。 */
28
+ total: number;
29
+ }
30
+ /**
31
+ * 把一份插件列表折成触发器上的头像堆叠。
32
+ *
33
+ * 前三项都占位,**不管有没有图标 token**:没有图标的用显示名首字起头的圆形占位,
34
+ * 「选了哪个」照样看得见。早先的实现把无图标的项从图标位里剔掉、还把它们算进
35
+ * `overflow`,于是选中一个没有图标的插件时触发器毫无变化——选择在入口上不可见。
36
+ */
37
+ export declare function triggerSummary(items: Array<Pick<PluginConnectorSelection, "name" | "displayName" | "icon" | "iconSource">>): TriggerSummary;
@@ -1,7 +1,17 @@
1
1
  import type { ToolCallInfo } from "@blade-hq/agent-client";
2
2
  export type ToolTone = "emerald" | "blue" | "amber" | "red";
3
+ /**
4
+ * 技能入口文件的所在目录就是技能名(`.agent/skills/<技能名>/SKILL.md`)。
5
+ * 只显示 "SKILL.md" 用户看不出读的是哪个技能。
6
+ *
7
+ * 改这里必须同步 apps/web/src/agent/components/chat/display-utils.ts 的同名函数。
8
+ */
9
+ export declare function getSkillNameFromFilePath(filePath: string | null | undefined): string | null;
3
10
  /** 归一化工具名:去掉命名空间前缀并映射到别名表(如 "fork:Agent" → "Agent")。 */
4
11
  export declare function formatToolName(name: string): string;
12
+ export declare function extractToolFilePath(toolCall: ToolCallInfo): string | null;
13
+ /** 从文件类工具参数中提取全部可定位的文件路径(按出现顺序去重)。 */
14
+ export declare function extractToolFilePaths(toolCall: ToolCallInfo): string[];
5
15
  /**
6
16
  * 返回工具调用的中文展示标签:优先取参数里的 description,
7
17
  * 其次取后端下发的 display_name / 系统工具中文名。
@@ -0,0 +1,7 @@
1
+ export interface HealResult {
2
+ text: string;
3
+ /** 从原始(未补闭合符的)文本里的这个字符偏移开始,往后的内容都落在
4
+ * "补的闭合符"影响范围内——`null` 表示这一轮没有需要补的地方。 */
5
+ pendingFromOffset: number | null;
6
+ }
7
+ export declare function healIncompleteMarkdown(text: string): HealResult;
@@ -0,0 +1,73 @@
1
+ import type { Root, RootContent } from "mdast";
2
+ import type { HealResult } from "./heal-incomplete";
3
+ /**
4
+ * 增量解析缓存:跨帧复用"已经确认不会再变"的块,只重新解析真正在增长的
5
+ * 尾部。算法对齐 vercel/streamdown PR #608(未发版,只做块级冻结,不需要
6
+ * 额外的"冻结代数"概念)——比 DeepSeek Harness 的实现更简单,这里直接采用。
7
+ */
8
+ export interface ParseCache {
9
+ /** 当前已知的全部块(冻结 + 尚不稳定的尾部),按源码顺序——上一次
10
+ * 增量解析的完整结果,下一帧从这里的最后一个块的起点开始重新解析
11
+ * (见 `incrementalParse`),不直接拿来渲染。 */
12
+ blocks: RootContent[];
13
+ /** 上一次成功走增量路径时的完整输入,用于判断新输入是不是纯追加。 */
14
+ input: string;
15
+ /** 是否已经在文档里见过定义语法——一旦见过就一直为 true(退化成整篇
16
+ * 重新解析是"一票否决、不会再变回增量路径"的单向状态),不需要每帧
17
+ * 重新确认已经扫过的部分。 */
18
+ hasDefinition: boolean;
19
+ /** 已经确认"从文档开头到这个偏移量之间的完整行都不含定义语法"的进度——
20
+ * 一定落在某一行的行首(要么是 0,要么紧跟在某个 `\n` 之后),下一次
21
+ * 只需要从这里往后扫新增的文本,不用把已经扫过的部分重新跑一遍正则。
22
+ * 最后一行只要还没被 `\n` 收尾就永远不推进过它的行首——它可能还在
23
+ * 增长,提前当作"已确认无定义"会漏检定义语法后半截还没到达的情况。
24
+ * 只统计不在围栏代码块内的行——代码示例里出现 `[ref]: url` 形状的
25
+ * 文本很常见(这本身就是个 markdown 渲染器项目,模型很可能给出这样的
26
+ * 教学示例),真按字面匹配会永久误判成"文档里有定义语法",之后每一帧
27
+ * 都退化成整篇 O(N) 重新解析,直接抵消增量解析本来要解决的性能问题;
28
+ * 见 `updateDefinitionScan`。 */
29
+ definitionScanOffset: number;
30
+ /** `definitionScanOffset` 这个位置是不是落在一个还没闭合的围栏代码块
31
+ * 内部——跨帧持续,配合 `fenceChar`/`fenceLength` 判断后续新增的行
32
+ * 是不是"闭合围栏",不是每次都从头重新判断整篇文本的围栏嵌套状态。 */
33
+ insideFencedCode: boolean;
34
+ fenceChar: string | null;
35
+ fenceLength: number;
36
+ }
37
+ export declare function createParseCache(): ParseCache;
38
+ /**
39
+ * 一个块只有在"以空行结尾,且后面至少还有两个块"时才能确定不会再被后续
40
+ * 内容改写——最后一个块必然是当前解析前沿,倒数第二个块是安全边际。取
41
+ * 倒数第三个块及之前的块作为安全冻结区。
42
+ */
43
+ /** 导出给增量解析等价性质测试用——测试要拿它对"整篇重新解析"的结果算出
44
+ * 同样的 `suppressPendingTableTail` 范围,才能公平比较两条路径的输出。 */
45
+ export declare function countStableBlocks(blockCount: number): number;
46
+ export interface IncrementalResult {
47
+ root: Root;
48
+ cache: ParseCache;
49
+ }
50
+ /** 用一个不渲染任何内容的占位节点替换掉"疑似正在长成表格、还没能确定"的
51
+ * 尾部段落——憋到确定下来(变成真正的 table,或者冻结成确定的普通段落)
52
+ * 再显示,不把过程中的原始竖线文字暴露给用户。导出给测试用:增量等价性质
53
+ * 测试要拿它对"整篇重新解析"的结果做同样的处理,才能验证增量路径没有
54
+ * 引入*额外*的差异(这里的差异是刻意的显示层降级,两边都做才是公平对比)。 */
55
+ export declare function suppressPendingTableTail(blocks: RootContent[], verifiedCount: number): RootContent[];
56
+ export interface IncrementalParseOptions {
57
+ /** streaming 模式下,在把"最后一个尚不稳定的块"渲染出来之前先过一遍
58
+ * 未闭合语法降级(给还没等到闭合符的自配对标记在末尾补上闭合符,让
59
+ * 它按最终形态渲染,只在对应节点上打一个"待确认"标记)。这一层健壮性
60
+ * 处理只影响*这一帧的显示*,不参与缓存/偏移量的持久化——补的闭合符会
61
+ * 改变字符串长度,如果拿它算出来的 offset 去驱动下一次的切片位置,
62
+ * 长度差会累积成偏移量错位,往后每一次切片都会切错地方。所以 `cache.blocks`
63
+ * 全程只认*原始未处理*文本解析出来的结果;这一层只用来现算现扔地重新
64
+ * 渲染最后那一个尚不稳定的块,每帧都重新做一遍,不持久化,不会有偏移量
65
+ * 累积错位的问题。 */
66
+ healTail?: (text: string) => HealResult;
67
+ }
68
+ /**
69
+ * 给定当前完整目标文本,返回可以直接渲染的 mdast 根节点,以及供下一次调用
70
+ * 复用的缓存。纯函数——不持有状态,调用方(渲染组件)自己在 ref 里存
71
+ * `ParseCache` 并逐帧传进来。
72
+ */
73
+ export declare function incrementalParse(cache: ParseCache, input: string, options?: IncrementalParseOptions): IncrementalResult;
@@ -0,0 +1,18 @@
1
+ import type { MarkdownRendererProps } from "./types";
2
+ export type { CodeBlockOverrides, LinkSafetyConfig, MarkdownComponents, MarkdownRendererProps, RawHtmlPlugin, } from "./types";
3
+ export { DEFAULT_ALLOWED_TAGS, type AllowedTags } from "./sanitize";
4
+ /**
5
+ * 增量 Markdown 渲染器:`packages/agent-react` 与 `apps/web` 两处
6
+ * `MarkdownContent.tsx` 共用的核心引擎(对齐 `CLAUDE.md`"共享能力先做进
7
+ * SDK 再让内置 Web 用"这条原则)。取代原来包一层 `<Streamdown>` 的做法——
8
+ * 用真正的增量解析(`incremental.ts`)替换 streamdown 每次都重新 lex 全部
9
+ * 已到达内容的实现,解决长回复流式渲染耗时随内容长度平方增长的问题。
10
+ *
11
+ * `mode="streaming"` 时:增量解析冻结前缀 + 未闭合语法降级都生效;
12
+ * `mode="static"`(默认)时:整篇一次性解析,不做任何增量假设——这是
13
+ * "流式与历史必须产生相同结果"这条硬规则唯一允许的例外只在真正实时直播
14
+ * 里生效的直接体现。
15
+ */
16
+ export declare function MarkdownRenderer({ children, className, mode, allowedTags, components, rawHtmlPlugins, codeBlockOverrides, }: MarkdownRendererProps): import("react").DetailedReactHTMLElement<{
17
+ className: string;
18
+ }, HTMLElement>;
@@ -0,0 +1,19 @@
1
+ import type { Root } from "mdast";
2
+ /**
3
+ * Markdown 字符串 -> mdast 语法树,启用 GFM(表格、任务列表、删除线、脚注、
4
+ * 自动链接)。不做增量缓存——那是 `incremental.ts` 的职责,这里只是纯函数
5
+ * 封装,方便增量解析对"整篇重新解析"和"只解析一段尾部"复用同一套扩展配置。
6
+ *
7
+ * `cjkFriendlyExtension` 覆盖了 CommonMark 默认的强调符号(`**`/`_`)
8
+ * flanking 判断——按标准规则,中文标点紧贴在闭合标记前、后面又紧跟着
9
+ * 普通汉字(既不是空白也不是标点)时,这个闭合标记既不算左 flanking
10
+ * 也不算右 flanking,整段 `**xxx**` 会被判定成没有真正的闭合符,渲染成
11
+ * 字面量星号——这是中文场景下最常见的加粗写法,例如"这一问是:**重点
12
+ * 内容**下一句",标准 CommonMark 库(真实生产环境验证过)都会渲染
13
+ * 失败。换引擎前 streamdown 通过 `@streamdown/cjk`(内部就是这个
14
+ * 扩展)解决了这个问题,新引擎必须带上同一个扩展,否则是从"迁移前能用"
15
+ * 倒退成"迁移后不能用"。必须排在 `gfm()` 之前——它需要在 GFM 扩展
16
+ * (比如删除线)接管同一批触发字符之前先接管强调符号的 tokenizer,
17
+ * 这是上游包自己文档里写明的顺序要求。
18
+ */
19
+ export declare function parseMarkdown(markdown: string): Root;
@@ -0,0 +1,79 @@
1
+ import type { Definition, FootnoteDefinition, FootnoteReference, Root } from "mdast";
2
+ import { type ReactNode } from "react";
3
+ import { type AllowedTags } from "./sanitize";
4
+ import type { CodeBlockOverrides, MarkdownComponents, RawHtmlPlugin } from "./types";
5
+ export interface RenderContext {
6
+ components?: MarkdownComponents;
7
+ allowedTags: AllowedTags;
8
+ rawHtmlPlugins?: RawHtmlPlugin[];
9
+ codeBlockOverrides?: CodeBlockOverrides;
10
+ /** 收集渲染过程中遇到的脚注定义,root 渲染完之后统一在文末生成脚注区。
11
+ * 由 `collectFootnoteMetadata` 在渲染前对完整树做一次独立遍历填好,
12
+ * `renderNode` 只读不写——见该函数的注释,原因是按块缓存命中时会跳过
13
+ * `renderNode`,不能再依赖它的遍历顺序当副作用来源。 */
14
+ footnoteDefinitions: Map<string, FootnoteDefinition>;
15
+ /** 脚注按"引用点第一次出现的顺序"编号(1、2、3……),不是按标识符文本
16
+ * 本身——`[^note]` 应该显示成"1"而不是字面量"note",跟 GFM 脚注惯例
17
+ * 和迁移前 streamdown 的行为一致。 */
18
+ footnoteOrder: Map<string, number>;
19
+ /** 同一个脚注标识符可能被正文引用不止一次(`[^note]` 出现两次)——每个
20
+ * 引用点必须有自己独立的 id,不能都叫 `fnref-note`(重复 id 是无效
21
+ * HTML,`aria-describedby` 和跳转都只能找到第一个);这张表记录每个
22
+ * 标识符已经生成过的引用点 id,脚注区据此给每一次引用各自渲染一条
23
+ * 回链,而不是只有第一次引用能跳回去。 */
24
+ footnoteRefIds: Map<string, string[]>;
25
+ /** 每一个具体的 `footnoteReference` 节点对象自己分到的引用点 id——
26
+ * `footnoteRefIds` 是按标识符聚合的完整列表(脚注区渲染全部回链要用),
27
+ * 但单个引用点在正文里渲染自己的 `id` 属性时,需要知道"我是这个标识符
28
+ * 第几次出现",而按块缓存命中会跳过一部分引用点节点的渲染调用,不能在
29
+ * `renderNode` 遍历时临时数"目前见过几次"来推算——用节点对象本身的
30
+ * 引用当 key,跟渲染是否命中缓存无关。 */
31
+ footnoteRefIdByNode: WeakMap<FootnoteReference, string>;
32
+ /** 流式模式下,当前这一帧顶层块列表里的最后一个节点——`codeBlockOverrides`
33
+ * 那类接管整块渲染的覆盖组件(比如 mermaid 图表)不能对"还可能是最后一
34
+ * 块、围栏还没写完"的代码块生效:围栏没闭合时这段代码的内容每一帧都在
35
+ * 变,如果立刻挂载覆盖组件,它会对着一堆语法上通常无效的半截内容反复
36
+ * 触发一次昂贵的渲染/解析,界面观感是卡顿甚至短暂报错。只有确认后面已
37
+ * 经跟上了别的内容(围栏必然已经闭合,不然 CommonMark 不会开始下一个
38
+ * 块),或者已经不在流式模式,才让覆盖组件接管。 */
39
+ lastTopLevelNode: unknown;
40
+ isStreaming: boolean;
41
+ /** 引用式链接/图片(`[text][ref]` + `[ref]: url`)的定义表——mdast 只有
42
+ * 定义真的存在时才会把引用节点解析成 linkReference/imageReference(没
43
+ * 匹配上定义的引用,解析阶段就已经退化成字面量文本节点,不会走到这里),
44
+ * 所以渲染时按 identifier 查这张表理论上总能查到;查不到时降级成只渲染
45
+ * children(对链接)或者不渲染(对图片),不让整段内容凭空消失。 */
46
+ definitions: Map<string, Definition>;
47
+ }
48
+ /** 递归收集整棵树里的 `definition` 节点——引用式链接/图片的定义可以出现在
49
+ * 文档任意位置(包括引用点之前或之后、嵌套在 blockquote/list 内部),
50
+ * 渲染 linkReference/imageReference 之前必须先有这张完整的表,不能等
51
+ * 渲染到 definition 节点那一刻才收集(那时引用点可能已经渲染过了)。 */
52
+ export declare function collectDefinitions(root: Root): Map<string, Definition>;
53
+ export interface FootnoteMetadata {
54
+ footnoteDefinitions: Map<string, FootnoteDefinition>;
55
+ footnoteOrder: Map<string, number>;
56
+ footnoteRefIds: Map<string, string[]>;
57
+ footnoteRefIdByNode: WeakMap<FootnoteReference, string>;
58
+ }
59
+ /** 独立于渲染遍历、单独对完整树做一次收集——脚注编号/引用点 id/定义内容
60
+ * 原来是 `renderNode` 遍历到 `footnoteReference`/`footnoteDefinition`
61
+ * 节点时顺手填的副作用,但 `renderMdastRoot` 的按块缓存命中时会完全
62
+ * 跳过 `renderNode`:一段脚注引用/定义所在的块一旦被冻结,后续帧里这
63
+ * 几张表就再也不会被重新填充,脚注区可能因此整体消失,或者冻结边界前后
64
+ * 混进两套不完整的编号,跟真实点开的顺序对不上。做法上跟 `collectDefinitions`
65
+ * 一致:对当前这一帧完整的树做一次独立遍历,不依赖某个节点是不是真的
66
+ * 走过了渲染。 */
67
+ export declare function collectFootnoteMetadata(root: Root): FootnoteMetadata;
68
+ export declare function renderFootnotesSection(context: RenderContext): ReactNode;
69
+ /**
70
+ * 顶层块级渲染,带按块缓存——增量解析冻结的块在多次渲染之间保持同一个
71
+ * 对象引用(见 `incremental.ts`),这里以对象引用做 key 缓存渲染结果,
72
+ * 冻结块跨帧直接复用同一个 React 元素对象,React 的 fiber 协调在
73
+ * props 引用相同时会跳过整棵子树的重新渲染——只解决了解析层的 O(N²)
74
+ * 还不够,渲染层每帧都重新 build 一遍全部元素同样是长文档卡顿的来源。
75
+ *
76
+ * 只在顶层(`root.children`)做这一层缓存:块一旦冻结就是原子不可变单元,
77
+ * 跟增量解析"冻结前缀"的粒度对齐;嵌套在块内部的子节点不需要单独缓存。
78
+ */
79
+ export declare function renderMdastRoot(root: Root, context: RenderContext, blockCache?: WeakMap<object, ReactNode>, groupIdentityMap?: WeakMap<object, object>): ReactNode[];
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 安全设计分两层,两层都要过(对齐需求 4):
3
+ *
4
+ * 1. 标签/属性白名单——只有声明过的标签名和对应属性才会被当作"结构化标签"
5
+ * 处理,其余原始 HTML 一律转义成文本。这一层解决"允许哪些自定义标签"。
6
+ * 2. 协议黑名单(不受第 1 层白名单影响)——链接 href、图片 src 在渲染前
7
+ * 统一检查协议,危险协议一律拒绝渲染成可交互元素。这一步在所有节点
8
+ * 类型的链接/图片渲染路径上统一收口成这一个函数调用,不分散在各处
9
+ * 判断,避免以后新增节点类型时漏掉。
10
+ */
11
+ export type AllowedTags = Record<string, string[]>;
12
+ /** 按标签名/属性名查一张"名字 -> 值"的表时,不能用 `obj[key]`/`key in obj`
13
+ * 直接查——markdown 内容不可信,标签名可以是任意字符串,`constructor`/
14
+ * `toString`/`hasOwnProperty` 这类名字会命中原型链继承来的属性而不是
15
+ * "没有声明",把一个函数/别的东西当成配置值用下去会直接崩溃整条消息的
16
+ * 渲染。所有按标签名/组件名查白名单、查覆盖组件的地方都要经过这个函数。 */
17
+ export declare function ownLookup<T>(table: Record<string, T> | undefined, key: string): T | undefined;
18
+ /**
19
+ * 默认放行的标准 HTML 标签——AI 生成内容里常见的、语义上安全的行内标签,
20
+ * 不需要调用方额外声明就可以直接渲染。调用方通过 `allowedTags` 声明的
21
+ * 自定义标签(比如业务里的 `system-reminder`)在这个默认集合之上叠加,
22
+ * 不覆盖。
23
+ */
24
+ export declare const DEFAULT_ALLOWED_TAGS: AllowedTags;
25
+ /** 按标签名逐个合并属性列表,不是整表浅拼接——调用方对某个标签重新声明
26
+ * 属性列表(比如只是想给 `details` 多加一个属性)不应该把内置的那份
27
+ * 属性列表整个挤掉(`{...DEFAULT_ALLOWED_TAGS, ...custom}` 会把 `details`
28
+ * 内置的 `open` 属性丢掉),这跟"自定义声明是叠加在内置白名单之上"的
29
+ * 文档承诺矛盾。 */
30
+ export declare function mergeAllowedTags(custom: AllowedTags | undefined): AllowedTags;
31
+ /**
32
+ * 协议黑名单:`javascript:`(含大小写、含空白/控制字符干扰的变体)一律
33
+ * 拒绝。不在名单里的协议(http/https/mailto/tel/相对路径等)一律放行——
34
+ * 调用方如果需要更严格的白名单式协议控制,用 `linkSafety` 扩展点自己叠加,
35
+ * 这一层只兜底"绝对不能允许"的场景。
36
+ */
37
+ export declare function isDangerousUrl(url: string): boolean;
38
+ /** 对链接 href/图片 src 做协议检查;危险协议返回 `null`(调用方据此去掉该属性或整体降级成纯文本)。 */
39
+ export declare function sanitizeUrl(url: string | undefined | null): string | undefined;
40
+ export interface ParsedTag {
41
+ tagName: string;
42
+ isClosing: boolean;
43
+ isSelfClosing: boolean;
44
+ attributes: Record<string, string>;
45
+ }
46
+ export declare function parseSingleTag(raw: string): ParsedTag | null;
47
+ export declare function normalizeBooleanAttributes(attributes: Record<string, string>): Record<string, unknown>;
48
+ /** 按标签的属性白名单过滤,同时对 `href`/`src` 额外做协议检查——两层
49
+ * 检查在这一个函数里收口,不分散在各处判断。 */
50
+ export declare function filterAttributes(tagName: string, attributes: Record<string, string>, allowedTags: AllowedTags): Record<string, string>;
@@ -0,0 +1,53 @@
1
+ import type { ComponentType, JSX, ReactNode } from "react";
2
+ import type { AllowedTags } from "./sanitize";
3
+ export type { AllowedTags };
4
+ /** 标签名(标准 HTML 或者 `allowedTags` 声明的自定义标签)-> React 组件。
5
+ * 形态对齐 streamdown 的 `components` prop,降低迁移改动量——标准标签名
6
+ * 拿到对应的 intrinsic props 类型(比如 `pre` 覆盖组件的 `children` 能
7
+ * 正确推断成 `ReactNode`),自定义标签名落到通用的 Record<string, unknown>。 */
8
+ export type MarkdownComponents = {
9
+ [Key in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[Key]>;
10
+ } & {
11
+ [tagName: string]: ComponentType<Record<string, unknown>> | undefined;
12
+ };
13
+ /**
14
+ * 原始 HTML 后处理钩子:解析出标签名和属性之后、渲染成 React 元素之前
15
+ * 调用,可以按需改写属性(比如把 kebab-case 属性名双写成 camelCase)。
16
+ * 返回值替换原有属性;不提供时原样使用 sanitize 之后的属性。
17
+ */
18
+ export type RawHtmlPlugin = (tagName: string, attributes: Record<string, string>) => Record<string, string>;
19
+ /** 对齐 streamdown 的 `linkSafety`——目前仓库内唯一用法是 `{ enabled: false }`
20
+ * 显式关闭,没有消费方真的启用过跳转确认弹窗,这里只保留类型形状,
21
+ * `enabled: true` 时暂不渲染确认弹窗(等真的有消费方需要再补)。 */
22
+ export interface LinkSafetyConfig {
23
+ enabled: boolean;
24
+ onLinkCheck?: (url: string) => Promise<boolean> | boolean;
25
+ renderModal?: (props: {
26
+ isOpen: boolean;
27
+ onClose: () => void;
28
+ onConfirm: () => void;
29
+ url: string;
30
+ }) => ReactNode;
31
+ }
32
+ /** 按围栏代码块的语言标注整体替换渲染(不是 `components.code` 那种"改写
33
+ * `<code>` 内部",而是连 `<pre>` 外壳都不要,整个块换成调用方自己的组件)。
34
+ * 给 mermaid 这类需要跳出代码块外壳、直接渲染图表/画布的场景用;语言不在
35
+ * 这张表里的代码块走正常的 `<pre><code class="language-xxx">` 路径。 */
36
+ export type CodeBlockOverrides = Record<string, ComponentType<{
37
+ code: string;
38
+ }>>;
39
+ export interface MarkdownRendererProps {
40
+ children: string;
41
+ className?: string;
42
+ /** 流式渲染传 "streaming"(打字机式增量解析 + 未闭合语法降级生效),
43
+ * 历史静态内容传 "static"(整篇一次性解析,不做增量缓存)。 */
44
+ mode?: "streaming" | "static";
45
+ /** 会话 id;透传给需要区分会话的下游组件(会话内图片路径解析等)。 */
46
+ sessionId?: string;
47
+ components?: MarkdownComponents;
48
+ /** 自定义标签的白名单声明,跟内置的标准安全标签集合叠加,不覆盖。 */
49
+ allowedTags?: AllowedTags;
50
+ rawHtmlPlugins?: RawHtmlPlugin[];
51
+ linkSafety?: LinkSafetyConfig;
52
+ codeBlockOverrides?: CodeBlockOverrides;
53
+ }
@@ -0,0 +1,13 @@
1
+ import type { SessionPluginConfigSet, SessionPluginConfigState } from "@blade-hq/agent-client";
2
+ export type ConfigPath = (string | number)[];
3
+ export type ConfigValues = Record<string, unknown>;
4
+ export declare function createConfigDraft(config: SessionPluginConfigState): {
5
+ values: ConfigValues;
6
+ retained: Map<string, string>;
7
+ };
8
+ /** Array edits address the original indices because the API applies sets before removals. */
9
+ export declare function originalConfigPath(path: ConfigPath, removed: Set<string>): ConfigPath;
10
+ export declare function configDraftOperations(initial: ConfigValues, values: ConfigValues, retained: Map<string, string>, removed: Set<string>): {
11
+ set: SessionPluginConfigSet[];
12
+ remove: string[];
13
+ };
@@ -0,0 +1,6 @@
1
+ import type { SessionPluginConfigState } from "@blade-hq/agent-client";
2
+ export type JsonSchema = Record<string, unknown>;
3
+ export declare function pointerOf(parts: (string | number)[]): string;
4
+ export declare function pointerParts(pointer: string): string[];
5
+ export declare function formSupport(schema: JsonSchema | null): string | null;
6
+ export declare function configIsEditable(state: SessionPluginConfigState): boolean;
@@ -0,0 +1,40 @@
1
+ import type { PluginCatalogEntry, SessionPlugin, SessionPluginConfigSummary } from "@blade-hq/agent-client";
2
+ /**
3
+ * 连接器面板里的一行插件。
4
+ *
5
+ * 展示字段(名称、图标)和启用状态分开表达:目录接口在没有会话时也能给出名称与
6
+ * 图标,而「这次会话是否启用」只有会话接口知道。首页候选行恒为 `active: false`,
7
+ * 因为它只是待选,不代表已经激活。
8
+ */
9
+ export interface PluginConnectorItem {
10
+ name: string;
11
+ display_name: string | null;
12
+ /** 图标 token;null 时显示统一占位,不隐藏整行。 */
13
+ icon: string | null;
14
+ /** 图标从哪个接口取:账号级目录,还是会话内已准备的包。 */
15
+ iconSource: "account" | "session";
16
+ installed: boolean | null;
17
+ /** 会话里已提交的选择;首页候选恒为 false。 */
18
+ active: boolean;
19
+ status: string;
20
+ reason?: string | null;
21
+ config?: SessionPluginConfigSummary;
22
+ projected: boolean;
23
+ }
24
+ /** 首页候选:只有账号级目录,没有会话状态。 */
25
+ export declare function catalogToConnectorItems(catalog: PluginCatalogEntry[]): PluginConnectorItem[];
26
+ /**
27
+ * 会话列表叠加账号目录。
28
+ *
29
+ * 目录可用时展示字段以目录为准(包可能还没准备,会话列表拿不到名称和图标),
30
+ * 目录不可用时退回会话行自己的字段,这样已选插件仍然可以取消。
31
+ */
32
+ export declare function mergeConnectorItems(sessionPlugins: SessionPlugin[], catalog: PluginCatalogEntry[] | null): PluginConnectorItem[];
33
+ /**
34
+ * 一行的配置说明文案;权威状态来自配置接口。
35
+ *
36
+ * `unchecked` 刻意没有文案:列表没有读过用户的 Home,说任何话都是噪音,
37
+ * 入口是开关本身。
38
+ */
39
+ /** 按名称和显示名做本地过滤;空查询返回全部。 */
40
+ export declare function filterConnectorItems(items: PluginConnectorItem[], query: string): PluginConnectorItem[];
@@ -0,0 +1 @@
1
+ export declare const QueuePanelAdjacencyContext: import("react").Context<boolean | null>;
@@ -0,0 +1,40 @@
1
+ import type { BladeClient, SessionPluginActivation, SessionPluginConfigState } from "@blade-hq/agent-client";
2
+ import type { ConfiguringPlugin } from "./PluginConnectorList";
3
+ import { type PluginConnectorItem } from "./plugin-connector";
4
+ export interface UseSessionPluginActivationResult {
5
+ items: PluginConnectorItem[];
6
+ loading: boolean;
7
+ /** 插件列表本身读不到(Hub 不可用等),已有选择仍然可取消。 */
8
+ listError: boolean;
9
+ /** 某次启用/停用/准备失败了。 */
10
+ mutationError: boolean;
11
+ /** 正在为哪些插件提交意图;对应的开关在转圈,但仍然可点(点是取消)。 */
12
+ inFlight: Record<string, true>;
13
+ busy: boolean;
14
+ configuring: ConfiguringPlugin | null;
15
+ reload: () => void;
16
+ toggle: (item: PluginConnectorItem, next: boolean) => void;
17
+ retry: (item: PluginConnectorItem) => void;
18
+ editConfig: (item: PluginConnectorItem) => void;
19
+ onConfigSaved: (state: SessionPluginConfigState) => void;
20
+ onConfigClose: () => void;
21
+ }
22
+ /**
23
+ * 会话内插件启用的唯一实现。
24
+ *
25
+ * 顺序是「先准备配置,再提交 activation」:缺配置时开关不落到已启用、立刻弹表单,
26
+ * 保存成功才提交;取消、校验失败、配置读取失败都不提交。绝不先持久化 active=true
27
+ * 再靠补偿关掉——那样用户下次刷新会看到一个从没被成功打开的插件。
28
+ *
29
+ * 停用仍然直接提交:在服务端它只是取消一条数据库选择,不准备包、不连 MCP。但如果
30
+ * 这次「启用」只是还在准备、压根没提交过 activation,取消就只是放弃本地意图。
31
+ *
32
+ * 所有异步响应都按 (lifetime, mutation) 校验:迟到的准备或激活结果不会把已经取消 /
33
+ * 已经切走会话的插件重新打开;反过来,旧响应发现服务端落库顺序与用户最新意图不同时,
34
+ * 会重新提交最新意图。
35
+ */
36
+ export declare function useSessionPluginActivation({ client, sessionId, onChange, }: {
37
+ client: BladeClient;
38
+ sessionId: string;
39
+ onChange?: (plugin: SessionPluginActivation) => void;
40
+ }): UseSessionPluginActivationResult;
package/dist/context.d.ts CHANGED
@@ -11,3 +11,4 @@ export interface BladeProviderProps {
11
11
  export declare function BladeProvider({ client, children }: BladeProviderProps): import("react/jsx-runtime").JSX.Element;
12
12
  /** 读取当前 BladeProvider 提供的 client。不在 Provider 内使用时抛错。 */
13
13
  export declare function useBladeClient(): BladeClient;
14
+ export declare function useOptionalBladeClient(): BladeClient | null;
@@ -18,9 +18,10 @@ export declare class BladeChatElement extends HTMLElement {
18
18
  connectedCallback(): void;
19
19
  /**
20
20
  * 自定义样式口子:<blade-chat> 的直接 <style> 子元素会被拷入 Shadow DOM,
21
- * 排在内置样式之后(同优先级时后者胜)。配合内置的 .blade-chat-* 类名
22
- * CSS 变量(--primary / --background 等,页面直接对 blade-chat 元素
23
- * 设置即可穿透),无需构建工具即可深度定制。
21
+ * 排在内置样式之后(同优先级时后者胜)。配合内置的 .blade-chat-* 类名、
22
+ * ::part()(每个可见区块都带与锚点同名的 part,如
23
+ * blade-chat::part(user-bubble))与 CSS 变量(--primary / --background 等,
24
+ * 页面直接对 blade-chat 元素设置即可穿透),无需构建工具即可深度定制。
24
25
  */
25
26
  private adoptUserStyles;
26
27
  disconnectedCallback(): void;