@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,57 @@
1
+ /* Identity and personality inside the Bot settings side window. The panel owns
2
+ no colour of its own: every surface, border, ink and focus ring is a product
3
+ token, so light, dark and the DS theme all follow the window around it. */
4
+ html body[data-bot-product-ui] .bp-profile { display: flex; flex-direction: column; gap: 12px; padding: 14px; border: 1px solid var(--bp-border); border-radius: 12px; background: var(--bp-surface); color: var(--bp-ink); font-family: var(--bp-font, system-ui); }
5
+ html body[data-bot-product-ui] .bp-profile-head { display: flex; align-items: center; gap: 11px; }
6
+ html body[data-bot-product-ui] .bp-profile-badge { display: grid; place-items: center; flex: none; width: 34px; height: 34px; border-radius: 10px; background: var(--bp-subtle); color: var(--bp-ink); }
7
+ html body[data-bot-product-ui] .bp-profile-title { min-width: 0; }
8
+ html body[data-bot-product-ui] .bp-profile-title h3 { margin: 0; font-size: 14px; font-weight: 600; }
9
+ html body[data-bot-product-ui] .bp-profile-title p { margin: 3px 0 0; font-size: 12px; line-height: 1.5; color: var(--bp-muted); }
10
+ html body[data-bot-product-ui] .bp-profile-revision { flex: none; margin-left: auto; padding: 4px 9px; border: 1px solid var(--bp-border); border-radius: 999px; font-size: 11px; color: var(--bp-muted); white-space: nowrap; }
11
+ html body[data-bot-product-ui] .bp-profile-search { display: flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-bg); color: var(--bp-muted); }
12
+ html body[data-bot-product-ui] .bp-profile-search:focus-within { border-color: var(--bp-focus); }
13
+ html body[data-bot-product-ui] .bp-profile-search input { flex: 1; min-width: 0; height: 34px; border: 0; outline: none; background: transparent; color: var(--bp-ink); font: inherit; font-size: 13px; }
14
+ html body[data-bot-product-ui] .bp-profile-search input::placeholder { color: var(--bp-quiet); }
15
+ html body[data-bot-product-ui] .bp-profile-search button { display: grid; place-items: center; flex: none; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--bp-muted); cursor: pointer; }
16
+ html body[data-bot-product-ui] .bp-profile-search button:hover { background: var(--bp-hover); color: var(--bp-ink); }
17
+ html body[data-bot-product-ui] .bp-profile-status { margin: 0; font-size: 12px; }
18
+ html body[data-bot-product-ui] .bp-profile-error { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--bp-border); border-radius: 9px; background: var(--bp-danger-bg); color: var(--bp-danger); font-size: 12px; }
19
+ html body[data-bot-product-ui] .bp-profile-error p { margin: 0; flex: 1; min-width: 0; }
20
+ html body[data-bot-product-ui] .bp-profile-error button { flex: none; padding: 5px 10px; border: 1px solid currentcolor; border-radius: 7px; background: transparent; color: inherit; font: inherit; font-size: 12px; cursor: pointer; }
21
+ html body[data-bot-product-ui] .bp-profile-group { border: 1px solid var(--bp-border); border-radius: 10px; background: var(--bp-bg); overflow: hidden; }
22
+ html body[data-bot-product-ui] .bp-profile-group[data-open="false"] { background: var(--bp-surface); }
23
+ html body[data-bot-product-ui] .bp-profile-group-head { display: flex; align-items: center; gap: 8px; width: 100%; padding: 10px 12px; border: 0; background: transparent; color: var(--bp-ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
24
+ html body[data-bot-product-ui] .bp-profile-group-head:hover { background: var(--bp-hover); }
25
+ html body[data-bot-product-ui] .bp-profile-group-head svg:first-child { flex: none; color: var(--bp-muted); }
26
+ html body[data-bot-product-ui] .bp-profile-group-label { font-weight: 600; }
27
+ html body[data-bot-product-ui] .bp-profile-group-hint { margin-left: auto; font-size: 11px; color: var(--bp-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
28
+ html body[data-bot-product-ui] .bp-profile-group-head svg:last-child { flex: none; color: var(--bp-muted); transition: rotate .15s ease; }
29
+ html body[data-bot-product-ui] .bp-profile-group[data-open="true"] .bp-profile-group-head svg:last-child { rotate: 180deg; }
30
+ html body[data-bot-product-ui] .bp-profile-group-body { display: flex; flex-direction: column; gap: 12px; padding: 2px 12px 12px; }
31
+ html body[data-bot-product-ui] .bp-profile-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
32
+ html body[data-bot-product-ui] .bp-profile-field-head { display: flex; align-items: baseline; gap: 8px; }
33
+ html body[data-bot-product-ui] .bp-profile-field-label { font-size: 12px; font-weight: 600; color: var(--bp-ink); }
34
+ html body[data-bot-product-ui] .bp-profile-field-count { margin-left: auto; font-size: 11px; color: var(--bp-quiet); font-variant-numeric: tabular-nums; }
35
+ html body[data-bot-product-ui] .bp-profile-field input, html body[data-bot-product-ui] .bp-profile-field textarea { width: 100%; min-width: 0; padding: 8px 10px; border: 1px solid var(--bp-border); border-radius: 8px; background: var(--bp-surface); color: var(--bp-ink); font: inherit; font-size: 13px; line-height: 1.55; resize: vertical; }
36
+ html body[data-bot-product-ui] .bp-profile-field input::placeholder, html body[data-bot-product-ui] .bp-profile-field textarea::placeholder { color: var(--bp-quiet); }
37
+ html body[data-bot-product-ui] .bp-profile-field input:focus-visible, html body[data-bot-product-ui] .bp-profile-field textarea:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: -1px; border-color: var(--bp-focus); }
38
+ html body[data-bot-product-ui] .bp-profile-field textarea { min-height: 92px; }
39
+ html body[data-bot-product-ui] .bp-profile-field-help { font-size: 11px; line-height: 1.55; color: var(--bp-muted); }
40
+ html body[data-bot-product-ui] .bp-profile-field-problem { font-size: 11px; line-height: 1.5; color: var(--bp-danger); }
41
+ html body[data-bot-product-ui] .bp-profile-save { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding-top: 2px; }
42
+ html body[data-bot-product-ui] .bp-profile-pill { flex: none; padding: 4px 9px; border: 1px solid var(--bp-border); border-radius: 999px; background: var(--bp-subtle); font-size: 11px; color: var(--bp-muted); }
43
+ html body[data-bot-product-ui] .bp-profile-pill[data-tone="warn"] { border-color: var(--bp-warning); background: var(--bp-warning-bg); color: var(--bp-warning); }
44
+ html body[data-bot-product-ui] .bp-profile-save button { flex: none; margin-left: auto; }
45
+ html body[data-bot-product-ui] .bp-profile-feedback { margin: 0; font-size: 12px; line-height: 1.55; color: var(--bp-muted); }
46
+ html body[data-bot-product-ui] .bp-profile-feedback[data-kind="saved"] { color: var(--bp-solid); }
47
+ html body[data-bot-product-ui] .bp-profile-feedback[data-kind="error"] { color: var(--bp-danger); }
48
+ html body[data-bot-product-ui] .bp-profile-feedback[data-kind="notice"] { color: var(--bp-warning); }
49
+ html body[data-bot-product-ui] .bp-profile button:focus-visible, html body[data-bot-product-ui] .bp-profile input:focus-visible, html body[data-bot-product-ui] .bp-profile textarea:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: 1px; }
50
+ /* A phone-width settings window keeps every control reachable without sideways scroll. */
51
+ @media (max-width: 520px) {
52
+ html body[data-bot-product-ui] .bp-profile { padding: 12px; gap: 10px; }
53
+ html body[data-bot-product-ui] .bp-profile-revision { width: 100%; margin-left: 0; align-self: flex-start; text-align: left; }
54
+ html body[data-bot-product-ui] .bp-profile-head { flex-wrap: wrap; }
55
+ html body[data-bot-product-ui] .bp-profile-group-hint { display: none; }
56
+ html body[data-bot-product-ui] .bp-profile-save button { margin-left: 0; width: 100%; }
57
+ }
@@ -0,0 +1,97 @@
1
+ import { BOT_PROFILE_LIMITS, botProfileNameProblem } from '../../packages/orchestration/src/bot-profile.mjs'
2
+
3
+ /**
4
+ * The identity slice's own model: which fields exist, what the Host persists,
5
+ * and the words the panel uses. The limits and the name rule come from the
6
+ * wiring service so the panel can never offer a value the Host would refuse;
7
+ * the Host still decides, this is only the pre-check that keeps a save from
8
+ * being sent to fail.
9
+ *
10
+ * What is deliberately absent: no soul column, no reply-style enum, no
11
+ * personality switch. The frozen roster persists name, role and instructions
12
+ * and nothing else about who a Bot is, so a control with no field behind it
13
+ * would be a lie the owner only discovers later.
14
+ */
15
+ export const BOT_PROFILE_FIELDS = Object.freeze(['name', 'role', 'instructions'])
16
+
17
+ export const BOT_PROFILE_GROUPS = Object.freeze([
18
+ { id: 'identity', label: '身份', icon: 'bot', hint: '名称与职责', fields: Object.freeze(['name', 'role']) },
19
+ { id: 'voice', label: '对话指令与性格', icon: 'spark', hint: '每次对话都会读', fields: Object.freeze(['instructions']) },
20
+ ])
21
+
22
+ export const BOT_PROFILE_FIELD_META = Object.freeze({
23
+ name: { label: '名称', limit: BOT_PROFILE_LIMITS.name, control: 'input', placeholder: '例如:研究伙伴',
24
+ help: '群聊与名单里显示的名字,不能与其他 Bot 同名。',
25
+ terms: Object.freeze(['名称', '名字', '昵称', 'name']) },
26
+ role: { label: '职责 / 描述', limit: BOT_PROFILE_LIMITS.role, control: 'input', placeholder: '例如:整理可借鉴的设计资料',
27
+ help: '一句话说明这个 Bot 负责什么;名单与群聊成员里显示这一句。',
28
+ terms: Object.freeze(['职责', '描述', '一句话', '定位', 'role', 'title']) },
29
+ instructions: { label: '对话指令 / 性格', limit: BOT_PROFILE_LIMITS.instructions, control: 'textarea', placeholder: '例如:先给结论,再给依据;不确定时说明还缺什么。',
30
+ help: '这些指令每次对话都会进入上下文,决定这个 Bot 怎么说话、怎么做事。它不授予任何工具权限:模型、工具与执行权限请在“模型与官方工具”里设置。',
31
+ terms: Object.freeze(['指令', '性格', '对话', '人设', 'instructions', 'personality', 'persona']) },
32
+ })
33
+
34
+ /** The editable draft: exactly the three fields the Host persists, nothing more. */
35
+ export function botProfileDraft(view) {
36
+ return { name: view?.bot?.name ?? '', role: view?.bot?.role ?? '', instructions: view?.bot?.instructions ?? '' }
37
+ }
38
+
39
+ /** Only changed fields travel, so an untouched field is never re-sent to the Host. */
40
+ export function botProfilePatch(draft, view) {
41
+ const patch = {}
42
+ for (const field of BOT_PROFILE_FIELDS) if (draft[field] !== (view?.bot?.[field] ?? '')) patch[field] = draft[field]
43
+ return patch
44
+ }
45
+
46
+ export function botProfileDirty(draft, view) {
47
+ return Object.keys(botProfilePatch(draft, view)).length > 0
48
+ }
49
+
50
+ /** Live problems that make a save pointless before it reaches the Host. */
51
+ export function botProfileProblems(draft) {
52
+ const problems = []
53
+ for (const field of BOT_PROFILE_FIELDS) {
54
+ const value = draft[field]
55
+ if (value.length > BOT_PROFILE_FIELD_META[field].limit) problems.push({ field, text: `${BOT_PROFILE_FIELD_META[field].label}不能超过 ${BOT_PROFILE_FIELD_META[field].limit} 个字符。` })
56
+ }
57
+ const name = botProfileNameProblem(draft.name.trim())
58
+ if (name !== undefined) problems.push({ field: 'name', text: name })
59
+ return problems
60
+ }
61
+
62
+ /** Group visibility for a search: a group matches its own words or any field's. */
63
+ export function botProfileSearch(query) {
64
+ const needle = String(query ?? '').trim().toLowerCase()
65
+ if (needle === '') return null
66
+ const hit = text => String(text).toLowerCase().includes(needle)
67
+ const groups = {}
68
+ for (const group of BOT_PROFILE_GROUPS) {
69
+ groups[group.id] = hit(group.label) || hit(group.hint) || group.fields.some(field => {
70
+ const meta = BOT_PROFILE_FIELD_META[field]
71
+ return hit(meta.label) || hit(meta.help) || hit(meta.placeholder) || meta.terms.some(hit)
72
+ })
73
+ }
74
+ return { needle, groups, any: Object.values(groups).some(Boolean) }
75
+ }
76
+
77
+ export function botProfileSavedText(result) {
78
+ const fields = Object.freeze({ name: '名称', role: '职责', instructions: '对话指令' })
79
+ const saved = (result?.saved ?? []).map(field => fields[field] ?? field)
80
+ if (saved.length === 0) return '与宿主一致,没有需要保存的修改。'
81
+ return `已保存:${saved.join('、')}。`
82
+ }
83
+
84
+ /** One place that turns a Host refusal into words the owner can act on. */
85
+ export function botProfileFailure(error) {
86
+ switch (error?.code) {
87
+ case 'BOT_PROFILE_CONFLICT': return '这个 Bot 的身份在别处已更新。请重新读取后再保存,你未保存的修改会保留。'
88
+ case 'BOT_PROFILE_UNKNOWN_BOT': return '这个 Bot 不在已提交的宿主目录中,无法修改它的身份。'
89
+ case 'BOT_PROFILE_UNCONFIRMED': return '宿主尚未确认这次保存,请重新读取后核对。'
90
+ case 'BOT_PROFILE_SEAM_UNAVAILABLE': return '当前宿主还不支持保存 Bot 身份,请更新完整插件。'
91
+ case 'BOT_PROFILE_NAME_INVALID': case 'BOT_PROFILE_NAME_TAKEN': case 'BOT_PROFILE_LIMIT': return error.message
92
+ case 'REMOTE_OWNER_REQUIRED': return '只有已认证的宿主拥有者可以修改 Bot 身份。'
93
+ case 'PRODUCT_ACTION_UNAVAILABLE': case 'INVALID_PRODUCT_ACTION': return '当前连接尚不支持身份与个性设置,请更新完整插件。'
94
+ case 'REQUEST_ABORTED': return '保存结果尚未确认,请重新读取后核对。'
95
+ default: return '暂时无法保存,请重新读取后核对。'
96
+ }
97
+ }
@@ -0,0 +1,18 @@
1
+ html body[data-bot-product-ui] .bp-bot-settings-backdrop { position: fixed; inset: 0; z-index: 9900; display: flex; justify-content: flex-end; background: #0005; color: var(--bp-ink); font-family: var(--bp-font, system-ui); }
2
+ html body[data-bot-product-ui] .bp-bot-settings-dialog { display: flex; flex-direction: column; width: min(620px, 100vw); min-width: 0; height: 100%; background: var(--bp-bg); box-shadow: -12px 0 45px #0002; border-left: 1px solid var(--bp-border); }
3
+ html body[data-bot-product-ui] .bp-bot-settings-header { display: flex; align-items: center; flex: none; gap: 12px; padding: 18px 22px; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); }
4
+ html body[data-bot-product-ui] .bp-bot-settings-header h2 { margin: 0; font-size: 17px; font-weight: 600; }
5
+ html body[data-bot-product-ui] .bp-bot-settings-header p { margin: 4px 0 0; font-size: 12px; color: var(--bp-muted); }
6
+ html body[data-bot-product-ui] .bp-bot-settings-header button { display: grid; place-items: center; flex: none; margin-left: auto; width: 36px; height: 36px; padding: 0; border: 0; border-radius: 9px; background: transparent; color: var(--bp-ink); cursor: pointer; }
7
+ html body[data-bot-product-ui] .bp-bot-settings-header button:hover { background: var(--bp-hover); }
8
+ html body[data-bot-product-ui] .bp-bot-settings-avatar { display: grid; place-items: center; flex: none; width: 42px; height: 42px; border-radius: 12px; background: var(--bp-subtle); }
9
+ html body[data-bot-product-ui] .bp-bot-settings-body { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 22px; }
10
+ html body[data-bot-product-ui] .bp-bot-settings-tabs { display: flex; flex: none; gap: 4px; padding: 10px 18px 0; border-bottom: 1px solid var(--bp-border); background: var(--bp-surface); }
11
+ html body[data-bot-product-ui] .bp-bot-settings-tabs button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 11px 12px; margin-bottom: -1px; color: var(--bp-muted); background: transparent; border: 0; border-bottom: 2px solid transparent; font: inherit; font-size: 13px; cursor: pointer; }
12
+ html body[data-bot-product-ui] .bp-bot-settings-tabs button[aria-selected=true] { color: var(--bp-ink); border-bottom-color: var(--bp-ink); font-weight: 600; }
13
+ html body[data-bot-product-ui] .bp-bot-settings-body > [hidden] { display: none; }
14
+ html body[data-bot-product-ui] .bp-bot-settings-error { padding: 12px; margin-bottom: 14px; border: 1px solid var(--bp-border); border-radius: 10px; color: var(--bp-ink); background: var(--bp-surface); font-size: 13px; }
15
+ html body[data-bot-product-ui] .bp-bot-settings-error button { padding: 6px 12px; color: inherit; background: var(--bp-subtle); border: 1px solid var(--bp-border); border-radius: 7px; cursor: pointer; }
16
+ html body[data-bot-product-ui] .bp-bot-settings-dialog button:focus-visible { outline: 2px solid var(--bp-focus); outline-offset: -2px; }
17
+ @media (max-width: 520px) { html body[data-bot-product-ui] .bp-bot-settings-body { padding: 16px; } html body[data-bot-product-ui] .bp-bot-settings-header { padding: 14px 16px; } }
18
+ html body[data-bot-product-ui]:has(.bp-bot-settings-backdrop) { overflow: hidden; }
@@ -0,0 +1,111 @@
1
+ import React, { useEffect, useId, useMemo, useRef, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { BotBindingEditor } from './binding-editor.jsx'
4
+ import { BotProfileIdentity } from './bot-profile-identity.jsx'
5
+ import { UiIcon } from './ui-icons.jsx'
6
+ import css from './bot-settings-dialog.css'
7
+
8
+ let nextScope = 0
9
+ export function BotSettingsDialog(props) {
10
+ const key = useMemo(() => ++nextScope, [props.productApi, props.botId])
11
+ return <SettingsDialog key={key} {...props}/>
12
+ }
13
+
14
+ function SettingsDialog({ productApi, botId, onClose, onOpenTools }) {
15
+ const [view, setView] = useState(null), [error, setError] = useState(''), [loading, setLoading] = useState(true), [busy, setBusy] = useState(false)
16
+ const [tab, setTab] = useState('binding'), [identityVisited, setIdentityVisited] = useState(false)
17
+ const tabId = useId()
18
+ const surface = useRef(null), closeButton = useRef(null)
19
+ const session = useRef({ live: false, epoch: 0, busy: false, requests: new Set() })
20
+ const call = async (action, payload) => {
21
+ const current = session.current, controller = new AbortController()
22
+ current.requests.add(controller)
23
+ let cancel
24
+ const deadline = new Promise((_resolve, reject) => { cancel = () => reject(new Error('REQUEST_ABORTED')); controller.signal.addEventListener('abort', cancel, { once: true }) })
25
+ const timer = setTimeout(() => controller.abort(), 10000)
26
+ try { return await Promise.race([Promise.resolve().then(() => productApi.dispatch(action, payload, controller.signal)), deadline]) }
27
+ finally { clearTimeout(timer); current.requests.delete(controller); controller.signal.removeEventListener('abort', cancel) }
28
+ }
29
+ const read = async () => {
30
+ const current = session.current, epoch = ++current.epoch
31
+ setLoading(true)
32
+ try {
33
+ const value = await call('binding.inspect', { botId })
34
+ if (!current.live || epoch !== current.epoch) return
35
+ if (value?.schemaVersion !== 1 || value.bot?.id !== botId || value.record?.botId !== botId) throw new Error('WRONG_BINDING_OWNER')
36
+ setView(value); setError('')
37
+ } catch (cause) {
38
+ if (current.live && epoch === current.epoch) setError(['PRODUCT_ACTION_UNAVAILABLE', 'INVALID_PRODUCT_ACTION'].includes(cause?.code) ? '当前连接尚不支持模型与工具设置,请更新完整插件。' : '暂时无法读取这个 Bot 的设置,请重试。')
39
+ } finally { if (current.live && epoch === current.epoch) setLoading(false) }
40
+ }
41
+ useEffect(() => {
42
+ const current = session.current; current.live = true
43
+ const restore = document.activeElement
44
+ const style = document.createElement('style'); style.dataset.botSettingsDialog = '1'; style.textContent = css; document.head.append(style)
45
+ closeButton.current?.focus()
46
+ void read()
47
+ return () => {
48
+ current.live = false; current.epoch++
49
+ for (const controller of current.requests) controller.abort()
50
+ style.remove()
51
+ if (restore?.isConnected && typeof restore.focus === 'function') restore.focus()
52
+ }
53
+ }, [productApi, botId])
54
+ useEffect(() => {
55
+ const keyboard = event => {
56
+ if (event.defaultPrevented || event.isComposing || event.keyCode === 229 || document.querySelector('.bp-model-popover')) return
57
+ const active = document.activeElement
58
+ if (!surface.current?.contains(active)) return
59
+ if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose?.() }
60
+ if (event.key === 'Tab') {
61
+ const controls = [...surface.current.querySelectorAll('*')].filter(node => ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'SUMMARY'].includes(node.tagName) && !node.disabled && node.getClientRects().length)
62
+ if (event.shiftKey && active === controls[0]) { event.preventDefault(); controls.at(-1)?.focus() }
63
+ else if (!event.shiftKey && active === controls.at(-1)) { event.preventDefault(); controls[0]?.focus() }
64
+ }
65
+ }
66
+ document.addEventListener('keydown', keyboard)
67
+ return () => document.removeEventListener('keydown', keyboard)
68
+ }, [onClose])
69
+ const dispatch = async (action, payload) => {
70
+ const current = session.current
71
+ if (!current.live || current.busy || !['binding.save', 'funding.account.save'].includes(action) || (action === 'binding.save' && payload?.botId !== botId)) throw new Error('设置暂时无法保存,请重新读取。')
72
+ current.busy = true; setBusy(true)
73
+ try {
74
+ const result = await call(action, payload)
75
+ if (current.live) await read()
76
+ return result
77
+ } catch (cause) {
78
+ if (cause?.message === 'REQUEST_ABORTED') {
79
+ if (current.live) setError('保存结果尚未确认,请重新读取后核对。')
80
+ throw new Error('保存结果尚未确认,请重新读取后核对。')
81
+ }
82
+ throw cause
83
+ } finally { current.busy = false; if (current.live) setBusy(false) }
84
+ }
85
+ const chooseTab = next => { setTab(next); if (next === 'identity') setIdentityVisited(true) }
86
+ const tabKeys = event => {
87
+ if (event.isComposing || event.keyCode === 229 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
88
+ event.preventDefault()
89
+ const next = event.key === 'Home' ? 'binding' : event.key === 'End' ? 'identity' : tab === 'binding' ? 'identity' : 'binding'
90
+ chooseTab(next)
91
+ event.currentTarget.querySelector(`[data-settings-tab="${next}"]`)?.focus()
92
+ }
93
+ return createPortal(<div className="bp-bot-settings-backdrop" onMouseDown={event => { if (event.target === event.currentTarget) onClose?.() }}>
94
+ <section ref={surface} className="bp-bot-settings-dialog" role="dialog" aria-modal="true" aria-label={`${view?.bot?.name ?? 'Bot'} 的设置`}>
95
+ <header className="bp-bot-settings-header"><span className="bp-bot-settings-avatar"><UiIcon name="bot" size={23}/></span><div><h2>{view?.bot?.name ?? 'Bot 设置'}</h2><p>模型、工具与 Bot 个性</p></div><button ref={closeButton} type="button" aria-label="关闭 Bot 设置" onClick={onClose}><UiIcon name="close" size={19}/></button></header>
96
+ <div className="bp-bot-settings-tabs" role="tablist" aria-label="Bot 设置分类" onKeyDown={tabKeys}>
97
+ {[['binding', '模型与工具', 'spark'], ['identity', '身份与个性', 'bot']].map(([id, label, icon]) => <button key={id} type="button" role="tab" data-settings-tab={id} id={`${tabId}-${id}`} aria-controls={`${tabId}-${id}-panel`} aria-selected={tab === id} tabIndex={tab === id ? 0 : -1} onClick={() => chooseTab(id)}><UiIcon name={icon} size={15}/>{label}</button>)}
98
+ </div>
99
+ <div className="bp-page bp-bot-settings-body">
100
+ <div role="tabpanel" id={`${tabId}-binding-panel`} aria-labelledby={`${tabId}-binding`} hidden={tab !== 'binding'}>
101
+ {loading && !view && <p className="bp-muted" role="status">正在读取 Bot 设置…</p>}
102
+ {error && <div className="bp-bot-settings-error" role="alert"><p>{error}</p><button type="button" disabled={busy || loading} onClick={() => void read()}>重新读取</button></div>}
103
+ {view && <BotBindingEditor bot={view.bot} record={view.record} accounts={view.accounts} dispatch={dispatch} hostScope={productApi} busy={busy || loading || Boolean(error)} onOpenTools={onOpenTools}/>}
104
+ </div>
105
+ <div role="tabpanel" id={`${tabId}-identity-panel`} aria-labelledby={`${tabId}-identity`} hidden={tab !== 'identity'}>
106
+ {identityVisited && <BotProfileIdentity productApi={productApi} botId={botId} busy={busy} onSaved={bot => setView(current => current && bot.id === botId ? { ...current, bot: { ...current.bot, name: bot.name, role: bot.role } } : current)}/>}
107
+ </div>
108
+ </div>
109
+ </section>
110
+ </div>, document.body)
111
+ }
@@ -0,0 +1,20 @@
1
+ /* The directory palette is scoped to .bp-threads. The header lives in the
2
+ existing chat toolbar and inherits that conversation's appearance instead. */
3
+ html body[data-bot-product-ui] .bp-threads-head-wrap {
4
+ --bp-thread-surface: var(--bp-chat-surface);
5
+ --bp-thread-inset: var(--bp-chat-inset);
6
+ --bp-thread-hover: var(--bp-chat-hover);
7
+ --bp-thread-line: var(--bp-chat-line);
8
+ --bp-thread-ink: var(--bp-chat-ink);
9
+ --bp-thread-muted: var(--bp-chat-muted);
10
+ --bp-thread-accent: var(--bp-chat-accent);
11
+ --bp-thread-accent-ink: var(--bp-chat-accent-ink);
12
+ --bp-thread-focus: var(--bp-chat-focus);
13
+ --bp-thread-wait: var(--bp-chat-warning);
14
+ }
15
+ html body[data-bot-product-ui] .bp-threads-head-wrap :is(button, input):focus-visible { outline: 2px solid var(--bp-chat-focus); outline-offset: 2px; }
16
+ @media (max-width: 640px) {
17
+ html body[data-bot-product-ui] .bp-threads-head-wrap { position: static; }
18
+ html body[data-bot-product-ui] .bp-threads-head-wrap .bp-threads-head { max-width: 76px; padding: 0 6px; }
19
+ html body[data-bot-product-ui] .bp-threads-head-wrap .bp-threads-head-name { max-width: 38px; }
20
+ }
@@ -0,0 +1,278 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
2
+ import { UiIcon } from './ui-icons.jsx'
3
+ import { THREAD_ACTIONS, formatThreadAt, isArchivedThread, sortedThreads, threadLabel } from './bot-threads-panel.jsx'
4
+
5
+ // The chat-header control for a Bot's threads.
6
+ //
7
+ // It answers two questions the native header cannot: which thread of this Bot
8
+ // is open, and what the Bot's other threads are. Everything else about the
9
+ // conversation — the Bot's model, its computer, its memory, its tasks — stays
10
+ // where the product already puts it, so this is one control, not a second
11
+ // toolbar.
12
+ //
13
+ // It owns no authority. Opening a thread asks the Host for that thread's real
14
+ // Session and only then hands the id to `onOpenSession` (the native
15
+ // `actions.openSession` by default); creating one is the same, after an
16
+ // explicit create. Nothing here sends a greeting, a message, a model request
17
+ // or a Task, and no thread parameter ever carries a model, a route, a folder
18
+ // or a credential — the Bot's committed grant decides what a turn may do.
19
+ //
20
+ // The Host is read through the product API with an abort, a deadline and an
21
+ // epoch per read: a late or withdrawn answer never rewrites the list, and the
22
+ // list is read for one Bot on a change or while the menu is open, never as a
23
+ // repeated full snapshot.
24
+
25
+ const REQUEST_MS = 8000
26
+ const MENU_MS = 2500
27
+ const MAX_NAME = 80
28
+
29
+ let headSeq = 0
30
+
31
+ // An IME composition is not a submission: Enter that confirms a candidate must
32
+ // never create a thread.
33
+ const submits = event => event.key === 'Enter' && event.nativeEvent?.isComposing !== true && event.keyCode !== 229
34
+
35
+ function abortable(work, signal) {
36
+ let cancel
37
+ const interrupted = new Promise((_resolve, reject) => {
38
+ cancel = () => reject(new Error('THREAD_REQUEST_ABORTED'))
39
+ signal.addEventListener('abort', cancel, { once: true })
40
+ if (signal.aborted) cancel()
41
+ })
42
+ return Promise.race([Promise.resolve().then(work), interrupted]).finally(() => signal.removeEventListener('abort', cancel))
43
+ }
44
+
45
+ /** The same Bot-scoped refusals the directory panel reports, in the owner's words. */
46
+ export function headerThreadNote(error) {
47
+ switch (error?.code) {
48
+ case 'BOT_THREADS_STORAGE_FAILED': return '线程目录无法读取,已停止写入与发送。请修复存储后重启宿主。'
49
+ case 'THREAD_ARCHIVED': return '这个线程已归档,只能只读打开。'
50
+ case 'SESSION_UNKNOWN': return '这个线程的会话建立结果未知,请在线程目录中核对后恢复。'
51
+ case 'SESSION_NOT_OWNED': return '宿主没有确认这个线程会话的归属。'
52
+ case 'BOT_GRANT_EMPTY': return '这个 Bot 的授权已被撤销或停用。'
53
+ case 'BOT_NOT_COMMITTED': return '这个 Bot 已不在已提交状态。'
54
+ case 'RUNTIME_MODE_MISMATCH': return '这个 Bot 已由另一个运行时提供服务。'
55
+ case 'THREAD_REQUEST_ABORTED': return '宿主没有在约定时间内回答。'
56
+ default: return '操作未得到宿主确认。'
57
+ }
58
+ }
59
+
60
+ export function BotThreadHeader({ sessionId, roster, productApi, actions, onOpenSession, onOpenPanel, className }) {
61
+ // The thread that is open, and the Bot it belongs to. The roster indexes a
62
+ // thread Session under its Bot, so both identities are known here.
63
+ const thread = roster?.threadsById?.[sessionId] ?? null
64
+ const bot = roster?.byId?.[sessionId] ?? null
65
+ const botId = thread?.botId ?? bot?.id ?? null
66
+ const [open, setOpen] = useState(false)
67
+ const [rows, setRows] = useState(null)
68
+ const [pending, setPending] = useState(false)
69
+ const [note, setNote] = useState('')
70
+ const [draft, setDraft] = useState('')
71
+ const [place, setPlace] = useState(null)
72
+ const live = useRef({ mounted: false, requests: new Set(), queue: Promise.resolve(), loaded: false, inflight: null })
73
+ const root = useRef(null)
74
+ const chip = useRef(null)
75
+ // Two epochs, one per kind of read: only the newest open may navigate, and
76
+ // only the newest list may rewrite the rows.
77
+ const opening = useRef(0)
78
+ const reading = useRef(0)
79
+ // One create intent, one key, held until the Host answers (see createThread).
80
+ const createKey = useRef(null)
81
+ const navigate = id => (typeof onOpenSession === 'function' ? onOpenSession(id) : actions?.openSession?.(id))
82
+ // Closing gives the caret back to the chip that opened the menu, so a keyboard
83
+ // owner is never dropped on the page when the input disappears.
84
+ const closeMenu = () => {
85
+ setOpen(false)
86
+ if (root.current?.contains(document.activeElement)) chip.current?.focus()
87
+ }
88
+ // One guarded call at a time, in the order it was made; a reply counts only
89
+ // while this control is mounted and no newer call of the same kind began.
90
+ const call = (action, payload, epoch) => {
91
+ const mine = epoch === undefined ? 0 : epoch.current + 1
92
+ if (epoch !== undefined) epoch.current = mine
93
+ // The newest call retires the one before it, so a Host that never answers
94
+ // cannot hold the queue.
95
+ const idle = live.current.inflight === null
96
+ live.current.inflight?.abort()
97
+ const run = idle ? step(action, payload, epoch, mine) : live.current.queue.then(() => step(action, payload, epoch, mine))
98
+ live.current.queue = run.then(() => undefined, () => undefined)
99
+ return run
100
+ }
101
+ const step = async (action, payload, epoch, mine) => {
102
+ const current = live.current
103
+ if (!current.mounted || typeof productApi?.dispatch !== 'function') return { abandoned: true }
104
+ const controller = new AbortController()
105
+ current.inflight = controller
106
+ current.requests.add(controller)
107
+ let expired = false
108
+ const timeout = setTimeout(() => { expired = true; controller.abort() }, REQUEST_MS)
109
+ try {
110
+ const result = await abortable(() => productApi.dispatch(action, payload, controller.signal), controller.signal)
111
+ if (!current.mounted || controller.signal.aborted || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
112
+ return { ok: true, result }
113
+ } catch (error) {
114
+ if (!current.mounted || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
115
+ // A deadline the Host missed is reported; a call a newer one retired is not.
116
+ if (expired) return { ok: false, error: Object.assign(new Error('宿主没有在约定时间内回答。'), { code: 'THREAD_REQUEST_ABORTED' }) }
117
+ return controller.signal.aborted ? { abandoned: true } : { ok: false, error }
118
+ } finally { clearTimeout(timeout); current.requests.delete(controller); if (current.inflight === controller) current.inflight = null }
119
+ }
120
+
121
+ const read = useCallback(async () => {
122
+ const current = live.current
123
+ if (!current.mounted || !botId) return
124
+ const outcome = await call(THREAD_ACTIONS.list, { botId }, reading)
125
+ if (outcome.ok) { current.loaded = true; setRows(sortedThreads(outcome.result?.threads)) }
126
+ else if (outcome.error !== undefined && (!current.loaded || outcome.error.code === 'BOT_THREADS_STORAGE_FAILED')) setNote(headerThreadNote(outcome.error))
127
+ }, [botId, productApi])
128
+
129
+ // Read once per Bot, and again only while the menu is open: a closed control
130
+ // pulls nothing, and an open one follows the Host without a full snapshot.
131
+ useEffect(() => {
132
+ const current = live.current
133
+ current.mounted = true
134
+ void read()
135
+ return () => { current.mounted = false; reading.current += 1; opening.current += 1; for (const controller of current.requests) controller.abort(); current.requests.clear() }
136
+ }, [read])
137
+ useEffect(() => {
138
+ if (!open) return undefined
139
+ const timer = setInterval(() => void read(), MENU_MS)
140
+ return () => clearInterval(timer)
141
+ }, [open, read])
142
+ useEffect(() => {
143
+ if (!open) return undefined
144
+ const away = event => { if (!root.current?.contains(event.target)) setOpen(false) }
145
+ // Escape closes the menu and gives the caret back to the chip that opened it,
146
+ // so a keyboard owner is never dropped on the page.
147
+ const key = event => {
148
+ if (event.key !== 'Escape') return
149
+ closeMenu()
150
+ }
151
+ document.addEventListener('mousedown', away)
152
+ document.addEventListener('keydown', key)
153
+ return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', key) }
154
+ }, [open])
155
+ // The menu is placed against the control's own box, not against whatever
156
+ // ancestor happens to be positioned: it sits in the product's chat toolbar,
157
+ // whose layout the toolbar itself owns.
158
+ useEffect(() => {
159
+ if (!open) { setPlace(null); return undefined }
160
+ const measure = () => {
161
+ const box = root.current?.getBoundingClientRect()
162
+ if (box === undefined) return
163
+ const width = Math.min(300, Math.max(200, window.innerWidth - 24))
164
+ const left = Math.min(Math.max(8, box.right - width), Math.max(8, window.innerWidth - width - 8))
165
+ const below = box.bottom + 6, above = box.top - 6
166
+ setPlace({ left, top: below + 320 < window.innerHeight || above < 320 ? below : undefined, bottom: below + 320 < window.innerHeight || above < 320 ? undefined : window.innerHeight - above })
167
+ }
168
+ measure()
169
+ window.addEventListener('resize', measure)
170
+ window.addEventListener('scroll', measure, true)
171
+ return () => { window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true) }
172
+ }, [open])
173
+
174
+ const openThread = async (threadId, { create = false } = {}) => {
175
+ setNote('')
176
+ setPending(true)
177
+ const outcome = await call(THREAD_ACTIONS.open, { botId, threadId, ...(create ? { create: true } : {}) }, opening)
178
+ setPending(false)
179
+ if (!outcome.ok) { if (outcome.error !== undefined) setNote(headerThreadNote(outcome.error)); return }
180
+ const id = outcome.result?.sessionId ?? null
181
+ // Only a Session the Host confirmed is navigated to.
182
+ if (typeof id === 'string' && id !== '') { closeMenu(); setDraft(''); navigate(id) }
183
+ else if (create) setNote('宿主没有返回这个线程的会话。')
184
+ }
185
+ const createThread = async () => {
186
+ // One intent, one key, held until the Host answers: a timeout or an unknown
187
+ // outcome followed by another click is the same intent, so the Host answers
188
+ // with the thread it already made instead of a second one. The key is
189
+ // retired only when the create is confirmed or the owner abandons it.
190
+ if (createKey.current === null) createKey.current = `head-${botId}-${++headSeq}`
191
+ const requestKey = createKey.current
192
+ const name = draft.trim().slice(0, MAX_NAME)
193
+ setNote('')
194
+ setPending(true)
195
+ const made = await call(THREAD_ACTIONS.create, { botId, name: name || undefined, requestKey })
196
+ setPending(false)
197
+ const threadId = made.result?.thread?.threadId
198
+ if (typeof threadId !== 'string') {
199
+ if (made.error !== undefined) setNote(headerThreadNote(made.error))
200
+ // Unconfirmed: the key stands, so the next attempt is the same intent.
201
+ return
202
+ }
203
+ createKey.current = null
204
+ setDraft('')
205
+ await openThread(threadId, { create: true })
206
+ }
207
+
208
+ const list = rows ?? []
209
+ const current = list.find(row => row.sessionId === sessionId || row.threadId === thread?.threadId) ?? null
210
+ // The Bot's own default chat is not a thread and is never given a substitute
211
+ // Session: it is listed so a thread can be left again, and opening it hands
212
+ // the shell the Bot id it already trusts.
213
+ const defaultChat = { threadId: null, name: `${bot?.name ?? botId} 的默认聊天`, sessionId: bot?.sessionId ?? bot?.id ?? botId, state: 'open', archivedAt: null, sessionState: 'owned', updatedAt: 0, active: current === null }
214
+ // The thread's own name comes from the freshest source that has it: the
215
+ // directory answer for a thread the Host has not recorded yet, the roster for
216
+ // one it has. The Bot beside it is the identity the roster confirms.
217
+ const label = current !== null ? threadLabel(current) : thread !== null ? threadLabel({ name: thread.name }) : (bot?.name ?? '')
218
+ const botName = bot?.name ?? null
219
+ const title = botName !== null ? `${label} · ${botName}` : label
220
+ // Without a Bot there is nothing to switch: the default chat is not a
221
+ // thread, and this control stays out of the way.
222
+ if (botId === null) return null
223
+ return (
224
+ <div className={`bp-threads-head-wrap${open ? ' is-open' : ''}${className ? ` ${className}` : ''}`} ref={root}>
225
+ <button type="button" className="bp-threads-head" ref={chip} aria-haspopup="menu" aria-expanded={open}
226
+ aria-label={botName !== null ? `当前线程 ${label}(${botName}),打开这个 Bot 的线程列表` : `打开这个 Bot 的线程列表`}
227
+ title={title} onClick={() => { setOpen(!open); if (!open) void read() }}>
228
+ <UiIcon name="list" size={14} />
229
+ <span className="bp-threads-head-name">{label}</span>
230
+ {list.length > 0 && <span className="bp-threads-head-count">{list.length + 1}</span>}
231
+ </button>
232
+ {open && (
233
+ <div className="bp-threads-menu" role="menu" aria-label={`${bot?.name ?? botId} 的会话线程`} style={place === null ? undefined : { position: 'fixed', left: `${place.left}px`, top: place.top === undefined ? undefined : `${place.top}px`, bottom: place.bottom === undefined ? undefined : `${place.bottom}px` }}>
234
+ <p className="bp-threads-menu-title">{bot?.name ?? botId} 的会话</p>
235
+ {note && <p className="bp-threads-note" role="alert">{note}</p>}
236
+ {rows === null && <p className="bp-threads-note" role="status">正在读取线程目录…</p>}
237
+ <ul className="bp-threads-menu-list">
238
+ <li key="default">
239
+ <button type="button" role="menuitem" data-state="default" className={`bp-threads-menu-row${current === null ? ' is-active' : ''}`}
240
+ disabled={pending} onClick={() => { if (current !== null) { closeMenu(); navigate(defaultChat.sessionId) } }}>
241
+ <span className="bp-threads-menu-name">{defaultChat.name}</span>
242
+ <span className="bp-threads-menu-meta">
243
+ {current === null && <span className="bp-thread-badge">当前</span>}
244
+ <span className="bp-thread-badge is-quiet">默认聊天</span>
245
+ </span>
246
+ </button>
247
+ </li>
248
+ {list.map(row => (
249
+ <li key={row.threadId}>
250
+ <button type="button" role="menuitem" data-state={isArchivedThread(row) ? 'archived' : 'open'} className={`bp-threads-menu-row${current?.threadId === row.threadId ? ' is-active' : ''}`}
251
+ disabled={pending} onClick={() => { if (current?.threadId !== row.threadId) void openThread(row.threadId, { create: !isArchivedThread(row) }) }}>
252
+ <span className="bp-threads-menu-name">{threadLabel(row)}</span>
253
+ <span className="bp-threads-menu-meta">
254
+ {current?.threadId === row.threadId && <span className="bp-thread-badge">当前</span>}
255
+ {isArchivedThread(row) && <span className="bp-thread-badge is-quiet">归档 · 只读</span>}
256
+ {row.sessionState === 'unbound' && <span className="bp-thread-badge is-wait">待宿主确认</span>}
257
+ <span className="bp-thread-time">{formatThreadAt(row.updatedAt)}</span>
258
+ </span>
259
+ </button>
260
+ </li>
261
+ ))}
262
+ </ul>
263
+ <div className="bp-threads-menu-new">
264
+ <input className="bp-threads-input" value={draft} maxLength={MAX_NAME} placeholder="新线程名称(可留空)"
265
+ aria-label="新线程名称" onChange={event => setDraft(event.target.value)}
266
+ onKeyDown={event => { if (submits(event) && !pending) void createThread() }} />
267
+ <button type="button" className="bp-threads-button" disabled={pending} onClick={() => void createThread()}>
268
+ <UiIcon name="plus" size={16} /><span>新建线程</span>
269
+ </button>
270
+ </div>
271
+ {typeof onOpenPanel === 'function' && (
272
+ <button type="button" className="bp-threads-menu-more" onClick={() => { closeMenu(); onOpenPanel() }}>在线程目录中管理归档与名称</button>
273
+ )}
274
+ </div>
275
+ )}
276
+ </div>
277
+ )
278
+ }