@icen.ai/ui 0.4.0 → 0.8.0

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 (155) hide show
  1. package/AGENTS.md +256 -0
  2. package/README.md +47 -13
  3. package/dist/base.css +31 -4
  4. package/dist/behaviors/accordion.d.mts +4 -0
  5. package/dist/behaviors/accordion.mjs +4 -3
  6. package/dist/behaviors/ai-chat.d.mts +57 -0
  7. package/dist/behaviors/ai-chat.mjs +557 -0
  8. package/dist/behaviors/ai-composer.d.mts +112 -0
  9. package/dist/behaviors/ai-composer.mjs +2032 -0
  10. package/dist/behaviors/ai-core.d.mts +151 -0
  11. package/dist/behaviors/ai-core.mjs +491 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +282 -0
  14. package/dist/behaviors/ai-panel.d.mts +98 -0
  15. package/dist/behaviors/ai-panel.mjs +1012 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1055 -0
  18. package/dist/behaviors/ai-tool.d.mts +18 -0
  19. package/dist/behaviors/ai-tool.mjs +2125 -0
  20. package/dist/behaviors/ai-tools.d.mts +81 -0
  21. package/dist/behaviors/ai-tools.mjs +1658 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +101 -0
  24. package/dist/behaviors/carousel.d.mts +4 -0
  25. package/dist/behaviors/carousel.mjs +34 -16
  26. package/dist/behaviors/charts.d.mts +250 -0
  27. package/dist/behaviors/charts.mjs +1301 -448
  28. package/dist/behaviors/command-palette.d.mts +8 -0
  29. package/dist/behaviors/command-palette.mjs +189 -0
  30. package/dist/behaviors/context-menu.d.mts +36 -0
  31. package/dist/behaviors/context-menu.mjs +6 -2
  32. package/dist/behaviors/copy.d.mts +4 -0
  33. package/dist/behaviors/copy.mjs +7 -6
  34. package/dist/behaviors/datatable.d.mts +78 -0
  35. package/dist/behaviors/datatable.mjs +54 -11
  36. package/dist/behaviors/date-picker.d.mts +4 -0
  37. package/dist/behaviors/date-picker.mjs +296 -0
  38. package/dist/behaviors/dropdown.d.mts +4 -0
  39. package/dist/behaviors/dropdown.mjs +7 -3
  40. package/dist/behaviors/input.d.mts +4 -0
  41. package/dist/behaviors/input.mjs +184 -8
  42. package/dist/behaviors/modal.d.mts +11 -0
  43. package/dist/behaviors/modal.mjs +11 -6
  44. package/dist/behaviors/nav.d.mts +7 -0
  45. package/dist/behaviors/nav.mjs +25 -9
  46. package/dist/behaviors/notification.d.mts +172 -0
  47. package/dist/behaviors/notification.mjs +908 -0
  48. package/dist/behaviors/popover.d.mts +46 -0
  49. package/dist/behaviors/popover.mjs +2 -1
  50. package/dist/behaviors/select.d.mts +4 -0
  51. package/dist/behaviors/select.mjs +268 -34
  52. package/dist/behaviors/sidebar.d.mts +4 -0
  53. package/dist/behaviors/sidebar.mjs +4 -3
  54. package/dist/behaviors/slider.d.mts +4 -0
  55. package/dist/behaviors/slider.mjs +4 -3
  56. package/dist/behaviors/split-pane.d.mts +4 -0
  57. package/dist/behaviors/split-pane.mjs +102 -0
  58. package/dist/behaviors/tabs.d.mts +8 -0
  59. package/dist/behaviors/tabs.mjs +44 -19
  60. package/dist/behaviors/tag-input.d.mts +4 -0
  61. package/dist/behaviors/tag-input.mjs +12 -3
  62. package/dist/behaviors/theme.d.mts +25 -0
  63. package/dist/behaviors/theme.mjs +3 -1
  64. package/dist/behaviors/toast.d.mts +26 -0
  65. package/dist/behaviors/toast.mjs +24 -18
  66. package/dist/behaviors/tree.d.mts +4 -0
  67. package/dist/behaviors/tree.mjs +4 -3
  68. package/dist/behaviors/upload.d.mts +4 -0
  69. package/dist/behaviors/upload.mjs +205 -17
  70. package/dist/components/ai-chat.css +1037 -0
  71. package/dist/components/ai-chat.mjs +3 -0
  72. package/dist/components/ai-composer.mjs +3 -0
  73. package/dist/components/ai-context.mjs +2 -0
  74. package/dist/components/ai-diff.css +311 -0
  75. package/dist/components/ai-diff.mjs +3 -0
  76. package/dist/components/ai-files.mjs +3 -0
  77. package/dist/components/ai-message.mjs +3 -0
  78. package/dist/components/ai-panel.css +682 -0
  79. package/dist/components/ai-reasoning.mjs +3 -0
  80. package/dist/components/ai-subagent.mjs +2 -0
  81. package/dist/components/ai-todo.mjs +2 -0
  82. package/dist/components/ai-tool-call.mjs +2 -0
  83. package/dist/components/ai-tool.css +532 -0
  84. package/dist/components/ai-usage.mjs +3 -0
  85. package/dist/components/back-top.css +88 -0
  86. package/dist/components/back-top.mjs +3 -0
  87. package/dist/components/badge.css +127 -0
  88. package/dist/components/badge.mjs +2 -0
  89. package/dist/components/breadcrumb.css +53 -0
  90. package/dist/components/btn.css +4 -7
  91. package/dist/components/card.css +66 -1
  92. package/dist/components/chart-bar.css +10 -0
  93. package/dist/components/chart-calendar.mjs +5 -0
  94. package/dist/components/chart-gauge.css +1 -1
  95. package/dist/components/chart-heatmap.css +40 -0
  96. package/dist/components/chart-scatter.css +24 -0
  97. package/dist/components/chart-scatter.mjs +4 -0
  98. package/dist/components/charts.css +86 -1
  99. package/dist/components/charts.mjs +1 -0
  100. package/dist/components/command-palette.css +246 -0
  101. package/dist/components/command-palette.mjs +3 -0
  102. package/dist/components/content.css +221 -8
  103. package/dist/components/copy.css +93 -0
  104. package/dist/components/copy.mjs +3 -0
  105. package/dist/components/datatable.css +4 -8
  106. package/dist/components/date-picker.css +240 -0
  107. package/dist/components/date-picker.mjs +3 -0
  108. package/dist/components/empty.css +69 -3
  109. package/dist/components/feedback.css +241 -10
  110. package/dist/components/form.css +129 -11
  111. package/dist/components/form.mjs +2 -0
  112. package/dist/components/input.css +25 -2
  113. package/dist/components/layout.css +36 -36
  114. package/dist/components/media.css +153 -6
  115. package/dist/components/menu.css +2 -2
  116. package/dist/components/modal.css +1 -1
  117. package/dist/components/nav.css +106 -2
  118. package/dist/components/notification.css +387 -0
  119. package/dist/components/notification.mjs +3 -0
  120. package/dist/components/pagination.css +1 -1
  121. package/dist/components/panel.css +104 -9
  122. package/dist/components/pill.css +82 -12
  123. package/dist/components/scroll-area.css +112 -0
  124. package/dist/components/scroll-area.mjs +2 -0
  125. package/dist/components/segmented.css +80 -5
  126. package/dist/components/segmented.mjs +1 -0
  127. package/dist/components/select.css +104 -17
  128. package/dist/components/sidebar.css +119 -1
  129. package/dist/components/slider.css +113 -0
  130. package/dist/components/split-pane.css +111 -0
  131. package/dist/components/split-pane.mjs +3 -0
  132. package/dist/components/stat.css +131 -12
  133. package/dist/components/steps.css +98 -1
  134. package/dist/components/switch.css +145 -7
  135. package/dist/components/table.css +111 -29
  136. package/dist/components/tabs.css +132 -22
  137. package/dist/components/tag-input.css +4 -4
  138. package/dist/components/tag.css +164 -0
  139. package/dist/components/tag.mjs +2 -0
  140. package/dist/components/toast.css +122 -5
  141. package/dist/components/toolbar.css +101 -0
  142. package/dist/components/toolbar.mjs +2 -0
  143. package/dist/components/tooltip.css +2 -2
  144. package/dist/components/upload.css +61 -4
  145. package/dist/index.d.mts +37 -0
  146. package/dist/index.mjs +9409 -929
  147. package/dist/registry.json +176 -4
  148. package/dist/retro-effects.css +21 -17
  149. package/dist/tokens/colors.css +4 -1
  150. package/dist/tokens/retro-effects.css +21 -17
  151. package/dist/tokens/style-profiles.css +21 -0
  152. package/dist/tokens.css +25 -1
  153. package/dist/ui.css +9616 -2839
  154. package/docs/spec/ai-native.md +659 -0
  155. package/package.json +15 -5
