diff --git a/core/client.js b/core/client.js index e007c4a72..0fa96e30b 100644 --- a/core/client.js +++ b/core/client.js @@ -1765,6 +1765,19 @@ function mentionTarget(event) { const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null; return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null; } +function nameMentions(text, roster) { + const value = String(text ?? ""); + if (!value.includes("@") || roster === null || typeof roster !== "object") return value; + const names = /* @__PURE__ */ new Map(); + for (const record of [...Array.isArray(roster.remoteBots) ? roster.remoteBots : [], ...Array.isArray(roster.bots) ? roster.bots : []]) { + const handle = typeof record?.remote === "object" && record.remote !== null ? record.remote.handle : record?.handle; + if (typeof handle === "string" && typeof record?.name === "string" && record.name !== "") names.set(handle.replace(/^@+/, "").toLowerCase(), record.name); + } + const handles = [...names.keys()].filter(Boolean).sort((a, b) => b.length - a.length); + if (handles.length === 0) return value; + const source = handles.map((handle) => handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); + return value.replace(new RegExp(`(^|[^a-z0-9_./])@(${source})(?![a-z0-9_\\-/\\\\])(?!\\.[a-z0-9_-])`, "gi"), (_all, before, handle) => `${before}@${names.get(handle.toLowerCase())}`); +} function clock(ms) { return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); } @@ -5138,7 +5151,7 @@ var workLine = (lead, tool, more) => (0, import_react9.createElement)(Shimmer, n function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id; const part = currentPart(bot); - const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); + const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part]), roster)); const running = useSessionStatus((map) => map.get(part)?.running === true); const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; @@ -5214,8 +5227,8 @@ function RemoteMark({ remote }) { } function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { const id = room2.id; - const fallback = useSessions((list) => previewOf(list, id)); - const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; + const fallback = useSessions((list) => nameMentions(previewOf(list, id), roster)); + const preview = nameMentions(room2.preview?.replace(/\s+/g, " ").trim(), roster) || fallback; const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; useMarkedUnread(id, active, marked, actions); @@ -13559,8 +13572,8 @@ function contentParts(content) { } function mentionableBots(roster, sessionId) { const room2 = roster.roomsById[sessionId]; - if (room2) return room2.members.map((id) => roster.byId[id]).filter(Boolean); - return roster.byId[sessionId] ? roster.bots : []; + const bots = room2 ? room2.members.map((id) => roster.byId[id]).filter(Boolean) : roster.byId[sessionId] ? roster.bots : []; + return bots.concat(bots.filter((bot) => typeof bot?.remote?.handle === "string").map((bot) => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ""), mentionLabel: bot.name }))); } function userTextParts(text, bots, referenceLabels = []) { const parts = []; @@ -13579,7 +13592,7 @@ function UserText({ text, bots, referenceLabels, skillNames, references, actions "button", { key: index, type: "button", className: "bt-umention", title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) }, (0, import_react35.createElement)(BotMark, { bot: part.bot, size: 16 }), - part.label + part.bot.mentionLabel ?? part.label ) : (0, import_react35.createElement)(import_react35.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references))); } function Attachments({ attachments, renderMessageImages }) { diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js index dd160bc72..0fcd0d7b5 100644 --- a/core/src/client/sidebar.js +++ b/core/src/client/sidebar.js @@ -3,7 +3,7 @@ import { Component, createElement as h, Fragment, useEffect, useLayoutEffect, us import { createPortal } from 'react-dom' import { FishLogo } from '@deepseek-ai/dsh-client-ui-primitives' import { isMainOf } from './sources.js' -import { currentPart, previewOf, relativeShort } from './text.js' +import { currentPart, nameMentions, previewOf, relativeShort } from './text.js' import { PlusIcon, SearchIcon, GaugeIcon, PlugIcon, ClockIcon, GearIcon, PinMarkIcon, ConnectorIcon, SwitchIcon, CloudDownloadIcon } from './icons.js' import { MAIN_LOOK } from './characters.js' import { botState, BotMark, BotAvatar, RoomAvatar } from './mark.js' @@ -41,7 +41,7 @@ const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id const part = currentPart(bot) - const preview = useSessions(list => previewOf(list, part, roster.exchangeTurns[part])) + const preview = useSessions(list => nameMentions(previewOf(list, part, roster.exchangeTurns[part]), roster)) const running = useSessionStatus(map => map.get(part)?.running === true) const active = useSessionRetainInfo(part, info => (info?.retainedBy?.mainView ?? 0) > 0) const unread = useSessionStatus(map => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== undefined) || (marked && !active) @@ -93,8 +93,8 @@ function RemoteMark({ remote }) { function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { const id = room.id - const fallback = useSessions(list => previewOf(list, id)) - const preview = room.preview?.replace(/\s+/g, ' ').trim() || fallback + const fallback = useSessions(list => nameMentions(previewOf(list, id), roster)) + const preview = nameMentions(room.preview?.replace(/\s+/g, ' ').trim(), roster) || fallback const active = useSessionRetainInfo(id, info => (info?.retainedBy?.mainView ?? 0) > 0) const unread = useSessionStatus(map => map.get(id)?.completionUnread === true) || (marked && !active) useMarkedUnread(id, active, marked, actions) diff --git a/core/src/client/text.js b/core/src/client/text.js index be4150010..d45cfd895 100644 --- a/core/src/client/text.js +++ b/core/src/client/text.js @@ -25,6 +25,25 @@ export function mentionTarget(event) { return anchor ? anchor.getAttribute('href').slice(MENTION_ORIGIN.length) : null } +// A friend's remote Bot is stored and delivered by its handle ("@bot-x.home": what the +// @ menu inserts, what the host matches); the user reads its display name. Previews +// rewrite the handle from the local roster, never from the text, so a remote name +// reaches the UI only through the sanitized roster record. One pass over the text: a +// name that itself contains a handle is never rescanned. +export function nameMentions(text, roster) { + const value = String(text ?? '') + if (!value.includes('@') || roster === null || typeof roster !== 'object') return value + const names = new Map() + for (const record of [...(Array.isArray(roster.remoteBots) ? roster.remoteBots : []), ...(Array.isArray(roster.bots) ? roster.bots : [])]) { + const handle = typeof record?.remote === 'object' && record.remote !== null ? record.remote.handle : record?.handle + if (typeof handle === 'string' && typeof record?.name === 'string' && record.name !== '') names.set(handle.replace(/^@+/, '').toLowerCase(), record.name) + } + const handles = [...names.keys()].filter(Boolean).sort((a, b) => b.length - a.length) + if (handles.length === 0) return value + const source = handles.map(handle => handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + return value.replace(new RegExp(`(^|[^a-z0-9_./])@(${source})(?![a-z0-9_\\-/\\\\])(?!\\.[a-z0-9_-])`, 'gi'), (_all, before, handle) => `${before}@${names.get(handle.toLowerCase())}`) +} + function clock(ms) { return new Date(ms).toLocaleTimeString(dateLocale(), { hour: 'numeric', minute: '2-digit' }) } diff --git a/core/src/client/user-message.js b/core/src/client/user-message.js index 54aa8ac52..f04bc8ae5 100644 --- a/core/src/client/user-message.js +++ b/core/src/client/user-message.js @@ -28,10 +28,15 @@ export function contentParts(content) { } // In a group only its members are called; elsewhere a mention opens that Bot's chat. +// A friend's remote Bot is also called by its handle — the @ menu inserts "@" +// and the host delivers on it — so the handle joins as a second name of the same Bot, +// and the chip reads the display name (mentionLabel) from the roster. export function mentionableBots(roster, sessionId) { const room = roster.roomsById[sessionId] - if (room) return room.members.map(id => roster.byId[id]).filter(Boolean) - return roster.byId[sessionId] ? roster.bots : [] + const bots = room ? room.members.map(id => roster.byId[id]).filter(Boolean) + : roster.byId[sessionId] ? roster.bots : [] + return bots.concat(bots.filter(bot => typeof bot?.remote?.handle === 'string') + .map(bot => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ''), mentionLabel: bot.name }))) } // Runs of plain text and Bot mentions, in order. @@ -51,7 +56,7 @@ export function userTextParts(text, bots, referenceLabels = []) { function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { return userTextParts(text, bots, referenceLabels).map((part, index) => (part.bot ? h('button', { key: index, type: 'button', className: 'bt-umention', title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) }, - h(BotMark, { bot: part.bot, size: 16 }), part.label) + h(BotMark, { bot: part.bot, size: 16 }), part.bot.mentionLabel ?? part.label) : h(Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, 'skill', references)))) } diff --git a/core/test/inbound-ui.test.js b/core/test/inbound-ui.test.js index 7734f24c2..e09b35e52 100644 --- a/core/test/inbound-ui.test.js +++ b/core/test/inbound-ui.test.js @@ -7,7 +7,7 @@ import { build } from 'esbuild' // The inbound friend conversation's display: trace lines, the thinking row, the remote speaker. const { outputFiles } = await build({ stdin: { - contents: "export { stripTrace, previewOf } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'", + contents: "export { stripTrace, previewOf, nameMentions } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'", resolveDir: fileURLToPath(new URL('..', import.meta.url)), }, bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', @@ -25,7 +25,7 @@ runInNewContext(outputFiles[0].text, { throw new Error(`Unexpected platform module ${name}`) }, }) -const { stripTrace, previewOf, remoteSpeaker } = module.exports +const { stripTrace, previewOf, nameMentions, remoteSpeaker } = module.exports test('a reply opening with a delegation trace shows without it', () => { assert.equal(stripTrace('Chief → 助手\n\nHi Chief! Good to hear from you.'), 'Hi Chief! Good to hear from you.') @@ -62,3 +62,22 @@ test("an inbound friend's message uses the friend's sidebar Bot (avatar, name), assert.notEqual(other.bot.color, 'gray') assert.equal(other.chatId, undefined) }) + +test("a friend Bot's handle in a preview reads as its display name (#71)", () => { + const roster = { bots: [{ id: 'f', name: '代码审查官', remote: { handle: '@bot-2a758c0f.danyang.home' } }], remoteBots: [{ handle: '@bot-2a758c0f.danyang.home', name: '代码审查官' }] } + assert.equal(nameMentions('@bot-2a758c0f.danyang.home 你是?', roster), '@代码审查官 你是?') + assert.equal(nameMentions('看 @bot-2a758c0f.danyang.home。', roster), '看 @代码审查官。') + // An address before the @, or a longer word after the handle, stays as written. + assert.equal(nameMentions('mail a@bot-2a758c0f.danyang.home', roster), 'mail a@bot-2a758c0f.danyang.home') + assert.equal(nameMentions('@bot-2a758c0f.danyang.home2 不在', roster), '@bot-2a758c0f.danyang.home2 不在') + // Unknown handles, missing rosters, and non-string input pass through. + assert.equal(nameMentions('@bot-x.home 你在?', roster), '@bot-x.home 你在?') + assert.equal(nameMentions('@bot-2a758c0f.danyang.home', undefined), '@bot-2a758c0f.danyang.home') + assert.equal(nameMentions(undefined, roster), '') + // One pass: a name that itself contains another handle is not rescanned. + const nested = { bots: [], remoteBots: [ + { handle: '@b.x.home', name: '请找 @a.x.home 的主人' }, + { handle: '@a.x.home', name: '代码审查官' }, + ] } + assert.equal(nameMentions('@b.x.home 你在?', nested), '@请找 @a.x.home 的主人 你在?') +}) diff --git a/core/test/mention-rule.test.js b/core/test/mention-rule.test.js index 0e0a1a221..eb3768983 100644 --- a/core/test/mention-rule.test.js +++ b/core/test/mention-rule.test.js @@ -83,6 +83,21 @@ test('the user bubble keeps the text around a mention, and a group only calls it assert.equal(canDrawUserMessages(), false) }) +test("a friend's Bot answers its handle too, and the chip reads its display name (#71)", () => { + const remote = { id: 'f', name: '代码审查官', remote: { handle: '@bot-2a758c0f.danyang.home', home: '@danyang.home' } } + const members = mentionableBots({ bots: [remote], byId: { f: remote }, roomsById: { r: { members: ['f'] } } }, 'r') + // The @ menu inserts "@" and the host delivers on it, so the bubble chips it + // like a name mention, under the display name from the roster. + const parts = userTextParts('@bot-2a758c0f.danyang.home 你是?', members) + assert.deepEqual(plain(parts.filter(part => part.bot).map(part => part.bot.id)), ['f']) + assert.equal(parts[0].bot.mentionLabel, '代码审查官') + assert.deepEqual(plain(parts[1]), { text: ' 你是?' }) + // The display-name form still matches the Bot itself, and an address before @ stays text. + const named = userTextParts('@代码审查官 你是谁', members) + assert.deepEqual(plain(named.filter(part => part.bot).map(part => [part.bot.id, part.label])), [['f', '代码审查官']]) + assert.deepEqual(plain(userTextParts('mail f@bot-2a758c0f.danyang.home', members).filter(part => part.bot)), []) +}) + test('a Bot name is something "@" can call without a second reading', () => { assert.equal(nameProblem('新 Bot'), undefined) assert.equal(nameProblem(' '), 'A Bot needs a name') diff --git a/docs/subsystems/ds-bot-core.md b/docs/subsystems/ds-bot-core.md index a9aec2186..8e4c3114a 100644 --- a/docs/subsystems/ds-bot-core.md +++ b/docs/subsystems/ds-bot-core.md @@ -59,6 +59,7 @@ Patched upstream `ds-bot` that ships as `core/`. Never edit `ds-bot/` by hand; c 5. The saved team (`state.json`) is versioned. Version 2 (#47) carries the identity schema — `members` records, a per-Bot `ownerId`, the owner's `designatedChiefId`. A legacy version-1 file is migrated on load with a one-time `state.json.v1` backup; an existing backup counts only when it is a readable file holding a parseable version-1 state, and anything else (or any malformed ownership) fails closed (read-only). An older binary refuses a newer version read-only. This is schema foundation only — team mode stays disabled, see [team](team.md). On top of it, member auth (#48) adds host module `src/host/member-auth.js`, installed after `store` in `MODULES` (`core/index.js`): `invitations` and `authSessions` maps live on state (no `STATE_VERSION` bump), the channel resolves the actor from the `x-flybot-member-session` header, and endpoints are `auth-invite` / `auth-redeem` / `auth-revoke` / `auth-whoami` / `auth-remove-member` / `auth-revoke-invite` with `body.actorId` ignored; `auth-invite` alone also opens for an anonymous caller on a host with no sessions yet (bootstrap first invite), `auth-revoke-invite` kills an unredeemed invitation (redeeming it afterwards fails with `revoked`), `removeMember` refuses a member that still owns a Bot (`has_bots`), and the auth mutations are serialized behind an in-process lock so concurrent `removeMember` calls cannot both pass the last-owner check. Sign-in surface only — still not team mode; details in [team](team.md). 6. Host diagnostics never depend on `ctx.logger`: DSH may export no logger, and a test or plugin-local host may carry one without `warn`. `rt.warn` still writes stderr and the diagnostics log in that case (`src/host/diagnostics.js`). 7. `remoteTurn` (`src/host/remote-chat.js`) finds what the user typed on a friend's remote chat by skipping host-appended `role:'user'` context: forms `snapshot`, `instructions` (the AGENTS.md baseline on a chat's first turn) and `notice`, and the time-context kinds. Match by form, not by a whitelist of senders, or the first message of a new chat is dropped (#66). +8. A friend's remote Bot is stored and delivered by its handle (`@.` — what the @ menu inserts and `sendUserMentions` matches). Where the user reads it — mention chips (`src/client/user-message.js`) and sidebar previews (`nameMentions` in `src/client/text.js`) — the client rewrites the handle to the display name taken from the local roster (`roster.bots[].remote`, `roster.remoteBots`), never from message text, so a remote-controlled name reaches the UI only through the sanitized roster records (#71; display names, #9). ## Tests diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index 790eaef78..156d5a225 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -92,10 +92,36 @@ index 28cd67da0010775b593175fc61404750e4730f21..778ff0445d3a194e0663c2bbbe6a6bb2 // its account plugin (the Desktop); the API key and custom models use the shell's // own editors. diff --git a/src/client/text.js b/src/client/text.js -index d21f4034624b4115bb0aede4ba95b761fc4a61be..be41500104d2c5941a3eb4dfa3d036e70955b33b 100644 +index d21f4034624b4115bb0aede4ba95b761fc4a61be..d45cfd895d488b98bafe7b9fe23ea123919f9bac 100644 --- a/src/client/text.js +++ b/src/client/text.js -@@ -62,6 +62,15 @@ export function reminderPrompts(text) { +@@ -25,6 +25,25 @@ export function mentionTarget(event) { + return anchor ? anchor.getAttribute('href').slice(MENTION_ORIGIN.length) : null + } + ++// A friend's remote Bot is stored and delivered by its handle ("@bot-x.home": what the ++// @ menu inserts, what the host matches); the user reads its display name. Previews ++// rewrite the handle from the local roster, never from the text, so a remote name ++// reaches the UI only through the sanitized roster record. One pass over the text: a ++// name that itself contains a handle is never rescanned. ++export function nameMentions(text, roster) { ++ const value = String(text ?? '') ++ if (!value.includes('@') || roster === null || typeof roster !== 'object') return value ++ const names = new Map() ++ for (const record of [...(Array.isArray(roster.remoteBots) ? roster.remoteBots : []), ...(Array.isArray(roster.bots) ? roster.bots : [])]) { ++ const handle = typeof record?.remote === 'object' && record.remote !== null ? record.remote.handle : record?.handle ++ if (typeof handle === 'string' && typeof record?.name === 'string' && record.name !== '') names.set(handle.replace(/^@+/, '').toLowerCase(), record.name) ++ } ++ const handles = [...names.keys()].filter(Boolean).sort((a, b) => b.length - a.length) ++ if (handles.length === 0) return value ++ const source = handles.map(handle => handle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') ++ return value.replace(new RegExp(`(^|[^a-z0-9_./])@(${source})(?![a-z0-9_\\-/\\\\])(?!\\.[a-z0-9_-])`, 'gi'), (_all, before, handle) => `${before}@${names.get(handle.toLowerCase())}`) ++} ++ + function clock(ms) { + return new Date(ms).toLocaleTimeString(dateLocale(), { hour: 'numeric', minute: '2-digit' }) + } +@@ -62,6 +81,15 @@ export function reminderPrompts(text) { } } export const stripHeader = text => text.replace(HEADER, '') @@ -111,7 +137,7 @@ index d21f4034624b4115bb0aede4ba95b761fc4a61be..be41500104d2c5941a3eb4dfa3d036e7 // Of the Bots in a conversation, the one whose memory changed last, by the host's // clock: changes reaching one window in the same poll share a `seen` time. -@@ -207,7 +216,8 @@ export function previewOf(list, id, hiddenTurns = []) { +@@ -207,7 +235,8 @@ export function previewOf(list, id, hiddenTurns = []) { const entry = outline[index] if (hiddenTurns.includes(entry.turn)) continue const text = entry.response || entry.prompt || '' diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index 4d8e027c5..a3081dee5 100644 --- a/patches/ds-bot/0003-flybot-api-client.patch +++ b/patches/ds-bot/0003-flybot-api-client.patch @@ -1,4 +1,4 @@ -FlyBot patch 0003-flybot-api-client on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; 12 file(s). +FlyBot patch 0003-flybot-api-client on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; 14 file(s). diff --git a/src/client/at-menu.js b/src/client/at-menu.js index 64d6cf053e838145eec326f1acf7cd744db9a990..01a5167b705b9e5579c1709c698b9fb4ff3feeb3 100644 @@ -641,17 +641,22 @@ index 203345b25d20a3635d2d9b157ef29bd2aaa46766..abe9c5a310250d2fd7a0c2dd866cef6f const installConversation = () => [ installCompiledStyles(CONVERSATION_CSS, 'conversation'), diff --git a/src/client/sidebar.js b/src/client/sidebar.js -index 2b3ea08fa8dc74de8166c07c3b4f704d6c120f10..dd160bc724c4a8030b0fca442819b01b1754919b 100644 +index 2b3ea08fa8dc74de8166c07c3b4f704d6c120f10..0fcd0d7b5d0628a81c00c5e9a8fcb0d012d08374 100644 --- a/src/client/sidebar.js +++ b/src/client/sidebar.js -@@ -1,4 +1,5 @@ +@@ -1,8 +1,9 @@ -import { createElement as h, Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { FlyBotSections, useSidebarStructure, botTree } from './flybot-slots.js' +import { Component, createElement as h, Fragment, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { FishLogo } from '@deepseek-ai/dsh-client-ui-primitives' import { isMainOf } from './sources.js' -@@ -37,7 +38,7 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { +-import { currentPart, previewOf, relativeShort } from './text.js' ++import { currentPart, nameMentions, previewOf, relativeShort } from './text.js' + import { PlusIcon, SearchIcon, GaugeIcon, PlugIcon, ClockIcon, GearIcon, PinMarkIcon, ConnectorIcon, SwitchIcon, CloudDownloadIcon } from './icons.js' + import { MAIN_LOOK } from './characters.js' + import { botState, BotMark, BotAvatar, RoomAvatar } from './mark.js' +@@ -37,16 +38,16 @@ const workKeys = text => (text === '' ? [] : text.split(',').map((key) => { })) const workLine = (lead, tool, more) => h(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ''}`) @@ -659,8 +664,10 @@ index 2b3ea08fa8dc74de8166c07c3b4f704d6c120f10..dd160bc724c4a8030b0fca442819b01b +function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id const part = currentPart(bot) - const preview = useSessions(list => previewOf(list, part, roster.exchangeTurns[part])) -@@ -46,7 +47,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +- const preview = useSessions(list => previewOf(list, part, roster.exchangeTurns[part])) ++ const preview = useSessions(list => nameMentions(previewOf(list, part, roster.exchangeTurns[part]), roster)) + const running = useSessionStatus(map => map.get(part)?.running === true) + const active = useSessionRetainInfo(part, info => (info?.retainedBy?.mainView ?? 0) > 0) const unread = useSessionStatus(map => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== undefined) || (marked && !active) useMarkedUnread(id, active, marked, actions) const main = isMainOf(roster, id) @@ -669,7 +676,7 @@ index 2b3ea08fa8dc74de8166c07c3b4f704d6c120f10..dd160bc724c4a8030b0fca442819b01b const approval = useApproval(useWork, entry => entry.botId === id) const alert = roster.questions?.[part] !== undefined || approval const doing = useActivity(map => map[id]) -@@ -78,11 +79,18 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -78,15 +79,22 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio h(BotAvatar, { bot, size: 36, state, main }), pinned && !wide ? h(PinMark) : null, wide ? h('span', { className: 'bt-row-body' }, @@ -688,7 +695,13 @@ index 2b3ea08fa8dc74de8166c07c3b4f704d6c120f10..dd160bc724c4a8030b0fca442819b01b + function RoomRow({ room, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { const id = room.id - const fallback = useSessions(list => previewOf(list, id)) +- const fallback = useSessions(list => previewOf(list, id)) +- const preview = room.preview?.replace(/\s+/g, ' ').trim() || fallback ++ const fallback = useSessions(list => nameMentions(previewOf(list, id), roster)) ++ const preview = nameMentions(room.preview?.replace(/\s+/g, ' ').trim(), roster) || fallback + const active = useSessionRetainInfo(id, info => (info?.retainedBy?.mainView ?? 0) > 0) + const unread = useSessionStatus(map => map.get(id)?.completionUnread === true) || (marked && !active) + useMarkedUnread(id, active, marked, actions) @@ -235,6 +243,30 @@ function useListGlide(listRef, layout, key) { }, [layout, key]) } @@ -844,6 +857,37 @@ index b538b95d77bf82d90c8ddde2339769f99f0fdedd..3e26b228a52035ea9af39d69a99678f8 .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} .bt-event>span{padding:0 2px} +diff --git a/src/client/user-message.js b/src/client/user-message.js +index 54aa8ac5221919a1ba294b08222e69ca2d377ed4..f04bc8ae59aaff480872f53829c411e06c2bc415 100644 +--- a/src/client/user-message.js ++++ b/src/client/user-message.js +@@ -28,10 +28,15 @@ export function contentParts(content) { + } + + // In a group only its members are called; elsewhere a mention opens that Bot's chat. ++// A friend's remote Bot is also called by its handle — the @ menu inserts "@" ++// and the host delivers on it — so the handle joins as a second name of the same Bot, ++// and the chip reads the display name (mentionLabel) from the roster. + export function mentionableBots(roster, sessionId) { + const room = roster.roomsById[sessionId] +- if (room) return room.members.map(id => roster.byId[id]).filter(Boolean) +- return roster.byId[sessionId] ? roster.bots : [] ++ const bots = room ? room.members.map(id => roster.byId[id]).filter(Boolean) ++ : roster.byId[sessionId] ? roster.bots : [] ++ return bots.concat(bots.filter(bot => typeof bot?.remote?.handle === 'string') ++ .map(bot => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ''), mentionLabel: bot.name }))) + } + + // Runs of plain text and Bot mentions, in order. +@@ -51,7 +56,7 @@ export function userTextParts(text, bots, referenceLabels = []) { + function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { + return userTextParts(text, bots, referenceLabels).map((part, index) => (part.bot + ? h('button', { key: index, type: 'button', className: 'bt-umention', title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) }, +- h(BotMark, { bot: part.bot, size: 16 }), part.label) ++ h(BotMark, { bot: part.bot, size: 16 }), part.bot.mentionLabel ?? part.label) + : h(Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, 'skill', references)))) + } + diff --git a/test/bot-tree.test.js b/test/bot-tree.test.js new file mode 100644 index 0000000000000000000000000000000000000000..649ca3f372a8b38d5ea9703b367f359697219be3 @@ -1024,3 +1068,29 @@ index 0000000000000000000000000000000000000000..4fd3cd074c28ea93380c4f8ed7e7c5d1 + // One remote list: the old built-in "Remote members" section is gone. + assert.doesNotMatch(source, /Remote members/) +}) +diff --git a/test/mention-rule.test.js b/test/mention-rule.test.js +index 0e0a1a221a1e90cdaa00ff92f84678ba7bc605e3..eb3768983f57059d7d1051f8d683ed5a5fd48eae 100644 +--- a/test/mention-rule.test.js ++++ b/test/mention-rule.test.js +@@ -83,6 +83,21 @@ test('the user bubble keeps the text around a mention, and a group only calls it + assert.equal(canDrawUserMessages(), false) + }) + ++test("a friend's Bot answers its handle too, and the chip reads its display name (#71)", () => { ++ const remote = { id: 'f', name: '代码审查官', remote: { handle: '@bot-2a758c0f.danyang.home', home: '@danyang.home' } } ++ const members = mentionableBots({ bots: [remote], byId: { f: remote }, roomsById: { r: { members: ['f'] } } }, 'r') ++ // The @ menu inserts "@" and the host delivers on it, so the bubble chips it ++ // like a name mention, under the display name from the roster. ++ const parts = userTextParts('@bot-2a758c0f.danyang.home 你是?', members) ++ assert.deepEqual(plain(parts.filter(part => part.bot).map(part => part.bot.id)), ['f']) ++ assert.equal(parts[0].bot.mentionLabel, '代码审查官') ++ assert.deepEqual(plain(parts[1]), { text: ' 你是?' }) ++ // The display-name form still matches the Bot itself, and an address before @ stays text. ++ const named = userTextParts('@代码审查官 你是谁', members) ++ assert.deepEqual(plain(named.filter(part => part.bot).map(part => [part.bot.id, part.label])), [['f', '代码审查官']]) ++ assert.deepEqual(plain(userTextParts('mail f@bot-2a758c0f.danyang.home', members).filter(part => part.bot)), []) ++}) ++ + test('a Bot name is something "@" can call without a second reading', () => { + assert.equal(nameProblem('新 Bot'), undefined) + assert.equal(nameProblem(' '), 'A Bot needs a name') diff --git a/patches/ds-bot/0005-social-seams.patch b/patches/ds-bot/0005-social-seams.patch index 7bfdb4175..de411fdd3 100644 --- a/patches/ds-bot/0005-social-seams.patch +++ b/patches/ds-bot/0005-social-seams.patch @@ -406,10 +406,10 @@ index 0000000000000000000000000000000000000000..23ce5956ddfc487174c094fbf31e9c22 +} diff --git a/test/inbound-ui.test.js b/test/inbound-ui.test.js new file mode 100644 -index 0000000000000000000000000000000000000000..7734f24c26e9793ed11c10acb7fc1f6f1a2d5ff3 +index 0000000000000000000000000000000000000000..e09b35e52c2b10c71cf28eb06e9198f3a00fde63 --- /dev/null +++ b/test/inbound-ui.test.js -@@ -0,0 +1,64 @@ +@@ -0,0 +1,83 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { fileURLToPath } from 'node:url' @@ -419,7 +419,7 @@ index 0000000000000000000000000000000000000000..7734f24c26e9793ed11c10acb7fc1f6f +// The inbound friend conversation's display: trace lines, the thinking row, the remote speaker. +const { outputFiles } = await build({ + stdin: { -+ contents: "export { stripTrace, previewOf } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'", ++ contents: "export { stripTrace, previewOf, nameMentions } from './src/client/text.js'\nexport { remoteSpeaker } from './src/client/cells.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', @@ -437,7 +437,7 @@ index 0000000000000000000000000000000000000000..7734f24c26e9793ed11c10acb7fc1f6f + throw new Error(`Unexpected platform module ${name}`) + }, +}) -+const { stripTrace, previewOf, remoteSpeaker } = module.exports ++const { stripTrace, previewOf, nameMentions, remoteSpeaker } = module.exports + +test('a reply opening with a delegation trace shows without it', () => { + assert.equal(stripTrace('Chief → 助手\n\nHi Chief! Good to hear from you.'), 'Hi Chief! Good to hear from you.') @@ -474,6 +474,25 @@ index 0000000000000000000000000000000000000000..7734f24c26e9793ed11c10acb7fc1f6f + assert.notEqual(other.bot.color, 'gray') + assert.equal(other.chatId, undefined) +}) ++ ++test("a friend Bot's handle in a preview reads as its display name (#71)", () => { ++ const roster = { bots: [{ id: 'f', name: '代码审查官', remote: { handle: '@bot-2a758c0f.danyang.home' } }], remoteBots: [{ handle: '@bot-2a758c0f.danyang.home', name: '代码审查官' }] } ++ assert.equal(nameMentions('@bot-2a758c0f.danyang.home 你是?', roster), '@代码审查官 你是?') ++ assert.equal(nameMentions('看 @bot-2a758c0f.danyang.home。', roster), '看 @代码审查官。') ++ // An address before the @, or a longer word after the handle, stays as written. ++ assert.equal(nameMentions('mail a@bot-2a758c0f.danyang.home', roster), 'mail a@bot-2a758c0f.danyang.home') ++ assert.equal(nameMentions('@bot-2a758c0f.danyang.home2 不在', roster), '@bot-2a758c0f.danyang.home2 不在') ++ // Unknown handles, missing rosters, and non-string input pass through. ++ assert.equal(nameMentions('@bot-x.home 你在?', roster), '@bot-x.home 你在?') ++ assert.equal(nameMentions('@bot-2a758c0f.danyang.home', undefined), '@bot-2a758c0f.danyang.home') ++ assert.equal(nameMentions(undefined, roster), '') ++ // One pass: a name that itself contains another handle is not rescanned. ++ const nested = { bots: [], remoteBots: [ ++ { handle: '@b.x.home', name: '请找 @a.x.home 的主人' }, ++ { handle: '@a.x.home', name: '代码审查官' }, ++ ] } ++ assert.equal(nameMentions('@b.x.home 你在?', nested), '@请找 @a.x.home 的主人 你在?') ++}) diff --git a/test/remote-chat.test.js b/test/remote-chat.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e96ef4d40dc3fa19344755d83db4d59b85144e0c diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 37ecdfca0..f659e4795 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -1,7 +1,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Generated by scripts/ds-bot-patches.mjs; 1 file(s). diff --git a/client.js b/client.js -index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba0eeb6e44 100644 +index f46d64171400ccaa6c00e4577c308297a70a0b4d..0fa96e30b5ba728d44f13cad71f0de424cf7c7af 100644 --- a/client.js +++ b/client.js @@ -89,9 +89,20 @@ var ZH = { @@ -374,7 +374,27 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }; } var allThemes = () => ({ ...THEMES, ...customThemes }); -@@ -1494,6 +1796,13 @@ function reminderPrompts(text) { +@@ -1463,6 +1765,19 @@ function mentionTarget(event) { + const anchor = event.target instanceof Element ? event.target.closest(`a[href^="${MENTION_ORIGIN}"]`) : null; + return anchor ? anchor.getAttribute("href").slice(MENTION_ORIGIN.length) : null; + } ++function nameMentions(text, roster) { ++ const value = String(text ?? ""); ++ if (!value.includes("@") || roster === null || typeof roster !== "object") return value; ++ const names = /* @__PURE__ */ new Map(); ++ for (const record of [...Array.isArray(roster.remoteBots) ? roster.remoteBots : [], ...Array.isArray(roster.bots) ? roster.bots : []]) { ++ const handle = typeof record?.remote === "object" && record.remote !== null ? record.remote.handle : record?.handle; ++ if (typeof handle === "string" && typeof record?.name === "string" && record.name !== "") names.set(handle.replace(/^@+/, "").toLowerCase(), record.name); ++ } ++ const handles = [...names.keys()].filter(Boolean).sort((a, b) => b.length - a.length); ++ if (handles.length === 0) return value; ++ const source = handles.map((handle) => handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"); ++ return value.replace(new RegExp(`(^|[^a-z0-9_./])@(${source})(?![a-z0-9_\\-/\\\\])(?!\\.[a-z0-9_-])`, "gi"), (_all, before, handle) => `${before}@${names.get(handle.toLowerCase())}`); ++} + function clock(ms) { + return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); + } +@@ -1494,6 +1809,13 @@ function reminderPrompts(text) { } } var stripHeader = (text) => text.replace(HEADER, ""); @@ -388,7 +408,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba function latestMemoryChange(news, ids) { const at = (id) => news?.[id]?.seen ? news[id].at ?? 0 : 0; return ids.reduce((best, id) => at(id) > at(best) ? id : best, ids[0] ?? null); -@@ -1604,7 +1913,7 @@ function previewOf(list, id, hiddenTurns = []) { +@@ -1604,7 +1926,7 @@ function previewOf(list, id, hiddenTurns = []) { const entry = outline[index]; if (hiddenTurns.includes(entry.turn)) continue; const text = entry.response || entry.prompt || ""; @@ -397,7 +417,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } return ""; } -@@ -1626,7 +1935,7 @@ function usageOf(list, ids) { +@@ -1626,7 +1948,7 @@ function usageOf(list, ids) { } // src/client/motion.js @@ -406,7 +426,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; function springEasing(stiffness, damping) { const w0 = Math.sqrt(stiffness); -@@ -1659,12 +1968,12 @@ function liveSprings(apply2) { +@@ -1659,12 +1981,12 @@ function liveSprings(apply2) { const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); last = now; const steps = Math.ceil(dt * 240); @@ -422,7 +442,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { value.x = value.to; -@@ -1699,10 +2008,10 @@ function liveSprings(apply2) { +@@ -1699,10 +2021,10 @@ function liveSprings(apply2) { } var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { @@ -436,7 +456,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (own.current === null) { own.current = { key: void 0, -@@ -1736,7 +2045,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai +@@ -1736,7 +2058,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); container.dataset.glide = ""; }; @@ -445,7 +465,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const state = own.current; const node = box.current; if (node === state.node && key === state.key) return; -@@ -1750,7 +2059,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai +@@ -1750,7 +2072,7 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai } place(fresh); }); @@ -454,7 +474,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba own.current.observer?.disconnect(); own.current.springs.stop(); }, []); -@@ -1758,16 +2067,16 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai +@@ -1758,16 +2080,16 @@ function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trai } function Segmented({ label, value, className, children }) { const glide = useGlide(value); @@ -475,7 +495,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const container = list.current; if (!container) return; const next = /* @__PURE__ */ new Map(); -@@ -1788,8 +2097,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) { +@@ -1788,8 +2110,8 @@ function useFlip(list, key, selector, id = (node) => node.dataset.flip) { }, [key]); } function useMorph(box, key) { @@ -486,7 +506,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const node = box.current; const state = last.current; if (!node) { -@@ -1817,8 +2126,8 @@ function useMorph(box, key) { +@@ -1817,8 +2139,8 @@ function useMorph(box, key) { }); } function useLinger(value, ms) { @@ -497,7 +517,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (value) { setKept(value); return void 0; -@@ -1856,6 +2165,11 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo +@@ -1856,6 +2178,11 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} @@ -509,7 +529,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -@@ -1950,6 +2264,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} +@@ -1950,6 +2277,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -517,7 +537,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba .bt-tag{flex:none;max-width:96px;font-size:12px;line-height:16px;font-weight:400;padding:1px 6px;border-radius:4px;background:var(--bt-tag-bg);color:var(--bt-tag-ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-row-preview{font-size:13px;line-height:18px;color:var(--bt-ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .bt-unread{width:8px;height:8px;border-radius:50%;background:#1084fe;flex:none;margin-right:2px;animation:bt-fade .13s cubic-bezier(.22,1,.36,1)} -@@ -1960,11 +2275,9 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} +@@ -1960,11 +2288,9 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-connect{flex:1;height:36px;border-radius:999px;border:1px solid var(--bt-line);background:var(--bt-main);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background-color .12s ease} .bt-connect:hover{background:var(--bt-hover)} .bt-connect svg{flex:none;width:15px;height:15px} @@ -531,7 +551,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba .bt-head{position:relative;display:flex;justify-content:center;align-items:center;height:40px;flex:none;pointer-events:none} .bt-pill{pointer-events:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:7.5px 13.5px 7.5px 7.5px;border-radius:999px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 2px 8px -1px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);color:var(--bt-ink);font-family:inherit;font-size:14px;line-height:20px;cursor:pointer;max-width:70%;box-sizing:border-box;transition:background-color .12s ease,transform ${SPRING_TAP.duration}ms ${SPRING_TAP.easing}} -@@ -2599,6 +2912,9 @@ var CONVERSATION_CSS = ` +@@ -2599,6 +2925,9 @@ var CONVERSATION_CSS = ` .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} .bt-think summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px} .bt-think summary::-webkit-details-marker{display:none} @@ -541,7 +561,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba .bt-think-text{white-space:pre-wrap;margin-top:6px;padding-left:10px;border-left:2px solid var(--bt-line-2)} .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} .bt-event>span{padding:0 2px} -@@ -2856,12 +3172,12 @@ function installCompiledStyles(text, tag) { +@@ -2856,12 +3185,12 @@ function installCompiledStyles(text, tag) { } // src/client/mark.js @@ -556,7 +576,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!enabled) return void 0; let frame = 0; let point = null; -@@ -2895,10 +3211,10 @@ function useGaze(ref, enabled) { +@@ -2895,10 +3224,10 @@ function useGaze(ref, enabled) { var FIDGETS = ["hop", "sway", "stretch", "look"]; function BotMark({ bot, look: given, size = 36, state = "idle", live = false, gaze = false, pokeable = false, label }) { const look = given ?? lookOf(bot); @@ -570,7 +590,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba useGaze(ref, gaze); const common = { ref, -@@ -2918,31 +3234,31 @@ function BotMark({ bot, look: given, size = 36, state = "idle", live = false, ga +@@ -2918,31 +3247,31 @@ function BotMark({ bot, look: given, size = 36, state = "idle", live = false, ga if (/^bt-poke/.test(event.animationName)) setPoked(false); } : void 0 }; @@ -612,7 +632,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const was = prev.current; prev.current = state; if (state !== "idle") { -@@ -2958,16 +3274,16 @@ function useSettled(state) { +@@ -2958,16 +3287,16 @@ function useSettled(state) { } function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { const settled = useSettled(state); @@ -636,7 +656,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function clusterSeats(count, frame) { -@@ -2984,21 +3300,21 @@ function clusterSeats(count, frame) { +@@ -2984,21 +3313,21 @@ function clusterSeats(count, frame) { function RoomAvatar({ room: room2, roster, size = 36 }) { const members = room2.members.map((id) => roster.byId[id]).filter(Boolean); const people = members.length === 1 ? [members[0], null] : members; @@ -663,7 +683,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "span", { className: "bt-cluster", style: { width: size, height: size } }, seats.map(([x, y, s], index) => { -@@ -3006,75 +3322,75 @@ function RoomAvatar({ room: room2, roster, size = 36 }) { +@@ -3006,75 +3335,75 @@ function RoomAvatar({ room: room2, roster, size = 36 }) { const style = { left: x, top: y, width: s, height: s }; if (holes2.length > 0) Object.assign(style, { WebkitMaskImage: holes2.join(","), maskImage: holes2.join(","), WebkitMaskComposite: "source-in", maskComposite: "intersect" }); if (overflow > 0 && index === 3) { @@ -764,7 +784,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var SmileIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M7.3 11.6a3.4 3.4 0 0 0 5.4 0", "M7.75 8.25h.01M12.25 8.25h.01"]); -@@ -3093,14 +3409,14 @@ var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 +@@ -3093,14 +3422,14 @@ var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 var GearIcon = lineIcon(["M10 7.4a2.6 2.6 0 1 1 0 5.2 2.6 2.6 0 0 1 0-5.2z", "M7.85 4.4l.29-1.87a7.7 7.7 0 0 1 3.72 0l.29 1.87a6 6 0 0 1 1.63.94l1.76-.69a7.7 7.7 0 0 1 1.86 3.23l-1.47 1.18a6 6 0 0 1 0 1.88l1.47 1.18a7.7 7.7 0 0 1-1.86 3.23l-1.76-.69a6 6 0 0 1-1.63.94l-.29 1.87a7.7 7.7 0 0 1-3.72 0l-.29-1.87a6 6 0 0 1-1.63-.94l-1.76.69a7.7 7.7 0 0 1-1.86-3.23l1.47-1.18a6 6 0 0 1 0-1.88L2.6 7.88a7.7 7.7 0 0 1 1.86-3.23l1.76.69a6 6 0 0 1 1.63-.94z"]); var PinIcon = lineIcon(["M12.5 3l4.5 4.5-3 1.5-2.5 2.5.5 3.5-1.5 1.5L7 13 3.5 16.5", "M7 13l-3.5-3.5L5 8l3.5.5L11 6l1.5-3"]); function PinMarkIcon() { @@ -783,7 +803,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -3112,6 +3428,7 @@ var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h +@@ -3112,6 +3441,7 @@ var CopyIcon = lineIcon(["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h var HideIcon = lineIcon(["M3 3l14 14", "M8.6 5.2A7.6 7.6 0 0 1 10 5c4.5 0 7 5 7 5a12.6 12.6 0 0 1-2 2.6M12.2 14.5A6.8 6.8 0 0 1 10 15c-4.5 0-7-5-7-5a12.4 12.4 0 0 1 3-3.4", "M8.5 8.5a2 2 0 0 0 3 3"]); var TrashIcon = lineIcon(["M3.5 5.5h13", "M8 5.5V4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5", "M5 5.5l.7 10A1.5 1.5 0 0 0 7.2 17h5.6a1.5 1.5 0 0 0 1.5-1.5l.7-10", "M8.5 9v4.5M11.5 9v4.5"]); var DuplicateIcon = lineIcon(["M7 7h8.5v9.5H7z", "M4.5 13V3.5H13", "M11.25 9.5v4.5M9 11.75h4.5"]); @@ -791,7 +811,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var SlidersIcon = lineIcon(["M4 6.5h5M12.5 6.5H16M4 13.5h4M11.5 13.5H16", "M10.5 4.5v4M9.5 11.5v4"]); var MemoryIcon = lineIcon(["M5.5 3.5h8A1.5 1.5 0 0 1 15 5v10a1.5 1.5 0 0 1-1.5 1.5h-8z", "M5.5 3.5v13", "M8.5 7.5h3.5M8.5 10.5h2.5", "M12 3.5v4l1-.8 1 .8"]); var FolderIcon = lineIcon(["M3 6A1.5 1.5 0 0 1 4.5 4.5h3.2l1.8 2h6A1.5 1.5 0 0 1 17 8v6.5a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5z"]); -@@ -3119,14 +3436,14 @@ var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10 +@@ -3119,14 +3449,14 @@ var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10 var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; // src/client/questions.js @@ -809,7 +829,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (count === 0) return void 0; const onKey = (event) => { if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; -@@ -3146,10 +3463,10 @@ function useOptionKeys(count, onPick, ownRef) { +@@ -3146,10 +3476,10 @@ function useOptionKeys(count, onPick, ownRef) { }, [count, ownRef]); } function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { @@ -824,7 +844,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const options = optionsOf(question); const multi = question.multiSelect === true; const custom = own.trim(); -@@ -3167,7 +3484,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised +@@ -3167,7 +3497,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised }; useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; @@ -833,7 +853,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: ownRef, rows: 1, value: own, -@@ -3186,7 +3503,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised +@@ -3186,7 +3516,7 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised }) : null; const rows = options.map((option, optionIndex) => { const selected = multi && picked.includes(option.label); @@ -842,7 +862,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: option.label, -@@ -3196,36 +3513,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised +@@ -3196,36 +3526,36 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), onClick: () => choose(option.label) }, @@ -892,7 +912,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "form", { className: raised ? "bt-q bt-q-raised" : "bt-q", -@@ -3235,24 +3552,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised +@@ -3235,24 +3565,24 @@ function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised submit(); } }, @@ -927,7 +947,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) : null, foot ); -@@ -3262,29 +3579,29 @@ function SettledQuestion({ question, answer }) { +@@ -3262,29 +3592,29 @@ function SettledQuestion({ question, answer }) { const prompt = question?.question ?? ""; const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; if (chosen.length === 0) { @@ -968,7 +988,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); })) ); -@@ -3292,8 +3609,8 @@ function SettledQuestion({ question, answer }) { +@@ -3292,8 +3622,8 @@ function SettledQuestion({ question, answer }) { function QuestionCard({ matched }) { const pending = matched; const questions = pending.questions ?? []; @@ -979,7 +999,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const question = questions[index]; if (question === void 0) return null; const onAnswer = (answer) => { -@@ -3305,10 +3622,10 @@ function QuestionCard({ matched }) { +@@ -3305,10 +3635,10 @@ function QuestionCard({ matched }) { } pending.answer({ answers: next }); }; @@ -992,7 +1012,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (answered === question.id) return null; const onAnswer = (answer) => { setAnswered(question.id); -@@ -3318,10 +3635,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) { +@@ -3318,10 +3648,10 @@ function PendingQuestion({ sessionId, question, actions, inline }) { setAnswered(question.id); void actions.dismissQuestion(sessionId); }; @@ -1005,7 +1025,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -@@ -3329,16 +3646,16 @@ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { +@@ -3329,16 +3659,16 @@ function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { const drawn = useCards?.((state) => state[sessionId]?.[question?.callId] > 0) === true; const loading = useSession?.(historyLoading) === true; if (question === void 0 || question.callId && (drawn || loading)) return null; @@ -1025,7 +1045,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: question.id, question, answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -@@ -3346,26 +3663,26 @@ function AnsweredCell({ node }) { +@@ -3346,26 +3676,26 @@ function AnsweredCell({ node }) { } // src/client/secret-card.js @@ -1062,7 +1082,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc"); var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname); -@@ -3376,7 +3693,7 @@ var scopeLabel = (scope, roster) => { +@@ -3376,7 +3706,7 @@ var scopeLabel = (scope, roster) => { }; var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : ""; function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { @@ -1071,7 +1091,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-secret-input", type: MASK_BY_CSS ? "text" : "password", "data-masked": MASK_BY_CSS ? "" : void 0, -@@ -3402,11 +3719,11 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { +@@ -3402,11 +3732,11 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) { } function ScopeSwitch({ value, onChange, bot }) { const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; @@ -1086,7 +1106,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: id, type: "button", role: "radio", -@@ -3417,26 +3734,26 @@ function ScopeSwitch({ value, onChange, bot }) { +@@ -3417,26 +3747,26 @@ function ScopeSwitch({ value, onChange, bot }) { ); } function SecretHead({ bot, title, detail, name, onClose }) { @@ -1125,7 +1145,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); const run = (promise) => { setBusy(true); -@@ -3452,73 +3769,73 @@ function SecretCard({ bot, request, roster, actions }) { +@@ -3452,73 +3782,73 @@ function SecretCard({ bot, request, roster, actions }) { if (!busy) void run(actions.secretAllow(request.id)); }; const filling = request.kind === "fill" || replacing; @@ -1222,7 +1242,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } return null; } -@@ -3531,44 +3848,44 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { +@@ -3531,44 +3861,44 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { const drawn = (entry) => entry.callId && entry.sessionId === sessionId && (loading || shown[entry.callId] > 0); const waiting = (roster.secretRequests?.[bot.id] ?? []).filter((entry) => entry.status === "pending" && !drawn(entry)); if (waiting.length === 0) return null; @@ -1281,7 +1301,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const long = value.trim().length >= MIN_VALUE; const save = () => { if (!long) return; -@@ -3583,66 +3900,66 @@ function ValueEditor({ secret, onSave }) { +@@ -3583,66 +3913,66 @@ function ValueEditor({ secret, onSave }) { setValue(next); setReplaced(false); }; @@ -1374,7 +1394,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; const save = () => { if (!ready) return; -@@ -3653,43 +3970,43 @@ function AddSecret({ actions, run, onDone }) { +@@ -3653,43 +3983,43 @@ function AddSecret({ actions, run, onDone }) { } }); }; @@ -1436,7 +1456,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ]; } var SECRET_CSS = ` -@@ -3767,7 +4084,7 @@ var thinkingLive = (step) => step.status === "running" && texts(step).length === +@@ -3767,7 +4097,7 @@ var thinkingLive = (step) => step.status === "running" && texts(step).length === var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); // src/client/cells.js @@ -1445,7 +1465,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var import_react_dom = require("react-dom"); var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); var mdLabels = () => ({ code: { copyLabel: t("Copy"), copiedLabel: t("Copied") }, footnotes: t("Footnotes") }); -@@ -3780,7 +4097,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, +@@ -3780,7 +4110,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, actions.openSession(target); }; const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; @@ -1454,7 +1474,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "div", { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, texts2.map((text, index) => { -@@ -3788,21 +4105,21 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, +@@ -3788,21 +4118,21 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, const picked = key ? reactions?.[index] ?? [] : []; const last = index === texts2.length - 1; const live = streaming && last; @@ -1482,7 +1502,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: emoji, type: "button", className: "bt-react", -@@ -3815,7 +4132,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, +@@ -3815,7 +4145,7 @@ function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, } var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; function useDismiss(open, close, refs) { @@ -1491,7 +1511,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!open) return void 0; const onDown = (event) => { if (refs.some((ref) => ref.current?.contains(event.target))) return; -@@ -3836,11 +4153,11 @@ function useDismiss(open, close, refs) { +@@ -3836,11 +4166,11 @@ function useDismiss(open, close, refs) { }, [open]); } function MessageTools({ text, name, sessionId, react, actions }) { @@ -1508,7 +1528,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const close = () => setPanel(null); useDismiss(panel !== null, close, [tools, pop]); const open = (kind, event) => { -@@ -3865,25 +4182,25 @@ function MessageTools({ text, name, sessionId, react, actions }) { +@@ -3865,25 +4195,25 @@ function MessageTools({ text, name, sessionId, react, actions }) { link.click(); setTimeout(() => URL.revokeObjectURL(url), 1e3); }; @@ -1543,7 +1563,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "div", { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, item(t("Copy"), CopyIcon, copy2), -@@ -3892,11 +4209,12 @@ function MessageTools({ text, name, sessionId, react, actions }) { +@@ -3892,11 +4222,12 @@ function MessageTools({ text, name, sessionId, react, actions }) { ); } function ThinkingRow({ text }) { @@ -1559,7 +1579,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,7 +4222,7 @@ function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +@@ -3904,7 +4235,7 @@ function useFileMentions({ node, openFile, fileMentions, useTurnData }) { const location2 = node.location; const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; const seq = node.data.finalNode?.seq; @@ -1568,7 +1588,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0; return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0; }, [fileMentions, openFile, seq, tail, turn]); -@@ -3916,7 +4234,7 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use +@@ -3916,7 +4247,7 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use const contexts = useTurnContext(String(data.turn)); const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); @@ -1577,7 +1597,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const running = data.status === "running"; useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); if (hidden) return null; -@@ -3924,13 +4242,13 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use +@@ -3924,13 +4255,13 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use const relays = relayPart(data, groupPart) ? relaysBefore(contexts, steps, data) : []; if (texts2.length === 0 && !reasoning && relays.length === 0) return null; const selfId = roster.byId[sessionId]?.id ?? sessionId; @@ -1594,7 +1614,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: relay.seq, prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), bots: [sender], -@@ -3940,61 +4258,126 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use +@@ -3940,61 +4271,126 @@ function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, use whole: true }); }), @@ -1742,7 +1762,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -@@ -4005,14 +4388,14 @@ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +@@ -4005,14 +4401,14 @@ function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { const target = other && other.id !== self?.id ? other : self; if (!target) return null; const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); @@ -1760,7 +1780,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } var scrollParent = (node) => { for (let at = node.parentElement; at; at = at.parentElement) { -@@ -4021,11 +4404,11 @@ var scrollParent = (node) => { +@@ -4021,11 +4417,11 @@ var scrollParent = (node) => { return null; }; function EarlierParts({ sessionId, roster, actions }) { @@ -1777,7 +1797,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const load = (cursor) => { if (busy.current) return; busy.current = true; -@@ -4041,19 +4424,19 @@ function EarlierParts({ sessionId, roster, actions }) { +@@ -4041,19 +4437,19 @@ function EarlierParts({ sessionId, roster, actions }) { if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); }); }; @@ -1800,7 +1820,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const node = top.current; if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; const observer = new IntersectionObserver((entries2) => { -@@ -4069,35 +4452,35 @@ function EarlierParts({ sessionId, roster, actions }) { +@@ -4069,35 +4465,35 @@ function EarlierParts({ sessionId, roster, actions }) { if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); } @@ -1847,7 +1867,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (selfId && other.id) actions.openExchange(selfId, other.id); }, whole: true }); } -@@ -4105,7 +4488,7 @@ function EarlierParts({ sessionId, roster, actions }) { +@@ -4105,7 +4501,7 @@ function EarlierParts({ sessionId, roster, actions }) { return null; } }), @@ -1856,7 +1876,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -@@ -4116,7 +4499,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us +@@ -4116,7 +4512,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us const deferred = source.kind === "bot" && source.deferred === true; const relayed = isRelayContext(data); const turn = node.location?.turn?.turn ?? data.turn; @@ -1865,7 +1885,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); }, [relayed, sessionId, turn, source.senderSessionId]); const order = useSessions((list) => { -@@ -4135,23 +4518,27 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us +@@ -4135,23 +4531,27 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); const body = triggerBody(); @@ -1899,7 +1919,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const run = relayed ? commRunOf( order.split(",").filter(Boolean).map(Number), Object.fromEntries(peers.split(",").filter(Boolean).map((pair) => [Number(pair.slice(0, pair.indexOf(":"))), pair.slice(pair.indexOf(":") + 1)])), -@@ -4165,7 +4552,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us +@@ -4165,7 +4565,7 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); } @@ -1908,7 +1928,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba prefix: source.role === "stopped" ? t("Stopped by") : count > 1 ? t("{count} messages with", { count }) : t("Message from"), bots, onOpen: (bot) => actions.openExchange(selfId, bot.id), -@@ -4174,24 +4561,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us +@@ -4174,24 +4574,24 @@ function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, us } if (source.kind === "schedule") { const prompts = reminderPrompts(contentText(data.content)); @@ -1938,7 +1958,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba actions.noteTurnTime(sessionId, turn, "first", start); }, [sessionId, turn, start]); const visit = useUi((state) => state.visits?.[sessionId]); -@@ -4199,7 +4586,7 @@ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTu +@@ -4199,7 +4599,7 @@ function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTu const own = state[sessionId]?.[turn]; return !hidden && typeof visit?.seen === "number" && own?.first !== void 0 && own.first <= visit.seen && own.end !== void 0 && own.end > visit.seen && own.end <= visit.entered; }); @@ -1947,7 +1967,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } function Nothing() { return null; -@@ -4236,7 +4623,7 @@ var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > vis +@@ -4236,7 +4636,7 @@ var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > vis function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { const turn = location2.turn; const time = location2.end.time; @@ -1956,7 +1976,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba actions.noteTurnTime(sessionId, turn, "end", time); }, [sessionId, turn, time]); const label = useTurnClock((state) => { -@@ -4250,15 +4637,15 @@ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions +@@ -4250,15 +4650,15 @@ function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions return start !== void 0 && start > visit.seen && start <= visit.entered; }); if (!label && !fresh) return null; @@ -1977,7 +1997,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } var PHRASES = { thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -@@ -4324,15 +4711,15 @@ function workPhrase(tool) { +@@ -4324,15 +4724,15 @@ function workPhrase(tool) { } function useLiveNote(actions, sessionId, id, entry) { const key = entry ? JSON.stringify(entry) : ""; @@ -1996,7 +2016,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (done) return void 0; let stop = false; let timer; -@@ -4366,66 +4753,66 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi +@@ -4366,66 +4766,66 @@ function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; if (replies.length === 0 && pending.length === 0) return null; const room2 = roster.roomsById[sessionId]; @@ -2080,7 +2100,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); }) ); -@@ -4435,52 +4822,52 @@ var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +@@ -4435,52 +4835,52 @@ var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); function callTree(root) { const calls = []; const seen = /* @__PURE__ */ new Set(); @@ -2149,7 +2169,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba case "create_group": case "update_group": case "delete_group": -@@ -4492,41 +4879,41 @@ function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot +@@ -4492,41 +4892,41 @@ function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot post_to_group: ["Posted in", "Couldn't post in the group"] }[tool]; if (!done) return null; @@ -2201,7 +2221,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba default: return null; } -@@ -4538,18 +4925,18 @@ function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +@@ -4538,18 +4938,18 @@ function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { const hidden = isHiddenTurn(roster, sessionId, turn); const findBot = (nameOrId) => roster.byId[nameOrId] ?? roster.bots.find((bot) => bot.name.toLowerCase() === String(nameOrId ?? "").toLowerCase()); const calls = hidden ? [] : callTree(root); @@ -2225,7 +2245,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var import_react_dom2 = require("react-dom"); var PHRASE_HOLD_MS = 800; var drawPhrase = (pool) => t(pool[Math.min(pool.length - 1, Math.floor(Math.random() * pool.length))]); -@@ -4560,8 +4947,8 @@ function elapsedLabel(ms) { +@@ -4560,8 +4960,8 @@ function elapsedLabel(ms) { return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; } function useRunningHost(active) { @@ -2236,7 +2256,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!active) { setHost(null); return void 0; -@@ -4585,9 +4972,9 @@ function useRunningHost(active) { +@@ -4585,9 +4985,9 @@ function useRunningHost(active) { return host; } function useRowEntrances(replacing) { @@ -2248,7 +2268,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const seen = /* @__PURE__ */ new WeakSet(); const started = Date.now(); let column = null; -@@ -4638,21 +5025,21 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us +@@ -4638,21 +5038,21 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us const pendingInput = useSession((snapshot) => Math.max(0, ...(snapshot?.pendingSubmissions ?? []).filter((sub) => sub.placement !== "queued").map((sub) => sub.time))); const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; @@ -2275,7 +2295,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (wantKey === null || shown?.key === wantKey) return void 0; const make = () => setShown({ key: wantKey, text: want.label ?? drawPhrase(want.pool), at: Date.now() }); const wait = shown ? shown.at + PHRASE_HOLD_MS - Date.now() : 0; -@@ -4663,12 +5050,12 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us +@@ -4663,12 +5063,12 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us const timer = setTimeout(make, wait); return () => clearTimeout(timer); }, [wantKey, shown]); @@ -2293,7 +2313,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (visible) { setPhase("in"); return void 0; -@@ -4680,7 +5067,7 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us +@@ -4680,7 +5080,7 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us }, 140); return () => clearTimeout(timer); }, [visible]); @@ -2302,7 +2322,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (phase !== null) return; endedAt.current = Date.now(); setShown(null); -@@ -4691,12 +5078,12 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us +@@ -4691,12 +5091,12 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us const delta = before - scroller.scrollTop; if (delta > 0) scroller.querySelector("[data-chat-flow]")?.animate([{ transform: `translateY(${-delta}px)` }, { transform: "none" }], { duration: 140, easing: "cubic-bezier(.23,1,.32,1)" }); }, [phase]); @@ -2318,7 +2338,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (phase !== "in") return void 0; const timer = setInterval(() => setNow(Date.now()), 1e4); return () => clearInterval(timer); -@@ -4704,18 +5091,18 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us +@@ -4704,18 +5104,18 @@ function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, us const host = useRunningHost(running); if (!host || phase === null || !shown) return null; const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; @@ -2343,7 +2363,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ), host); -@@ -4732,23 +5119,23 @@ function showsDshRow(dsh, app) { +@@ -4732,32 +5132,32 @@ function showsDshRow(dsh, app) { } // src/client/sidebar.js @@ -2371,8 +2391,10 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba +function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked, asRow = false }) { const id = bot.id; const part = currentPart(bot); - const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); -@@ -4757,7 +5144,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); ++ const preview = useSessions((list) => nameMentions(previewOf(list, part, roster.exchangeTurns[part]), roster)); + const running = useSessionStatus((map) => map.get(part)?.running === true); + const active = useSessionRetainInfo(part, (info) => (info?.retainedBy?.mainView ?? 0) > 0); const unread = useSessionStatus((map) => map.get(part)?.completionUnread === true || map.get(part)?.pendingInteraction !== void 0) || marked && !active; useMarkedUnread(id, active, marked, actions); const main = isMainOf(roster, id); @@ -2381,7 +2403,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const approval = useApproval(useWork, (entry) => entry.botId === id); const alert = roster.questions?.[part] !== void 0 || approval; const doing = useActivity((map) => map[id]); -@@ -4773,7 +5160,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -4773,7 +5173,7 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio const pinned = bot.pinned === true && !tile; const label = [bot.name, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", approval ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); if (tile && wide) { @@ -2390,7 +2412,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -4785,12 +5172,12 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -4785,12 +5185,12 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio onClick: () => actions.openSession(id), onContextMenu }, @@ -2407,7 +2429,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -4802,15 +5189,27 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio +@@ -4802,21 +5202,33 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio onClick: () => actions.openSession(id), onContextMenu }, @@ -2441,7 +2463,15 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { -@@ -4827,7 +5226,7 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, + const id = room2.id; +- const fallback = useSessions((list) => previewOf(list, id)); +- const preview = room2.preview?.replace(/\s+/g, " ").trim() || fallback; ++ const fallback = useSessions((list) => nameMentions(previewOf(list, id), roster)); ++ const preview = nameMentions(room2.preview?.replace(/\s+/g, " ").trim(), roster) || fallback; + const active = useSessionRetainInfo(id, (info) => (info?.retainedBy?.mainView ?? 0) > 0); + const unread = useSessionStatus((map) => map.get(id)?.completionUnread === true) || marked && !active; + useMarkedUnread(id, active, marked, actions); +@@ -4827,7 +5239,7 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, if (room2.hidden && !active) return null; const pinned = room2.pinned === true; const label = [room2.name, pinned ? t("Pinned") : "", members.length > 0 ? t("Working") : "", approval ? t("Waiting for your approval") : ""].filter(Boolean).join(", "); @@ -2450,7 +2480,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -4841,20 +5240,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, +@@ -4841,20 +5253,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -2479,7 +2509,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!renaming) return; input.current?.focus(); input.current?.select(); -@@ -4875,10 +5274,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4875,10 +5287,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { } })(); }; @@ -2492,7 +2522,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: input, defaultValue: session.title, "aria-label": t("Rename"), -@@ -4891,7 +5290,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4891,7 +5303,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { }) ); } @@ -2501,7 +2531,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -4905,8 +5304,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { +@@ -4905,8 +5317,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -2512,7 +2542,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +5340,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4941,10 +5353,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -2525,7 +2555,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -4959,30 +5358,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4959,30 +5371,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -2567,7 +2597,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: session.id, session, agent, -@@ -4996,8 +5395,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, +@@ -4996,8 +5408,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -2578,7 +2608,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +5413,40 @@ function useListGlide(listRef, layout, key) { +@@ -5014,9 +5426,40 @@ function useListGlide(listRef, layout, key) { tops.current = { layout, map: next }; }, [layout, key]); } @@ -2620,7 +2650,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); -@@ -5025,10 +5455,11 @@ function TeamSidebar(props) { +@@ -5025,10 +5468,11 @@ function TeamSidebar(props) { for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); @@ -2634,7 +2664,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ...roster.rooms.map((room2) => ({ kind: "room", item: room2, at: stamps[room2.id] ?? room2.createdAt })), // The Agent sorts by its most recent Session, falling back to when it was added. ...(roster.agents ?? []).map((agent) => ({ kind: "agent", item: agent, at: Math.max(0, ...agent.sessions.map((id) => stamps[id] ?? 0)) || agent.createdAt })) -@@ -5036,6 +5467,10 @@ function TeamSidebar(props) { +@@ -5036,6 +5480,10 @@ function TeamSidebar(props) { return entries2.sort((a, b) => Number(Boolean(b.item.pinned)) - Number(Boolean(a.item.pinned)) || b.at - a.at); }, [roster, updated]); const main = roster.byId[roster.mainBotId]; @@ -2645,7 +2675,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba useListGlide(listRef, wide, order.map((entry) => entry.item.id).join(",")); useLandOnMainBot(roster, useSessions, actions); const rowProps = { roster, actions, useSessions, useUi, useSessionStatus: props.useSessionStatus, useSessionRetainInfo: props.useSessionRetainInfo, useActivity: props.useActivity, useWork: props.useWork, wide }; -@@ -5045,39 +5480,40 @@ function TeamSidebar(props) { +@@ -5045,39 +5493,40 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; @@ -2706,7 +2736,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; -@@ -5120,10 +5556,10 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5120,10 +5569,10 @@ function UpdateButton({ useUpdate, actions }) { const message = error || (phase === "failed" ? update.error : ""); const dshMessage = dshError || (dshPhase === "failed" ? dsh?.error : ""); const dshText = dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : dsh?.available === false && dsh?.blockedBy ? t("Update DS Bot {version} first.", { version: dsh.blockedBy }) : t("You have {version}.", { version: dsh?.current }); @@ -2720,7 +2750,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: button, type: "button", className: "bt-round bt-update", -@@ -5133,21 +5569,21 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5133,21 +5582,21 @@ function UpdateButton({ useUpdate, actions }) { "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle @@ -2752,7 +2782,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-update-go", disabled: phase === "installing", -@@ -5155,17 +5591,17 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5155,17 +5604,17 @@ function UpdateButton({ useUpdate, actions }) { }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, @@ -2777,7 +2807,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-update-go", disabled: dshPhase === "installing", -@@ -5177,60 +5613,24 @@ function UpdateButton({ useUpdate, actions }) { +@@ -5177,60 +5626,24 @@ function UpdateButton({ useUpdate, actions }) { ); } function ConnectPlugins({ wide, actions }) { @@ -2847,7 +2877,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var openShellSettings = null; -@@ -5288,32 +5688,32 @@ function pressSettingsShortcut() { +@@ -5288,32 +5701,32 @@ function pressSettingsShortcut() { })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { @@ -2888,7 +2918,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!open) return void 0; const onDown = (event) => { if (event.target instanceof Element && (event.target.closest(".bt-account-menu") || button.current?.contains(event.target))) return; -@@ -5349,22 +5749,22 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi +@@ -5349,22 +5762,22 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] ] ]; @@ -2920,7 +2950,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: label, type: "button", role: "menuitem", -@@ -5372,7 +5772,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi +@@ -5372,7 +5785,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -2929,7 +2959,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba )) ), document.body) : null ); -@@ -5382,8 +5782,8 @@ function useLandOnMainBot(roster, useSessions, actions) { +@@ -5382,8 +5795,8 @@ function useLandOnMainBot(roster, useSessions, actions) { const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); @@ -2940,7 +2970,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5404,16 +5804,16 @@ function useLandOnMainBot(roster, useSessions, actions) { +@@ -5404,16 +5817,16 @@ function useLandOnMainBot(roster, useSessions, actions) { function FooterAccount(props) { const seated = props.useUi((value) => value.accountSeated === true); if (seated) return null; @@ -2961,7 +2991,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var RETURN_CSS = ` -@@ -5427,10 +5827,10 @@ var RETURN_CSS = ` +@@ -5427,10 +5840,10 @@ var RETURN_CSS = ` .bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} `; function BrandMark({ size = 24 }) { @@ -2974,7 +3004,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5494,15 +5894,15 @@ var SIDEBAR_CSS = ` +@@ -5494,15 +5907,15 @@ var SIDEBAR_CSS = ` `; // src/client/header.js @@ -2994,7 +3024,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const left = seen + MEMORY_NOTICE_MS - Date.now(); setShown(seen > 0 && left > 0); if (seen === 0 || left <= 0) return void 0; -@@ -5512,16 +5912,16 @@ function useMemoryNotice(ids, useMemoryNews) { +@@ -5512,16 +5925,16 @@ function useMemoryNotice(ids, useMemoryNews) { return shown; } function MemoryNotice({ shown }) { @@ -3016,7 +3046,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { -@@ -5537,7 +5937,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5537,7 +5950,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; }); @@ -3025,7 +3055,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (opening) document.body.style.setProperty("--bt-first-sep", JSON.stringify(opening)); else document.body.style.removeProperty("--bt-first-sep"); return () => document.body.style.removeProperty("--bt-first-sep"); -@@ -5553,14 +5953,14 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5553,14 +5966,14 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe const memoryIds = bot ? [bot.id] : room2?.members ?? []; const notice = useMemoryNotice(memoryIds, useNews); const changed = useNews((map) => latestMemoryChange(map, memoryIds)); @@ -3043,7 +3073,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (room2) document.body.dataset.btRoom = ""; else delete document.body.dataset.btRoom; return () => { -@@ -5568,10 +5968,10 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5568,10 +5981,10 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe }; }, [Boolean(room2)]); const memoryNotice = notice && changed !== null && roster.memory !== false; @@ -3056,7 +3086,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -5581,19 +5981,19 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe +@@ -5581,19 +5994,19 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed]?.name ?? name }) : t("View conversation details"), onClick: () => memoryNotice ? actions.openMemory(changed) : actions.toggleDetails(sessionId) }, @@ -3085,7 +3115,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); -@@ -5658,18 +6058,18 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5658,18 +6071,18 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; @@ -3109,7 +3139,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +6077,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5677,16 +6090,16 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { }, [showDown]); if (!view || !showDown && !showUp) return null; const label = (count) => count === 1 ? t("1 new message") : count > 0 ? t("{count} new messages", { count }) : t("New messages"); @@ -3130,7 +3160,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), -@@ -5694,7 +6094,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5694,7 +6107,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { event.stopPropagation(); onDismiss(); } @@ -3139,7 +3169,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5704,8 +6104,8 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5704,8 +6117,8 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { }; const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); return (0, import_react_dom4.createPortal)( @@ -3150,7 +3180,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba null, showUp ? pill("up", unread, { left: view.center, top: view.top }, toDivider, () => setAbove((state) => ({ ...state, dismissed: true }))) : null, showDown ? pill("down", fresh, { left: view.center, bottom: view.bottom }, toLatest, () => setRelease((state) => ({ ...state, dismissed: true }))) : null -@@ -5715,18 +6115,18 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +@@ -5715,18 +6128,18 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { } // src/client/composer.js @@ -3176,7 +3206,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); -@@ -5779,7 +6179,7 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +@@ -5779,7 +6192,7 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); }); const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); @@ -3185,7 +3215,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!quote || !inputActions) return; actions.clearQuote(quote.token); const excerpt = quote.text.replace(/\s+/g, " ").trim(); -@@ -5797,10 +6197,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +@@ -5797,10 +6210,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; @@ -3199,7 +3229,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +6208,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +@@ -5808,16 +6221,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { title: dictation.listening ? t("Stop dictation") : t("Dictate"), onMouseDown: keepComposerFocus, onClick: () => dictation.listening ? dictation.stop() : dictation.start() @@ -3220,7 +3250,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function menuIcons(roster) { -@@ -5825,17 +6225,17 @@ function menuIcons(roster) { +@@ -5825,17 +6238,17 @@ function menuIcons(roster) { return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; @@ -3243,7 +3273,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +6276,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5876,10 +6289,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); @@ -3257,7 +3287,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: button, type: "button", className: "bt-plus", -@@ -5889,8 +6289,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5889,8 +6302,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle @@ -3268,7 +3298,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "div", { ref: menu, -@@ -5900,9 +6300,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +@@ -5900,9 +6313,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, @@ -3281,7 +3311,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ), document.body) : null ); } -@@ -5929,6 +6329,7 @@ function mentionRows(roster, sessionId, query = "") { +@@ -5929,6 +6342,7 @@ function mentionRows(roster, sessionId, query = "") { if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { rows.push({ room: here.room, text: `@${everyone} ` }); } @@ -3289,7 +3319,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba return rows; } function chatRows(roster, list, sessionId) { -@@ -5959,14 +6360,25 @@ function atSources({ roster, sessions, launched, iconFor }) { +@@ -5959,14 +6373,25 @@ function atSources({ roster, sessions, launched, iconFor }) { order: -20, showGroupTitle: false, async candidates(session, { query }) { @@ -3317,7 +3347,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }, onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 }; -@@ -6122,15 +6534,15 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +@@ -6122,15 +6547,15 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { } // src/client/overlay-hooks.js @@ -3336,7 +3366,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); escapeLayers.push(ref); return () => { -@@ -6140,8 +6552,8 @@ function useEscape(onClose) { +@@ -6140,8 +6565,8 @@ function useEscape(onClose) { }, []); } function usePaneLeft() { @@ -3347,7 +3377,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const column = document.querySelector('[class*="_footArea"]')?.parentElement; if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); }, []); -@@ -6149,13 +6561,13 @@ function usePaneLeft() { +@@ -6149,13 +6574,13 @@ function usePaneLeft() { } // src/client/palette.js @@ -3365,7 +3395,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const wanted = query.trim().toLowerCase(); const close = () => { if (leaving) return; -@@ -6191,7 +6603,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { +@@ -6191,7 +6616,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { const people = [ ...roster.bots.filter((bot) => matchesName(bot.name, bot.role)).map((bot) => ({ key: bot.id, @@ -3374,7 +3404,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba title: bot.name, subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), badge: bot.hidden ? t("Hidden") : void 0, -@@ -6202,7 +6614,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { +@@ -6202,7 +6627,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { })), ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ key: room2.id, @@ -3383,7 +3413,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba title: room2.name, subtitle: t("{count} members", { count: room2.members.length }), badge: room2.hidden ? t("Hidden") : void 0, -@@ -6215,7 +6627,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { +@@ -6215,7 +6640,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { if (people.length) sections.push([t("Bots"), people]); const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ key: agent.id, @@ -3392,7 +3422,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba title: "DSH Agent", subtitle: t("Plain dsh session"), run: () => actions.openAgent(agent.id) -@@ -6228,16 +6640,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { +@@ -6228,16 +6653,16 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { key: `${id}:${turn}`, title: bot?.name ?? room2?.name ?? t("Bot"), subtitle: snippet, @@ -3414,7 +3444,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ].filter((command) => matchesName(command.title)); if (commands.length) sections.push([t("Commands"), commands]); const rows = sections.flatMap(([, items]) => items); -@@ -6247,7 +6659,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { +@@ -6247,7 +6672,7 @@ function CommandPalette({ start, token, roster, actions, useSessions }) { ${shown}`, { axis: "y", ...QUICK_EDGES }); const listRef = glide.box; useFlip(listRef, shown, "[data-flip]"); @@ -3423,7 +3453,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); }, [active, query]); const choose = (row) => { -@@ -6256,18 +6668,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); +@@ -6256,18 +6681,18 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); close(); }; let index = -1; @@ -3448,7 +3478,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba autoFocus: true, value: query, placeholder: t("Search Bots, chats and messages"), -@@ -6293,29 +6705,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); +@@ -6293,29 +6718,29 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); } } }), @@ -3490,7 +3520,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: row.key, -@@ -6329,14 +6741,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); +@@ -6329,14 +6754,14 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); }, onClick: () => choose(row) }, @@ -3510,7 +3540,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); }) )) -@@ -6346,7 +6758,7 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); +@@ -6346,7 +6771,7 @@ ${shown}`, { axis: "y", ...QUICK_EDGES }); } // src/client/model-menu.js @@ -3519,7 +3549,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var import_react_dom6 = require("react-dom"); var MENU_EXIT_MS = 120; function groupModels(models) { -@@ -6376,8 +6788,8 @@ function modelOf(bot, roster) { +@@ -6376,8 +6801,8 @@ function modelOf(bot, roster) { } var effortCache = /* @__PURE__ */ new Map(); function useEfforts(served, ref, actions) { @@ -3530,7 +3560,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba setFetched(void 0); if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; let live = true; -@@ -6402,12 +6814,12 @@ function effortRows(reasoning) { +@@ -6402,12 +6827,12 @@ function effortRows(reasoning) { ]; } function EffortRows({ reasoning, current, onPick }) { @@ -3548,7 +3578,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: row.effort ?? "default", -@@ -6416,17 +6828,17 @@ function EffortRows({ reasoning, current, onPick }) { +@@ -6416,17 +6841,17 @@ function EffortRows({ reasoning, current, onPick }) { "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, @@ -3572,7 +3602,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: model.ref, -@@ -6436,20 +6848,20 @@ function ModelRows({ roster, current, onPick }) { +@@ -6436,20 +6861,20 @@ function ModelRows({ roster, current, onPick }) { title: model.ref, onClick: () => onPick(model.ref) }, @@ -3601,7 +3631,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (place === null) return void 0; const onDown = (event) => { if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +6896,7 @@ function useAnchoredMenu() { +@@ -6484,6 +6909,7 @@ function useAnchoredMenu() { return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { @@ -3609,7 +3639,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); const shown = modelOf(bot, roster); const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6924,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo +@@ -6511,10 +6937,10 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); }; const models = roster.models ?? []; @@ -3623,7 +3653,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { ref: button, -@@ -6527,37 +6940,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo +@@ -6527,37 +6953,38 @@ function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffo "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, onClick: toggle }, @@ -3675,7 +3705,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const models = roster.models ?? []; const lost = bot.model ? !models.some((model) => model.ref === bot.model) : false; if (bot.model && (!lost || models.length === 0)) return null; -@@ -6570,12 +6984,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { +@@ -6570,12 +6997,12 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { }).finally(() => setBusy("")); }; const text = lost ? t("{model} is not available anymore. Pick another model for me.", { model: bot.model.split("/").slice(1).join("/") }) : models.length === 0 ? t("Hi, I'm {name}. I don't have a model yet. How do you want to set one up?", { name: bot.name }) : t("Hi, I'm {name}. Pick a model for me first. You can change it later in my details.", { name: bot.name }); @@ -3692,7 +3722,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: model.ref, type: "button", className: "bt-model-chip", -@@ -6584,29 +6998,49 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { +@@ -6584,29 +7011,49 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { "aria-label": `${model.name} · ${provider}`, title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, onClick: () => pick2(model.ref) @@ -3752,7 +3782,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function setupChoices(actions) { -@@ -6620,14 +7054,14 @@ function DeepSeekSignIn({ useUi, actions }) { +@@ -6620,14 +7067,14 @@ function DeepSeekSignIn({ useUi, actions }) { const open = useUi((state) => state.signIn); const entry = open ? actions.signInEntry?.() : null; if (!entry) return null; @@ -3772,7 +3802,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ...ops, ...hooks, t: actions.accountT(), -@@ -6676,17 +7110,17 @@ var MODEL_CSS = ` +@@ -6676,17 +7123,17 @@ var MODEL_CSS = ` `; // src/client/folder-menu.js @@ -3797,7 +3827,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { -@@ -6719,35 +7153,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6719,35 +7166,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -3849,7 +3879,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-folder-input", autoFocus: true, value: draft, -@@ -6759,33 +7193,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6759,33 +7206,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), @@ -3899,7 +3929,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { ref: button, -@@ -6798,19 +7232,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } +@@ -6798,19 +7245,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } title: value, onClick: toggle }, @@ -3925,7 +3955,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ), document.body) : null ); } -@@ -6830,8 +7264,8 @@ var FOLDER_CSS = ` +@@ -6830,8 +7277,8 @@ var FOLDER_CSS = ` `; // src/client/schedules.js @@ -3936,7 +3966,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var shared = null; var listeners = /* @__PURE__ */ new Set(); function publish(view) { -@@ -6839,8 +7273,8 @@ function publish(view) { +@@ -6839,8 +7286,8 @@ function publish(view) { for (const listener of listeners) listener(view); } function useSchedules(actions) { @@ -3947,7 +3977,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba listeners.add(setView); let stopped = false; let timer; -@@ -6848,7 +7282,7 @@ function useSchedules(actions) { +@@ -6848,7 +7295,7 @@ function useSchedules(actions) { if (!stopped) publish(next); }).catch(() => { }).finally(() => { @@ -3956,7 +3986,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }); load(); return () => { -@@ -6898,9 +7332,9 @@ function timingText(task) { +@@ -6898,9 +7345,9 @@ function timingText(task) { } var nextText = (task) => task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); function TaskRow({ task, actions, onView, showBot, roster }) { @@ -3969,7 +3999,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -6914,19 +7348,19 @@ function TaskRow({ task, actions, onView, showBot, roster }) { +@@ -6914,19 +7361,19 @@ function TaskRow({ task, actions, onView, showBot, roster }) { void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; @@ -3997,7 +4027,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, -@@ -6934,7 +7368,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { +@@ -6934,7 +7381,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove @@ -4006,7 +4036,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var pad = (number) => String(number).padStart(2, "0"); -@@ -6946,17 +7380,17 @@ function inAnHour() { +@@ -6946,17 +7393,17 @@ function inAnHour() { var KINDS = [["at", "Once"], ["daily", "Every day"], ["weekly", "Every week"], ["every", "Repeat"]]; var UNITS = [["minutes", 1, "minutes"], ["hours", 60, "hours"], ["days", 1440, "days"]]; function TaskForm({ roster, actions, start, onDone, onCancel }) { @@ -4035,7 +4065,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const timing = () => { if (kind === "at") return { kind, at: new Date(at).toISOString() }; if (kind === "every") return { kind, minutes: Math.round(Number(count) * UNITS.find(([id]) => id === unit)[1]) }; -@@ -6981,101 +7415,101 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { +@@ -6981,101 +7428,101 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { }); }; const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); @@ -4180,7 +4210,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba )) ]; } -@@ -7083,21 +7517,21 @@ function BotSchedules({ bot, roster, actions }) { +@@ -7083,21 +7530,21 @@ function BotSchedules({ bot, roster, actions }) { const view = useSchedules(actions); if (view === null) return null; const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; @@ -4210,7 +4240,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ] ) ); -@@ -7105,16 +7539,16 @@ function BotSchedules({ bot, roster, actions }) { +@@ -7105,16 +7552,16 @@ function BotSchedules({ bot, roster, actions }) { function ScheduleSummary({ bot, actions }) { const view = useSchedules(actions); const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; @@ -4233,7 +4263,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ))) ); } -@@ -7158,7 +7592,7 @@ var SCHEDULE_CSS = ` +@@ -7158,7 +7605,7 @@ var SCHEDULE_CSS = ` `; // src/client/capability-settings.js @@ -4242,7 +4272,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var blank = () => ({ name: "", kind: "mcp", description: "", transport: "stdio", command: "", argsText: "", envText: "{}", url: "", headersText: "{}", content: "", items: [] }); var parseMap = (text, label) => { let value; -@@ -7172,11 +7606,11 @@ var parseMap = (text, label) => { +@@ -7172,11 +7619,11 @@ var parseMap = (text, label) => { }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { @@ -4257,7 +4287,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, -@@ -7188,7 +7622,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl +@@ -7188,7 +7635,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { @@ -4266,7 +4296,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); -@@ -7219,95 +7653,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { +@@ -7219,95 +7666,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); @@ -4402,7 +4432,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); -@@ -7330,20 +7764,20 @@ function CapabilitiesPage({ roster, actions, run }) { +@@ -7330,20 +7777,20 @@ function CapabilitiesPage({ roster, actions, run }) { setConfirming(""); void run(actions.deleteCapability(item.name)); }; @@ -4430,7 +4460,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: item.name, item, confirming: confirming === item.name, -@@ -7359,10 +7793,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow +@@ -7359,10 +7806,10 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow const attached = new Set(bot.capabilities ?? []); const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; if (items.length === 0) return null; @@ -4443,7 +4473,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: item.name, label: item.name, hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7373,8 +7807,8 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow +@@ -7373,8 +7820,8 @@ function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow } // src/client/memory-panel.js @@ -4454,7 +4484,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,8 +7818,8 @@ function noteCount(botId, count) { +@@ -7384,8 +7831,8 @@ function noteCount(botId, count) { for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { @@ -4465,7 +4495,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const listener = () => setCount(counts.get(botId) ?? null); countListeners.add(listener); listener(); -@@ -7404,16 +7838,16 @@ function useMemoryCount(botId, actions) { +@@ -7404,16 +7851,16 @@ function useMemoryCount(botId, actions) { function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -4488,7 +4518,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -7428,45 +7862,45 @@ function ago(at, now) { +@@ -7428,45 +7875,45 @@ function ago(at, now) { return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { @@ -4553,7 +4583,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -7478,9 +7912,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { +@@ -7478,9 +7925,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { return groups; }, []); function EntryRow({ entry, remove }) { @@ -4566,7 +4596,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -7495,16 +7929,16 @@ function EntryRow({ entry, remove }) { +@@ -7495,16 +7942,16 @@ function EntryRow({ entry, remove }) { setBusy(true); if (!await remove(entry)) setBusy(false); }; @@ -4588,7 +4618,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, -@@ -7512,7 +7946,7 @@ function EntryRow({ entry, remove }) { +@@ -7512,7 +7959,7 @@ function EntryRow({ entry, remove }) { "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click @@ -4597,7 +4627,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function Entries({ bot, view, remove }) { -@@ -7521,25 +7955,25 @@ function Entries({ bot, view, remove }) { +@@ -7521,25 +7968,25 @@ function Entries({ bot, view, remove }) { { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; @@ -4634,7 +4664,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba )) ); } -@@ -7551,52 +7985,52 @@ function changeLine(change) { +@@ -7551,52 +7998,52 @@ function changeLine(change) { return change.scope === "team" ? t("Added to team memory: {text}", { text }) : t("Added: {text}", { text }); } function Reply({ answer, dismiss }) { @@ -4706,7 +4736,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba alive.current = true; void load({ summarize: true }); return () => { -@@ -7607,8 +8041,8 @@ function useMemoryView(botId, actions) { +@@ -7607,8 +8054,8 @@ function useMemoryView(botId, actions) { return { view, error, load, show }; } function useNow(every) { @@ -4717,7 +4747,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [every]); -@@ -7616,18 +8050,18 @@ function useNow(every) { +@@ -7616,18 +8063,18 @@ function useNow(every) { } function MemoryDialog({ bot, actions, leaving = false }) { const { view, error, load, show } = useMemoryView(bot.id, actions); @@ -4745,7 +4775,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!menu) return void 0; const onDown = (event) => { if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7689,52 +8123,52 @@ function MemoryDialog({ bot, actions, leaving = false }) { +@@ -7689,52 +8136,52 @@ function MemoryDialog({ bot, actions, leaving = false }) { const when3 = view?.summary ? ago(view.summary.at, now) : null; const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; const busy = answer?.pending === true; @@ -4821,7 +4851,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: inputRef, className: "bt-mem-input", value: text, -@@ -7744,10 +8178,10 @@ function MemoryDialog({ bot, actions, leaving = false }) { +@@ -7744,10 +8191,10 @@ function MemoryDialog({ bot, actions, leaving = false }) { "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), onChange: (event) => setText(event.target.value) }), @@ -4834,7 +4864,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ) -@@ -7756,7 +8190,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { +@@ -7756,7 +8203,7 @@ function MemoryDialog({ bot, actions, leaving = false }) { } // src/client/bot-settings.js @@ -4843,7 +4873,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var capitalize = (word) => word[0].toUpperCase() + word.slice(1); async function avatarImage(file) { if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +8214,8 @@ async function avatarImage(file) { +@@ -7780,8 +8227,8 @@ async function avatarImage(file) { throw new Error(t("That image is too large")); } function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { @@ -4854,7 +4884,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const commit = () => { const next = draft?.trim(); setDraft(null); -@@ -7792,7 +8226,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu +@@ -7792,7 +8239,7 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu if (next === void 0 || next === (value ?? "") || required && next === "") return; void onSave(next); }; @@ -4863,7 +4893,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", value: draft ?? value ?? "", placeholder, -@@ -7813,29 +8247,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu +@@ -7813,29 +8260,29 @@ function InlineText({ label, value, placeholder, required = false, maxLength, mu }); } function Row({ label, hint, stack = false, children }) { @@ -4901,7 +4931,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } function modelHint(bot, roster) { -@@ -7846,22 +8280,22 @@ function modelHint(bot, roster) { +@@ -7846,22 +8293,22 @@ function modelHint(bot, roster) { return t("Applies from the next message."); } function ShapeRing({ shape, size }) { @@ -4929,7 +4959,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: id, type: "button", className: "bt-shape", -@@ -7869,8 +8303,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { +@@ -7869,8 +8316,8 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { title: t(shapeOf(id).label), "aria-pressed": !hasPhoto && look.shape === id, onClick: () => onShape(id) @@ -4940,7 +4970,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: color, type: "button", className: "bt-color", -@@ -7878,19 +8312,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { +@@ -7878,19 +8325,19 @@ function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { title: t(capitalize(color)), "aria-pressed": look.color === color, onClick: () => onColor(color) @@ -4966,7 +4996,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const onDown = (event) => { if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; onClose(); -@@ -7908,7 +8342,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7908,7 +8355,7 @@ function AvatarEditor({ bot, actions, run, onClose }) { window.removeEventListener("keydown", onKey, true); }; }, [onClose]); @@ -4975,7 +5005,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (tab !== "upload") return void 0; const onPaste = (event) => { const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +8356,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7922,32 +8369,32 @@ function AvatarEditor({ bot, actions, run, onClose }) { }, [tab, bot]); const hasPhoto = Boolean(look.image); const glide = useGlide(tab); @@ -5016,7 +5046,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "div", { className: "bt-dropzone", -@@ -7963,15 +8397,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7963,15 +8410,15 @@ function AvatarEditor({ bot, actions, run, onClose }) { upload(event.dataTransfer.files?.[0]); } }, @@ -5038,7 +5068,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const file = event.target.files?.[0]; event.target.value = ""; upload(file); -@@ -7982,22 +8416,22 @@ function AvatarEditor({ bot, actions, run, onClose }) { +@@ -7982,22 +8429,22 @@ function AvatarEditor({ bot, actions, run, onClose }) { function MemoryCard({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -5069,7 +5099,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 4e3); return () => clearTimeout(timer); -@@ -8011,112 +8445,113 @@ function BotSettingsForm({ bot, roster, actions, run }) { +@@ -8011,112 +8458,113 @@ function BotSettingsForm({ bot, roster, actions, run }) { if (confirming) void run(actions.deleteBot(bot.id)); else setConfirming(true); }; @@ -5230,7 +5260,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ); -@@ -8176,7 +8611,7 @@ function createPayload({ name, look, image, model, effort, brief }) { +@@ -8176,7 +8624,7 @@ function createPayload({ name, look, image, model, effort, brief }) { } // src/client/create-bot.js @@ -5239,7 +5269,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var pick = (list) => list[Math.floor(Math.random() * list.length)]; var randomLook = (avoid = {}) => { const shapes = DEFAULT_SHAPES.filter((shape) => shape !== avoid.shape); -@@ -8184,17 +8619,17 @@ var randomLook = (avoid = {}) => { +@@ -8184,17 +8632,17 @@ var randomLook = (avoid = {}) => { return { shape: pick(shapes), color: pick(colors) }; }; function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCreated, inline = false }) { @@ -5267,7 +5297,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8219,25 +8654,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8219,25 +8667,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; @@ -5301,7 +5331,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba look, hasPhoto: Boolean(image), onShape: (shape) => { -@@ -8249,27 +8684,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8249,27 +8697,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr setLook((current) => ({ ...current, color })); } }), @@ -5337,7 +5367,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-input", autoFocus: !inline, value: name, -@@ -8284,55 +8719,55 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr +@@ -8284,55 +8732,55 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr } } }), @@ -5420,7 +5450,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const close = () => actions.closeOverlay(); useEscape(close); const paneLeft = usePaneLeft(); -@@ -8418,14 +8853,14 @@ function NewChat({ mode, roster, actions }) { +@@ -8418,14 +8866,14 @@ function NewChat({ mode, roster, actions }) { }); const options = []; if (!group && picked.length === 0 && mode !== "search" && wanted === "") { @@ -5438,7 +5468,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba await actions.addAgent(); close(); }) }); -@@ -8434,19 +8869,19 @@ function NewChat({ mode, roster, actions }) { +@@ -8434,19 +8882,19 @@ function NewChat({ mode, roster, actions }) { for (const bot of matches) { options.push({ key: bot.id, bot, label: bot.name, hint: choosingAdmin ? t("Make admin") : group || picked.length > 0 ? t("Add to group chat") : void 0, run: () => openBot(bot) }); } @@ -5462,7 +5492,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba roster, actions, start: draft, -@@ -8467,33 +8902,33 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8467,33 +8915,33 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; @@ -5509,7 +5539,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: inputRef, autoFocus: true, value: query, -@@ -8517,20 +8952,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8517,20 +8965,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } else if (event.key === "Backspace" && query === "") unpick(); } }), @@ -5535,7 +5565,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: option.key, -@@ -8542,21 +8977,21 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8542,21 +8990,21 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); onMouseEnter: () => setCursor(index), onClick: option.run }, @@ -5566,7 +5596,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba rows: 1, value: message, placeholder, -@@ -8569,7 +9004,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8569,7 +9017,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } } }), @@ -5575,7 +5605,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ) -@@ -8577,18 +9012,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); +@@ -8577,18 +9025,18 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } // src/client/exchange.js @@ -5598,7 +5628,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba let stop = false; let timer; let seen = null; -@@ -8612,48 +9047,48 @@ function ExchangeDialog({ pair, roster, actions }) { +@@ -8612,48 +9060,48 @@ function ExchangeDialog({ pair, roster, actions }) { const right = roster.byId[pair[1]]; let lastDay = 0; const paneLeft = usePaneLeft(); @@ -5663,7 +5693,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var plainSpeech = (text) => stripHeader(text).replace(/```[\s\S]*?```/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_`#>|~-]+/g, " ").replace(/\s+/g, " ").trim(); var VOICE_LABELS = { listening: "Listening", -@@ -8665,8 +9100,8 @@ var VOICE_LABELS = { +@@ -8665,8 +9113,8 @@ var VOICE_LABELS = { }; var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; function useMicLevel(enabled) { @@ -5674,7 +5704,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!enabled || !navigator.mediaDevices?.getUserMedia) return void 0; let stream = null; let audio = null; -@@ -8716,18 +9151,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8716,18 +9164,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); @@ -5704,7 +5734,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; -@@ -8747,12 +9182,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8747,12 +9195,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } dictation.stop(); submit(text); }); @@ -5719,7 +5749,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +9202,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8767,16 +9215,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } }; }, [speech]); const listen = phase === "listening"; @@ -5739,7 +5769,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; -@@ -8838,7 +9273,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8838,7 +9286,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); @@ -5748,7 +5778,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (phase !== "thinking" || running || waiting.current === null) return; const [count, text] = [lastReply.slice(0, lastReply.indexOf("|")), lastReply.slice(lastReply.indexOf("|") + 1)]; if (count === waiting.current || !text.trim()) return; -@@ -8858,7 +9293,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8858,7 +9306,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); @@ -5757,7 +5787,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,50 +9310,50 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } +@@ -8875,50 +9323,50 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; @@ -5825,7 +5855,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var ROOM_MODES = [ ["admin", "Admin leads", "Plain messages go to the admin, who calls others with @Name."], ["everyone", "Everyone", "Every member answers in turn, admin first."] -@@ -8929,9 +9364,9 @@ function roomSubtitle(room2, roster) { +@@ -8929,9 +9377,9 @@ function roomSubtitle(room2, roster) { return [t("{count} members", { count: room2.members.length }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); } function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { @@ -5838,7 +5868,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 4e3); return () => clearTimeout(timer); -@@ -8955,41 +9390,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { +@@ -8955,41 +9403,41 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { if (done) onDeleted?.(); }); }; @@ -5894,7 +5924,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: id, type: "button", "aria-pressed": mode === id, -@@ -8999,86 +9434,86 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { +@@ -8999,86 +9447,86 @@ function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { if (mode !== id) void update({ mode: id }); } }, t(label)))), @@ -6014,7 +6044,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); const pickAdmin = (id) => { setAdmin(id); -@@ -9094,48 +9529,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +@@ -9094,48 +9542,48 @@ function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { if (done && room2) onCreated(room2); else setBusy(false); }; @@ -6078,7 +6108,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -9294,7 +9729,7 @@ function percent(part, whole) { +@@ -9294,7 +9742,7 @@ function percent(part, whole) { } // src/client/usage-page.js @@ -6087,7 +6117,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var HEAT_WEEKS = 53; var TOP_SERIES = 7; var RING_R = 42; -@@ -9341,9 +9776,9 @@ function titleOf(range, bucket) { +@@ -9341,9 +9789,9 @@ function titleOf(range, bucket) { return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { @@ -6100,7 +6130,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); -@@ -9363,9 +9798,9 @@ function useTween(value) { +@@ -9363,9 +9811,9 @@ function useTween(value) { }, [value]); return shown; } @@ -6112,7 +6142,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +9817,7 @@ function ownerFace(owner, roster) { +@@ -9382,7 +9830,7 @@ function ownerFace(owner, roster) { sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], @@ -6121,7 +6151,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }; } if (owner.kind === "room") { -@@ -9392,7 +9827,7 @@ function ownerFace(owner, roster) { +@@ -9392,7 +9840,7 @@ function ownerFace(owner, roster) { name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", @@ -6130,7 +6160,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }; } if (owner.kind === "agent") { -@@ -9414,30 +9849,30 @@ function Tabs({ label, value, options, onChange, boxRef }) { +@@ -9414,30 +9862,30 @@ function Tabs({ label, value, options, onChange, boxRef }) { const names = options.map(([, text]) => text).join("\n"); const glide = useGlide(`${value} ${names}`, { box: boxRef }); @@ -6171,7 +6201,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +9890,83 @@ function niceTop(max) { +@@ -9455,83 +9903,83 @@ function niceTop(max) { return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { @@ -6282,7 +6312,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9975,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick +@@ -9540,69 +9988,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -6379,7 +6409,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; -@@ -9628,17 +10063,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9628,17 +10076,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { const [from, to] = [anchor, key].sort(); onPick(from, to); }; @@ -6400,7 +6430,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key, type: "button", className: "bt-us-day", -@@ -9654,15 +10089,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9654,15 +10102,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) @@ -6422,7 +6452,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); }; const shortcuts = [ -@@ -9671,30 +10106,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { +@@ -9671,30 +10119,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; @@ -6461,7 +6491,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); -@@ -9710,13 +10145,13 @@ function Heat({ view, keep, paint, index }) { +@@ -9710,13 +10158,13 @@ function Heat({ view, keep, paint, index }) { for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { @@ -6477,7 +6507,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: ms, className: "bt-us-cell", style: { "--w": week }, -@@ -9724,7 +10159,7 @@ function Heat({ view, keep, paint, index }) { +@@ -9724,7 +10172,7 @@ function Heat({ view, keep, paint, index }) { "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) @@ -6486,7 +6516,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } } const tipSide = hover ? { -@@ -9733,61 +10168,61 @@ function Heat({ view, keep, paint, index }) { +@@ -9733,61 +10181,61 @@ function Heat({ view, keep, paint, index }) { } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); @@ -6574,7 +6604,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +10232,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9797,7 +10245,7 @@ function UsagePage({ roster, actions, focusId }) { const value = await actions.usage(); await resized; if (stopped) return; @@ -6583,7 +6613,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { -@@ -9814,7 +10249,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9814,7 +10262,7 @@ function UsagePage({ roster, actions, focusId }) { }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); @@ -6592,7 +6622,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +10258,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9823,7 +10271,7 @@ function UsagePage({ roster, actions, focusId }) { const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; @@ -6601,7 +6631,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +10306,23 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9871,23 +10319,23 @@ function UsagePage({ roster, actions, focusId }) { }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); @@ -6633,7 +6663,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -9912,7 +10347,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9912,7 +10360,7 @@ function UsagePage({ roster, actions, focusId }) { ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; @@ -6642,7 +6672,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), -@@ -9924,7 +10359,7 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9924,7 +10372,7 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); @@ -6651,7 +6681,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: `m${at}`, kind: "model", glyph: models[at].glyph(32), -@@ -9936,30 +10371,30 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9936,30 +10384,30 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); @@ -6692,7 +6722,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ]; } else { -@@ -9968,39 +10403,39 @@ function UsagePage({ roster, actions, focusId }) { +@@ -9968,39 +10416,39 @@ function UsagePage({ roster, actions, focusId }) { const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ @@ -6746,7 +6776,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba body ); } -@@ -10157,12 +10592,12 @@ button.bt-us-row:hover{background:var(--bt-hover)} +@@ -10157,12 +10605,12 @@ button.bt-us-row:hover{background:var(--bt-hover)} `; // src/client/connectors.js @@ -6762,7 +6792,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); var MARKS = { github: GitHubMark }; var ABOUT = { github: () => t("Repositories, issues, and pull requests") }; -@@ -10173,10 +10608,10 @@ function statusText2(connector) { +@@ -10173,10 +10621,10 @@ function statusText2(connector) { return ABOUT[connector.id]?.() ?? ""; } function ConnectorRow({ connector, actions }) { @@ -6777,7 +6807,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const run = (task, after) => { setBusy(true); setError(null); -@@ -10193,63 +10628,63 @@ function ConnectorRow({ connector, actions }) { +@@ -10193,63 +10641,63 @@ function ConnectorRow({ connector, actions }) { }; const on = connector.state !== "off"; const Mark = MARKS[connector.id]; @@ -6864,7 +6894,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); function download(name, text) { const url = URL.createObjectURL(new Blob([text], { type: "text/plain;charset=utf-8" })); -@@ -10272,16 +10707,16 @@ function entrySummary(entry) { +@@ -10272,16 +10720,16 @@ function entrySummary(entry) { } var SOURCES = { turn: "Model call", host: "DS Bot", api: "DS Bot", browser: "This window" }; function FeedbackPage({ actions, run, Section, Row: Row3 }) { @@ -6886,7 +6916,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +10724,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10289,7 +10737,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); @@ -6895,7 +6925,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; -@@ -10317,59 +10752,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10317,59 +10765,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ @@ -6979,7 +7009,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ]; -@@ -10377,8 +10812,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { +@@ -10377,8 +10825,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); @@ -6990,7 +7020,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +10824,16 @@ function ErrorToast({ useUi, actions }) { +@@ -10389,15 +10837,16 @@ function ErrorToast({ useUi, actions }) { }; }, [toast?.token]); if (!toast) return null; @@ -7012,7 +7042,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var FEEDBACK_CSS = ` -@@ -10428,7 +10864,7 @@ var FEEDBACK_CSS = ` +@@ -10428,7 +10877,7 @@ var FEEDBACK_CSS = ` `; // src/client/about.js @@ -7021,7 +7051,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var CHEER_MS = 1800; var starCount = (count) => new Intl.NumberFormat(dateLocale(), { notation: "compact", maximumFractionDigits: 1 }).format(count); function botHint(update) { -@@ -10454,9 +10890,9 @@ function dshHint(dsh, shown) { +@@ -10454,9 +10903,9 @@ function dshHint(dsh, shown) { var linkProps = (href) => ({ className: "bt-soft bt-soft-sm bt-about-link", href, target: "_blank", rel: "noopener noreferrer" }); function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { const update = useUpdate((value) => value) ?? {}; @@ -7034,7 +7064,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); -@@ -10466,8 +10902,8 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { +@@ -10466,8 +10915,8 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { live = false; }; }, []); @@ -7045,7 +7075,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!cheer) return void 0; const timer = setTimeout(() => setCheer(false), CHEER_MS); return () => clearTimeout(timer); -@@ -10482,35 +10918,35 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { +@@ -10482,35 +10931,35 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { const phase = update.phase ?? "idle"; const dsh = update.dsh; const dshPhase = dsh?.phase ?? "idle"; @@ -7091,7 +7121,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "a", { className: "bt-star", -@@ -10520,23 +10956,23 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { +@@ -10520,23 +10969,23 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { title: t("Star DS Bot on GitHub"), "aria-label": stars === null ? t("Star DS Bot on GitHub") : `${t("Star DS Bot on GitHub")} · ${t("{count} stars", { count: stars })}` }, @@ -7125,7 +7155,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ]; } -@@ -10569,54 +11005,54 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s +@@ -10569,54 +11018,54 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s `; // src/client/settings.js @@ -7196,7 +7226,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: ink, type: "button", className: "bt-swatch", -@@ -10626,21 +11062,21 @@ function GeneralPage({ roster, actions, run }) { +@@ -10626,21 +11075,21 @@ function GeneralPage({ roster, actions, run }) { style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), @@ -7223,7 +7253,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ) ]; -@@ -10649,52 +11085,52 @@ function MainBotsSection({ roster, actions, run }) { +@@ -10649,52 +11098,52 @@ function MainBotsSection({ roster, actions, run }) { const mains = (roster.mainBotIds ?? []).map((id) => roster.byId[id]).filter(Boolean); const [first, ...others] = mains; const candidates = roster.bots.filter((bot) => !isMainOf(roster, bot.id)); @@ -7289,7 +7319,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: id, type: "button", "aria-pressed": motion === id, -@@ -10706,69 +11142,69 @@ function MotionSection({ roster, actions, run }) { +@@ -10706,69 +11155,69 @@ function MotionSection({ roster, actions, run }) { ); } function BotsPage({ roster, actions, run }) { @@ -7388,7 +7418,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10786,32 +11222,32 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false +@@ -10786,32 +11235,32 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false const nav = useGlide(shown, { axis: "y" }); const pageKey = `${shown}:${bot?.id ?? room2?.id ?? ""}`; const depth = bot || room2 ? 1 : 0; @@ -7432,7 +7462,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-settings-item bt-settings-shell", title: t("Models, providers, and the rest of DeepSeek Harness"), -@@ -10819,64 +11255,65 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false +@@ -10819,64 +11268,65 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false close(); openHarnessSettings(); } @@ -7524,7 +7554,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; -@@ -10901,20 +11338,21 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { +@@ -10901,20 +11351,21 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null ], @@ -7551,7 +7581,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: label, type: "button", role: "menuitem", -@@ -10923,14 +11361,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { +@@ -10923,14 +11374,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -7570,7 +7600,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +11390,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { +@@ -10952,14 +11403,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; @@ -7590,7 +7620,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: label, type: "button", role: "menuitem", -@@ -10968,7 +11406,7 @@ function AgentMenu({ menu, roster, actions, leaving }) { +@@ -10968,7 +11419,7 @@ function AgentMenu({ menu, roster, actions, leaving }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } @@ -7599,7 +7629,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba )) ); } -@@ -11039,7 +11477,7 @@ function migrateStorage(storage = globalThis.localStorage) { +@@ -11039,7 +11490,7 @@ function migrateStorage(storage = globalThis.localStorage) { } // src/client/member-status.js @@ -7608,7 +7638,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; var ROWS_MAX = 4; function memberWork(room2, work = NO_WORK) { -@@ -11077,35 +11515,35 @@ function summaryText(busy) { +@@ -11077,35 +11528,35 @@ function summaryText(busy) { return { waiting, doing }; } function Face({ entry, size }) { @@ -7656,7 +7686,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -11119,20 +11557,20 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) { +@@ -11119,20 +11570,20 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) { const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : []; const shown = busy.length > 0 || groups.length > 0; const now = useNow2(shown); @@ -7682,7 +7712,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: item.room.id, bot, tool: item.tool, -@@ -11143,7 +11581,7 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) { +@@ -11143,7 +11594,7 @@ function MemberDock({ sessionId, useRoster, useWork, actions }) { }))); } function useFlowColumn(ref, active, key) { @@ -7691,7 +7721,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const node = ref.current; const flow = document.querySelector("[data-chat-flow]"); if (!active || !node || !flow) return void 0; -@@ -11160,8 +11598,8 @@ function useFlowColumn(ref, active, key) { +@@ -11160,8 +11611,8 @@ function useFlowColumn(ref, active, key) { }, [active, key]); } function useNow2(active) { @@ -7702,7 +7732,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!active) return void 0; const timer = setInterval(() => setNow(Date.now()), 3e4); return () => clearInterval(timer); -@@ -11170,38 +11608,38 @@ function useNow2(active) { +@@ -11170,38 +11621,38 @@ function useNow2(active) { } var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; function ApprovalCard({ approval, bot, actions }) { @@ -7756,7 +7786,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } if (entry.state !== "work") return null; -@@ -11210,14 +11648,14 @@ function MemberWork({ entry, bot, room: room2, roster, actions, now }) { +@@ -11210,14 +11661,14 @@ function MemberWork({ entry, bot, room: room2, roster, actions, now }) { setStopping(true); void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); }; @@ -7775,7 +7805,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) ); } -@@ -11264,7 +11702,7 @@ body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a} +@@ -11264,7 +11715,7 @@ body[data-ds-dark-theme] .bt-mw-ask{color:#f0b84a} `; // src/client/details-drawer.js @@ -7784,7 +7814,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba function transcriptOf(list, id, name, hiddenTurns = []) { const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; if (!Array.isArray(outline)) return ""; -@@ -11289,10 +11727,10 @@ var EMPTY_TABS = { ids: [], active: null }; +@@ -11289,10 +11740,10 @@ var EMPTY_TABS = { ids: [], active: null }; var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { @@ -7797,7 +7827,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; -@@ -11302,14 +11740,14 @@ function PermissionRow({ bot, roster, actions, run }) { +@@ -11302,14 +11753,14 @@ function PermissionRow({ bot, roster, actions, run }) { setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; @@ -7816,7 +7846,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +11761,42 @@ function PermissionRow({ bot, roster, actions, run }) { +@@ -11323,42 +11774,42 @@ function PermissionRow({ bot, roster, actions, run }) { function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -7874,7 +7904,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), -@@ -11367,71 +11805,71 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow +@@ -11367,71 +11818,71 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow void actions.browserCloseTab(bot.id, tabId); } }, "×"), @@ -7974,7 +8004,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) : null ]; } -@@ -11439,15 +11877,15 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11439,15 +11890,15 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const owner = roster.byId[sessionId]; const part = owner ? currentPart(owner) : sessionId; const isMainBot = isMainOf(roster, owner?.id); @@ -7997,7 +8027,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; -@@ -11472,36 +11910,36 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11472,36 +11923,36 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setTimeout(() => setCopied(false), 1600); }); }; @@ -8048,7 +8078,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ]; } const current = bot?.name ?? room2.name; -@@ -11515,12 +11953,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11515,12 +11966,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ @@ -8065,7 +8095,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,55 +11973,55 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a +@@ -11535,55 +11986,55 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setEditing(false); } } @@ -8144,7 +8174,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var import_react_dom8 = require("react-dom"); var PANEL_LINGER_MS = 300; var WIDTH_KEY = STORAGE.browserWidth; -@@ -11598,10 +12036,10 @@ var EMPTY_TABS2 = { ids: [], active: null }; +@@ -11598,10 +12049,10 @@ var EMPTY_TABS2 = { ids: [], active: null }; var SPRING_LEAD2 = springEasing(576, 39.4); var SPRING_TRAIL2 = springEasing(144, 19.7); var icon = (paths) => function BrowserIcon() { @@ -8157,7 +8187,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); }; var GlobeIcon = icon(["M10 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M3 10h14", "M10 3c2 2.2 2.9 4.5 2.9 7S12 14.8 10 17c-2-2.2-2.9-4.5-2.9-7S8 5.2 10 3z"]); -@@ -11613,9 +12051,9 @@ var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); +@@ -11613,9 +12064,9 @@ var CloseIcon2 = icon(["M5.5 5.5l9 9M14.5 5.5l-9 9"]); var PlusTabIcon = icon(["M10 5v10M5 10h10"]); var LockIcon2 = icon(["M6.5 9V7a3.5 3.5 0 0 1 7 0v2", "M5.5 9h9v7h-9z"]); function Favicon({ src, className }) { @@ -8170,7 +8200,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } var savedWidth = () => { try { -@@ -11674,11 +12112,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref +@@ -11674,11 +12125,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); @@ -8187,7 +8217,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; -@@ -11707,7 +12145,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11707,7 +12158,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, trail.cancel(); }; }); @@ -8196,7 +8226,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { -@@ -11721,13 +12159,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11721,13 +12172,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, observer.observe(box); return () => observer.disconnect(); }, []); @@ -8213,7 +8243,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { type: "button", -@@ -11737,13 +12175,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11737,13 +12188,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, title: bot.name, onClick: () => onView("details") }, @@ -8230,7 +8260,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: tabId, -@@ -11764,9 +12202,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11764,9 +12215,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, } } }, @@ -8243,7 +8273,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), -@@ -11778,20 +12216,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, +@@ -11778,20 +12229,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, }, "×") ); }), @@ -8271,7 +8301,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,7 +12255,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11817,7 +12268,7 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -8280,7 +8310,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba setDraft(null); if (landed && !page.url) input.current?.focus(); }, [tabId, landed]); -@@ -11833,27 +12271,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11833,27 +12284,27 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) }; const editing = draft !== null; const secure = /^https:/i.test(page.url); @@ -8317,7 +8347,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ref: input, type: "text", spellCheck: false, -@@ -11877,25 +12315,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) +@@ -11877,25 +12328,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) } }) ), @@ -8353,7 +8383,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ) : null ) ); -@@ -11907,41 +12345,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -11907,41 +12358,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); @@ -8411,7 +8441,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba let timer; const onResize = () => { setResizing(true); -@@ -11955,28 +12393,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -11955,28 +12406,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu window.removeEventListener("resize", onResize); }; }, []); @@ -8447,7 +8477,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +12451,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12013,7 +12464,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu return actions[name](...args); }; } @@ -8456,7 +8486,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba const move = fly.current; if (!move) return; fly.current = null; -@@ -12096,7 +12534,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12096,7 +12547,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; @@ -8465,7 +8495,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "aside", { ref: root, -@@ -12113,15 +12551,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu +@@ -12113,15 +12564,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu if (event.target === event.currentTarget) setLanded(true); } }, @@ -8486,7 +8516,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, -@@ -12218,7 +12656,7 @@ var BROWSER_CSS = ` +@@ -12218,7 +12669,7 @@ var BROWSER_CSS = ` `; // src/client/overlays.js @@ -8495,7 +8525,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var import_react_dom9 = require("react-dom"); var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; -@@ -12227,15 +12665,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity +@@ -12227,15 +12678,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); @@ -8515,7 +8545,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (open) { setKept(open); return void 0; -@@ -12247,17 +12685,17 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity +@@ -12247,17 +12698,17 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); const [memory, memoryLeaving] = useLinger(ui2.memory && roster.byId[ui2.memory] ? ui2.memory : null, DIALOG_EXIT_MS); const [menu, menuLeaving] = useLinger(ui2.menu, MENU_EXIT_MS3); @@ -8543,7 +8573,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } -@@ -13105,7 +13543,7 @@ function writeMode(mode, storage = globalThis.localStorage) { +@@ -13105,7 +13556,7 @@ function writeMode(mode, storage = globalThis.localStorage) { } // src/client/user-message.js @@ -8552,7 +8582,18 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { -@@ -13137,38 +13575,38 @@ function userTextParts(text, bots, referenceLabels = []) { +@@ -13121,8 +13572,8 @@ function contentParts(content) { + } + function mentionableBots(roster, sessionId) { + const room2 = roster.roomsById[sessionId]; +- if (room2) return room2.members.map((id) => roster.byId[id]).filter(Boolean); +- return roster.byId[sessionId] ? roster.bots : []; ++ const bots = room2 ? room2.members.map((id) => roster.byId[id]).filter(Boolean) : roster.byId[sessionId] ? roster.bots : []; ++ return bots.concat(bots.filter((bot) => typeof bot?.remote?.handle === "string").map((bot) => ({ ...bot, name: bot.remote.handle.replace(/^@+/, ""), mentionLabel: bot.name }))); + } + function userTextParts(text, bots, referenceLabels = []) { + const parts = []; +@@ -13137,38 +13588,38 @@ function userTextParts(text, bots, referenceLabels = []) { return parts; } function UserText({ text, bots, referenceLabels, skillNames, references, actions }) { @@ -8561,9 +8602,10 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba "button", { key: index, type: "button", className: "bt-umention", title: `@${part.bot.name}`, onClick: () => actions.openSession(part.bot.id) }, - (0, import_react34.createElement)(BotMark, { bot: part.bot, size: 16 }), -+ (0, import_react35.createElement)(BotMark, { bot: part.bot, size: 16 }), - part.label +- part.label - ) : (0, import_react34.createElement)(import_react34.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references))); ++ (0, import_react35.createElement)(BotMark, { bot: part.bot, size: 16 }), ++ part.bot.mentionLabel ?? part.label + ) : (0, import_react35.createElement)(import_react35.Fragment, { key: index }, ui.projectUserText(part.text, referenceLabels, skillNames, "skill", references))); } function Attachments({ attachments, renderMessageImages }) { @@ -8601,7 +8643,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba } var clock3 = (time) => typeof time === "number" ? new Date(time).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }) : void 0; function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageImages, openFile, openSkill }) { -@@ -13178,22 +13616,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma +@@ -13178,22 +13629,22 @@ function UserMessageCell({ node, sessionId, useRoster, actions, renderMessageIma const referenceLabels = data.referenceLabels ?? []; const references = openFile && openSkill ? { openFile, openSkill } : void 0; const steering = node.kind === "steering"; @@ -8632,7 +8674,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ); } var USER_MESSAGE_CSS = ` -@@ -13250,6 +13688,14 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since +@@ -13250,6 +13701,14 @@ function noticeApprovals({ work, roster, shownSession, openSession, since: since // src/client/index.js var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; function apply(ctx) { @@ -8647,7 +8689,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ctx.effect(() => installLocale(ctx.get("locale")), "ds-bot: interface copy"); const roster = createSource(EMPTY_ROSTER); migrateStorage(); -@@ -13501,7 +13947,7 @@ function apply(ctx) { +@@ -13501,7 +13960,7 @@ function apply(ctx) { }; render(); const offRoster = roster.subscribe(render); @@ -8656,7 +8698,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba return () => { offRoster(); offRegistry(); -@@ -13520,7 +13966,7 @@ function apply(ctx) { +@@ -13520,7 +13979,7 @@ function apply(ctx) { const accent = isHex(prefs.accent) ? prefs.accent : void 0; const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal"; document.body.dataset.btMotion = motion; @@ -8665,7 +8707,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba if (next === key) return; key = next; style.textContent = themeCss(themes[id], accent); -@@ -13536,7 +13982,7 @@ function apply(ctx) { +@@ -13536,7 +13995,7 @@ function apply(ctx) { }; render(); const offRoster = roster.subscribe(render); @@ -8674,7 +8716,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba return () => { offRoster(); offRegistry(); -@@ -13846,6 +14292,8 @@ function apply(ctx) { +@@ -13846,6 +14305,8 @@ function apply(ctx) { noteClientError(where, error); ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, @@ -8683,7 +8725,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { -@@ -13890,6 +14338,7 @@ function apply(ctx) { +@@ -13890,6 +14351,7 @@ function apply(ctx) { actions.closePanel = () => { ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state); }; @@ -8691,7 +8733,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null; actions.canSignIn = () => Boolean(actions.signInEntry()); actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 })); -@@ -13964,7 +14413,7 @@ function apply(ctx) { +@@ -13964,7 +14426,7 @@ function apply(ctx) { }); const browserPages = browsers?.pages ?? createSource({}); const browserTabs = browsers?.tabs ?? createSource({}); @@ -8700,7 +8742,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba ctx.effect(() => noticeApprovals({ work, roster, -@@ -14024,6 +14473,7 @@ function apply(ctx) { +@@ -14024,6 +14486,7 @@ function apply(ctx) { installCompiledStyles(SCHEDULE_CSS, "schedules"), installCompiledStyles(FEEDBACK_CSS, "feedback"), installCompiledStyles(ABOUT_CSS, "about"), @@ -8708,7 +8750,7 @@ index f46d64171400ccaa6c00e4577c308297a70a0b4d..e007c4a726a74d40b0be2a32659dd8ba installCompiledStyles(MEMBER_CSS, "members"), installCompiledStyles(USER_MESSAGE_CSS, "user-messages"), browsers ? installCompiledStyles(BROWSER_CSS, "browser") : null, -@@ -14039,7 +14489,8 @@ function apply(ctx) { +@@ -14039,7 +14502,8 @@ function apply(ctx) { shadow("settings.launcher", { priority: -10, inject: face }, AccountLauncher), shadow("shell.overlay", { id: "bot", order: 50, inject: face }, Overlays), shadow("shell.overlay", { id: "bot-signin", order: 55, inject: face }, DeepSeekSignIn), diff --git a/patches/ds-bot/topics.json b/patches/ds-bot/topics.json index ad9186ac1..59bd46f18 100644 --- a/patches/ds-bot/topics.json +++ b/patches/ds-bot/topics.json @@ -36,9 +36,11 @@ "src/client/context-menu.js", "src/client/at-menu.js", "src/client/bot-settings.js", + "src/client/user-message.js", "src/client/styles.js", "src/client/icons.js", "test/flybot-client.test.js", + "test/mention-rule.test.js", "test/bot-tree.test.js" ], "0004-flybot-api-types": [