@ljwei-stak/dsh-model-route-bot 0.66.4 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/bot-product.mjs +81 -5
- package/.dsh-plugin/client.js +17490 -1791
- package/README.i18n.yaml +4 -4
- package/README.md +409 -364
- package/README.zh.md +409 -364
- package/apps/integration-client/README.md +31 -7
- package/apps/integration-client/api.mjs +14 -3
- package/apps/integration-client/binding-editor.jsx +308 -109
- package/apps/integration-client/binding-model.mjs +262 -0
- package/apps/integration-client/binding.css +144 -0
- package/apps/integration-client/bot-model-status.mjs +18 -0
- package/apps/integration-client/bot-profile-identity.jsx +197 -0
- package/apps/integration-client/bot-profile.css +57 -0
- package/apps/integration-client/bot-profile.mjs +97 -0
- package/apps/integration-client/bot-settings-dialog.css +18 -0
- package/apps/integration-client/bot-settings-dialog.jsx +111 -0
- package/apps/integration-client/bot-thread-header-bridge.css +20 -0
- package/apps/integration-client/bot-thread-header.jsx +278 -0
- package/apps/integration-client/bot-threads-panel.jsx +378 -0
- package/apps/integration-client/bot-threads.css +169 -0
- package/apps/integration-client/chat-design.mjs +250 -0
- package/apps/integration-client/chat-ui.jsx +175 -0
- package/apps/integration-client/chat.css +477 -0
- package/apps/integration-client/composer-draft-binding.mjs +50 -0
- package/apps/integration-client/composer-draft-bound.jsx +14 -0
- package/apps/integration-client/composer-draft-host.jsx +142 -0
- package/apps/integration-client/composer-draft-session.jsx +287 -0
- package/apps/integration-client/composer-draft-witness.mjs +115 -0
- package/apps/integration-client/composer-draft.css +4 -0
- package/apps/integration-client/composer-draft.mjs +322 -0
- package/apps/integration-client/contact-attention.jsx +54 -0
- package/apps/integration-client/data-panel.jsx +439 -0
- package/apps/integration-client/data.css +69 -0
- package/apps/integration-client/desktop-bridge.mjs +1 -0
- package/apps/integration-client/desktop-http-socket.mjs +49 -0
- package/apps/integration-client/desktop-keyboard.mjs +32 -0
- package/apps/integration-client/desktop-pane.jsx +99 -30
- package/apps/integration-client/desktop-stream.mjs +13 -3
- package/apps/integration-client/desktop-tools.jsx +19 -0
- package/apps/integration-client/desktop-touch.mjs +27 -0
- package/apps/integration-client/desktop.css +41 -0
- package/apps/integration-client/independent-chat-controller.mjs +224 -0
- package/apps/integration-client/independent-chat-entry.jsx +61 -0
- package/apps/integration-client/independent-chat-navigation.mjs +21 -0
- package/apps/integration-client/independent-chat-page.jsx +148 -0
- package/apps/integration-client/independent-chat-rich.jsx +64 -0
- package/apps/integration-client/independent-chat-rich.mjs +30 -0
- package/apps/integration-client/independent-chat.css +73 -0
- package/apps/integration-client/index.jsx +24 -16
- package/apps/integration-client/library-panel.jsx +4 -1
- package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
- package/apps/integration-client/main-route-editor.jsx +94 -0
- package/apps/integration-client/main-route-model.mjs +100 -0
- package/apps/integration-client/model-picker.css +37 -0
- package/apps/integration-client/model-picker.jsx +83 -0
- package/apps/integration-client/native-activity.css +141 -0
- package/apps/integration-client/native-activity.jsx +276 -0
- package/apps/integration-client/native-activity.mjs +384 -0
- package/apps/integration-client/notifications-panel.jsx +186 -0
- package/apps/integration-client/notifications.css +126 -0
- package/apps/integration-client/pairing-qr.jsx +79 -0
- package/apps/integration-client/panels.jsx +126 -115
- package/apps/integration-client/product-slots.jsx +46 -0
- package/apps/integration-client/routine-recorder.mjs +23 -2
- package/apps/integration-client/secure-pairing.jsx +84 -0
- package/apps/integration-client/tasks-panel.jsx +32 -10
- package/apps/integration-client/ui-icons.jsx +17 -0
- package/apps/integration-client/usage-explorer.jsx +43 -0
- package/apps/integration-client/usage-report.mjs +71 -0
- package/apps/integration-client/usage.css +34 -0
- package/apps/integration-client/web-panel-model.mjs +140 -0
- package/apps/integration-client/web-panel-page.jsx +221 -0
- package/apps/integration-client/web-panel-pins-live.jsx +28 -0
- package/apps/integration-client/web-panel.css +139 -0
- package/apps/integration-client/workspace-model.mjs +67 -0
- package/apps/integration-client/workspace-navigation.mjs +56 -0
- package/apps/integration-client/workspace-ui.jsx +173 -0
- package/apps/integration-client/workspace.css +332 -0
- package/modules/bot-runtime/SOURCE.json +176 -50
- package/modules/bot-runtime/index.mjs +169 -13
- package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
- package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
- package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
- package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
- package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
- package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
- package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
- package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
- package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
- package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
- package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
- package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
- package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
- package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
- package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
- package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
- package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
- package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
- package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
- package/modules/bot-runtime/read-model.mjs +19 -4
- package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
- package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
- package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
- package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
- package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
- package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
- package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
- package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
- package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
- package/modules/independent-runtime/index.mjs +152 -62
- package/modules/independent-runtime/remote-tools.mjs +8 -5
- package/modules/openmaus-protocol/NOTICE +10 -0
- package/modules/openmaus-protocol/README.md +15 -0
- package/modules/openmaus-protocol/SOURCE.json +68 -0
- package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
- package/modules/openmaus-protocol/index.mjs +1 -0
- package/modules/openmaus-protocol/transport.mjs +77 -0
- package/modules/openmaus-protocol/vendor/LICENSE +202 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
- package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
- package/package.json +18 -6
- package/packages/bot-acp/README.md +156 -0
- package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-acp/licenses/NOTICE.md +27 -0
- package/packages/bot-acp/src/acp-adapter.mjs +795 -0
- package/packages/bot-acp/src/acp-host.mjs +148 -0
- package/packages/bot-acp/src/native-launch.mjs +121 -0
- package/packages/bot-acp/src/permission-policy.mjs +283 -0
- package/packages/bot-acp/src/rpc-codec.mjs +265 -0
- package/packages/bot-acp/src/session-core.mjs +581 -0
- package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
- package/packages/bot-engines/README.md +281 -0
- package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
- package/packages/bot-engines/licenses/NOTICE.md +32 -0
- package/packages/bot-engines/src/claude-engine.mjs +772 -0
- package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
- package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
- package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
- package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
- package/packages/bot-engines/src/claude-protocol.mjs +177 -0
- package/packages/bot-engines/src/driver-catalog.mjs +334 -0
- package/packages/bot-engines/src/host-loop-request.mjs +95 -0
- package/packages/bot-engines/src/host-registrations.mjs +105 -0
- package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
- package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
- package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
- package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
- package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
- package/packages/cli-runtime/src/native-peer.ps1 +51 -3
- package/packages/cli-runtime/src/native-process.mjs +77 -14
- package/packages/model-router-plugin/package.json +1 -1
- package/packages/orchestration/src/binding-product.mjs +19 -0
- package/packages/orchestration/src/bot-data-actions.mjs +45 -0
- package/packages/orchestration/src/bot-data-chart.mjs +109 -0
- package/packages/orchestration/src/bot-data-context.mjs +44 -0
- package/packages/orchestration/src/bot-data-engine.mjs +809 -0
- package/packages/orchestration/src/bot-data-io.mjs +186 -0
- package/packages/orchestration/src/bot-data-sql.mjs +195 -0
- package/packages/orchestration/src/bot-data-surface.mjs +144 -0
- package/packages/orchestration/src/bot-data-tools.mjs +68 -0
- package/packages/orchestration/src/bot-data-worker.mjs +228 -0
- package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
- package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
- package/packages/orchestration/src/bot-loop.mjs +152 -19
- package/packages/orchestration/src/bot-notifications.mjs +159 -0
- package/packages/orchestration/src/bot-policy.mjs +43 -4
- package/packages/orchestration/src/bot-profile.mjs +171 -0
- package/packages/orchestration/src/bot-routine.mjs +4 -4
- package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
- package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
- package/packages/orchestration/src/bot-serving.mjs +164 -0
- package/packages/orchestration/src/bot-session-store.mjs +105 -9
- package/packages/orchestration/src/bot-settings.mjs +50 -11
- package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
- package/packages/orchestration/src/bot-thread-host.mjs +187 -0
- package/packages/orchestration/src/bot-threads.mjs +790 -0
- package/packages/orchestration/src/data-product-access.mjs +37 -0
- package/packages/orchestration/src/data-tool-contract.mjs +291 -0
- package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
- package/packages/orchestration/src/desktop-proxy.mjs +311 -0
- package/packages/orchestration/src/desktop-sessions.mjs +43 -9
- package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
- package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
- package/packages/orchestration/src/independent-chat-page.mjs +39 -0
- package/packages/orchestration/src/independent-chat-service.mjs +308 -0
- package/packages/orchestration/src/independent-context-state.mjs +44 -0
- package/packages/orchestration/src/independent-message-wire.mjs +84 -0
- package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
- package/packages/orchestration/src/independent-tool-families.mjs +73 -0
- package/packages/orchestration/src/independent-tool-host.mjs +344 -0
- package/packages/orchestration/src/main-bot-grant.mjs +15 -0
- package/packages/orchestration/src/main-model-routes.mjs +208 -0
- package/packages/orchestration/src/model-accounting.mjs +99 -35
- package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
- package/packages/orchestration/src/model-loop-frame.mjs +179 -0
- package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
- package/packages/orchestration/src/model-request-bounds.mjs +111 -0
- package/packages/orchestration/src/native-activity-contract.mjs +491 -0
- package/packages/orchestration/src/native-activity-service.mjs +102 -0
- package/packages/orchestration/src/native-activity-store.mjs +249 -0
- package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
- package/packages/orchestration/src/native-data-tools.mjs +100 -0
- package/packages/orchestration/src/native-remote-actions.mjs +742 -0
- package/packages/orchestration/src/native-team-tools.mjs +73 -0
- package/packages/orchestration/src/pairing-invite.mjs +37 -0
- package/packages/orchestration/src/product-composition.mjs +1559 -1001
- package/packages/orchestration/src/remote-machines.mjs +82 -14
- package/packages/orchestration/src/routine-execution.mjs +5 -3
- package/packages/orchestration/src/routine-session.mjs +19 -13
- package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
- package/packages/orchestration/src/task-journal.mjs +29 -0
- package/packages/orchestration/src/task-product.mjs +17 -0
- package/packages/orchestration/src/team-assignment-store.mjs +120 -0
- package/packages/orchestration/src/team-bridge.mjs +282 -0
- package/packages/orchestration/src/team-scheduler.mjs +83 -0
- package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
- package/packages/orchestration/src/team-tool-contract.mjs +284 -0
- package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
- package/packages/orchestration/src/web-panel-actions.mjs +103 -0
- package/packages/orchestration/src/web-panel-framing.mjs +227 -0
- package/packages/orchestration/src/web-panel-store.mjs +206 -0
- package/remote-computer-agent/README.md +39 -29
- package/remote-computer-agent/build.mjs +3 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
- package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
- package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
- package/remote-computer-agent/dist/cli.mjs +1688 -168
- package/remote-computer-agent/dist/cli.mjs.map +4 -4
- package/remote-computer-agent/dist/index.mjs +1579 -122
- package/remote-computer-agent/dist/index.mjs.map +4 -4
- package/remote-computer-agent/dist/native-peer.ps1 +51 -3
- package/remote-computer-agent/package-lock.json +308 -2
- package/remote-computer-agent/package.json +2 -1
- package/remote-computer-agent/pair-device.ps1 +20 -0
- package/remote-computer-agent/setup.ps1 +38 -16
- package/remote-computer-agent/src/cli.ts +24 -3
- package/remote-computer-agent/src/desktop-session.ts +11 -3
- package/remote-computer-agent/src/e2e-server.ts +81 -0
- package/remote-computer-agent/src/pairing-page.ts +35 -0
- package/remote-computer-agent/src/protocol.ts +25 -25
- package/remote-computer-agent/src/secure-pairing.ts +105 -0
- package/remote-computer-agent/src/store.ts +20 -5
- package/remote-computer-agent/src/transport.ts +39 -15
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
4
|
+
import css from './model-picker.css'
|
|
5
|
+
|
|
6
|
+
const routeOf = value => `${value.provider}/${value.model}`
|
|
7
|
+
const labelOf = value => value?.label && value.label !== routeOf(value) ? value.label : value?.model ?? ''
|
|
8
|
+
|
|
9
|
+
/** OpenMausBot-style compact picker: provider rail, searchable models, explicit choice. */
|
|
10
|
+
export function BotModelPicker({ models = [], value = '', onSelect, disabled = false, label = '默认模型', note = '此选择用于当前 Bot,保存配置后生效。' }) {
|
|
11
|
+
const selected = models.find(model => routeOf(model) === value)
|
|
12
|
+
const providers = useMemo(() => [...new Set(models.map(model => model.provider))], [models])
|
|
13
|
+
const [open, setOpen] = useState(false), [provider, setProvider] = useState(''), [query, setQuery] = useState('')
|
|
14
|
+
const [placement, setPlacement] = useState(null)
|
|
15
|
+
const trigger = useRef(null), panel = useRef(null), search = useRef(null)
|
|
16
|
+
const close = (restore = true) => { setOpen(false); if (restore) trigger.current?.focus() }
|
|
17
|
+
const filtered = models.filter(model => (!provider || model.provider === provider) && `${model.provider} ${model.model} ${model.label ?? ''}`.toLowerCase().includes(query.trim().toLowerCase()))
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const style = document.createElement('style'); style.dataset.botModelPicker = '1'; style.textContent = css; document.head.append(style)
|
|
20
|
+
return () => style.remove()
|
|
21
|
+
}, [])
|
|
22
|
+
useEffect(() => { if (disabled) setOpen(false) }, [disabled])
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
if (!open) return undefined
|
|
25
|
+
const position = () => {
|
|
26
|
+
const rect = trigger.current?.getBoundingClientRect()
|
|
27
|
+
if (!rect) return
|
|
28
|
+
const width = Math.min(560, window.innerWidth - 24), height = Math.min(430, window.innerHeight - 24)
|
|
29
|
+
const below = window.innerHeight - rect.bottom - 12
|
|
30
|
+
const top = below >= Math.min(height, 290) ? rect.bottom + 8 : Math.max(12, Math.min(rect.top - height - 8, window.innerHeight - height - 12))
|
|
31
|
+
setPlacement({ left: Math.max(12, Math.min(rect.left, window.innerWidth - width - 12)), top, width, maxHeight: Math.max(160, Math.min(height, window.innerHeight - top - 12)) })
|
|
32
|
+
}
|
|
33
|
+
position()
|
|
34
|
+
const outside = event => { if (!panel.current?.contains(event.target) && !trigger.current?.contains(event.target)) close(false) }
|
|
35
|
+
document.addEventListener('pointerdown', outside)
|
|
36
|
+
window.addEventListener('resize', position); window.addEventListener('scroll', position, true)
|
|
37
|
+
return () => { document.removeEventListener('pointerdown', outside); window.removeEventListener('resize', position); window.removeEventListener('scroll', position, true) }
|
|
38
|
+
}, [open])
|
|
39
|
+
// The first portal paint is hidden until placement is known. Focus it only
|
|
40
|
+
// after that visible paint; focusing a hidden input is ignored by browsers.
|
|
41
|
+
useLayoutEffect(() => { if (open && placement) search.current?.focus() }, [open, Boolean(placement)])
|
|
42
|
+
const choose = model => { if (!disabled && typeof onSelect === 'function') { onSelect(model); close() } }
|
|
43
|
+
const onKey = event => {
|
|
44
|
+
if (event.isComposing || event.nativeEvent?.isComposing || event.keyCode === 229) return
|
|
45
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); close(); return }
|
|
46
|
+
const rows = [...panel.current.querySelectorAll('[data-model-choice]')]
|
|
47
|
+
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) && (event.target === search.current || event.target.hasAttribute('data-model-choice'))) {
|
|
48
|
+
event.preventDefault()
|
|
49
|
+
const index = rows.indexOf(document.activeElement)
|
|
50
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? rows.length - 1 : index < 0 ? event.key === 'ArrowUp' ? rows.length - 1 : 0 : (index + (event.key === 'ArrowDown' ? 1 : -1) + rows.length) % rows.length
|
|
51
|
+
rows[next]?.focus(); return
|
|
52
|
+
}
|
|
53
|
+
if (event.key === 'Tab') {
|
|
54
|
+
const controls = [...panel.current.querySelectorAll('*')].filter(node => ['BUTTON', 'INPUT'].includes(node.tagName) && !node.disabled)
|
|
55
|
+
if (event.shiftKey && document.activeElement === controls[0]) { event.preventDefault(); controls.at(-1)?.focus() }
|
|
56
|
+
else if (!event.shiftKey && document.activeElement === controls.at(-1)) { event.preventDefault(); controls[0]?.focus() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return <div className="bp-model-picker">
|
|
60
|
+
<span className="bp-model-picker-label">{label}</span>
|
|
61
|
+
<button ref={trigger} className="bp-model-picker-trigger" type="button" disabled={disabled || !models.length} aria-label={`选择${label}:${selected ? labelOf(selected) : value || '未选择'}`} aria-haspopup="dialog" aria-expanded={open} onClick={() => {
|
|
62
|
+
if (open) { close(); return }
|
|
63
|
+
setProvider(selected?.provider ?? ''); setQuery(''); setOpen(true)
|
|
64
|
+
}}>
|
|
65
|
+
<span className="bp-model-provider-mark" aria-hidden="true">{(selected?.provider ?? '?').slice(0, 2).toUpperCase()}</span>
|
|
66
|
+
<span className="bp-model-current"><strong>{selected ? labelOf(selected) : value || '选择模型'}</strong><small>{selected?.provider ?? (value ? '当前模型暂不可用' : '选择提供商和模型')}</small></span>
|
|
67
|
+
<UiIcon name="chevron" size={15}/>
|
|
68
|
+
</button>
|
|
69
|
+
{!models.length && <p className="bp-model-picker-note">还没有可用模型,请先在模型设置中连接提供商。</p>}
|
|
70
|
+
{value && !selected && <p className="bp-model-picker-note" role="status">已保存的模型暂不在可用目录中,请重新选择或恢复连接。</p>}
|
|
71
|
+
{open && createPortal(<div ref={panel} className="bp-model-popover" role="dialog" aria-modal="true" aria-label="选择 Bot 模型" style={placement ?? { visibility: 'hidden' }} onKeyDown={onKey}>
|
|
72
|
+
<header className="bp-model-search"><UiIcon name="search" size={17}/><input ref={search} aria-label="搜索可用模型" placeholder="搜索模型或提供商…" value={query} onChange={event => setQuery(event.target.value)}/><button type="button" aria-label="关闭模型选择" onClick={() => close()}><UiIcon name="close" size={16}/></button></header>
|
|
73
|
+
<div className="bp-model-options">
|
|
74
|
+
<nav className="bp-model-provider-rail" aria-label="模型提供商"><button type="button" aria-pressed={!provider} onClick={() => setProvider('')}>全部提供商</button>{providers.map(id => <button type="button" data-provider={id} aria-pressed={provider === id} key={id} onClick={() => setProvider(id)}><span aria-hidden="true">{id.slice(0, 2).toUpperCase()}</span>{id}</button>)}</nav>
|
|
75
|
+
<div className="bp-model-results" role="group" aria-label="可用模型"><p className="bp-model-results-caption">{provider || '全部模型'}<span>{filtered.length}</span></p>{filtered.length ? filtered.map(model => {
|
|
76
|
+
const route = routeOf(model)
|
|
77
|
+
return <button key={route} data-model-choice={route} data-route={route} className="bp-model-option" type="button" aria-pressed={route === value} onClick={() => choose(model)}><span><strong>{labelOf(model)}</strong><small>{model.provider} · {model.model}</small></span>{route === value && <UiIcon name="check" size={16}/>}</button>
|
|
78
|
+
}) : <p className="bp-model-empty">没有匹配的模型,试试其他关键词或提供商。</p>}</div>
|
|
79
|
+
</div>
|
|
80
|
+
<footer>{note}</footer>
|
|
81
|
+
</div>, document.body)}
|
|
82
|
+
</div>
|
|
83
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* Native tool activity surface.
|
|
2
|
+
*
|
|
3
|
+
* One calm, compact surface for what a native engine actually did. It shares the
|
|
4
|
+
* chat tokens (--bp-chat-*) and the body[data-bot-product-ui] scope, so light,
|
|
5
|
+
* dark and 375px come from the same palette and rules as the rest of the chat.
|
|
6
|
+
*
|
|
7
|
+
* Scope. Every rule hangs off body[data-bot-product-ui], so the Harness
|
|
8
|
+
* workbench, other Sessions and the plain Agent view keep their own styling.
|
|
9
|
+
* Nothing here overrides layout, the composer or any native control: this is an
|
|
10
|
+
* observation, not a second toolbar.
|
|
11
|
+
*
|
|
12
|
+
* Colour lives in the chat tokens. The browser verifier checks state-label
|
|
13
|
+
* contrast on the rendered surface. The four states use shape and word as well
|
|
14
|
+
* as colour, so a colour-blind owner still reads 进行中 / 已返回 / 返回错误 /
|
|
15
|
+
* 待核对, and a late CLI return never borrows the success colour.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
html body[data-bot-product-ui] .bp-na-chip,
|
|
19
|
+
html body[data-bot-product-ui] .bp-na-card-toggle {
|
|
20
|
+
display: inline-flex; align-items: center; gap: 6px; min-height: 26px; max-width: 100%;
|
|
21
|
+
padding: 3px 9px 3px 7px; border: 1px solid var(--bp-chat-line); border-radius: 999px;
|
|
22
|
+
background: var(--bp-chat-surface); color: var(--bp-chat-muted); font-size: 11px; line-height: 1.4;
|
|
23
|
+
text-align: left; white-space: nowrap; cursor: pointer;
|
|
24
|
+
}
|
|
25
|
+
html body[data-bot-product-ui] .bp-na-chip:hover,
|
|
26
|
+
html body[data-bot-product-ui] .bp-na-card-toggle:hover { color: var(--bp-chat-ink); background: var(--bp-chat-hover); }
|
|
27
|
+
html body[data-bot-product-ui] .bp-na-chip > svg,
|
|
28
|
+
html body[data-bot-product-ui] .bp-na-card-toggle > svg { flex: none; }
|
|
29
|
+
html body[data-bot-product-ui] .bp-na-chip-text { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
30
|
+
html body[data-bot-product-ui] .bp-na-chip-count {
|
|
31
|
+
flex: none; padding: 0 5px; border-radius: 999px; background: var(--bp-chat-inset);
|
|
32
|
+
color: var(--bp-chat-muted); font-size: 10px; font-variant-numeric: tabular-nums;
|
|
33
|
+
}
|
|
34
|
+
/* A state is carried by its word first and its colour second, so the four states
|
|
35
|
+
stay apart without depending on hue alone. */
|
|
36
|
+
html body[data-bot-product-ui] .bp-na-chip[data-state=running],
|
|
37
|
+
html body[data-bot-product-ui] .bp-na-card[data-state=running] { color: var(--bp-chat-accent); border-color: var(--bp-chat-accent); }
|
|
38
|
+
html body[data-bot-product-ui] .bp-na-chip[data-state=running] > svg:first-child { animation: bp-na-pulse 1.6s ease-in-out infinite; }
|
|
39
|
+
@keyframes bp-na-pulse { 50% { opacity: .4; } }
|
|
40
|
+
html body[data-bot-product-ui] .bp-na-chip[data-state=cli-error],
|
|
41
|
+
html body[data-bot-product-ui] .bp-na-card[data-state=cli-error] { color: var(--bp-chat-danger); border-color: var(--bp-chat-danger); }
|
|
42
|
+
html body[data-bot-product-ui] .bp-na-chip[data-state=turn-unconfirmed],
|
|
43
|
+
html body[data-bot-product-ui] .bp-na-card[data-state=turn-unconfirmed] { color: var(--bp-chat-warning); border-color: var(--bp-chat-warning); }
|
|
44
|
+
|
|
45
|
+
html body[data-bot-product-ui] .bp-na-dialog {
|
|
46
|
+
position: fixed; z-index: 60; width: min(360px, calc(100vw - 24px)); max-height: min(420px, calc(100vh - 32px));
|
|
47
|
+
display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--bp-chat-line-strong);
|
|
48
|
+
border-radius: 14px; background: var(--bp-chat-surface); color: var(--bp-chat-ink); box-shadow: var(--bp-chat-shadow-float);
|
|
49
|
+
}
|
|
50
|
+
html body[data-bot-product-ui] .bp-na-dialog > header {
|
|
51
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px; flex: none;
|
|
52
|
+
padding: 9px 10px 9px 13px; border-bottom: 1px solid var(--bp-chat-line); font-size: 12px;
|
|
53
|
+
}
|
|
54
|
+
html body[data-bot-product-ui] .bp-na-dialog > header button {
|
|
55
|
+
display: grid; place-items: center; width: 28px; height: 28px; border: 0; border-radius: 8px;
|
|
56
|
+
background: transparent; color: var(--bp-chat-muted); cursor: pointer;
|
|
57
|
+
}
|
|
58
|
+
html body[data-bot-product-ui] .bp-na-dialog > header button:hover { color: var(--bp-chat-ink); background: var(--bp-chat-inset); }
|
|
59
|
+
html body[data-bot-product-ui] .bp-na-dialog-body { min-height: 0; overflow: auto; padding: 8px 10px 11px; }
|
|
60
|
+
html body[data-bot-product-ui] .bp-na-scope {
|
|
61
|
+
margin: 0 3px 8px; color: var(--bp-chat-muted); font-size: 10px; line-height: 1.5;
|
|
62
|
+
}
|
|
63
|
+
html body[data-bot-product-ui] .bp-na-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
64
|
+
html body[data-bot-product-ui] .bp-na-row {
|
|
65
|
+
display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; grid-auto-rows: auto;
|
|
66
|
+
align-items: start; gap: 3px 8px; padding: 7px 3px; border-radius: 9px;
|
|
67
|
+
}
|
|
68
|
+
html body[data-bot-product-ui] .bp-na-row:hover { background: var(--bp-chat-inset); }
|
|
69
|
+
html body[data-bot-product-ui] .bp-na-row-icon {
|
|
70
|
+
display: grid; place-items: center; width: 22px; height: 22px; border-radius: 7px;
|
|
71
|
+
background: var(--bp-chat-inset); color: var(--bp-chat-muted);
|
|
72
|
+
}
|
|
73
|
+
html body[data-bot-product-ui] .bp-na-row[data-state=running] .bp-na-row-icon { color: var(--bp-chat-accent); }
|
|
74
|
+
html body[data-bot-product-ui] .bp-na-row[data-state=cli-error] .bp-na-row-icon { color: var(--bp-chat-danger); }
|
|
75
|
+
html body[data-bot-product-ui] .bp-na-row[data-state=turn-unconfirmed] .bp-na-row-icon { color: var(--bp-chat-warning); }
|
|
76
|
+
html body[data-bot-product-ui] .bp-na-row[data-state=cli-ok] .bp-na-row-icon { color: var(--bp-chat-accent); }
|
|
77
|
+
html body[data-bot-product-ui] .bp-na-row-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
78
|
+
html body[data-bot-product-ui] .bp-na-row-title { font-size: 12px; color: var(--bp-chat-ink); overflow-wrap: anywhere; }
|
|
79
|
+
html body[data-bot-product-ui] .bp-na-row-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 10px; color: var(--bp-chat-muted); }
|
|
80
|
+
html body[data-bot-product-ui] .bp-na-state {
|
|
81
|
+
display: inline-flex; align-items: center; gap: 4px; padding: 0 6px; border-radius: 999px;
|
|
82
|
+
border: 1px solid var(--bp-chat-line); background: var(--bp-chat-canvas); font-size: 10px; color: var(--bp-chat-muted);
|
|
83
|
+
}
|
|
84
|
+
html body[data-bot-product-ui] .bp-na-state[data-state=running] { color: var(--bp-chat-accent); border-color: var(--bp-chat-accent); }
|
|
85
|
+
html body[data-bot-product-ui] .bp-na-state[data-state=cli-error] { color: var(--bp-chat-danger); border-color: var(--bp-chat-danger); }
|
|
86
|
+
html body[data-bot-product-ui] .bp-na-state[data-state=turn-unconfirmed] { color: var(--bp-chat-warning); border-color: var(--bp-chat-warning); }
|
|
87
|
+
html body[data-bot-product-ui] .bp-na-row-note { font-size: 10px; color: var(--bp-chat-warning); }
|
|
88
|
+
html body[data-bot-product-ui] .bp-na-row-more {
|
|
89
|
+
align-self: center; min-height: 24px; padding: 2px 7px; border: 1px solid var(--bp-chat-line); border-radius: 8px;
|
|
90
|
+
background: var(--bp-chat-canvas); color: var(--bp-chat-muted); font-size: 10px; cursor: pointer; white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
html body[data-bot-product-ui] .bp-na-row-more:hover { color: var(--bp-chat-ink); background: var(--bp-chat-hover); }
|
|
93
|
+
html body[data-bot-product-ui] .bp-na-detail {
|
|
94
|
+
grid-column: 2 / -1; margin: 3px 0 2px; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 10px;
|
|
95
|
+
padding: 8px 10px; border-radius: 9px; background: var(--bp-chat-canvas); font-size: 10px; color: var(--bp-chat-muted);
|
|
96
|
+
}
|
|
97
|
+
html body[data-bot-product-ui] .bp-na-detail dt { color: var(--bp-chat-muted); }
|
|
98
|
+
html body[data-bot-product-ui] .bp-na-detail dd { margin: 0; color: var(--bp-chat-ink); overflow-wrap: anywhere; }
|
|
99
|
+
html body[data-bot-product-ui] .bp-na-detail-limit { grid-column: 1 / -1; margin: 5px 0 0; color: var(--bp-chat-muted); }
|
|
100
|
+
html body[data-bot-product-ui] .bp-na-note {
|
|
101
|
+
display: block; margin: 6px 3px 0; padding: 7px 9px; border-radius: 9px; font-size: 10px; line-height: 1.5;
|
|
102
|
+
color: var(--bp-chat-warning); background: var(--bp-chat-warning-bg); overflow-wrap: anywhere;
|
|
103
|
+
}
|
|
104
|
+
/* In the header the note stays a quiet pill beside the chip: it never wraps the toolbar. */
|
|
105
|
+
html body[data-bot-product-ui] .bp-na-header { display: inline-flex; align-items: center; gap: 4px; min-width: 0; max-width: 100%; }
|
|
106
|
+
html body[data-bot-product-ui] .bp-na-header > .bp-na-note { margin: 0; max-width: 216px; padding: 3px 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
107
|
+
html body[data-bot-product-ui] .bp-na-note[data-kind=failed] { color: var(--bp-chat-danger); background: var(--bp-chat-danger-bg); }
|
|
108
|
+
html body[data-bot-product-ui] .bp-na-actions { margin: 8px 3px 0; }
|
|
109
|
+
html body[data-bot-product-ui] .bp-na-actions button {
|
|
110
|
+
min-height: 28px; padding: 4px 10px; border: 1px solid var(--bp-chat-line-strong); border-radius: 9px;
|
|
111
|
+
background: var(--bp-chat-surface); color: var(--bp-chat-ink); font-size: 11px; cursor: pointer;
|
|
112
|
+
}
|
|
113
|
+
html body[data-bot-product-ui] .bp-na-actions button:disabled { opacity: .55; cursor: default; }
|
|
114
|
+
html body[data-bot-product-ui] .bp-na-sr-only { position: absolute; display: block; margin: 0; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
|
|
115
|
+
/* Every control keeps a visible ring on keyboard focus, at the chat's own focus token. */
|
|
116
|
+
html body[data-bot-product-ui] .bp-na-chip:focus-visible,
|
|
117
|
+
html body[data-bot-product-ui] .bp-na-card-toggle:focus-visible,
|
|
118
|
+
html body[data-bot-product-ui] .bp-na-row-more:focus-visible,
|
|
119
|
+
html body[data-bot-product-ui] .bp-na-actions button:focus-visible,
|
|
120
|
+
html body[data-bot-product-ui] .bp-na-dialog > header button:focus-visible,
|
|
121
|
+
html body[data-bot-product-ui] .bp-na-dialog:focus-visible { outline: 2px solid var(--bp-chat-focus); outline-offset: 2px; }
|
|
122
|
+
|
|
123
|
+
/* The transcript card: the same records, quieter, inside the independent chat. */
|
|
124
|
+
html body[data-bot-product-ui] .bp-na-card {
|
|
125
|
+
align-self: stretch; min-width: 0; border: 1px solid var(--bp-chat-line); border-radius: 13px;
|
|
126
|
+
background: var(--bp-chat-surface); padding: 3px 9px; margin: 2px 0;
|
|
127
|
+
}
|
|
128
|
+
html body[data-bot-product-ui] .bp-na-card-toggle { border: 0; background: transparent; width: 100%; padding: 6px 2px; }
|
|
129
|
+
html body[data-bot-product-ui] .bp-na-card-toggle:hover { background: var(--bp-chat-inset); }
|
|
130
|
+
html body[data-bot-product-ui] .bp-na-card-count { margin-left: auto; flex: none; font-size: 10px; color: var(--bp-chat-muted); }
|
|
131
|
+
html body[data-bot-product-ui] .bp-na-card-body { padding: 0 0 7px; }
|
|
132
|
+
html body[data-bot-product-ui] .bp-na-card .bp-na-row { grid-template-columns: 20px minmax(0, 1fr) auto; }
|
|
133
|
+
|
|
134
|
+
/* 375px: the chip keeps its word and its dot, the dialog takes the width it can. */
|
|
135
|
+
@media (max-width: 480px) {
|
|
136
|
+
html body[data-bot-product-ui] .bp-na-chip { max-width: 168px; }
|
|
137
|
+
html body[data-bot-product-ui] .bp-na-dialog { width: calc(100vw - 16px); max-height: calc(100vh - 24px); }
|
|
138
|
+
html body[data-bot-product-ui] .bp-na-row { grid-template-columns: 20px minmax(0, 1fr) auto; }
|
|
139
|
+
html body[data-bot-product-ui] .bp-na-row-more { grid-column: 3; justify-self: end; margin-top: 0; min-height: 32px; }
|
|
140
|
+
html body[data-bot-product-ui] .bp-na-detail { grid-column: 1 / -1; }
|
|
141
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
|
|
2
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
3
|
+
import nativeActivityCss from './native-activity.css'
|
|
4
|
+
import {
|
|
5
|
+
NATIVE_ACTIVITY_LABELS, NATIVE_ACTIVITY_STATES,
|
|
6
|
+
NATIVE_ACTIVITY_ACTION,
|
|
7
|
+
createNativeActivityController, formatActivityAt, nativeActivityLatest, nativeActivityOutcomeWord,
|
|
8
|
+
nativeActivityRecordingNote, nativeActivityScopeKey, nativeActivityState, nativeActivitySummary, nativeActivityTool,
|
|
9
|
+
} from './native-activity.mjs'
|
|
10
|
+
export { NATIVE_ACTIVITY_LABELS, NATIVE_ACTIVITY_STATES }
|
|
11
|
+
|
|
12
|
+
// The shared activity surface.
|
|
13
|
+
//
|
|
14
|
+
// One React surface serves both entry modes: the integrated chat header shows a
|
|
15
|
+
// compact chip with an anchored detail dialog, the independent transcript shows a
|
|
16
|
+
// compact card that expands in place. Both read the same owner action
|
|
17
|
+
// `nativeActivity.list`, through the same controller, and both show the same four
|
|
18
|
+
// honest states.
|
|
19
|
+
//
|
|
20
|
+
// What this surface cannot do is deliberate. There is no send, no stop, no
|
|
21
|
+
// retry, no approval and no task mutation anywhere in it: a cancel request and a
|
|
22
|
+
// confirmed stop are displayed as the two separate facts they are, and nothing
|
|
23
|
+
// here executes a tool. A tool's parameters and its result are not part of the
|
|
24
|
+
// observation, so the expanded detail shows provenance — record, turn, times,
|
|
25
|
+
// reported name — and says plainly that the command body is not available.
|
|
26
|
+
|
|
27
|
+
/** The stylesheet rides with the surface, so no other file has to import it. Two
|
|
28
|
+
* mounted surfaces share one tag: the last one out removes it. */
|
|
29
|
+
function useNativeActivityStyles() {
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (typeof document === 'undefined') return undefined
|
|
32
|
+
const release = style => {
|
|
33
|
+
const users = Math.max(0, Number(style.dataset.botNativeActivityUsers ?? '1') - 1)
|
|
34
|
+
style.dataset.botNativeActivityUsers = String(users)
|
|
35
|
+
if (users === 0) style.remove()
|
|
36
|
+
}
|
|
37
|
+
const existing = document.head.querySelector('style[data-bot-native-activity]')
|
|
38
|
+
if (existing) {
|
|
39
|
+
const users = Number(existing.dataset.botNativeActivityUsers ?? '1') + 1
|
|
40
|
+
existing.dataset.botNativeActivityUsers = String(users)
|
|
41
|
+
return () => release(existing)
|
|
42
|
+
}
|
|
43
|
+
const style = document.createElement('style')
|
|
44
|
+
style.dataset.botNativeActivity = '1'
|
|
45
|
+
style.dataset.botNativeActivityUsers = '1'
|
|
46
|
+
style.textContent = nativeActivityCss
|
|
47
|
+
document.head.append(style)
|
|
48
|
+
return () => release(style)
|
|
49
|
+
}, [])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const noopSubscribe = () => () => {}
|
|
53
|
+
const emptySnapshot = () => null
|
|
54
|
+
|
|
55
|
+
// Independent chat already owns a directory snapshot and supplies the flag.
|
|
56
|
+
// The integrated header has no directory state: read availability once per API,
|
|
57
|
+
// never start an activity poll against an older Host that did not advertise it.
|
|
58
|
+
function useAdvertisedActivity(productApi, enabled) {
|
|
59
|
+
const [known, setKnown] = useState({ api: null, available: false })
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (enabled !== undefined || typeof productApi?.snapshot !== 'function') return undefined
|
|
62
|
+
let live = true
|
|
63
|
+
const controller = new AbortController()
|
|
64
|
+
const timer = setTimeout(() => controller.abort(), 8000)
|
|
65
|
+
void Promise.resolve().then(() => productApi.snapshot(controller.signal)).then(value => {
|
|
66
|
+
if (live && !controller.signal.aborted) setKnown({ api: productApi,
|
|
67
|
+
available: value?.schemaVersion === 1 && Array.isArray(value.availableActions) && value.availableActions.includes(NATIVE_ACTIVITY_ACTION) })
|
|
68
|
+
}, () => { if (live) setKnown({ api: productApi, available: false }) }).finally(() => clearTimeout(timer))
|
|
69
|
+
return () => { live = false; clearTimeout(timer); controller.abort() }
|
|
70
|
+
}, [productApi, enabled])
|
|
71
|
+
return enabled === undefined ? known.api === productApi && known.available : enabled === true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One controller for one mounted scope. A different API instance, Bot, mode,
|
|
76
|
+
* session or context is a new scope: the previous controller is disposed (timer
|
|
77
|
+
* cleared, in-flight read aborted) before the next one starts, so a retired
|
|
78
|
+
* answer can never land in a new conversation.
|
|
79
|
+
*/
|
|
80
|
+
export function useNativeActivity({ productApi, botId, mode, sessionId, contextId, pollMs, limit, enabled } = {}) {
|
|
81
|
+
const scopeKey = useMemo(() => {
|
|
82
|
+
try { return nativeActivityScopeKey({ botId, mode, sessionId, contextId }) } catch { return null }
|
|
83
|
+
}, [botId, mode, sessionId, contextId])
|
|
84
|
+
const available = useAdvertisedActivity(productApi, scopeKey === null ? false : enabled)
|
|
85
|
+
const controller = useMemo(() => {
|
|
86
|
+
if (scopeKey === null || !available) return null
|
|
87
|
+
try { return createNativeActivityController({ api: productApi, botId, mode, sessionId, contextId, pollMs, limit }) } catch { return null }
|
|
88
|
+
}, [scopeKey, productApi, pollMs, limit, available])
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!controller) return undefined
|
|
91
|
+
controller.start()
|
|
92
|
+
return () => controller.dispose()
|
|
93
|
+
}, [controller])
|
|
94
|
+
const activity = useSyncExternalStore(controller ? controller.subscribe : noopSubscribe,
|
|
95
|
+
controller ? controller.getSnapshot : emptySnapshot, emptySnapshot)
|
|
96
|
+
return { activity, controller }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Only a real observation is a surface: an unrecorded session shows nothing at all. */
|
|
100
|
+
const surfaceVisible = activity => activity !== null
|
|
101
|
+
&& (activity.items.length > 0 || activity.recording.status === 'degraded' || activity.error !== '')
|
|
102
|
+
|
|
103
|
+
function NativeActivityRow({ record, open, onToggle }) {
|
|
104
|
+
const tool = nativeActivityTool(record), state = nativeActivityState(record), toggle = useRef(null)
|
|
105
|
+
const close = useCallback(() => { onToggle(null); toggle.current?.focus() }, [onToggle])
|
|
106
|
+
return <li className="bp-na-row" data-state={state} data-call-key={record.callKey}
|
|
107
|
+
onKeyDown={event => {
|
|
108
|
+
if (event.key !== 'Escape' || !open || event.nativeEvent?.isComposing === true) return
|
|
109
|
+
event.preventDefault(); event.stopPropagation(); close()
|
|
110
|
+
}}>
|
|
111
|
+
<span className="bp-na-row-icon"><UiIcon name={tool.icon} size={14} /></span>
|
|
112
|
+
<span className="bp-na-row-body">
|
|
113
|
+
<span className="bp-na-row-title">{tool.label}</span>
|
|
114
|
+
<span className="bp-na-row-meta">
|
|
115
|
+
<span className="bp-na-state" data-state={state}>{NATIVE_ACTIVITY_LABELS[state]}</span>
|
|
116
|
+
<span>{formatActivityAt(record.startedAt)}</span>
|
|
117
|
+
</span>
|
|
118
|
+
{record.cancelRequested && !record.stopConfirmed && <span className="bp-na-row-note">已请求停止会话,进程停止尚未确认</span>}
|
|
119
|
+
{record.stopConfirmed && <span className="bp-na-row-note">会话进程已停止</span>}
|
|
120
|
+
{state === 'turn-unconfirmed' && record.lateCliReturn && <span className="bp-na-row-note">命令返回较晚,结果仍待核对</span>}
|
|
121
|
+
</span>
|
|
122
|
+
<button ref={toggle} type="button" className="bp-na-row-more" aria-expanded={open}
|
|
123
|
+
aria-label={`${tool.label} 的记录详情`} onClick={() => onToggle(open ? null : record.callKey)}>{open ? '收起' : '详情'}</button>
|
|
124
|
+
{open && <dl className="bp-na-detail">
|
|
125
|
+
<dt>记录编号</dt><dd>{record.callKey}</dd>
|
|
126
|
+
{record.turnId && <><dt>回合编号</dt><dd>{record.turnId}</dd></>}
|
|
127
|
+
{record.callId && <><dt>调用编号</dt><dd>{record.callId}</dd></>}
|
|
128
|
+
{record.toolName && <><dt>宿主报告的名称</dt><dd>{record.toolName}</dd></>}
|
|
129
|
+
<dt>开始时间</dt><dd>{formatActivityAt(record.startedAt)}</dd>
|
|
130
|
+
<dt>结束时间</dt><dd>{record.endedAt === null ? '尚未记录' : formatActivityAt(record.endedAt)}</dd>
|
|
131
|
+
<dt>工具返回</dt><dd>{record.cliOk === true ? '已返回' : record.cliOk === false ? '返回错误' : '尚未记录'}</dd>
|
|
132
|
+
<dt>回合结果</dt><dd>{record.turnOutcome === null ? '宿主未记录结果' : nativeActivityOutcomeWord(record.turnOutcome)}</dd>
|
|
133
|
+
<dt>停止请求</dt><dd>{record.cancelRequested ? '已请求' : '未请求'}</dd>
|
|
134
|
+
<dt>进程停止确认</dt><dd>{record.stopConfirmed ? '已确认' : '尚未确认'}</dd>
|
|
135
|
+
<dd className="bp-na-detail-limit">这里不显示命令内容与返回内容;工具已返回或会话进程已停止,都不能证明远程操作已完成或停止。</dd>
|
|
136
|
+
</dl>}
|
|
137
|
+
</li>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The loaded records: latest4 as a view crop, older pages on an explicit read. */
|
|
141
|
+
function NativeActivityList({ activity, controller, shown = 4 }) {
|
|
142
|
+
const [openKey, setOpenKey] = useState(null), [showAll, setShowAll] = useState(false)
|
|
143
|
+
useEffect(() => { setOpenKey(null); setShowAll(false) }, [controller])
|
|
144
|
+
const rows = showAll ? activity.items : nativeActivityLatest(activity.items, shown)
|
|
145
|
+
const older = activity.items.length - rows.length
|
|
146
|
+
return <>
|
|
147
|
+
<p className="bp-na-scope">只显示当前已经读到的 {activity.items.length} 条记录,不是全部历史。</p>
|
|
148
|
+
<ul className="bp-na-list">
|
|
149
|
+
{rows.map(record => <NativeActivityRow key={record.callKey} record={record} open={openKey === record.callKey} onToggle={setOpenKey} />)}
|
|
150
|
+
</ul>
|
|
151
|
+
{activity.items.length > shown && <p className="bp-na-actions"><button type="button" onClick={() => setShowAll(value => !value)}>{showAll ? `只看最近 ${shown} 条` : `查看已读取的 ${activity.items.length} 条`}</button></p>}
|
|
152
|
+
{older > 0 && <p className="bp-na-scope">还有 {older} 条更早的记录已经读到,可展开查看。</p>}
|
|
153
|
+
{activity.truncated && <p className="bp-na-note">更早的记录没有全部载入,已停止继续读取。</p>}
|
|
154
|
+
{activity.nextCursor !== null && <p className="bp-na-actions"><button type="button" disabled={activity.loadingOlder || activity.loading} onClick={() => { setShowAll(true); void controller?.older() }}>{activity.loadingOlder ? '正在读取…' : '读取更早的记录'}</button></p>}
|
|
155
|
+
{activity.loadingOlder && <p className="bp-na-scope" role="status">正在读取更早的记录…</p>}
|
|
156
|
+
</>
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A read failure or a degraded recorder is always visible: it must never look like no work. */
|
|
160
|
+
function NativeActivityFlags({ activity }) {
|
|
161
|
+
const error = activity.error !== '' ? activity.error : ''
|
|
162
|
+
const degraded = nativeActivityRecordingNote(activity.recording)
|
|
163
|
+
if (error === '' && degraded === '') return null
|
|
164
|
+
return <>
|
|
165
|
+
{error !== '' && <span className="bp-na-note" data-kind="failed" title={error}>{error}</span>}
|
|
166
|
+
{degraded !== '' && <span className="bp-na-note" title={degraded}>{degraded}</span>}
|
|
167
|
+
</>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The integrated header: a compact chip plus an anchored, dismissible detail dialog. */
|
|
171
|
+
export function NativeActivityHeader({ productApi, botId, sessionId, pollMs, limit, className, enabled }) {
|
|
172
|
+
const { activity, controller } = useNativeActivity({ productApi, botId, mode: 'integration', sessionId, pollMs, limit, enabled })
|
|
173
|
+
useNativeActivityStyles()
|
|
174
|
+
const [open, setOpen] = useState(false), [place, setPlace] = useState(null)
|
|
175
|
+
const root = useRef(null), chip = useRef(null), dialog = useRef(null)
|
|
176
|
+
const close = useCallback(() => {
|
|
177
|
+
setOpen(false)
|
|
178
|
+
if (dialog.current?.contains(document.activeElement)) chip.current?.focus()
|
|
179
|
+
}, [])
|
|
180
|
+
useEffect(() => { setOpen(false) }, [controller])
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!open) { setPlace(null); return undefined }
|
|
183
|
+
const measure = () => {
|
|
184
|
+
const box = chip.current?.getBoundingClientRect()
|
|
185
|
+
if (box === undefined) return
|
|
186
|
+
const width = Math.min(360, Math.max(240, window.innerWidth - 24))
|
|
187
|
+
const left = Math.min(Math.max(8, box.right - width), Math.max(8, window.innerWidth - width - 8))
|
|
188
|
+
const below = box.bottom + 6, above = box.top - 6
|
|
189
|
+
const room = Math.min(420, window.innerHeight - 32)
|
|
190
|
+
setPlace({ left, ...(below + room < window.innerHeight || above < room ? { top: below } : { bottom: window.innerHeight - above }) })
|
|
191
|
+
}
|
|
192
|
+
measure()
|
|
193
|
+
window.addEventListener('resize', measure)
|
|
194
|
+
window.addEventListener('scroll', measure, true)
|
|
195
|
+
return () => { window.removeEventListener('resize', measure); window.removeEventListener('scroll', measure, true) }
|
|
196
|
+
}, [open])
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!open) return undefined
|
|
199
|
+
dialog.current?.focus()
|
|
200
|
+
const away = event => { if (!dialog.current?.contains(event.target) && !root.current?.contains(event.target)) setOpen(false) }
|
|
201
|
+
const key = event => {
|
|
202
|
+
if (event.key === 'Escape' && event.nativeEvent?.isComposing !== true) { event.preventDefault(); close(); return }
|
|
203
|
+
if (event.key !== 'Tab' || !dialog.current) return
|
|
204
|
+
const focusable = [...dialog.current.querySelectorAll('button:not([disabled]),[href],summary,[tabindex]:not([tabindex="-1"])')]
|
|
205
|
+
if (!focusable.length) return
|
|
206
|
+
const first = focusable[0], last = focusable[focusable.length - 1]
|
|
207
|
+
if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog.current)) { event.preventDefault(); first.focus() }
|
|
208
|
+
else if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) { event.preventDefault(); last.focus() }
|
|
209
|
+
}
|
|
210
|
+
document.addEventListener('mousedown', away)
|
|
211
|
+
document.addEventListener('keydown', key)
|
|
212
|
+
return () => { document.removeEventListener('mousedown', away); document.removeEventListener('keydown', key) }
|
|
213
|
+
}, [open, close])
|
|
214
|
+
if (!surfaceVisible(activity)) return null
|
|
215
|
+
const summary = nativeActivitySummary(activity.items)
|
|
216
|
+
const text = summary === null ? '工具记录读取异常' : summary.text
|
|
217
|
+
const state = summary === null ? 'turn-unconfirmed' : summary.state
|
|
218
|
+
const label = summary === null ? `${text},打开工具记录详情` : `${text},已读到 ${activity.items.length} 条,打开工具记录详情`
|
|
219
|
+
return <span className={`bp-na-header${className ? ` ${className}` : ''}`} ref={root}>
|
|
220
|
+
<button ref={chip} type="button" className="bp-na-chip" data-state={state} aria-haspopup="dialog" aria-expanded={open}
|
|
221
|
+
title={label} aria-label={label} onClick={() => setOpen(value => !value)}>
|
|
222
|
+
<UiIcon name={summary === null ? 'clock' : summary.icon} size={14} />
|
|
223
|
+
<span className="bp-na-chip-text">{text}</span>
|
|
224
|
+
{activity.items.length > 0 && <span className="bp-na-chip-count" aria-hidden="true">{activity.items.length}</span>}
|
|
225
|
+
</button>
|
|
226
|
+
<span className="bp-na-sr-only" role="status">{summary === null ? text : `${text},共 ${activity.items.length} 条已读到`}</span>
|
|
227
|
+
<NativeActivityFlags activity={activity} />
|
|
228
|
+
{open && <div ref={dialog} className="bp-na-dialog" role="dialog" aria-modal="true" aria-label="工具记录" tabIndex={-1}
|
|
229
|
+
style={place === null ? undefined : { left: `${place.left}px`, top: place.top === undefined ? undefined : `${place.top}px`, bottom: place.bottom === undefined ? undefined : `${place.bottom}px` }}>
|
|
230
|
+
<header><strong>工具记录</strong><button type="button" aria-label="关闭工具记录" onClick={close}><UiIcon name="close" size={14} /></button></header>
|
|
231
|
+
<div className="bp-na-dialog-body">
|
|
232
|
+
<NativeActivityList activity={activity} controller={controller} />
|
|
233
|
+
</div>
|
|
234
|
+
</div>}
|
|
235
|
+
</span>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The independent transcript: a compact card that expands in place, never a modal.
|
|
239
|
+
*
|
|
240
|
+
* The transcript is already one polite live region, so the card inside it adds no
|
|
241
|
+
* second announcer: `announce` stays false there and true for a standalone card. */
|
|
242
|
+
export function NativeActivityCardView({ activity, controller, className, announce = true }) {
|
|
243
|
+
useNativeActivityStyles()
|
|
244
|
+
const [open, setOpen] = useState(false)
|
|
245
|
+
const toggle = useRef(null)
|
|
246
|
+
useEffect(() => { setOpen(false) }, [controller])
|
|
247
|
+
if (!surfaceVisible(activity)) return null
|
|
248
|
+
const summary = nativeActivitySummary(activity.items)
|
|
249
|
+
const text = summary === null ? '工具记录读取异常' : summary.text
|
|
250
|
+
const state = summary === null ? 'turn-unconfirmed' : summary.state
|
|
251
|
+
const label = `${text},${open ? '收起' : '展开'}工具记录`
|
|
252
|
+
return <section className={`bp-na-card${className ? ` ${className}` : ''}`} data-state={state} aria-label="工具记录">
|
|
253
|
+
<button ref={toggle} type="button" className="bp-na-card-toggle" aria-expanded={open} title={label} aria-label={label}
|
|
254
|
+
onKeyDown={event => {
|
|
255
|
+
if (event.key !== 'Escape' || !open || event.nativeEvent?.isComposing === true) return
|
|
256
|
+
event.preventDefault(); event.stopPropagation(); setOpen(false); toggle.current?.focus()
|
|
257
|
+
}}
|
|
258
|
+
onClick={() => setOpen(value => !value)}>
|
|
259
|
+
<UiIcon name={summary === null ? 'clock' : summary.icon} size={14} />
|
|
260
|
+
<span className="bp-na-chip-text">{text}</span>
|
|
261
|
+
{activity.items.length > 0 && <span className="bp-na-card-count">已读到 {activity.items.length} 条</span>}
|
|
262
|
+
<UiIcon name="chevron" size={13} style={{ transform: open ? 'rotate(90deg)' : 'none' }} />
|
|
263
|
+
</button>
|
|
264
|
+
{announce && <span className="bp-na-sr-only" role="status">{summary === null ? text : `${text},共 ${activity.items.length} 条已读到`}</span>}
|
|
265
|
+
<NativeActivityFlags activity={activity} />
|
|
266
|
+
{open && <div className="bp-na-card-body">
|
|
267
|
+
<NativeActivityList activity={activity} controller={controller} />
|
|
268
|
+
</div>}
|
|
269
|
+
</section>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Self-contained card, for a caller that owns no controller of its own. */
|
|
273
|
+
export function NativeActivityCard({ productApi, botId, contextId, pollMs, limit, className, enabled }) {
|
|
274
|
+
const owned = useNativeActivity({ productApi, botId, mode: 'independent', contextId, pollMs, limit, enabled })
|
|
275
|
+
return <NativeActivityCardView activity={owned.activity} controller={owned.controller} className={className} />
|
|
276
|
+
}
|