@ljwei-stak/dsh-model-route-bot 0.66.3 → 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 (272) 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 +20 -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/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -0,0 +1,378 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
2
+ import { UiIcon } from './ui-icons.jsx'
3
+
4
+ // The thread panel: several independent conversation threads under one Bot.
5
+ //
6
+ // It is a directory surface, never an execution surface. Every write goes to
7
+ // the Host as a structured action and takes effect only after the Host
8
+ // acknowledges it; the panel offers no send, cancel, retry, stop or approval
9
+ // control, because a thread carries no authority of its own — the Bot's
10
+ // committed model, tools and funding still decide what a turn may do. The
11
+ // browser only ever selects a thread the store already verified.
12
+ //
13
+ // Opening a thread is navigation, and it is the owner's own act: the panel asks
14
+ // the Host for the thread's real Session and only then hands that Session id to
15
+ // `onOpenSession`, which is the one thing the product shell may do with it.
16
+ // Nothing here sends a greeting, a message, a model request or a Task, and a
17
+ // late, timed-out or withdrawn answer never moves the user anywhere.
18
+
19
+ const VISIBLE_MS = 5000
20
+ const FAILED_MS = 15000
21
+ const REQUEST_MS = 8000
22
+ const MAX_NAME = 80
23
+ export const THREAD_ACTIONS = Object.freeze({
24
+ readModel: 'threads.readModel',
25
+ list: 'threads.list',
26
+ create: 'threads.create',
27
+ rename: 'threads.rename',
28
+ switch: 'threads.switch',
29
+ open: 'threads.open',
30
+ recover: 'threads.recover',
31
+ archive: 'threads.archive',
32
+ unarchive: 'threads.unarchive',
33
+ })
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
+ /** Present, not truthy: a thread stored with archivedAt 0 is archived. */
46
+ export function isArchivedThread(thread) {
47
+ return thread?.archivedAt !== undefined && thread.archivedAt !== null
48
+ }
49
+
50
+ export function formatThreadAt(at, now = Date.now()) {
51
+ if (!Number.isSafeInteger(at) || at < 0) return ''
52
+ const seconds = Math.max(0, Math.round((now - at) / 1000))
53
+ if (seconds < 45) return '刚刚'
54
+ const minutes = Math.round(seconds / 60)
55
+ if (minutes < 60) return `${minutes} 分钟前`
56
+ const hours = Math.round(minutes / 60)
57
+ if (hours < 24) return `${hours} 小时前`
58
+ return `${Math.round(hours / 24)} 天前`
59
+ }
60
+
61
+ export function threadLabel(thread) {
62
+ const name = typeof thread?.name === 'string' ? thread.name.trim() : ''
63
+ return name || '新线程'
64
+ }
65
+
66
+ /** The same order the store keeps: active first, open before archived, newest first. */
67
+ export function sortedThreads(threads) {
68
+ return [...(Array.isArray(threads) ? threads : [])].sort((left, right) => {
69
+ if (Boolean(left.active) !== Boolean(right.active)) return left.active ? -1 : 1
70
+ if (isArchivedThread(left) !== isArchivedThread(right)) return isArchivedThread(left) ? 1 : -1
71
+ return (right.updatedAt ?? 0) - (left.updatedAt ?? 0)
72
+ })
73
+ }
74
+
75
+ /** Why the Host refused, in the owner's words. A code the panel does not know is not guessed at. */
76
+ export function threadFailure(error) {
77
+ switch (error?.code) {
78
+ case 'BOT_THREADS_STORAGE_FAILED': return '线程目录无法读取,已停止写入与发送。请修复存储后重启宿主。'
79
+ case 'THREAD_ARCHIVED': return '这个线程已归档,只能只读打开;恢复后才能继续对话。'
80
+ case 'SESSION_UNKNOWN': return '这个线程的会话建立结果未知,请核对后由所有者确认恢复,不要自动重试。'
81
+ case 'SESSION_NOT_OWNED': return '宿主没有确认这个线程会话的归属,暂时不能打开。'
82
+ case 'BOT_GRANT_EMPTY': return '这个 Bot 的授权已被撤销或停用,线程暂不可写入。'
83
+ case 'BOT_NOT_COMMITTED': return '这个 Bot 已不在已提交状态,线程暂时不可用。'
84
+ case 'RUNTIME_MODE_MISMATCH': return '这个 Bot 已切到另一个运行时,集成路径不能为它建立线程会话。'
85
+ case 'THREAD_REQUEST_ABORTED': return '宿主没有在约定时间内回答,本次打开已取消。'
86
+ default: return '操作未得到宿主确认,请重试。'
87
+ }
88
+ }
89
+
90
+ // An IME composition is not a submission: Enter that confirms a candidate must
91
+ // never create a thread or commit a rename.
92
+ const submits = event => event.key === 'Enter' && event.nativeEvent?.isComposing !== true && event.keyCode !== 229
93
+
94
+ let scopeKey = 0
95
+ let createSeq = 0
96
+ export function BotThreadsPanel(props) {
97
+ const key = useMemo(() => ++scopeKey, [props.productApi, props.available, props.botId])
98
+ return <ThreadPanel key={key} {...props} />
99
+ }
100
+
101
+ function ThreadPanel({ productApi, botId, available = true, onOpenSession }) {
102
+ const [view, setView] = useState(null)
103
+ const [status, setStatus] = useState(available ? 'loading' : 'unavailable')
104
+ const [pending, setPending] = useState(false)
105
+ const [feedback, setFeedback] = useState('')
106
+ const [recover, setRecover] = useState(null)
107
+ const [renaming, setRenaming] = useState(null)
108
+ const [draft, setDraft] = useState('')
109
+ const session = useRef({ live: false, epoch: 0, busy: false, requests: new Set() })
110
+ // The poll loop lives in the effect; a write that returns no list asks it to
111
+ // re-read immediately instead of waiting for the next tick.
112
+ const reloadRef = useRef(null)
113
+ // Navigation has its own epoch: only the newest open may move the user, so a
114
+ // late reply, a timeout or a Host that switched this Bot away cannot land the
115
+ // owner on a Session they did not ask for.
116
+ const opening = useRef(0)
117
+ // One create intent, one request key. A reply that never arrives reuses the
118
+ // key, so a retry is answered with the thread the first attempt already made
119
+ // instead of a second one; a new intent mints a new key.
120
+ const createKey = useRef(null)
121
+ const navigate = id => { if (typeof onOpenSession === 'function') onOpenSession(id) }
122
+
123
+ // Each write is retired with its Host: a thread only changes after the store
124
+ // acknowledges it, and a stale or aborted reply never rewrites the list. The
125
+ // calls run in the order the owner made them, so a reply always belongs to
126
+ // the call that is being answered; `epoch` is the one that lets the newest
127
+ // navigation win over an older one that is still in flight.
128
+ const queue = useRef(Promise.resolve())
129
+ const inflight = useRef(null)
130
+ const call = (action, payload, epoch) => {
131
+ const mine = epoch === undefined ? 0 : epoch.current + 1
132
+ if (epoch !== undefined) epoch.current = mine
133
+ // The newest call retires the one before it: a Host that never answers must
134
+ // not hold the queue, and the owner's latest intent is the one that counts.
135
+ const idle = inflight.current === null
136
+ inflight.current?.abort()
137
+ // Nothing in flight: answer at once, so an owner's click is not held behind a
138
+ // turn of the event loop for nothing.
139
+ const run = idle ? step(action, payload, epoch, mine) : queue.current.then(() => step(action, payload, epoch, mine))
140
+ queue.current = run.then(() => undefined, () => undefined)
141
+ return run
142
+ }
143
+ const step = async (action, payload, epoch, mine) => {
144
+ const current = session.current
145
+ if (!current.live || typeof productApi?.dispatch !== 'function') return { abandoned: true }
146
+ current.busy = true
147
+ // A write retires the reads that started before it, so an older list can
148
+ // never land on top of a newer one.
149
+ current.epoch += 1
150
+ const controller = new AbortController()
151
+ inflight.current = controller
152
+ current.requests.add(controller)
153
+ setPending(true)
154
+ let expired = false
155
+ const timeout = setTimeout(() => { expired = true; controller.abort() }, REQUEST_MS)
156
+ try {
157
+ const result = await abortable(() => productApi.dispatch(action, payload, controller.signal), controller.signal)
158
+ if (!current.live || controller.signal.aborted || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
159
+ return { ok: true, result }
160
+ } catch (error) {
161
+ if (!current.live || (epoch !== undefined && epoch.current !== mine)) return { abandoned: true }
162
+ // A deadline the Host missed is reported; a call a newer one retired is not.
163
+ if (expired) return { ok: false, error: Object.assign(new Error('宿主没有在约定时间内回答。'), { code: 'THREAD_REQUEST_ABORTED' }) }
164
+ return controller.signal.aborted ? { abandoned: true } : { ok: false, error }
165
+ } finally {
166
+ clearTimeout(timeout); current.requests.delete(controller); if (inflight.current === controller) inflight.current = null
167
+ current.busy = false
168
+ if (current.live) setPending(false)
169
+ }
170
+ }
171
+ // A write the Host confirmed is applied; one it refused is reported with the
172
+ // reason it gave, and an unknown create outcome offers the owner's explicit
173
+ // recovery instead of a silent retry.
174
+ const write = async (action, payload) => {
175
+ setFeedback(''); setRecover(null)
176
+ const outcome = await call(action, payload)
177
+ if (outcome.ok) { refresh(outcome.result); return outcome.result }
178
+ if (outcome.error !== undefined) {
179
+ setFeedback(threadFailure(outcome.error))
180
+ if (outcome.error.code === 'SESSION_UNKNOWN') setRecover(payload.threadId ?? null)
181
+ }
182
+ return null
183
+ }
184
+
185
+ useEffect(() => {
186
+ if (!available || typeof productApi?.dispatch !== 'function' || !botId) { setStatus('unavailable'); return undefined }
187
+ let live = true, epoch = 0, timer, request
188
+ const currentSession = session.current
189
+ currentSession.live = true
190
+ const schedule = delay => { clearTimeout(timer); if (live && !document.hidden) timer = setTimeout(load, delay) }
191
+ const load = async () => {
192
+ clearTimeout(timer)
193
+ if (!live || document.hidden) return
194
+ if (currentSession.busy) { schedule(VISIBLE_MS); return }
195
+ const current = ++epoch, controller = new AbortController()
196
+ const revision = currentSession.epoch
197
+ request?.abort(); request = controller
198
+ currentSession.requests.add(controller)
199
+ const deadline = setTimeout(() => controller.abort(), REQUEST_MS)
200
+ const valid = () => live && current === epoch && revision === currentSession.epoch && !controller.signal.aborted
201
+ let failed = false
202
+ try {
203
+ const page = await abortable(() => productApi.dispatch(THREAD_ACTIONS.list, { botId }, controller.signal), controller.signal)
204
+ if (!valid()) return
205
+ setView({ threads: sortedThreads(page?.threads), activeThreadId: page?.activeThreadId ?? null })
206
+ setStatus('ready')
207
+ } catch {
208
+ failed = true
209
+ // A transient failure keeps the rows on screen; only a first load that
210
+ // never succeeded reports an error state.
211
+ if (live && current === epoch && revision === currentSession.epoch) setStatus('error')
212
+ } finally {
213
+ clearTimeout(deadline)
214
+ currentSession.requests.delete(controller)
215
+ if (request === controller) request = undefined
216
+ if (live && current === epoch) schedule(failed ? FAILED_MS : VISIBLE_MS)
217
+ }
218
+ }
219
+ const visible = () => { if (document.hidden) { epoch++; clearTimeout(timer); request?.abort(); request = undefined } else void load() }
220
+ document.addEventListener('visibilitychange', visible)
221
+ reloadRef.current = load
222
+ void load()
223
+ return () => { live = false; currentSession.live = false; reloadRef.current = null; epoch++; clearTimeout(timer); for (const controller of currentSession.requests) controller.abort(); document.removeEventListener('visibilitychange', visible) }
224
+ }, [productApi, available, botId])
225
+
226
+ const threads = view?.threads ?? []
227
+ const active = threads.find(thread => thread.active) ?? null
228
+ const openCount = threads.filter(thread => !isArchivedThread(thread)).length
229
+ const archivedCount = threads.length - openCount
230
+ // A write reply carries the new list when the store chose to return one; when
231
+ // it does not, the panel re-reads through the same guarded loop — it never
232
+ // invents a state the Host did not confirm.
233
+ const refresh = result => {
234
+ if (Array.isArray(result?.threads)) {
235
+ setView({ threads: sortedThreads(result.threads), activeThreadId: result.activeThreadId ?? null })
236
+ return
237
+ }
238
+ reloadRef.current?.()
239
+ }
240
+ const createThread = async () => {
241
+ const name = draft.trim().slice(0, MAX_NAME)
242
+ if (createKey.current === null) createKey.current = `panel-${botId}-${++createSeq}`
243
+ const requestKey = createKey.current
244
+ const made = await write(THREAD_ACTIONS.create, { botId, name: name || undefined, requestKey })
245
+ const threadId = made?.thread?.threadId
246
+ // Only a Host-confirmed thread clears the composer and retires the key: an
247
+ // unconfirmed attempt keeps both, so the next one is the same intent and
248
+ // the Host answers it with the thread it already made.
249
+ if (typeof threadId !== 'string') return
250
+ setDraft('')
251
+ createKey.current = null
252
+ // A new thread is opened as itself: the Host makes or confirms its real
253
+ // Session, and only that id is navigated to. No greeting, no message.
254
+ await openThread(threadId, { create: true })
255
+ }
256
+ // Opening is navigation with a confirmed Session. `create` is the explicit
257
+ // owner act that may make one; an archived thread is opened read-only, with
258
+ // the Session the Host kept for it.
259
+ const openThread = async (threadId, { create = false } = {}) => {
260
+ setFeedback(''); setRecover(null)
261
+ const outcome = await call(THREAD_ACTIONS.open, { botId, threadId, ...(create ? { create: true } : {}) }, opening)
262
+ if (outcome.ok) {
263
+ refresh(outcome.result)
264
+ const sessionId = outcome.result?.sessionId ?? null
265
+ if (typeof sessionId === 'string' && sessionId !== '') navigate(sessionId)
266
+ else if (create) setFeedback('宿主没有返回这个线程的会话,暂时没有打开。')
267
+ return outcome.result
268
+ }
269
+ if (outcome.error !== undefined) {
270
+ setFeedback(threadFailure(outcome.error))
271
+ if (outcome.error.code === 'SESSION_UNKNOWN') setRecover(threadId)
272
+ }
273
+ return null
274
+ }
275
+ const switchTo = thread => { if (!thread.active) void openThread(thread.threadId, { create: !isArchivedThread(thread) }) }
276
+ const recoverThread = async threadId => {
277
+ const result = await write(THREAD_ACTIONS.recover, { botId, threadId })
278
+ const sessionId = result?.sessionId ?? null
279
+ if (typeof sessionId === 'string' && sessionId !== '') navigate(sessionId)
280
+ }
281
+ const renameThread = thread => {
282
+ const name = draft.trim().slice(0, MAX_NAME)
283
+ setRenaming(null); setDraft('')
284
+ if (!name) return
285
+ void write(THREAD_ACTIONS.rename, { botId, threadId: thread.threadId, name })
286
+ }
287
+ const archiveThread = thread => void write(THREAD_ACTIONS.archive, { botId, threadId: thread.threadId })
288
+ const unarchiveThread = thread => void write(THREAD_ACTIONS.unarchive, { botId, threadId: thread.threadId })
289
+
290
+ return (
291
+ <section className="bp-threads" aria-label="会话线程">
292
+ <header className="bp-threads-bar">
293
+ <h2 className="bp-threads-title">会话线程</h2>
294
+ <span className="bp-threads-count" aria-label={`${openCount} 条进行中,${archivedCount} 条已归档`}>{openCount} 进行中 · {archivedCount} 归档</span>
295
+ </header>
296
+ {feedback && <p className="bp-threads-note" role="alert">{feedback}</p>}
297
+ {recover && (
298
+ <div className="bp-threads-recover">
299
+ <p className="bp-threads-note">这个线程上一次建立会话的结果未知:可能已经创建但未能记录。核对后由所有者确认恢复,宿主确认后才能继续对话。</p>
300
+ <button type="button" className="bp-threads-button" onClick={() => recoverThread(recover)} disabled={pending}>
301
+ <UiIcon name="refresh" size={16} /><span>核对并恢复这个线程</span>
302
+ </button>
303
+ </div>
304
+ )}
305
+ {status === 'unavailable' && <p className="bp-threads-note">当前连接暂不支持线程目录。宿主接入线程服务后,可在这里为同一个 Bot 建立多条独立会话。</p>}
306
+ {status === 'loading' && <p className="bp-threads-note" role="status">正在读取线程目录…</p>}
307
+ {status === 'error' && <p className="bp-threads-note" role="status">线程目录暂时没有更新,稍后会自动重试。</p>}
308
+ {status === 'ready' && threads.length === 0 && <p className="bp-threads-note">这个 Bot 还没有线程。新建的线程使用 Bot 已保存的模型与权限,互不串上下文。</p>}
309
+ {status === 'ready' && (
310
+ <>
311
+ <div className="bp-threads-new">
312
+ <input className="bp-threads-input" value={draft} maxLength={MAX_NAME} placeholder="新线程名称(可留空)"
313
+ aria-label="新线程名称" onChange={event => setDraft(event.target.value)}
314
+ onKeyDown={event => { if (submits(event)) void createThread() }} />
315
+ <button type="button" className="bp-threads-button" onClick={() => void createThread()} disabled={pending || renaming !== null}>
316
+ <UiIcon name="plus" size={16} /><span>新建线程</span>
317
+ </button>
318
+ </div>
319
+ <ul className="bp-threads-list">
320
+ {threads.map(thread => (
321
+ <li key={thread.threadId} className={`bp-thread${thread.active ? ' is-active' : ''}${isArchivedThread(thread) ? ' is-archived' : ''}`} data-thread={thread.threadId} data-state={isArchivedThread(thread) ? 'archived' : 'open'}>
322
+ {renaming === thread.threadId ? (
323
+ <div className="bp-thread-rename">
324
+ <input className="bp-threads-input" value={draft} maxLength={MAX_NAME} aria-label="线程名称"
325
+ onChange={event => setDraft(event.target.value)}
326
+ onKeyDown={event => { if (submits(event)) renameThread(thread); if (event.key === 'Escape') { setRenaming(null); setDraft('') } }} />
327
+ <button type="button" className="bp-threads-button" onClick={() => renameThread(thread)} disabled={pending}>保存</button>
328
+ <button type="button" className="bp-threads-button is-quiet" onClick={() => { setRenaming(null); setDraft('') }}>取消</button>
329
+ </div>
330
+ ) : (
331
+ <button type="button" className="bp-thread-open" onClick={() => switchTo(thread)} disabled={thread.active} aria-current={thread.active ? 'true' : undefined} aria-busy={pending ? 'true' : undefined}>
332
+ <span className="bp-thread-name">{threadLabel(thread)}</span>
333
+ <span className="bp-thread-meta">
334
+ {thread.active && <span className="bp-thread-badge">当前</span>}
335
+ {isArchivedThread(thread) && <span className="bp-thread-badge is-quiet">已归档 · 只读</span>}
336
+ {thread.sessionState === 'unbound' && <span className="bp-thread-badge is-wait">等待宿主确认归属</span>}
337
+ {thread.sessionState === 'unknown' && <span className="bp-thread-badge is-wait">建立结果未知</span>}
338
+ <span className="bp-thread-time">{formatThreadAt(thread.updatedAt)}</span>
339
+ </span>
340
+ </button>
341
+ )}
342
+ <div className="bp-thread-actions">
343
+ <button type="button" className="bp-threads-icon" onClick={() => { setRenaming(thread.threadId); setDraft(threadLabel(thread)) }} disabled={pending} aria-label="重命名线程"><UiIcon name="settings" size={16} /></button> {isArchivedThread(thread)
344
+ ? <button type="button" className="bp-threads-icon" onClick={() => unarchiveThread(thread)} disabled={pending} aria-label="恢复线程"><UiIcon name="refresh" size={16} /></button>
345
+ : <button type="button" className="bp-threads-icon" onClick={() => archiveThread(thread)} disabled={pending} aria-label="归档线程"><UiIcon name="folder" size={16} /></button>}
346
+ </div>
347
+ </li>
348
+ ))}
349
+ </ul>
350
+ {active?.sessionState === 'unbound' && (
351
+ <p className="bp-threads-note">当前线程的会话尚未得到宿主归属确认,暂时不能继续对话;这不影响新建、切换与归档。</p>
352
+ )}
353
+ {active?.sessionState === 'missing' && (
354
+ <p className="bp-threads-note">当前线程的宿主会话已不存在,下次打开这个线程时会重新建立。</p>
355
+ )}
356
+ {isArchivedThread(active) && (
357
+ <p className="bp-threads-note">已归档的线程可以只读打开,历史记录都还在;恢复之后才重新获得这个 Bot 的写入授权。</p>
358
+ )}
359
+ </>
360
+ )}
361
+ </section>
362
+ )
363
+ }
364
+
365
+ /**
366
+ * The chat-title addon: the active thread's name, with the counts needed to
367
+ * decide whether to open the panel. It owns no state and dispatches nothing.
368
+ */
369
+ export function BotThreadHeaderAddon({ thread, openCount = 0, archivedCount = 0, onOpen }) {
370
+ if (!thread) return null
371
+ return (
372
+ <button type="button" className="bp-threads-head" onClick={onOpen} aria-label={`当前线程 ${threadLabel(thread)},打开线程目录`}>
373
+ <UiIcon name="list" size={14} />
374
+ <span className="bp-threads-head-name">{threadLabel(thread)}</span>
375
+ {openCount + archivedCount > 1 && <span className="bp-threads-head-count">{openCount + archivedCount}</span>}
376
+ </button>
377
+ )
378
+ }
@@ -0,0 +1,169 @@
1
+ /*
2
+ * Thread directory styles. Colour lives in the --bp-thread-* tokens below and
3
+ * follows one appearance: light is the default, the dark block only names what
4
+ * changes. Body text stays on the high-contrast ink; state is carried by a
5
+ * leading rail and badges, so a status never trades readability for hue.
6
+ */
7
+ html body[data-bot-product-ui] .bp-threads {
8
+ --bp-thread-canvas: #f4f5f7;
9
+ --bp-thread-surface: #ffffff;
10
+ --bp-thread-inset: #eef0f3;
11
+ --bp-thread-hover: #e6e9ee;
12
+ --bp-thread-line: #dde1e8;
13
+ --bp-thread-ink: #252a33;
14
+ --bp-thread-muted: #5f6875;
15
+ --bp-thread-accent: #4a6484;
16
+ --bp-thread-accent-ink: #ffffff;
17
+ --bp-thread-focus: #4a6484;
18
+ --bp-thread-ok: #2e7d32;
19
+ --bp-thread-bad: #b3261e;
20
+ --bp-thread-wait: #8a5a12;
21
+ --bp-thread-idle: #5f6875;
22
+ display: flex; flex-direction: column; min-height: 0; gap: 8px;
23
+ color: var(--bp-thread-ink);
24
+ background: var(--bp-thread-canvas);
25
+ }
26
+ html body[data-bot-product-ui][data-bp-appearance="dark"] .bp-threads,
27
+ html body[data-bot-product-ui][data-ds-dark-theme]:not([data-bp-appearance="light"]) .bp-threads {
28
+ --bp-thread-canvas: #131519;
29
+ --bp-thread-surface: #1c1f24;
30
+ --bp-thread-inset: #232730;
31
+ --bp-thread-hover: #2a2f39;
32
+ --bp-thread-line: #333a45;
33
+ --bp-thread-ink: #e8eaee;
34
+ --bp-thread-muted: #aab2bd;
35
+ --bp-thread-accent: #a9c4e0;
36
+ --bp-thread-accent-ink: #131519;
37
+ --bp-thread-focus: #a9c4e0;
38
+ --bp-thread-ok: #7bd48a;
39
+ --bp-thread-bad: #f2b8b5;
40
+ --bp-thread-wait: #f6c76b;
41
+ --bp-thread-idle: #aab2bd;
42
+ }
43
+
44
+ html body[data-bot-product-ui] .bp-threads-bar {
45
+ display: flex; align-items: center; gap: 8px; padding: 12px 16px;
46
+ border-bottom: 1px solid var(--bp-thread-line); background: var(--bp-thread-surface);
47
+ }
48
+ html body[data-bot-product-ui] .bp-threads-title { margin: 0; font-size: 15px; font-weight: 650; }
49
+ html body[data-bot-product-ui] .bp-threads-count { margin-left: auto; font-size: 12px; color: var(--bp-thread-muted); }
50
+ html body[data-bot-product-ui] .bp-threads-note { margin: 0; padding: 12px 16px; color: var(--bp-thread-muted); font-size: 13px; }
51
+
52
+ html body[data-bot-product-ui] .bp-threads-new { display: flex; gap: 8px; padding: 0 16px; }
53
+ html body[data-bot-product-ui] .bp-threads-input {
54
+ flex: 1; min-width: 0; min-height: 36px; padding: 0 10px;
55
+ border: 1px solid var(--bp-thread-line); border-radius: 8px;
56
+ background: var(--bp-thread-inset); color: var(--bp-thread-ink); font: inherit; font-size: 13px;
57
+ }
58
+ html body[data-bot-product-ui] .bp-threads-button {
59
+ display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; flex: none;
60
+ border: 1px solid var(--bp-thread-line); border-radius: 8px;
61
+ background: var(--bp-thread-accent); color: var(--bp-thread-accent-ink); font-size: 13px; cursor: pointer;
62
+ }
63
+ html body[data-bot-product-ui] .bp-threads-button:hover:not(:disabled) { filter: brightness(1.08); }
64
+ html body[data-bot-product-ui] .bp-threads-button:disabled { opacity: 0.5; cursor: default; }
65
+ html body[data-bot-product-ui] .bp-threads-button.is-quiet { background: var(--bp-thread-inset); color: var(--bp-thread-ink); }
66
+
67
+ html body[data-bot-product-ui] .bp-threads-list { list-style: none; margin: 0; padding: 0 8px 8px; display: flex; flex-direction: column; gap: 6px; overflow-y: auto; }
68
+ html body[data-bot-product-ui] .bp-thread {
69
+ position: relative; display: flex; align-items: stretch; gap: 4px;
70
+ background: var(--bp-thread-surface); border: 1px solid var(--bp-thread-line); border-radius: 10px;
71
+ border-left: 3px solid var(--bp-thread-idle);
72
+ }
73
+ html body[data-bot-product-ui] .bp-thread.is-active { border-left-color: var(--bp-thread-accent); }
74
+ html body[data-bot-product-ui] .bp-thread.is-archived { border-left-color: var(--bp-thread-wait); }
75
+ html body[data-bot-product-ui] .bp-thread-open {
76
+ flex: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 4px; padding: 12px; min-height: 40px;
77
+ background: none; border: 0; border-radius: 8px; text-align: left; color: inherit; cursor: pointer; font: inherit;
78
+ }
79
+ html body[data-bot-product-ui] .bp-thread-open:hover:not(:disabled) { background: var(--bp-thread-hover); }
80
+ html body[data-bot-product-ui] .bp-thread-open:disabled { cursor: default; }
81
+ html body[data-bot-product-ui] .bp-thread-name { font-size: 14px; line-height: 1.35; }
82
+ html body[data-bot-product-ui] .bp-thread-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; font-size: 12px; color: var(--bp-thread-muted); }
83
+ html body[data-bot-product-ui] .bp-thread-badge {
84
+ padding: 0 6px; border-radius: 999px; background: var(--bp-thread-accent); color: var(--bp-thread-accent-ink); font-size: 11px; font-weight: 700;
85
+ }
86
+ html body[data-bot-product-ui] .bp-thread-badge.is-quiet { background: var(--bp-thread-inset); color: var(--bp-thread-muted); }
87
+ html body[data-bot-product-ui] .bp-thread-badge.is-wait { background: var(--bp-thread-wait); color: #131519; }
88
+ html body[data-bot-product-ui][data-bp-appearance="dark"] .bp-thread-badge.is-wait,
89
+ html body[data-bot-product-ui][data-ds-dark-theme]:not([data-bp-appearance="light"]) .bp-thread-badge.is-wait { color: #131519; }
90
+
91
+ html body[data-bot-product-ui] .bp-thread-actions { display: flex; align-items: center; flex: none; }
92
+ html body[data-bot-product-ui] .bp-threads-icon {
93
+ display: inline-flex; align-items: center; justify-content: center; width: 40px; min-height: 40px;
94
+ border: 0; border-left: 1px solid var(--bp-thread-line); border-radius: 0 8px 8px 0;
95
+ background: none; color: var(--bp-thread-muted); cursor: pointer;
96
+ }
97
+ html body[data-bot-product-ui] .bp-threads-icon:hover:not(:disabled) { background: var(--bp-thread-hover); color: var(--bp-thread-ink); }
98
+ html body[data-bot-product-ui] .bp-threads-icon:disabled { opacity: 0.5; cursor: default; }
99
+ html body[data-bot-product-ui] .bp-thread-rename { display: flex; flex: 1; flex-wrap: wrap; gap: 8px; padding: 10px 12px; }
100
+
101
+ /* The chat-title addon: one pill, no colour of its own. */
102
+ html body[data-bot-product-ui] .bp-threads-head {
103
+ display: inline-flex; align-items: center; gap: 6px; min-height: 28px; max-width: 220px; padding: 0 10px;
104
+ border: 1px solid var(--bp-thread-line); border-radius: 999px;
105
+ background: var(--bp-thread-inset); color: var(--bp-thread-ink); font-size: 12px; cursor: pointer;
106
+ }
107
+ html body[data-bot-product-ui] .bp-threads-head:hover { background: var(--bp-thread-hover); }
108
+ html body[data-bot-product-ui] .bp-threads-head-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
109
+ html body[data-bot-product-ui] .bp-threads-head-count {
110
+ min-width: 18px; padding: 0 5px; border-radius: 999px;
111
+ background: var(--bp-thread-accent); color: var(--bp-thread-accent-ink); font-size: 11px; font-weight: 700; text-align: center;
112
+ }
113
+
114
+ /* The owner's explicit recovery for a create whose outcome is unknown. It is
115
+ the only way forward, so it sits with the note that explains it. */
116
+ html body[data-bot-product-ui] .bp-threads-recover {
117
+ display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin: 0 16px; padding: 10px 12px;
118
+ border: 1px solid var(--bp-thread-wait); border-radius: 10px; background: var(--bp-thread-surface);
119
+ }
120
+
121
+ /* The chat-header control. It sits beside the product's own conversation tools
122
+ as one compact chip, so it never becomes a second toolbar. */
123
+ html body[data-bot-product-ui] .bp-threads-head-wrap { position: relative; display: inline-flex; flex: none; }
124
+ /* The menu is placed against the control's own box by the component, so the
125
+ position here is only the fallback before the first measurement. */
126
+ html body[data-bot-product-ui] .bp-threads-menu {
127
+ z-index: 40; width: min(300px, calc(100vw - 24px));
128
+ display: flex; flex-direction: column; gap: 8px; padding: 10px;
129
+ border: 1px solid var(--bp-thread-line); border-radius: 12px;
130
+ background: var(--bp-thread-surface); color: var(--bp-thread-ink);
131
+ box-shadow: 0 12px 32px -12px rgba(15, 23, 42, .38);
132
+ }html body[data-bot-product-ui] .bp-threads-menu-title { margin: 0; font-size: 12px; font-weight: 650; color: var(--bp-thread-muted); }
133
+ html body[data-bot-product-ui] .bp-threads-menu-list { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0; list-style: none; max-height: 264px; overflow-y: auto; }
134
+ html body[data-bot-product-ui] .bp-threads-menu-row {
135
+ display: flex; flex-direction: column; gap: 2px; width: 100%; min-height: 36px; padding: 6px 8px; text-align: left;
136
+ border: 0; border-radius: 8px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer;
137
+ }
138
+ html body[data-bot-product-ui] .bp-threads-menu-row:hover { background: var(--bp-thread-hover); }
139
+ html body[data-bot-product-ui] .bp-threads-menu-row.is-active { background: var(--bp-thread-inset); }
140
+ html body[data-bot-product-ui] .bp-threads-menu-row[disabled] { cursor: default; opacity: .65; }
141
+ html body[data-bot-product-ui] .bp-threads-menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
142
+ html body[data-bot-product-ui] .bp-threads-menu-meta { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--bp-thread-muted); }
143
+ html body[data-bot-product-ui] .bp-threads-menu-new { display: flex; gap: 6px; }
144
+ html body[data-bot-product-ui] .bp-threads-menu-new .bp-threads-input { min-height: 32px; font-size: 12px; }
145
+ html body[data-bot-product-ui] .bp-threads-menu-new .bp-threads-button { min-height: 32px; padding: 0 8px; font-size: 12px; }
146
+ html body[data-bot-product-ui] .bp-threads-menu-more {
147
+ padding: 4px 0; border: 0; background: none; color: var(--bp-thread-accent);
148
+ font: inherit; font-size: 12px; text-align: left; cursor: pointer;
149
+ }
150
+
151
+ html body[data-bot-product-ui] .bp-threads :is(button, input):focus-visible {
152
+ outline: 2px solid var(--bp-thread-focus); outline-offset: 1px;
153
+ }
154
+
155
+ /* A phone keeps every row full-width and comfortable at 375px. */
156
+ @media (max-width: 640px) {
157
+ html body[data-bot-product-ui] .bp-threads-bar { padding: 10px 12px; }
158
+ html body[data-bot-product-ui] .bp-threads-new { padding: 0 12px; }
159
+ html body[data-bot-product-ui] .bp-thread-open { padding: 10px; }
160
+ html body[data-bot-product-ui] .bp-threads-button span { display: none; }
161
+ html body[data-bot-product-ui] .bp-threads-head { max-width: 140px; }
162
+ }
163
+
164
+ /* Touch: every control reaches a 40px target and nothing depends on hover. */
165
+ @media (pointer: coarse) {
166
+ html body[data-bot-product-ui] .bp-threads-button { min-width: 40px; min-height: 40px; }
167
+ html body[data-bot-product-ui] .bp-threads-icon { width: 44px; }
168
+ html body[data-bot-product-ui] .bp-threads-head { min-height: 40px; }
169
+ }