@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
|
@@ -17,7 +17,39 @@
|
|
|
17
17
|
|
|
18
18
|
const hostOf = (url) => { try { return new URL(url).hostname.replace(/^www\./, '') } catch { return '' } }
|
|
19
19
|
const shortUrl = url => String(url ?? '').replace(/^https?:\/\//i, '').replace(/^www\./i, '').replace(/\/$/, '')
|
|
20
|
-
@@ -
|
|
20
|
+
@@ -194,13 +195,20 @@
|
|
21
|
+
|
|
22
|
+
export function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, actions, useSessions, useSessionStatus, useActivity, useWork, useBrowserTabs, useBrowserPages }) {
|
|
23
|
+
const owner = roster.byId[sessionId]
|
|
24
|
+
- const part = owner ? currentPart(owner) : sessionId
|
|
25
|
+
+ // A thread Session is its own part: the drawer shows that thread's
|
|
26
|
+
+ // transcript, stats, questions and usage, while everything Bot-scoped
|
|
27
|
+
+ // (settings, model, folder, memory, computer) stays the Bot's — a thread has
|
|
28
|
+
+ // no authority of its own.
|
|
29
|
+
+ const thread = roster.threadsById?.[sessionId]
|
|
30
|
+
+ const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
|
|
31
|
+
+ const part = thread ? (thread.sessionId ?? sessionId) : (owner ? currentPart(owner) : sessionId)
|
|
32
|
+
const isMainBot = isMainOf(roster, owner?.id)
|
|
33
|
+
const [tab, setTab] = useState('details')
|
|
34
|
+
const room = roster.roomsById[sessionId]
|
|
35
|
+
// The info pane swaps its overview for the Bot or group settings view in place.
|
|
36
|
+
const [view, setView] = useState(startTab === 'settings' && (owner || room) ? 'settings' : 'overview')
|
|
37
|
+
- const [editing, setEditing] = useState(Boolean(startRenaming && (roster.byId[sessionId] || roster.roomsById[sessionId])))
|
|
38
|
+
+ // A thread is named in the thread directory, never by renaming its Bot here.
|
|
39
|
+
+ const [editing, setEditing] = useState(Boolean(startRenaming && thread === undefined && (roster.byId[sessionId] || roster.roomsById[sessionId])))
|
|
40
|
+
const [draftName, setDraftName] = useState(roster.byId[sessionId]?.name ?? (roster.roomsById[sessionId]?.named ? roster.roomsById[sessionId].name : ''))
|
|
41
|
+
const [copied, setCopied] = useState(false)
|
|
42
|
+
const [error, setError] = useState('')
|
|
43
|
+
@@ -218,7 +226,7 @@
|
|
44
|
+
const running = useSessionStatus(map => map.get(part)?.running === true)
|
|
45
|
+
const doing = useActivity(map => map[bot?.id ?? sessionId])
|
|
46
|
+
const transcript = useSessions(list => transcriptOf(list, part, bot?.name ?? room?.name ?? t('Bot'), roster.exchangeTurns[part]))
|
|
47
|
+
- const usage = useSessions(list => usageOf(list, [part, ...(bot?.parts ?? room?.parts ?? [])]))
|
|
48
|
+
+ const usage = useSessions(list => usageOf(list, [part, ...(thread === undefined ? (bot?.parts ?? room?.parts ?? []) : [])]))
|
|
49
|
+
const run = promise => Promise.resolve(promise)
|
|
50
|
+
.then(() => { setError(''); return true })
|
|
51
|
+
.catch(failure => { setError(failure?.message ?? String(failure)); return false })
|
|
52
|
+
@@ -268,7 +276,7 @@
|
|
21
53
|
}
|
|
22
54
|
const subtitle = bot ? bot.role : roomSubtitle(room, roster)
|
|
23
55
|
// Tab sets: a 1:1 Bot has Details and Browser; a group has only Details, so no tab row.
|
|
@@ -26,7 +58,21 @@
|
|
|
26
58
|
return [h(Fragment, { key: 'top' }, top),
|
|
27
59
|
h('div', { key: 'id', className: 'bt-drawer-id', 'data-move': side },
|
|
28
60
|
bot ? h(BotAvatar, { bot, size: 64, state: botState(running, doing, roster.questions?.[part] !== undefined), main: isMainBot, badge: false, gaze: true, pokeable: true })
|
|
29
|
-
@@ -
|
|
61
|
+
@@ -280,8 +288,11 @@
|
|
62
|
+
onChange: event => setDraftName(event.target.value), onBlur: saveName,
|
|
63
|
+
onKeyDown: (event) => { if (event.key === 'Enter') void saveName(); if (event.key === 'Escape') { event.stopPropagation(); setEditing(false) } },
|
|
64
|
+
})
|
|
65
|
+
- : h('button', { type: 'button', className: 'bt-drawer-name', 'aria-label': t('Rename {name}', { name: current }), title: t('Rename'), onClick: () => { setDraftName(room && !room.named ? '' : current); setEditing(true) } }, current),
|
|
66
|
+
- subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null),
|
|
67
|
+
+ : thread !== undefined
|
|
68
|
+
+ ? h('span', { className: 'bt-drawer-name', 'data-thread': '' }, current)
|
|
69
|
+
+ : h('button', { type: 'button', className: 'bt-drawer-name', 'aria-label': t('Rename {name}', { name: current }), title: t('Rename'), onClick: () => { setDraftName(room && !room.named ? '' : current); setEditing(true) } }, current),
|
|
70
|
+
+ subtitle ? h('span', { className: 'bt-drawer-role' }, subtitle) : null,
|
|
71
|
+
+ threadName ? h('span', { className: 'bt-drawer-role', 'data-thread': threadName }, threadName) : null),
|
|
72
|
+
tabs.length > 1 ? h('div', { key: 'tabs', ref: glide.box, className: 'bt-tabs', role: 'tablist', 'data-move': side },
|
|
73
|
+
h('span', { ref: glide.thumb, className: 'bt-thumb', 'aria-hidden': true }),
|
|
74
|
+
tabs.map(key => h('button', { key, type: 'button', role: 'tab', className: 'bt-tab', 'aria-selected': tab === key, onClick: () => setTab(key) }, t(TAB_LABELS[key])))) : null,
|
|
75
|
+
@@ -306,6 +317,7 @@
|
|
30
76
|
h('div', { className: 'bt-kv' }, h('span', null, bot.name), h('span', null, usage))) : null,
|
|
31
77
|
h(ScheduleSummary, { key: 'routines', bot, actions }),
|
|
32
78
|
] : null,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
--- a/src/client/header.js
|
|
2
|
+
+++ b/src/client/header.js
|
|
3
|
+
@@ -72,6 +72,11 @@
|
|
4
|
+
const doing = useActivity(map => map[owner])
|
|
5
|
+
const bot = roster.byId[sessionId]
|
|
6
|
+
const room = roster.roomsById[sessionId]
|
|
7
|
+
+ // A thread is another own chat of this Bot: the header keeps naming the Bot,
|
|
8
|
+
+ // because that is who is speaking, and adds the thread so the owner can tell
|
|
9
|
+
+ // two contexts of the same Bot apart at a glance.
|
|
10
|
+
+ const thread = roster.threadsById?.[sessionId]
|
|
11
|
+
+ const threadName = typeof thread?.name === 'string' ? thread.name.trim() : ''
|
|
12
|
+
const goal = room?.goal
|
|
13
|
+
const goalCompose = useUi(state => state.goalCompose === sessionId)
|
|
14
|
+
const goalOpen = useUi(state => state.goalOpen === sessionId)
|
|
15
|
+
@@ -116,7 +121,7 @@
|
|
16
|
+
}, [showGoal])
|
|
17
|
+
const pill = h('button', {
|
|
18
|
+
type: 'button', className: 'bt-pill', 'aria-expanded': away ? undefined : detailsOpen, 'data-memory': memoryNotice ? 'updated' : undefined,
|
|
19
|
+
- 'aria-label': away ? t('Back to chat') : memoryNotice ? t('Memory updated. Open {name}\'s memory', { name: roster.byId[changed]?.name ?? name }) : t('View conversation details'),
|
|
20
|
+
+ 'aria-label': away ? t('Back to chat') : memoryNotice ? t('Memory updated. Open {name}\'s memory', { name: roster.byId[changed]?.name ?? name }) : threadName ? `${t('View conversation details')} · ${threadName}` : t('View conversation details'),
|
|
21
|
+
title: away ? name : undefined,
|
|
22
|
+
onClick: () => (away ? selectView('chat') : memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId)),
|
|
23
|
+
},
|
|
24
|
+
@@ -127,6 +132,7 @@
|
|
25
|
+
h('span', { className: 'bt-pill-goal' }, h('span', { className: 'bt-pill-ico', 'aria-hidden': true }, h(GoalIcon)), h('span', { className: 'bt-pill-title' }, view.title)),
|
|
26
|
+
h('span', { className: 'bt-pill-name' }, name))
|
|
27
|
+
: h('span', { className: 'bt-pill-name' }, away ? t('Back to chat') : bot?.name ?? room?.name ?? (title || t('New chat'))),
|
|
28
|
+
+ threadName ? h('span', { className: 'bt-swap' }, threadName) : null,
|
|
29
|
+
goalShown && view?.number !== undefined ? h('span', { className: 'bt-pill-count' }, `${view.number}/${view.count}`) : null,
|
|
30
|
+
h(MemoryNotice, { shown: memoryNotice }),
|
|
31
|
+
h('span', { className: 'bt-pill-go', 'aria-hidden': true }, h(GoArrow)))
|
|
32
|
+
@@ -134,7 +140,8 @@
|
|
33
|
+
showGoal ? h(GoalIsland, { room, goal, view, open: goalOpen, composing: goalCompose, roomWork, roster, actions, pill }) : pill,
|
|
34
|
+
h('span', { className: 'bt-sr', role: 'status' }, memoryNotice ? t('Memory updated') : ''),
|
|
35
|
+
h(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }),
|
|
36
|
+
- h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }))
|
|
37
|
+
+ h(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, useWork, actions }),
|
|
38
|
+
+ typeof actions.conversationAddon === 'function' ? h(actions.conversationAddon, { sessionId, bot, room, roster, actions, running }) : null)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// New-message pills. "N new messages ↓" takes Back to bottom's place when
|
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
--- a/src/client/index.js
|
|
2
2
|
+++ b/src/client/index.js
|
|
3
|
-
@@ -
|
|
3
|
+
@@ -199,7 +199,7 @@
|
|
4
|
+
for (const id of pendingAdopt.keys()) {
|
|
5
|
+
if (roster.getSnapshot().agentOf[id] !== undefined || listed[id] === undefined) pendingAdopt.delete(id)
|
|
6
|
+
}
|
|
7
|
+
- for (const id of [...value.bots.map(currentPart), ...value.rooms.map(room => room.sessionId ?? room.id)]) {
|
|
8
|
+
+ for (const id of [...value.bots.map(currentPart), ...value.rooms.map(room => room.sessionId ?? room.id), ...(value.bots ?? []).flatMap(bot => (bot.threads ?? []).map(thread => thread.sessionId))]) {
|
|
9
|
+
if (refreshed.has(id)) continue
|
|
10
|
+
refreshed.add(id)
|
|
11
|
+
void Promise.resolve(ctx.sessions.refreshProjections?.(id)).catch(quietly('refresh projections'))
|
|
12
|
+
@@ -407,6 +407,13 @@
|
|
13
|
+
// A Bot id or a group chat id, or any part of either's chat, opens the current part.
|
|
14
|
+
const partFor = (id) => {
|
|
15
|
+
const snapshot = roster.getSnapshot()
|
|
16
|
+
+ // A thread Session, or a thread id, opens its own Session: it is not the
|
|
17
|
+
+ // Bot's default chat, and opening it is never redirected there. The thread
|
|
18
|
+
+ // is resolved before the Bot, because a thread Session also resolves to its
|
|
19
|
+
+ // Bot in `byId` — every other Bot surface needs that, and this one line
|
|
20
|
+
+ // keeps the two from being confused.
|
|
21
|
+
+ const thread = snapshot.threadsById?.[id]
|
|
22
|
+
+ if (thread !== undefined) return thread.sessionId ?? id
|
|
23
|
+
const bot = snapshot.byId[id]
|
|
24
|
+
if (bot !== undefined) return currentPart(bot)
|
|
25
|
+
const room = snapshot.roomsById[id]
|
|
26
|
+
@@ -437,7 +444,7 @@
|
|
27
|
+
const snapshot = roster.getSnapshot()
|
|
28
|
+
if (!snapshot.ready || !snapshot.mainBotId) return
|
|
29
|
+
const main = mainView()
|
|
30
|
+
- if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined) {
|
|
31
|
+
+ if (main !== undefined && snapshot.byId[main] === undefined && snapshot.roomsById[main] === undefined && snapshot.threadsById?.[main] === undefined) {
|
|
32
|
+
actions.openSession(snapshot.mainBotId)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
@@ -1082,7 +1089,7 @@
|
|
4
36
|
let landed = false
|
|
5
37
|
let declared = true
|
|
6
38
|
try {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
--- a/src/client/learning-card.js
|
|
2
|
+
+++ b/src/client/learning-card.js
|
|
3
|
+
@@ -1,7 +1,7 @@
|
|
4
|
+
import { createElement as h, useId, useMemo, useRef, useState } from 'react'
|
|
5
|
+
import { useTurnFacts } from './btw.js'
|
|
6
|
+
import { editLines, lineDiff, proposalPlacement } from './learning-card-model.js'
|
|
7
|
+
-import { currentPart } from './text.js'
|
|
8
|
+
+import { isOwnChatOf } from './sources.js'
|
|
9
|
+
import { t } from './i18n.js'
|
|
10
|
+
import { CheckIcon, MemoryIcon, PencilIcon } from './icons.js'
|
|
11
|
+
import { spring, SPRING_TAP } from './motion.js'
|
|
12
|
+
@@ -106,7 +106,7 @@
|
|
13
|
+
const facts = useTurnFacts(sessionId, actions)
|
|
14
|
+
const [failed, setFailed] = useState({})
|
|
15
|
+
const bot = roster.byId?.[sessionId]
|
|
16
|
+
- if (!bot || currentPart(bot) !== sessionId) return null
|
|
17
|
+
+ if (!bot || !isOwnChatOf(bot, sessionId)) return null
|
|
18
|
+
const proposals = roster.learning?.[bot.id] ?? []
|
|
19
|
+
const visible = [...proposals]
|
|
20
|
+
for (const entry of Object.values(failed)) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
--- a/src/client/model-menu.js
|
|
2
|
+
+++ b/src/client/model-menu.js
|
|
3
|
+
@@ -1,6 +1,6 @@
|
|
4
|
+
import { createElement as h, Fragment, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
-import { currentPart } from './text.js'
|
|
7
|
+
+import { isOwnChatOf } from './sources.js'
|
|
8
|
+
import { CheckIcon, SlidersIcon } from './icons.js'
|
|
9
|
+
import { BotMark } from './mark.js'
|
|
10
|
+
import { KeyIcon, MaskedInput } from './secret-card.js'
|
|
11
|
+
@@ -202,9 +202,11 @@
|
|
12
|
+
const bot = roster.byId[sessionId]
|
|
13
|
+
const [busy, setBusy] = useState('')
|
|
14
|
+
const [error, setError] = useState('')
|
|
15
|
+
- const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready
|
|
16
|
+
+ // A thread is another own chat of the same Bot, so the Bot's model choice
|
|
17
|
+
+ // belongs to it exactly as it belongs to the default chat.
|
|
18
|
+
+ const shown = Boolean(bot) && isOwnChatOf(bot, sessionId) && roster.ready
|
|
19
|
+
useEffect(() => { if (shown) void actions.refreshModels?.() }, [shown, sessionId])
|
|
20
|
+
- if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null
|
|
21
|
+
+ if (!bot || !isOwnChatOf(bot, sessionId) || !roster.ready) return null
|
|
22
|
+
const models = roster.models ?? []
|
|
23
|
+
// A model whose provider has no API key fails every turn, so it is no choice.
|
|
24
|
+
const usable = models.filter(model => !model.needsKey)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
--- a/src/client/secret-card.js
|
|
2
|
+
+++ b/src/client/secret-card.js
|
|
3
|
+
@@ -5,11 +5,11 @@
|
|
4
|
+
import { createElement as h, useEffect, useId, useRef, useState } from 'react'
|
|
5
|
+
import { CheckIcon, CloseIcon, TrashIcon } from './icons.js'
|
|
6
|
+
import { BotAvatar, BotMark } from './mark.js'
|
|
7
|
+
-import { contentText, currentPart } from './text.js'
|
|
8
|
+
+import { contentText } from './text.js'
|
|
9
|
+
import { dateLocale, t } from './i18n.js'
|
|
10
|
+
import { SPRING_POP, SPRING_SHAPE, spring } from './motion.js'
|
|
11
|
+
import { useDockHold } from './card-hold.js'
|
|
12
|
+
-import { heldCardKey } from './sources.js'
|
|
13
|
+
+import { heldCardKey, isOwnChatOf } from './sources.js'
|
|
14
|
+
import { CellEmpty } from './cell-empty.js'
|
|
15
|
+
|
|
16
|
+
const MIN_VALUE = 4
|
|
17
|
+
@@ -178,7 +178,7 @@
|
|
18
|
+
const shown = useCards?.(state => state[sessionId]) ?? NONE
|
|
19
|
+
const held = useHeldCards?.(state => state[sessionId]) ?? NONE
|
|
20
|
+
const bot = roster.byId[sessionId]
|
|
21
|
+
- if (!bot || currentPart(bot) !== sessionId) return null
|
|
22
|
+
+ if (!bot || !isOwnChatOf(bot, sessionId)) return null
|
|
23
|
+
const drawn = entry => entry.callId && entry.sessionId === sessionId && shown[entry.callId] > 0 && !(held[heldCardKey('secret', entry.id)] > 0)
|
|
24
|
+
const waiting = (roster.secretRequests?.[bot.id] ?? []).filter(entry => entry.status === 'pending' && !drawn(entry))
|
|
25
|
+
if (waiting.length === 0) return null
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- a/src/client/sources.js
|
|
2
|
+
+++ b/src/client/sources.js
|
|
3
|
+
@@ -16,7 +16,7 @@
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
-export const EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, questions: {}, btwCards: {}, sideChats: {}, learning: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }
|
|
8
|
+
+export const EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], agents: [], exchangeTurns: {}, questions: {}, btwCards: {}, sideChats: {}, learning: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, threadsById: {}, ready: false }
|
|
9
|
+
// Any Main Bot holds the Main Bot powers; `mainBotId`, the first one, is the sidebar tile.
|
|
10
|
+
export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ?? []).includes(id)
|
|
11
|
+
|
|
12
|
+
@@ -28,6 +28,21 @@
|
|
13
|
+
for (const bot of value.bots) {
|
|
14
|
+
for (const id of [...(bot.parts ?? []), bot.sessionId ?? bot.id, bot.id]) byId[id] = bot
|
|
15
|
+
}
|
|
16
|
+
+ // A thread is another own chat of its Bot, and it is indexed twice on
|
|
17
|
+
+ // purpose. `threadsById` holds the thread itself — its own Session, its name,
|
|
18
|
+
+ // its archive — so opening one opens the thread and never the Bot's default
|
|
19
|
+
+ // chat. Its Session also resolves to its Bot in `byId`, so every surface that
|
|
20
|
+
+ // asks "whose chat is this?" (the header, the composer's extra menu, the
|
|
21
|
+
+ // details drawer, and with it the model, memory, computer, voice and
|
|
22
|
+
+ // attachment controls) keeps the Bot instead of going blank inside a thread.
|
|
23
|
+
+ // A thread never takes a Bot's own chat identity: a Session the Bot already
|
|
24
|
+
+ // owns stays the Bot's.
|
|
25
|
+
+ const threadsById = {}
|
|
26
|
+
+ for (const bot of value.bots) for (const thread of bot.threads ?? []) {
|
|
27
|
+
+ if (thread.sessionId !== undefined) threadsById[thread.sessionId] = { botId: bot.id, ...thread }
|
|
28
|
+
+ if (thread.threadId !== undefined) threadsById[thread.threadId] = { botId: bot.id, ...thread }
|
|
29
|
+
+ if (thread.sessionId !== undefined && byId[thread.sessionId] === undefined) byId[thread.sessionId] = bot
|
|
30
|
+
+ }
|
|
31
|
+
const roomsById = {}
|
|
32
|
+
// A group's own Session is a chain of parts like a Bot's chat: `room.id` is its
|
|
33
|
+
// first part's id and `sessionId` its current one; every part id finds the room.
|
|
34
|
+
@@ -41,9 +56,17 @@
|
|
35
|
+
agentsById[agent.id] = agent
|
|
36
|
+
for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id
|
|
37
|
+
}
|
|
38
|
+
- return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }
|
|
39
|
+
+ return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, threadsById, ready: true }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
+// Whether a Session is one of this Bot's own chats: its current part, or one of
|
|
43
|
+
+// its threads' Sessions. A thread is another own chat of the same Bot, so the
|
|
44
|
+
+// Bot-scoped surfaces (model choice, memory learning, secret requests, turn
|
|
45
|
+
+// errors) belong to it exactly as they belong to the default chat. Earlier
|
|
46
|
+
+// parts are deliberately not included: they keep the behaviour they had.
|
|
47
|
+
+export const isOwnChatOf = (bot, sessionId) => bot !== undefined && bot !== null && typeof sessionId === 'string'
|
|
48
|
+
+ && ((bot.sessionId ?? bot.id) === sessionId || (bot.threads ?? []).some(thread => thread.sessionId === sessionId))
|
|
49
|
+
+
|
|
50
|
+
// Whether a main-view Session id belongs to the team's DSH Agent: the conversation
|
|
51
|
+
// group then stays withdrawn while the shell keeps the sidebar. Before the roster
|
|
52
|
+
// arrives, the ids cached from the last pull answer.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
--- a/src/client/turn-error.js
|
|
2
|
+
+++ b/src/client/turn-error.js
|
|
3
|
+
@@ -1,9 +1,10 @@
|
|
4
|
+
import { createElement as h, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
5
|
+
-import { currentPart, isHiddenTurn } from './text.js'
|
|
6
|
+
+import { isHiddenTurn } from './text.js'
|
|
7
|
+
import { CheckIcon, CopyIcon } from './icons.js'
|
|
8
|
+
import { ModelKeyCard, manageModels } from './model-menu.js'
|
|
9
|
+
import { knownFailure } from './diagnostics.js'
|
|
10
|
+
import { hostText, t } from './i18n.js'
|
|
11
|
+
+import { isOwnChatOf } from './sources.js'
|
|
12
|
+
|
|
13
|
+
// -------------------------------------------------------------------------
|
|
14
|
+
// A failed turn in a Bot's chat: what went wrong and what the user can do about it,
|
|
15
|
+
@@ -157,7 +158,7 @@
|
|
16
|
+
const provider = blocked?.provider ?? providerOf(bot, roster)
|
|
17
|
+
const text = failureText(data.code, { name: bot?.name ?? t('This Bot'), provider: providerName(roster, provider), status: blocked?.status })
|
|
18
|
+
const facts = failureFacts(data, blocked)
|
|
19
|
+
- const current = bot !== undefined && currentPart(bot) === sessionId && !later
|
|
20
|
+
+ const current = bot !== undefined && isOwnChatOf(bot, sessionId) && !later
|
|
21
|
+
// The key route never takes the account's credential, so a sign-in alone leaves the Bot
|
|
22
|
+
// failing; it moves onto the account route. With no account model listed yet, the
|
|
23
|
+
// user picks one.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- a/src/host/channel.js
|
|
2
|
+
+++ b/src/host/channel.js
|
|
3
|
+
@@ -43,8 +43,16 @@
|
|
4
|
+
// `sessionId` is the Bot's current own-chat part; `parts` its earlier ones.
|
|
5
|
+
// `waiting`: the Bot holds its greeting until the user picks its model or fixes
|
|
6
|
+
// what failed it. `blocked`: the facts of a failure only the user can fix (turns.js).
|
|
7
|
+
- bots: Object.values(state.bots).map(({ segments, kickoff, ...bot }) => ({
|
|
8
|
+
- ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId), ...(kickoff ? { waiting: true } : {}),
|
|
9
|
+
+ bots: Object.values(state.bots).map(({ segments, threads, kickoff, ...bot }) => ({
|
|
10
|
+
+ ...bot, sessionId: rt.chatOf(bot.id), parts: (segments ?? []).map(segment => segment.sessionId),
|
|
11
|
+
+ // `sessionId` stays the default chat; `threads` are the Bot's other own
|
|
12
|
+
+ // chats, each with its own Session. An archived thread keeps its id and
|
|
13
|
+
+ // its history and opens read-only.
|
|
14
|
+
+ threads: (threads ?? []).map(thread => ({ threadId: thread.threadId, sessionId: thread.sessionId,
|
|
15
|
+
+ ...(thread.name === undefined ? {} : { name: thread.name }),
|
|
16
|
+
+ ...(thread.createdAt === undefined ? {} : { createdAt: thread.createdAt }),
|
|
17
|
+
+ ...(thread.archivedAt === undefined || thread.archivedAt === null ? {} : { archivedAt: thread.archivedAt }) })),
|
|
18
|
+
+ ...(kickoff ? { waiting: true } : {}),
|
|
19
|
+
})),
|
|
20
|
+
// `sessionId` is the group's own Session's current part, `ownParts` its earlier
|
|
21
|
+
// ones; `parts` the members' hidden group Sessions and their earlier segments,
|
|
22
|
+
@@ -435,6 +443,7 @@
|
|
23
|
+
await rt.load()
|
|
24
|
+
const past = rt.pastOwners.get(body.sessionId)
|
|
25
|
+
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
|
|
26
|
+
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
|
|
27
|
+
const question = rt.own(state.questions, sessionId)
|
|
28
|
+
if (question === undefined || question.id !== body.questionId) return fail('This question is no longer open', 'bot/stale')
|
|
29
|
+
const reply = questionReply(question, body.answer)
|
|
30
|
+
@@ -458,6 +467,7 @@
|
|
31
|
+
await rt.load()
|
|
32
|
+
const past = rt.pastOwners.get(body.sessionId)
|
|
33
|
+
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId) : past && past.roomId === undefined ? rt.chatOf(past.botId) : body.sessionId
|
|
34
|
+
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
|
|
35
|
+
const pending = rt.own(state.views, sessionId)
|
|
36
|
+
if (pending === undefined || pending.id !== body.viewId) return fail('This view is no longer open', 'bot/stale')
|
|
37
|
+
const checked = validateViewAnswer(pending.view, body.action, body.values)
|
|
38
|
+
@@ -485,6 +495,8 @@
|
|
39
|
+
// A Bot id, a group chat's id, or an earlier part of either means the
|
|
40
|
+
// current part.
|
|
41
|
+
const past = rt.pastOwners.get(body.sessionId)
|
|
42
|
+
+ // An archived thread keeps its history but takes no new input.
|
|
43
|
+
+ if (past?.threadId !== undefined) return fail('This thread is archived; open it again to write in it', 'bot/archived')
|
|
44
|
+
const room = rt.roomOf(body.sessionId)
|
|
45
|
+
const sessionId = rt.botOf(body.sessionId) ? rt.chatOf(body.sessionId)
|
|
46
|
+
: room !== undefined ? rt.sessionOfRoom(room)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
--- a/src/host/memory.js
|
|
2
|
+
+++ b/src/host/memory.js
|
|
3
|
+
@@ -866,7 +866,7 @@
|
|
4
|
+
// A deleted Bot's memory moves to the archive, whole.
|
|
5
|
+
function archiveBotMemory(botId) {
|
|
6
|
+
const bot = rt.botOf(botId)
|
|
7
|
+
- for (const sessionId of [botId, rt.chatOf(botId), ...(bot?.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(botId)]) renewMemory(sessionId)
|
|
8
|
+
+ for (const sessionId of rt.sessionsOfBot(botId)) renewMemory(sessionId)
|
|
9
|
+
summaries.delete(botId)
|
|
10
|
+
news.delete(botId)
|
|
11
|
+
return queued(async () => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
--- a/src/host/models.js
|
|
2
|
+
+++ b/src/host/models.js
|
|
3
|
+
@@ -244,6 +244,13 @@
|
|
4
|
+
await selectSessionModel(sessionId, route)
|
|
5
|
+
.catch(error => rt.warn('ds-bot: model for %s in a group not applied: %s', bot.name, error))
|
|
6
|
+
}
|
|
7
|
+
+ // A thread follows its Bot's model, as a group Session does.
|
|
8
|
+
+ for (const sessionId of rt.threadsOf(bot.id).map(thread => thread.sessionId)) {
|
|
9
|
+
+ const applied = sessionSelections.get(sessionId)
|
|
10
|
+
+ if (!force && applied !== undefined && sameSelection(applied, route)) continue
|
|
11
|
+
+ await selectSessionModel(sessionId, route)
|
|
12
|
+
+ .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
|
|
13
|
+
+ }
|
|
14
|
+
if (!ownChanged) return ref
|
|
15
|
+
bot.appliedModel = ref
|
|
16
|
+
if (effort === undefined) delete bot.appliedEffort
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- a/src/host/parts.js
|
|
2
|
+
+++ b/src/host/parts.js
|
|
3
|
+
@@ -446,6 +446,11 @@
|
|
4
|
+
const session = agent.session
|
|
5
|
+
const header = session.requestHeader()
|
|
6
|
+
const route = header?.config
|
|
7
|
+
if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
|
|
8
|
+
+ // The Host's prepared auxiliary stream binds one adapter generation for this note and
|
|
9
|
+
+ // admits the money against that same generation. A Host that has not wired it refuses
|
|
10
|
+
+ // the note here, rather than re-resolving the route at dispatch the way a bare stream
|
|
11
|
+
+ // does.
|
|
12
|
+
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
|
|
13
|
+
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
|
|
14
|
+
const messages = session.deriveMessages()
|
|
15
|
+
const base = folder ? FOLDER_PROMPT : HANDOFF_PROMPT
|
|
16
|
+
let retry
|
|
17
|
+
@@ -453,7 +458,7 @@
|
|
18
|
+
for (let attempt = 1; attempt <= NOTE_ATTEMPTS; attempt += 1) {
|
|
19
|
+
const prompt = retry === undefined ? base : `${base}\n\n${retry}`
|
|
20
|
+
const assembler = new BlockAssembler()
|
|
21
|
+
- for await (const chunk of ctx.llm.stream({
|
|
22
|
+
+ for await (const chunk of auxiliary.stream({
|
|
23
|
+
provider: route.provider,
|
|
24
|
+
model: route.model,
|
|
25
|
+
...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
|
|
26
|
+
@@ -515,6 +520,12 @@
|
|
27
|
+
async function writeCheckpoint(input, agent, signal) {
|
|
28
|
+
const session = agent.session
|
|
29
|
+
const route = session.requestHeader()?.config
|
|
30
|
+
if (!route?.provider || !route?.model) throw new Error('the Session has not called a model yet')
|
|
31
|
+
+ // The Host's prepared auxiliary stream binds one adapter generation for this checkpoint,
|
|
32
|
+
+ // so the summary is written by the generation the accounting admitted. A Host that has
|
|
33
|
+
+ // not wired it refuses the checkpoint instead of pricing one generation and streaming
|
|
34
|
+
+ // another.
|
|
35
|
+
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
|
|
36
|
+
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
|
|
37
|
+
// The results list grows with the work, so the checkpoint may write as much as one
|
|
38
|
+
// of the Bot's own replies.
|
|
39
|
+
const maxTokens = route.maxTokens
|
|
40
|
+
@@ -522,7 +531,7 @@
|
|
41
|
+
const tail = checkpointTail(head, session.deriveMessages())
|
|
42
|
+
const prompt = tail.length === 0 ? CHECKPOINT_PROMPT : `${CHECKPOINT_PROMPT}\n\n${checkpointTailNote(tail)}`
|
|
43
|
+
const assembler = new BlockAssembler()
|
|
44
|
+
- for await (const chunk of ctx.llm.stream({
|
|
45
|
+
+ for await (const chunk of auxiliary.stream({
|
|
46
|
+
provider: route.provider,
|
|
47
|
+
model: route.model,
|
|
48
|
+
...(route.reasoningEffort ? { reasoningEffort: route.reasoningEffort } : {}),
|
|
49
|
+
@@ -571,18 +582,19 @@
|
|
50
|
+
const lines = []
|
|
51
|
+
for await (const line of partLines(agent.id, AbortSignal.timeout(HANDOFF_READ_MS), { group })) {
|
|
52
|
+
lines.push({ who: line.who, text: line.who === 'you' || line.who === 'user' ? line.text.replace(/^(?:You|User): /, '') : line.text })
|
|
53
|
+
if (lines.length >= STEWARD_LINES) break
|
|
54
|
+
}
|
|
55
|
+
- const info = await ctx.llm.resolveModelInfo(route.provider, route.model).catch(() => undefined)
|
|
56
|
+
- // The decision needs no long thinking.
|
|
57
|
+
- const off = info?.reasoning?.efforts?.some(effort => effort.id === 'off')
|
|
58
|
+
+ // The decision needs no long thinking. Whether this model can be asked to think less
|
|
59
|
+
+ // is decided from the generation this very call prepares, not from a read of whatever
|
|
60
|
+
+ // registration happens to be current now.
|
|
61
|
+
+ const auxiliary = ctx.get('botAuxiliaryModelStream')
|
|
62
|
+
+ if (typeof auxiliary !== 'function') throw new Error('the Host has no prepared auxiliary model stream')
|
|
63
|
+
const assembler = new BlockAssembler()
|
|
64
|
+
- for await (const chunk of ctx.llm.stream({
|
|
65
|
+
+ for await (const chunk of auxiliary.stream({
|
|
66
|
+
provider: route.provider,
|
|
67
|
+
model: route.model,
|
|
68
|
+
- ...(off ? { reasoningEffort: 'off' } : {}),
|
|
69
|
+
system: STEWARD_SYSTEM,
|
|
70
|
+
messages: rt.tagUsage([{ role: 'user', content: [{ type: 'text', text: stewardRequest({ lines, midTurn, checkpoints: checkpointCount(session.deriveMessages()), group }) }] }], { sessionId: agent.id, kind: 'compaction' }),
|
|
71
|
+
maxTokens: STEWARD_MAX_TOKENS,
|
|
72
|
+
sessionId: session.id,
|
|
73
|
+
purpose: 'compaction',
|
|
74
|
+
signal: AbortSignal.timeout(STEWARD_TIMEOUT_MS),
|
|
75
|
+
- })) assembler.push(chunk)
|
|
76
|
+
+ }, { controls: auxiliary.reasoningOff })) assembler.push(chunk)
|
|
77
|
+
@@ -732,7 +743,10 @@
|
|
78
|
+
|
|
79
|
+
const placeOf = (sessionId) => {
|
|
80
|
+
const botId = rt.chatOwners.get(sessionId)
|
|
81
|
+
- if (botId !== undefined) return { botId }
|
|
82
|
+
+ // A thread is not a part chain: it keeps its own Session for as long as it
|
|
83
|
+
+ // lives, so the steward neither condenses it nor hands its input on to the
|
|
84
|
+
+ // Bot's default chat.
|
|
85
|
+
+ if (botId !== undefined) return rt.threadOf(sessionId) === undefined ? { botId } : undefined
|
|
86
|
+
const owner = rt.groupOwners.get(sessionId)
|
|
87
|
+
if (owner !== undefined) return { botId: owner.botId, roomId: owner.roomId }
|
|
88
|
+
@@ -1234,6 +1245,7 @@
|
|
89
|
+
// past it.
|
|
90
|
+
async function checkContext(agent) {
|
|
91
|
+
if (switches.has(agent.id) || freshParts.has(agent.id) || checking.has(agent.id) || readOnly()) return
|
|
92
|
+
+ if (placeOf(agent.id) === undefined) return
|
|
93
|
+
if (lastTurnEnd(agent)?.data?.reason?.kind === 'error') return
|
|
94
|
+
const lines = await contextLines(agent.session)
|
|
95
|
+
const tokens = tokensOf(agent.session)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
--- a/src/host/privacy.js
|
|
2
|
+
+++ b/src/host/privacy.js
|
|
3
|
+
@@ -30,7 +30,7 @@
|
|
4
|
+
const privateBots = () => Object.values(state.bots).filter(bot => bot.private === true)
|
|
5
|
+
// Deleting a private Bot does not make the files and memory it leaves behind public.
|
|
6
|
+
const protectedBots = () => [...new Map([...(state.privateArchives ?? []), ...privateBots()].map(bot => [bot.id, bot])).values()]
|
|
7
|
+
- const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.chatOf(bot.id), ...(bot.segments ?? []).map(segment => segment.sessionId), ...rt.groupSessionsOf(bot.id)])])]
|
|
8
|
+
+ const sessionsOf = bot => [...new Set([bot.id, ...(bot.sessionIds ?? [rt.sessionsOfBot(bot.id)])])]
|
|
9
|
+
const dataRoots = () => [rt.dshHome, rt.home, process.env.DS_BOT_USER_DSH_HOME?.trim()].filter(Boolean)
|
|
10
|
+
|
|
11
|
+
// A path as it is and as the file system resolves it, folded for comparing. realBeyond
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
--- a/src/host/roster.js
|
|
2
|
+
+++ b/src/host/roster.js
|
|
3
|
+
@@ -2,7 +2,7 @@
|
|
4
|
+
// the first Main Bot of a new team.
|
|
5
|
+
import { mkdir } from 'node:fs/promises'
|
|
6
|
+
import { MAX_AVATAR_IMAGE, ROOM_MAX_MEMBERS, ROOM_MODES, ROOM_NAME_MAX, ROOM_NOTICE_MAX } from './constants.js'
|
|
7
|
+
-import { currentColor, isColor, normalize, unquote } from './text.js'
|
|
8
|
+
+import { currentColor, isColor, normalize, splitRef, unquote } from './text.js'
|
|
9
|
+
import { nameProblem } from '../shared/mentions.js'
|
|
10
|
+
|
|
11
|
+
const BROWSER_GRANT_KINDS = new Set(['post', 'upload', 'download', 'delete', '*'])
|
|
12
|
+
@@ -420,6 +420,114 @@
|
|
13
|
+
return bot
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
+ // Trusted product-only profile edit. No notice is delivered, no model is selected,
|
|
17
|
+
+ // and no kickoff is released. The owner service supplies the final synchronous
|
|
18
|
+
+ // committed-state check; the live base is compared at the same mutation boundary.
|
|
19
|
+
+ async function updateBotProfile(id, patch, { base, check } = {}) {
|
|
20
|
+
+ const fail = (code, message) => { throw Object.assign(new Error(message), { code }) }
|
|
21
|
+
+ const fields = { name: 40, role: 24, instructions: 4000 }
|
|
22
|
+
+ const plain = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
23
|
+
+ if (!plain(patch) || !Object.keys(patch).length || Object.keys(patch).some(key => !Object.hasOwn(fields, key))
|
|
24
|
+
+ || !plain(base) || typeof check !== 'function') fail('BOT_PROFILE_INVALID', 'Invalid profile mutation')
|
|
25
|
+
+ const bot = rt.botOf(id)
|
|
26
|
+
+ if (!bot || state.bots[id] !== bot) fail('BOT_PROFILE_UNKNOWN_BOT', 'Unknown Bot')
|
|
27
|
+
+ if (rt.readOnly() !== undefined) fail('PRODUCT_READ_ONLY', rt.readOnly())
|
|
28
|
+
+ const next = {}
|
|
29
|
+
+ for (const field of Object.keys(patch)) {
|
|
30
|
+
+ if (typeof patch[field] !== 'string' || patch[field].trim().length > fields[field]) fail('BOT_PROFILE_INVALID', 'Invalid profile field')
|
|
31
|
+
+ if (typeof base[field] !== 'string' || (bot[field] ?? '') !== base[field]) fail('BOT_PROFILE_CONFLICT', 'Bot profile changed before commit')
|
|
32
|
+
+ next[field] = patch[field].trim()
|
|
33
|
+
+ }
|
|
34
|
+
+ if (next.name !== undefined) {
|
|
35
|
+
+ const clash = rt.findBot(next.name)
|
|
36
|
+
+ const pending = isHeld('bot', next.name) && normalize(next.name) !== normalize(bot.name)
|
|
37
|
+
+ const problem = next.name === bot.name ? undefined : nameProblem(next.name)
|
|
38
|
+
+ if (!next.name || (clash && clash.id !== id) || pending || problem !== undefined) fail('BOT_PROFILE_NAME_INVALID', 'Bot name is not available')
|
|
39
|
+
+ }
|
|
40
|
+
+ const checked = check(bot)
|
|
41
|
+
+ if (checked && typeof checked.then === 'function') fail('BOT_PROFILE_INVALID', 'Profile commit check must be synchronous')
|
|
42
|
+
+ // No await between the check and all three field assignments.
|
|
43
|
+
+ const renamed = next.name !== undefined && next.name !== bot.name
|
|
44
|
+
+ Object.assign(bot, next)
|
|
45
|
+
+ const rooms = renamed ? Object.values(state.rooms).filter(room => !room.named && room.members.includes(id)) : []
|
|
46
|
+
+ for (const room of rooms) room.name = autoRoomName(room.members)
|
|
47
|
+
+ await rt.save()
|
|
48
|
+
+ // Session titles are presentation. A failure cannot undo an already persisted
|
|
49
|
+
+ // profile or turn the successful save into a request the UI might retry.
|
|
50
|
+
+ if (renamed) {
|
|
51
|
+
+ try {
|
|
52
|
+
+ const agent = await rt.resolveAgent(rt.chatOf(id))
|
|
53
|
+
+ const current = rt.botOf(id)
|
|
54
|
+
+ if (current) await ctx.sessionTitle.rename(agent.session, current.name)
|
|
55
|
+
+ } catch (error) { rt.warn('ds-bot: profile session title not updated: %s', error?.message ?? error) }
|
|
56
|
+
+ for (const room of rooms) await retitleRoom(room)
|
|
57
|
+
+ }
|
|
58
|
+
+ return bot
|
|
59
|
+
+ }
|
|
60
|
+
+
|
|
61
|
+
+ // Trusted product-only thread records. A thread Session is made by the caller
|
|
62
|
+
+ // through rt.createSession — the same factory every own chat uses — and
|
|
63
|
+
+ // recorded here in one save, so the committed state the Host lifecycle
|
|
64
|
+
+ // compiles and the directory the product keeps can never disagree about who
|
|
65
|
+
+ // owns a Session. Nothing here delivers a notice, picks a model beyond the
|
|
66
|
+
+ // Bot's own, or starts a turn.
|
|
67
|
+
+ const THREAD_LIMIT = 64
|
|
68
|
+
+ const THREAD_ID = /^[A-Za-z0-9_-]{1,128}$/
|
|
69
|
+
+ const threadFail = (code, message) => { throw Object.assign(new Error(message), { code }) }
|
|
70
|
+
+ async function addThreadSession(botId, threadId, sessionId, { name } = {}) {
|
|
71
|
+
+ const readOnly = rt.readOnly()
|
|
72
|
+
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
|
|
73
|
+
+ const bot = rt.botOf(botId)
|
|
74
|
+
+ if (!bot || state.bots[botId] !== bot) threadFail('BOT_THREAD_UNKNOWN_BOT', 'Unknown Bot')
|
|
75
|
+
+ if (!THREAD_ID.test(threadId ?? '') || !THREAD_ID.test(sessionId ?? '')) threadFail('BOT_THREAD_INVALID', 'Invalid thread identity')
|
|
76
|
+
+ const threads = bot.threads ?? []
|
|
77
|
+
+ if (threads.length >= THREAD_LIMIT) threadFail('BOT_THREAD_LIMIT', 'Too many threads on one Bot')
|
|
78
|
+
+ if (threads.some(thread => thread.threadId === threadId)) threadFail('BOT_THREAD_KNOWN', 'That thread already has a Session')
|
|
79
|
+
+ for (const other of Object.values(state.bots)) {
|
|
80
|
+
+ for (const thread of other.threads ?? []) if (thread.sessionId === sessionId) threadFail('BOT_THREAD_SESSION_TAKEN', 'That Session already belongs to a thread')
|
|
81
|
+
+ }
|
|
82
|
+
+ const record = { threadId, sessionId, createdAt: Date.now(), ...(typeof name === 'string' && name.trim() ? { name: name.trim().slice(0, 80) } : {}) }
|
|
83
|
+
+ ;(bot.threads ??= []).push(record)
|
|
84
|
+
+ await rt.save()
|
|
85
|
+
+ // A thread runs on its Bot's model and permission preset, as a fresh part does.
|
|
86
|
+
+ const ref = await rt.desiredModel(bot)
|
|
87
|
+
+ if (ref !== undefined) {
|
|
88
|
+
+ const effort = await rt.desiredEffort(bot, ref)
|
|
89
|
+
+ await rt.selectSessionModel(sessionId, { ...splitRef(ref), ...(effort === undefined ? {} : { reasoningEffort: effort }) })
|
|
90
|
+
+ .catch(error => rt.warn('ds-bot: model for a thread of %s not applied: %s', bot.name, error))
|
|
91
|
+
+ }
|
|
92
|
+
+ await rt.applyPermission(bot, sessionId)
|
|
93
|
+
+ .catch(error => rt.warn('ds-bot: permission for a thread of %s not applied: %s', bot.name, error))
|
|
94
|
+
+ return { ...record }
|
|
95
|
+
+ }
|
|
96
|
+
+
|
|
97
|
+
+ async function renameThread(botId, threadId, name) {
|
|
98
|
+
+ const readOnly = rt.readOnly()
|
|
99
|
+
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
|
|
100
|
+
+ const bot = rt.botOf(botId)
|
|
101
|
+
+ const thread = bot?.threads?.find(entry => entry.threadId === threadId)
|
|
102
|
+
+ if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
|
|
103
|
+
+ if (typeof name !== 'string' || name.trim().length > 80) threadFail('BOT_THREAD_INVALID', 'Invalid thread name')
|
|
104
|
+
+ thread.name = name.trim()
|
|
105
|
+
+ await rt.save()
|
|
106
|
+
+ return { ...thread }
|
|
107
|
+
+ }
|
|
108
|
+
+
|
|
109
|
+
+ // Archive hides a thread and drops its authority in the same save: the
|
|
110
|
+
+ // Session and its history stay, and only an explicit restore brings it back.
|
|
111
|
+
+ async function setThreadArchived(botId, threadId, archivedAt) {
|
|
112
|
+
+ const readOnly = rt.readOnly()
|
|
113
|
+
+ if (readOnly !== undefined) threadFail('PRODUCT_READ_ONLY', readOnly)
|
|
114
|
+
+ const bot = rt.botOf(botId)
|
|
115
|
+
+ const thread = bot?.threads?.find(entry => entry.threadId === threadId)
|
|
116
|
+
+ if (!bot || !thread) threadFail('BOT_THREAD_UNKNOWN', 'Unknown thread')
|
|
117
|
+
+ if (archivedAt === undefined || archivedAt === null) delete thread.archivedAt
|
|
118
|
+
+ else if (Number.isFinite(archivedAt) && archivedAt >= 0) thread.archivedAt = archivedAt
|
|
119
|
+
+ else threadFail('BOT_THREAD_INVALID', 'Invalid archive stamp')
|
|
120
|
+
+ await rt.save()
|
|
121
|
+
+ return { ...thread }
|
|
122
|
+
+ }
|
|
123
|
+
+
|
|
124
|
+
async function setFlags(id, { pinned, hidden }) {
|
|
125
|
+
const entry = rt.botOf(id) ?? rt.roomOf(id) ?? rt.own(state.agents, id)
|
|
126
|
+
if (entry === undefined) throw new Error('Unknown conversation')
|
|
127
|
+
@@ -694,7 +802,7 @@
|
|
128
|
+
const archived = rt.archiveBotMemory(id)
|
|
129
|
+
const current = rt.chatOf(id)
|
|
130
|
+
// Questions and exchange turns are kept per Session, so every part of the chat is cleared.
|
|
131
|
+
- const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId)])
|
|
132
|
+
+ const parts = new Set([id, current, ...(bot.segments ?? []).map(segment => segment.sessionId), ...(bot.threads ?? []).map(thread => thread.sessionId)])
|
|
133
|
+
// `archiving` stands until the archive's path is known: the memory may already be
|
|
134
|
+
// there, and no region of its own closes it yet.
|
|
135
|
+
const closedArchive = bot.private === true ? {
|
|
136
|
+
@@ -769,7 +877,8 @@
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Object.assign(rt, {
|
|
140
|
+
- ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, setFlags,
|
|
141
|
+
+ ensureWorkspace, keepTeamFolder, createSession, createBot, duplicateBot, updateBot, updateBotProfile,
|
|
142
|
+
+ addThreadSession, renameThread, setThreadArchived, setFlags,
|
|
143
|
+
roomBusy, autoRoomName, retitleRoom, createRoom, updateRoom, deleteRoom,
|
|
144
|
+
setMain, deleteBot, bootstrap, releaseKickoff, greetingOut, holdKickoffAgain, userWrote, userAnswered,
|
|
145
|
+
})
|