@ljwei-stak/dsh-model-route-bot 0.66.4 → 0.67.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 (271) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +18 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/package.json +1 -1
  178. package/packages/orchestration/src/binding-product.mjs +19 -0
  179. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  180. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  181. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  182. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  183. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  184. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  185. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  186. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  187. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  188. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  189. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  190. package/packages/orchestration/src/bot-loop.mjs +152 -19
  191. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  192. package/packages/orchestration/src/bot-policy.mjs +43 -4
  193. package/packages/orchestration/src/bot-profile.mjs +171 -0
  194. package/packages/orchestration/src/bot-routine.mjs +4 -4
  195. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  196. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  197. package/packages/orchestration/src/bot-serving.mjs +164 -0
  198. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  199. package/packages/orchestration/src/bot-settings.mjs +50 -11
  200. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  201. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  202. package/packages/orchestration/src/bot-threads.mjs +790 -0
  203. package/packages/orchestration/src/data-product-access.mjs +37 -0
  204. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  205. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  206. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  207. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  208. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  209. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  210. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  211. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  212. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  213. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  214. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  215. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  216. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  217. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  218. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  219. package/packages/orchestration/src/model-accounting.mjs +99 -35
  220. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  221. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  222. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  223. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  224. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  225. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  226. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  227. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  228. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  229. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  230. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  231. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  232. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  233. package/packages/orchestration/src/remote-machines.mjs +82 -14
  234. package/packages/orchestration/src/routine-execution.mjs +5 -3
  235. package/packages/orchestration/src/routine-session.mjs +19 -13
  236. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  237. package/packages/orchestration/src/task-journal.mjs +29 -0
  238. package/packages/orchestration/src/task-product.mjs +17 -0
  239. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  240. package/packages/orchestration/src/team-bridge.mjs +282 -0
  241. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  242. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  243. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  244. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  245. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  246. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  247. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  248. package/remote-computer-agent/README.md +39 -29
  249. package/remote-computer-agent/build.mjs +3 -0
  250. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  253. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  255. package/remote-computer-agent/dist/cli.mjs +1688 -168
  256. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  257. package/remote-computer-agent/dist/index.mjs +1579 -122
  258. package/remote-computer-agent/dist/index.mjs.map +4 -4
  259. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  260. package/remote-computer-agent/package-lock.json +308 -2
  261. package/remote-computer-agent/package.json +2 -1
  262. package/remote-computer-agent/pair-device.ps1 +20 -0
  263. package/remote-computer-agent/setup.ps1 +38 -16
  264. package/remote-computer-agent/src/cli.ts +24 -3
  265. package/remote-computer-agent/src/desktop-session.ts +11 -3
  266. package/remote-computer-agent/src/e2e-server.ts +81 -0
  267. package/remote-computer-agent/src/pairing-page.ts +35 -0
  268. package/remote-computer-agent/src/protocol.ts +25 -25
  269. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  270. package/remote-computer-agent/src/store.ts +20 -5
  271. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,250 @@
