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