@@ -0,0 +1,659 @@
1
+ # AI 原生组件族 · 设计规格(v0.6.x 新增)
2
+
3
+ > 唯一事实源。施工代理按此文件执行,不得偏离契约(类名 / 导出签名 / 事件名)。
4
+ > 设计调研依据:AG-UI 协议、Vercel AI SDK UIMessage parts、OpenAI/Anthropic wire 格式、
5
+ > MCP tools schema、A2A Task 状态;产品侧 Claude Code / Kimi Code / Cursor / Copilot / Codex 的已标准化模式。
6
+
7
+ ## 0. 设计原则
8
+
9
+ 1. **继承用 CSS 变量级联实现**:`.ai-item` 基元定义 `--ai-item-*` 局部变量,kind/变体只覆盖变量不改结构(btn.css 的 `--btn-*` 模式的推广)。
10
+ 2. **多态用 kind 注册表**:MCP、skill 不是独立组件,是 tool-call 的 kind。`registerAiKind()` 开放第三方扩展。
11
+ 3. **组件只消费归一化模型**:业界格式(OpenAI / Anthropic / AI SDK / 素朴)由适配层统一翻译,组件不认识任何 provider。
12
+ 4. **状态机是唯一语言**:所有 AI 条目共享 7 态,语义色与动画全库一致。
13
+ 5. 遵守库全部既有纪律:token 消费 / `.is-*` 状态 / aria 等价 / SSR 守卫 / 幂等 init / 禁 innerHTML(SVG 走 `svgIcon()` 消毒解析)/ 事件 `icen:ai-*` 前缀 / 浮层 z 标尺。
14
+
15
+ ## 1. 状态机(一切 AI 条目共享)
16
+
17
+ ```
18
+ pending → running → streaming → done
19
+ │ │ │
20
+ └→ approval(待人确认,HITL) ├→ error
21
+ └→ cancelled ←──────────┘
22
+ ```
23
+
24
+ | 状态 | 类 | 视觉 |
25
+ |---|---|---|
26
+ | 等待 | `.is-pending` | faint 空心点 |
27
+ | 执行中 | `.is-running` | accent 脉冲点 |
28
+ | 流式中 | `.is-streaming` | 流式光标(▍闪烁,`animate-blink` 复用) |
29
+ | 待人确认 | `.is-approval` | warning 色 + 内联 允许/拒绝 按钮 |
30
+ | 完成 | `.is-done` | success 对勾 |
31
+ | 失败 | `.is-error` | error 色,失败条目**自动展开**(Copilot 模式) |
32
+ | 已取消 | `.is-cancelled` | faint + 删除线标题 |
33
+
34
+ 协议映射:AI SDK `input-streaming/input-available`→pending、`approval-requested`→approval、`output-available`→done、`output-error`→error;AG-UI `TOOL_CALL_RESULT`→done、`outcome.interrupt`→approval;A2A `INPUT_REQUIRED/AUTH_REQUIRED`→approval、`WORKING`→running、终态→done/error/cancelled。
35
+
36
+ ## 2. 条目基元 `.ai-item`(CSS 骨架,所有行式条目的"基类")
37
+
38
+ ```html
39
+ <div class="ai-item is-running" style="--ai-item-tint: var(--token-accent)">
40
+ <span class="ai-item-icon"><!-- svg --></span>
41
+ <div class="ai-item-main">
42
+ <div class="ai-item-title">一行摘要</div>
43
+ <div class="ai-item-sub">次要信息(路径/参数)</div>
44
+ </div>
45
+ <div class="ai-item-side">
46
+ <span class="ai-item-status"></span> <!-- 状态点,纯 CSS 由 .is-* 驱动 -->
47
+ <span class="ai-item-meta">1.2s</span>
48
+ </div>
49
+ <div class="ai-item-detail" hidden>展开详情</div>
50
+ </div>
51
+ ```
52
+
53
+ 局部变量:`--ai-item-tint`(图标/状态点着色槽,默认 accent)。状态点 `.ai-item-status` 是纯 CSS 圆点,由 `.is-*` 决定颜色与动效(running 脉冲用 `--pill-pulse-duration` 同款变量化时长)。
54
+
55
+ ## 3. 文件与 slug 组织
56
+
57
+ ### CSS(4 个文件,MERGED_CSS 登记)
58
+
59
+ | 文件 | 服务 slug |
60
+ |---|---|
61
+ | `src/components/ai-chat.css` | `ai-chat` `ai-message` `ai-reasoning` `ai-composer` |
62
+ | `src/components/ai-tool.css` | `ai-tool-call` `ai-subagent` |
63
+ | `src/components/ai-diff.css` | `ai-diff` `ai-files` |
64
+ | `src/components/ai-panel.css` | `ai-todo` `ai-context` `ai-usage` |
65
+
66
+ ### Behaviors(6 个文件,index.ts 全部 `export *`)
67
+
68
+ | 文件 | 导出 |
69
+ |---|---|
70
+ | `src/behaviors/ai-core.ts` | 状态机 / 注册表 / 适配器 / 格式化 / svgIcon(见 §6) |
71
+ | `src/behaviors/ai-chat.ts` | `initAiChat` `createAiStream` |
72
+ | `src/behaviors/ai-composer.ts` | `initAiComposer` |
73
+ | `src/behaviors/ai-tool.ts` | `initAiTool` `initAiSubagent` `renderAiToolCall` `renderAiSubagent` |
74
+ | `src/behaviors/ai-diff.ts` | `initAiDiff` `renderAiDiff` `parseUnifiedDiff` |
75
+ | `src/behaviors/ai-panel.ts` | `initAiTodo` `renderAiTodo` `initAiContext` `renderAiContext` `renderAiUsage` |
76
+
77
+ ### slugs.mjs 登记(S4 执行)
78
+
79
+ - SLUGS 追加 11 个(新分组「AI」):`ai-chat, ai-message, ai-reasoning, ai-composer, ai-tool-call, ai-subagent, ai-diff, ai-files, ai-todo, ai-context, ai-usage`
80
+ - MERGED_CSS:ai-message/ai-reasoning/ai-composer→`ai-chat.css`;ai-subagent→`ai-tool.css`;ai-files→`ai-diff.css`;ai-todo/ai-context/ai-usage→`ai-panel.css`
81
+ - SLUG_INIT:ai-chat→initAiChat, ai-composer→initAiComposer, ai-tool-call→initAiTool, ai-subagent→initAiSubagent, ai-diff→initAiDiff, ai-todo→initAiTodo, ai-context→initAiContext
82
+ - SLUG_BEHAVIOR:ai-message→ai-chat, ai-reasoning→ai-chat, ai-files→ai-diff, ai-usage→ai-panel(纯渲染/函数式的挂对应模块)
83
+ - SLUG_EXPORTS:ai-message/ai-reasoning→['createAiStream'] 等按实际补;函数式 slug 给渲染函数提示
84
+
85
+ ## 4. 组件契约(类名固定,施工中不得改名)
86
+
87
+ ### 4.1 ai-chat(ai-chat.css + ai-chat.ts)
88
+
89
+ ```html
90
+ <div class="ai-chat" data-density="normal"> <!-- verbose|normal|summary 透明度三档 -->
91
+ <div class="ai-chat-scroll">
92
+ <div class="ai-msg ai-msg--user | ai-msg--assistant | ai-msg--system | ai-msg--tool">
93
+ <span class="ai-msg-avatar"></span>
94
+ <div class="ai-msg-body">…内容(消费方可自行渲染 markdown)…</div>
95
+ <div class="ai-msg-actions"><button data-ai-msg-action="copy">…<button data-ai-msg-action="retry">…</div>
96
+ <div class="ai-msg-meta">12:04 · 1.2k tok</div>
97
+ </div>
98
+ </div>
99
+ <button class="ai-chat-jump" hidden>回到底部 · 3 条新消息</button>
100
+ </div>
101
+ ```
102
+
103
+ - `initAiChat(root?)`:幂等(`__icenAiChatInit`)。滚动钉底:scroll 监听,贴底时自动跟随新内容(监听 scroll 容器尺寸变化用 ResizeObserver);用户上滚暂停跟随并显示 `.ai-chat-jump`(带未读计数),点击回底恢复。消息动作委托:copy→复制正文+派 `icen:ai-copy`;retry→派 `icen:ai-retry`(detail 含消息元素)。reasoning 折叠委托也在这里(`.ai-reasoning-head` 点击切换)。
104
+ - `createAiStream(el)` → `{ append(text), done(), cancel(), fail(message?) }`:textContent 级追加(不解析 HTML),追加期间宿主挂 `.is-streaming`,done/cancel 移除;`fail()` 终止流并给宿主挂 `.is-error`(错误路径,append 之后失效)。Markdown 重渲染是消费方职责(文档里写明)。
105
+ - `renderAiMessage(scrollEl, model)` → `{ el, body, setMeta, setError, stream() }`:DOM API 构建整条消息(含多模态部件渲染,见 §10),返回句柄;`stream()` 在正文末尾开 `createAiStream`。model:
106
+
107
+ ```ts
108
+ interface AiMessageModel {
109
+ role: 'user' | 'assistant' | 'system' | 'tool';
110
+ content: AiContent; // string | AiContentPart[](§10)
111
+ meta?: string; // "12:04 · 1.2k tok"
112
+ model?: string; // assistant 消息的模型名标签
113
+ error?: string; // → .ai-msg--error + 错误文本行
114
+ streaming?: boolean; // 初始即流式态
115
+ }
116
+ ```
117
+
118
+ ### 4.2 ai-message / ai-reasoning(ai-chat.css 一族)
119
+
120
+ ```html
121
+ <div class="ai-reasoning is-done">
122
+ <button class="ai-reasoning-head" aria-expanded="false">
123
+ <span class="ai-reasoning-label">思考过程</span><span class="ai-reasoning-time">3s</span>
124
+ </button>
125
+ <div class="ai-reasoning-body" hidden>…</div>
126
+ </div>
127
+ ```
128
+
129
+ 流式中 `.is-streaming`:head 显示 shimmer「正在思考…」(动画时长走 `--ai-shimmer-duration` 局部变量);完成自动折叠并显示耗时。**新 behavior 复用 ai-chat 的委托,不独立 init。**
130
+
131
+ ### 4.3 ai-composer(ai-chat.css + ai-composer.ts)
132
+
133
+ ```html
134
+ <div class="ai-composer" data-ai-composer>
135
+ <div class="ai-composer-queue" hidden><!-- 排队消息 chips,可逐个 × --></div>
136
+ <div class="ai-composer-attach" hidden><!-- 附件 chips --></div>
137
+ <div class="ai-composer-box control">
138
+ <textarea class="ai-composer-input" rows="1" placeholder="…"></textarea>
139
+ <div class="ai-composer-actions">
140
+ <button class="ai-composer-btn" data-ai-attach aria-label="附件"></button>
141
+ <button class="ai-composer-send" data-ai-send></button> <!-- 运行态变 .is-stop 停止钮 -->
142
+ </div>
143
+ </div>
144
+ </div>
145
+ ```
146
+
147
+ - `initAiComposer(root?)`:autosize(上限 ~8 行后内滚);Enter 发送 / Shift+Enter 换行 / IME 组合态安全(复用 input.ts 模式);发送派 `icen:ai-send {text}` 并清空。
148
+ - **运行/停止态**:`setComposerRunning(el, bool)` 导出——运行中发送钮变停止钮(点击派 `icen:ai-stop`),此时输入不丢:回车转为入队(queue chip)并派 `icen:ai-queue {text}`;chip × 移除派 `icen:ai-dequeue {index}`。(queue / steer / stop 三动词分化,对齐 Cursor/Copilot 2026 模式)
149
+ - 附件钮打开 file input,选中文件生成 chip,派 `icen:ai-attach {files}`。
150
+
151
+ ### 4.4 ai-tool-call(ai-tool.css + ai-tool.ts)
152
+
153
+ ```html
154
+ <div class="ai-tool ai-tool--shell is-done"> <!-- kind 修饰类:--shell/--read/--edit/… -->
155
+ <button class="ai-tool-head" aria-expanded="false">
156
+ <span class="ai-item-icon"></span>
157
+ <span class="ai-item-main"><span class="ai-item-title">Read</span>
158
+ <span class="ai-item-sub">src/foo.ts:1-50</span></span>
159
+ <span class="ai-item-status"></span>
160
+ <span class="ai-item-meta">0.4s</span>
161
+ </button>
162
+ <div class="ai-tool-body" hidden>
163
+ <div class="ai-tool-io"><div class="ai-tool-io-label">输入</div><pre class="ai-tool-io-content">…</pre></div>
164
+ <div class="ai-tool-io"><div class="ai-tool-io-label">输出</div><pre class="ai-tool-io-content">…</pre></div>
165
+ <div class="ai-tool-approval"> <!-- 仅 .is-approval 显示 -->
166
+ <button class="btn btn-sm btn-primary" data-ai-approve>允许</button>
167
+ <button class="btn btn-sm" data-ai-reject>拒绝</button>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ ```
172
+
173
+ - `initAiTool(root?)`:委托展开/折叠(head click + 键盘);**默认展开集**(spec §4.4.1)首渲染自动展开;approval 按钮派 `icen:ai-approve` / `icen:ai-reject`(detail `{id, kind}`)。
174
+ - **默认展开集 §4.4.1(一切 AI 渲染物可折叠,默认态按「是否必须立刻看见」定)**:`chart` kind(数据可视化)/ `error`(Copilot 模式)/ `approval`(待人操作按钮必须可见)/ 多模态回执(output 含 image/video/audio 部件)→ 展开;其余 kind(shell/read/edit/write/rm/grep/glob/mcp/skill/todo/plan/subagent…)→ 折叠只露一行摘要。**用户手动切换后(data-ai-user-toggled)默认策略不再干预**;`update(patch)` 流转中新状态命中展开集仍会展开。
175
+ - `renderAiToolCall(el, model: AiToolCallModel)`:DOM API 构建整卡(流式场景用),返回 `{ el, update(patch) }`。
176
+ - kind 视觉:`ai-tool--<kind>` 覆盖 `--ai-item-tint` 与图标(注册表驱动)。内置 kinds:`shell read edit write rm grep glob browser search fetch mcp skill todo plan subagent note`。
177
+ - 展开动画:grid-template-rows 0fr→1fr 或 max-height 之外,用 `hidden` + 入场 `icen-pop-in`(简单可靠)。
178
+
179
+ ### 4.5 ai-subagent(ai-tool.css + ai-tool.ts)
180
+
181
+ ```html
182
+ <div class="ai-subagent is-running">
183
+ <button class="ai-subagent-head" aria-expanded="false">
184
+ <span class="ai-item-icon"></span>
185
+ <span class="ai-item-main"><span class="ai-item-title">explore</span>
186
+ <span class="ai-item-sub">搜索 auth 模块的所有入口</span></span>
187
+ <span class="ai-item-status"></span>
188
+ <span class="ai-item-meta">12.3s · 4 步</span>
189
+ </button>
190
+ <div class="ai-subagent-body" hidden>
191
+ <div class="ai-subagent-stream"><!-- 嵌套 .ai-item / .ai-tool / .ai-reasoning 活动流 --></div>
192
+ <div class="ai-subagent-result">完成回执摘要</div>
193
+ </div>
194
+ </div>
195
+ ```
196
+
197
+ - 嵌套流用**缩进 + 左侧引导线**表达层级(CSS 伪元素),可再嵌套子代理(递归)。
198
+ - `renderAiSubagent(el, model)`:model 含 `activities: AiToolCallModel[]`(复用 renderAiToolCall 渲染嵌套项——递归复用即"继承")。
199
+ - 交互:点击 head 展开看细节(**用户明确要求比 kimi/codex 更优雅**——展开时活动项逐条 `icen-pop-in` 渐入)。
200
+
201
+ ### 4.6 ai-diff(ai-diff.css + ai-diff.ts)
202
+
203
+ ```html
204
+ <div class="ai-diff">
205
+ <div class="ai-diff-file is-modified"> <!-- is-added/is-modified/is-deleted/is-renamed -->
206
+ <div class="ai-diff-head">
207
+ <span class="ai-diff-path">src/foo.ts</span>
208
+ <span class="ai-diff-stat"><b class="ai-diff-add">+12</b> <b class="ai-diff-del">−4</b></span>
209
+ <span class="ai-diff-actions">
210
+ <button data-ai-diff-accept>接受</button><button data-ai-diff-reject>拒绝</button>
211
+ </span>
212
+ </div>
213
+ <div class="ai-diff-body" hidden>
214
+ <table class="ai-diff-table">
215
+ <tr class="ai-diff-line--hunk"><td colspan="3">@@ -10,6 +10,8 @@</td></tr>
216
+ <tr class="ai-diff-line--ctx"><td class="ai-diff-ln">10</td><td class="ai-diff-ln">10</td><td class="ai-diff-code">…</td></tr>
217
+ <tr class="ai-diff-line--add">…</tr>
218
+ <tr class="ai-diff-line--del">…</tr>
219
+ </table>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ ```
224
+
225
+ - `parseUnifiedDiff(text)` → 结构化 hunks(纯函数,SSR 安全);`renderAiDiff(el, {files})` DOM API 渲染(文本全 textContent)。
226
+ - `initAiDiff(root?)`:文件展开/折叠委托;accept/reject 派 `icen:ai-diff-accept` / `icen:ai-diff-reject`(detail `{path}`),点击后该文件标记 `.is-accepted` / `.is-rejected`。
227
+ - 长 diff 默认折叠只露 head;`.ai-diff--inline` 变体支持内联并排?——不做并排,保持 unified 单栏(范围控制)。
228
+
229
+ ### 4.7 ai-files(ai-diff.css 一族,纯 CSS)
230
+
231
+ ```html
232
+ <div class="ai-files">
233
+ <span class="ai-file-chip is-added"><span class="ai-file-icon"></span>src/foo.ts</span>
234
+ </div>
235
+ ```
236
+
237
+ chip 态:`is-added/is-modified/is-deleted` + hover 操作(可选)。无 behavior(文档说明)。
238
+
239
+ ### 4.8a 输入框实时待办(setComposerTodo,业界模式)
240
+
241
+ 待办是**工具调用**(对话里只留折叠的 TodoWrite 卡,todo kind summarize 显示 x/y 完成),
242
+ **实时状态挂在输入区**而非对话流:`setComposerTodo(el, items | null)` 在工具条渲染
243
+ `.ai-composer-todo` chip(清单图标 + x/y;进行中 accent / 全部完成 success / 有失败
244
+ warning),点击弹层展示完整清单(popover + renderAiTodo 只读);传 null 清除。
245
+
246
+ ### 4.8 ai-todo(ai-panel.css + ai-panel.ts)
247
+
248
+ ```html
249
+ <div class="ai-todo">
250
+ <div class="ai-todo-head"><span class="ai-todo-progress">2/5</span><div class="ai-todo-bar"><i style="width:40%"></i></div></div>
251
+ <div class="ai-todo-item is-running">
252
+ <span class="ai-item-status"></span>
253
+ <span class="ai-todo-text">实现登录页</span>
254
+ <span class="ai-todo-active">正在实现登录页…</span> <!-- activeForm:运行中替换文案(Claude Code 模式) -->
255
+ </div>
256
+ </div>
257
+ ```
258
+
259
+ - `renderAiTodo(el, items)`:`items: {content, status: AiStatus, activeForm?}[]`;`initAiTodo(root?)`:`data-ai-todo-interactive` 时可点击循环状态并派 `icen:ai-todo-toggle {index, status}`;默认只读。
260
+ - 进度条用 feedback 的 progress 视觉(token 消费)。
261
+
262
+ ### 4.9 ai-usage(ai-panel.css + ai-panel.ts)
263
+
264
+ ```html
265
+ <div class="ai-usage">
266
+ <div class="ai-usage-bar">
267
+ <i class="ai-usage-seg ai-usage-seg--input" style="width:35%"></i>
268
+ <i class="ai-usage-seg ai-usage-seg--cache-read" style="width:20%"></i>
269
+ <i class="ai-usage-seg ai-usage-seg--cache-write" style="width:5%"></i>
270
+ <i class="ai-usage-seg ai-usage-seg--reasoning" style="width:10%"></i>
271
+ <i class="ai-usage-seg ai-usage-seg--output" style="width:12%"></i>
272
+ </div>
273
+ <div class="ai-usage-legend"><!-- 各段色点+数值;缓存分列(计费诚实) --></div>
274
+ <div class="ai-usage-total">82k / 200k · 41%</div>
275
+ </div>
276
+ ```
277
+
278
+ - `renderAiUsage(el, usage: AiUsage, opts?: { total?: number; cost?: number })`:分段条 + 图例 + 占比;`normalizeUsage` 在 ai-core。
279
+ - 配色:input=accent、output=success、cacheRead=info、cacheWrite=warning、reasoning=faint——语义 token。
280
+ - **环形形态 `renderAiUsageRing`**(Claude Desktop 式上下文窗口指示器):紧凑圆环(SVG dasharray,`viewBox 36 / r 15.9155` → 周长恰为 100),中心百分比;点击弹出完整分段分解(弹层为一次性动态 `.popover.ai-usage-popover`,复用 `openPopover`/`closePopover`,anchor 豁免外点关闭,Esc/外点/再点关闭即移除)。状态档:<60% is-ok(accent)/ 60–85% is-warn / >85% is-hot(error + 脉冲)。`--lg` 大号变体。
281
+
282
+ ### 4.10 ai-context(ai-panel.css + ai-panel.ts)—— 右上角资源抽屉
283
+
284
+ ```html
285
+ <aside class="ai-context" hidden>
286
+ <div class="ai-context-head"><span>上下文</span><button data-ai-context-close>×</button></div>
287
+ <section class="ai-context-section"><h3>用量</h3><!-- ai-usage --></section>
288
+ <section class="ai-context-section"><h3>文件 (3)</h3><!-- ai-files chips --></section>
289
+ <section class="ai-context-section"><h3>MCP (2)</h3><!-- server 行:名称+状态点+工具数 --></section>
290
+ <section class="ai-context-section"><h3>Skills</h3><!-- skill chip 列表 --></section>
291
+ </aside>
292
+ ```
293
+
294
+ - `initAiContext(root?)`:触发器 `[data-ai-context-open]` 全局委托开合;抽屉 fixed 右侧滑入(`--z-chrome`),Esc/外点关闭;`.ai-context--inline` 变体为页面流内嵌(不做 fixed)。
295
+ - `renderAiContext(el, { usage?, files?, mcpServers?, skills? })`:组合渲染(内部复用 renderAiUsage / ai-files chip / ai-item 行)。
296
+ - MCP server 行:`.ai-item` 基元 + `is-done`(已连接)/`is-error`(断开)。
297
+
298
+ ## 5. 事件汇总(全部 bubbles)
299
+
300
+ | 事件 | detail | 来源 |
301
+ |---|---|---|
302
+ | `icen:ai-send` | `{text}` | composer 发送 |
303
+ | `icen:ai-stop` | `{}` | composer 停止 |
304
+ | `icen:ai-queue` / `icen:ai-dequeue` | `{text}` / `{index}` | 排队消息 |
305
+ | `icen:ai-attach` | `{files}` | 附件(钮选 / 粘贴 / 拖放共用) |
306
+ | `icen:ai-copy` / `icen:ai-retry` | `{el}` | 消息操作 |
307
+ | `icen:ai-approve` / `icen:ai-reject` | `{id, kind}` | 工具审批 |
308
+ | `icen:ai-diff-accept` / `icen:ai-diff-reject` | `{path}` | diff 审阅 |
309
+ | `icen:ai-todo-toggle` | `{index, status}` | 交互 todo |
310
+ | `icen:ai-toggle` | `{el, open}` | 通用条目展开/折叠 |
311
+ | `icen:ai-model-change` | `{provider, model, label, context}` | 模型切换(§8) |
312
+ | `icen:ai-command` | `{name, args}` | 斜杠命令(§8) |
313
+ | `icen:ai-ref` | `{action:'add'\|'remove', ref}` | @ 引用(§8) |
314
+ | `icen:ai-usage` | `AiUsage`(本次请求归一) | client 每次请求完成(§9) |
315
+ | `icen:ai-done` | `{status:'ok'\|'error'\|'cancelled', provider, model, stream, usage?, cost?, error?, durationMs, ttftMs?}` | client 每次请求收尾(chat 与 stream 的 finally;绑定层的驱动事件,§11) |
316
+ | `icen:ai-audit-clear` | `{}` | renderAiAudit 清除钮(§12) |
317
+
318
+ ## 6. ai-core.ts 契约(S1 实现,其余 slice 消费)
319
+
320
+ ```ts
321
+ export type AiStatus = 'pending'|'running'|'streaming'|'approval'|'done'|'error'|'cancelled';
322
+ export interface AiUsage { input?; output?; cacheRead?; cacheWrite?; reasoning?; total?; }
323
+ export interface AiKindDef { label: string; icon: string; tint?: 'accent'|'success'|'warning'|'error'|'info'|'muted'; summarize?: (input: unknown) => string; }
324
+ export interface AiToolCallModel {
325
+ id: string; name: string; kind: string; status: AiStatus;
326
+ input?: unknown; output?: unknown; errorText?: string;
327
+ approval?: { reason?: string }; durationMs?: number; usage?: AiUsage;
328
+ activities?: AiToolCallModel[]; // subagent 嵌套
329
+ }
330
+ export function aiStatusLabel(s: AiStatus): string; // 中文:等待/执行中/流式中/待确认/完成/失败/已取消
331
+ export function registerAiKind(name: string, def: AiKindDef): void;
332
+ export function getAiKind(name: string): AiKindDef; // 未注册回退 'note'
333
+ export function inferKind(toolName: string): string; // Bash/Shell→shell, Read→read, mcp__*→mcp, 等
334
+ export function normalizeToolCall(raw: unknown): AiToolCallModel; // OpenAI tool_calls 项 / Anthropic tool_use|tool_result block / AI SDK tool-* part / 素朴
335
+ export function normalizeUsage(raw: unknown): AiUsage; // OpenAI usage / Anthropic usage / Gemini usageMetadata / 素朴
336
+ export function formatTokens(n: number): string; // 1234 → "1.2k"
337
+ export function formatDuration(ms: number): string; // 1234 → "1.2s"
338
+ export function svgIcon(svg: string): SVGElement | null; // DOMParser 消毒(剥 on* 属性与 script/foreignObject),SSR 返回 null
339
+ ```
340
+
341
+ 内置 kind 图标:内联 SVG 字符串常量(lucide 风格 24×24 stroke 图标,手写最简路径;terminal/file/file-pen/file-plus/file-trash/search/folder-search/globe/brain/list/bot 等)。
342
+
343
+ ## 7. 施工分工(文件互斥,严禁越界改别的文件)
344
+
345
+ - **S1**:`src/behaviors/ai-core.ts` `src/behaviors/ai-chat.ts` `src/behaviors/ai-composer.ts` `src/components/ai-chat.css` `src/index.ts`(追加 6 个 ai 行为导出)
346
+ - **S2**:`src/behaviors/ai-tool.ts` `src/components/ai-tool.css`
347
+ - **S3**:`src/behaviors/ai-diff.ts` `src/behaviors/ai-panel.ts` `src/components/ai-diff.css` `src/components/ai-panel.css`
348
+ - **S4**(S1-S3 完成后):`scripts/slugs.mjs` `site/src/lib/components.ts`(新分组「AI」+ 11 个交互 demo 条目)`AGENTS.md`(追加 AI 组件族约定)`README.md`(组件统计更新)
349
+
350
+ 验证:各自 `bunx tsc --noEmit`(ui/ 根目录;node 需 `export PATH="/e/Env/Node/fnm/node-versions/v22.22.3/installation:$PATH"`);不跑 `bun run build`(dist 冲突),统一由主代理构建。
351
+
352
+ ---
353
+
354
+ ## 8. ai-composer v2(输入台重做,2026 业界集大成)
355
+
356
+ 结构(自底向上工具条,业界主流形态):
357
+
358
+ ```html
359
+ <div class="ai-composer" data-ai-composer>
360
+ <div class="ai-composer-queue" hidden></div> <!-- 排队 chips(v1 已有) -->
361
+ <div class="ai-composer-refs" hidden></div> <!-- @ 引用 chips(分离渲染:textarea 不嵌 chip) -->
362
+ <div class="ai-composer-attach" hidden></div> <!-- 附件 chips(v1 已有) -->
363
+ <div class="ai-composer-box control">
364
+ <textarea class="ai-composer-input" rows="1"></textarea>
365
+ <div class="ai-composer-toolbar">
366
+ <button class="ai-composer-model" data-ai-model-open> <!-- 模型切换:provider 图标 + 模型名 + chevron -->
367
+ <span class="ai-composer-spacer"></span>
368
+ <span class="ai-composer-usage" data-ai-usage></span> <!-- 上下文环挂载点 -->
369
+ <button class="ai-composer-btn" data-ai-attach></button>
370
+ <button class="ai-composer-send" data-ai-send></button>
371
+ </div>
372
+ </div>
373
+ </div>
374
+ ```
375
+
376
+ 新 API(全部可选配置,向后兼容 v1 契约):
377
+
378
+ - `setComposerModels(el, providers: AiProviderOption[], current?: {provider, model})`:
379
+ `AiProviderOption = { id, label, icon?, models: { id, label, context? }[] }`。
380
+ 工具条出现模型钮;点击开 `.ai-composer-popup`(portal body,`computePopoverLayout` side:top)——
381
+ 按 provider 分组(图标+名)+ 模型行(label + 上下文窗口如 `1M`)+ 搜索过滤 + 当前勾选。
382
+ 选择派 `icen:ai-model-change { provider, model, label, context }`;
383
+ **切换时若 cacheRead 占比高则弹层内提示"换模型会使 prompt 缓存失效"**(Kimi Code 细节)。
384
+ 选中模型的 `context` 自动作为 `setComposerUsage` 的 total 默认值(闭环)。
385
+ - `setComposerCommands(el, commands: { name, description?, argsHint? }[])`:
386
+ 输入开头为 `/` 时弹层过滤;↑↓ 导航、Enter/Tab 选中、Esc 关;选中派 `icen:ai-command { name, args }`
387
+ 并清空输入(命令被拦截执行、不进消息流——业界共识)。
388
+ - `setComposerRefs(el, sources: { kind: 'file'|'folder'|'doc'|'agent', id, label, sub? }[])`:
389
+ 任意位置输入 `@` 触发弹层(分组按 kind + 图标);选中后文本插入 `@label ` 且 refs chip 行 +1,
390
+ 派 `icen:ai-ref { action: 'add', ref }`;chip × 移除(同时删文本里首个 `@label`)派 `{ action: 'remove' }`。
391
+ - `setComposerUsage(el, usage, opts)`:工具条右侧挂 `renderAiUsageRing`。
392
+ - 历史:输入为空时按 `↑` 取回上一条已发送文本(会话内历史数组,每 composer 独立)。
393
+ - 运行态(v1 已有 queue/stop)补充:运行中按 `↑` 把最后一条排队消息取回输入框重新编辑(Claude Code 模式)。
394
+ - 弹层接管时键盘事件 preventDefault;IME 组合态安全(沿用 v1)。
395
+ - 图标对齐修复:工具条按钮统一 28px 方形、svg 14px 居中、gap 走 density token。
396
+
397
+ 事件新增:`icen:ai-model-change` / `icen:ai-command` / `icen:ai-ref`(全 bubbles)。
398
+
399
+ ## 9. ai-provider(provider 适配层:填入 key 即工作)
400
+
401
+ 新文件 `src/behaviors/ai-provider.ts`(index.ts export)。调研依据:五家官方文档 + Vercel AI SDK openai-compatible 架构 + LiteLLM 映射思路。
402
+
403
+ ### 9.1 注册表
404
+
405
+ ```ts
406
+ export interface AiProviderModel { id: string; label: string; context?: number; outputLimit?: number; }
407
+ export interface AiProviderDef {
408
+ id: string; label: string; icon: string; // icon: 单色 svg 字母章(svgIcon 消毒)
409
+ baseURL: string; // 默认,可覆盖(自定义 BaseURL 是一等公民)
410
+ chatPath: string; // /chat/completions | /messages
411
+ wire: 'openai' | 'anthropic';
412
+ auth: 'bearer' | 'x-api-key';
413
+ extraHeaders?: Record<string, string>; // 如 anthropic-version / dangerous-direct-browser-access
414
+ models: AiProviderModel[];
415
+ /** 浏览器直连 CORS:'yes'(Anthropic 官方支持)/ 'no'(OpenAI 官方禁止)/ 'unknown'(DeepSeek/GLM/Kimi 无承诺,建议代理) */
416
+ browserDirect: 'yes' | 'no' | 'unknown';
417
+ }
418
+ export function registerAiProvider(def: AiProviderDef): void; // 第三方/自建网关注册入口
419
+ export function getAiProvider(id: string): AiProviderDef | undefined;
420
+ export function listAiProviders(): AiProviderDef[];
421
+ ```
422
+
423
+ 内置五家(2026-10 调研值):
424
+ - openai:`https://api.openai.com/v1`,wire openai,browserDirect 'no';模型 gpt-5 / gpt-5-mini / gpt-5.1 / gpt-5.5 等
425
+ - claude:`https://api.anthropic.com/v1`,wire anthropic,browserDirect 'yes'(自动带 anthropic-version + anthropic-dangerous-direct-browser-access);claude-opus-5-5 / claude-sonnet-5-5 / claude-fable-5-1 / claude-haiku-4-5
426
+ - deepseek:`https://api.deepseek.com/v1`,wire openai,'unknown';deepseek-flash / deepseek-v4-pro(1M ctx)
427
+ - glm:`https://open.bigmodel.cn/api/paas/v4`,wire openai,'unknown'(API key 直接当 Bearer;JWT 可选不做);glm-5.3 / glm-5.3-flash / glm-5.2 / glm-4.6
428
+ - kimi:`https://api.moonshot.cn/v1`,wire openai,'unknown';kimi-k3(1M)/ kimi-k2.7-code / kimi-k2.6
429
+
430
+ ### 9.2 客户端
431
+
432
+ ```ts
433
+ createAiClient({
434
+ provider: string, // 注册表 id
435
+ apiKey: string,
436
+ baseURL?: string, // 覆盖默认(自定义网关/代理)
437
+ model?: string, // 默认取 provider.models[0]
438
+ onAudit?: (entry: AiAuditEntry) => void,
439
+ auditor?: AiAuditor, // 传实例则每次请求自动 log(entry 含 cost/ttftMs,§12)
440
+ fetch?: typeof fetch, // mock 注入(AI SDK 同款)
441
+ }) → {
442
+ chat(req: AiChatRequest): Promise<AiChatResult>;
443
+ stream(req: AiChatRequest): AiStreamSession; // AsyncIterable<AiStreamChunk> + cancel() + done
444
+ config: 只读当前配置(provider/baseURL/model)
445
+ }
446
+
447
+ AiChatRequest = { messages: AiChatMessage[], model?, temperature?, maxTokens?, signal? }
448
+ AiChatMessage = {
449
+ role: 'system' | 'user' | 'assistant' | 'tool';
450
+ content: string | AiContentPart[]; // §10 标准化内容(多模态)
451
+ toolCallId?: string; // role 'tool' 时:openai 族映射 tool_call_id;
452
+ // anthropic 族映射为 user 消息的 tool_result block(真回灌)
453
+ cacheControl?: boolean; // anthropic 族:该消息末块打 cache_control 断点(prompt 缓存
454
+ // 一等公民);openai 族自动缓存,字段忽略
455
+ }
456
+ AiChatResult = { text, usage: AiUsage(normalizeUsage 归一), cost?: number(定价表命中时), finishReason?, raw? }
457
+ AiStreamChunk = { type:'text', delta } | { type:'done' } | { type:'error', message }
458
+ ```
459
+
460
+ **多模态传输映射(parts → wire)**:
461
+ - openai 族:text → `{type:'text'}`;image → `{type:'image_url',image_url:{url}}`(data 拼回 data URI);audio → `{type:'input_audio',input_audio:{data,format: mimeType 后缀}}`;file(url 为 file_id 时)→ `{type:'file',file:{file_id}}`;**video → 诚实抛 `AiProviderError{type:'unsupported'}`**(无主流 API 支持);resource-link → text 部件降级。
462
+ - anthropic 族:text → `{type:'text'}`;image → `{type:'image',source}`(url 或 base64);audio → `{type:'audio',source:{type:'base64'}}`(仅音频模型,官方口径);file(pdf 等)→ `{type:'document',source}`;video → unsupported;resource-link → text 降级。
463
+ - 响应侧 `chat()` 的 content parts 数组(assistant 回图等)→ `result.parts?: AiContentPart[]`(normalizeContentParts 归一),`text` 仍为拼接文本。
464
+
465
+ - **两族线协议**:openai 族 POST chat/completions(bearer;`stream_options.include_usage` 自动注入——AI SDK 同款);anthropic 族 POST /messages(x-api-key + 版本头 + 浏览器直连头;`max_tokens` 必填——缺省 4096;system 顶层参数转换)。
466
+ - **SSE 解析器两套**:openai 族按空行分隔的 `data:` 帧 + `[DONE]`;anthropic 族 `event:` 分派(text_delta 出文本;usage 取 message_start 的 input + message_delta 的 output,**不 += 累加**)。缓冲按空行切,不按 read 边界。
467
+ - **usage 归一**:走 ai-core `normalizeUsage`(已含 OpenAI/Anthropic/GLM details 路径);本次给 ai-core 补 DeepSeek 顶层 `prompt_cache_hit_tokens`→cacheRead、Kimi 顶层 `cached_tokens`→cacheRead 与 `prompt_tokens_details.cache_write_tokens`→cacheWrite。
468
+ - **错误归一**:OpenAI/DeepSeek 同构 `error.{message,type}`;Kimi 无 param/code 自有枚举;GLM 业务码信封;Anthropic 多一层 `type:"error"` 包装——统一抛 `AiProviderError { provider, status, type, message, raw }`。
469
+ - **CORS 诚实**:browserDirect 'no'/'unknown' 的 provider 在浏览器直连失败时,错误信息里建议走代理(文档写明,不静默)。
470
+
471
+ ### 9.3 审计("完美进入 AI 体系"的闭环)
472
+
473
+ ```ts
474
+ createAiAuditor({ persist?: string(localStorage key), max?: number(默认 100,环形) }) → {
475
+ log(entry): void; list(): AiAuditEntry[]; clear(): void;
476
+ summary(): AiUsage; // 全量聚合 → 直接喂 renderAiUsage / renderAiUsageRing
477
+ byModel(): Record<string, AiUsage>;
478
+ }
479
+ AiAuditEntry = { id, ts, provider, model, baseURL, stream, status: 'ok'|'error', durationMs, usage?, error? }
480
+ ```
481
+
482
+ client 每次请求自动产审计记录(onAudit 回调 + 若传了 auditor 实例则自动 log)。usage 增量事件派 `icen:ai-usage`(detail AiUsage,bubbles,挂 document)——上下文环/用量面板监听即实时更新。
483
+
484
+ ### 9.4 可测试性(补)
485
+
486
+ `createAiClient` 接受 `fetch?: typeof fetch` 注入(AI SDK 同款)——单测与文档站 demo 用 mock fetch 返回预制 SSE 流,离线可演示全链路(流式文本 + usage + 审计)。
487
+
488
+ ---
489
+
490
+ ## 10. 标准化内容模型 `AiContent`(v0.7.1 新增,ai-core)
491
+
492
+ > 调研依据(2026-10):Vercel AI SDK v5 UIMessage parts(text / reasoning / tool-\<name\> 六态 / file{mediaType,url} / source-url / source-document / data-*)、
493
+ > MCP 内容类型(TextContent / ImageContent / AudioContent / ResourceLink / EmbeddedResource + structuredContent,全部带 annotations)、
494
+ > OpenAI content parts(text / image_url / input_audio / file)、Anthropic content blocks(text / image / document / audio / tool_use / tool_result)。
495
+ > 设计目标:**消费方用一套 parts 表达任意输入;渲染组件、估算器、传输层全部只认这一套。**
496
+
497
+ ### 10.1 类型
498
+
499
+ ```ts
500
+ export type AiContent = string | AiContentPart[];
501
+
502
+ export type AiContentPart =
503
+ | { type: 'text'; text: string; state?: 'streaming' | 'done' } // AI SDK TextUIPart 对齐
504
+ | { type: 'image'; url?: string; data?: string; mimeType?: string; alt?: string }
505
+ | { type: 'audio'; url?: string; data?: string; mimeType?: string }
506
+ | { type: 'video'; url?: string; data?: string; mimeType?: string }
507
+ | { type: 'file'; url?: string; data?: string; mimeType?: string; filename?: string }
508
+ | { type: 'resource-link'; uri: string; name?: string; mimeType?: string }; // MCP ResourceLink
509
+ ```
510
+
511
+ 约定:`url` 与 `data` 二选一——`data` 为**无前缀 base64** 且必须伴随 `mimeType`;渲染用 `aiContentUrl(part)` 归一(data → `data:<mime>;base64,<data>`)。
512
+ video 当前无主流 chat API 支持,属 UI 前瞻位(渲染与估算齐备,传输层诚实报不支持)。
513
+
514
+ ### 10.2 归一化(业界 wire → parts)
515
+
516
+ ```ts
517
+ export function normalizeContentParts(raw: unknown): AiContentPart[];
518
+ ```
519
+
520
+ 一函数覆盖四族来源(逐项探测,非本届即跳过):
521
+ - **OpenAI**:`{type:'text'}` / `{type:'image_url',image_url:{url}}`(data URI 自动拆 data+mimeType)/ `{type:'input_audio',input_audio:{data,format}}` / `{type:'file',file:{file_id}}`(→ file 部件,url 放 file_id)
522
+ - **Anthropic**:`{type:'text'}` / `{type:'image'|'document'|'audio', source:{type:'base64',media_type,data} | {type:'url',url}}`(document → file 部件)
523
+ - **MCP**:`{type:'text'}` / `{type:'image'|'audio', data, mimeType}` / `{type:'resource_link', uri, name}` / `{type:'resource', resource:{uri, mimeType, blob|text}}`(blob → 对应媒体部件、text → text 部件)
524
+ - **素朴**:已是本类型族的原样通过;字符串 → 单 text 部件;未知 type 剔除
525
+
526
+ `normalizeMcpContent(raw)` 为 MCP 场景别名(同实现,可发现性);MCP `structuredContent`(JSON)由消费方自行决定序列化为 text 部件。
527
+
528
+ ### 10.3 工具与文本
529
+
530
+ ```ts
531
+ export function isAiContentPartArray(v: unknown): v is AiContentPart[]; // 守卫(tool output 多模态探测)
532
+ export function contentToText(content: AiContent): string; // 拼接全部 text 部件(复制/降级传输用)
533
+ export function aiContentUrl(part: AiContentPart): string | undefined; // url 或 data URI(渲染 src)
534
+ ```
535
+
536
+ `AiToolCallModel.output` 保持 `unknown`;`renderAiToolCall` 内部用 `isAiContentPartArray` 探测——数组部件渲染媒体/链接,其余保持 JSON `<pre>`(**MCP 回执的多模态呈现零改动打通**)。
537
+
538
+ ### 10.4 估算器
539
+
540
+ ```ts
541
+ export function estimateTokens(content: AiContent | AiUsage): number;
542
+ ```
543
+ - 内容启发式(无依赖,业界粗估惯例):CJK 字符 ×0.6 + 其余字符 ÷4;文件/图片部件按 mimeType 给经验值(image ≈ 1000、audio ≈ 25/秒无时长信息时 1500、video ≈ 300/秒缺省 6000、file 文本类 ≈ 内容未知给 500)。
544
+ - 传 `AiUsage` 时直接返回归一 total。
545
+
546
+ ```ts
547
+ export function contextEstimate(usage: AiUsage): number;
548
+ ```
549
+ **下一轮上下文估算**(上下文环的正确口径):`(input??0)+(cacheRead??0)+(cacheWrite??0)+(output??0)`——本次输出会进入下轮输入;reasoning 已计入 output(OpenAI)或单列(Anthropic 计费口径),单列时也加上。**与 `auditor.summary()`(累计计费口径)语义不同,二者不可混喂同一个环**——绑定层(§11)负责选对口径。
550
+
551
+ ---
552
+
553
+ ## 11. 绑定层 `bindComposer`(v0.7.1 新增,ai-composer.ts)
554
+
555
+ 「零接线全链路」的胶水:**事件驱动、消费方拥有传输会话的所有权不变**。绑定层监听 §5 的 `icen:ai-*` 事件把 composer ↔ 消息区 ↔ client ↔ 用量环接成闭环。
556
+
557
+ ```ts
558
+ export interface AiComposerBindOpts {
559
+ client?: AiClient; // 传入即托管对话:send → 用户消息渲染 + running + client.stream
560
+ // → assistant 消息 + createAiStream 逐 chunk 追加 → done 收尾
561
+ // (错误 → fail + setError;stop → session.cancel;排队消息在
562
+ // 每轮完成后自动发送下一条)
563
+ messages?: HTMLElement; // .ai-chat-scroll 或任意消息容器:renderAiMessage 的挂载点(client 模式必填)
564
+ usage?: { from: 'context' | 'billing' | AiAuditor; total?: number };
565
+ // 环口径:'context'(默认,icen:ai-done 的 contextEstimate——环语义正确)
566
+ // | 'billing'(icen:ai-done 的请求 usage 累加——计费视角)
567
+ // | 传 auditor 实例则用其 summary()
568
+ running?: boolean; // 运行态自动管理(默认 true):icen:ai-send → running;icen:ai-done → 解除
569
+ }
570
+ export interface AiComposerBinding { unbind(): void }
571
+ export function bindComposer(el: HTMLElement, opts?: AiComposerBindOpts): AiComposerBinding;
572
+ ```
573
+
574
+ - **client 模式**下 stop 钮绑定 `session.cancel()`;`icen:ai-done` 的 `cost` 自动进环的 `opts.cost` 累计;assistant 消息 meta 自动填 token 数与模型名。
575
+ - 不传 client 时是纯状态绑定(send→running、done→解除、环更新),传输仍归消费方——**渐进采用**。
576
+ - composer 同时新增:粘贴文件与拖放文件落附件(`icen:ai-attach` 同事件);`MODEL_CTX` 在选中模型无 `context` 时显式清除(环回退无上限形态)。
577
+
578
+ ---
579
+
580
+ ## 12. 定价与审计闭环(v0.7.1 新增,ai-provider.ts + ai-panel.ts)
581
+
582
+ ### 12.1 定价表(注册表内联,USD / MTok,2026-10 调研近似值)
583
+
584
+ ```ts
585
+ export interface AiPricing { input: number; output: number; cacheRead?: number; cacheWrite?: number }
586
+ // AiProviderModel 增:pricing?: AiPricing
587
+ export function estimateCost(usage: AiUsage, provider: string, model: string): number | undefined;
588
+ ```
589
+
590
+ 内置值(锚点:Anthropic Opus 5.5 $4/$20、Sonnet 5 $2/$10、Haiku 4.5 $1/$5、Fable $10/$50、缓存读 ~10% 输入价;
591
+ GPT-5 $1.25/$10、mini $0.25/$2;DeepSeek V4 Pro ~$0.55/$1.1(峰谷取中)、Flash ~$0.14/$0.28;
592
+ GLM-5.x ~$0.5/$3 档;Kimi K3 $3/$15、缓存读 ~$0.3,K2.6 ~$0.95/$3)。cacheWrite 缺省按 input 的 1.25 倍
593
+ (Anthropic 5-minute cache write 口径)。**第三方网关注册时自带来价,estimateCost 未命中定价返回 undefined(不猜价)。**
594
+
595
+ ### 12.2 审计增强
596
+
597
+ - `AiAuditEntry` 增 `ttftMs?: number`(首 token 延迟——stream 在首个 text chunk 记时)与 `cost?: number`(按定价表算出,未命中缺省)。
598
+ - `AiAuditor` 增 `totals(): { usage: AiUsage; cost: number; requests: number; errors: number; avgTtftMs?: number }`
599
+ (`summary()` 保留为 usage-only 别名,喂 renderAiUsage 不变)。
600
+ - **`renderAiAudit(elm, source, opts?)`**(ai-panel.ts,source 为 `AiAuditor | AiAuditEntry[]`):
601
+ totals 行(请求数 ok/error · 累计 tokens · 累计成本 · 平均 TTFT)+ byModel 分组行 + 最近请求列表
602
+ (limit 默认 10:状态点 · provider/model · 时长 · TTFT · tokens · 错误行)。清除钮派 `icen:ai-audit-clear`
603
+ (消费方调 `auditor.clear()` 后重渲染);`opts.onClear` 可替代事件。
604
+ - `AiContextModel` 增 `audit?: AiAuditor | AiAuditEntry[]`:上下文抽屉自动出现「审计」节(复用 renderAiAudit 内部件,无清除钮)。
605
+ - localStorage 持久化审计在多 tab 下为 last-write-wins:绑定层不自动同步,文档写明(审计以单 tab 会话为准)。
606
+
607
+ ---
608
+
609
+ ## 13. AI 工具体系(v0.8,`src/behaviors/ai-tools.ts`)
610
+
611
+ > 任何 UI 能力(图表渲染、未来的 diff 应用 / todo 写入……)注册为 **AiToolDef**,
612
+ > 模型侧可调用、宿主侧全控。库内置 `render_chart` 一个工具(吃 ChartSpec 纯 JSON)。
613
+
614
+ ### 13.1 注册表
615
+
616
+ ```ts
617
+ export interface AiToolDef<TInput = unknown> {
618
+ name: string; // 模型侧 function name(snake_case)
619
+ description: string; // 给模型看的说明
620
+ inputSchema: Record<string, unknown>; // OpenAI parameters / MCP inputSchema 兼容
621
+ run: (input: TInput, mount: HTMLElement) => unknown; // 在挂载点上渲染,返回值即工具结果
622
+ present?: 'mount' | 'data';
623
+ }
624
+ registerAiTool(def) / unregisterAiTool(name) / getAiTool(name) / listAiTools()
625
+ ```
626
+
627
+ ### 13.2 挂载区(挂不挂 / 挂哪些 / 挂几个——宿主三层控制)
628
+
629
+ ```ts
630
+ createAiToolArea(el, { tools?: string[]; max?: number; itemMinHeight?: number }) → AiToolArea
631
+ // tools 白名单支持通配 'chart-*';max 超出 LRU 淘汰最旧挂载
632
+ // AiToolArea: { el, count, call(name, input), can(name), setTools, setMax, clear, onChange(fn) }
633
+ ```
634
+
635
+ - 三层控制:① 不 import 本模块 = 生态无工具体系(tree-shake);② area 白名单 + max;
636
+ ③ 运行时 setTools/setMax/clear + unregisterAiTool 全局下架。
637
+ - 挂载项结构:`.ai-tools-mount > .ai-tools-item > head(折叠钮 chevron/工具名/时间/×,button,aria-expanded)+ body(工具自渲染,默认展开,head 点击折叠;× 拦截冒泡只做移除)`。
638
+ - 事件(bubbles):`icen:ai-tool-call {name, input, el}` / `icen:ai-tool-result {name, ok, el, error?, count}`
639
+ / `icen:ai-tool-evict {name, el}`。
640
+
641
+ ### 13.3 模型侧导出(agent loop 直接消费)
642
+
643
+ ```ts
644
+ aiToolsToOpenAI(names?) // → chat.completions tools 参数格式
645
+ aiToolsToMcp(names?) // → MCP tools 格式(name/description/inputSchema)
646
+ aiToolsManifest(names?) // → system prompt 能力清单文本
647
+ parseAiToolArgs(raw) // tool_call 参数容错解析(JSON 字符串/对象/坏 JSON→{})
648
+ ```
649
+
650
+ 宿主 agent loop 接线:模型回 `render_chart` tool_call → `area.call('render_chart', parseAiToolArgs(args))`。
651
+
652
+ ### 13.4 图表与 AI 族的接缝
653
+
654
+ - `render_chart` 工具:inputSchema = ChartSpec JSON Schema;run = renderChart(自动选型 /
655
+ 容错归一 / 交互事件族 / 可隐藏图例全数继承)。
656
+ - **工具回执内嵌图**:`renderAiToolCall` 的 output 为 `{type:'chart', spec}` 信封或裸
657
+ ChartSpec 时,IO 区直接渲染小图(`chartSpecOf` 探测,`.ai-tool-io-chart` 容器)。
658
+ - **kind**:ai-core 内置 `chart` kind(折线图标,info tint,summarize 取 title);
659
+ `inferKind`:工具名含 chart/plot/visualize → 'chart'。