@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +20 -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/README.md +3 -3
- 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,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unsent composer drafts: the text a person typed and did not send.
|
|
3
|
+
*
|
|
4
|
+
* What this is and nothing more. No attachment bytes, no OS paths, no Blob URLs,
|
|
5
|
+
* no credentials, and no send: restoring a draft only fills the Host composer's
|
|
6
|
+
* input, it never submits, starts a turn, calls a model or answers an approval.
|
|
7
|
+
*
|
|
8
|
+
* Why a store of its own: the Host composer keeps its draft only while its
|
|
9
|
+
* session stays mounted, so switching conversations or reloading the window loses
|
|
10
|
+
* what was typed. The client surface the product injects (sessions,
|
|
11
|
+
* uiConversation, slots, theme, layout) exposes no draft state of its own, so the
|
|
12
|
+
* product keeps the text here instead of reading the composer's DOM — a hashed
|
|
13
|
+
* class selector or a global contenteditable query would be a guess about the
|
|
14
|
+
* Host's markup, and this store only uses identities the Host already publishes.
|
|
15
|
+
*
|
|
16
|
+
* Host scope: several Hosts can share one origin (dsh-app://app), so a plain
|
|
17
|
+
* origin store would let one Host read another's drafts. Every blob carries the
|
|
18
|
+
* Host namespace that wrote it, and a store refuses to read, write or adopt a
|
|
19
|
+
* blob from a different namespace or from an older replay of its own. A session
|
|
20
|
+
* is only reachable once the Host surface has put it in the saved directory, and
|
|
21
|
+
* the identity always comes from the mounted conversation's own roster row, never
|
|
22
|
+
* from an argument a model could supply.
|
|
23
|
+
*
|
|
24
|
+
* The namespace itself is the Host's: it arrives from the Host's own
|
|
25
|
+
* `ui.namespace` read (see composer-draft-host.jsx). `resolveHostId` below is a
|
|
26
|
+
* fixture helper for tests and the offline browser preview; the production
|
|
27
|
+
* wiring never imports it.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const COMPOSER_DRAFT_KEY = 'bot-product:composer-drafts'
|
|
31
|
+
export const COMPOSER_DRAFT_HOST_KEY = 'bot-product:composer-draft-host'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One Host's blob lives under its own key. Two Hosts in one origin then cannot
|
|
35
|
+
* overwrite each other, whatever their epochs say; the namespace inside the blob
|
|
36
|
+
* is the second lock on the same door.
|
|
37
|
+
*/
|
|
38
|
+
export const composerDraftKey = hostId => `${COMPOSER_DRAFT_KEY}:${hostId}`
|
|
39
|
+
/** Per-conversation text and per-Host totals. A refusal names its reason. */
|
|
40
|
+
export const COMPOSER_DRAFT_LIMITS = Object.freeze({
|
|
41
|
+
maxSessions: 48,
|
|
42
|
+
maxTextChars: 20000,
|
|
43
|
+
maxTextBytes: 65536,
|
|
44
|
+
maxTotalBytes: 262144,
|
|
45
|
+
maxDirectory: 512,
|
|
46
|
+
saveDelayMs: 500,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const KINDS = new Set(['bot', 'room'])
|
|
50
|
+
const HOST_PATTERN = /^host-[0-9a-f]{16}$/
|
|
51
|
+
/** The threadId shape the Host's own thread directory accepts. */
|
|
52
|
+
const THREAD_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
|
53
|
+
const encoder = typeof TextEncoder === 'function' ? new TextEncoder() : undefined
|
|
54
|
+
|
|
55
|
+
/** UTF-8 length, so a Chinese draft is budgeted by what it costs to store. */
|
|
56
|
+
export const textBytes = text => (encoder ? encoder.encode(text).length : String(text).length)
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The storage identity of one conversation: a Bot or a group, never a part. A
|
|
60
|
+
* Bot's chat spans numbered parts, and an unsent line survives a part rotation
|
|
61
|
+
* because it belongs to the conversation, not to the session that was open.
|
|
62
|
+
*
|
|
63
|
+
* A thread is a separate conversation under the same Bot: each `threadId` gets
|
|
64
|
+
* its own draft, so two threads of one Bot never share text. The threadId comes
|
|
65
|
+
* from the Host's own thread directory mapped through the trusted roster, never
|
|
66
|
+
* from a model argument.
|
|
67
|
+
*/
|
|
68
|
+
export function conversationKey({ kind, ownerId, threadId } = {}) {
|
|
69
|
+
if (!KINDS.has(kind)) return ''
|
|
70
|
+
const owner = typeof ownerId === 'string' ? ownerId.trim() : ''
|
|
71
|
+
if (!owner || /[\s:/]/.test(owner)) return ''
|
|
72
|
+
const thread = typeof threadId === 'string' ? threadId.trim() : ''
|
|
73
|
+
if (!thread) return `${kind}:${owner}`
|
|
74
|
+
if (!THREAD_PATTERN.test(thread)) return ''
|
|
75
|
+
return `${kind}:${owner}:thread:${thread}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The key for a mounted session, read from the Host roster it came with. */
|
|
79
|
+
export function ownerKeyFromRoster(sessionId, roster) {
|
|
80
|
+
const id = typeof sessionId === 'string' ? sessionId : ''
|
|
81
|
+
if (!id || !roster) return ''
|
|
82
|
+
const thread = roster.threadsById?.[id]
|
|
83
|
+
if (thread) return conversationKey({ kind: 'bot', ownerId: thread.botId, threadId: thread.threadId })
|
|
84
|
+
const bot = roster.byId?.[id]
|
|
85
|
+
if (bot) return conversationKey({ kind: 'bot', ownerId: bot.id ?? id })
|
|
86
|
+
const room = roster.roomsById?.[id]
|
|
87
|
+
if (room) return conversationKey({ kind: 'room', ownerId: room.id ?? id })
|
|
88
|
+
return ''
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The key for a mounted session inside one thread of a Bot. `threadOf` maps the
|
|
93
|
+
* mounted session to the stable threadId the Host directory gave it; a session
|
|
94
|
+
* the mapping does not name is the Bot's default conversation, which every part
|
|
95
|
+
* of that Bot shares.
|
|
96
|
+
*/
|
|
97
|
+
export function threadKeyFromRoster(sessionId, roster, threadOf) {
|
|
98
|
+
const id = typeof sessionId === 'string' ? sessionId : ''
|
|
99
|
+
if (!id || !roster) return ''
|
|
100
|
+
const thread = roster.threadsById?.[id]
|
|
101
|
+
if (thread) return conversationKey({ kind: 'bot', ownerId: thread.botId, threadId: thread.threadId })
|
|
102
|
+
const bot = roster.byId?.[id]
|
|
103
|
+
if (bot) return conversationKey({ kind: 'bot', ownerId: bot.id ?? id, threadId: threadOf?.(id) })
|
|
104
|
+
const room = roster.roomsById?.[id]
|
|
105
|
+
if (room) return conversationKey({ kind: 'room', ownerId: room.id ?? id })
|
|
106
|
+
return ''
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const mintHostId = random => {
|
|
110
|
+
const raw = typeof random === 'function' ? String(random() ?? '') : ''
|
|
111
|
+
const hex = raw.replace(/[^0-9a-f]/gi, '').slice(0, 16)
|
|
112
|
+
// A namespace that silently collides would hand one Host another's drafts.
|
|
113
|
+
if (hex.length !== 16) throw new Error('composer-draft: the Host namespace needs 16 hex characters')
|
|
114
|
+
return `host-${hex.toLowerCase()}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The persisted Host namespace. Generated once and kept, so a reload reads the
|
|
119
|
+
* same drafts; a blocked storage keeps it for this window only.
|
|
120
|
+
*
|
|
121
|
+
* FIXTURE HELPER. This is a browser-origin id, not a Host identity: two Hosts
|
|
122
|
+
* sharing the `dsh-app://app` origin would mint and read the same value, which
|
|
123
|
+
* is exactly the boundary the production path must not have. The production
|
|
124
|
+
* namespace comes from the Host's own `ui.namespace` read through the product
|
|
125
|
+
* API (composer-draft-host.jsx); nothing in the production wiring imports this.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveHostId({ storage, key = COMPOSER_DRAFT_HOST_KEY, random } = {}) {
|
|
128
|
+
const read = () => {
|
|
129
|
+
try { return storage?.getItem?.(key) ?? '' } catch { return '' }
|
|
130
|
+
}
|
|
131
|
+
const found = read()
|
|
132
|
+
if (HOST_PATTERN.test(found)) return found
|
|
133
|
+
const minted = mintHostId(random)
|
|
134
|
+
try { storage?.setItem?.(key, minted) } catch { /* a blocked store keeps this window's namespace */ }
|
|
135
|
+
return minted
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const readBlob = (storage, key) => {
|
|
139
|
+
try {
|
|
140
|
+
const raw = storage?.getItem?.(key)
|
|
141
|
+
const parsed = raw ? JSON.parse(raw) : null
|
|
142
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
143
|
+
} catch { return null }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const writeBlob = (storage, key, blob) => {
|
|
147
|
+
try { storage?.setItem?.(key, JSON.stringify(blob)); return true } catch { return false }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cleanId = value => (typeof value === 'string' && value.length <= 200 ? value : '')
|
|
151
|
+
|
|
152
|
+
const bytesOf = sessions => Object.values(sessions).reduce((sum, row) => sum + (row?.bytes ?? 0), 0)
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* One Host's draft store. Every entry point refuses instead of guessing: an
|
|
156
|
+
* unknown conversation, a foreign or replayed blob, an over-budget text and a
|
|
157
|
+
* stale revision all return `{ ok: false, reason }` and change nothing.
|
|
158
|
+
*/
|
|
159
|
+
export function createComposerDraftStore({ storage, hostId, limits = {}, now, schedule } = {}) {
|
|
160
|
+
const bound = { ...COMPOSER_DRAFT_LIMITS, ...limits }
|
|
161
|
+
const clock = typeof now === 'function' ? now : () => Date.now()
|
|
162
|
+
const later = typeof schedule === 'function' ? schedule : run => setTimeout(run, bound.saveDelayMs)
|
|
163
|
+
const namespace = typeof hostId === 'string' && HOST_PATTERN.test(hostId) ? hostId : ''
|
|
164
|
+
if (!namespace) throw new Error('composer-draft: a store needs a persisted Host namespace')
|
|
165
|
+
const blobKey = composerDraftKey(namespace)
|
|
166
|
+
|
|
167
|
+
let state = { epoch: 0, directory: {}, sessions: {} }
|
|
168
|
+
let written = 0
|
|
169
|
+
let loaded = false
|
|
170
|
+
let scheduled = false
|
|
171
|
+
let dirty = false
|
|
172
|
+
|
|
173
|
+
const refuse = reason => ({ ok: false, reason })
|
|
174
|
+
|
|
175
|
+
const load = () => {
|
|
176
|
+
if (loaded) return
|
|
177
|
+
loaded = true
|
|
178
|
+
const blob = readBlob(storage, blobKey)
|
|
179
|
+
if (!blob) return
|
|
180
|
+
// Another Host's blob, or an older replay of this one, is not adopted.
|
|
181
|
+
if (blob.hostId !== namespace) return
|
|
182
|
+
if (!Number.isInteger(blob.epoch) || blob.epoch < written) return
|
|
183
|
+
state = {
|
|
184
|
+
epoch: blob.epoch,
|
|
185
|
+
directory: blob.directory && typeof blob.directory === 'object' ? blob.directory : {},
|
|
186
|
+
sessions: blob.sessions && typeof blob.sessions === 'object' ? blob.sessions : {},
|
|
187
|
+
}
|
|
188
|
+
written = blob.epoch
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const commit = () => {
|
|
192
|
+
if (!dirty) return
|
|
193
|
+
state.epoch += 1
|
|
194
|
+
if (writeBlob(storage, blobKey, { version: 1, hostId: namespace, epoch: state.epoch, directory: state.directory, sessions: state.sessions })) written = state.epoch
|
|
195
|
+
dirty = false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const soon = () => {
|
|
199
|
+
dirty = true
|
|
200
|
+
if (scheduled) return
|
|
201
|
+
scheduled = true
|
|
202
|
+
later(() => { scheduled = false; load(); commit() })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Drops drafts the Host no longer serves, then the oldest over budget. */
|
|
206
|
+
const prune = keep => {
|
|
207
|
+
if (keep !== undefined) {
|
|
208
|
+
const live = keep instanceof Set ? keep : new Set(keep ?? [])
|
|
209
|
+
for (const key of Object.keys(state.directory)) if (!live.has(key)) delete state.directory[key]
|
|
210
|
+
for (const key of Object.keys(state.sessions)) if (!live.has(key)) delete state.sessions[key]
|
|
211
|
+
}
|
|
212
|
+
const order = Object.entries(state.sessions).sort((left, right) => (left[1]?.updatedAt ?? 0) - (right[1]?.updatedAt ?? 0))
|
|
213
|
+
while (order.length > bound.maxSessions) {
|
|
214
|
+
const [key] = order.shift()
|
|
215
|
+
delete state.sessions[key]
|
|
216
|
+
}
|
|
217
|
+
for (const [key, row] of order) {
|
|
218
|
+
if (bytesOf(state.sessions) <= bound.maxTotalBytes) break
|
|
219
|
+
delete state.sessions[key]
|
|
220
|
+
}
|
|
221
|
+
const seen = Object.entries(state.directory).sort((left, right) => (left[1]?.seenAt ?? 0) - (right[1]?.seenAt ?? 0))
|
|
222
|
+
while (seen.length > bound.maxDirectory) delete state.directory[seen.shift()[0]]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The key of a conversation the saved Host directory knows, or null. A
|
|
227
|
+
* conversation the Host is not serving has no draft to read or write.
|
|
228
|
+
*/
|
|
229
|
+
const served = key => {
|
|
230
|
+
load()
|
|
231
|
+
if (!key || !state.directory[key]) return null
|
|
232
|
+
return key
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
hostId: namespace,
|
|
237
|
+
limits: bound,
|
|
238
|
+
|
|
239
|
+
/** Records that the Host surface is serving this conversation right now. */
|
|
240
|
+
see({ kind, ownerId, threadId, sessionId } = {}) {
|
|
241
|
+
const key = conversationKey({ kind, ownerId, threadId })
|
|
242
|
+
if (!key) return false
|
|
243
|
+
load()
|
|
244
|
+
const previous = state.directory[key] ?? {}
|
|
245
|
+
state.directory[key] = { sessionId: cleanId(sessionId) || previous.sessionId || '', seenAt: clock() }
|
|
246
|
+
prune()
|
|
247
|
+
soon()
|
|
248
|
+
return true
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/** Keeps only the conversations the Host still lists. */
|
|
252
|
+
reconcile(keys) {
|
|
253
|
+
load()
|
|
254
|
+
prune(keys)
|
|
255
|
+
soon()
|
|
256
|
+
return Object.keys(state.sessions).length
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/** The saved draft, or null. Never a draft from another Host. */
|
|
260
|
+
read({ kind, ownerId, threadId } = {}) {
|
|
261
|
+
const key = served(conversationKey({ kind, ownerId, threadId }))
|
|
262
|
+
if (key === null) return null
|
|
263
|
+
const row = state.sessions[key]
|
|
264
|
+
return row ? { ...row, key } : null
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/** Stores one draft. A stale revision is dropped, never written over. */
|
|
268
|
+
save({ kind, ownerId, threadId, sessionId, text, revision, pendingAt, pendingRequestId } = {}) {
|
|
269
|
+
const key = served(conversationKey({ kind, ownerId, threadId }))
|
|
270
|
+
if (key === null) return refuse('unknown-conversation')
|
|
271
|
+
const body = typeof text === 'string' ? text : ''
|
|
272
|
+
if (!body) return refuse('empty-draft')
|
|
273
|
+
if (body.length > bound.maxTextChars) return refuse('too-long')
|
|
274
|
+
const bytes = textBytes(body)
|
|
275
|
+
if (bytes > bound.maxTextBytes) return refuse('too-large')
|
|
276
|
+
const previous = state.sessions[key]
|
|
277
|
+
if (!Number.isInteger(revision) || revision <= (previous?.revision ?? 0)) return refuse('stale-revision')
|
|
278
|
+
const others = bytesOf(state.sessions) - (previous?.bytes ?? 0)
|
|
279
|
+
if (others + bytes > bound.maxTotalBytes) return refuse('over-budget')
|
|
280
|
+
state.sessions[key] = {
|
|
281
|
+
text: body, bytes, revision, sessionId: cleanId(sessionId), updatedAt: clock(),
|
|
282
|
+
// A send that took this text and has not been answered yet, so the next
|
|
283
|
+
// mount asks the witness before showing it back.
|
|
284
|
+
pendingAt: Number.isFinite(pendingAt) && pendingAt > 0 ? Math.floor(pendingAt) : 0,
|
|
285
|
+
pendingRequestId: typeof pendingRequestId === 'string' && pendingRequestId.length <= 256 ? pendingRequestId : '',
|
|
286
|
+
}
|
|
287
|
+
prune()
|
|
288
|
+
soon()
|
|
289
|
+
return { ok: true }
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
/** Drops one draft. Only a revision at least as new as the saved one may. */
|
|
293
|
+
clear({ kind, ownerId, threadId, revision } = {}) {
|
|
294
|
+
const key = served(conversationKey({ kind, ownerId, threadId }))
|
|
295
|
+
if (key === null) return refuse('unknown-conversation')
|
|
296
|
+
const previous = state.sessions[key]
|
|
297
|
+
if (!previous) return { ok: true }
|
|
298
|
+
if (!Number.isInteger(revision) || revision <= previous.revision) return refuse('stale-revision')
|
|
299
|
+
delete state.sessions[key]
|
|
300
|
+
soon()
|
|
301
|
+
return { ok: true }
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
/** Writes the pending change now, before a window goes away. */
|
|
305
|
+
flush() {
|
|
306
|
+
scheduled = false
|
|
307
|
+
load()
|
|
308
|
+
commit()
|
|
309
|
+
return { ok: true }
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
snapshot() {
|
|
313
|
+
load()
|
|
314
|
+
return {
|
|
315
|
+
hostId: namespace,
|
|
316
|
+
epoch: state.epoch,
|
|
317
|
+
sessions: Object.fromEntries(Object.entries(state.sessions).map(([key, row]) => [key, { ...row }])),
|
|
318
|
+
directory: Object.fromEntries(Object.entries(state.directory).map(([key, row]) => [key, { ...row }])),
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { ContactNavigation } from './chat-ui.jsx'
|
|
3
|
+
import { taskProgress } from './task-progress.mjs'
|
|
4
|
+
|
|
5
|
+
/** The contact footer reads the same committed task projection as the inbox. */
|
|
6
|
+
export function contactAttention(snapshot) {
|
|
7
|
+
if (snapshot?.schemaVersion !== 1 || snapshot.tasks?.status !== 'available') return { known: false, count: 0, partial: false }
|
|
8
|
+
return { known: true, count: taskProgress(snapshot).attention.length, partial: snapshot.tasks.truncated === true }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function notificationAttention(snapshot) {
|
|
12
|
+
const value = snapshot?.schemaVersion === 1 ? snapshot.notifications : null
|
|
13
|
+
if (value?.status !== 'available' || !Number.isSafeInteger(value.unreadCount) || value.unreadCount < 0) return { known: false, count: 0, partial: false }
|
|
14
|
+
return { known: true, count: value.unreadCount, partial: value.truncated === true }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read-only polling stops while hidden and retires every result from a replaced Host. */
|
|
18
|
+
export function ContactAttentionNavigation({ productApi, ...props }) {
|
|
19
|
+
const [state, setState] = useState(null)
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (typeof productApi?.attention !== 'function') return undefined
|
|
22
|
+
let live = true, epoch = 0, timer, request
|
|
23
|
+
const publish = value => { if (live) setState({ scope: productApi, ...value }) }
|
|
24
|
+
const pause = () => { epoch++; clearTimeout(timer); request?.abort(); request = undefined; publish({ value: null }) }
|
|
25
|
+
const load = async () => {
|
|
26
|
+
clearTimeout(timer)
|
|
27
|
+
if (!live || document.hidden) return
|
|
28
|
+
const current = ++epoch, controller = new AbortController()
|
|
29
|
+
request?.abort(); request = controller
|
|
30
|
+
const deadline = setTimeout(() => { controller.abort(); if (live && current === epoch) publish({ value: null }) }, 8000)
|
|
31
|
+
const valid = () => live && current === epoch && !controller.signal.aborted
|
|
32
|
+
let failed = false
|
|
33
|
+
try {
|
|
34
|
+
const value = await productApi.attention(controller.signal)
|
|
35
|
+
if (valid()) publish({ value })
|
|
36
|
+
} catch {
|
|
37
|
+
failed = true
|
|
38
|
+
if (live && current === epoch) publish({ value: null })
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(deadline)
|
|
41
|
+
if (request === controller) request = undefined
|
|
42
|
+
if (live && current === epoch && !document.hidden) timer = setTimeout(load, failed ? 15000 : 5000)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const visible = () => { if (document.hidden) pause(); else void load() }
|
|
46
|
+
document.addEventListener('visibilitychange', visible)
|
|
47
|
+
void load()
|
|
48
|
+
return () => { live = false; epoch++; clearTimeout(timer); request?.abort(); document.removeEventListener('visibilitychange', visible) }
|
|
49
|
+
}, [productApi])
|
|
50
|
+
const attention = contactAttention(state?.scope === productApi ? state.value : null)
|
|
51
|
+
const notifications = notificationAttention(state?.scope === productApi ? state.value : null)
|
|
52
|
+
return <ContactNavigation {...props} attentionCount={attention.count} attentionPartial={attention.partial} attentionKnown={attention.known}
|
|
53
|
+
notificationCount={notifications.count} notificationPartial={notifications.partial} notificationKnown={notifications.known}/>
|
|
54
|
+
}
|