@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,371 @@
1
+ import { createElement as h, Fragment, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { currentPart } from './text.js'
4
+ import { CheckIcon, SlidersIcon } from './icons.js'
5
+ import { BotMark } from './mark.js'
6
+ import { KeyIcon, MaskedInput } from './secret-card.js'
7
+ import { SPRING_POP, SPRING_SHAPE, spring, useLinger, useViewport, viewBottom } from './motion.js'
8
+ import { hostText, t } from './i18n.js'
9
+
10
+ const MENU_EXIT_MS = 120
11
+
12
+ // -------------------------------------------------------------------------
13
+ // A Bot's model: the picker in its details panel and settings, and the message its
14
+ // chat opens on while it has none. Models come from DeepSeek Harness, grouped by provider.
15
+
16
+ export function groupModels(models) {
17
+ const groups = new Map()
18
+ for (const model of models ?? []) {
19
+ const provider = model.provider ?? model.providerName ?? model.ref.split('/')[0]
20
+ if (!groups.has(provider)) groups.set(provider, [])
21
+ groups.get(provider).push(model)
22
+ }
23
+ const names = new Map()
24
+ for (const [provider, list] of groups) {
25
+ const name = list[0].providerName ?? provider
26
+ names.set(name, (names.get(name) ?? 0) + 1)
27
+ }
28
+ return [...groups].map(([provider, list]) => {
29
+ const name = list[0].providerName ?? provider
30
+ return [names.get(name) > 1 ? `${name} (${provider})` : name, list]
31
+ })
32
+ }
33
+
34
+ const servedOf = (roster, ref) => (roster.models ?? []).find(model => model.ref === ref)
35
+
36
+ // What a Bot runs on, for display: the chosen model, else the one it last ran on.
37
+ export function modelOf(bot, roster) {
38
+ const ref = bot.model ?? bot.appliedModel
39
+ if (!ref) return undefined
40
+ const served = servedOf(roster, ref)
41
+ const [provider, ...rest] = ref.split('/')
42
+ return { ref, name: served?.name ?? rest.join('/'), provider: served?.providerName ?? provider, served: served !== undefined }
43
+ }
44
+
45
+ // The thinking levels of one model, `{efforts, defaultEffort?}` or undefined when it
46
+ // has none. The catalog carries them for models Bots use; any other ref asks the host.
47
+ const effortCache = new Map()
48
+ function useEfforts(served, ref, actions) {
49
+ const [fetched, setFetched] = useState(undefined)
50
+ useEffect(() => {
51
+ setFetched(undefined)
52
+ if (ref === undefined || served?.reasoning !== undefined || actions?.efforts === undefined) return undefined
53
+ let live = true
54
+ if (effortCache.has(ref)) setFetched(effortCache.get(ref))
55
+ else void Promise.resolve(actions.efforts(ref)).then((value) => {
56
+ const reasoning = value?.efforts ? value : null
57
+ effortCache.set(ref, reasoning)
58
+ if (live) setFetched(reasoning)
59
+ }).catch(() => {})
60
+ return () => { live = false }
61
+ }, [ref, served?.reasoning !== undefined, actions])
62
+ return served?.reasoning ?? fetched
63
+ }
64
+
65
+ // The rows of the Thinking section: a Default row that clears the Bot's pick, then every
66
+ // level the model serves.
67
+ export function effortRows(reasoning) {
68
+ if (reasoning?.efforts === undefined || reasoning.efforts.length === 0) return []
69
+ return [
70
+ { effort: undefined, label: t('Default') },
71
+ ...reasoning.efforts.map(effort => ({ effort: effort.id, label: effort.name })),
72
+ ]
73
+ }
74
+
75
+ function EffortRows({ reasoning, current, onPick }) {
76
+ return h(Fragment, null,
77
+ h('div', { className: 'bt-menu-sep', role: 'separator' }),
78
+ h('div', { className: 'bt-model-group', role: 'presentation' }, t('Thinking level')),
79
+ effortRows(reasoning).map(row => h('button', {
80
+ key: row.effort ?? 'default', type: 'button', role: 'menuitemradio', 'aria-checked': (row.effort ?? null) === (current ?? null),
81
+ onClick: () => onPick(row.effort),
82
+ },
83
+ h('span', { className: 'bt-menu-label' }, row.label),
84
+ h('span', { className: 'bt-model-tick', 'aria-hidden': true }, (row.effort ?? null) === (current ?? null) ? h(CheckIcon) : null))))
85
+ }
86
+
87
+ function ModelRows({ roster, current, onPick }) {
88
+ return groupModels(roster.models).map(([provider, list]) => h(Fragment, { key: list[0].provider ?? provider },
89
+ h('div', { className: 'bt-model-group', role: 'presentation' }, provider),
90
+ list.map(model => h('button', {
91
+ key: model.ref, type: 'button', role: 'menuitemradio', 'aria-checked': model.ref === current, title: model.ref,
92
+ onClick: () => onPick(model.ref),
93
+ },
94
+ h('span', { className: 'bt-menu-label' }, model.name),
95
+ model.ref === roster.defaultModel ? h('span', { className: 'bt-menu-hint' }, t('Default')) : null,
96
+ h('span', { className: 'bt-model-tick', 'aria-hidden': true }, model.ref === current ? h(CheckIcon) : null)))))
97
+ }
98
+
99
+ export const Caret = () => h('svg', { className: 'bt-model-caret', width: 12, height: 12, viewBox: '0 0 20 20', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, h('path', { d: 'M5.5 8l4.5 4.5L14.5 8' }))
100
+
101
+ // A menu that opens under its trigger, or above when the window has no room below. A
102
+ // click outside, Escape, or a scroll outside it closes it.
103
+ export function useAnchoredMenu() {
104
+ const [anchor, setAnchor] = useState(null)
105
+ const [place, setPlace] = useState(null)
106
+ const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS)
107
+ const button = useRef(null)
108
+ const menu = useRef(null)
109
+ const tick = useViewport()
110
+ const close = () => { setAnchor(null); setPlace(null) }
111
+ useEffect(() => {
112
+ if (place === null) return undefined
113
+ const onDown = (event) => {
114
+ if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return
115
+ close()
116
+ }
117
+ const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); close(); button.current?.focus() } }
118
+ const onScroll = (event) => { if (!menu.current?.contains(event.target)) close() }
119
+ window.addEventListener('mousedown', onDown)
120
+ window.addEventListener('keydown', onKey, true)
121
+ window.addEventListener('scroll', onScroll, true)
122
+ return () => {
123
+ window.removeEventListener('mousedown', onDown)
124
+ window.removeEventListener('keydown', onKey, true)
125
+ window.removeEventListener('scroll', onScroll, true)
126
+ }
127
+ }, [place])
128
+ const open = () => setAnchor({})
129
+ // Re-reads the trigger's rect whenever the visible viewport moves: the menu
130
+ // tracks its anchor through the on-screen keyboard, not past it.
131
+ useLayoutEffect(() => {
132
+ if (anchor === null || !button.current) return
133
+ const rect = button.current.getBoundingClientRect()
134
+ const width = Math.max(240, rect.width)
135
+ const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8))
136
+ const below = viewBottom() - rect.bottom
137
+ setPlace(below >= 260 || below >= rect.top
138
+ ? { left, width, top: rect.bottom + 4, maxHeight: below - 16, '--bt-origin': 'top right' }
139
+ : { left, width, bottom: viewBottom() - rect.top + 4, maxHeight: rect.top - 16, '--bt-origin': 'bottom right' })
140
+ }, [anchor, tick])
141
+ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }
142
+ }
143
+
144
+ // The trigger shows the model name over its provider. A Thinking section lists the
145
+ // levels of the shown model when it has them.
146
+ // `onPick`/`onPickEffort` replace saving to the Bot, for a Bot that does not exist yet.
147
+ // `withEfforts: false` keeps the Thinking section out, for a pick that has no level of its own.
148
+ export function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort, withEfforts = true }) {
149
+ const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu()
150
+ const shown = modelOf(bot, roster)
151
+ const current = bot.model ?? bot.appliedModel
152
+ const served = servedOf(roster, current)
153
+ const reasoning = useEfforts(served, withEfforts ? current : undefined, actions)
154
+ // The level the Bot runs on: its pick when the model serves it, else the model's own.
155
+ const picked = reasoning?.efforts?.some(effort => effort.id === bot.effort) ? bot.effort : undefined
156
+ const currentEffort = picked ?? reasoning?.defaultEffort
157
+ const effortLabel = currentEffort === undefined
158
+ ? (reasoning?.efforts?.length > 0 ? t('Default') : undefined)
159
+ : reasoning.efforts.find(effort => effort.id === currentEffort)?.name ?? currentEffort
160
+ const toggle = () => {
161
+ if (isOpen) { close(); return }
162
+ // DSH settings can change the list at any time, so opening asks again.
163
+ void actions?.refreshModels?.()
164
+ open()
165
+ }
166
+ const pick = (ref) => {
167
+ close()
168
+ if (onPick) onPick(ref)
169
+ else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref }))
170
+ }
171
+ const pickEffort = (effort) => {
172
+ close()
173
+ if (onPickEffort) onPickEffort(effort)
174
+ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null }))
175
+ }
176
+ const models = roster.models ?? []
177
+ const manage = manageModels(actions)
178
+ return h(Fragment, null,
179
+ h('button', {
180
+ ref: button, type: 'button', className: 'bt-model-trigger', 'data-wide': wide || undefined,
181
+ 'aria-haspopup': 'menu', 'aria-expanded': isOpen, 'aria-label': t('Model: {model}', { model: shown?.name ?? t('none') }),
182
+ 'data-missing': shown !== undefined && !shown.served && models.length > 0 ? '' : undefined, onClick: toggle,
183
+ },
184
+ h('span', { className: 'bt-model-copy' },
185
+ h('span', { className: 'bt-model-name' }, shown?.name ?? t('Choose a model')),
186
+ shown ? h('span', { className: 'bt-model-provider' }, shown.served || models.length === 0
187
+ ? (!withEfforts || effortLabel === undefined ? shown.provider : `${shown.provider} · ${effortLabel}`)
188
+ : t('{provider} · not available', { provider: shown.provider })) : null),
189
+ h(Caret)),
190
+ shownPlace ? createPortal(h('div', { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== undefined ? ' bt-menu-up' : ''}`, role: 'menu', 'aria-label': t('Model for {name}', { name: bot.name }), style: shownPlace, 'data-leaving': leaving || undefined },
191
+ models.length > 0 ? h(ModelRows, { roster, current, onPick: pick })
192
+ : h('div', { className: 'bt-model-none' }, t('No models are set up in DeepSeek Harness yet.')),
193
+ withEfforts && effortRows(reasoning).length > 0 ? h(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null,
194
+ manage ? h('div', { className: 'bt-menu-sep', role: 'separator' }) : null,
195
+ manage ? h('button', { type: 'button', role: 'menuitem', onClick: () => { close(); manage() } }, h(SlidersIcon), h('span', { className: 'bt-menu-label' }, t('Models and providers…'))) : null), document.body) : null)
196
+ }
197
+
198
+ // The card a Bot's chat opens on while the Bot has no model, or has one that is no
199
+ // longer served. The first Main Bot greets the user only after a pick.
200
+ export function ModelChoiceDock({ sessionId, useRoster, actions }) {
201
+ const roster = useRoster(value => value)
202
+ const bot = roster.byId[sessionId]
203
+ const [busy, setBusy] = useState('')
204
+ const [error, setError] = useState('')
205
+ const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready
206
+ useEffect(() => { if (shown) void actions.refreshModels?.() }, [shown, sessionId])
207
+ if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null
208
+ const models = roster.models ?? []
209
+ // A model whose provider has no API key fails every turn, so it is no choice.
210
+ const usable = models.filter(model => !model.needsKey)
211
+ const lost = bot.model ? !models.some(model => model.ref === bot.model) : false
212
+ // An unknown catalog (none listed) cannot tell a lost model from a slow provider.
213
+ if (bot.model && (!lost || models.length === 0)) return null
214
+ // The missing key is taken here, through the host.
215
+ const keyFor = usable.length === 0 && actions.setProviderKey ? models.find(model => model.needsKey) : undefined
216
+ const manage = manageModels(actions)
217
+ const pick = (ref) => {
218
+ setBusy(ref)
219
+ setError('')
220
+ void Promise.resolve(actions.updateBot(bot.id, { model: ref }))
221
+ .catch((failure) => { setError(failure?.message ?? String(failure)); void actions.refreshModels?.() })
222
+ .finally(() => setBusy(''))
223
+ }
224
+ // The pick reads as the Bot's own first message, with the choices as replies under it.
225
+ const text = lost
226
+ ? t('{model} is not available anymore. Pick another model for me.', { model: bot.model.split('/').slice(1).join('/') })
227
+ : keyFor
228
+ ? t("Hi, I'm {name}. I need a {provider} API key before I can start.", { name: bot.name, provider: keyFor.providerName ?? keyFor.provider })
229
+ : usable.length === 0
230
+ ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name })
231
+ : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name })
232
+ const choices = usable.length === 0
233
+ ? setupChoices(actions).map(([label, run]) => h('button', { key: label, type: 'button', className: 'bt-model-chip', onClick: run }, label))
234
+ : [
235
+ ...groupModels(usable).map(([provider, list]) => h('div', { key: list[0].provider ?? provider, className: 'bt-model-choice-group', role: 'group', 'aria-label': provider },
236
+ h('div', { className: 'bt-model-choice-provider', 'aria-hidden': true }, provider),
237
+ h('div', { className: 'bt-model-choice-chips' }, list.map(model => h('button', {
238
+ key: model.ref, type: 'button', className: 'bt-model-chip', disabled: busy !== '', 'aria-busy': busy === model.ref || undefined,
239
+ 'aria-label': `${model.name} · ${provider}`,
240
+ title: model.ref === roster.defaultModel ? `${model.ref} · ${t('Harness default')}` : model.ref,
241
+ onClick: () => pick(model.ref),
242
+ }, model.name, busy === model.ref ? h('span', { className: 'bt-model-spin', 'aria-label': t('Saving…') }) : null))))),
243
+ manage ? h('button', { key: 'manage', type: 'button', className: 'bt-model-chip bt-model-chip-quiet', onClick: manage }, h(SlidersIcon), t('Manage models')) : null,
244
+ ]
245
+ return h('div', { className: 'bt-model-dock' },
246
+ h('div', { className: 'bt-astack', 'data-lead': '' },
247
+ h('div', { className: 'bt-msg' },
248
+ h('div', { className: 'bt-msg-line' },
249
+ h(BotMark, { bot, size: 22 }),
250
+ h('div', { className: 'bt-bubble' }, text)))),
251
+ keyFor
252
+ ? h(ModelKeyCard, { key: keyFor.provider, model: keyFor, actions })
253
+ : h('div', { className: 'bt-model-chips', role: 'group', 'aria-label': t('Choose a model') }, choices),
254
+ error ? h('div', { className: 'bt-secret-error bt-model-error', role: 'alert' }, error) : null)
255
+ }
256
+
257
+ const DEEPSEEK_KEYS_URL = 'https://platform.deepseek.com/api_keys'
258
+
259
+ // The provider's API key, saved through the host; once it is set, the provider's
260
+ // models become the choices. `onSaved` runs once the host took the key.
261
+ export function ModelKeyCard({ model, actions, onSaved }) {
262
+ const [value, setValue] = useState('')
263
+ const [busy, setBusy] = useState(false)
264
+ const [error, setError] = useState('')
265
+ const label = t('{provider} API key', { provider: model.providerName ?? model.provider })
266
+ const ready = value.trim() !== '' && !busy
267
+ const save = () => {
268
+ if (!ready) return
269
+ setBusy(true)
270
+ setError('')
271
+ void Promise.resolve(actions.setProviderKey(model.provider, value))
272
+ .then(() => { setValue(''); onSaved?.() }, failure => setError(hostText(failure?.message ?? String(failure))))
273
+ .finally(() => setBusy(false))
274
+ }
275
+ const official = model.provider === 'deepseek-official'
276
+ return h('div', { className: 'bt-q bt-model-key', role: 'group', 'aria-label': label },
277
+ h('div', { className: 'bt-model-key-title' }, h(KeyIcon), label),
278
+ h('label', { className: 'bt-q-field bt-secret-field' }, h(MaskedInput, { value, onChange: setValue, onEnter: save, label, autoFocus: true })),
279
+ official ? h('div', { className: 'bt-model-key-help' },
280
+ h('a', { className: 'bt-model-key-link', href: DEEPSEEK_KEYS_URL, target: '_blank', rel: 'noopener noreferrer' }, t('Create a key on the DeepSeek platform ↗')),
281
+ h('span', { className: 'bt-model-key-note' }, t('The API is billed by usage; the account needs a balance first.'))) : null,
282
+ error ? h('div', { className: 'bt-secret-error', role: 'alert' }, error) : null,
283
+ h('div', { className: 'bt-q-foot' },
284
+ actions.openModels ? h('button', { type: 'button', className: 'bt-q-btn bt-model-key-other', onClick: actions.openModels }, t('Another provider')) : null,
285
+ h('button', { type: 'button', className: 'bt-q-submit', disabled: !ready, onClick: save }, busy ? t('Saving…') : t('Save'))))
286
+ }
287
+
288
+ // Where the providers are set up: DS Bot's Models page, else DSH's own.
289
+ export const manageModels = actions => actions.openModels ?? actions.openHarnessModels ?? actions.openHarnessSettings
290
+
291
+ // Ways to get a first model. The DeepSeek sign-in exists only where the shell carries
292
+ // its account plugin; the API key uses the shell's own editor.
293
+ function setupChoices(actions) {
294
+ const custom = manageModels(actions)
295
+ return [
296
+ actions.canSignIn?.() ? [t('Sign in to DeepSeek'), () => actions.signIn()] : null,
297
+ actions.openHarnessOnboarding ? [t('Enter a DeepSeek API key'), () => actions.openHarnessOnboarding('deepseek-official')] : null,
298
+ custom ? [t('Open settings to set up a custom model'), () => custom()] : null,
299
+ ].filter(Boolean)
300
+ }
301
+
302
+ // The shell's own DeepSeek sign-in dialog, borrowed from its account plugin: the
303
+ // plugin's model sign-in entry renders here with the plugin's own operations.
304
+ export function DeepSeekSignIn({ useUi, actions }) {
305
+ const open = useUi(state => state.signIn)
306
+ const entry = open ? actions.signInEntry?.() : null
307
+ if (!entry) return null
308
+ return h(SignInHost, { key: open, entry, actions })
309
+ }
310
+
311
+ function SignInHost({ entry, actions }) {
312
+ const [ops] = useState(() => entry.inject?.() ?? {})
313
+ const close = () => actions.closeSignIn()
314
+ const useHook = source => selector => useSyncExternalStore(source.subscribe, () => selector(source.getSnapshot()))
315
+ const [hooks] = useState(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) }))
316
+ return h(entry.component, {
317
+ ...ops,
318
+ ...hooks,
319
+ t: actions.accountT(),
320
+ complete: close,
321
+ useApiKey: () => { close(); actions.openHarnessOnboarding('deepseek-official') },
322
+ })
323
+ }
324
+
325
+ export const MODEL_CSS = `
326
+ .bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease}
327
+ .bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)}
328
+ .bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
329
+ .bt-model-trigger[data-missing] .bt-model-provider{color:var(--bt-danger)}
330
+ .bt-model-trigger[data-wide]{display:flex;width:100%;min-height:48px;padding:7px 12px;justify-content:space-between;border-radius:12px;background:var(--bt-card);box-shadow:inset 0 0 0 1px var(--bt-line)}
331
+ .bt-model-trigger[data-wide]:hover,.bt-model-trigger[data-wide][aria-expanded=true]{background:var(--bt-hover)}
332
+ .bt-model-copy{display:flex;flex-direction:column;min-width:0}
333
+ .bt-model-name{font-size:13px;line-height:18px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
334
+ .bt-model-provider{font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
335
+ .bt-model-caret{flex:none;color:var(--bt-ink-3);transition:${spring(SPRING_SHAPE, 'transform')}}
336
+ .bt-model-trigger[aria-expanded=true] .bt-model-caret{transform:rotate(180deg)}
337
+ .bt-model-menu{overflow-y:auto;scrollbar-width:thin}
338
+ .bt-model-group{flex:none;padding:8px 8px 2px;font-size:11px;line-height:14px;font-weight:500;color:var(--bt-ink-3);text-transform:none}
339
+ .bt-model-group:first-child{padding-top:2px}
340
+ .bt-model-tick{flex:none;width:16px;height:16px;display:inline-flex;color:var(--bt-ink-2)}
341
+ .bt-model-tick svg{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 60ms backwards}
342
+ .bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)}
343
+ /* DSH centers the message column inside its scroll padding, which can sit a few px off this dock's center; the side shadows keep the band over the column's edges. */
344
+ .bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;background:var(--bt-main);box-shadow:-12px 0 0 var(--bt-main),12px 0 0 var(--bt-main);animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards}
345
+ /* The scroll box clips at its padding edge, so 3px of padding keeps the chips' focus rings visible; the negative margins cancel it so the chips stay in place. */
346
+ .bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;box-sizing:border-box;padding:3px;margin:-3px 0 -3px 27px;max-width:min(566px,calc(100% - 27px));max-height:min(246px,calc(36vh + 6px));max-height:min(246px,calc(36dvh + 6px));overflow-y:auto;scrollbar-width:thin}
347
+ .bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0}
348
+ .bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere}
349
+ .bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px}
350
+ .bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease}
351
+ .bt-model-chip:hover:not(:disabled){background:var(--bt-hover)}
352
+ .bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px}
353
+ .bt-model-chip-quiet{color:var(--bt-ink-2)}
354
+ .bt-model-chip svg{width:14px;height:14px}
355
+ .bt-model-chip:disabled{cursor:default}
356
+ .bt-model-chip:disabled:not([aria-busy]){opacity:.5}
357
+ .bt-model-error{margin-left:30px}
358
+ .bt-q.bt-model-key{margin:0 0 0 30px;width:min(480px,calc(100% - 30px));gap:10px}
359
+ .bt-model-key-title{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;font-weight:500;color:var(--bt-ink-2)}
360
+ .bt-model-key-title svg{flex:none;width:16px;height:16px}
361
+ .bt-model-key .bt-q-foot{margin-top:2px}
362
+ .bt-model-key-help{display:flex;flex-direction:column;gap:2px;font-size:12px;line-height:17px;color:var(--bt-ink-2)}
363
+ .bt-model-key-link{align-self:flex-start;color:var(--bt-ink);text-decoration:underline;text-decoration-color:var(--bt-line-2);text-underline-offset:2px}
364
+ .bt-model-key-link:hover{text-decoration-color:currentColor}
365
+ .bt-model-key-link:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px;border-radius:4px}
366
+ .bt-model-key-other{margin-right:auto}
367
+ @keyframes bt-model-in{from{opacity:0;transform:translateY(6px)}}
368
+ .bt-model-spin{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--bt-line-2);border-top-color:var(--bt-ink-2);animation:bt-model-spin .7s linear infinite}
369
+ @keyframes bt-model-spin{to{transform:rotate(360deg)}}
370
+ @media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}}
371
+ `