1
+ // The chat surface's display model: the numbers the design commits to, plus the
2
+ // checks that read them back out of chat.css. chat.css stays the single source of
3
+ // truth for colour values; this module holds the rules those values must satisfy,
4
+ // so a palette edit that drops below AA fails a test instead of a person's eyes.
5
+ //
6
+ // Nothing here touches the DOM: the node test runs it directly, and the browser
7
+ // verification script imports the same rules so both report one verdict.
8
+
9
+ /** WCAG 2.1 relative luminance of an sRGB colour. */
10
+ export function luminance(color) {
11
+ const channels = channelsOf(color)
12
+ return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722
13
+ }
14
+
15
+ /** WCAG 2.1 contrast ratio, 1:1 through 21:1. */
16
+ export function contrastRatio(a, b) {
17
+ const first = luminance(a), second = luminance(b)
18
+ return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05)
19
+ }
20
+
21
+ /** The design's own numbers. Every value is one a reader can argue with. */
22
+ export const CHAT_DESIGN = Object.freeze({
23
+ /** Body copy is 14px and up, so the bar is the normal-text 4.5:1. */
24
+ bodyMinContrast: 4.5,
25
+ /** Focus rings, hairlines and control borders: WCAG 1.4.11 wants 3:1. */
26
+ uiMinContrast: 3,
27
+ /** Pointer targets stay legible and tappable without becoming furniture. */
28
+ minTouchTarget: 24,
29
+ coarseTouchTarget: 40,
30
+ menuRowHeight: 36,
31
+ focusRingWidth: 2,
32
+ focusRingOffset: 2,
33
+ /** A bubble never takes the whole column: the reply keeps a readable measure. */
34
+ bubbleShare: 0.78,
35
+ /** The narrow layout has to hold on the smallest phone we support. */
36
+ narrowViewport: 375,
37
+ wideViewport: 1360,
38
+ /** Radius ladder, from the composer card down to a badge. */
39
+ radius: Object.freeze({ card: 20, bubble: 16, control: 10, menu: 12, badge: 6 }),
40
+ })
41
+
42
+ // Token pairs the palette must clear, per mode. `on` is what the ink sits on;
43
+ // the minimum comes from CHAT_DESIGN. A pair only exists when both tokens are
44
+ // declared, so adding a surface never breaks an older mode.
45
+ const TEXT_PAIRS = [
46
+ ['--bp-chat-ink', '--bp-chat-canvas'],
47
+ ['--bp-chat-ink', '--bp-chat-surface'],
48
+ ['--bp-chat-ink', '--bp-chat-sidebar'],
49
+ ['--bp-chat-ink', '--bp-chat-inset'],
50
+ ['--bp-chat-muted', '--bp-chat-surface'],
51
+ ['--bp-chat-muted', '--bp-chat-canvas'],
52
+ ['--bp-chat-muted', '--bp-chat-sidebar'],
53
+ ['--bp-chat-muted', '--bp-chat-inset'],
54
+ ['--bp-chat-on-user', '--bp-chat-user'],
55
+ ['--bp-chat-danger', '--bp-chat-surface'],
56
+ ['--bp-chat-danger', '--bp-chat-canvas'],
57
+ ['--bp-chat-danger', '--bp-chat-danger-bg'],
58
+ ['--bp-chat-warning', '--bp-chat-warning-bg'],
59
+ ['--bp-chat-on-accent', '--bp-chat-accent'],
60
+ ]
61
+ const UI_PAIRS = [
62
+ ['--bp-chat-accent', '--bp-chat-surface'],
63
+ ['--bp-chat-accent', '--bp-chat-canvas'],
64
+ ['--bp-chat-accent', '--bp-chat-sidebar'],
65
+ ['--bp-chat-focus', '--bp-chat-surface'],
66
+ ['--bp-chat-focus', '--bp-chat-canvas'],
67
+ ]
68
+ // A hairline is decoration, not a control boundary: WCAG 1.4.11 does not reach it.
69
+ // It still has to be visible, so the floor is a line you can see on its surface.
70
+ const HAIRLINE_MIN = 1.2
71
+ const HAIRLINE_PAIRS = [
72
+ ['--bp-chat-line', '--bp-chat-surface'],
73
+ ['--bp-chat-line', '--bp-chat-canvas'],
74
+ ['--bp-chat-line', '--bp-chat-sidebar'],
75
+ ]
76
+
77
+ const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
78
+
79
+ function channelsOf(color) {
80
+ const value = String(color).trim()
81
+ if (HEX.test(value)) {
82
+ const digits = value.slice(1)
83
+ const wide = digits.length <= 4 ? [...digits].map(digit => digit + digit) : [digits.slice(0, 2), digits.slice(2, 4), digits.slice(4, 6)]
84
+ return wide.slice(0, 3).map(pair => parseInt(pair, 16) / 255).map(channel => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)
85
+ }
86
+ const numbers = value.match(/-?\d*\.?\d+/g)
87
+ if (!numbers) throw new Error(`chat design: cannot read a colour out of ${value}`)
88
+ return numbers.slice(0, 3).map(Number).map(channel => channel / 255).map(channel => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4)
89
+ }
90
+
91
+ /** Splits CSS text into `{ selectors, declarations, atRule }` blocks, nested ones included. */
92
+ export function cssBlocks(text) {
93
+ const blocks = []
94
+ const walk = (start, end, atRule) => {
95
+ let cursor = start
96
+ while (cursor < end) {
97
+ const open = text.indexOf('{', cursor)
98
+ if (open < 0 || open >= end) break
99
+ const close = matchBrace(text, open, end)
100
+ // The prelude carries the comments that sat between this rule and the last one.
101
+ const prelude = text.slice(cursor, open).replace(/\/\*[\s\S]*?\*\//g, '').trim()
102
+ if (prelude.startsWith('@keyframes')) { cursor = close + 1; continue }
103
+ if (prelude.startsWith('@')) walk(open + 1, close, prelude)
104
+ else if (prelude) {
105
+ blocks.push({ selectors: prelude, body: text.slice(open + 1, close), atRule })
106
+ walk(open + 1, close, atRule)
107
+ }
108
+ cursor = close + 1
109
+ }
110
+ }
111
+ walk(0, text.length, null)
112
+ return blocks
113
+ }
114
+
115
+ function matchBrace(text, open, end) {
116
+ let depth = 0
117
+ for (let at = open; at < end; at += 1) {
118
+ if (text[at] === '{') depth += 1
119
+ else if (text[at] === '}') { depth -= 1; if (depth === 0) return at }
120
+ }
121
+ throw new Error('chat design: unbalanced braces in the chat stylesheet')
122
+ }
123
+
124
+ function declarationsOf(body) {
125
+ const found = new Map()
126
+ for (const statement of body.split(';')) {
127
+ const at = statement.indexOf(':')
128
+ if (at < 0) continue
129
+ const name = statement.slice(0, at).trim()
130
+ const value = statement.slice(at + 1).trim()
131
+ if (name.startsWith('--')) found.set(name, value)
132
+ }
133
+ return found
134
+ }
135
+
136
+ /** A selector list split on its top-level commas, so `:not([a=x])` stays one selector. */
137
+ export function selectorList(selectors) {
138
+ const list = []
139
+ let depth = 0, start = 0
140
+ for (let at = 0; at < selectors.length; at += 1) {
141
+ const character = selectors[at]
142
+ if (character === '(') depth += 1
143
+ else if (character === ')') depth -= 1
144
+ else if (character === ',' && depth === 0) { list.push(selectors.slice(start, at).trim()); start = at + 1 }
145
+ }
146
+ list.push(selectors.slice(start).trim())
147
+ return list.filter(Boolean)
148
+ }
149
+
150
+ /**
151
+ * A selector with its guards taken out. `:not([data-bp-appearance=light])` is a
152
+ * condition on when the rule applies, never an appearance of its own, so reading
153
+ * the raw text would file a dark rule under the light one.
154
+ */
155
+ function withoutGuards(selector) {
156
+ let text = selector
157
+ for (let guard = /:(?:not|has|is|where|matches)\(([^()]*)\)/; guard.test(text);) text = text.replace(guard, '')
158
+ return text
159
+ }
160
+
161
+ const isDarkSelector = selector => /data-ds-dark-theme|data-bp-appearance=dark/.test(withoutGuards(selector))
162
+ const isLightSelector = selector => /data-bp-appearance=light/.test(withoutGuards(selector))
163
+
164
+ /** Which appearance a rule paints: `dark`, `light` or `default` (the light values). */
165
+ export function ruleMode(selectors) {
166
+ const list = selectorList(selectors)
167
+ if (list.some(isDarkSelector)) return 'dark'
168
+ if (list.some(isLightSelector)) return 'light'
169
+ return 'default'
170
+ }
171
+
172
+ /**
173
+ * The `--bp-chat-*` tokens chat.css declares, per mode. The dark mode inherits
174
+ * anything the light mode declares and it does not, which is what the stylesheet
175
+ * itself does, so a missing dark token is a real gap rather than a parse artifact.
176
+ */
177
+ export function chatTokens(css) {
178
+ const light = new Map(), dark = new Map()
179
+ for (const block of cssBlocks(css)) {
180
+ if (block.atRule || !/data-bot-product-ui/.test(block.selectors)) continue
181
+ const declarations = declarationsOf(block.body)
182
+ const names = [...declarations.keys()].filter(name => name.startsWith('--bp-chat-'))
183
+ if (!names.length) continue
184
+ // One selector list can paint both appearances at once (the default block names
185
+ // the dark theme as one of its selectors), so each selector is read on its own.
186
+ const targets = new Map()
187
+ for (const selector of selectorList(block.selectors)) {
188
+ const modes = isDarkSelector(selector) ? ['dark'] : isLightSelector(selector) ? ['light'] : ['light', 'dark']
189
+ for (const mode of modes) targets.set(mode, mode === 'dark' ? dark : light)
190
+ }
191
+ for (const [name, value] of declarations) for (const target of targets.values()) if (targets.size) target.set(name, value)
192
+ }
193
+ for (const [name, value] of light) if (!dark.has(name)) dark.set(name, value)
194
+ return { light, dark }
195
+ }
196
+
197
+ /**
198
+ * Every contrast pair the palette declares, with the ratio it actually reaches.
199
+ * `minimum` is 4.5:1 for text and 3:1 for rings and borders.
200
+ */
201
+ export function chatContrast(css) {
202
+ const tokens = chatTokens(css)
203
+ const rows = []
204
+ for (const [mode, values] of [['light', tokens.light], ['dark', tokens.dark]]) {
205
+ for (const [ink, on] of TEXT_PAIRS) {
206
+ if (!values.has(ink) || !values.has(on)) continue
207
+ rows.push({ mode, ink, on, ratio: contrastRatio(values.get(ink), values.get(on)), minimum: CHAT_DESIGN.bodyMinContrast, kind: 'text' })
208
+ }
209
+ for (const [ink, on] of UI_PAIRS) {
210
+ if (!values.has(ink) || !values.has(on)) continue
211
+ rows.push({ mode, ink, on, ratio: contrastRatio(values.get(ink), values.get(on)), minimum: CHAT_DESIGN.uiMinContrast, kind: 'ui' })
212
+ }
213
+ for (const [ink, on] of HAIRLINE_PAIRS) {
214
+ if (!values.has(ink) || !values.has(on)) continue
215
+ rows.push({ mode, ink, on, ratio: contrastRatio(values.get(ink), values.get(on)), minimum: HAIRLINE_MIN, kind: 'hairline' })
216
+ }
217
+ }
218
+ return rows
219
+ }
220
+
221
+ /** The pairs that fall short, as readable lines. */
222
+ export function chatViolations(css) {
223
+ return chatContrast(css)
224
+ .filter(row => row.ratio < row.minimum)
225
+ .map(row => `${row.mode}: ${row.ink} on ${row.on} is ${row.ratio.toFixed(2)}:1, under ${row.minimum}:1`)
226
+ }
227
+
228
+ /**
229
+ * Tokens the dark block has to name for itself. A dark appearance that simply
230
+ * inherits a light token still parses and still computes — it just reads wrong on
231
+ * a dark canvas (a hairline that disappears, a border that turns to mud), which is
232
+ * exactly the kind of regression a contrast check on the inherited value misses.
233
+ */
234
+ export function darkTokenGaps(css) {
235
+ const lightNames = new Set(), darkNames = new Set()
236
+ for (const block of cssBlocks(css)) {
237
+ if (block.atRule || !/data-bot-product-ui/.test(block.selectors)) continue
238
+ const selectors = selectorList(block.selectors)
239
+ const darkOnly = selectors.length > 0 && selectors.every(isDarkSelector)
240
+ const names = [...declarationsOf(block.body).keys()].filter(name => name.startsWith('--bp-chat-'))
241
+ for (const name of names) (darkOnly ? darkNames : lightNames).add(name)
242
+ }
243
+ return [...lightNames].filter(name => !darkNames.has(name)).map(name => `${name} has no dark declaration`)
244
+ }
245
+
246
+ /** Both modes must name the same tokens, or one appearance silently loses a surface. */
247
+ export function tokenGaps(css) {
248
+ const { light, dark } = chatTokens(css)
249
+ return [...light.keys()].filter(name => !dark.has(name)).map(name => `${name} has no dark value`)
250
+ }
@@ -0,0 +1,175 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { UiIcon } from './ui-icons.jsx'
3
+ import { AppearanceControls } from './workspace-ui.jsx'
4
+ import { BotSettingsDialog } from './bot-settings-dialog.jsx'
5
+ import { botModelStatus } from './bot-model-status.mjs'
6
+ import { BotThreadHeader } from './bot-thread-header.jsx'
7
+ import { NativeActivityHeader } from './native-activity.jsx'
8
+ import { IndependentChatEntry } from './independent-chat-entry.jsx'
9
+
10
+ let threadHeaderScope = 0
11
+
12
+ const idOf = value => typeof value === 'string' && value.length > 0 ? value : null
13
+ const resources = Object.freeze([
14
+ { section: 'notifications', label: '通知中心', icon: 'bell' },
15
+ { section: 'bindings', label: 'Bot 团队', icon: 'team' },
16
+ { section: 'machines', label: '远程电脑', icon: 'computer' },
17
+ { section: 'library', label: '资料库', icon: 'folder' },
18
+ { section: 'data', label: '数据与图表', icon: 'chart' },
19
+ { section: 'routine', label: '例行工作', icon: 'repeat' },
20
+ { section: 'usage', label: '费用与用量', icon: 'chart' },
21
+ ])
22
+
23
+ function useUiAction(identity) {
24
+ const [feedback, setFeedback] = useState(null)
25
+ const current = useRef({ identity, epoch: 0, token: 0, mounted: false, pending: false })
26
+ if (current.current.identity !== identity) {
27
+ current.current.identity = identity
28
+ current.current.epoch += 1
29
+ current.current.pending = false
30
+ }
31
+ useEffect(() => {
32
+ current.current.mounted = true
33
+ return () => {
34
+ current.current.mounted = false
35
+ current.current.epoch += 1
36
+ current.current.pending = false
37
+ }
38
+ }, [])
39
+ const run = (label, action) => {
40
+ const state = current.current
41
+ if (!state.mounted || state.pending) return
42
+ const epoch = state.epoch, token = ++state.token
43
+ state.pending = true
44
+ setFeedback({ identity, pending: label, error: '' })
45
+ const finish = (error, failed = false) => {
46
+ if (!state.mounted || state.identity !== identity || state.epoch !== epoch || state.token !== token) return
47
+ state.pending = false
48
+ setFeedback({ identity, pending: '', error: failed ? `${label}失败:${error?.message ?? String(error ?? '未知错误')}` : '' })
49
+ }
50
+ try { Promise.resolve(action()).then(() => finish(), error => finish(error, true)) }
51
+ catch (error) { finish(error, true) }
52
+ }
53
+ return {
54
+ run,
55
+ pending: feedback?.identity === identity && Boolean(feedback.pending),
56
+ error: feedback?.identity === identity ? feedback.error : '',
57
+ dismiss: () => setFeedback(null),
58
+ }
59
+ }
60
+
61
+ function ActionError({ error, onDismiss, className = '' }) {
62
+ if (!error) return null
63
+ return <div className={`bp-chat-action-error ${className}`} role="alert"><span>{error}</span><button type="button" aria-label="关闭错误提示" onClick={onDismiss}><UiIcon name="close" size={14} /></button></div>
64
+ }
65
+
66
+ function closeDetailsOnEscape(event) {
67
+ if (event.key !== 'Escape' || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return
68
+ const details = event.target.closest?.('details[open]')
69
+ if (!details || !event.currentTarget.contains(details)) return
70
+ event.preventDefault()
71
+ event.stopPropagation()
72
+ details.open = false
73
+ details.querySelector('summary')?.focus()
74
+ }
75
+
76
+ /** Supporting navigation below the native Bot contact list. Destinations only change UI focus. */
77
+ export function ContactNavigation({ onNavigate, onOpenAdvanced, compact = false, activeSection, attentionCount, attentionPartial = false, attentionKnown = true, notificationCount = 0, notificationPartial = false, notificationKnown = false }) {
78
+ const [announcedSection, setAnnouncedSection] = useState(null)
79
+ const menu = useRef(null)
80
+ const action = useUiAction('contact-navigation')
81
+ useEffect(() => {
82
+ const change = event => {
83
+ const id = event.detail?.id
84
+ setAnnouncedSection(typeof id === 'string' && id.startsWith('bot-product-') ? id.slice('bot-product-'.length) : id === 'model-router-galgame' ? 'advanced' : null)
85
+ }
86
+ document.addEventListener('bot-product:navigation', change)
87
+ return () => document.removeEventListener('bot-product:navigation', change)
88
+ }, [])
89
+ const active = activeSection === undefined ? announcedSection : activeSection
90
+ const count = Number.isFinite(attentionCount) && attentionCount > 0 ? Math.floor(attentionCount) : 0
91
+ const attentionLabel = !attentionKnown ? '任务待办,状态尚未更新' : count ? `任务待办,${attentionPartial ? '至少 ' : ''}${count} 项需要处理${attentionPartial ? ',仅统计已加载任务' : ''}` : '任务待办'
92
+ const unread = notificationKnown && Number.isSafeInteger(notificationCount) && notificationCount > 0 ? notificationCount : 0
93
+ const unreadLabel = unread ? `${notificationPartial ? '至少 ' : ''}${unread} 条未读通知` : notificationKnown && notificationPartial ? '较早通知的未读状态尚未完整统计' : ''
94
+ const navigate = section => {
95
+ if (typeof onNavigate !== 'function') return
96
+ if (menu.current) menu.current.open = false
97
+ action.run('打开页面', () => onNavigate({ section }))
98
+ }
99
+ return <nav className={`bp-contact-nav${compact ? ' bp-contact-nav-compact' : ''}`} aria-label="聊天辅助导航" onKeyDown={closeDetailsOnEscape}>
100
+ <div className="bp-contact-nav-row">
101
+ {typeof onNavigate === 'function' && <>
102
+ <button type="button" className="bp-contact-nav-button" data-contact-nav="inbox" title={attentionLabel} aria-label={attentionLabel} aria-current={active === 'inbox' || active === 'tasks' ? 'page' : undefined} disabled={action.pending} onClick={() => navigate('inbox')}><UiIcon name="inbox" size={17} /><span className="bp-contact-nav-label">待办</span>{count > 0 && <span className="bp-contact-attention" aria-hidden="true">{count > 99 ? '99+' : `${count}${attentionPartial ? '+' : ''}`}</span>}</button>
103
+ <button type="button" className="bp-contact-nav-button" data-contact-nav="home" title="工作空间" aria-label="工作空间" aria-current={active === 'home' ? 'page' : undefined} disabled={action.pending} onClick={() => navigate('home')}><UiIcon name="home" size={17} /><span className="bp-contact-nav-label">总览</span></button>
104
+ <details ref={menu} className="bp-contact-resources"><summary title={`资源与管理${unreadLabel ? `,${unreadLabel}` : ''}`} aria-label={`资源与管理${unreadLabel ? `,${unreadLabel}` : ''}`} data-active={resources.some(row => row.section === active) || active === 'advanced' ? '' : undefined}><UiIcon name="folder" size={17} /><span className="bp-contact-nav-label">资源</span>{unreadLabel && <span className="bp-contact-unread-dot" aria-hidden="true" />}</summary><div className="bp-contact-resources-menu" role="group" aria-label="资源与管理">
105
+ {resources.map(({ section, label, icon }) => <button type="button" key={section} aria-label={section === 'notifications' && unreadLabel ? `${label},${unreadLabel}` : undefined} aria-current={active === section ? 'page' : undefined} disabled={action.pending} onClick={() => navigate(section)}><UiIcon name={icon} size={17} /><span>{label}</span>{section === 'notifications' && unread > 0 ? <small className="bp-contact-unread-count">{unread > 99 ? '99+' : `${unread}${notificationPartial ? '+' : ''}`}</small> : <UiIcon name="chevron" size={12} />}</button>)}
106
+ {typeof onOpenAdvanced === 'function' && <button type="button" className="bp-contact-advanced" aria-current={active === 'advanced' ? 'page' : undefined} disabled={action.pending} onClick={() => {
107
+ if (menu.current) menu.current.open = false
108
+ action.run('打开高级工作台', () => onOpenAdvanced())
109
+ }}><UiIcon name="settings" size={17} /><span>高级工作台</span><UiIcon name="chevron" size={12} /></button>}
110
+ </div></details>
111
+ </>}
112
+ <AppearanceControls />
113
+ </div>
114
+ <ActionError error={action.error} onDismiss={action.dismiss} />
115
+ </nav>
116
+ }
117
+
118
+ /** Optional replacement for the sidebar's native search/new controls, using their existing dialogs. */
119
+ export function ContactToolbar({ actions, compact = false }) {
120
+ const action = useUiAction('contact-toolbar')
121
+ const search = actions?.openPalette, create = actions?.openNewChat
122
+ return <div className={`bp-contact-toolbar${compact ? ' bp-contact-toolbar-compact' : ''}`}>
123
+ {!compact && <strong>Bot 伙伴</strong>}
124
+ <div className="bp-contact-toolbar-actions">
125
+ {typeof search === 'function' && <button type="button" title="搜索聊天" aria-label="搜索聊天" disabled={action.pending} onClick={() => action.run('打开聊天搜索', () => search(''))}><UiIcon name="search" size={18} /></button>}
126
+ {typeof create === 'function' && <button type="button" title="新建聊天" aria-label="新建聊天" disabled={action.pending} onClick={() => action.run('打开新建聊天', () => create('new'))}><UiIcon name="plus" size={19} /></button>}
127
+ </div>
128
+ <ActionError error={action.error} onDismiss={action.dismiss} />
129
+ </div>
130
+ }
131
+
132
+ /** Addon for HeaderPill. Native details/model/desktop/voice views retain ownership of their workflows. */
133
+ export function ConversationTools({ sessionId, bot, room, roster, actions, running = false, onNavigate, productApi, onOpenTools, onOpenSession, onOpenIndependentChat }) {
134
+ const session = idOf(sessionId), botId = idOf(bot?.id), roomId = idOf(room?.id)
135
+ const isBot = botId !== null && !room, isRoom = roomId !== null && !bot
136
+ const ownerId = isBot ? botId : isRoom ? roomId : null
137
+ const identity = JSON.stringify([session, isBot ? botId : null, isRoom ? roomId : null])
138
+ const action = useUiAction(identity)
139
+ const threadKey = useMemo(() => ++threadHeaderScope, [session, productApi])
140
+ const [settingsOpen, setSettingsOpen] = useState(false)
141
+ useEffect(() => { setSettingsOpen(false) }, [identity, productApi])
142
+ const overflow = useRef(null)
143
+ const details = actions?.showDetails, memory = actions?.openMemory, voice = actions?.openVoice
144
+ if (!session || !ownerId) return null
145
+ const name = (isBot ? bot.name : room.name) || (isBot ? 'Bot' : '群聊')
146
+ const modelState = botModelStatus(isBot ? bot : null, roster?.models)
147
+ const modelName = modelState.label
148
+ const members = isRoom && Array.isArray(room.members) ? room.members.length : null
149
+ const canDetails = typeof details === 'function'
150
+ const buttons = [
151
+ ...(typeof onNavigate === 'function' ? [{ key: 'library', label: isBot ? `${name} 的资料` : '团队资料库', short: '资料库', icon: 'folder', run: () => onNavigate({ section: 'library', ...(isBot ? { botId } : {}) }) }] : []),
152
+ ...(isBot && typeof onNavigate === 'function' ? [{ key: 'data', label: `${name} 的数据与图表`, short: '数据', icon: 'chart', run: () => onNavigate({ section: 'data', botId }) }] : []),
153
+ ...(typeof memory === 'function' && roster?.memory !== false ? [{ key: 'memory', label: isBot ? `${name} 的记忆` : `${name} 的群聊记忆`, short: '记忆', icon: 'memory', run: () => memory(ownerId) }] : []),
154
+ ...(typeof onNavigate === 'function' ? [{ key: 'tasks', label: isBot ? `${name} 的任务` : '团队任务', short: '任务', icon: 'tasks', run: () => onNavigate({ section: 'tasks', ...(isBot ? { botId } : {}) }) }] : []),
155
+ ...(isBot && typeof voice === 'function' ? [{ key: 'voice', label: `和 ${name} 语音聊天`, short: '语音聊天', icon: 'voice', run: () => voice(session) }] : []),
156
+ ]
157
+ if (!canDetails && !buttons.length && !running) return null
158
+ const activate = row => {
159
+ if (overflow.current) overflow.current.open = false
160
+ action.run(`打开${row.short}`, row.run)
161
+ }
162
+ const shortcut = (row, menu = false) => <button type="button" key={row.key} data-chat-command={row.key} aria-label={row.label} title={row.label} disabled={action.pending} onClick={() => activate(row)}><UiIcon name={row.icon} size={17} />{menu && <span>{row.short}</span>}</button>
163
+ return <div className="bp-conversation-tools" data-conversation-kind={isBot ? 'bot' : 'room'} data-running={running ? '' : undefined} data-bot-id={isBot ? botId : undefined} data-room-id={isRoom ? roomId : undefined} role="group" aria-label={`${name} 的聊天工具`} onKeyDown={closeDetailsOnEscape}>
164
+ {isBot && typeof onOpenIndependentChat === 'function' && <IndependentChatEntry productApi={productApi} botId={botId} onOpen={onOpenIndependentChat}/>}
165
+ {isBot && typeof productApi?.dispatch === 'function' && <BotThreadHeader key={threadKey} sessionId={session} roster={roster} productApi={productApi} actions={actions} onOpenSession={onOpenSession} onOpenPanel={typeof onNavigate === 'function' ? () => onNavigate({ section: 'threads', botId }) : undefined}/>}
166
+ {canDetails && (isBot ? <button type="button" className="bp-conversation-model" data-chat-command="model" data-model-different={modelState.different ? '' : undefined} aria-label={`${name} 的模型:${modelName},${modelState.title},打开模型与官方工具设置`} title={`${modelState.title} · 模型与官方工具`} disabled={action.pending} onClick={() => action.run('打开模型设置', () => typeof productApi?.dispatch === 'function' ? setSettingsOpen(true) : typeof onNavigate === 'function' ? onNavigate({ section: 'bindings', botId, compose: true }) : details(session, 'details'))}><UiIcon name="spark" size={15} /><span className="bp-conversation-model-name">{modelName}</span>{modelState.different && <span className="bp-conversation-model-different" aria-hidden="true"/>}<UiIcon name="chevron" size={11} /></button>
167
+ : <button type="button" className="bp-conversation-members" data-chat-command="members" aria-label={`查看 ${name} 的群聊详情`} title="群聊详情" disabled={action.pending} onClick={() => action.run('打开群聊详情', () => details(session, 'details'))}><UiIcon name="team" size={16} /><span>{members === null ? '群聊详情' : `${members} 位成员`}</span></button>)}
168
+ {isBot && <NativeActivityHeader key={`native-activity-${session}-${botId}`} productApi={productApi} botId={botId} sessionId={session}/>}
169
+ {running && <span className="bp-conversation-live" role="status" aria-label="正在处理" title="正在处理"><span aria-hidden="true" /></span>}
170
+ {isBot && canDetails && <button type="button" className="bp-conversation-computer" data-chat-command="desktop" aria-label={`查看 ${name} 的电脑`} title="电脑与桌面" disabled={action.pending} onClick={() => action.run('打开电脑', () => details(session, 'desktop'))}><UiIcon name="computer" size={18} /></button>}
171
+ {buttons.length > 0 && <><div className="bp-conversation-shortcuts">{buttons.map(row => shortcut(row))}</div><details ref={overflow} className="bp-conversation-overflow"><summary title="更多聊天工具" aria-label="更多聊天工具"><UiIcon name="settings" size={18} /></summary><div className="bp-conversation-menu" role="group" aria-label="更多聊天工具">{buttons.map(row => shortcut(row, true))}</div></details></>}
172
+ <ActionError error={action.error} onDismiss={action.dismiss} />
173
+ {settingsOpen && isBot && <BotSettingsDialog productApi={productApi} botId={botId} onClose={() => setSettingsOpen(false)} onOpenTools={typeof onOpenTools === 'function' ? async () => { await onOpenTools(); setSettingsOpen(false) } : undefined}/>}
174
+ </div>
175
+ }