diff --git a/changelog.d/2247-readonly-banner-reload.fixed.md b/changelog.d/2247-readonly-banner-reload.fixed.md new file mode 100644 index 00000000..2cb8117e --- /dev/null +++ b/changelog.d/2247-readonly-banner-reload.fixed.md @@ -0,0 +1 @@ +A read-only FlyBot window's sidebar banner now offers **Reload** in both the wide and the rail shape: it re-reads the whole team instead of reloading the page, so the banner and every refused control clear as soon as the other window has let go, and an unsent composer line is not lost. ([#2247](https://github.com/ThinkFlowLab/FlyBot/issues/2247)). diff --git a/core/client.js b/core/client.js index 26ab9de3..907ee5f4 100644 --- a/core/client.js +++ b/core/client.js @@ -2817,6 +2817,11 @@ function createSource(initial) { } var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], humans: [], self: null, agents: [], exchangeTurns: {}, remoteTypedTurns: {}, remoteAwaiting: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }; var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id); +var stateRequest = (current, { fresh = false } = {}) => ({ + since: fresh ? void 0 : current?.revision, + models: current?.modelsKey ?? "" +}); +var stateAnswerIsStale = (value, current, { fresh = false } = {}) => value?.unchanged === true || !fresh && value?.revision === current?.revision && value?.modelsKey === current?.modelsKey; function indexRoster(value) { setLocalBots(value.bots); setPluginTools(value.pluginTools); @@ -10567,6 +10572,13 @@ function TeamSidebar(props) { }, [roster, updated]); const main = roster.byId[roster.mainBotId]; const readOnlySaid = typeof roster.readOnly === "string" && roster.readOnly.trim() !== ""; + const readOnlyReload = (0, import_react18.createElement)("button", { + type: "button", + className: "bt-read-only-reload", + onClick: () => { + void Promise.resolve(actions.reloadTeam?.()).catch((error) => actions.reportError(error, "reload")); + } + }, t("Reload")); const structure = useSidebarStructure(); const [collapsed, setCollapsed] = (0, import_react18.useState)({}); const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); @@ -10599,7 +10611,9 @@ function TeamSidebar(props) { (0, import_react18.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react18.createElement)(PlusIcon)) ), wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + wide && readOnlySaid ? readOnlyReload : null, !wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, + !wide && readOnlySaid ? readOnlyReload : null, (0, import_react18.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), (0, import_react18.createElement)(KanbanRow, { wide, useUi, actions }), (0, import_react18.createElement)(ProjectsNav, { wide }), @@ -10961,6 +10975,9 @@ var SIDEBAR_CSS = ` .bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} .bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} .bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} +.bt-read-only-reload:hover{background:var(--bt-hover)} +.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} [class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} [class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} .bt-foot-account{order:-1;flex:none;display:inline-flex} @@ -22172,10 +22189,11 @@ function apply(ctx) { const refreshed = /* @__PURE__ */ new Set(); let pulls = 0; let applied = 0; - const pull = async () => { + let reloading = 0; + const pull = async ({ fresh = false } = {}) => { const order = ++pulls; const known = roster.getSnapshot(); - const value = await call("state", { since: known.revision, models: known.modelsKey ?? "", locale: language(), watching: !document.hidden }); + const value = await call("state", { ...stateRequest(known, { fresh }), locale: language(), watching: !document.hidden }); if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); @@ -22203,7 +22221,7 @@ function apply(ctx) { live2.pluginTools = value.pluginTools; } if (Object.keys(live2).length > 0 && current.ready) roster.set({ ...current, ...live2 }); - if (value.unchanged || value.revision === current.revision && value.modelsKey === current.modelsKey) return; + if (stateAnswerIsStale(value, current, { fresh })) return; if (Object.keys(seenAt).length === 0) leaveVisits([...value.bots, ...value.rooms].map((entry) => entry.id)); roster.set(indexRoster(value)); noteFriendLines(value); @@ -22230,6 +22248,10 @@ function apply(ctx) { const loop = async () => { clearTimeout(timer); if (running || stopped) return; + if (reloading > 0) { + timer = setTimeout(loop, 200); + return; + } running = true; try { await pull(); @@ -22494,6 +22516,14 @@ function apply(ctx) { selectPanel: (id) => { ctx.layout.selectPanel(id); }, + // The read-only banner's Reload: read the whole team again, so a window whose other holder let + // go becomes writable without a page reload that would drop what is in the composer (#2247). + reloadTeam: () => { + reloading += 1; + return pull({ fresh: true }).finally(() => { + reloading -= 1; + }); + }, openNewChat: (mode) => { ui2.set((state) => ({ ...state, newChat: mode, menu: null, palette: null })); }, diff --git a/core/src/client/index.js b/core/src/client/index.js index b568bc42..5258d652 100644 --- a/core/src/client/index.js +++ b/core/src/client/index.js @@ -5,7 +5,7 @@ import { TURN_FAILURES_CSS, TurnFailures } from './turn-failures.js' import { call } from './api.js' import { isHex } from './inks.js' import { SHAPES, markupCache } from './characters.js' -import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession } from './sources.js' +import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession, stateAnswerIsStale, stateRequest } from './sources.js' import { themeCss, shellTokens, registry, registerShape, registerTheme, allThemes } from './themes.js' import { currentPart, landingPart, setPluginTools } from './text.js' import { mentionCss } from './mention-style.js' @@ -172,11 +172,14 @@ export function apply(ctx) { // Pulls can overlap (the poll and a navigation); an older answer never wins. let pulls = 0 let applied = 0 - const pull = async () => { + // A Reload in flight makes the poll stand back. Otherwise the loop's `unchanged` answer can take + // the applied slot first and the full answer the press asked for is dropped as the older one. + let reloading = 0 + const pull = async ({ fresh = false } = {}) => { const order = ++pulls const known = roster.getSnapshot() // `watching`: this window is on screen, so the host reads friends' inboxes at the live cadence (#497). - const value = await call('state', { since: known.revision, models: known.modelsKey ?? '', locale: language(), watching: !document.hidden }) + const value = await call('state', { ...stateRequest(known, { fresh }), locale: language(), watching: !document.hidden }) if (order < applied) return applied = order const nextActivity = JSON.stringify(value.activity ?? {}) @@ -194,7 +197,7 @@ export function apply(ctx) { // Plugin tool names come with a plugin reload, not a revision; context rows re-read them (#419). if (value.pluginTools !== undefined && JSON.stringify(value.pluginTools) !== JSON.stringify(current.pluginTools ?? [])) { setPluginTools(value.pluginTools); live.pluginTools = value.pluginTools } if (Object.keys(live).length > 0 && current.ready) roster.set({ ...current, ...live }) - if (value.unchanged || (value.revision === current.revision && value.modelsKey === current.modelsKey)) return + if (stateAnswerIsStale(value, current, { fresh })) return // On a browser with no visit record yet, everything already there counts as seen. if (Object.keys(seenAt).length === 0) leaveVisits([...value.bots, ...value.rooms].map(entry => entry.id)) roster.set(indexRoster(value)) @@ -223,6 +226,7 @@ export function apply(ctx) { const loop = async () => { clearTimeout(timer) if (running || stopped) return + if (reloading > 0) { timer = setTimeout(loop, 200); return } running = true try { await pull() } catch (error) { if (!stopped) console.warn('[ds-bot] state', error) } finally { running = false } if (!stopped) timer = setTimeout(loop, document.hidden ? 10000 : 1500) @@ -461,6 +465,12 @@ export function apply(ctx) { ctx.uiWorkspace.openSession(target) }, selectPanel: (id) => { ctx.layout.selectPanel(id) }, + // The read-only banner's Reload: read the whole team again, so a window whose other holder let + // go becomes writable without a page reload that would drop what is in the composer (#2247). + reloadTeam: () => { + reloading += 1 + return pull({ fresh: true }).finally(() => { reloading -= 1 }) + }, openNewChat: (mode) => { ui.set(state => ({ ...state, newChat: mode, menu: null, palette: null })) }, openPalette: (query = '') => { ui.set(state => ({ ...state, palette: { query, token: Date.now() }, newChat: null, menu: null })) }, // The token keeps a late exit timer from closing a palette opened again since. diff --git a/core/src/client/sidebar.js b/core/src/client/sidebar.js index 9f7e4de8..b8b15fcf 100644 --- a/core/src/client/sidebar.js +++ b/core/src/client/sidebar.js @@ -369,6 +369,14 @@ export function TeamSidebar(props) { // Read the same way as the workflow page (#2176), so the two surfaces cannot disagree; the text // drawn is still the host's own sentence, not a trimmed copy. const readOnlySaid = typeof roster.readOnly === 'string' && roster.readOnly.trim() !== '' + // The host's sentence tells the person to close the other window and reload; this is that + // entry. It reads the team again instead of reloading the page, so the composer is kept, and + // it sits beside the status line rather than inside it so the live region stays text (#2247). + const readOnlyReload = h('button', { + type: 'button', + className: 'bt-read-only-reload', + onClick: () => { void Promise.resolve(actions.reloadTeam?.()).catch(error => actions.reportError(error, 'reload')) }, + }, t('Reload')) // A plugin (the tree) may give the Bots a structure: the list then is one tree under the Main Bot. const structure = useSidebarStructure() const [collapsed, setCollapsed] = useState({}) @@ -398,7 +406,9 @@ export function TeamSidebar(props) { h(UpdateButton, { useUpdate: props.useUpdate, actions }), h('button', { type: 'button', className: 'bt-round', 'aria-label': t('New…'), title: t('New…'), onClick: () => actions.openNewChat('new') }, h(PlusIcon))), wide && readOnlySaid ? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null, + wide && readOnlySaid ? readOnlyReload : null, !wide && readOnlySaid ? h('div', { className: 'bt-hint bt-read-only', role: 'status', title: hostText(roster.readOnly), 'aria-label': hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, + !wide && readOnlySaid ? readOnlyReload : null, h(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), h(KanbanRow, { wide, useUi, actions }), h(ProjectsNav, { wide }), @@ -740,6 +750,9 @@ export const SIDEBAR_CSS = ` .bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} .bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} .bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} +.bt-read-only-reload:hover{background:var(--bt-hover)} +.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} [class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} [class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} .bt-foot-account{order:-1;flex:none;display:inline-flex} diff --git a/core/src/client/sources.js b/core/src/client/sources.js index df64f430..0b629617 100644 --- a/core/src/client/sources.js +++ b/core/src/client/sources.js @@ -23,6 +23,19 @@ export const EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bot // Any Main Bot holds the Main Bot powers; `mainBotId`, the first one, is the sidebar tile. export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ?? []).includes(id) +// The `state` request the poll sends. `since` asks for a revision delta; a person who pressed +// Reload asks for the whole team, so the answer carries `readOnly` re-read by the host (#2247). +export const stateRequest = (current, { fresh = false } = {}) => ({ + since: fresh ? undefined : current?.revision, + models: current?.modelsKey ?? '', +}) + +// Whether a `state` answer may be skipped. An `unchanged` answer carries no team, so it is never +// indexed; the poll also skips the same revision and models key. A reload draws that same revision: +// that is how a stale read-only sentence clears without a page reload (#2247). +export const stateAnswerIsStale = (value, current, { fresh = false } = {}) => + value?.unchanged === true || (!fresh && value?.revision === current?.revision && value?.modelsKey === current?.modelsKey) + // A Bot's chat is a chain of Sessions ("parts"): the Bot id is its first part's id // and `sessionId` its current one. Every part id finds the Bot; Session state // (running, unread, turns) is read from the current part, team state from the Bot id. diff --git a/core/test/client-reload-state.test.js b/core/test/client-reload-state.test.js new file mode 100644 index 00000000..cb977668 --- /dev/null +++ b/core/test/client-reload-state.test.js @@ -0,0 +1,55 @@ +// #2247: a read-only window told the person to reload, and there was no reload entry. The +// sidebar's new Reload button asks for the team again. Two rules make that read useful, and +// they belong together: the request must carry no `since` (otherwise the host answers +// `unchanged`, with no team at all), and the answer must be drawn even though the revision did +// not move (otherwise the stale read-only sentence stays). Neither rule alone is enough: a +// skipped answer still drawn would be an empty roster, and a drawn answer still skipped is +// today's behaviour. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { stateRequest, stateAnswerIsStale } from './src/client/sources.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) +const module = { exports: {} } +runInNewContext(outputFiles[0].text, { + module, exports: module.exports, console, Date, JSON, Promise, Symbol, + require: (name) => { + if (name === 'react') return { createElement: () => ({}) } + throw new Error(`Unexpected platform module ${name}`) + }, +}) +const { stateRequest, stateAnswerIsStale } = module.exports + +test('the poll asks for a revision delta; a reload asks for the whole team', () => { + const known = { revision: 7, modelsKey: 'models-1' } + const polled = stateRequest(known) + assert.equal(polled.since, 7) + assert.equal(polled.models, 'models-1') + const fresh = stateRequest(known, { fresh: true }) + assert.equal(fresh.since, undefined, 'no `since`, or the host answers `unchanged` and sends no team') + assert.equal(fresh.models, 'models-1') + // An empty roster before the first pull sends no number either. + assert.equal(stateRequest({ revision: undefined, modelsKey: undefined }).since, undefined) +}) + +test('a reload draws the same revision; the poll still skips it', () => { + const current = { revision: 7, modelsKey: 'models-1', readOnly: 'open in another dsh' } + const answer = { revision: 7, modelsKey: 'models-1', readOnly: null, bots: [] } + assert.equal(stateAnswerIsStale(answer, current), true, 'the poll skips an unchanged revision') + assert.equal(stateAnswerIsStale(answer, current, { fresh: true }), false, 'a reload draws it, which is how the sentence can clear') + // An `unchanged` answer carries no team, so it is never indexed, reload or not. + assert.equal(stateAnswerIsStale({ revision: 7, unchanged: true }, current, { fresh: true }), true) + // A new revision is always drawn. + assert.equal(stateAnswerIsStale({ revision: 8, modelsKey: 'models-1' }, current), false) + // A new models key is drawn even at the same revision. + assert.equal(stateAnswerIsStale({ revision: 7, modelsKey: 'models-2' }, current), false) +}) diff --git a/core/test/sidebar-readonly-reload.test.js b/core/test/sidebar-readonly-reload.test.js new file mode 100644 index 00000000..51a40332 --- /dev/null +++ b/core/test/sidebar-readonly-reload.test.js @@ -0,0 +1,166 @@ +// #2247: the host's read-only sentence tells the person to "close the other one and reload", +// but the window offered no reload entry at all. The banner now carries one Reload button in +// both sidebar shapes. It sits beside the status line, not inside it: a `role="status"` region +// must stay text, or a screen reader reads the button as part of every announcement. The button +// runs `actions.reloadTeam`, the client's full-state read, so the window becomes writable once +// the other holder let go — without a page reload that would drop what is in the composer. +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { TeamSidebar, SIDEBAR_CSS } from './src/client/sidebar.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) + +const h = (type, props, ...children) => ({ type, props: { ...props, children: children.flat(Infinity) } }) +class Component { + constructor(props) { this.props = props } + setState() {} +} +const react = { + createElement: h, + Fragment: Symbol('Fragment'), + Component, + PureComponent: Component, + createRef: () => ({ current: null }), + useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}], + useEffect() {}, + useLayoutEffect() {}, + useRef: value => ({ current: value }), + useCallback: fn => fn, + useMemo: fn => fn(), +} +const slots = { entries: () => [], subscribe: () => () => {}, version: () => 0 } + +const load = () => { + const module = { exports: {} } + runInNewContext(outputFiles[0].text, { + module, + exports: module.exports, + console, + Date, + JSON, + Promise, + Symbol, + __flybot__: { slots, call: () => {} }, + require: (name) => { + if (name === 'react') return react + if (name === 'react-dom') return { createPortal: node => node } + if (name === '@deepseek-ai/dsh-client-ui-primitives') return {} + throw new Error(`Unexpected platform module ${name}`) + }, + }) + return module.exports +} + +const { TeamSidebar, SIDEBAR_CSS } = load() + +const HOST_TEXT = 'The Bot team is open in another dsh.' + +function renderSidebar({ wide, readOnly = HOST_TEXT, readOnlyKey = true, reloadTeam, reportError } = {}) { + const roster = { ready: true, bots: [], rooms: [], agents: [], byId: {}, roomsById: {}, mainBotId: undefined, ...readOnlyKey ? { readOnly } : {} } + const actions = { + openInbox() {}, openBoard() {}, openPalette() {}, openNewChat() {}, + landOnMainBot() {}, clearUnread() {}, isAgentSession: () => false, + reloadTeam, reportError, + } + return TeamSidebar({ + wide, + useRoster: select => select(roster), + useUi: select => select({}), + useSessions: select => select({ ids: [], byId: {} }), + useRegistry: () => {}, + useUpdate: select => select({}), + useWork: select => select({}), + useSessionStatus: select => select({}), + useSessionRetainInfo: select => select({}), + useActivity: select => select({}), + actions, + }) +} + +// The element tree down to the child components, which stay opaque. +const walk = (node, out = []) => { + if (Array.isArray(node)) { + for (const child of node) walk(child, out) + return out + } + if (!node || typeof node !== 'object') return out + out.push(node) + walk(node.props?.children, out) + return out +} +const all = tree => walk(tree) +const byClass = (tree, name) => all(tree).filter(node => { + const value = node.props?.className + return typeof value === 'string' && value.split(' ').includes(name) +}) +const textOf = node => (node.props?.children ?? []) + .filter(child => typeof child === 'string' || typeof child === 'number') + .join('') + +test('both sidebar shapes offer one Reload button beside the read-only sentence', () => { + for (const wide of [true, false]) { + const shape = wide ? 'wide' : 'rail' + const tree = renderSidebar({ wide }) + const hints = byClass(tree, 'bt-read-only') + assert.equal(hints.length, 1, `${shape}: the host's sentence is still drawn once`) + const buttons = byClass(tree, 'bt-read-only-reload') + assert.equal(buttons.length, 1, `${shape}: the banner offers one Reload entry`) + const button = buttons[0] + assert.equal(button.type, 'button', `${shape}: it is a button`) + assert.equal(button.props.type, 'button', `${shape}: it does not submit a form`) + assert.equal(textOf(button), 'Reload', `${shape}: its label comes from the shared copy`) + // The sentence keeps its status line; the control lives outside it, in reading order after it. + assert.equal(all(hints[0]).includes(button), false, `${shape}: the live region stays text-only`) + assert.ok(all(tree).indexOf(hints[0]) < all(tree).indexOf(button), `${shape}: the sentence is read first`) + } +}) + +test('the Reload button starts the client\'s full-state read', async () => { + let calls = 0 + const tree = renderSidebar({ wide: true, reloadTeam: () => { calls += 1; return Promise.resolve() } }) + const [button] = byClass(tree, 'bt-read-only-reload') + button.props.onClick() + await Promise.resolve() + await Promise.resolve() + assert.equal(calls, 1, 'one press reads the team once') +}) + +test('a failed reload says so through the sidebar error channel, not an unhandled rejection', async () => { + const failure = new Error('network error: Failed to fetch') + const seen = [] + const tree = renderSidebar({ wide: false, reloadTeam: () => Promise.reject(failure), reportError: (error, where) => seen.push([error, where]) }) + const [button] = byClass(tree, 'bt-read-only-reload') + button.props.onClick() + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + assert.deepEqual(seen, [[failure, 'reload']]) +}) + +test('a writable window, or a host sentence of only whitespace, offers no Reload entry', () => { + for (const wide of [true, false]) { + const shape = wide ? 'wide' : 'rail' + for (const readOnly of [null, '', ' ', '\n\t ']) { + const tree = renderSidebar({ wide, readOnly }) + assert.equal(byClass(tree, 'bt-read-only-reload').length, 0, `${shape}, ${JSON.stringify(readOnly)}`) + } + assert.equal(byClass(renderSidebar({ wide, readOnlyKey: false }), 'bt-read-only-reload').length, 0, `${shape}, no field`) + } +}) + +test('both shapes carry a style for the Reload button, and the rail rule wins over the generic one', () => { + assert.match(SIDEBAR_CSS, /\.bt-read-only-reload\{[^}]*cursor:pointer/) + const rail = SIDEBAR_CSS.match(/\.bt-rail \.bt-read-only-reload\{([^}]*)\}/) + assert.ok(rail, 'the rail needs its own compact rule') + assert.match(rail[1], /text-align:center/) + assert.ok(SIDEBAR_CSS.indexOf('.bt-rail .bt-read-only-reload') > SIDEBAR_CSS.indexOf('.bt-read-only-reload{')) +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index 2c827e16..cb351132 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -14,6 +14,7 @@ The browser side of the patched `ds-bot` core (`core/`): sources in `core/src/cl - Browser client: sidebar, chat, panels, themes, characters, Bot browser panel (`core/client.js` generated from `core/src/client/`). The sidebar **+** new-chat picker (`core/src/client/new-chat.js`) takes extra rows from slot `flybot.new-chat.action` (after New group chat; `label` / `hint` go through the same `t()` map) and lists only `listedChatBots` (gone, hidden, and home-level remotes stay out; owner labels match the sidebar). Covered by `core/test/new-chat.test.js` in patch `0003-flybot-api-client`. - The chat's `ask_user` card (`questions.js`) answers where it is drawn: one option answers a single-select card at once, a multi-select card toggles its rows and **Submit** sends the set, and the × dismisses it; the same card waits in the dock above the composer when its call is not on screen. In a window that holds the team read-only the card keeps the question, its rows and the host's sentence (`roster.readOnly`, read through `lockedWindow` from `schedules.js`) and draws none of those controls — no option button, **Submit**, **Skip**, × or own-answer field, and no form to submit. Each of them reached a write the host refuses (`answer-question`, `dismiss-question`), and the refusal was invisible: a pressed answer marked the card answered and the next poll said nothing, while the × hid the card until that poll brought it back. Tests: `core/test/questions-readonly.test.js`. +- Read-only banner: when another dsh holds the team (`roster.readOnly` a non-empty string), the sidebar draws the host's own sentence and, beside it, one **Reload** control in both shapes (wide and rail; the control stays outside the `role="status"` line so a live region remains text). It calls `actions.reloadTeam`, which re-reads the whole team (`state` without `since`, see `stateRequest` / `stateAnswerIsStale` in `sources.js`) so the banner and every refused control clear once the other holder let go, without a page reload that would drop what is in the composer. Tests: `core/test/sidebar-readonly-reload.test.js`, `core/test/client-reload-state.test.js`. - Chat context rows: a tool the plugin registers at install draws one `tool-addition` row, and `cells.js` keeps every name in `TEAM_TOOLS` out of the chat (`SILENT_REGISTRATION_TOOLS`, #1101) — the Project Assistant's `project_digest` joins them (#1712), so its 09:00 Session shows the digest, not its registration. - A Settings dialog takes focus when it opens (`useDialogFocus` in `core/src/client/overlay-hooks.js`; the `.bt-settings` dialog carries `tabIndex -1` and a ref) and gives it back to the still-connected opener when it closes; it is not a focus trap, so Tab can still leave the dialog for the page behind the scrim (#2251). diff --git a/patches/ds-bot/0003-flybot-api-client.patch b/patches/ds-bot/0003-flybot-api-client.patch index e09a51c9..57f0b9ac 100644 --- a/patches/ds-bot/0003-flybot-api-client.patch +++ b/patches/ds-bot/0003-flybot-api-client.patch @@ -1474,9 +1474,10 @@ diff --git a/src/client/index.js b/src/client/index.js import { call } from './api.js' -import { isHex, inkText, paintCss } from './inks.js' -import { DEFAULT_MASK, SHAPES, markupCache, maskOf } from './characters.js' +-import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession } from './sources.js' +import { isHex } from './inks.js' +import { SHAPES, markupCache } from './characters.js' - import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession } from './sources.js' ++import { countCards, createSource, EMPTY_ROSTER, indexRoster, isAgentSession, stateAnswerIsStale, stateRequest } from './sources.js' import { themeCss, shellTokens, registry, registerShape, registerTheme, allThemes } from './themes.js' -import { MENTION_ORIGIN, currentPart } from './text.js' +import { currentPart, landingPart, setPluginTools } from './text.js' @@ -1570,7 +1571,7 @@ diff --git a/src/client/index.js b/src/client/index.js // Per Bot, the turn it runs and its tool; and the approvals waiting for the user. const work = createSource({ bots: {}, approvals: [] }) let workKey = '' -@@ -125,6 +125,17 @@ export function apply(ctx) { +@@ -125,18 +125,35 @@ export function apply(ctx) { try { localStorage.setItem(UNREAD_KEY, JSON.stringify(unread)) } catch { /* storage unavailable */ } return { ...state, unread } }) @@ -1588,13 +1589,17 @@ diff --git a/src/client/index.js b/src/client/index.js const refreshed = new Set() // Pulls can overlap (the poll and a navigation); an older answer never wins. let pulls = 0 -@@ -132,11 +132,14 @@ export function apply(ctx) { - const pull = async () => { + let applied = 0 +- const pull = async () => { ++ // A Reload in flight makes the poll stand back. Otherwise the loop's `unchanged` answer can take ++ // the applied slot first and the full answer the press asked for is dropped as the older one. ++ let reloading = 0 ++ const pull = async ({ fresh = false } = {}) => { const order = ++pulls const known = roster.getSnapshot() - const value = await call('state', { since: known.revision, models: known.modelsKey ?? '', locale: language() }) + // `watching`: this window is on screen, so the host reads friends' inboxes at the live cadence (#497). -+ const value = await call('state', { since: known.revision, models: known.modelsKey ?? '', locale: language(), watching: !document.hidden }) ++ const value = await call('state', { ...stateRequest(known, { fresh }), locale: language(), watching: !document.hidden }) if (order < applied) return applied = order const nextActivity = JSON.stringify(value.activity ?? {}) @@ -1611,7 +1616,8 @@ diff --git a/src/client/index.js b/src/client/index.js + // Plugin tool names come with a plugin reload, not a revision; context rows re-read them (#419). + if (value.pluginTools !== undefined && JSON.stringify(value.pluginTools) !== JSON.stringify(current.pluginTools ?? [])) { setPluginTools(value.pluginTools); live.pluginTools = value.pluginTools } if (Object.keys(live).length > 0 && current.ready) roster.set({ ...current, ...live }) - if (value.unchanged || (value.revision === current.revision && value.modelsKey === current.modelsKey)) return +- if (value.unchanged || (value.revision === current.revision && value.modelsKey === current.modelsKey)) return ++ if (stateAnswerIsStale(value, current, { fresh })) return // On a browser with no visit record yet, everything already there counts as seen. if (Object.keys(seenAt).length === 0) leaveVisits([...value.bots, ...value.rooms].map(entry => entry.id)) roster.set(indexRoster(value)) @@ -1619,6 +1625,14 @@ diff --git a/src/client/index.js b/src/client/index.js try { const ids = (value.agents ?? []).flatMap(agent => agent.sessions ?? []) localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)) +@@ -174,6 +174,7 @@ export function apply(ctx) { + const loop = async () => { + clearTimeout(timer) + if (running || stopped) return ++ if (reloading > 0) { timer = setTimeout(loop, 200); return } + running = true + try { await pull() } catch (error) { if (!stopped) console.warn('[ds-bot] state', error) } finally { running = false } + if (!stopped) timer = setTimeout(loop, document.hidden ? 10000 : 1500) @@ -252,17 +252,7 @@ export function apply(ctx) { const style = installStyles('', 'mentions') let last = '' @@ -1655,7 +1669,7 @@ diff --git a/src/client/index.js b/src/client/index.js // A DSH Agent Session id lands on the plain surface, not the Bot skin. if (isAgentSessionId(id)) return actions.openAgentSession(id) ui.set(state => (state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state)) -@@ -409,11 +409,14 @@ export function apply(ctx) { +@@ -409,15 +409,24 @@ export function apply(ctx) { }, // `last` is the Bot conversation the view held before it cleared: when that part // was archived for a fresh one, the view goes on in the fresh one. @@ -1674,6 +1688,16 @@ diff --git a/src/client/index.js b/src/client/index.js expected = target ctx.uiWorkspace.openSession(target) }, + selectPanel: (id) => { ctx.layout.selectPanel(id) }, ++ // The read-only banner's Reload: read the whole team again, so a window whose other holder let ++ // go becomes writable without a page reload that would drop what is in the composer (#2247). ++ reloadTeam: () => { ++ reloading += 1 ++ return pull({ fresh: true }).finally(() => { reloading -= 1 }) ++ }, + openNewChat: (mode) => { ui.set(state => ({ ...state, newChat: mode, menu: null, palette: null })) }, + openPalette: (query = '') => { ui.set(state => ({ ...state, palette: { query, token: Date.now() }, newChat: null, menu: null })) }, + // The token keeps a late exit timer from closing a palette opened again since. @@ -444,10 +444,22 @@ export function apply(ctx) { // Opening the conversation would move focus to its composer and blur the name field. renameBot: (id) => { ui.set(state => ({ ...state, details: id, view: 'details', renaming: true, tab: null })) }, @@ -2714,7 +2738,7 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js export function TeamSidebar(props) { const { wide, useRoster, useUi, useSessions, actions } = props const listRef = useRef(null) -@@ -249,14 +249,30 @@ export function TeamSidebar(props) { +@@ -249,14 +249,38 @@ export function TeamSidebar(props) { const order = useMemo(() => { const stamps = JSON.parse(updated) const entries = [ @@ -2736,6 +2760,14 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js + // Read the same way as the workflow page (#2176), so the two surfaces cannot disagree; the text + // drawn is still the host's own sentence, not a trimmed copy. + const readOnlySaid = typeof roster.readOnly === 'string' && roster.readOnly.trim() !== '' ++ // The host's sentence tells the person to close the other window and reload; this is that ++ // entry. It reads the team again instead of reloading the page, so the composer is kept, and ++ // it sits beside the status line rather than inside it so the live region stays text (#2247). ++ const readOnlyReload = h('button', { ++ type: 'button', ++ className: 'bt-read-only-reload', ++ onClick: () => { void Promise.resolve(actions.reloadTeam?.()).catch(error => actions.reportError(error, 'reload')) }, ++ }, t('Reload')) + // A plugin (the tree) may give the Bots a structure: the list then is one tree under the Main Bot. + const structure = useSidebarStructure() + const [collapsed, setCollapsed] = useState({}) @@ -2748,13 +2780,15 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js 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 } -@@ -276,14 +276,20 @@ export function TeamSidebar(props) { +@@ -276,14 +276,22 @@ export function TeamSidebar(props) { : h(Fragment, null, h(UpdateButton, { useUpdate: props.useUpdate, actions }), h('button', { type: 'button', className: 'bt-round', 'aria-label': t('New…'), title: t('New…'), onClick: () => actions.openNewChat('new') }, h(PlusIcon))), - wide && roster.readOnly ? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null, + wide && readOnlySaid ? h('div', { className: 'bt-hint bt-read-only', role: 'status' }, hostText(roster.readOnly)) : null, ++ wide && readOnlySaid ? readOnlyReload : null, + !wide && readOnlySaid ? h('div', { className: 'bt-hint bt-read-only', role: 'status', title: hostText(roster.readOnly), 'aria-label': hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, ++ !wide && readOnlySaid ? readOnlyReload : null, + h(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), + h(KanbanRow, { wide, useUi, actions }), + h(ProjectsNav, { wide }), @@ -2968,11 +3002,14 @@ diff --git a/src/client/sidebar.js b/src/client/sidebar.js // shell's footer classes are CSS-module names, so they match on their stems. export const SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -607,6 +607,7 @@ export const SIDEBAR_CSS = ` +@@ -607,6 +607,10 @@ export const SIDEBAR_CSS = ` .bt-rail .bt-row{position:relative} .bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} .bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} ++.bt-read-only-reload:hover{background:var(--bt-hover)} ++.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} [class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} [class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} .bt-foot-account{order:-1;flex:none;display:inline-flex} diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index 5535ebcc..5b7bc8c6 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -2170,7 +2170,7 @@ diff --git a/client.js b/client.js var formatTokens = (count) => count >= 1e6 ? `${(count / 1e6).toFixed(1)}M` : count >= 1e3 ? `${(count / 1e3).toFixed(1)}K` : String(count); function usageOf(list, ids) { let input = 0; -@@ -1624,901 +1624,1984 @@ function usageOf(list, ids) { +@@ -1624,902 +1624,1990 @@ function usageOf(list, ids) { const hit = input > 0 ? Math.round(cacheRead / input * 100) : 0; return t("{tokens} tok · Cache hit {hit}%", { tokens: formatTokens(input + output), hit }); } @@ -2323,6 +2323,11 @@ diff --git a/client.js b/client.js - container.dataset.glide = ""; +var EMPTY_ROSTER = { revision: -1, mainBotId: null, mainBotIds: [], bots: [], rooms: [], humans: [], self: null, agents: [], exchangeTurns: {}, remoteTypedTurns: {}, remoteAwaiting: {}, questions: {}, secrets: [], capabilities: [], capabilityStatuses: {}, secretRequests: {}, models: [], defaultModel: null, modelsKey: null, prefs: {}, byId: {}, roomsById: {}, agentsById: {}, agentOf: {}, ready: false }; +var isMainOf = (roster, id) => id !== void 0 && (roster.mainBotIds ?? []).includes(id); ++var stateRequest = (current, { fresh = false } = {}) => ({ ++ since: fresh ? void 0 : current?.revision, ++ models: current?.modelsKey ?? "" ++}); ++var stateAnswerIsStale = (value, current, { fresh = false } = {}) => value?.unchanged === true || !fresh && value?.revision === current?.revision && value?.modelsKey === current?.modelsKey; +function indexRoster(value) { + setLocalBots(value.bots); + setPluginTools(value.pluginTools); @@ -2434,15 +2439,11 @@ diff --git a/client.js b/client.js - }; - animation.oncancel = () => { - if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; -- }; -- }); +function isAgentSession(roster, cache, sessionId) { + if (sessionId === void 0 || sessionId === null || sessionId === "") return false; + if (roster.ready) return roster.agentOf[sessionId] !== void 0; + return cache.has(sessionId); - } --function useLinger(value, ms) { -- const [kept, setKept] = (0, import_react.useState)(value); ++} +function countCards(state, sessionId, callIds, delta) { + const own = { ...state[sessionId] }; + for (const callId of callIds) { @@ -2459,9 +2460,7 @@ diff --git a/client.js b/client.js +var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +function useGaze(ref, enabled) { - (0, import_react.useEffect)(() => { -- if (value) { -- setKept(value); ++ (0, import_react.useEffect)(() => { + if (!enabled) return void 0; + let frame = 0; + let point = null; @@ -2473,7 +2472,8 @@ diff --git a/client.js b/client.js + const clamp = (value) => Math.max(-1, Math.min(1, value)); + node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); + node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -+ }; + }; +- }); + const onMove = (event) => { + point = { x: event.clientX, y: event.clientY }; + if (!frame) frame = requestAnimationFrame(place); @@ -2491,7 +2491,9 @@ diff --git a/client.js b/client.js + document.documentElement.removeEventListener("pointerleave", onLeave); + }; + }, [enabled]); -+} + } +-function useLinger(value, ms) { +- const [kept, setKept] = (0, import_react.useState)(value); +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 ?? (bot?.remote ? lookAvoidingLocals(bot) : lookOf(bot)); @@ -2571,7 +2573,9 @@ diff --git a/client.js b/client.js +function useSettled(state) { + const prev = (0, import_react.useRef)(state); + const [settled, setSettled] = (0, import_react.useState)(false); -+ (0, import_react.useEffect)(() => { + (0, import_react.useEffect)(() => { +- if (value) { +- setKept(value); + const was = prev.current; + prev.current = state; + if (state !== "idle") { @@ -4016,6 +4020,7 @@ diff --git a/client.js b/client.js -.bt-mem-summary>:nth-child(5){animation-delay:.12s} -.bt-mem-summary>:nth-child(6){animation-delay:.15s} -.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} +-@keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} +// src/client/motion.js +var import_react4 = require("react"); +var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; @@ -5020,10 +5025,11 @@ diff --git a/client.js b/client.js +.bt-mem-summary>:nth-child(5){animation-delay:.12s} +.bt-mem-summary>:nth-child(6){animation-delay:.15s} +.bt-mem-summary>:nth-child(n+7){animation-delay:.18s} - @keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} ++@keyframes bt-mem-line{from{opacity:0;transform:translateY(4px)}} .bt-mem-skel{display:flex;flex-direction:column;gap:24px} .bt-mem-skel-block{display:flex;flex-direction:column;gap:10px} -@@ -2574,2935 +2574,7472 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} + .bt-mem-skel-block span{display:block;height:12px;border-radius:6px;background:linear-gradient(90deg,var(--bt-hover) 25%,var(--bt-active) 50%,var(--bt-hover) 75%);background-size:200% 100%;animation:bt-mem-skel 1.4s ease-in-out infinite} +@@ -2574,4596 +2574,9011 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} @media (max-width:640px){.bt-mem{width:100vw;height:100%;border-radius:0;border:0}.bt-mem-body{padding:18px 18px 20px}.bt-mem-foot{padding:8px 12px 12px}} @media (prefers-reduced-motion:reduce){.bt-mem,.bt-mem-scrim,.bt-mem-summary>*,.bt-mem-empty,.bt-mem-reply,.bt-mem-skel-block span,.bt-mem-entries,.bt-mem-send>*,.bt-mem-changes li svg{animation:none}} @@ -6054,7 +6060,121 @@ diff --git a/client.js b/client.js + ) + ) + ); -+} + } +-@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} +-@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} +-[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} +-[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} +-[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} +-[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} +-[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} +-[data-composer-card]>[class*="_row"]{display:contents!important} +-[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} +-[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} +-[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} +-[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} +-[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} +-[data-composer-placeholder]{font-size:0!important;line-height:0!important} +-[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} +-/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ +-[data-composer-card] button[class*="_add"]{display:none!important} +-.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-plus:active{scale:.9} +-.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} +-.bt-plus[aria-expanded=true] svg{rotate:45deg} +-.bt-plus-menu{min-width:180px} +-[data-composer-card] [class*="_tools"]{gap:0!important} +-[data-composer-card] [class*="_modes"]:empty{display:none!important} +-[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} +-[data-composer-card] [class*="_trailing"]{gap:8px} +-[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} +-[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} +-[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} +-@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} +-[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} +-[data-composer-card] button[class*="_primary"]:disabled{display:none!important} +-[data-composer-dock]{display:none!important} +-.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} +-.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} +-.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} +-.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} +-@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} +-.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} +-.bt-voice:hover{opacity:.9} +-.bt-mic:active,.bt-voice:active{scale:.9} +-[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} +-body[data-bt-room] .bt-voice{display:none!important} +-body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} +-.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} +-[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} +-.bt-q{order:1} +-div:has(> nav [class*="_marks"]){display:none!important} +-/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) +- takes its surface and its text from the one --bt-user pair: a skin that repaints +- the host's bubble surface alone would otherwise leave the text unreadable. */ +-[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} +-[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} +-/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ +-[class*="_userRow"] [class*="_actions"]{display:none!important} +-[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} +-[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} +-[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} +-[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} +-[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} +-[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} +-[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} +-.bt-q-inline{display:flex;margin:4px 0 6px} +-[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} +-[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} +-.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} +-.bt-q-dock{width:100%;display:flex;justify-content:center} +-.bt-q-dock .bt-q{margin:0 auto 8px} +-[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} +-[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} +-[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} +-[class*="_toBottomSlot"]>button:active{scale:.9} +-[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} +-[class*="_toBottomSlot"]>button svg{display:none} +-[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} +-@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} +-[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} +-.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} +-@keyframes bt-news-in{from{translate:0 8px;scale:.9}} +-.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} +-@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} +-.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} +-.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} +-.bt-news-ico svg{width:16px;height:16px} +-.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} +-.bt-news-x svg{width:12px;height:12px} +-.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} +-@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} +-.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} +-.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} +-.bt-open-new{display:flex;flex-direction:column} +-.bt-open-new .bt-new-sep{margin:18px 0 0} +-.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} +-.bt-earlier{display:flex;flex-direction:column;gap:6px} +-.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} +-.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} +-.bt-urow{display:flex;justify-content:flex-end} +-.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} +-.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} +-.bt-ububble p{margin:0;line-height:22px} +-.bt-uimages{display:block;font-size:12px;opacity:.7} +-.bt-earlier .bt-event{margin:4px 0} +-.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} +-.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} +-`; +-function installStyles(text, tag) { +- const style = document.createElement("style"); +- style.dataset.dshBot = tag; +- style.textContent = text; +- document.head.appendChild(style); +- return style; +function openPlugins(actions) { + actions.closeSettings(); + actions.selectPanel("plugins"); @@ -6289,7 +6409,22 @@ diff --git a/client.js b/client.js + hidden > 0 ? (0, import_react6.createElement)("button", { key: "more", type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules") }, t("{count} more not shown", { count: hidden })) : null + ) + ); -+} + } +-var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; +-var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; +-function sheetClassNames(sheet) { +- const names = []; +- const visit = (rules) => { +- for (const rule of rules) { +- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); +- if (rule.cssRules) visit(rule.cssRules); +- } +- }; +- try { +- visit(sheet.cssRules); +- } catch { +- } +- return names; +function ScheduleSummary({ bot, roster, actions }) { + const view = useSchedules(actions); + if (view === null || view.readFailure !== void 0) return null; @@ -6301,14 +6436,35 @@ diff --git a/client.js b/client.js + tasks.length === 0 ? (0, import_react6.createElement)("div", { className: "bt-section-title" }, t("Scheduled tasks")) : (0, import_react6.createElement)(SummaryLines, { tasks, actions }), + tasks.length === 0 ? (0, import_react6.createElement)("button", { type: "button", className: "bt-more", onClick: () => actions.openSettings("schedules", bot.id) }, view.available === false ? t("Switch on Automation tasks on the Plugins page.") : view.someHidden ? t("No scheduled tasks you can see. Add one") : t("No scheduled tasks yet. Add one")) : null + ); -+} + } +-function compileFragmentSelectors(text, names) { +- const compiled = /* @__PURE__ */ new Map(); +- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { +- if (!compiled.has(fragment)) { +- const exact = []; +- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); +- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); +- } +- return compiled.get(fragment); +- }); +function RoomSchedules({ room: room2, actions }) { + const view = useSchedules(actions); + if (view === null || view.readFailure !== void 0 || room2.origin === "face-to-face") return null; + const tasks = view.available ? view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id) : []; + if (tasks.length === 0) return null; + return (0, import_react6.createElement)("div", null, (0, import_react6.createElement)(SummaryLines, { tasks, actions })); -+} + } +-function installCompiledStyles(text, tag) { +- const style = installStyles("", tag); +- const cache = /* @__PURE__ */ new WeakMap(); +- const render = () => { +- const names = /* @__PURE__ */ new Set(); +- for (const sheet of document.styleSheets) { +- if (sheet.ownerNode?.dataset?.dshBot) continue; +- let list = cache.get(sheet); +- if (!list) { +- list = sheetClassNames(sheet); +- cache.set(sheet, list); +var SCHEDULE_CSS = ` +.bt-task{gap:10px} +.bt-task-copy{flex-wrap:wrap;gap:8px} @@ -6377,7 +6533,8 @@ diff --git a/client.js b/client.js + } else if (index === count && ownRef.current) { + event.preventDefault(); + ownRef.current.focus(); -+ } + } +- for (const name of list) names.add(name); + }; + document.addEventListener("keydown", onKey, true); + return () => document.removeEventListener("keydown", onKey, true); @@ -6396,13 +6553,32 @@ diff --git a/client.js b/client.js + if (multi) { + setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); + return; -+ } + } +- const next = compileFragmentSelectors(text, names); +- if (style.textContent !== next) style.textContent = next; +- }; +- render(); +- let timer; +- const schedule = () => { +- clearTimeout(timer); +- timer = setTimeout(render, 120); +- }; +- const onLoad = (event) => { +- if (event.target instanceof HTMLLinkElement) schedule(); + onAnswer({ selected: [label] }); -+ }; + }; +- const observer = new MutationObserver(schedule); +- observer.observe(document.head, { childList: true }); +- document.addEventListener("load", onLoad, true); +- return () => { +- observer.disconnect(); +- document.removeEventListener("load", onLoad, true); +- clearTimeout(timer); +- style.remove(); + const submit = () => { + if (!canSubmit) return; + onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -+ }; + }; + useOptionKeys(locked ? 0 : options.length, (index) => choose(options[index].label), ownRef); + const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; + const field = ownAnswer && !locked ? (0, import_react7.createElement)("textarea", { @@ -6493,7 +6669,44 @@ diff --git a/client.js b/client.js + ) : null, + foot + ); -+} + } +- +-// src/client/mark.js +-var import_react2 = require("react"); +-var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; +-var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; +-var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; +-function useGaze(ref, enabled) { +- (0, import_react2.useEffect)(() => { +- if (!enabled) return void 0; +- let frame = 0; +- let point = null; +- const place = () => { +- frame = 0; +- const node = ref.current; +- if (!node || !point) return; +- const box = node.getBoundingClientRect(); +- const clamp = (value) => Math.max(-1, Math.min(1, value)); +- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); +- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); +- }; +- const onMove = (event) => { +- point = { x: event.clientX, y: event.clientY }; +- if (!frame) frame = requestAnimationFrame(place); +- }; +- const onLeave = () => { +- point = null; +- ref.current?.style.setProperty("--bt-gx", "0"); +- ref.current?.style.setProperty("--bt-gy", "0"); +- }; +- window.addEventListener("pointermove", onMove, { passive: true }); +- document.documentElement.addEventListener("pointerleave", onLeave); +- return () => { +- cancelAnimationFrame(frame); +- window.removeEventListener("pointermove", onMove); +- document.documentElement.removeEventListener("pointerleave", onLeave); +- }; +- }, [enabled]); +function SettledQuestion({ question, answer }) { + const options = optionsOf(question); + const prompt = question?.question ?? ""; @@ -6525,15 +6738,40 @@ diff --git a/client.js b/client.js + ); + })) + ); -+} -+function QuestionCard({ matched }) { -+ const pending = matched; -+ const questions = pending.questions ?? []; -+ const [index, setIndex] = (0, import_react7.useState)(0); -+ const [answers, setAnswers] = (0, import_react7.useState)([]); -+ const question = questions[index]; -+ if (question === void 0) return null; -+ const onAnswer = (answer) => { + } +-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); +- const ref = (0, import_react2.useRef)(null); +- const maskId = (0, import_react2.useRef)(""); +- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; +- const [poked, setPoked] = (0, import_react2.useState)(false); +- useGaze(ref, gaze); +- const common = { +- ref, +- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", +- "data-shape": look.image ? "image" : shapeId(look.shape), +- "data-state": STATES.includes(state) ? state : "idle", +- "data-live": live ? "" : void 0, +- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, +- "data-gaze": gaze ? "" : void 0, +- "data-poke": pokeable ? "" : void 0, +- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, +- role: label ? "img" : void 0, +- "aria-label": label, +- "aria-hidden": label ? void 0 : true, +- onClick: pokeable ? () => setPoked(true) : void 0, +- onAnimationEnd: poked ? (event) => { +- if (/^bt-poke/.test(event.animationName)) setPoked(false); +- } : void 0 ++function QuestionCard({ matched }) { ++ const pending = matched; ++ const questions = pending.questions ?? []; ++ const [index, setIndex] = (0, import_react7.useState)(0); ++ const [answers, setAnswers] = (0, import_react7.useState)([]); ++ const question = questions[index]; ++ if (question === void 0) return null; ++ const onAnswer = (answer) => { + const next = [...answers, { id: question.id, ...answer }]; + if (index + 1 < questions.length) { + setAnswers(next); @@ -6541,9 +6779,17 @@ diff --git a/client.js b/client.js + return; + } + pending.answer({ answers: next }); -+ }; + }; +- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); +- const box = shapeOf(look.shape).size; +- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); + return (0, import_react7.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); -+} + } +-function StarIcon({ size = 8 }) { +- return (0, import_react2.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) +function PendingQuestion({ sessionId, question, actions, roster, inline }) { + const [answered, setAnswered] = (0, import_react7.useState)(null); + if (answered === question.id) return null; @@ -6561,8 +6807,10 @@ diff --git a/client.js b/client.js + { className: inline ? "bt-q-inline" : "bt-q-dock" }, + ...locked ? [(0, import_react7.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly))] : [], + (0, import_react7.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline, locked }) -+ ); -+} + ); + } +-function AdminStar() { +- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); +function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { + const roster = useRoster((value) => value); + const question = roster.questions?.[sessionId]; @@ -6570,7 +6818,13 @@ diff --git a/client.js b/client.js + const loading = useSession?.(historyLoading) === true; + if (question === void 0 || question.callId && (drawn || loading)) return null; + return (0, import_react7.createElement)(PendingQuestion, { sessionId, question, actions, roster, inline: false }); -+} + } +-function MainStar() { +- return (0, import_react2.createElement)( +- "svg", +- { viewBox: "0 0 20 20", "aria-hidden": true }, +- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +- ); +function AnsweredCell({ node }) { + const data = node.data ?? {}; + const questions = Array.isArray(data.questions) ? data.questions : []; @@ -6583,7 +6837,16 @@ diff --git a/client.js b/client.js + question, + answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } + }))); -+} + } +-function useSettled(state) { +- const prev = (0, import_react2.useRef)(state); +- const [settled, setSettled] = (0, import_react2.useState)(false); +- (0, import_react2.useEffect)(() => { +- const was = prev.current; +- prev.current = state; +- if (state !== "idle") { +- setSettled(false); +- return void 0; + +// src/client/secret-card.js +var import_react8 = require("react"); @@ -6637,9 +6900,27 @@ diff --git a/client.js b/client.js + if (event.key !== "Enter" || event.nativeEvent.isComposing) return; + event.preventDefault(); + onEnter?.(); -+ } + } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; + }); -+} + } +-function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { +- const settled = useSettled(state); +- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); +- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, +- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), +- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, +- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, +- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, +- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null +function ScopeSwitch({ value, onChange, bot }) { + const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]]; + return (0, import_react8.createElement)( @@ -6654,8 +6935,15 @@ diff --git a/client.js b/client.js + className: "bt-secret-seg", + onClick: () => onChange(id) + }, text2)) -+ ); -+} + ); + } +-function clusterSeats(count, frame) { +- if (count === 1) return [[0, 0, frame]]; +- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; +- if (count === 3) { +- const size = frame * 5 / 9; +- const rest = frame - size; +- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; +function SecretHead({ bot, title, detail, name, onClose }) { + return (0, import_react8.createElement)( + "div", @@ -6759,7 +7047,9 @@ diff --git a/client.js b/client.js + if (request) return (0, import_react8.createElement)("div", { className: "bt-q-inline" }, (0, import_react8.createElement)(RequestView, { bot, request, roster, actions })); + if (done && /already saved/.test(contentText(content))) { + return (0, import_react8.createElement)("div", { className: "bt-event" }, (0, import_react8.createElement)("span", { className: "bt-secret-event" }, (0, import_react8.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -+ } + } +- const half = frame / 2; +- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; + return null; +} +function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { @@ -6807,7 +7097,33 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) + ) + ); -+} + } +-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; +- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); +- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); +- if (size <= 20 && people.length > 1) { +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, +- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), +- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null +- ); +- } +- const seats = clusterSeats(Math.min(people.length, 4), size); +- const overflow = people.length > 4 ? people.length - 3 : 0; +- const gap = size / 14; +- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); +- return (0, import_react2.createElement)( +- "span", +- { className: "bt-cluster", style: { width: size, height: size } }, +- seats.map(([x, y, s], index) => { +- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); +- 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) { +- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); +function ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react8.useState)(""); + const [replaced, setReplaced] = (0, import_react8.useState)(false); @@ -6818,7 +7134,10 @@ diff --git a/client.js b/client.js + if (ok) { + setValue(""); + setReplaced(true); -+ } + } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) + }); + }; + const edit = (next) => { @@ -6836,8 +7155,24 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) + ), + value !== "" && !long ? (0, import_react8.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react8.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react8.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null -+ ); -+} + ); + } +- +-// src/client/icons.js +-var import_react3 = require("react"); +-function PlusIcon() { +- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function SearchIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-} +-function PeopleIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), +- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), +- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) +function SecretRow({ secret, roster, actions, run, open, onToggle }) { + const [confirm, setConfirm] = (0, import_react8.useState)(false); + (0, import_react8.useEffect)(() => { @@ -6879,8 +7214,17 @@ diff --git a/client.js b/client.js + ] : (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react8.createElement)(TrashIcon), t("Delete")) + ) + ) : null -+ ); -+} + ); + } +-function ArrowUpIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); +-} +-function MicIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), +- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) +function AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react8.useState)(""); + const [purpose, setPurpose] = (0, import_react8.useState)(""); @@ -6908,8 +7252,14 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) + ) -+ ); -+} + ); + } +-function WaveIcon({ size = 16 }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) +- ); +function SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react8.useState)(null); + const [adding, setAdding] = (0, import_react8.useState)(false); @@ -6933,7 +7283,13 @@ diff --git a/client.js b/client.js + ), + adding ? null : (0, import_react8.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react8.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) + ]; -+} + } +-function ShareIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +- ); +var SECRET_CSS = ` +.bt-secret-head{align-items:center;gap:10px} +.bt-secret-who{position:relative;flex:none;display:inline-flex} @@ -7052,10 +7408,20 @@ diff --git a/client.js b/client.js + people2.push({ key, name: seat.person || (seat.mine ? cleanPeerName(roster.self?.displayName, "") : "") || t("Someone"), mine: seat.mine, seats: [seat] }); + } + return [...people2.filter((person) => person.mine), ...people2.filter((person) => !person.mine)]; -+} + } +-function CloseIcon() { +- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +function facePersonLabel(person) { + return person.mine ? t("{name} (you)", { name: person.name }) : person.name; -+} + } +-var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +-var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); +-var lineIcon = (paths) => function LineIcon() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) +- ); + +// src/client/held-reply.js +var import_react9 = require("react"); @@ -7075,7 +7441,41 @@ diff --git a/client.js b/client.js + render() { + return this.state.failed ? this.props.fallback : this.props.children; + } -+}; + }; +-var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; +-function CloudDownloadIcon({ done = false }) { +- return (0, import_react3.createElement)( +- "svg", +- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react3.createElement)("path", { d: CLOUD }), +- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) +- ); +-} +-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"]); +-var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); +-var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); +-var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); +-var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); +-var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); +-var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); +-var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); +-var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); +-var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); +-var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); +-var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); +-var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); +-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() { +- return (0, import_react3.createElement)( +- "svg", +- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, +- (0, import_react3.createElement)( +- "g", +- { transform: "rotate(45 10 10)" }, +- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), +- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) +- ) +function useInlineEntry(policy) { + const slots = registry(); + const [, setVersion] = (0, import_react9.useState)(0); @@ -7133,20 +7533,43 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react9.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react9.createElement)(custom.component, { held, decide, call: call3, actions })) : plain -+ ); -+} -+ -+// src/client/turn-reasoning.js -+function turnReasoning(steps, current) { -+ const members = steps.length ? steps : [current]; -+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); -+} -+var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); -+var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); -+var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); -+ -+// src/client/cells.js + ); + } +-var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); +-var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); +-var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); +-var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); +-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-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); +-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"]); +-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"]); +-var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); +-var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; + +-// src/client/questions.js +-var import_react4 = require("react"); +-var keyLetter = (index) => String.fromCharCode(65 + index); +-var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); +-var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; +-function useOptionKeys(count, onPick, ownRef) { +- const pick2 = (0, import_react4.useRef)(onPick); +- pick2.current = onPick; +- (0, import_react4.useEffect)(() => { +- if (count === 0) return void 0; ++// src/client/turn-reasoning.js ++function turnReasoning(steps, current) { ++ const members = steps.length ? steps : [current]; ++ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; ++ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); ++} ++var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); ++var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); ++var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); ++ ++// src/client/cells.js +var import_react10 = require("react"); +var import_react_dom = require("react-dom"); +var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -7218,12 +7641,25 @@ diff --git a/client.js b/client.js + if (refs.some((ref) => ref.current?.contains(event.target))) return; + close(); + }; -+ const onKey = (event) => { + const onKey = (event) => { +- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.key.length !== 1) return; +- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; +- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; +- const index = event.key.toUpperCase().charCodeAt(0) - 65; +- if (index >= 0 && index < count) { +- event.preventDefault(); +- pick2.current(index); +- } else if (index === count && ownRef.current) { +- event.preventDefault(); +- ownRef.current.focus(); + if (event.key === "Escape") { + event.stopPropagation(); + close(); -+ } -+ }; + } + }; +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); + return () => { @@ -7231,7 +7667,19 @@ diff --git a/client.js b/client.js + window.removeEventListener("keydown", onKey, true); + }; + }, [open]); -+} + } +-function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { +- const [own, setOwn] = (0, import_react4.useState)(""); +- const [picked, setPicked] = (0, import_react4.useState)([]); +- const ownRef = (0, import_react4.useRef)(null); +- const titleId = (0, import_react4.useId)(); +- const options = optionsOf(question); +- const multi = question.multiSelect === true; +- const custom = own.trim(); +- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; +- const choose = (label) => { +- if (multi) { +- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); +function MessageTools({ text: text2, name, sessionId, react, actions }) { + const [panel, setPanel] = (0, import_react10.useState)(null); + const [place, setPlace] = (0, import_react10.useState)(null); @@ -7243,19 +7691,75 @@ diff --git a/client.js b/client.js + const open = (kind, event) => { + if (panel === kind) { + close(); -+ return; -+ } + return; + } +- onAnswer({ selected: [label] }); + const rect = event.currentTarget.getBoundingClientRect(); + const below = rect.bottom + 220 < window.innerHeight; + setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); + setPanel(kind); -+ }; + }; +- const submit = () => { +- if (!canSubmit) return; +- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); + const copy2 = () => { + void navigator.clipboard?.writeText(text2).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1e3); + }); -+ }; + }; +- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); +- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; +- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { +- ref: ownRef, +- rows: 1, +- value: own, +- autoComplete: "off", +- spellCheck: false, +- className: "bt-q-input", +- "aria-label": t("Your own answer"), +- placeholder: t("Type your own answer"), +- "aria-keyshortcuts": ownLetter?.toLowerCase(), +- onChange: (event) => setOwn(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- submit(); +- } +- }) : null; +- const rows = options.map((option, optionIndex) => { +- const selected = multi && picked.includes(option.label); +- return (0, import_react4.createElement)( +- "button", +- { +- key: option.label, +- type: "button", +- className: "bt-q-opt", +- "aria-pressed": multi ? selected : void 0, +- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), +- onClick: () => choose(option.label) +- }, +- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), +- (0, import_react4.createElement)( +- "span", +- { className: "bt-q-body" }, +- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), +- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null +- ), +- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null +- ); +- }); +- if (raised && field) { +- rows.push((0, import_react4.createElement)( +- "label", +- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, +- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) +- )); +- } +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( + const download2 = () => { + const url = URL.createObjectURL(new Blob([text2], { type: "text/markdown" })); + const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); @@ -7273,7 +7777,25 @@ diff --git a/client.js b/client.js + sessionId ? (0, import_react10.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react10.createElement)(ReplyIcon)) : null, + (0, import_react10.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react10.createElement)(CheckIcon) : (0, import_react10.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-q-foot" }, +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), +- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) +- ); +- } else if ((raised || multi) && canSubmit) { +- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); +- } +- return (0, import_react4.createElement)( +- "form", +- { +- className: raised ? "bt-q bt-q-raised" : "bt-q", +- "aria-labelledby": titleId, +- onSubmit: (event) => { +- event.preventDefault(); +- submit(); +- } +- }, +- (0, import_react4.createElement)( + { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + REACTIONS.map((emoji) => (0, import_react10.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { + close(); @@ -7281,7 +7803,16 @@ diff --git a/client.js b/client.js + } }, emoji)) + ), document.body) : null, + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), +- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, +- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null +- ), +- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) + { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, + item(t("Copy"), CopyIcon, copy2), + item(t("Download"), DownloadIcon, download2) @@ -7320,7 +7851,15 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"), + t("Thought it through"), + peek ? (0, import_react10.createElement)("span", { className: "bt-think-peek" }, peek) : null -+ ), + ), +- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, +- field && !raised ? (0, import_react4.createElement)( +- "div", +- { className: "bt-q-custom" }, +- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), +- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null +- ) : null, +- foot + (0, import_react10.createElement)("div", { className: "bt-think-text" }, text2) + ); +} @@ -7393,122 +7932,14 @@ diff --git a/client.js b/client.js + { className: "bt-held-reply", "data-state": held.status, "data-held": isApprovalWaiting(held.status) ? "" : void 0 }, + children, + closing ? (0, import_react10.createElement)(HeldReplyActions, { held: { ...held, toName, from: bot?.name }, actions }) : null -+ ); + ); } --@keyframes bt-fade-in{from{opacity:0}to{opacity:1}} --@keyframes bt-fade-out{from{opacity:1}to{opacity:0}} --[data-composer-card]{display:flex!important;flex-direction:row;flex-wrap:wrap;align-items:center;gap:0!important;min-height:44px;border-radius:22px!important;padding:7.5px!important;box-sizing:border-box;background:var(--bt-main)!important;border:.5px solid var(--bt-line-2)!important;box-shadow:0 2px 8px -1px rgba(0,0,0,.05),0 1px 2px rgba(0,0,0,.03),0 0 0 1px rgba(228,228,228,.04)!important;transition:border-color .15s ease,background-color .15s ease,${spring(SPRING_SHAPE, "border-radius")}!important} --[data-composer-card]:hover,[data-composer-card]:focus-within{border-color:color-mix(in srgb,var(--bt-ink) 30%,transparent)!important} --[data-composer-card]:has(> [data-slot="conversation.input.attachments"] *),[data-composer-card]:has([data-composer-input] br + *),[data-composer-card]:has([data-composer-input] > :nth-child(2)){border-radius:18px!important} --[class*="_root"]:has(> [data-composer-card]){padding:0 11px 13px 16px!important} --[class*="_root"]:has(> [data-composer-card])>[data-composer-card]{max-width:min(992px,100%)!important} --[data-composer-card]>[class*="_row"]{display:contents!important} --[data-composer-card] [class*="_tools"]{order:1;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-input-scroll]{order:2;flex:1 1 0;min-width:0;padding:0 8px!important;margin:0!important} --[data-composer-card] [class*="_trailing"]{order:3;flex:none;align-self:flex-end;margin:0} --[data-composer-card]>[data-slot="conversation.input.attachments"]{order:0;flex-basis:100%} --[data-composer-card] [data-composer-input]{min-height:24px;padding:2px 0!important} --[data-composer-card] [data-composer-placeholder]{left:0!important;top:2px!important} --[data-composer-placeholder]{font-size:0!important;line-height:0!important} --[data-composer-placeholder]::after{content:var(--bt-placeholder,"Message");display:block;font-size:14px;line-height:24px;color:color-mix(in srgb,var(--bt-ink) 36%,transparent)} --/* The team's "+" (ComposerPlus) takes the seat of the shell's command-menu button. */ --[data-composer-card] button[class*="_add"]{display:none!important} --.bt-plus{flex:none;width:28px;height:28px;padding:0;border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2);cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-plus:hover,.bt-plus[aria-expanded=true]{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-plus:active{scale:.9} --.bt-plus svg{width:14px;height:14px;transition:${spring(SPRING_POP, "rotate")}} --.bt-plus[aria-expanded=true] svg{rotate:45deg} --.bt-plus-menu{min-width:180px} --[data-composer-card] [class*="_tools"]{gap:0!important} --[data-composer-card] [class*="_modes"]:empty{display:none!important} --[data-composer-card] [class*="_activity"]:not(:has([data-slot] > *)){display:none!important} --[data-composer-card] [class*="_trailing"]{gap:8px} --[data-composer-card] [class*="_standardControls"]{display:flex;align-items:center;gap:8px} --[data-composer-card] button[class*="_primary"]{width:28px!important;height:28px!important;background:var(--bt-accent)!important;color:var(--bt-accent-ink,#fff)!important;border-radius:50%!important;transition:opacity .15s ease,${spring(SPRING_TAP, "scale")};animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --[data-composer-card] button[class*="_primary"]:hover:not(:disabled){opacity:.9} --[data-composer-card] button[class*="_primary"]:active:not(:disabled){scale:.9} --@keyframes bt-icon-swap{from{opacity:0;transform:scale(.5) rotate(-30deg)}} --[data-composer-card] button[class*="_primary"] svg{width:14px;height:14px} --[data-composer-card] button[class*="_primary"]:disabled{display:none!important} --[data-composer-dock]{display:none!important} --.bt-mic,.bt-voice{flex:none;width:28px;height:28px;border-radius:50%;border:0;padding:0;display:inline-grid;place-items:center;cursor:pointer;transition:background-color .15s ease,color .15s ease,${spring(SPRING_TAP, "scale")}} --.bt-mic{background:color-mix(in srgb,var(--bt-ink) 5%,transparent);box-shadow:inset 0 0 0 .5px var(--bt-line);color:var(--bt-ink-2)} --.bt-mic:hover{background:color-mix(in srgb,var(--bt-ink) 10%,transparent);color:var(--bt-ink)} --.bt-mic[aria-pressed=true]{background:#ff3e51;color:#fff;animation:bt-mic-pulse 1.4s ease-in-out infinite} --@keyframes bt-mic-pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,62,81,.35)}50%{box-shadow:0 0 0 5px rgba(255,62,81,0)}} --.bt-voice{display:none;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);animation:bt-icon-swap ${SPRING_POP.duration}ms ${SPRING_POP.easing}} --.bt-voice:hover{opacity:.9} --.bt-mic:active,.bt-voice:active{scale:.9} --[data-composer-card]:has(button[class*="_primary"]:disabled) .bt-voice{display:inline-grid} --body[data-bt-room] .bt-voice{display:none!important} --body[data-bt-room] [data-composer-card]:has(button[class*="_primary"]:disabled) .bt-mic:not([aria-pressed=true]){background:var(--bt-accent);color:var(--bt-accent-ink,#fff)} --.bt-mic-note{position:absolute;right:12px;bottom:calc(100% + 8px);padding:5px 10px;border-radius:8px;background:var(--bt-user);color:var(--bt-user-ink);font-size:12px;line-height:16px;white-space:nowrap;pointer-events:none;animation:bt-rise .16s ease-out} --[data-chain-overlay-fallback="conversation.composer"]:has(~ .bt-q){display:block!important;order:2} --.bt-q{order:1} --div:has(> nav [class*="_marks"]){display:none!important} --/* Every user-side bubble (sent, steering mid-turn, not yet admitted, question replies) -- takes its surface and its text from the one --bt-user pair: a skin that repaints -- the host's bubble surface alone would otherwise leave the text unreadable. */ --[class*="_userRow"] [class*="_bubble"],[data-chat-flow-kind=question-reply] [class*="_bubble"],[data-chat-flow-kind=command-input] [class*="_bubble"]{background:var(--bt-user)!important;color:var(--bt-user-ink)!important;border-radius:18px!important;padding:7px 12px!important;font-size:var(--dsh-content-font-size,14px)!important;line-height:calc(var(--dsh-content-font-size,14px) + 8px)!important} --[class*="_userRow"] [class*="_bubble"] [class*="_markdown"],[class*="_userRow"] [class*="_bubble"] a{color:inherit} --/* Sent messages get the hover toolbar below; steering and unsent bubbles keep none. */ --[class*="_userRow"] [class*="_actions"]{display:none!important} --[data-chat-flow-kind=user] [class*="_userRow"]{flex-direction:row-reverse;align-items:center;justify-content:flex-start} --[data-chat-flow-kind=user] [class*="_actions"]{display:flex!important;height:24px!important;gap:2px!important;opacity:0!important;visibility:hidden;transition:opacity .12s ease,visibility .12s!important;flex:none} --[data-chat-flow-kind=user] [class*="_userRow"]:hover [class*="_actions"],[data-chat-flow-kind=user] [class*="_userRow"]:focus-within [class*="_actions"]{opacity:1!important;visibility:visible} --[data-chat-flow-kind=user] [class*="_actions"] [class*="_timeStart"]{display:none} --[data-chat-flow-kind=user] [class*="_actions"] button{width:24px;height:24px;border-radius:6px;color:var(--bt-ink-2)} --[data-chat-flow-kind=user] [class*="_actions"] button:hover{background:var(--bt-hover);color:var(--bt-ink)} --[data-chat-flow-kind=user] [class*="_actions"] svg{width:15px;height:15px} --.bt-q-inline{display:flex;margin:4px 0 6px} --[data-chat-flow-kind=assistant-step]+[data-step-process]:has(.bt-q-inline){margin-top:0!important} --[data-chat-flow-kind=question-reply]:has(.bt-q-inline),[data-chat-group-key]:has([data-chat-flow-kind=question-reply] .bt-q-inline),[data-chat-group-key]:has(.bt-q-gone){margin-top:0!important} --.bt-q-inline .bt-q{margin:0;max-width:min(550px,86%);order:0} --.bt-q-dock{width:100%;display:flex;justify-content:center} --.bt-q-dock .bt-q{margin:0 auto 8px} --[class*="_toBottomSlot"]{justify-content:center!important;padding-right:0!important} --[data-conversation-scroll] [class*="_toBottomSlot"]{bottom:calc(var(--dsh-composer-height,152px) + 8px)!important} --[class*="_toBottomSlot"]>button{width:36px!important;height:36px!important;margin-top:-36px!important;background:var(--bt-main)!important;color:var(--bt-ink)!important;box-shadow:0 0 0 .5px var(--bt-line-2),0 2px 8px -2px rgba(0,0,0,.12)!important;animation:bt-tobottom-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out;transition:background-color .12s ease,${spring(SPRING_TAP, "scale")}} --[class*="_toBottomSlot"]>button:active{scale:.9} --[class*="_toBottomSlot"]>button:hover{background:color-mix(in srgb,var(--bt-ink) 4%,var(--bt-main))!important} --[class*="_toBottomSlot"]>button svg{display:none} --[class*="_toBottomSlot"]>button::before{content:"";width:20px;height:20px;background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat;mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10 4v12M5 11l5 5 5-5'/%3E%3C/svg%3E") center/contain no-repeat} --@keyframes bt-tobottom-in{from{translate:0 8px;scale:.8}} --[data-bt-news] [class*="_toBottomSlot"]>button{visibility:hidden!important;pointer-events:none!important} --.bt-news{position:fixed;z-index:30;transform:translateX(-50%);display:inline-flex;align-items:center;overflow:hidden;border-radius:999px;background:#0c64c1;color:#fcfcfc;box-shadow:inset 0 0 0 1px rgba(20,20,20,.05),0 1px 3px 0 rgba(0,0,0,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-news-in ${SPRING_TAP.duration}ms ${SPRING_TAP.easing},bt-veil-in .16s ease-out} --@keyframes bt-news-in{from{translate:0 8px;scale:.9}} --.bt-news[data-direction=up]{animation-name:bt-news-in-up,bt-veil-in} --@keyframes bt-news-in-up{from{translate:0 -8px;scale:.9}} --.bt-news-jump{display:inline-flex;align-items:center;gap:2px;margin:0;padding:4px 26px 4px 4px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:13px;line-height:18px;white-space:nowrap;cursor:pointer;outline:none} --.bt-news-ico{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px} --.bt-news-ico svg{width:16px;height:16px} --.bt-news-x{position:absolute;right:4px;top:50%;z-index:1;transform:translateY(-50%);width:20px;height:20px;padding:0;border:0;border-radius:999px;background:transparent;color:inherit;opacity:.75;display:inline-flex;align-items:center;justify-content:center;cursor:pointer} --.bt-news-x svg{width:12px;height:12px} --.bt-news-x:hover{opacity:1;background:rgba(20,20,20,.5)} --@media (prefers-reduced-motion:reduce){.bt-news,[class*="_toBottomSlot"]>button{animation:none}} --.bt-new-sep{display:flex;align-items:center;gap:8px;margin:14px 0 8px;padding:2px 0;font-size:11px;line-height:14px;font-weight:500;letter-spacing:.02em;text-transform:uppercase;color:#0c64c1;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} --.bt-new-sep::before,.bt-new-sep::after{content:"";flex:1;height:1px;background:color-mix(in srgb,#0c64c1 20%,transparent)} --.bt-open-new{display:flex;flex-direction:column} --.bt-open-new .bt-new-sep{margin:18px 0 0} --.bt-open-new .bt-time-sep{height:16px;margin:32px 0 2px} --.bt-earlier{display:flex;flex-direction:column;gap:6px} --.bt-earlier-top{display:flex;justify-content:center;align-items:center;height:34px;flex:none} --.bt-earlier-top .bt-soft{height:28px;font-size:12px;padding:0 12px} --.bt-urow{display:flex;justify-content:flex-end} --.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere} --.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important} --.bt-ububble p{margin:0;line-height:22px} --.bt-uimages{display:block;font-size:12px;opacity:.7} --.bt-earlier .bt-event{margin:4px 0} --.bt-part-line{display:flex;align-items:center;gap:10px;margin:10px 0 6px;font-size:11px;line-height:14px;color:var(--bt-ink-3)} --.bt-part-line::before,.bt-part-line::after{content:"";flex:1;height:1px;background:var(--bt-line)} --`; --function installStyles(text, tag) { -- const style = document.createElement("style"); -- style.dataset.dshBot = tag; -- style.textContent = text; -- document.head.appendChild(style); -- return style; +-function SettledQuestion({ question, answer }) { +- const options = optionsOf(question); +- const prompt = question?.question ?? ""; +- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; +- if (chosen.length === 0) { +- return (0, import_react4.createElement)( +function PersonMessage({ name, texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi }) { + if (!(texts2 ?? []).some((text2) => String(text2 ?? "").trim() !== "")) return null; + const lead = (0, import_react10.createElement)("span", { className: "bt-lead", title: name }, (0, import_react10.createElement)(PersonMark, { name })); @@ -7522,7 +7953,13 @@ diff --git a/client.js b/client.js +function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { + if (whole && bots.length === 1) { + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, +- (0, import_react4.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), +- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) + { className: "bt-event" }, + (0, import_react10.createElement)( + "button", @@ -7530,9 +7967,10 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("span", null, prefix), + (0, import_react10.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react10.createElement)("span", { className: "bt-event-name" }, bots[0].name) -+ ) -+ ); -+ } + ) + ); + } +- return (0, import_react4.createElement)( + const parts = []; + parts.push((0, import_react10.createElement)("span", { key: "p" }, prefix)); + bots.forEach((bot, index) => { @@ -7594,12 +8032,54 @@ diff --git a/client.js b/client.js + room2?.admin === bot.id ? (0, import_react10.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + ); + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, +- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, +- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { +- const optionIndex = options.findIndex((option) => option.label === label); +- return (0, import_react4.createElement)( +- "div", +- { key: `${label}-${index}`, className: "bt-q-opt" }, +- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, +- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), +- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) +- ); +- })) + { className: "bt-group-msg" }, + name, + (0, import_react10.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) -+ ); -+} + ); + } +-function QuestionCard({ matched }) { +- const pending = matched; +- const questions = pending.questions ?? []; +- const [index, setIndex] = (0, import_react4.useState)(0); +- const [answers, setAnswers] = (0, import_react4.useState)([]); +- const question = questions[index]; +- if (question === void 0) return null; +- const onAnswer = (answer) => { +- const next = [...answers, { id: question.id, ...answer }]; +- if (index + 1 < questions.length) { +- setAnswers(next); +- setIndex(index + 1); +- return; +- } +- pending.answer({ answers: next }); +- }; +- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +-} +-function PendingQuestion({ sessionId, question, actions, inline }) { +- const [answered, setAnswered] = (0, import_react4.useState)(null); +- if (answered === question.id) return null; +- const onAnswer = (answer) => { +- setAnswered(question.id); +- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); +- }; +- const onDismiss = () => { +- setAnswered(question.id); +- void actions.dismissQuestion(sessionId); +- }; +- return (0, import_react4.createElement)( +function remoteSpeaker(source, roster, sessionId) { + const handle = source.remote.handle; + const member = (roster.remoteBots ?? []).find((bot2) => bot2.handle === handle); @@ -7658,7 +8138,9 @@ diff --git a/client.js b/client.js + const mark = (0, import_react10.createElement)(BotMark, { bot, size: 22 }); + const lead = chatId ? (0, import_react10.createElement)("button", { type: "button", className: "bt-lead", title: handle, "aria-label": t("Open {name}'s chat", { name }), onClick: () => actions.openSession(chatId) }, mark) : (0, import_react10.createElement)("span", { className: "bt-lead", title: handle }, mark); + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: inline ? "bt-q-inline" : "bt-q-dock" }, +- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) + { className: "bt-group-msg bt-remote-msg", "data-remote": handle }, + // A face room's header is the one name alone; the handle is on hover only (#457). + face ? (0, import_react10.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react10.createElement)( @@ -7669,14 +8151,32 @@ diff --git a/client.js b/client.js + ), + String(words ?? "").trim() === "" ? null : (0, import_react10.createElement)(Bubbles, { texts: [words], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react10.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null -+ ); -+} + ); + } +-function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { +- const question = useRoster((value) => value.questions?.[sessionId]); +- 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; +- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); +function FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; + const bot = roster.byId[sessionId]?.name ?? t("your Bot"); + return (0, import_react10.createElement)("div", { className: "bt-event", "data-first-contact": source.remote.handle }, (0, import_react10.createElement)("span", null, t("{name} is talking to {bot} through Bot Social", { name: who, bot }))); -+} + } +-function AnsweredCell({ node }) { +- const data = node.data ?? {}; +- const questions = Array.isArray(data.questions) ? data.questions : []; +- if (questions.length === 0) { +- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; +- } +- const answers = Array.isArray(data.answers) ? data.answers : []; +- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { +- key: question.id, +- question, +- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } +- }))); +function GlobeMark() { + return (0, import_react10.createElement)( + "svg", @@ -7684,7 +8184,60 @@ diff --git a/client.js b/client.js + (0, import_react10.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react10.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ); -+} + } +- +-// src/client/secret-card.js +-var import_react5 = require("react"); +-var MIN_VALUE = 4; +-var NONE = {}; +-var KeyIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), +- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) +-); +-var LockIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), +- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) +-); +-var WarnIcon = () => (0, import_react5.createElement)( +- "svg", +- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, +- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), +- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) +-); +-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); +-var scopeLabel = (scope, roster) => { +- if (scope === "all") return t("All Bots"); +- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); +- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); +-}; +-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 }) { +- return (0, import_react5.createElement)("input", { +- className: "bt-secret-input", +- type: MASK_BY_CSS ? "text" : "password", +- "data-masked": MASK_BY_CSS ? "" : void 0, +- value, +- autoFocus, +- autoComplete: MASK_BY_CSS ? "off" : "new-password", +- autoCorrect: "off", +- autoCapitalize: "off", +- spellCheck: false, +- "data-1p-ignore": "", +- "data-lpignore": "true", +- "data-bwignore": "", +- "data-form-type": "other", +- "aria-label": label, +- placeholder: t("Paste or type the key"), +- onChange: (event) => onChange(event.target.value), +- onKeyDown: (event) => { +- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; +- event.preventDefault(); +- onEnter?.(); +function RemoteFiles({ files, plugin, botId }) { + const [busy, setBusy] = (0, import_react10.useState)(""); + const [error, setError] = (0, import_react10.useState)(""); @@ -7703,7 +8256,8 @@ diff --git a/client.js b/client.js + setError(String(reason?.message ?? reason)); + } finally { + setBusy(""); -+ } + } +- }); + }; + return (0, import_react10.createElement)( + "div", @@ -7715,15 +8269,30 @@ diff --git a/client.js b/client.js + )), + error ? (0, import_react10.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null + ); -+} + } +-function ScopeSwitch({ value, onChange, bot }) { +- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; +- return (0, import_react5.createElement)( +function GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, +- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), +- options.map(([id, text]) => (0, import_react5.createElement)("button", { +- key: id, +- type: "button", +- role: "radio", +- "aria-checked": value === id, +- className: "bt-secret-seg", +- onClick: () => onChange(id) +- }, text)) + { className: "bt-event" }, + (0, import_react10.createElement)("span", null, prefix), + room2 ? (0, import_react10.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react10.createElement)("span", null, name) : null -+ ); -+} + ); + } +-function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.createElement)( +function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { + if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; + const self = roster.byId[sessionId]; @@ -7733,11 +8302,40 @@ diff --git a/client.js b/client.js + if (!target) return null; + const label = team ? t("Team memory updated") : target === self ? t("Memory updated") : t("Updated {name}'s memory", { name: target.name }); + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-q-head bt-secret-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-text" }, +- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), +- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") +- ), +- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null + { className: "bt-event" }, + (0, import_react10.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react10.createElement)(MemoryIcon), label) -+ ); -+} + ); + } +-function SecretCard({ bot, request, roster, actions }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [scope, setScope] = (0, import_react5.useState)("all"); +- const [replacing, setReplacing] = (0, import_react5.useState)(false); +- const [busy, setBusy] = (0, import_react5.useState)(false); +- const [error, setError] = (0, import_react5.useState)(""); +- const noteId = (0, import_react5.useId)(); +- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); +- const run = (promise) => { +- setBusy(true); +- setError(""); +- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); +- }; +- const cancel = () => run(actions.secretCancel(request.id)); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); +- }; +- const allow = () => { +- if (!busy) void run(actions.secretAllow(request.id)); +function PartLine({ time }) { + return (0, import_react10.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react10.createElement)("span", null, dayLabel(time)) : null); +} @@ -7767,15 +8365,35 @@ diff --git a/client.js b/client.js + busy.current = false; + if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); + }); -+ }; -+ (0, import_react10.useEffect)(() => { -+ alive.current = true; -+ load(void 0); -+ return () => { -+ alive.current = false; -+ }; -+ }, [sessionId]); -+ (0, import_react10.useLayoutEffect)(() => { + }; +- const filling = request.kind === "fill" || replacing; +- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, +- (0, import_react5.createElement)(SecretHead, { +- bot, +- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), +- detail: request.purpose, +- name: request.name, +- onClose: busy ? void 0 : cancel +- }), +- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, +- filling ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-body" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), +- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null +- ) : null, +- (0, import_react5.createElement)( ++ (0, import_react10.useEffect)(() => { ++ alive.current = true; ++ load(void 0); ++ return () => { ++ alive.current = false; ++ }; ++ }, [sessionId]); ++ (0, import_react10.useLayoutEffect)(() => { + const pending = restore.current; + restore.current = null; + if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; @@ -7808,10 +8426,24 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, + (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-secret-note", id: noteId }, +- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), +- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), +- ".", +- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" + { ref: top, className: "bt-earlier-top" }, + view.loading ? (0, import_react10.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react10.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react10.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null -+ ), + ), +- warn, +- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-foot" }, +- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), +- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) +- ) + rows.map((row, index) => { + const key = rows.length - index; + switch (row.kind) { @@ -7841,16 +8473,27 @@ diff --git a/client.js b/client.js + } + }), + (0, import_react10.createElement)(PartLine, { time: view.startedAt }) -+ ); + ); } --var FRAGMENT_SELECTOR = /\[class\*="(_[\w-]+)"\]/g; --var CLASS_TOKEN = /\.((?:\\.|[\w-])+)/g; --function sheetClassNames(sheet) { -- const names = []; -- const visit = (rules) => { -- for (const rule of rules) { -- if (rule.selectorText) for (const match of rule.selectorText.matchAll(CLASS_TOKEN)) names.push(match[1]); -- if (rule.cssRules) visit(rule.cssRules); +-function SettledSecret({ bot, request, roster }) { +- const canceled = request.status === "canceled"; +- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-q-head" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), +- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( +- "p", +- { className: "bt-q-title" }, +- text, +- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) +- )), +- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) +- ) +- ); +function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { + const roster = useRoster((value) => value); + const data = node.data; @@ -7885,11 +8528,7 @@ diff --git a/client.js b/client.js + if (source.remote?.handle) { + const message = (0, import_react10.createElement)(RemoteMessage, { source, text: remoteMessageText(stripHeader(contentText(data.content))), roster, actions, sessionId, msgKey: `${sessionId}:${data.turn}:remote`, useUi }); + return source.remote.firstContact === true ? (0, import_react10.createElement)(import_react10.Fragment, null, (0, import_react10.createElement)(FirstContact, { source, roster, sessionId }), message) : message; - } -- }; -- try { -- visit(sheet.cssRules); -- } catch { ++ } + if (source.kind === "bot") { + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react10.createElement)(EarlierParts, { sessionId, roster, actions, useUi }); + if (source.role === "room" || source.role === "kickoff") return null; @@ -7933,21 +8572,21 @@ diff --git a/client.js b/client.js + } + if (source.kind === "user-question-reply") return null; + return (0, import_react10.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); - } -- return names; ++ } } --function compileFragmentSelectors(text, names) { -- const compiled = /* @__PURE__ */ new Map(); -- return text.replace(FRAGMENT_SELECTOR, (whole, fragment) => { -- if (!compiled.has(fragment)) { -- const exact = []; -- for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -- compiled.set(fragment, exact.length ? `:is(${exact.join(",")})` : whole); -- } -- return compiled.get(fragment); +-function RequestView({ bot, request, roster, actions }) { +- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); +function NewDivider() { + return (0, import_react10.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react10.createElement)("span", null, t("New"))); -+} + } +-function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { +- const bot = roster.byId[sessionId]; +- if (!bot) return null; +- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); +- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); +- if (done && /already saved/.test(contentText(content))) { +- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); +- } +function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { + const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; + (0, import_react10.useEffect)(() => { @@ -7963,84 +8602,22 @@ diff --git a/client.js b/client.js + const fresh = useTurnClock((state) => { + 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; - }); ++ }); + return fresh ? (0, import_react10.createElement)(NewDivider) : null; - } --function installCompiledStyles(text, tag) { -- const style = installStyles("", tag); -- const cache = /* @__PURE__ */ new WeakMap(); -- const render = () => { -- const names = /* @__PURE__ */ new Set(); -- for (const sheet of document.styleSheets) { -- if (sheet.ownerNode?.dataset?.dshBot) continue; -- let list = cache.get(sheet); -- if (!list) { -- list = sheetClassNames(sheet); -- cache.set(sheet, list); -- } -- for (const name of list) names.add(name); -- } -- const next = compileFragmentSelectors(text, names); -- if (style.textContent !== next) style.textContent = next; -- }; -- render(); -- let timer; -- const schedule = () => { -- clearTimeout(timer); -- timer = setTimeout(render, 120); -- }; -- const onLoad = (event) => { -- if (event.target instanceof HTMLLinkElement) schedule(); -- }; -- const observer = new MutationObserver(schedule); -- observer.observe(document.head, { childList: true }); -- document.addEventListener("load", onLoad, true); -- return () => { -- observer.disconnect(); -- document.removeEventListener("load", onLoad, true); -- clearTimeout(timer); -- style.remove(); -- }; ++} +function Nothing() { -+ return null; + return null; } -- --// src/client/mark.js --var import_react2 = require("react"); --var STATES = ["idle", "thinking", "searching", "working", "sending", "orbit", "alert", "done"]; --var botState = (running, activity, alert) => running ? STATES.includes(activity) ? activity : "working" : alert ? "alert" : "idle"; --var toolState = (name) => /^(message_bot|stop_bot|create_bot|update_bot|create_group|update_group|delete_group|post_to_group)$/.test(name) ? "sending" : /search|fetch|browse|read|grep|glob|list|recall/i.test(name) ? "searching" : "working"; --function useGaze(ref, enabled) { -- (0, import_react2.useEffect)(() => { -- if (!enabled) return void 0; -- let frame = 0; -- let point = null; -- const place = () => { -- frame = 0; -- const node = ref.current; -- if (!node || !point) return; -- const box = node.getBoundingClientRect(); -- const clamp = (value) => Math.max(-1, Math.min(1, value)); -- node.style.setProperty("--bt-gx", clamp((point.x - box.left - box.width / 2) / 160).toFixed(2)); -- node.style.setProperty("--bt-gy", clamp((point.y - box.top - box.height / 2) / 160).toFixed(2)); -- }; -- const onMove = (event) => { -- point = { x: event.clientX, y: event.clientY }; -- if (!frame) frame = requestAnimationFrame(place); -- }; -- const onLeave = () => { -- point = null; -- ref.current?.style.setProperty("--bt-gx", "0"); -- ref.current?.style.setProperty("--bt-gy", "0"); -- }; -- window.addEventListener("pointermove", onMove, { passive: true }); -- document.documentElement.addEventListener("pointerleave", onLeave); -- return () => { -- cancelAnimationFrame(frame); -- window.removeEventListener("pointermove", onMove); -- document.documentElement.removeEventListener("pointerleave", onLeave); -- }; -- }, [enabled]); +-function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { +- const roster = useRoster((value) => value); +- const shown = useCards?.((state) => state[sessionId]) ?? NONE; +- const loading = useSession?.(historyLoading) === true; +- const bot = roster.byId[sessionId]; +- if (!bot || currentPart(bot) !== sessionId) return null; +- 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; +- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); +var HIDDEN_REMOVED_TOOLS = /* @__PURE__ */ new Set(["ask_user_question"]); +var isConnectorTool = (name) => typeof name === "string" && /^mcp__(github|composio)__/.test(name); +var SILENT_REGISTRATION_TOOLS = /* @__PURE__ */ new Set([ @@ -8097,35 +8674,38 @@ diff --git a/client.js b/client.js + if (parts.length === 0) return null; + return (0, import_react10.createElement)("div", { className: "bt-event" }, parts.join(" · ")); } --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); -- const ref = (0, import_react2.useRef)(null); -- const maskId = (0, import_react2.useRef)(""); -- if (maskId.current === "") maskId.current = `bt-mark-${nextMarkSerial()}`; -- const [poked, setPoked] = (0, import_react2.useState)(false); -- useGaze(ref, gaze); -- const common = { -- ref, -- className: poked ? "bt-mark bt-mark-poke" : "bt-mark", -- "data-shape": look.image ? "image" : shapeId(look.shape), -- "data-state": STATES.includes(state) ? state : "idle", -- "data-live": live ? "" : void 0, -- "data-fidget": live ? FIDGETS[fnv1a(`${bot?.id ?? ""}:fidget`) % FIDGETS.length] : void 0, -- "data-gaze": gaze ? "" : void 0, -- "data-poke": pokeable ? "" : void 0, -- style: { width: size, height: size, ...live ? { "--bt-delay": `${-(fnv1a(String(bot?.id ?? "")) % 5e3) / 1e3}s`, "--bt-fidget-delay": `${-(fnv1a(`${bot?.id ?? ""}:when`) % 9e3) / 1e3}s` } : {} }, -- role: label ? "img" : void 0, -- "aria-label": label, -- "aria-hidden": label ? void 0 : true, -- onClick: pokeable ? () => setPoked(true) : void 0, -- onAnimationEnd: poked ? (event) => { -- if (/^bt-poke/.test(event.animationName)) setPoked(false); -- } : void 0 -- }; -- if (look.image) return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("img", { className: "bt-mark-img", src: look.image, alt: "", draggable: false })); -- const box = shapeOf(look.shape).size; -- return (0, import_react2.createElement)("span", common, (0, import_react2.createElement)("svg", { viewBox: `0 0 ${box} ${box}`, dangerouslySetInnerHTML: { __html: characterMarkup(look.shape, look.color).replaceAll(MASK_ID, maskId.current) } })); +-function ScopeEditor({ secret, roster, onSave }) { +- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); +- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); +- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, +- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( +- "label", +- { key: id, className: "bt-secret-radio" }, +- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), +- text +- )) +- ), +- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { +- key: bot.id, +- type: "button", +- className: "bt-secret-bot", +- "aria-pressed": picked.includes(bot.id), +- onClick: () => toggle(bot.id) +- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) +- ) +- ); +var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +function clockLabel(time, now = Date.now()) { + const date = new Date(time); @@ -8137,11 +8717,35 @@ diff --git a/client.js b/client.js + const sameYear = date.getFullYear() === new Date(now).getFullYear(); + return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; } --function StarIcon({ size = 8 }) { -- return (0, import_react2.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 10 10", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M5 .6l1.3 2.8 3 .3-2.3 2 .7 3L5 7.2 2.3 8.7l.7-3L.7 3.7l3-.3z", fill: "#fff" }) +-function ValueEditor({ secret, onSave }) { +- const [value, setValue] = (0, import_react5.useState)(""); +- const [replaced, setReplaced] = (0, import_react5.useState)(false); +- const long = value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!long) return; +- void Promise.resolve(onSave(value)).then((ok) => { +- if (ok) { +- setValue(""); +- setReplaced(true); +- } +- }); +- }; +- const edit = (next) => { +- setValue(next); +- setReplaced(false); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit-block" }, +- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-inline" }, +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) +- ), +- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null +- ); +function nextTurnStart(turns, turn) { + let next; + for (const key in turns) { @@ -8149,7 +8753,48 @@ diff --git a/client.js b/client.js + if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; + } + return next === void 0 ? void 0 : turns[next].first; -+} + } +-function SecretRow({ secret, roster, actions, run, open, onToggle }) { +- const [confirm, setConfirm] = (0, import_react5.useState)(false); +- (0, import_react5.useEffect)(() => { +- if (!open) setConfirm(false); +- }, [open]); +- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); +- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); +- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, +- (0, import_react5.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- ), +- (0, import_react5.createElement)( +- "span", +- { className: "bt-set-control" }, +- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) +- ) +- ), +- open ? (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-edit" }, +- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), +- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- confirm ? [ +- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), +- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), +- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) +- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) +- ) +- ) : null +function openingTurn(turns, firstTurns, hiddenTurns) { + if (!turns || !firstTurns) return null; + const start = nextTurnStart(turns, -Infinity); @@ -8193,27 +8838,60 @@ diff --git a/client.js b/client.js + fresh ? (0, import_react10.createElement)(NewDivider) : null ); } --function AdminStar() { -- return (0, import_react2.createElement)("span", { className: "bt-admin-star", role: "img", "aria-label": t("Admin") }, (0, import_react2.createElement)(StarIcon, { size: 7 })); --} --function MainStar() { -- return (0, import_react2.createElement)( -- "svg", -- { viewBox: "0 0 20 20", "aria-hidden": true }, -- (0, import_react2.createElement)("path", { d: "M10 2.4l2.29 5.04 5.51.63-4.09 3.74 1.11 5.42L10 14.5l-4.82 2.73 1.11-5.42L2.2 8.07l5.51-.63z" }) +-function AddSecret({ actions, run, onDone }) { +- const [name, setName] = (0, import_react5.useState)(""); +- const [purpose, setPurpose] = (0, import_react5.useState)(""); +- const [value, setValue] = (0, import_react5.useState)(""); +- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; +- const save = () => { +- if (!ready) return; +- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { +- if (ok) { +- setValue(""); +- onDone(); +- } +- }); +- }; +- return (0, import_react5.createElement)( +- "div", +- { className: "bt-set-row bt-set-paste bt-secret-add" }, +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), +- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), +- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), +- (0, import_react5.createElement)( +- "div", +- { className: "bt-secret-row-actions" }, +- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), +- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) +- ) - ); +function Shimmer({ children }) { + return (0, import_react10.createElement)("span", { className: "bt-shimmer" }, children); } --function useSettled(state) { -- const prev = (0, import_react2.useRef)(state); -- const [settled, setSettled] = (0, import_react2.useState)(false); -- (0, import_react2.useEffect)(() => { -- const was = prev.current; -- prev.current = state; -- if (state !== "idle") { -- setSettled(false); -- return void 0; +-function SecretsPage({ roster, actions, run, Section }) { +- const [open, setOpen] = (0, import_react5.useState)(null); +- const [adding, setAdding] = (0, import_react5.useState)(false); +- const secrets = roster.secrets ?? []; +- return [ +- (0, import_react5.createElement)( +- "p", +- { key: "about", className: "bt-secret-about" }, +- t("Keys your Bots asked for. Bots never see the values: they use them as "), +- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), +- t(" in their shell, and every value is replaced by "), +- (0, import_react5.createElement)("code", null, "[secret:NAME]"), +- t(" before anything goes to a model.") +- ), +- (0, import_react5.createElement)( +- Section, +- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, +- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, +- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), +- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null +- ), +- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) +- ]; +var PHRASES = { + thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], + working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] @@ -8243,13 +8921,7 @@ diff --git a/client.js b/client.js + case "message_bot": { + const target = findBot(arg("to")); + return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; - } -- if (was === "idle" || was === "alert") return void 0; -- setSettled(true); -- const timer = setTimeout(() => setSettled(false), 1800); -- return () => clearTimeout(timer); -- }, [state]); -- return settled; ++ } + case "stop_bot": { + const target = findBot(arg("bot")); + return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; @@ -8278,48 +8950,101 @@ diff --git a/client.js b/client.js + return { label: phrase ? t(phrase) : null, state: toolState(name) }; + } } --function BotAvatar({ bot, size, state = "idle", main, admin, badge = true, live = true, gaze = false, pokeable = false }) { -- const settled = useSettled(state); -- if (bot === void 0) return (0, import_react2.createElement)(BotMark, { size }); -- const badgeKind = !badge ? null : state === "alert" ? "alert" : state !== "idle" ? "working" : admin ? "admin" : main ? "main" : null; -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-mark", style: { width: size, height: size, "--bt-size": `${size}px` } }, -- (0, import_react2.createElement)(BotMark, { bot, size, state: settled ? "done" : state, live, gaze, pokeable }), -- badgeKind === "working" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-working", "aria-label": t("Working"), role: "img" }) : null, -- badgeKind === "alert" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-alert", "aria-label": t("Waiting for your answer"), role: "img" }, "?") : null, -- badgeKind === "admin" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-admin", "aria-label": t("Admin"), role: "img" }, (0, import_react2.createElement)(StarIcon)) : null, -- badgeKind === "main" ? (0, import_react2.createElement)("span", { className: "bt-badge bt-badge-main", "aria-label": t("Main Bot"), role: "img" }, (0, import_react2.createElement)(MainStar)) : null -- ); +-var SECRET_CSS = ` +-.bt-secret-head{align-items:center;gap:10px} +-.bt-secret-who{position:relative;flex:none;display:inline-flex} +-.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} +-.bt-secret-lock svg{width:11px;height:11px} +-.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-secret-body{display:flex;flex-direction:column;gap:8px} +-.bt-secret-field{align-items:center;min-height:36px} +-.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} +-.bt-secret-input[data-masked]{-webkit-text-security:disc} +-.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} +-.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} +-.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-secret-glide[data-at="1"]{transform:translateX(100%)} +-.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} +-.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} +-.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} +-/* A key card asks the user to act, so the process group carrying it stays open in every +- work-details mode (same override as .bt-group-replies in styles.js). */ +-[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} +-[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} +-.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} +-.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} +-@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} +-.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} +-.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} +-.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} +-.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} +-.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} +-.bt-secret-done .bt-q-head{align-items:center} +-.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} +-.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} +-.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} +-@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} +-@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} +-.bt-secret-event{display:inline-flex;align-items:center;gap:5px} +-.bt-secret-dock{flex-direction:column;align-items:center} +-.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} +-.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} +-.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} +-.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} +-.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} +-.bt-secret-faces{display:inline-flex} +-.bt-secret-faces>*+*{margin-left:-6px} +-.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} +-.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} +-.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} +-.bt-secret-radios{display:flex;gap:16px;font-size:13px} +-.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} +-.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} +-.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} +-.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} +-.bt-secret-bot svg{width:13px;height:13px} +-.bt-secret-inline{display:flex;gap:8px;align-items:center} +-.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} +-.bt-secret-row-actions .bt-set-hint{flex:1} +-.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} +-.bt-secret-danger svg{width:13px;height:13px} +-.bt-secret-add .bt-input{height:34px} +-@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +-`; +- +-// src/client/turn-reasoning.js +-function turnReasoning(steps, current) { +- const members = steps.length ? steps : [current]; +- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; +- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); +function workPhrase(tool) { + if (!tool) return t("Thinking about it"); + return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); } --function clusterSeats(count, frame) { -- if (count === 1) return [[0, 0, frame]]; -- if (count === 2) return [[0, 0, frame * 2 / 3], [frame / 3, frame / 3, frame * 2 / 3]]; -- if (count === 3) { -- const size = frame * 5 / 9; -- const rest = frame - size; -- return [[rest / 2, 0, size], [0, rest, size], [rest, rest, size]]; -- } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; -+function workText(tool, step) { -+ return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; - } --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; -- const seat = (bot, seatSize, key) => bot ? (0, import_react2.createElement)(BotMark, { key, bot, size: seatSize }) : (0, import_react2.createElement)("span", { key, className: "bt-cluster-you", style: { width: seatSize * 0.92, height: seatSize * 0.92, margin: seatSize * 0.04 } }); -- if (people.length === 0) return (0, import_react2.createElement)(BotMark, { size }); -- if (size <= 20 && people.length > 1) { -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-stack", style: { "--bt-stack-size": `${size}px` } }, -- people.slice(0, 3).map((bot, index) => seat(bot, size, bot?.id ?? `you-${index}`)), -- people.length > 3 ? (0, import_react2.createElement)("span", { className: "bt-stack-more" }, `+${people.length - 3}`) : null -- ); +-var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); +-var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); +-var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); +- +-// src/client/cells.js +-var import_react6 = require("react"); +-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") }); +-function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { +- const onClick = (event) => { +- const target = mentionTarget(event); +- if (target === null) return; +- event.preventDefault(); +- event.stopPropagation(); +- actions.openSession(target); +- }; +- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; +- return (0, import_react6.createElement)( ++function workText(tool, step) { ++ return step ? t("{action}: {detail}", { action: workPhrase(tool), detail: step }) : `${workPhrase(tool)}…`; ++} +function useLiveNote(actions, sessionId, id, entry) { + const key = entry ? JSON.stringify(entry) : ""; + (0, import_react10.useEffect)(() => { @@ -8359,24 +9084,21 @@ diff --git a/client.js b/client.js + } catch { + replies = []; + } - } -- const seats = clusterSeats(Math.min(people.length, 4), size); -- const overflow = people.length > 4 ? people.length - 3 : 0; -- const gap = size / 14; -- const rings = seats.map(([x, y, s]) => ({ cx: x + s / 2, cy: y + s / 2, r: s * 0.48 + gap })); -- return (0, import_react2.createElement)( -- "span", -- { className: "bt-cluster", style: { width: size, height: size } }, -- seats.map(([x, y, s], index) => { -- const holes2 = rings.slice(index + 1).filter((ring) => Math.hypot(ring.cx - x - s / 2, ring.cy - y - s / 2) < ring.r + s * 0.46).map((ring) => `radial-gradient(circle at ${ring.cx - x}px ${ring.cy - y}px,transparent ${ring.r}px,#000 ${ring.r + 0.5}px)`); -- 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) { -- return (0, import_react2.createElement)("span", { key: "more", className: "bt-cluster-more", style: { ...style, width: s * 0.92, height: s * 0.92, margin: s * 0.04, fontSize: Math.max(8, Math.round(s * 0.36)) } }, `+${overflow}`); ++ } + if (replies.length === 0) return null; + const room2 = roster.roomsById[sessionId]; + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, +- texts2.map((text, index) => { +- const key = msgKey ? `${msgKey}:${index}` : void 0; +- const picked = key ? reactions?.[index] ?? [] : []; +- const last = index === texts2.length - 1; +- const live = streaming && last; +- return (0, import_react6.createElement)( +- "div", +- { key: index, className: "bt-msg" }, +- (0, import_react6.createElement)( + { className: "bt-group-replies" }, + replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; @@ -8424,7 +9146,24 @@ diff --git a/client.js b/client.js + } + if (reply.kind === "approval" || reply.kind === "working") { + return (0, import_react10.createElement)( -+ "div", + "div", +- { className: "bt-msg-line" }, +- lead && last ? lead : null, +- (0, import_react6.createElement)( +- "div", +- { className: "bt-bubble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) +- ), +- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) +- ), +- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { +- key: emoji, +- type: "button", +- className: "bt-react", +- "aria-label": t("Remove reaction {emoji}", { emoji }), +- onClick: () => actions.toggleReaction(msgKey, index, emoji) +- }, emoji))) : null +- ); + { key: `${reply.botId}-${index}`, className: "bt-event" }, + (0, import_react10.createElement)( + "button", @@ -8454,18 +9193,62 @@ diff --git a/client.js b/client.js + msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, + useUi + }); - } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); ++ } + return (0, import_react10.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi, seq: reply.seq }); }) ); } -- --// src/client/icons.js --var import_react3 = require("react"); --function PlusIcon() { -- return (0, import_react3.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 3.5v13M3.5 10h13", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +-function useDismiss(open, close, refs) { +- (0, import_react6.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (refs.some((ref) => ref.current?.contains(event.target))) return; +- close(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +-} +-function MessageTools({ text, name, sessionId, react, actions }) { +- const [panel, setPanel] = (0, import_react6.useState)(null); +- const [place, setPlace] = (0, import_react6.useState)(null); +- const [copied, setCopied] = (0, import_react6.useState)(false); +- const tools = (0, import_react6.useRef)(null); +- const pop = (0, import_react6.useRef)(null); +- const close = () => setPanel(null); +- useDismiss(panel !== null, close, [tools, pop]); +- const open = (kind, event) => { +- if (panel === kind) { +- close(); +- return; +- } +- const rect = event.currentTarget.getBoundingClientRect(); +- const below = rect.bottom + 220 < window.innerHeight; +- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); +- setPanel(kind); +- }; +- const copy2 = () => { +- void navigator.clipboard?.writeText(text).then(() => { +- setCopied(true); +- setTimeout(() => setCopied(false), 1e3); +- }); +- }; +- const download2 = () => { +- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); +- link.click(); +- setTimeout(() => URL.revokeObjectURL(url), 1e3); +var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +function callTree(root) { @@ -8477,12 +9260,42 @@ diff --git a/client.js b/client.js + seen.add(id); + calls.push(call4); + for (const child of call4.subCalls ?? []) visit(child); -+ }; + }; +- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { +- close(); +- run(); +- } }, (0, import_react6.createElement)(Icon), label); +- return (0, import_react6.createElement)( +- "div", +- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, +- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, +- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, +- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), +- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, +- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { +- close(); +- react(emoji); +- } }, emoji)) +- ), document.body) : null, +- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( +- "div", +- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, +- item(t("Copy"), CopyIcon, copy2), +- item(t("Download"), DownloadIcon, download2) +- ), document.body) : null +- ); + visit(root); + return calls; } --function SearchIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react3.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-function ThinkingRow({ text }) { +- return (0, import_react6.createElement)( +- "details", +- { className: "bt-think" }, +- (0, import_react6.createElement)("summary", null, t("Thought it through")), +- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +- ); +function useShownCards(actions, sessionId, callIds) { + const key = JSON.stringify(callIds); + (0, import_react10.useLayoutEffect)(() => { @@ -8491,7 +9304,16 @@ diff --git a/client.js b/client.js + actions.noteCards(sessionId, ids, 1); + return () => actions.noteCards(sessionId, ids, -1); + }, [sessionId, key]); -+} + } +-function useFileMentions({ node, openFile, fileMentions, useTurnData }) { +- const tail = useTurnData?.("turn-tail"); +- const location2 = node.location; +- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; +- const seq = node.data.finalNode?.seq; +- return (0, import_react6.useMemo)(() => { +- 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]); +function renderCall(call4, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call4); + const done = call4.kind === "tool-result"; @@ -8569,16 +9391,41 @@ diff --git a/client.js b/client.js + return null; + } } --function PeopleIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), -- (0, import_react3.createElement)("path", { d: "M2 16.5c.6-2.8 2.8-4.5 5.5-4.5s4.9 1.7 5.5 4.5" }), -- (0, import_react3.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) -- ); +-function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { +function ToolCell({ node, sessionId, useRoster, useUi, actions }) { -+ const roster = useRoster((value) => value); + const roster = useRoster((value) => value); +- const data = node.data; +- const steps = useTurnSteps(String(data.turn)); +- const contexts = useTurnContext(String(data.turn)); +- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); +- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); +- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); +- 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; +- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); +- 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; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- relays.map((relay) => { +- const source = relay.source; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(EventLine, { +- key: relay.seq, +- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), +- bots: [sender], +- onOpen: () => { +- if (sender.id) actions.openExchange(selfId, sender.id); +- }, +- whole: true +- }); +- }), +- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, +- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +- ); + const root = node.data.root; + const turn = node.location?.turn?.turn ?? root.turn; + const hidden = isHiddenTurn(roster, sessionId, turn); @@ -8593,8 +9440,32 @@ diff --git a/client.js b/client.js + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react10.createElement)(import_react10.Fragment, null, drawn.map((call4, index) => (0, import_react10.createElement)(import_react10.Fragment, { key: call4.callId ?? index }, renderCall(call4, env)))); } --function ArrowUpIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M10 16V4M5 9l5-5 5 5", stroke: "currentColor", strokeWidth: 1.8, fill: "none", strokeLinecap: "round", strokeLinejoin: "round" })); +-function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { +- if (whole && bots.length === 1) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, +- (0, import_react6.createElement)("span", null, prefix), +- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) +- ) +- ); +- } +- const parts = []; +- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- bots.forEach((bot, index) => { +- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); +- parts.push((0, import_react6.createElement)( +- "button", +- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- bot.name +- )); +- }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -8603,26 +9474,37 @@ diff --git a/client.js b/client.js + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; } --function MicIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), -- (0, import_react3.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) -- ); -+ -+// src/client/remote-approval-wait.js +-function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { +- const lead = (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) +- ); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-group-msg" }, +- (0, import_react6.createElement)( +- "span", +- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, +- bot.name, +- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null +- ), +- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) +- ); ++ ++// src/client/remote-approval-wait.js +var REMOTE_APPROVAL_LABEL = "Waiting for their approval"; +function remoteApprovalPhrase(remote, { tool, inputAt = 0 } = {}) { + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; } --function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: size, height: size, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- [[3, 7.5, 5], [6.5, 4.5, 11], [10, 2.5, 15], [13.5, 5.5, 9], [17, 8, 4]].map(([x, y, height]) => (0, import_react3.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) +-function GroupEvent({ prefix, room: room2, name, roster, actions }) { +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("span", null, prefix), +- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null - ); + +// src/client/activity-row-host.js @@ -8634,11 +9516,18 @@ diff --git a/client.js b/client.js + if (typeof Element !== "undefined" && root instanceof Element) return root.querySelector("[data-chat-running]"); + return root.querySelector(RUNNING_HOST); } --function ShareIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: "M10 12.5V3M6.5 6.5L10 3l3.5 3.5M4 11v4.5A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5V11" }) +-function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { +- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; +- const self = roster.byId[sessionId]; +- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); +- const other = team || !scope ? void 0 : findBot(scope); +- 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 }); +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-event" }, +- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) - ); + +// src/client/activity-row.js @@ -8652,8 +9541,8 @@ diff --git a/client.js b/client.js + const hours = Math.floor(minutes / 60); + return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; } --function CloseIcon() { -- return (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); +-function PartLine({ time }) { +- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -8666,14 +9555,11 @@ diff --git a/client.js b/client.js + }, [shown, at]); + return shown; } --var ChevronLeftIcon = () => (0, import_react3.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); --var ChevronRightIcon = () => (0, import_react3.createElement)("svg", { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react3.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); --var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) -- ); +-var scrollParent = (node) => { +- for (let at = node.parentElement; at; at = at.parentElement) { +- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; +- } +- return null; +function findHost(where) { + if (where !== TAIL_HOST) return findRunningHostIn(document); + const flow = document.querySelector("[data-conversation-scroll] [data-chat-flow]") ?? document.querySelector("[data-chat-flow]"); @@ -8688,78 +9574,25 @@ diff --git a/client.js b/client.js + for (const node of document.querySelectorAll(`[data-chat-flow-kind${kind ? `="${kind}"` : ""}][data-chat-turn]`)) top = Math.max(top, Number(node.dataset.chatTurn)); + return top; }; --var CLOUD = "M6.5 15.25H5.75a3.25 3.25 0 0 1-.55-6.45 4.75 4.75 0 0 1 9.2-1.1 3.75 3.75 0 0 1 .1 7.55H13.5"; --function CloudDownloadIcon({ done = false }) { -- return (0, import_react3.createElement)( -- "svg", -- { width: 16, height: 16, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react3.createElement)("path", { d: CLOUD }), -- done ? (0, import_react3.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react3.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react3.createElement)("path", { d: "M10 9.75v7" }), (0, import_react3.createElement)("path", { d: "M7.75 14.5L10 16.75l2.25-2.25" })) -- ); --} --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"]); --var ReplyIcon = lineIcon(["M8 5L4 9l4 4", "M4 9h7.5a4.5 4.5 0 0 1 4.5 4.5V15"]); --var DotsIcon = lineIcon(["M5 10h.01M10 10h.01M15 10h.01"]); --var CheckIcon = lineIcon(["M4.5 10.5l3.5 3.5 7.5-8"]); --var ArrowDownIcon = lineIcon(["M10 4v12", "M5 11l5 5 5-5"]); --var DownloadIcon = lineIcon(["M10 3.5v9", "M6.5 9.5L10 13l3.5-3.5", "M4 16h12"]); --var GaugeIcon = lineIcon(["M3.5 13.5a6.5 6.5 0 1 1 13 0", "M10 13.5l3-4", "M10 13.5h.01"]); --var PlugIcon = lineIcon(["M7 3v4M13 3v4", "M5 7h10v2.5a5 5 0 0 1-10 0z", "M10 14.5V17"]); --var CalendarIcon = lineIcon(["M5.5 5h9A1.5 1.5 0 0 1 16 6.5v8a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 4 14.5v-8A1.5 1.5 0 0 1 5.5 5z", "M4 8.75h12", "M7.25 3.5v3M12.75 3.5v3"]); --var ConnectorIcon = lineIcon(["M7 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M18 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z", "M7 10h6"]); --var SwitchIcon = lineIcon(["M4 7h9.5M10.5 3.5L14 7l-3.5 3.5", "M16 13H6.5M9.5 9.5L6 13l3.5 3.5"]); --var PaperclipIcon = lineIcon(["M15.5 9.5l-5.6 5.6a3.4 3.4 0 0 1-4.8-4.8l6.1-6.1a2.25 2.25 0 0 1 3.2 3.2l-6 6a1.1 1.1 0 0 1-1.6-1.6l5.5-5.5"]); --var ClockIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 6.5V10l2.5 1.5"]); --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() { -- return (0, import_react3.createElement)( -- "svg", -- { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( -- "g", -- { transform: "rotate(45 10 10)" }, -- (0, import_react3.createElement)("path", { d: "M8 2.5h4a.9.9 0 0 1 0 1.8h-.4v4.1c0 1.4 1.1 2.6 2.6 3.1l.7.3v1.5H5.1v-1.5l.7-.3c1.5-.5 2.6-1.7 2.6-3.1V4.3H8a.9.9 0 0 1 0-1.8z" }), -- (0, import_react3.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) -- ) -- ); --} --var UnreadIcon = lineIcon(["M10 3.5a4.5 4.5 0 0 0-4.5 4.5v3L4 13.5h12L14.5 11V8", "M8.3 16a1.8 1.8 0 0 0 3.4 0", "M15.5 3.2a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"]); --var PencilIcon = lineIcon(["M9 4H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-3", "M14.3 3.7a1.6 1.6 0 0 1 2.3 2.3L10.5 12l-3 .8.8-3z"]); --var InfoIcon = lineIcon(["M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z", "M10 9.25v4", "M10 6.6h.01"]); --var StarLineIcon = lineIcon(["M10 3l2.1 4.3 4.7.7-3.4 3.3.8 4.7L10 13.8 5.8 16l.8-4.7L3.2 8l4.7-.7z"]); --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-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"]); --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"]); --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"]); --var ChatsIcon = lineIcon(["M3.5 5.5A1.5 1.5 0 0 1 5 4h7a1.5 1.5 0 0 1 1.5 1.5V10A1.5 1.5 0 0 1 12 11.5H8L5 14v-2.5A1.5 1.5 0 0 1 3.5 10z", "M13.5 7.5H15A1.5 1.5 0 0 1 16.5 9v4.5A1.5 1.5 0 0 1 15 15v2l-2.5-2h-3A1.5 1.5 0 0 1 8 13.5V13"]); --var SETTINGS_ICONS = { general: GearIcon, bots: PeopleIcon, groups: ChatsIcon, usage: GaugeIcon }; -- --// src/client/questions.js --var import_react4 = require("react"); --var keyLetter = (index) => String.fromCharCode(65 + index); --var optionsOf = (question) => (question?.options ?? []).map((option) => typeof option === "string" ? { label: option } : option); --var isTypingTarget = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement || node instanceof HTMLElement && node.isContentEditable; --function useOptionKeys(count, onPick, ownRef) { -- const pick2 = (0, import_react4.useRef)(onPick); -- pick2.current = onPick; -- (0, import_react4.useEffect)(() => { -- 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; -- if (isTypingTarget(event.target) || isTypingTarget(document.activeElement)) return; -- if (document.querySelector('[role="dialog"], [aria-modal="true"]') !== null) return; -- const index = event.key.toUpperCase().charCodeAt(0) - 65; -- if (index >= 0 && index < count) { -- event.preventDefault(); -- pick2.current(index); -- } else if (index === count && ownRef.current) { -- event.preventDefault(); -- ownRef.current.focus(); -- } +-function EarlierParts({ sessionId, roster, actions }) { +- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); +- const top = (0, import_react6.useRef)(null); +- const busy = (0, import_react6.useRef)(false); +- const alive = (0, import_react6.useRef)(true); +- const restore = (0, import_react6.useRef)(null); +- const load = (cursor) => { +- if (busy.current) return; +- busy.current = true; +- setView((current) => ({ ...current, loading: true, failed: false })); +- actions.earlier(sessionId, cursor).then((page) => { +- busy.current = false; +- if (!alive.current) return; +- const scroller = top.current && scrollParent(top.current); +- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; +- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); +- }, () => { +- busy.current = false; +- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); +function useReplyAfterSend(remote, sending) { + const sendTurn = (0, import_react11.useRef)(-1); + const wasSending = (0, import_react11.useRef)(false); @@ -8781,116 +9614,147 @@ diff --git a/client.js b/client.js + const check = () => { + frame = 0; + if (lastTurnOnScreen("assistant-step") > sendTurn.current) setReplied(true); - }; -- document.addEventListener("keydown", onKey, true); -- return () => document.removeEventListener("keydown", onKey, true); -- }, [count, ownRef]); ++ }; + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(check); -+ }); + }); +- }; +- (0, import_react6.useEffect)(() => { +- alive.current = true; +- load(void 0); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + check(); -+ return () => { + return () => { +- alive.current = false; + cancelAnimationFrame(frame); + observer.disconnect(); -+ }; + }; +- }, [sessionId]); +- (0, import_react6.useLayoutEffect)(() => { +- const pending = restore.current; +- restore.current = null; +- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; +- }, [view.items]); +- (0, import_react6.useEffect)(() => { +- const node = top.current; +- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; +- const observer = new IntersectionObserver((entries2) => { +- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); +- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); +- observer.observe(node); +- return () => observer.disconnect(); +- }, [view.cursor, view.loading, view.failed]); +- const selfId = roster.byId[sessionId]?.id; +- const rows = []; +- for (const item of view.items) { +- const last = rows.at(-1); +- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); +- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); +- } +- return (0, import_react6.createElement)( +- "div", +- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, +- (0, import_react6.createElement)( +- "div", +- { ref: top, className: "bt-earlier-top" }, +- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null +- ), +- rows.map((row, index) => { +- const key = rows.length - index; +- switch (row.kind) { +- case "divider": +- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); +- case "user": +- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, +- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null +- )); +- case "answer": +- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); +- case "bot": +- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); +- case "event": { +- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); +- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; +- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); +- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { +- if (selfId && other.id) actions.openExchange(selfId, other.id); +- }, whole: true }); +- } +- default: +- return null; +- } +- }), +- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) +- ); + }, [remote, sending]); + return replied; } --function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { -- const [own, setOwn] = (0, import_react4.useState)(""); -- const [picked, setPicked] = (0, import_react4.useState)([]); -- const ownRef = (0, import_react4.useRef)(null); -- const titleId = (0, import_react4.useId)(); -- const options = optionsOf(question); -- const multi = question.multiSelect === true; -- const custom = own.trim(); -- const canSubmit = multi ? picked.length > 0 || custom !== "" : custom !== ""; -- const choose = (label) => { -- if (multi) { -- setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]); -- return; -- } -- onAnswer({ selected: [label] }); -- }; -- const submit = () => { -- if (!canSubmit) return; -- onAnswer({ selected: multi ? picked : [], ...custom ? { custom } : {} }); -- }; -- useOptionKeys(options.length, (index) => choose(options[index].label), ownRef); -- const ownLetter = options.length > 0 ? keyLetter(options.length) : void 0; -- const field = ownAnswer ? (0, import_react4.createElement)("textarea", { -- ref: ownRef, -- rows: 1, -- value: own, -- autoComplete: "off", -- spellCheck: false, -- className: "bt-q-input", -- "aria-label": t("Your own answer"), -- placeholder: t("Type your own answer"), -- "aria-keyshortcuts": ownLetter?.toLowerCase(), -- onChange: (event) => setOwn(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.shiftKey || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- submit(); +-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +- const roster = useRoster((value) => value); +- const data = node.data; +- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; +- const selfId = roster.byId[sessionId]?.id ?? sessionId; +- const deferred = source.kind === "bot" && source.deferred === true; +- const relayed = isRelayContext(data); +- const turn = node.location?.turn?.turn ?? data.turn; +- (0, import_react6.useLayoutEffect)(() => { +- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); +- }, [relayed, sessionId, turn, source.senderSessionId]); +- const order = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; +- }); +- const peers = useTurnClock((state) => { +- if (!relayed) return ""; +- const turns = state[sessionId] ?? {}; +- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const hiddenTurns = roster.exchangeTurns[sessionId]; +- const unreadOpening = useTurnClock((state) => { +- const first = openingTurn(state[sessionId], order, hiddenTurns); +- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; +- }); +- const body = triggerBody(); +- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; +- function triggerBody() { +- if (source.kind === "bot") { +- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); +- if (source.role === "kickoff" || source.role === "room") return null; +- if (source.role === "secret" || deferred) return null; +- if (source.role === "post") { +- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; +- const room2 = roster.roomsById[sessionId]; +- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); +- } +- if (source.role === "report") { +- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); +- } +- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); +- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); +- 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)])), +- turn, +- hiddenTurns +- ) : null; +- if (run === "folded") return null; +- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; +- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); +- const bots = [sender]; +- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { +- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react11.useState)(null); + (0, import_react11.useEffect)(() => { + if (!active) { + setHost(null); + return void 0; - } -- }) : null; -- const rows = options.map((option, optionIndex) => { -- const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( -- "button", -- { -- key: option.label, -- type: "button", -- className: "bt-q-opt", -- "aria-pressed": multi ? selected : void 0, -- "aria-keyshortcuts": keyLetter(optionIndex).toLowerCase(), -- onClick: () => choose(option.label) -- }, -- (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), -- (0, import_react4.createElement)( -- "span", -- { className: "bt-q-body" }, -- (0, import_react4.createElement)("span", { className: "bt-q-label" }, option.label), -- option.description ? (0, import_react4.createElement)("span", { className: "bt-q-desc" }, option.description) : null -- ), -- selected && !raised ? (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) : null -- ); -- }); -- if (raised && field) { -- rows.push((0, import_react4.createElement)( -- "label", -- { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, -- ownLetter ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, field) -- )); -- } -- let foot = null; -- if (raised && multi) { -- foot = (0, import_react4.createElement)( -- "div", -- { className: "bt-q-foot" }, -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), -- (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit", disabled: !canSubmit }, t("Submit")) -- ); -- } else if ((raised || multi) && canSubmit) { -- foot = (0, import_react4.createElement)("div", { className: "bt-q-foot" }, (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); -- } -- return (0, import_react4.createElement)( -- "form", -- { -- className: raised ? "bt-q bt-q-raised" : "bt-q", -- "aria-labelledby": titleId, -- onSubmit: (event) => { -- event.preventDefault(); -- submit(); ++ } + let frame = 0; + const find = () => { + frame = 0; @@ -8929,13 +9793,18 @@ diff --git a/client.js b/client.js + observer.disconnect(); + column = next; + if (column) observer.observe(column, { childList: true, subtree: true }); -+ } + } +- return (0, import_react6.createElement)(EventLine, { +- 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), +- whole: true + if (!column) return; + const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; + let last = -1; + items.forEach((item, index) => { + if (seen.has(item)) last = index; -+ }); + }); + const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); + for (const item of fresh) seen.add(item); + if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; @@ -8944,29 +9813,7 @@ diff --git a/client.js b/client.js + const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); + item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; + item.addEventListener("animationend", settle); - } -- }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-text" }, -- (0, import_react4.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), -- question.detail ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, -- raised && multi ? (0, import_react4.createElement)("div", { className: "bt-q-detail" }, t("Choose all that apply")) : null -- ), -- (0, import_react4.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react4.createElement)(CloseIcon)) -- ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( -- "div", -- { className: "bt-q-custom" }, -- (0, import_react4.createElement)("label", { className: "bt-q-field" }, field), -- !multi && canSubmit ? (0, import_react4.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null -- ) : null, -- foot -- ); ++ } + }; + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(scan); @@ -8977,24 +9824,7 @@ diff --git a/client.js b/client.js + observer.disconnect(); + }; + }, []); - } --function SettledQuestion({ question, answer }) { -- const options = optionsOf(question); -- const prompt = question?.question ?? ""; -- const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; -- if (chosen.length === 0) { -- return (0, import_react4.createElement)( -- "div", -- { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( -- "div", -- { className: "bt-q-head" }, -- (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt)), -- (0, import_react4.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) -- ) -- ); -- } -- return (0, import_react4.createElement)( ++} +function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { + const roster = useRoster((value) => value); + const running = useSessionStatus((map) => map.get(sessionId)?.running === true); @@ -9033,7 +9863,10 @@ diff --git a/client.js b/client.js + if (wait <= 0) { + make(); + return void 0; -+ } + } +- if (source.kind === "schedule") { +- const prompts = reminderPrompts(contentText(data.content)); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); + const timer = setTimeout(make, wait); + return () => clearTimeout(timer); + }, [wantKey, shown]); @@ -9046,7 +9879,10 @@ diff --git a/client.js b/client.js + if (visible) { + setPhase("in"); + return void 0; -+ } + } +- if (source.kind === "user-question-reply") return null; +- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); +- } + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -9090,20 +9926,7 @@ diff --git a/client.js b/client.js + if (!host || phase === null || !shown) return null; + const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; + return (0, import_react_dom2.createPortal)((0, import_react11.createElement)( - "div", -- { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, -- prompt ? (0, import_react4.createElement)("div", { className: "bt-q-head" }, (0, import_react4.createElement)("div", { className: "bt-q-text" }, (0, import_react4.createElement)("p", { className: "bt-q-title" }, prompt))) : null, -- (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group", "aria-label": t("Your answer") }, chosen.map((label, index) => { -- const optionIndex = options.findIndex((option) => option.label === label); -- return (0, import_react4.createElement)( -- "div", -- { key: `${label}-${index}`, className: "bt-q-opt" }, -- optionIndex >= 0 ? (0, import_react4.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, -- (0, import_react4.createElement)("span", { className: "bt-q-body" }, (0, import_react4.createElement)("span", { className: "bt-q-label" }, label)), -- (0, import_react4.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react4.createElement)(CheckIcon)) -- ); -- })) -- ); ++ "div", + { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "data-carry": carry && phase !== "out" ? "" : void 0, "data-remote": remote ? "" : void 0, "data-flybot": shown.key === "remote-approval" ? "remote-approval" : void 0, "aria-hidden": true }, + bot ? (0, import_react11.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react11.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react11.createElement)( @@ -9118,23 +9941,8 @@ diff --git a/client.js b/client.js + ) + ), host); } --function QuestionCard({ matched }) { -- const pending = matched; -- const questions = pending.questions ?? []; -- const [index, setIndex] = (0, import_react4.useState)(0); -- const [answers, setAnswers] = (0, import_react4.useState)([]); -- const question = questions[index]; -- if (question === void 0) return null; -- const onAnswer = (answer) => { -- const next = [...answers, { id: question.id, ...answer }]; -- if (index + 1 < questions.length) { -- setAnswers(next); -- setIndex(index + 1); -- return; -- } -- pending.answer({ answers: next }); -- }; -- return (0, import_react4.createElement)(QuestionView, { key: index, question, onAnswer, onDismiss: () => pending.dismiss(), ownAnswer: true, raised: true }); +-function NewDivider() { +- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); + +// src/client/dsh-offer.js +var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); @@ -9145,22 +9953,23 @@ diff --git a/client.js b/client.js + if (dsh.kind === "npm") return true; + return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); } --function PendingQuestion({ sessionId, question, actions, inline }) { -- const [answered, setAnswered] = (0, import_react4.useState)(null); -- if (answered === question.id) return null; -- const onAnswer = (answer) => { -- setAnswered(question.id); -- void actions.answerQuestion(sessionId, question.id, { selected: answer.selected ?? [], custom: answer.custom ?? "" }).catch(() => setAnswered(null)); -- }; -- const onDismiss = () => { -- setAnswered(question.id); -- void actions.dismissQuestion(sessionId); -- }; -- return (0, import_react4.createElement)( -- "div", -- { className: inline ? "bt-q-inline" : "bt-q-dock" }, -- (0, import_react4.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) -- ); +-function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { +- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; +- (0, import_react6.useEffect)(() => { +- if (!open) turnProcess.setOpen(true); +- }, [open, turnProcess]); +- const turn = node?.location?.turn?.turn ?? node?.data?.turn; +- const start = node?.location?.turn?.start?.time; +- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "first", start); +- }, [sessionId, turn, start]); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- 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; +- }); +- return fresh ? (0, import_react6.createElement)(NewDivider) : null; + +// src/client/overlay-hooks.js +var import_react12 = require("react"); @@ -9180,13 +9989,9 @@ diff --git a/client.js b/client.js + }; + }, []); } --function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -- const question = useRoster((value) => value.questions?.[sessionId]); -- 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; -- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); -+function useDialogFocus(ref) { +-function Nothing() { +- return null; ++function useDialogFocus(ref) { + (0, import_react12.useEffect)(() => { + const before = document.activeElement; + ref.current?.focus?.({ preventScroll: true }); @@ -9195,18 +10000,16 @@ diff --git a/client.js b/client.js + }; + }, []); } --function AnsweredCell({ node }) { -- const data = node.data ?? {}; -- const questions = Array.isArray(data.questions) ? data.questions : []; -- if (questions.length === 0) { -- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null; -- } -- const answers = Array.isArray(data.answers) ? data.answers : []; -- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, { -- key: question.id, -- question, -- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -- }))); +-var SEPARATOR_GAP_MS = 30 * 60 * 1e3; +-function clockLabel(time, now = Date.now()) { +- const date = new Date(time); +- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); +- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); +- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); +- if (days === 0) return t("Today {time}", { time: clock4 }); +- if (days === 1) return t("Yesterday {time}", { time: clock4 }); +- const sameYear = date.getFullYear() === new Date(now).getFullYear(); +- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react12.useState)(280); + (0, import_react12.useLayoutEffect)(() => { @@ -9215,35 +10018,14 @@ diff --git a/client.js b/client.js + }, []); + return paneLeft; } - --// src/client/secret-card.js --var import_react5 = require("react"); --var MIN_VALUE = 4; --var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 15, height: 15, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), -- (0, import_react5.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) --); --var LockIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), -- (0, import_react5.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) --); --var WarnIcon = () => (0, import_react5.createElement)( -- "svg", -- { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, -- (0, import_react5.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), -- (0, import_react5.createElement)("path", { d: "M10 8.5v3.5M10 14.2h.01" }) --); --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); --var scopeLabel = (scope, roster) => { -- if (scope === "all") return t("All Bots"); -- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean); -- return names.length === 0 ? t("No Bot") : names.join(t(" and ")); +-function nextTurnStart(turns, turn) { +- let next; +- for (const key in turns) { +- const candidate = Number(key); +- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +- } +- return next === void 0 ? void 0 : turns[next].first; ++ +// src/client/member-status.js +var import_react13 = require("react"); +var NO_WORK = { bots: {}, rooms: {}, approvals: [] }; @@ -9251,31 +10033,7 @@ diff --git a/client.js b/client.js +var stepIn = (work, botId, roomId, tool) => { + const own = work.bots?.[botId]; + return own?.roomId === roomId && own.tool === tool ? own.step : void 0; - }; --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 }) { -- return (0, import_react5.createElement)("input", { -- className: "bt-secret-input", -- type: MASK_BY_CSS ? "text" : "password", -- "data-masked": MASK_BY_CSS ? "" : void 0, -- value, -- autoFocus, -- autoComplete: MASK_BY_CSS ? "off" : "new-password", -- autoCorrect: "off", -- autoCapitalize: "off", -- spellCheck: false, -- "data-1p-ignore": "", -- "data-lpignore": "true", -- "data-bwignore": "", -- "data-form-type": "other", -- "aria-label": label, -- placeholder: t("Paste or type the key"), -- onChange: (event) => onChange(event.target.value), -- onKeyDown: (event) => { -- if (event.key !== "Enter" || event.nativeEvent.isComposing) return; -- event.preventDefault(); -- onEnter?.(); -- } ++}; +var workIn = (room2, work, id) => { + const local = (work.rooms?.[room2.id] ?? []).find((entry) => entry.botId === id); + if (local) return { ...local, roomId: room2.id, step: stepIn(work, id, room2.id, local.tool) }; @@ -9289,37 +10047,114 @@ diff --git a/client.js b/client.js + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; + }); -+} + } +-function openingTurn(turns, firstTurns, hiddenTurns) { +- if (!turns || !firstTurns) return null; +- const start = nextTurnStart(turns, -Infinity); +- if (start === void 0) return null; +- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); +- const listed = firstTurns.split(",").map(Number); +- const earlier = listed.filter((entry) => entry < turn); +- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; +function busyMembers(room2, work, roster) { + const busy = memberWork(room2, work).filter((entry) => entry.state !== "idle" && roster.byId[entry.id]); + return [...busy.filter((entry) => entry.state === "ask"), ...busy.filter((entry) => entry.state === "work")].map((entry) => ({ ...entry, bot: roster.byId[entry.id] })); -+} + } +-var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +-function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { +- const turn = location2.turn; +- const time = location2.end.time; +- (0, import_react6.useEffect)(() => { +- actions.noteTurnTime(sessionId, turn, "end", time); +- }, [sessionId, turn, time]); +- const label = useTurnClock((state) => { +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const fresh = useTurnClock((state) => { +- if (typeof visit?.seen !== "number" || time > visit.seen) return false; +- const start = nextTurnStart(state[sessionId] ?? {}, turn); +- return start !== void 0 && start > visit.seen && start <= visit.entered; +- }); +- if (!label && !fresh) return null; +- return (0, import_react6.createElement)( +- import_react6.Fragment, +- null, +- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, +- fresh ? (0, import_react6.createElement)(NewDivider) : null +- ); +function splitBusy(busy) { + const here = busy.filter((entry) => entry.state === "work" && entry.here); + const rows = [...here.filter((entry) => entry.doing.speaking === true), ...here.filter((entry) => entry.doing.speaking !== true)].slice(0, ROWS_MAX); + return { rows, rest: busy.filter((entry) => !rows.includes(entry)) }; -+} + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +function botGroupWork(botId, work, roster) { + return Object.entries(work.rooms ?? {}).flatMap(([roomId, entries2]) => { + const entry = entries2.find((item) => item.botId === botId); + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; - }); ++ }); } --function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, -- (0, import_react5.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), -- options.map(([id, text]) => (0, import_react5.createElement)("button", { -- key: id, -- type: "button", -- role: "radio", -- "aria-checked": value === id, -- className: "bt-secret-seg", -- onClick: () => onChange(id) -- }, text)) +-var PHRASES = { +- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], +- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] +-}; +-var TOOL_PHRASES = [ +- [/web_?search|search_?web/i, "Searching online"], +- [/fetch|browse|browser|web/i, "Reading a page"], +- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], +- [/^(read|view|cat)\b|read_/i, "Opening a file"], +- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], +- [/write|edit|patch|apply|replace/i, "Writing changes"], +- [/image|photo|draw/i, "Working on an image"] +-]; +-var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; +-function toolActivity(name, arg, findBot, self) { +- switch (name) { +- case "ask_user": +- return null; +- case "request_secret": +- return null; +- case "list_secrets": +- return { label: t("Checking the keys"), state: "searching" }; +- case "ask_user_question": +- return { label: t("Waiting for your answer"), state: "alert" }; +- case "group_relay": +- return { relay: true, state: "orbit" }; +- case "message_bot": { +- const target = findBot(arg("to")); +- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; +- } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; +- } +- case "create_bot": +- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; +- } +- case "list_bots": +- return { label: t("Checking the team"), state: "searching" }; +- case "read_group_chat": +- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; +- case "read_own_chat": +- return { label: t("Looking back through the chat"), state: "searching" }; +- case "remember": +- return { label: t("Saving to memory"), state: "working" }; +- case "forget": +- return { label: t("Updating memory"), state: "working" }; +- case "recall": +- return { label: t("Checking memory"), state: "searching" }; +- default: +- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; +- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; +- return { label: phrase ? t(phrase) : null, state: toolState(name) }; +- } +var moreRooms = (count) => count === 1 ? t("1 more room") : t("{count} more rooms", { count }); +function summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); @@ -9329,16 +10164,24 @@ diff --git a/client.js b/client.js + const first = workers[0]; + const doing = first ? `${first.bot.name} · ${workText(first.doing.tool, first.doing.step)}${workers.length > 1 ? ` +${workers.length - 1}` : ""}` : ""; + return { waiting, doing }; -+} + } +-function workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +function Face({ entry, size }) { + return (0, import_react13.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react13.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) - ); ++ ); } --function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( +-function useLiveNote(actions, sessionId, id, entry) { +- const key = entry ? JSON.stringify(entry) : ""; +- (0, import_react6.useEffect)(() => { +- if (!key) return void 0; +- actions.noteLive(sessionId, id, JSON.parse(key)); +- return () => actions.noteLive(sessionId, id, null); +- }, [sessionId, id, key]); +var elapsedSince = (now, at) => typeof at === "number" && now - at >= 6e4 ? elapsedLabel(now - at) : ""; +function WorkRow({ bot, tool, step, label, at, now, onOpen }) { + const text2 = `${label} · ${workText(tool, step)}`; @@ -9350,7 +10193,42 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react13.createElement)(Shimmer, null, text2)), + elapsed ? (0, import_react13.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null + ); -+} + } +-function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { +- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); +- (0, import_react6.useEffect)(() => { +- if (done) return void 0; +- let stop = false; +- let timer; +- let seen = ""; +- const tick = async () => { +- const result = await actions.roomProgress(sessionId); +- if (stop) return; +- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; +- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; +- if (entry && key !== seen) { +- seen = key; +- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +- } +- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); +- }; +- void tick(); +- return () => { +- stop = true; +- clearTimeout(timer); +- }; +- }, [done, sessionId, callId]); +- let replies = live.replies; +- if (done) { +- try { +- replies = JSON.parse(contentText(content)).replies ?? []; +- } catch { +- replies = []; +- } +- } +- const pending = done || !live.speaking ? [] : [live.speaking]; +- const speakerTool = useWork?.((work) => work.bots?.[live.speaking]?.tool) ?? void 0; +- if (replies.length === 0 && pending.length === 0) return null; +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -9371,8 +10249,9 @@ diff --git a/client.js b/client.js +function MemberDock({ sessionId, useRoster, useWork, actions }) { + const roster = useRoster((value) => value); + const work = useWork((value) => value); -+ const room2 = roster.roomsById[sessionId]; -+ const bot = room2 ? void 0 : roster.byId[sessionId]; + const room2 = roster.roomsById[sessionId]; +- return (0, import_react6.createElement)( ++ const bot = room2 ? void 0 : roster.byId[sessionId]; + const ownChat = bot !== void 0 && work.bots?.[bot.id] !== void 0 && work.bots[bot.id].roomId === void 0; + const busy = room2 ? busyMembers(room2, work, roster) : []; + const groups = bot !== void 0 && !ownChat ? botGroupWork(bot.id, work, roster) : []; @@ -9395,9 +10274,66 @@ diff --git a/client.js b/client.js + const more = groups.length - rows.length; + return (0, import_react13.createElement)( "div", -- { className: "bt-q-head bt-secret-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 28 }), (0, import_react5.createElement)("span", { className: "bt-secret-lock" }, (0, import_react5.createElement)(LockIcon))), -- (0, import_react5.createElement)( +- { className: "bt-group-replies" }, +- replies.map((reply, index) => { +- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; +- if (reply.kind === "user") { +- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( +- "div", +- { className: "bt-ububble" }, +- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) +- )); +- } +- if (reply.kind === "failed") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, +- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) +- ); +- } +- if (reply.kind === "lookup") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) +- ); +- } +- if (reply.kind === "missed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); +- } +- if (reply.kind === "resumed") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); +- } +- if (reply.kind === "cut") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); +- } +- if (reply.kind === "approval" || reply.kind === "working") { +- return (0, import_react6.createElement)( +- "div", +- { key: `${reply.botId}-${index}`, className: "bt-event" }, +- (0, import_react6.createElement)( +- "button", +- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, +- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), +- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) +- ) +- ); +- } +- if (reply.kind === "stopped") { +- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); +- } +- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); +- }), +- pending.map((id) => { +- const bot = roster.byId[id]; +- if (!bot) return null; +- return (0, import_react6.createElement)( +- "div", +- { key: `pending-${id}`, className: "bt-activity" }, +- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), +- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) +- ); +- }) + { ref, className: "bt-mw-dock-stack" }, + rows.map((item) => (0, import_react13.createElement)(WorkRow, { + key: item.room.id, @@ -9410,26 +10346,26 @@ diff --git a/client.js b/client.js + onOpen: () => actions.openSession(item.room.id) + })), + more > 0 ? (0, import_react13.createElement)( - "div", -- { className: "bt-q-text" }, -- (0, import_react5.createElement)("p", { className: "bt-q-title" }, title), -- (0, import_react5.createElement)("div", { className: "bt-q-detail" }, (0, import_react5.createElement)("code", { className: "bt-secret-name" }, name), detail ? ` · ${detail}` : "") -- ), -- onClose ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react5.createElement)(CloseIcon)) : null ++ "div", + { className: "bt-mw-row bt-mw-line bt-mw-static" }, + (0, import_react13.createElement)("span", { className: "bt-mw-row-text" }, moreRooms(more)) + ) : null ); } --function SecretCard({ bot, request, roster, actions }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [scope, setScope] = (0, import_react5.useState)("all"); -- const [replacing, setReplacing] = (0, import_react5.useState)(false); -- const [busy, setBusy] = (0, import_react5.useState)(false); -- const [error, setError] = (0, import_react5.useState)(""); -- const noteId = (0, import_react5.useId)(); -- const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); -- const run = (promise) => { +-var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); +-var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); +-function callTree(root) { +- const calls = []; +- const seen = /* @__PURE__ */ new Set(); +- const visit = (call2) => { +- const id = call2.callId ?? call2; +- if (seen.has(id)) return; +- seen.add(id); +- calls.push(call2); +- for (const child of call2.subCalls ?? []) visit(child); +- }; +- visit(root); +- return calls; +function useFlowColumn(ref, active, key) { + (0, import_react13.useLayoutEffect)(() => { + const node = ref.current; @@ -9446,7 +10382,15 @@ diff --git a/client.js b/client.js + for (const target of [flow, flow.parentElement, node.parentElement]) if (target) observer.observe(target); + return () => observer.disconnect(); + }, [active, key]); -+} + } +-function useShownCards(actions, sessionId, callIds) { +- const key = JSON.stringify(callIds); +- (0, import_react6.useLayoutEffect)(() => { +- const ids = JSON.parse(key); +- if (ids.length === 0 || !actions.noteCards) return void 0; +- actions.noteCards(sessionId, ids, 1); +- return () => actions.noteCards(sessionId, ids, -1); +- }, [sessionId, key]); +function useNow(active) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -9455,7 +10399,82 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [active]); + return now; -+} + } +-function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { +- const tool = toolName(call2); +- const done = call2.kind === "tool-result"; +- const { text: arg, value: argValue } = toolArgs(call2); +- const self = roster.byId[sessionId]; +- switch (tool) { +- case "message_bot": { +- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "stop_bot": { +- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; +- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); +- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); +- } +- case "create_bot": { +- const created = findBot(arg("name")); +- if (!done || created === void 0) return null; +- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); +- } +- case "update_bot": { +- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); +- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; +- } +- case "group_relay": +- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); +- case "create_group": +- case "update_group": +- case "delete_group": +- case "post_to_group": { +- const verbs = { +- create_group: ["Created group", "Couldn't create the group"], +- update_group: ["Updated group", "Couldn't update the group"], +- delete_group: ["Deleted group", "Couldn't delete the group"], +- post_to_group: ["Posted in", "Couldn't post in the group"] +- }[tool]; +- if (!done) return null; +- const result = contentText(call2.content); +- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); +- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); +- const name = named[2]; +- const room2 = roster.rooms.find((entry) => entry.name === name); +- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); +- } +- case "ask_user": { +- const question = roster.questions?.[sessionId]; +- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); +- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); +- } +- case "request_secret": +- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); +- case "ask_user_question": { +- if (!done) return null; +- let parsed = null; +- try { +- parsed = JSON.parse(contentText(call2.content)); +- } catch { +- parsed = null; +- } +- const answer = parsed?.answers?.[0]; +- const question = argValue("questions")?.[0]; +- if (!answer) return null; +- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); +- } +- case "read_group_chat": { +- if (!done) return null; +- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); +- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; +- } +- case "remember": +- case "forget": +- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; +- default: +- return null; +var since = (now, at) => typeof at === "number" && now - at >= 6e4 ? ` · ${elapsedLabel(now - at)}` : ""; +var alwaysChoices = (approval, bot) => bot?.remote ? [] : [ + ["always-bot", "Always allow for this Bot"], @@ -9464,75 +10483,29 @@ diff --git a/client.js b/client.js +function ApprovalCard({ approval, bot, actions }) { + const [busy, setBusy] = (0, import_react13.useState)(false); + const answer = (choice) => { - setBusy(true); -- setError(""); -- return Promise.resolve(promise).then(() => setValue(""), (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); -- }; -- const cancel = () => run(actions.secretCancel(request.id)); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (long && !busy) void run(actions.secretSet({ requestId: request.id, value, scope })); -- }; -- const allow = () => { -- if (!busy) void run(actions.secretAllow(request.id)); ++ setBusy(true); + void Promise.resolve(actions.answerApproval(approval.id, choice)).catch((error) => actions.reportError(error, "approval")).finally(() => setBusy(false)); - }; -- const filling = request.kind === "fill" || replacing; -- const warn = insecure() ? (0, import_react5.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react5.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; -- return (0, import_react5.createElement)( ++ }; + return (0, import_react13.createElement)( - "div", -- { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { -- bot, -- title: request.kind === "allow" ? t("Let {bot} use {name}?", { bot: bot.name, name: request.name }) : t("{name} needs a key", { name: bot.name }), -- detail: request.purpose, -- name: request.name, -- onClose: busy ? void 0 : cancel -- }), -- request.kind === "allow" ? (0, import_react5.createElement)("div", { className: "bt-secret-saved" }, (0, import_react5.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, -- filling ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-body" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), -- request.kind === "fill" ? (0, import_react5.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null -- ) : null, -- (0, import_react5.createElement)( ++ "div", + { className: "bt-mw-card", role: "group", "aria-label": t("{name} is waiting for your approval", { name: bot.name }) }, + (0, import_react13.createElement)("div", { className: "bt-mw-tool" }, approval.tool), + approval.detail?.text ? (0, import_react13.createElement)("div", { className: "bt-mw-text" }, approval.detail.text) : null, + approval.reason ? (0, import_react13.createElement)("div", { className: "bt-mw-reason" }, approval.reason) : null, + approval.detail?.code ? (0, import_react13.createElement)("code", { className: "bt-mw-code", title: approval.detail.code }, approval.detail.code) : null, + approval.answerable ? (0, import_react13.createElement)( - "div", -- { className: "bt-secret-note", id: noteId }, -- t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), -- (0, import_react5.createElement)("code", null, `$DSH_SECRET_${request.name}`), -- ".", -- filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" -- ), -- warn, -- error ? (0, import_react5.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, -- (0, import_react5.createElement)( ++ "div", + { className: "bt-mw-actions" }, + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => answer("reject") }, t("Reject")), + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: () => answer("allow") }, t("Allow once")), + ...alwaysChoices(approval, bot).map(([choice, label]) => (0, import_react13.createElement)("button", { key: choice, type: "button", className: "bt-q-btn", "data-choice": choice, disabled: busy, onClick: () => answer(choice) }, t(label))) + ) : (0, import_react13.createElement)( - "div", -- { className: "bt-q-foot" }, -- request.kind === "allow" && !replacing ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), -- filling ? (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react5.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) ++ "div", + { className: "bt-mw-actions" }, + (0, import_react13.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) - ) - ); - } --function SettledSecret({ bot, request, roster }) { -- const canceled = request.status === "canceled"; -- const text = canceled ? t("{bot} asked for {name}", { bot: bot.name, name: request.name }) : request.status === "allowed" ? t("Allowed {bot} to use {name}", { bot: bot.name, name: request.name }) : t("Saved {name}", { name: request.name }); -- return (0, import_react5.createElement)( ++ ) ++ ); ++} +function MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react13.useState)(false); + if (entry.state === "ask") { @@ -9542,52 +10515,44 @@ diff --git a/client.js b/client.js + (0, import_react13.createElement)("div", { className: "bt-mw-state bt-mw-ask" }, `${t("Waiting for your approval")}${since(now, entry.approvals[0].at)}`), + entry.approvals.map((approval) => (0, import_react13.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) + ); -+ } + } + if (entry.state !== "work") return null; + const stop = () => { + setStopping(true); + void Promise.resolve(actions.stopMemberWork(room2.id, bot.id)).catch((error) => actions.reportError(error, "stop")).finally(() => setStopping(false)); + }; + return (0, import_react13.createElement)( - "div", -- { className: "bt-q bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text, "data-status": request.status }, -- (0, import_react5.createElement)( ++ "div", + { className: "bt-mw-member" }, + (0, import_react13.createElement)( - "div", -- { className: "bt-q-head" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-who" }, (0, import_react5.createElement)(BotMark, { bot, size: 22 })), -- (0, import_react5.createElement)("div", { className: "bt-q-text" }, (0, import_react5.createElement)( -- "p", -- { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) -- )), -- canceled ? (0, import_react5.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react5.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react5.createElement)(CheckIcon)) ++ "div", + { className: "bt-mw-state bt-mw-work" }, + (0, import_react13.createElement)("span", null, (0, import_react13.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react13.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null - ) - ); ++ ) ++ ); } --function RequestView({ bot, request, roster, actions }) { -- return request.status === "pending" ? (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react5.createElement)(SettledSecret, { bot, request, roster }); --} --function SecretToolCell({ sessionId, callId, name, done, content, roster, actions }) { -- const bot = roster.byId[sessionId]; -- if (!bot) return null; -- const request = (roster.secretRequests?.[bot.id] ?? []).find((entry) => entry.callId === callId && (entry.sessionId ?? sessionId) === sessionId); -- if (request) return (0, import_react5.createElement)("div", { className: "bt-q-inline" }, (0, import_react5.createElement)(RequestView, { bot, request, roster, actions })); -- if (done && /already saved/.test(contentText(content))) { -- return (0, import_react5.createElement)("div", { className: "bt-event" }, (0, import_react5.createElement)("span", { className: "bt-secret-event" }, (0, import_react5.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); -- } -- return null; +-function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +- const roster = useRoster((value) => value); +- const root = node.data.root; +- const turn = node.location?.turn?.turn ?? root.turn; +- 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); +- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); +- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); +- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; +- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); +- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); +- if (drawn.length === 0) return null; +- const env = { sessionId, roster, actions, useUi, useWork, findBot }; +- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); +function useMemberWork(room2, useWork) { + const work = useWork?.((value) => value) ?? NO_WORK; + const entries2 = room2 ? memberWork(room2, work) : []; + const now = useNow(entries2.some((entry) => entry.state !== "idle")); + return { entries: entries2, now }; -+} + } +var MEMBER_CSS = ` +.bt-mw-dock-stack{box-sizing:border-box;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:var(--dsh-chat-content-width,min(960px,100%));margin:0 auto 6px;pointer-events:none} +.bt-mw-row{pointer-events:auto;display:flex;align-items:center;gap:10px;min-width:0;max-width:calc(100% + 6px);height:32px;margin-inline-start:-6px;padding:0 12px 0 6px;border:0;border-radius:16px;background:none;color:var(--bt-ink-2);font:inherit;font-size:14px;cursor:pointer;animation:bt-mw-in .32s cubic-bezier(.23,1,.32,1) backwards;transition:background-color .16s ease-out} @@ -9622,7 +10587,17 @@ diff --git a/client.js b/client.js +@keyframes bt-mw-in{from{opacity:0;transform:translateY(4px)}} +@media (prefers-reduced-motion:reduce){.bt-mw-row{animation:none}} +`; -+ + +-// src/client/activity-row.js +-var import_react7 = require("react"); +-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))]); +-function elapsedLabel(ms) { +- const minutes = Math.floor(ms / 6e4); +- if (minutes < 60) return `${Math.max(1, minutes)}m`; +- const hours = Math.floor(minutes / 60); +- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; +// src/shared/task-board.js +var COLUMNS = ["not-started", "in-progress", "done"]; +var PRIORITIES = ["urgent", "high", "normal", "low"]; @@ -9674,7 +10649,22 @@ diff --git a/client.js b/client.js + const value = task?.progress; + if (typeof value !== "number" || !Number.isFinite(value)) return columnOf(task) === "done" && task?.status !== "cancelled" && task?.status !== "failed" ? 100 : 0; + return Math.max(0, Math.min(100, Math.round(value))); -+} + } +-function useRunningHost(active) { +- const [host, setHost] = (0, import_react7.useState)(null); +- (0, import_react7.useEffect)(() => { +- if (!active) { +- setHost(null); +- return void 0; +- } +- let frame = 0; +- const find = () => { +- frame = 0; +- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); +- setHost((prev) => prev === node ? prev : node); +- }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(find); + +// src/client/inbox.js +var import_react14 = require("react"); @@ -9729,13 +10719,46 @@ diff --git a/client.js b/client.js + }).catch(() => { + }).finally(() => { + if (!stopped) timer = setTimeout(load, POLL_MS3); -+ }); + }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); + load(); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); +- }; +- }, [active]); +- return host; +-} +-function useRowEntrances(replacing) { +- const check = (0, import_react7.useRef)(replacing); +- check.current = replacing; +- (0, import_react7.useEffect)(() => { +- const seen = /* @__PURE__ */ new WeakSet(); +- const started = Date.now(); +- let column = null; +- let frame = 0; +- const settle = (event) => { +- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; +- delete event.currentTarget.dataset.btEnter; +- event.currentTarget.removeEventListener("animationend", settle); + stopped = true; + clearTimeout(timer); + taskListeners.delete(setTasks); -+ }; + }; +- const scan = () => { +- frame = 0; +- const next = document.querySelector("[data-chat-flow]"); +- if (next !== column) { +- observer.disconnect(); +- column = next; +- if (column) observer.observe(column, { childList: true, subtree: true }); +- } +- if (!column) return; +- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; +- let last = -1; +- items.forEach((item, index) => { +- if (seen.has(item)) last = index; + }, []); + return tasks; +} @@ -9753,7 +10776,15 @@ diff --git a/client.js b/client.js + workflowListeners.add(setWorkflows); + if (workflowPoll === null && typeof actions?.workflowState === "function") { + const load = () => Promise.resolve(actions.workflowState()).then(publishWorkflows, () => { -+ }); + }); +- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); +- for (const item of fresh) seen.add(item); +- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; +- for (const item of fresh) { +- if (items.indexOf(item) < last) continue; +- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); +- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; +- item.addEventListener("animationend", settle); + load(); + workflowPoll = setInterval(load, POLL_MS3); + } @@ -9762,8 +10793,10 @@ diff --git a/client.js b/client.js + if (workflowListeners.size === 0 && workflowPoll !== null) { + clearInterval(workflowPoll); + workflowPoll = null; -+ } -+ }; + } + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); + }, []); + return workflows; +} @@ -9836,7 +10869,11 @@ diff --git a/client.js b/client.js + chat: approval.roomId ?? bot.id, + approval, + ...people(roster, bot.id) -+ }); + }); +- scan(); +- return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + } + for (const [key, question] of Object.entries(roster.questions ?? {})) { + if (question == null || typeof question !== "object") continue; @@ -9939,7 +10976,8 @@ diff --git a/client.js b/client.js + chat: askedIn2 ?? chatOfTask(task), + task, + ...people(roster, task.workBotId ?? bot.id) -+ }; + }; +- }, []); + if (task.status === "failed") { + if (fresh(task.updatedAt)) needs.push({ ...card, status: "failed", reason: lastWhy(task) }); + } else if (task.status === "result-ready") needs.push({ ...card, status: "result", reason: task.result?.summary ?? "" }); @@ -10031,7 +11069,35 @@ diff --git a/client.js b/client.js + return Promise.resolve(actions.answerApproval(card.approval.id, choice)); + } + return void 0; -+} + } +-function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { +- const roster = useRoster((value) => value); +- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); +- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); +- 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]; +- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); +- const streaming = entries2.some((entry) => entry.text); +- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); +- const relaying = tools.some((entry) => entry.relay); +- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); +- const tool = tools.at(-1); +- const inputRef = (0, import_react7.useRef)(0); +- const inputAt = Math.max(inputRef.current, pendingInput); +- inputRef.current = inputAt; +- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; +- const [shown, setShown] = (0, import_react7.useState)(null); +- const wantKey = want?.key ?? null; +- const markState = (0, import_react7.useRef)("thinking"); +- if (want) markState.current = want.state; +- (0, import_react7.useEffect)(() => { +- 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; +- if (wait <= 0) { +- make(); +- return void 0; +var answerSlots = /* @__PURE__ */ new Map(); +var answerSlot = (id) => { + let slot = answerSlots.get(id); @@ -10102,7 +11168,19 @@ diff --git a/client.js b/client.js + if (selected.length === 0 && custom === "") return; + onAnswer(card, { selected, custom }); + return; -+ } + } +- const timer = setTimeout(make, wait); +- return () => clearTimeout(timer); +- }, [wantKey, shown]); +- const [phase, setPhase] = (0, import_react7.useState)(null); +- const endedAt = (0, import_react7.useRef)(0); +- const shift = (0, import_react7.useRef)(null); +- const leaving = (0, import_react7.useRef)(false); +- leaving.current = phase !== null && !visible; +- (0, import_react7.useEffect)(() => { +- if (visible) { +- setPhase("in"); +- return void 0; + if (custom === "") return; + onAnswer(card, custom); + }; @@ -10111,7 +11189,39 @@ diff --git a/client.js b/client.js + if (!multi) { + onAnswer(card, { selected: [label], custom: "" }); + return; -+ } + } +- setPhase((current) => current === "in" ? "out" : current); +- const timer = setTimeout(() => { +- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; +- setPhase((current) => current === "out" ? null : current); +- }, 140); +- return () => clearTimeout(timer); +- }, [visible]); +- (0, import_react7.useLayoutEffect)(() => { +- if (phase !== null) return; +- endedAt.current = Date.now(); +- setShown(null); +- const before = shift.current; +- shift.current = null; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (before === null || !scroller || reducedMotion()) return; +- 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]); +- (0, import_react7.useEffect)(() => { +- if (!running) actions.clearLive(sessionId); +- }, [running, sessionId]); +- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); +- const [now, setNow] = (0, import_react7.useState)(Date.now); +- (0, import_react7.useEffect)(() => { +- if (phase !== "in") return void 0; +- const timer = setInterval(() => setNow(Date.now()), 1e4); +- return () => clearInterval(timer); +- }, [phase]); +- const host = useRunningHost(running); +- if (!host || phase === null || !shown) return null; +- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; +- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( + slot.picked = slot.picked.includes(label) ? slot.picked.filter((item) => item !== label) : [...slot.picked, label]; + redraw((count) => count + 1); + }; @@ -10145,40 +11255,9 @@ diff --git a/client.js b/client.js + }), + (0, import_react14.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy, onClick: send }, t("Submit")) + ); - } --function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) { ++} +function InboxRow({ wide, useRoster, useWork, useUi, actions }) { - const roster = useRoster((value) => value); -- const shown = useCards?.((state) => state[sessionId]) ?? NONE; -- const loading = useSession?.(historyLoading) === true; -- const bot = roster.byId[sessionId]; -- if (!bot || currentPart(bot) !== sessionId) return null; -- 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; -- return (0, import_react5.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react5.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); --} --function ScopeEditor({ secret, roster, onSave }) { -- const [mode, setMode] = (0, import_react5.useState)(secret.scope === "all" ? "all" : "some"); -- const [picked, setPicked] = (0, import_react5.useState)(secret.scope === "all" ? [] : secret.scope); -- const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const same = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join(); -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, -- [["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react5.createElement)( -- "label", -- { key: id, className: "bt-secret-radio" }, -- (0, import_react5.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), -- text -- )) -- ), -- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { -- key: bot.id, ++ const roster = useRoster((value) => value); + const work = useWork ? useWork((value) => value) : {}; + const open = useUi ? useUi((value) => Boolean(value.inbox)) : false; + const tasks = useTasks(actions); @@ -10188,16 +11267,7 @@ diff --git a/client.js b/client.js + return (0, import_react14.createElement)( + "button", + { - type: "button", -- className: "bt-secret-bot", -- "aria-pressed": picked.includes(bot.id), -- onClick: () => toggle(bot.id) -- }, (0, import_react5.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react5.createElement)(CheckIcon) : null))) : null, -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access")) -- ) ++ type: "button", + className: "bt-row bt-inbox-row", + "aria-label": label, + title: wide ? void 0 : label, @@ -10208,50 +11278,12 @@ diff --git a/client.js b/client.js + (0, import_react14.createElement)("span", { className: "bt-inbox-mark" }, (0, import_react14.createElement)(InboxIcon)), + wide ? (0, import_react14.createElement)("span", { className: "bt-inbox-label" }, t("Inbox")) : null, + count > 0 ? (0, import_react14.createElement)("span", { className: "bt-inbox-count", "aria-hidden": true }, count > 99 ? "99+" : String(count)) : null - ); - } --function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); -- const long = value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!long) return; -- void Promise.resolve(onSave(value)).then((ok) => { -- if (ok) { -- setValue(""); -- setReplaced(true); -- } -- }); -- }; -- const edit = (next) => { -- setValue(next); -- setReplaced(false); -- }; -- return (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit-block" }, -- (0, import_react5.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-inline" }, -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !long, onClick: save }, t("Replace")) -- ), -- value !== "" && !long ? (0, import_react5.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react5.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react5.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null -- ); ++ ); ++} +function Status({ status }) { + const { label, tone, Icon } = STATUSES[status]; + return (0, import_react14.createElement)("span", { className: "bt-inbox-status", "data-tone": tone }, (0, import_react14.createElement)(Icon), (0, import_react14.createElement)("span", null, t(label))); - } --function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { -- if (!open) setConfirm(false); -- }, [open]); -- const asker = secret.requestedBy ? roster.byId[secret.requestedBy]?.name ?? t("a deleted Bot") : t("you"); -- const allowed = secret.scope === "all" ? [] : secret.scope.map((id) => roster.byId[id]).filter(Boolean); -- const meta = [t("Asked by {name}", { name: asker }), t("added {time}", { time: when(secret.createdAt) }), secret.lastUsedAt ? t("last used {time}", { time: when(secret.lastUsedAt) }) : t("not used yet")].join(" · "); -- return (0, import_react5.createElement)( ++} +function whenText(card, now) { + if (card.status === "running" || card.status === "task") { + return typeof card.since === "number" && now - card.since >= 6e4 ? t("for {time}", { time: elapsedLabel(now - card.since) }) : t("just started"); @@ -10270,30 +11302,25 @@ diff --git a/client.js b/client.js + const unconfirmed = card.kind === "task" && card.task?.writeBack?.state === "unconfirmed" ? card.task.writeBack : void 0; + return (0, import_react14.createElement)( "div", -- { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( +- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, +- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), +- (0, import_react7.createElement)( + { className: "bt-inbox-card", role: "group", "aria-label": `${t(STATUSES[card.status].label)}: ${card.title}`, "data-status": card.status, "data-run": card.kind === "workflow" ? card.run?.id : void 0 }, + (0, import_react14.createElement)("span", { className: "bt-inbox-ava" }, card.bot ? (0, import_react14.createElement)(BotAvatar, { bot: card.bot, size: 28, badge: false, live: false }) : card.room ? (0, import_react14.createElement)(RoomAvatar, { room: card.room, roster, size: 28 }) : null), + (0, import_react14.createElement)( -+ "span", + "span", +- { className: "bt-typing-text" }, +- (0, import_react7.createElement)( + { className: "bt-inbox-copy" }, + (0, import_react14.createElement)( "span", -- { className: "bt-set-copy" }, -- (0, import_react5.createElement)("span", { className: "bt-secret-title" }, (0, import_react5.createElement)(KeyIcon), (0, import_react5.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react5.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, meta) +- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, +- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), +- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null + { className: "bt-inbox-top" }, + (0, import_react14.createElement)(Status, { status: card.status }), + (0, import_react14.createElement)("span", { className: "bt-inbox-meta", title: meta }, meta) - ), -- (0, import_react5.createElement)( -- "span", -- { className: "bt-set-control" }, -- secret.scope === "all" ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react5.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react5.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react5.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) -- ) ++ ), + (0, import_react14.createElement)("span", { className: "bt-inbox-title", title: card.title }, card.title), + card.calls > 0 ? (0, import_react14.createElement)("span", { className: "bt-inbox-calls" }, t("{count} tool calls", { count: card.calls })) : null, + card.reason ? (0, import_react14.createElement)("span", { className: "bt-inbox-reason", title: card.reason }, failureSentence(card.reason, t)) : null, @@ -10320,22 +11347,7 @@ diff --git a/client.js b/client.js + (0, import_react14.createElement)(AnswerFields, { card, busy, onAnswer }), + (0, import_react14.createElement)(ResultActions, { card, busy, onAnswer }), + error ? (0, import_react14.createElement)("span", { className: "bt-inbox-error", role: "alert" }, error) : null - ), -- open ? (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-edit" }, -- (0, import_react5.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), -- (0, import_react5.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), -- (0, import_react5.createElement)( -- "div", -- { className: "bt-secret-row-actions" }, -- confirm ? [ -- (0, import_react5.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), -- (0, import_react5.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), -- (0, import_react5.createElement)("button", { key: "yes", type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => run(actions.secretDelete(secret.name)) }, t("Delete")) -- ] : (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react5.createElement)(TrashIcon), t("Delete")) -- ) -- ) : null ++ ), + (0, import_react14.createElement)( + "span", + { className: "bt-inbox-actions" }, @@ -10344,21 +11356,8 @@ diff --git a/client.js b/client.js + ...(answerable ? alwaysChoices(card.approval, card.bot) : []).map(([choice, label]) => (0, import_react14.createElement)("button", { key: choice, type: "button", className: "bt-soft bt-soft-sm", "data-choice": choice, disabled: busy, onClick: () => onAnswer(card, choice) }, t(label))), + (0, import_react14.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", "aria-label": who ? t("Open {name}", { name: card.room?.name ?? who }) : t("Open"), onClick: () => onOpen(card) }, t("Open")) + ) - ); - } --function AddSecret({ actions, run, onDone }) { -- const [name, setName] = (0, import_react5.useState)(""); -- const [purpose, setPurpose] = (0, import_react5.useState)(""); -- const [value, setValue] = (0, import_react5.useState)(""); -- const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; -- const save = () => { -- if (!ready) return; -- void run(actions.secretSet({ name, purpose, value, scope: "all" })).then((ok) => { -- if (ok) { -- setValue(""); -- onDone(); -- } -- }); ++ ); ++} +function useNow2(ms) { + const [now, setNow] = (0, import_react14.useState)(Date.now); + (0, import_react14.useEffect)(() => { @@ -10408,24 +11407,14 @@ diff --git a/client.js b/client.js + const name = card.bot?.name ?? ""; + setFeedback(card.kind === "approval" ? choice === "allow" ? t("Allowed once for {name}", { name }) : choice === "always-bot" ? t("Always allowed for {name}", { name }) : choice === "always-room" ? t("Always allowed in this group") : t("Rejected for {name}", { name }) : t("Answered")); + }, (failure) => setErrors((value) => ({ ...value, [card.id]: hostText(failure?.message ?? String(failure)) }))).finally(() => setBusy("")); - }; -- return (0, import_react5.createElement)( ++ }; + const open = (card) => openCard(actions, card); + return (0, import_react14.createElement)( - "div", -- { className: "bt-set-row bt-set-paste bt-secret-add" }, -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("Key name"), placeholder: t("NAME, e.g. GITHUB_TOKEN"), value: name, spellCheck: false, autoComplete: "off", autoFocus: true, onChange: (event) => setName(event.target.value.toUpperCase()) }), -- (0, import_react5.createElement)("input", { className: "bt-input", "aria-label": t("What it is for"), placeholder: t("What it is for"), value: purpose, autoComplete: "off", onChange: (event) => setPurpose(event.target.value) }), -- (0, import_react5.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react5.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), -- (0, import_react5.createElement)( ++ "div", + { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 }, + (0, import_react14.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react14.createElement)( - "div", -- { className: "bt-secret-row-actions" }, -- (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), -- (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) ++ "div", + { className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" }, + (0, import_react14.createElement)( + "div", @@ -10473,96 +11462,11 @@ diff --git a/client.js b/client.js + }, t(allDone ? "Show fewer" : "Show all")) : null + ); + }) -+ ) + ) ) - ); +- ), host); ++ ); } --function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); -- const secrets = roster.secrets ?? []; -- return [ -- (0, import_react5.createElement)( -- "p", -- { key: "about", className: "bt-secret-about" }, -- t("Keys your Bots asked for. Bots never see the values: they use them as "), -- (0, import_react5.createElement)("code", null, "$DSH_SECRET_NAME"), -- t(" in their shell, and every value is replaced by "), -- (0, import_react5.createElement)("code", null, "[secret:NAME]"), -- t(" before anything goes to a model.") -- ), -- (0, import_react5.createElement)( -- Section, -- { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, -- secrets.length === 0 && !adding ? (0, import_react5.createElement)("div", { className: "bt-set-row" }, (0, import_react5.createElement)("span", { className: "bt-set-hint" }, t("No keys yet. When a Bot needs one, it shows a card in its chat."))) : null, -- secrets.map((secret) => (0, import_react5.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), -- adding ? (0, import_react5.createElement)(AddSecret, { actions, run, onDone: () => setAdding(false) }) : null -- ), -- adding ? null : (0, import_react5.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react5.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) -- ]; --} --var SECRET_CSS = ` --.bt-secret-head{align-items:center;gap:10px} --.bt-secret-who{position:relative;flex:none;display:inline-flex} --.bt-secret-lock{position:absolute;right:-5px;bottom:-4px;width:16px;height:16px;border-radius:50%;background:var(--bt-main);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;box-shadow:0 0 0 1.5px var(--bt-bubble)} --.bt-secret-lock svg{width:11px;height:11px} --.bt-secret-name{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;background:var(--bt-hover);color:var(--bt-ink)} --.bt-secret-body{display:flex;flex-direction:column;gap:8px} --.bt-secret-field{align-items:center;min-height:36px} --.bt-secret-input{flex:1;width:100%;min-width:0;margin:0;padding:0;border:0;outline:none;background:transparent;color:var(--bt-ink);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:14px;line-height:20px;letter-spacing:.04em} --.bt-secret-input[data-masked]{-webkit-text-security:disc} --.bt-secret-input::placeholder{color:var(--bt-ink-3);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;letter-spacing:0} --.bt-secret-scope{position:relative;display:inline-grid;grid-template-columns:1fr 1fr;align-self:flex-start;padding:2px;border-radius:999px;background:var(--bt-hover)} --.bt-secret-glide{position:absolute;top:2px;bottom:2px;left:2px;width:calc(50% - 2px);border-radius:999px;background:var(--bt-main);box-shadow:0 1px 3px rgba(20,20,20,.08),inset 0 0 0 .5px color-mix(in srgb,var(--bt-ink) 6%,transparent);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-secret-glide[data-at="1"]{transform:translateX(100%)} --.bt-secret-seg{position:relative;z-index:1;height:28px;padding:0 14px;border:0;border-radius:999px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;white-space:nowrap;cursor:pointer;transition:color .16s ease} --.bt-secret-seg[aria-checked=true]{color:var(--bt-ink);font-weight:500} --.bt-secret-seg:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} --/* A key card asks the user to act, so the process group carrying it stays open in every -- work-details mode (same override as .bt-group-replies in styles.js). */ --[data-step-process]:has(.bt-secret)>div:first-child{display:none!important} --[data-step-process]:has(.bt-secret)>[data-step-process-body]{display:block!important;content-visibility:visible!important;max-height:none!important;overflow:visible!important;mask-image:none!important;scrollbar-gutter:auto!important} --.bt-secret-ok{display:flex;align-items:center;gap:4px;margin-top:6px;animation:bt-secret-in .22s cubic-bezier(.22,1,.36,1)} --.bt-secret-ok svg{width:13px;height:13px;color:#16a34a} --@keyframes bt-secret-in{from{opacity:0;transform:translateY(-2px)}} --.bt-secret-saved{display:flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-secret-note{font-size:12px;line-height:17px;color:var(--bt-ink-3)} --.bt-secret-note code,.bt-secret-about code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px} --.bt-secret-warn{display:flex;align-items:center;gap:6px;padding:6px 10px;border-radius:8px;font-size:12px;line-height:16px;color:#b25e09;background:color-mix(in srgb,#f59e0b 12%,transparent)} --.bt-secret-error{font-size:12px;line-height:16px;color:#e02135} --.bt-secret .bt-q-btn:disabled{opacity:.5;cursor:default} --.bt-secret-done .bt-q-head{align-items:center} --.bt-secret-done .bt-q-title{font-size:13px;line-height:18px;color:var(--bt-ink-2);font-weight:500} --.bt-secret-scope-tag{font-weight:400;color:var(--bt-ink-3)} --.bt-secret-check{color:var(--bt-green,#22a06b);animation:bt-secret-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-secret-done{animation:bt-secret-settle .32s cubic-bezier(.22,1,.36,1) backwards} --@keyframes bt-secret-pop{from{opacity:0;transform:scale(.4)}} --@keyframes bt-secret-settle{from{opacity:.4;transform:scale(.985)}} --.bt-secret-event{display:inline-flex;align-items:center;gap:5px} --.bt-secret-dock{flex-direction:column;align-items:center} --.bt-secret-dock .bt-q{max-width:min(550px,calc(100% - 32px))} --.bt-secret-about{margin:0 0 12px;padding:0 4px;font-size:12px;line-height:17px;color:var(--bt-ink-2)} --.bt-secret-title{display:flex;align-items:center;gap:6px;min-width:0;color:var(--bt-ink)} --.bt-secret-title svg{flex:none;color:var(--bt-ink-2)} --.bt-secret-purpose{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--bt-ink-2)} --.bt-secret-faces{display:inline-flex} --.bt-secret-faces>*+*{margin-left:-6px} --.bt-secret-edit{display:flex;flex-direction:column;gap:14px;padding:4px 12px 14px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing} backwards,bt-veil-in .2s ease-out backwards} --.bt-secret-edit-block{display:flex;flex-direction:column;gap:8px} --.bt-secret-edit-label{font-size:12px;line-height:16px;font-weight:600;color:var(--bt-ink-2)} --.bt-secret-radios{display:flex;gap:16px;font-size:13px} --.bt-secret-radio{display:inline-flex;align-items:center;gap:6px;cursor:pointer} --.bt-secret-bots{display:flex;flex-wrap:wrap;gap:6px} --.bt-secret-bot{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px 0 5px;border-radius:999px;border:.5px solid var(--bt-line);background:var(--bt-card);color:var(--bt-ink-2);font:inherit;font-size:12px;cursor:pointer;transition:background-color .12s ease,color .12s ease,border-color .12s ease} --.bt-secret-bot[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink);border-color:var(--bt-line-2)} --.bt-secret-bot svg{width:13px;height:13px} --.bt-secret-inline{display:flex;gap:8px;align-items:center} --.bt-secret-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap} --.bt-secret-row-actions .bt-set-hint{flex:1} --.bt-secret-danger{color:#e02135;display:inline-flex;align-items:center;gap:4px} --.bt-secret-danger svg{width:13px;height:13px} --.bt-secret-add .bt-input{height:34px} --@media (prefers-reduced-motion:reduce){.bt-secret-glide{transition:none}.bt-secret-check,.bt-secret-done,.bt-secret-ok,.bt-secret-edit{animation:none}} +var INBOX_CSS = ` +.bt-inbox-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-inbox-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -10615,30 +11519,16 @@ diff --git a/client.js b/client.js +.bt-inbox-answer button[aria-pressed=true]{background:var(--bt-active);color:var(--bt-ink)} +.bt-inbox-actions{flex:none;display:flex;align-items:center;gap:6px;align-self:center} +.bt-inbox-actions .bt-send{height:28px;font-size:12px} - `; ++`; --// src/client/turn-reasoning.js --function turnReasoning(steps, current) { -- const members = steps.length ? steps : [current]; -- if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return ""; -- return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n"); --} --var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== ""); --var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning"); --var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); -- --// src/client/cells.js --var import_react6 = require("react"); --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") }); --function Bubbles({ texts: texts2, roster, selfId, actions, streaming, sessionId, msgKey, useUi, lead, mentions }) { -- const onClick = (event) => { -- const target = mentionTarget(event); -- if (target === null) return; -- event.preventDefault(); -- event.stopPropagation(); -- actions.openSession(target); +-// src/client/dsh-offer.js +-var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); +-var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; +-var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); +-function showsDshRow(dsh, app) { +- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; +- if (dsh.kind === "npm") return true; +- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); +// src/shared/board-card.js +function taskCardKey(id) { + const text2 = String(id ?? "").trim(); @@ -10652,39 +11542,7 @@ diff --git a/client.js b/client.js + key, + id: String(task.id), + title: String(task.title ?? "") - }; -- const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -- texts2.map((text, index) => { -- const key = msgKey ? `${msgKey}:${index}` : void 0; -- const picked = key ? reactions?.[index] ?? [] : []; -- const last = index === texts2.length - 1; -- const live = streaming && last; -- return (0, import_react6.createElement)( -- "div", -- { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( -- "div", -- { className: "bt-msg-line" }, -- lead && last ? lead : null, -- (0, import_react6.createElement)( -- "div", -- { className: "bt-bubble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text, roster.bots, selfId), streaming: live, labels: mdLabels(), variant: "compact", fileMentions: mentions }) -- ), -- live ? null : (0, import_react6.createElement)(MessageTools, { text, name: roster.byId[selfId]?.name ?? t("Bot"), sessionId, react: key ? (emoji) => actions.toggleReaction(msgKey, index, emoji) : void 0, actions }) -- ), -- picked.length ? (0, import_react6.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react6.createElement)("button", { -- key: emoji, -- type: "button", -- className: "bt-react", -- "aria-label": t("Remove reaction {emoji}", { emoji }), -- onClick: () => actions.toggleReaction(msgKey, index, emoji) -- }, emoji))) : null -- ); -- }) ++ }; +} + +// src/client/board-card.js @@ -10747,30 +11605,17 @@ diff --git a/client.js b/client.js + meta, + footer, + actions - ); ++ ); } --var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; --function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { -- if (!open) return void 0; -- const onDown = (event) => { -- if (refs.some((ref) => ref.current?.contains(event.target))) return; -- close(); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- close(); -- } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [open]); -+ + +-// src/client/sidebar.js +-var import_react8 = require("react"); +-var import_react_dom3 = require("react-dom"); +-var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); +-function useMarkedUnread(id, active, marked, actions) { +- (0, import_react8.useEffect)(() => { +- if (active && marked) actions.clearUnread(id); +- }, [active, marked]); +// src/client/kanban.js +var import_react16 = require("react"); +var POLL_MS4 = 15e3; @@ -10783,18 +11628,35 @@ diff --git a/client.js b/client.js + (0, import_react16.createElement)("rect", { x: 13.5, y: 3, width: 4, height: 12, rx: 1 }) + ); } --function MessageTools({ text, name, sessionId, react, actions }) { -- const [panel, setPanel] = (0, import_react6.useState)(null); -- const [place, setPlace] = (0, import_react6.useState)(null); -- const [copied, setCopied] = (0, import_react6.useState)(false); -- const tools = (0, import_react6.useRef)(null); -- const pop = (0, import_react6.useRef)(null); -- const close = () => setPanel(null); -- useDismiss(panel !== null, close, [tools, pop]); -- const open = (kind, event) => { -- if (panel === kind) { -- close(); -- return; +-var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); +-var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; +-var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; +-var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { +- const [id, tool] = key.split("|"); +- return { id, tool: tool || void 0 }; +-}); +-var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); +-function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { +- const id = bot.id; +- const part = currentPart(bot); +- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); +- 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); +- const tile = roster.mainBotId === id; +- const approval = useApproval(useWork, (entry) => entry.botId === id); +- const alert = roster.questions?.[part] !== void 0 || approval; +- const doing = useActivity((map) => map[id]); +- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); +- const away = !running && groups.length > 0; +- const state = botState(running || away, doing, alert); +- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; +- if (bot.hidden && !active && !main) return null; +- const onContextMenu = (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); +function shortId(id) { + const hex = String(id ?? "").replace(/^task-/, ""); + return `#${hex.slice(-6)}`; @@ -10860,23 +11722,27 @@ diff --git a/client.js b/client.js + for (const bot of children.get(parent) ?? []) { + rows.push({ kind: "bot", id: bot.id, name: bot.name, depth, bot }); + walk(bot.id, depth + 1); - } -- const rect = event.currentTarget.getBoundingClientRect(); -- const below = rect.bottom + 220 < window.innerHeight; -- setPlace(kind === "react" ? { left: rect.left + rect.width / 2, top: rect.top - 6, "--bt-origin": "bottom center" } : { left: Math.min(rect.left, window.innerWidth - 208), ...below ? { top: rect.bottom + 4 } : { bottom: window.innerHeight - rect.top + 4, "--bt-origin": "bottom left" } }); -- setPanel(kind); -- }; -- const copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { -- setCopied(true); -- setTimeout(() => setCopied(false), 1e3); -- }); ++ } }; -- const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); -- const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); -- link.click(); -- setTimeout(() => URL.revokeObjectURL(url), 1e3); +- 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) { +- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-tile", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: preview || bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu +- }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), +- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), +- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null +- )); + walk("", 0); + const groups = (roster?.rooms ?? []).filter((room2) => room2 && room2.hidden !== true && room2.origin !== "face-to-face" && adminOf(roster, room2)).map((room2) => ({ kind: "group", id: room2.id, name: room2.displayName || room2.name || t("Group"), room: room2 })); + return { bots: rows, groups }; @@ -10917,7 +11783,8 @@ diff --git a/client.js b/client.js + return t("Failed"); + default: + return t("Not started"); -+ } + } +- return (0, import_react8.createElement)( +} +function dropLabel(column) { + if (column === "in-progress") return t("Start task"); @@ -10981,26 +11848,22 @@ diff --git a/client.js b/client.js + if (scope?.kind === "group") setGroupTasks((list) => done2 ? without(list, task.id) : upsert(list, task)); + else setOwn((list) => done2 ? without(list, task.id) : upsert(list, task)); + setArchive((list) => done2 ? [task, ...without(list, task.id)] : without(list, task.id)); - }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { -- close(); -- run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( -- "div", -- { ref: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, -- react ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Add reaction"), title: t("Add reaction"), "aria-expanded": panel === "react", onClick: (event) => open("react", event) }, (0, import_react6.createElement)(SmileIcon)) : null, -- sessionId ? (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text) }, (0, import_react6.createElement)(ReplyIcon)) : null, -- (0, import_react6.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("More message actions"), title: t("More"), "aria-haspopup": "menu", "aria-expanded": panel === "more", onClick: (event) => open("more", event) }, copied ? (0, import_react6.createElement)(CheckIcon) : (0, import_react6.createElement)(DotsIcon)), -- panel === "react" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( ++ }; + const done = scope?.kind === "group" ? archive : archive.filter((task) => !scope?.botId || task.ownerBotId === scope.botId); + return { own, groupTasks, tasks, archive: done, replace }; +} +function RailButton({ row, selected, meta, onSelect }) { + return (0, import_react16.createElement)( -+ "button", -+ { -+ type: "button", + "button", + { + type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-bot-id": id, +- title: wide ? void 0 : bot.name, +- onClick: () => actions.openSession(id), +- onContextMenu + className: "bt-board-rail-item", + "data-kind": row.kind, + "data-id": row.id, @@ -11008,11 +11871,47 @@ diff --git a/client.js b/client.js + "aria-current": selected ? "true" : void 0, + style: row.depth ? { paddingLeft: `${10 + row.depth * 14}px` } : void 0, + onClick: () => onSelect(row.kind === "group" ? { kind: "group", roomId: row.id } : { kind: "bot", botId: row.id }) -+ }, + }, +- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + (0, import_react16.createElement)("span", { className: "bt-board-rail-name" }, row.name), + meta ? (0, import_react16.createElement)("span", { className: "bt-board-rail-meta" }, meta) : null -+ ); -+} + ); + } +-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 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); +- const approval = useApproval(useWork, (entry) => entry.roomId === id); +- const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); +- const first = roster.byId[members[0]?.id]; +- const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; +- 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(", "); +- return (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-room-id": id, +- onClick: () => actions.openSession(id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); +- } +function BoardCard2({ task, roster, movable, onMove, onDrag, partOf: partOf2 }) { + const column = columnOf(task); + const priority = priorityOf(task); @@ -11029,7 +11928,10 @@ diff --git a/client.js b/client.js + "data-column": column, + "data-priority": priority, + "data-kind": overall ? "group" : void 0 -+ }, + }, +- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), +- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, +- wide ? (0, import_react8.createElement)( + idText: shortId(task.id), + emptyTitle: t("Untitled task"), + topExtra: [ @@ -11043,7 +11945,13 @@ diff --git a/client.js b/client.js + question ? (0, import_react16.createElement)("span", { key: "wait", className: "bt-board-wait" }, question) : null + ], + footer: (0, import_react16.createElement)( -+ "span", + "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), +- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null +- ); + { className: "bt-board-foot" }, + who ? (0, import_react16.createElement)("span", { className: "bt-board-who" }, who) : null, + Number.isFinite(finished) && finished > 0 ? (0, import_react16.createElement)("span", { key: "finished", className: "bt-board-when" }, relativeShort(finished)) : null, @@ -11067,51 +11975,95 @@ diff --git a/client.js b/client.js + onDragStart: () => onDrag?.(task.id), + onDragEnd: () => onDrag?.(null) + }); -+} -+function cardProps(task, roster, onMove, onDrag, partOf2, locked) { -+ return { task, roster, movable: !locked && canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; -+} -+function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull, locked }) { -+ const tree = lanes ? columnTree(tasks) : null; -+ const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; -+ const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; -+ return (0, import_react16.createElement)( -+ "section", -+ { -+ className: "bt-board-column", -+ "data-column": column, -+ "aria-label": columnTitle(column), -+ onDragOver: (event) => { -+ if (locked || !dragging) return; -+ event.preventDefault(); -+ }, -+ onDrop: (event) => { -+ if (locked) return; -+ event.preventDefault(); -+ const id = event.dataTransfer?.getData("text/plain"); -+ if (id) onMove?.(id, column); -+ onDrag?.(null); -+ } -+ }, -+ (0, import_react16.createElement)( -+ "h3", -+ { className: "bt-board-column-head" }, -+ (0, import_react16.createElement)("span", null, columnTitle(column)), -+ // #2045: the Done column is one archive page. `tasks.length` is what was drawn, not what the -+ // owner has, so a full page says `20 newest` instead of claiming the page is the total. -+ (0, import_react16.createElement)("span", { className: "bt-board-count" }, column === "done" && archiveFull ? t("{count} newest", { count: tasks.length }) : String(tasks.length)) -+ ), -+ dragging ? (0, import_react16.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, -+ tasks.length === 0 && !dragging ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, -+ tree ? tree.nests.map((nest) => (0, import_react16.createElement)( - "div", -- { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, -- REACTIONS.map((emoji) => (0, import_react6.createElement)("button", { key: emoji, type: "button", role: "menuitem", "aria-label": emoji, onClick: () => { -- close(); -- react(emoji); -- } }, emoji)) -- ), document.body) : null, -- panel === "more" ? (0, import_react_dom.createPortal)((0, import_react6.createElement)( + } +-function AgentSessionRow({ session, agent, actions, renaming, index }) { +- const input = (0, import_react8.useRef)(null); +- (0, import_react8.useLayoutEffect)(() => { +- if (!renaming) return; +- input.current?.focus(); +- input.current?.select(); +- }, [renaming]); +- if (renaming) { +- const commit = (keep) => { +- const value = input.current?.value ?? ""; +- if (!keep || value.trim() === "" || value.trim() === session.title) { +- actions.endAgentRename(); +- return; +- } +- void (async () => { +- try { +- await actions.renameAgentSession(agent.id, session.id, value); +- actions.endAgentRename(); +- } catch (error) { +- actions.reportError(error, "agent-rename"); +- } +- })(); +- }; +- return (0, import_react8.createElement)( +- "div", +- { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, +- (0, import_react8.createElement)("input", { +- ref: input, +- defaultValue: session.title, +- "aria-label": t("Rename"), +- onKeyDown: (event) => { +- if (event.nativeEvent.isComposing) return; +- if (event.key === "Enter") commit(true); +- else if (event.key === "Escape") commit(false); +- }, +- onBlur: () => commit(true) +- }) +- ); +- } +- return (0, import_react8.createElement)( +- "button", ++function cardProps(task, roster, onMove, onDrag, partOf2, locked) { ++ return { task, roster, movable: !locked && canMoveTask(roster, task), onMove, onDrag, partOf: partOf2 }; ++} ++function BoardColumn({ column, tasks, roster, lanes, dragging, onMove, onDrag, archiveFull, locked }) { ++ const tree = lanes ? columnTree(tasks) : null; ++ const linked = tree ? tree.loose.filter((task) => task.parentId || task.parentTitle) : []; ++ const plain = tree ? tree.loose.filter((task) => !task.parentId && !task.parentTitle) : tasks; ++ return (0, import_react16.createElement)( ++ "section", + { +- type: "button", +- className: "bt-agent-sub-row", +- "aria-current": session.active ? "page" : void 0, +- style: { "--i": index }, +- onClick: () => actions.openAgentSession(session.id), +- onContextMenu: (event) => { ++ className: "bt-board-column", ++ "data-column": column, ++ "aria-label": columnTitle(column), ++ onDragOver: (event) => { ++ if (locked || !dragging) return; + event.preventDefault(); +- event.stopPropagation(); +- actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); ++ }, ++ onDrop: (event) => { ++ if (locked) return; ++ event.preventDefault(); ++ const id = event.dataTransfer?.getData("text/plain"); ++ if (id) onMove?.(id, column); ++ onDrag?.(null); + } + }, +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") ++ (0, import_react16.createElement)( ++ "h3", ++ { className: "bt-board-column-head" }, ++ (0, import_react16.createElement)("span", null, columnTitle(column)), ++ // #2045: the Done column is one archive page. `tasks.length` is what was drawn, not what the ++ // owner has, so a full page says `20 newest` instead of claiming the page is the total. ++ (0, import_react16.createElement)("span", { className: "bt-board-count" }, column === "done" && archiveFull ? t("{count} newest", { count: tasks.length }) : String(tasks.length)) ++ ), ++ dragging ? (0, import_react16.createElement)("div", { className: "bt-board-drop" }, dropLabel(column)) : null, ++ tasks.length === 0 && !dragging ? (0, import_react16.createElement)("p", { className: "bt-board-empty" }, t("Nothing in this column.")) : null, ++ tree ? tree.nests.map((nest) => (0, import_react16.createElement)( ++ "div", + { key: nest.task.id, className: "bt-board-nest" }, + (0, import_react16.createElement)(BoardCard2, cardProps(nest.task, roster, onMove, onDrag, void 0, locked)), + nest.children.map((child) => (0, import_react16.createElement)( @@ -11122,30 +12074,77 @@ diff --git a/client.js b/client.js + )) : null, + linked.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, task.parentTitle || "", locked) })), + lanes ? lanesOf(plain, roster).map((lane) => (0, import_react16.createElement)( - "div", -- { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, -- item(t("Copy"), CopyIcon, copy2), -- item(t("Download"), DownloadIcon, download2) -- ), document.body) : null ++ "div", + { key: lane.id, className: "bt-board-lane" }, + (0, import_react16.createElement)("span", { className: "bt-board-lane-name" }, lane.name), + lane.tasks.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) + )) : plain.map((task) => (0, import_react16.createElement)(BoardCard2, { key: task.id, ...cardProps(task, roster, onMove, onDrag, void 0, locked) })) ); } --function ThinkingRow({ text }) { -- return (0, import_react6.createElement)( -- "details", -- { className: "bt-think" }, -- (0, import_react6.createElement)("summary", null, t("Thought it through")), -- (0, import_react6.createElement)("div", { className: "bt-think-text" }, text) +-function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { +- const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { +- const row = list.byId[id]; +- return { +- id, +- // A blank Session's title is the workspace folder name; show the label instead. +- title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", +- at: row?.updatedAt ?? 0, +- running: row?.running === true, +- active: (row?.retainedBy?.mainView ?? 0) > 0 +- }; +- }).sort((a, b) => b.at - a.at))); +- const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ +- id, +- running: map.get(id)?.running === true, +- unread: map.get(id)?.completionUnread === true +- })))); +- const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); +- const sessions = JSON.parse(info).map((session) => ({ +- ...session, +- running: statuses[session.id]?.running ?? session.running, +- unread: statuses[session.id]?.unread ?? false, +- title: session.title === "" ? t("New session") : session.title +- })); +- const renaming = useUi((value) => value.renameAgent); +- const active = sessions.some((session) => session.active); +- const running = sessions.some((session) => session.running); +- const unread = sessions.some((session) => session.unread) || marked && !active; +- useMarkedUnread(agent.id, active, marked, actions); +- 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; +- return (0, import_react8.createElement)( +function BoardView({ roster, tasks, rail, scope, counts: counts2, dragging, error, draft, onDraft, onSelect, onMove, onDrag, onCreate, archiveFull, locked }) { + const columns = boardColumns(tasks); + const lanes = scope?.kind === "group"; + const selectedBot = scope?.kind === "bot" ? scope.botId : ""; + const selectedRoom = scope?.kind === "group" ? scope.roomId : ""; + return (0, import_react16.createElement)( -+ "div", + "div", +- { className: "bt-agent" }, +- (0, import_react8.createElement)( +- "button", +- { +- type: "button", +- className: "bt-row", +- "aria-label": label, +- "aria-current": active ? "page" : void 0, +- "data-agent-id": agent.id, +- title: wide ? void 0 : "DSH Agent", +- onClick: () => actions.openAgent(agent.id), +- onContextMenu: (event) => { +- event.preventDefault(); +- actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); +- } +- }, +- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), +- wide ? (0, import_react8.createElement)( +- "span", +- { className: "bt-row-body" }, +- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), +- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null +- ) : null, +- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + { className: "bt-board-body" }, + (0, import_react16.createElement)( + "aside", @@ -11165,9 +12164,12 @@ diff --git a/client.js b/client.js + }), + rail.groups.length > 0 ? (0, import_react16.createElement)("h3", { className: "bt-board-rail-head" }, t("Groups you admin")) : null, + rail.groups.map((row) => (0, import_react16.createElement)(RailButton, { key: row.id, row, selected: row.id === selectedRoom, onSelect })) -+ ), + ), +- (0, import_react8.createElement)( + (0, import_react16.createElement)( -+ "div", + "div", +- { className: "bt-agent-sub", "data-open": open || void 0 }, +- (0, import_react8.createElement)( + { className: "bt-board-main" }, + error ? (0, import_react16.createElement)("p", { className: "bt-board-error", role: "alert" }, error) : null, + lanes ? (0, import_react16.createElement)("p", { className: "bt-board-hint" }, t("Assign, claim, and progress live on this board: one overall task, a subtask per part, an assignee, a column, and progress. @ only wakes someone.")) : null, @@ -11184,7 +12186,23 @@ diff --git a/client.js b/client.js + // so the lock's own sentence is drawn here too, from the host, not in a second wording. + locked ? (0, import_react16.createElement)("p", { className: "bt-board-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, + (0, import_react16.createElement)( -+ "div", + "div", +- { className: "bt-agent-sub-in" }, +- (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { +- void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); +- } }, +- (0, import_react8.createElement)(PlusIcon), +- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) +- ), +- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { +- key: session.id, +- session, +- agent, +- actions, +- index: index + 1, +- renaming: renaming === session.id + { className: "bt-board-columns" }, + ["not-started", "in-progress", "done"].map((column) => (0, import_react16.createElement)(BoardColumn, { + key: column, @@ -11197,54 +12215,30 @@ diff --git a/client.js b/client.js + onDrag, + archiveFull, + locked -+ })) -+ ) -+ ) + })) + ) + ) ); } --function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -- const tail = useTurnData?.("turn-tail"); -- const location2 = node.location; -- const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0; -- const seq = node.data.finalNode?.seq; -- return (0, import_react6.useMemo)(() => { -- 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]); --} --function AssistantCell({ node, groupPart, sessionId, openFile, fileMentions, useTurnData, useRoster, useUi, useTurnSteps, useTurnContext, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const steps = useTurnSteps(String(data.turn)); -- const contexts = useTurnContext(String(data.turn)); -- const mentions = useFileMentions({ node, openFile, fileMentions, useTurnData }); -- const hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); -- const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block) => block.text); -- 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; -- const reasoning = groupPart === "response" ? "" : turnReasoning(steps, data); -- 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; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- relays.map((relay) => { -- const source = relay.source; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(EventLine, { -- key: relay.seq, -- prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), -- bots: [sender], -- onOpen: () => { -- if (sender.id) actions.openExchange(selfId, sender.id); -- }, -- whole: true -- }); -- }), -- reasoning ? (0, import_react6.createElement)(ThinkingRow, { text: reasoning }) : null, -- texts2.length ? (0, import_react6.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions }) : null +-var GLIDE = springEasing(1e3, 63); +-function useListGlide(listRef, layout, key) { +- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); +- (0, import_react8.useLayoutEffect)(() => { +- const list = listRef.current; +- if (!list) return; +- const next = /* @__PURE__ */ new Map(); +- const still = reducedMotion() || tops.current.layout !== layout; +- for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { +- const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; +- const top = row.offsetTop; +- next.set(id, top); +- const before = tops.current.map.get(id); +- if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { +- row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); +- } +- } +- tops.current = { layout, map: next }; +- }, [layout, key]); +function KanbanRow({ wide, useUi, actions }) { + const open = useUi ? useUi((value) => Boolean(value.board)) : false; + const label = t("Tasks"); @@ -11261,16 +12255,43 @@ diff --git a/client.js b/client.js + }, + (0, import_react16.createElement)("span", { className: "bt-board-mark" }, (0, import_react16.createElement)(BoardIcon)), + wide ? (0, import_react16.createElement)("span", { className: "bt-board-label" }, label) : null - ); - } --function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { -- if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( ++ ); ++} +function scopeName(roster, scope) { + if (scope?.kind === "group") return roster.roomsById?.[scope.roomId]?.name || t("Group"); + if (scope?.kind === "bot") return roster.byId?.[scope.botId]?.name || ""; + return ""; -+} + } +-function TeamSidebar(props) { +- const { wide, useRoster, useUi, useSessions, actions } = props; +- const listRef = (0, import_react8.useRef)(null); +- const roster = useRoster((value) => value); +- const markedUnread = useUi((value) => value.unread); +- props.useRegistry((value) => value); +- const updated = useSessions((list) => { +- const stamps = {}; +- for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; +- return JSON.stringify(stamps); +- }); +- const order = (0, import_react8.useMemo)(() => { +- const stamps = JSON.parse(updated); +- const entries2 = [ +- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), +- ...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 })) +- ]; +- 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]; +- 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 }; +- const typeToSearch = (event) => { +- if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; +- if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; +- event.preventDefault(); +- actions.openPalette(event.key); +function KanbanDialog({ roster, actions, leaving = false }) { + const locked = typeof roster?.readOnly === "string" && roster.readOnly.trim() !== ""; + const rail = boardRail(roster); @@ -11310,7 +12331,8 @@ diff --git a/client.js b/client.js + } catch (failure) { + setError(failure?.message ?? String(failure)); + } -+ }; + }; +- return (0, import_react8.createElement)( + const create = async (title) => { + if (locked) return; + const text2 = String(title ?? "").trim(); @@ -11325,20 +12347,30 @@ diff --git a/client.js b/client.js + }; + const name = scopeName(roster, scope); + return (0, import_react16.createElement)( -+ "div", + "div", +- { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, +- wide ? (0, import_react8.createElement)( +- "div", +- { className: "bt-side-top" }, +- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) +- ) : (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), +- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) +- ), +- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, +- (0, import_react8.createElement)( + { className: "bt-mem-layer bt-board-layer", "data-leaving": leaving || void 0 }, + (0, import_react16.createElement)("div", { className: "bt-scrim", onClick: close }), + (0, import_react16.createElement)( "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, -- (0, import_react6.createElement)("span", null, prefix), -- (0, import_react6.createElement)(BotMark, { bot: bots[0], size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, bots[0].name) -- ) -- ); +- { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, +- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), +- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) + { className: "bt-mem bt-board", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-board-heading" }, + (0, import_react16.createElement)( + "div", @@ -11367,9 +12399,18 @@ diff --git a/client.js b/client.js + onDrag: setDragging, + onCreate: create + }) -+ ) -+ ); -+} + ) + ); + } +-function UpdateButton({ useUpdate, actions }) { +- const update = useUpdate((value) => value); +- const [open, setOpen] = (0, import_react8.useState)(false); +- const [box, setBox] = (0, import_react8.useState)(null); +- const [error, setError] = (0, import_react8.useState)(""); +- const [dshError, setDshError] = (0, import_react8.useState)(""); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!open) return void 0; +var KANBAN_CSS = ` +.bt-board-row{min-height:40px;padding:6px 8px;gap:10px} +.bt-board-mark{flex:none;width:36px;height:28px;display:inline-flex;align-items:center;justify-content:center;color:var(--bt-ink-2)} @@ -11477,23 +12518,9 @@ diff --git a/client.js b/client.js + for (const role of choices) { + if (role === current) continue; + items.push({ role, label: roleText(role), current: false, pick: true }); - } -- const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); -- bots.forEach((bot, index) => { -- if (index > 0) parts.push((0, import_react6.createElement)("span", { key: `j${index}` }, joiner)); -- parts.push((0, import_react6.createElement)( -- "button", -- { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- bot.name -- )); -- }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); ++ } + return items; - } --function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { -- const lead = (0, import_react6.createElement)( ++} +function roleNote(answer) { + if (answer?.changed === true) return t("Role changed."); + const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused"); @@ -11501,11 +12528,7 @@ diff --git a/client.js b/client.js +} +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react17.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react17.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react17.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react17.createElement)( - "button", -- { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22 }) -- ); -- return (0, import_react6.createElement)( ++ "button", + { + key: String(project.id), + type: "button", @@ -11523,134 +12546,62 @@ diff --git a/client.js b/client.js + roleText(project.role) === "" ? null : (0, import_react17.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role)) + )); + return (0, import_react17.createElement)( - "div", -- { className: "bt-group-msg" }, -- (0, import_react6.createElement)( -- "span", -- { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, -- bot.name, -- room2?.admin === bot.id ? (0, import_react6.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null -- ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) ++ "div", + { className: "bt-projects-list", role: "group", "aria-label": t("Projects") }, + rows, + note === "" || note === void 0 ? null : (0, import_react17.createElement)("div", { className: "bt-projects-note", role: "status" }, note) - ); - } --function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("span", null, prefix), -- room2 ? (0, import_react6.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react6.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react6.createElement)("span", null, name) : null ++ ); ++} +function Chevron() { + return (0, import_react17.createElement)( + "svg", + { width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react17.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" }) - ); - } --function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -- if (!/^(Saved to|Updated in|Removed from) /.test(result)) return null; -- const self = roster.byId[sessionId]; -- const team = /^(team|团队|共享)$/i.test(String(scope ?? "").trim()); -- const other = team || !scope ? void 0 : findBot(scope); -- 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 }); -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-event" }, -- (0, import_react6.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react6.createElement)(MemoryIcon), label) ++ ); ++} +function ProjectsMark() { + return (0, import_react17.createElement)( + "svg", + { width: 18, height: 18, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinejoin: "round", "aria-hidden": true }, + (0, import_react17.createElement)("path", { d: "M2.5 5.5h6l1.5 2h7.5v8a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1z" }), + (0, import_react17.createElement)("path", { d: "M2.5 5.5v-1a1 1 0 0 1 1-1h4l1.5 2" }) - ); - } --function PartLine({ time }) { -- return (0, import_react6.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react6.createElement)("span", null, dayLabel(time)) : null); --} --var scrollParent = (node) => { -- for (let at = node.parentElement; at; at = at.parentElement) { -- if (/(auto|scroll)/.test(getComputedStyle(at).overflowY)) return at; -- } -- return null; --}; --function EarlierParts({ sessionId, roster, actions }) { -- const [view, setView] = (0, import_react6.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); -- const top = (0, import_react6.useRef)(null); -- const busy = (0, import_react6.useRef)(false); -- const alive = (0, import_react6.useRef)(true); -- const restore = (0, import_react6.useRef)(null); -- const load = (cursor) => { -- if (busy.current) return; -- busy.current = true; -- setView((current) => ({ ...current, loading: true, failed: false })); -- actions.earlier(sessionId, cursor).then((page) => { -- busy.current = false; -- if (!alive.current) return; -- const scroller = top.current && scrollParent(top.current); -- if (scroller) restore.current = { scroller, fromEnd: scroller.scrollHeight - scroller.scrollTop }; -- setView((current) => ({ items: [...page.items, ...current.items], cursor: page.cursor, loading: false, failed: false, startedAt: page.startedAt ?? current.startedAt })); -- }, () => { -- busy.current = false; -- if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); -- }); -- }; -- (0, import_react6.useEffect)(() => { -- alive.current = true; -- load(void 0); ++ ); ++} +function ProjectRoleMenu({ project, x, y, onPick, onClose }) { + const ref = (0, import_react17.useRef)(null); + (0, import_react17.useEffect)(() => { -+ const onDown = (event) => { + const onDown = (event) => { +- if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; +- setOpen(false); + if (!ref.current?.contains(event.target)) onClose(); -+ }; -+ const onKey = (event) => { + }; + const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); +- } + if (event.key === "Escape") onClose(); -+ }; + }; +- window.addEventListener("mousedown", onDown); + window.addEventListener("mousedown", onDown, true); -+ window.addEventListener("keydown", onKey, true); + window.addEventListener("keydown", onKey, true); return () => { -- alive.current = false; +- window.removeEventListener("mousedown", onDown); + window.removeEventListener("mousedown", onDown, true); -+ window.removeEventListener("keydown", onKey, true); + window.removeEventListener("keydown", onKey, true); }; -- }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { -- const pending = restore.current; -- restore.current = null; -- if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; -- }, [view.items]); -- (0, import_react6.useEffect)(() => { -- const node = top.current; -- if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; -- const observer = new IntersectionObserver((entries2) => { -- if (entries2.some((entry) => entry.isIntersecting)) load(view.cursor); -- }, { root: scrollParent(node), rootMargin: "600px 0px 0px 0px" }); -- observer.observe(node); -- return () => observer.disconnect(); -- }, [view.cursor, view.loading, view.failed]); -- const selfId = roster.byId[sessionId]?.id; -- const rows = []; -- for (const item of view.items) { -- const last = rows.at(-1); -- if (item.kind === "bot" && last?.kind === "bot") last.texts.push(item.text); -- else rows.push(item.kind === "bot" ? { kind: "bot", texts: [item.text] } : item); -- } -- return (0, import_react6.createElement)( +- }, [open]); +- const phase = update?.phase ?? "idle"; +- const dsh = update?.dsh; +- const dshPhase = dsh?.phase ?? "idle"; +- const dshRow = showsDshRow(dsh, update?.app); +- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; +- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react17.createElement)( - "div", -- { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( -- "div", -- { ref: top, className: "bt-earlier-top" }, -- view.loading ? (0, import_react6.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react6.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Load earlier")) : null ++ "div", + { + ref, + className: "bt-project-menu", @@ -11703,17 +12654,86 @@ diff --git a/client.js b/client.js + if (!expanded) return; + void load(); + }, [expanded, load]); -+ const toggle = () => { + const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); +- setOpen((value) => !value); +- }; +- const start = () => { +- setError(""); +- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); +- }; +- const startDsh = () => { +- setDshError(""); +- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); + setNote(""); + if (!wide) { + void open(""); + return; + } + setExpanded((value) => !value); -+ }; -+ const listed = expanded && wide; -+ return (0, import_react17.createElement)( -+ "div", + }; +- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); +- 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 }); +- return (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)("button", { +- ref: button, +- type: "button", +- className: "bt-round bt-update", +- "data-phase": cloudPhase, +- "aria-label": label, +- title: label, +- "aria-haspopup": "dialog", +- "aria-expanded": open, +- onClick: toggle +- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( +- "div", +- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, +- update?.available || phase !== "idle" ? (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), +- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-update-actions" }, +- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, +- phase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: phase === "installing", +- onClick: start +- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null, +- dshRow ? (0, import_react8.createElement)( +- "div", +- { className: "bt-update-dsh" }, +- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), +- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), +- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, +- (0, import_react8.createElement)( +- "div", +- { className: "bt-update-actions" }, +- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { +- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { +- type: "button", +- className: "bt-update-go", +- disabled: dshPhase === "installing", +- onClick: startDsh +- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) +- ) +- ) : null +- ), document.body) : null ++ const listed = expanded && wide; ++ return (0, import_react17.createElement)( ++ "div", + { className: "bt-projects" }, + (0, import_react17.createElement)( + "button", @@ -11728,30 +12748,7 @@ diff --git a/client.js b/client.js + (0, import_react17.createElement)("span", { className: "bt-projects-mark" }, (0, import_react17.createElement)(ProjectsMark)), + wide ? (0, import_react17.createElement)("span", { className: "bt-projects-label" }, t("Projects")) : null, + wide ? (0, import_react17.createElement)("span", { className: "bt-projects-chevron", "aria-hidden": true }, (0, import_react17.createElement)(Chevron)) : null - ), -- rows.map((row, index) => { -- const key = rows.length - index; -- switch (row.kind) { -- case "divider": -- return (0, import_react6.createElement)(PartLine, { key, time: row.time }); -- case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- row.text ? (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, -- row.images ? (0, import_react6.createElement)("span", { className: "bt-uimages" }, row.images === 1 ? t("1 image") : t("{count} images", { count: row.images })) : null -- )); -- case "answer": -- return (0, import_react6.createElement)(AnsweredCell, { key, node: { data: row } }); -- case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); -- case "event": { -- if (row.role === "report") return (0, import_react6.createElement)(GroupEvent, { key, prefix: t("Replies from"), room: roster.roomsById[row.roomId], name: t("a group chat"), roster, actions }); -- const other = roster.byId[row.botId] ?? { id: row.botId, name: row.name ?? t("a Bot"), color: "gray" }; -- const prefix = { stopped: t("Stopped by"), reply: t("Reply from") }[row.role] ?? t("Message from"); -- return (0, import_react6.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { -- if (selfId && other.id) actions.openExchange(selfId, other.id); -- }, whole: true }); ++ ), + listed ? (0, import_react17.createElement)(ProjectList, { projects, error, note, onOpen: open, onRole: (project, event) => setMenu({ project, x: event.clientX, y: event.clientY }) }) : null, + menu === null ? null : (0, import_react17.createElement)(ProjectRoleMenu, { + project: menu.project, @@ -11767,82 +12764,32 @@ diff --git a/client.js b/client.js + if (answer?.changed === true) void load(); + } catch (failure) { + setNote(failure?.message ?? String(failure)); - } -- default: -- return null; - } -- }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) ++ } ++ } + }) ); } --function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -- const roster = useRoster((value) => value); -- const data = node.data; -- const source = data.source !== null && typeof data.source === "object" ? data.source : {}; -- const selfId = roster.byId[sessionId]?.id ?? sessionId; -- const deferred = source.kind === "bot" && source.deferred === true; -- const relayed = isRelayContext(data); -- const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { -- if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); -- }, [relayed, sessionId, turn, source.senderSessionId]); -- const order = useSessions((list) => { -- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; -- return Array.isArray(outline) ? outline.map((entry) => entry.turn).join(",") : ""; -- }); -- const peers = useTurnClock((state) => { -- if (!relayed) return ""; -- const turns = state[sessionId] ?? {}; -- return Object.keys(turns).filter((key) => turns[key].peer !== void 0).map((key) => `${key}:${turns[key].peer}`).join(","); -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const hiddenTurns = roster.exchangeTurns[sessionId]; -- const unreadOpening = useTurnClock((state) => { -- const first = openingTurn(state[sessionId], order, hiddenTurns); -- return first?.turn === turn && unseenSince(visit, first.start) ? clockLabel(first.start) : ""; -- }); -- const body = triggerBody(); -- return unreadOpening ? (0, import_react6.createElement)(import_react6.Fragment, null, (0, import_react6.createElement)("div", { className: "bt-open-new" }, (0, import_react6.createElement)(NewDivider), (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; -- function triggerBody() { -- if (source.kind === "bot") { -- if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react6.createElement)(EarlierParts, { sessionId, roster, actions }); -- if (source.role === "kickoff" || source.role === "room") return null; -- if (source.role === "secret" || deferred) return null; -- if (source.role === "post") { -- if (source.failed !== void 0 || source.resumed === true || source.stopped === true) return null; -- const room2 = roster.roomsById[sessionId]; -- const poster = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- return (0, import_react6.createElement)(GroupMessage, { bot: poster, text: stripHeader(contentText(data.content)), room: room2, roster, actions, msgKey: `${sessionId}:${data.turn}:post`, useUi }); -- } -- if (source.role === "report") { -- return (0, import_react6.createElement)(GroupEvent, { prefix: t("Replies from"), room: roster.roomsById[source.roomId], name: t("a group chat"), roster, actions }); -- } -- if (source.role === "resume" || source.resumed === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); -- if (source.role === "retry" || source.retried === true) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Asked to make the tool call it wrote as text")); -- 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)])), -- turn, -- hiddenTurns -- ) : null; -- if (run === "folded") return null; -- const sender = roster.byId[source.senderSessionId] ?? { id: source.senderSessionId, name: source.senderName ?? t("a Bot"), color: "gray" }; -- const count = run?.count ?? (isHiddenTurn(roster, sessionId, turn) ? 2 : 1); -- const bots = [sender]; -- for (const peer of (run?.peerIds ?? []).map((id) => roster.byId[id]).filter(Boolean)) { -- if (!bots.some((bot) => bot.id === peer.id)) bots.push(peer); -- } -- return (0, import_react6.createElement)(EventLine, { -- 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), -- whole: true -- }); -- } -- if (source.kind === "schedule") { -- const prompts = reminderPrompts(contentText(data.content)); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); +-function ConnectPlugins({ wide, actions }) { +- const tipId = (0, import_react8.useId)(); +- const [tip, setTip] = (0, import_react8.useState)(false); +- const timer = (0, import_react8.useRef)(0); +- const button = (0, import_react8.useRef)(null); +- const [box, setBox] = (0, import_react8.useState)(null); +- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); +- if (!wide) return null; +- const show = () => { +- clearTimeout(timer.current); +- timer.current = setTimeout(() => { +- const rect = button.current?.getBoundingClientRect(); +- if (!rect) return; +- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); +- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); +- setTip(true); +- }, 250); +- }; +- const hide = () => { +- clearTimeout(timer.current); +- setTip(false); + +// src/client/sidebar.js +var import_react18 = require("react"); @@ -11884,7 +12831,11 @@ diff --git a/client.js b/client.js + const onContextMenu = (event) => { + event.preventDefault(); + actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); -+ }; + }; +- return (0, import_react8.createElement)( +- "span", +- { className: "bt-connect-wrap" }, +- (0, import_react8.createElement)( + const pinned = bot.pinned === true && !tile; + const remote = Boolean(bot.remote); + const shown = remote ? friendBotTitle(bot) : bot.name; @@ -11892,9 +12843,17 @@ diff --git a/client.js b/client.js + const label = [shown, main ? t("Main Bot") : "", pinned ? t("Pinned") : "", running || away ? t("Working") : "", waiting ? t("Waiting for your approval") : alert ? t("Waiting for your answer") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); + if (tile && wide) { + return (0, import_react18.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react18.createElement)( -+ "button", -+ { -+ type: "button", + "button", + { +- ref: button, + type: "button", +- className: "bt-connect bt-connect-foot", +- "aria-describedby": tip ? tipId : void 0, +- onMouseEnter: show, +- onMouseLeave: hide, +- onFocus: show, +- onBlur: hide, +- onClick: () => actions.selectPanel("plugins") + className: "bt-tile", + "aria-label": label, + "aria-current": active ? "page" : void 0, @@ -11902,7 +12861,12 @@ diff --git a/client.js b/client.js + title: preview || bot.name, + onClick: () => actions.openSession(id), + onContextMenu -+ }, + }, +- (0, import_react8.createElement)(PlugIcon), +- t("Connect plugins"), +- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") +- ), +- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + (0, import_react18.createElement)(BotAvatar, { bot, size: 60, state, main: true }), + (0, import_react18.createElement)("span", { className: "bt-tile-name" }, shown), + // The Main Bot's tile gets the same unread dot as a row (#420); a wait shows on its avatar. @@ -11927,7 +12891,10 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)(BotAvatar, { bot, size: 36, state, main }), + pinned && !wide ? (0, import_react18.createElement)(PinMark) : null, + wide ? (0, import_react18.createElement)( -+ "span", + "span", +- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, +- t("Plugin compatibility is not fully verified.") +- ), document.body) : null + { className: "bt-row-body" }, + (0, import_react18.createElement)( + "span", @@ -11939,7 +12906,7 @@ diff --git a/client.js b/client.js + secondLine ? (0, import_react18.createElement)("span", { className: "bt-row-preview" }, secondLine) : null + ) : null, + unreadMark -+ ); + ); + const settingsLabel = bot.remote ? t("Bot settings") : t("Edit Bot"); + const settings = wide ? (0, import_react18.createElement)("button", { + type: "button", @@ -11951,10 +12918,7 @@ diff --git a/client.js b/client.js + event.stopPropagation(); + keepSidebarStill(event.currentTarget); + actions.openSettings("bots", id); - } -- if (source.kind === "user-question-reply") return null; -- return (0, import_react6.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); -- } ++ } + }, (0, import_react18.createElement)(GearIcon)) : null; + if (settings) return (0, import_react18.createElement)("div", { className: "bt-row-wrap", "data-bot-id": id }, row, settings); + return row; @@ -11968,11 +12932,14 @@ diff --git a/client.js b/client.js + reset(); + if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => requestAnimationFrame(reset)); } --function NewDivider() { -- return (0, import_react6.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react6.createElement)("span", null, t("New"))); +-function YouAvatar() { +- return (0, import_react8.createElement)( +function RemoteMark({ remote }) { + return (0, import_react18.createElement)( -+ "span", + "span", +- { className: "bt-you-wrap" }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), +- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + { className: "bt-row-remote", title: remote.gone ? `${remote.handle} · ${t("no longer shared")}` : remote.handle, "aria-label": t("Friend's Bot") }, + (0, import_react18.createElement)( + "svg", @@ -11980,34 +12947,52 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("circle", { cx: 12, cy: 12, r: 9 }), + (0, import_react18.createElement)("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" }) + ) -+ ); + ); } --function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { -- const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { -- if (!open) turnProcess.setOpen(true); -- }, [open, turnProcess]); -- const turn = node?.location?.turn?.turn ?? node?.data?.turn; -- const start = node?.location?.turn?.start?.time; -- const hidden = useRoster((roster) => isHiddenTurn(roster, sessionId, turn)); -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "first", start); -- }, [sessionId, turn, start]); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- 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; -- }); -- return fresh ? (0, import_react6.createElement)(NewDivider) : null; -+function isPastFaceRoom(room2) { -+ if (room2?.origin !== "face-to-face") return false; -+ const status = room2.face?.status; -+ if (status === "closed" || status === "left") return true; -+ const expires = room2.face?.expiresAt; -+ return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); +-var openShellSettings = null; +-var openShellOnboarding = null; +-var shellShortcuts = null; +-function holdShellShortcuts(shortcuts) { +- shellShortcuts = shortcuts; +- return () => { +- if (shellShortcuts === shortcuts) shellShortcuts = null; +- }; +-} +-function openHarnessSettings() { +- requestAnimationFrame(() => { +- if (openShellSettings) { +- openShellSettings(); +- return; +- } +- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); +- if (desktop) openThroughMenu(desktop); +- else pressSettingsShortcut(); +- }); +-} +-function openHarnessOnboarding(id) { +- requestAnimationFrame(() => { +- if (openShellOnboarding) openShellOnboarding(id); +- else openHarnessSettings(); +- }); ++function isPastFaceRoom(room2) { ++ if (room2?.origin !== "face-to-face") return false; ++ const status = room2.face?.status; ++ if (status === "closed" || status === "left") return true; ++ const expires = room2.face?.expiresAt; ++ return status === "candidate" && expires && Number.isFinite(Date.parse(expires)) && Date.parse(expires) <= Date.now(); } --function Nothing() { -- return null; +-function openThroughMenu(trigger) { +- const before = new Set(document.querySelectorAll('[role="menu"]')); +- trigger.click(); +- let frames = 0; +- const look = () => { +- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); +- const item = menu?.querySelector('button[role="menuitem"]'); +- if (item) item.click(); +- else if (++frames < 30) requestAnimationFrame(look); +- else pressSettingsShortcut(); +- }; +- requestAnimationFrame(look); +function FaceMark() { + return (0, import_react18.createElement)( + "span", @@ -12021,16 +13006,20 @@ diff --git a/client.js b/client.js + ) + ); } --var SEPARATOR_GAP_MS = 30 * 60 * 1e3; --function clockLabel(time, now = Date.now()) { -- const date = new Date(time); -- const midnight = (value) => new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime(); -- const days = Math.round((midnight(new Date(now)) - midnight(date)) / 864e5); -- const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); -- if (days === 0) return t("Today {time}", { time: clock4 }); -- if (days === 1) return t("Yesterday {time}", { time: clock4 }); -- const sameYear = date.getFullYear() === new Date(now).getFullYear(); -- return `${date.toLocaleDateString(dateLocale(), { weekday: "short", month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } })} ${clock4}`; +-function pressSettingsShortcut() { +- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; +- if (!binding) return; +- const held = new Set(binding.modifiers); +- window.dispatchEvent(new KeyboardEvent("keydown", { +- code: binding.code, +- key: binding.code === "Comma" ? "," : "", +- bubbles: true, +- cancelable: true, +- ctrlKey: held.has("control"), +- altKey: held.has("alt"), +- shiftKey: held.has("shift"), +- metaKey: held.has("meta") +- })); +function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useWork, wide, marked }) { + const id = room2.id; + const fallback = useSessions((list) => nameMentions(previewOf(list, id, [], [], botNames(roster)), roster)); @@ -12080,11 +13069,37 @@ diff --git a/client.js b/client.js + wide && alert && !active ? (0, import_react18.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react18.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null + ); } --function nextTurnStart(turns, turn) { -- let next; -- for (const key in turns) { -- const candidate = Number(key); -- if (candidate > turn && turns[key].first !== void 0 && (next === void 0 || candidate < next)) next = candidate; +-function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { +- const [open, setOpen] = (0, import_react8.useState)(false); +- (0, import_react8.useLayoutEffect)(() => { +- if (!seat) return void 0; +- actions.seatAccount(true); +- return () => actions.seatAccount(false); +- }, [seat]); +- const [box, setBox] = (0, import_react8.useState)(null); +- const button = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (settingsOpen) setOpen(false); +- }, [settingsOpen]); +- (0, import_react8.useEffect)(() => { +- if (!openSettings) return void 0; +- openShellSettings = openSettings; +- return () => { +- if (openShellSettings === openSettings) openShellSettings = null; +- }; +- }, [openSettings]); +- (0, import_react8.useEffect)(() => { +- if (!openOnboarding) return void 0; +- openShellOnboarding = openOnboarding; +- return () => { +- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; +- }; +- }, [openOnboarding]); +- (0, import_react8.useEffect)(() => { +- 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; +- setOpen(false); +function AgentSessionRow({ session, agent, actions, renaming, index }) { + const input = (0, import_react18.useRef)(null); + (0, import_react18.useLayoutEffect)(() => { @@ -12107,7 +13122,9 @@ diff --git a/client.js b/client.js + actions.reportError(error, "agent-rename"); + } + })(); -+ }; + }; +- const onKey = (event) => { +- if (event.key === "Escape") { + return (0, import_react18.createElement)( + "div", + { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, @@ -12123,41 +13140,7 @@ diff --git a/client.js b/client.js + onBlur: () => commit(true) + }) + ); - } -- return next === void 0 ? void 0 : turns[next].first; --} --function openingTurn(turns, firstTurns, hiddenTurns) { -- if (!turns || !firstTurns) return null; -- const start = nextTurnStart(turns, -Infinity); -- if (start === void 0) return null; -- const turn = Number(Object.keys(turns).filter((key) => turns[key].first === start)[0]); -- const listed = firstTurns.split(",").map(Number); -- const earlier = listed.filter((entry) => entry < turn); -- return listed.includes(turn) && earlier.every((entry) => hiddenTurns?.includes(entry)) ? { turn, start } : null; --} --var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; --function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { -- const turn = location2.turn; -- const time = location2.end.time; -- (0, import_react6.useEffect)(() => { -- actions.noteTurnTime(sessionId, turn, "end", time); -- }, [sessionId, turn, time]); -- const label = useTurnClock((state) => { -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start - time >= SEPARATOR_GAP_MS ? clockLabel(start) : ""; -- }); -- const visit = useUi((state) => state.visits?.[sessionId]); -- const fresh = useTurnClock((state) => { -- if (typeof visit?.seen !== "number" || time > visit.seen) return false; -- const start = nextTurnStart(state[sessionId] ?? {}, turn); -- return start !== void 0 && start > visit.seen && start <= visit.entered; -- }); -- if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -- null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++ } + return (0, import_react18.createElement)( + "button", + { @@ -12168,56 +13151,16 @@ diff --git a/client.js b/client.js + onClick: () => actions.openAgentSession(session.id), + onContextMenu: (event) => { + event.preventDefault(); -+ event.stopPropagation(); + event.stopPropagation(); +- setOpen(false); +- button.current?.focus(); + actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); -+ } + } + }, + (0, import_react18.createElement)("span", { className: "bt-agent-sub-title" }, session.title), + (0, import_react18.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") - ); - } --function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); --} --var PHRASES = { -- thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -- working: ["On it", "Busy with it", "Moving ahead", "Taking care of it", "Making headway"] --}; --var TOOL_PHRASES = [ -- [/web_?search|search_?web/i, "Searching online"], -- [/fetch|browse|browser|web/i, "Reading a page"], -- [/shell|bash|pwsh|powershell|exec|command|terminal/i, "Running shell commands"], -- [/^(read|view|cat)\b|read_/i, "Opening a file"], -- [/grep|glob|list_dir|^ls$|find|search/i, "Looking through files"], -- [/write|edit|patch|apply|replace/i, "Writing changes"], -- [/image|photo|draw/i, "Working on an image"] --]; --var GROUP_PHRASES = { create_group: "Creating a group", update_group: "Updating the group", delete_group: "Deleting the group", post_to_group: "Posting in the group" }; --function toolActivity(name, arg, findBot, self) { -- switch (name) { -- case "ask_user": -- return null; -- case "request_secret": -- return null; -- case "list_secrets": -- return { label: t("Checking the keys"), state: "searching" }; -- case "ask_user_question": -- return { label: t("Waiting for your answer"), state: "alert" }; -- case "group_relay": -- return { relay: true, state: "orbit" }; -- case "message_bot": { -- const target = findBot(arg("to")); -- return { label: target ? t("Asking {name}", { name: target.name }) : t("Asking another Bot"), state: "sending" }; -- } -- case "stop_bot": { -- const target = findBot(arg("bot")); -- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; -- } -- case "create_bot": -- return { label: t("Creating {name}", { name: arg("name") ?? t("a Bot") }), state: "sending" }; -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return { label: t("Updating {name}", { name: target?.name ?? t("a Bot") }), state: "sending" }; ++ ); ++} +function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { + const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { + const row = list.byId[id]; @@ -12228,7 +13171,34 @@ diff --git a/client.js b/client.js + at: row?.updatedAt ?? 0, + running: row?.running === true, + active: (row?.retainedBy?.mainView ?? 0) > 0 -+ }; + }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [open]); +- const toggle = () => { +- const rect = button.current?.getBoundingClientRect(); +- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); +- setOpen((value) => !value); +- }; +- const groups = [ +- [ +- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], +- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], +- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], +- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] +- ], +- [ +- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] +- ] +- ]; +- return (0, import_react8.createElement)( +- import_react8.Fragment, +- null, +- (0, import_react8.createElement)( + }).sort((a, b) => b.at - a.at))); + const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ + id, @@ -12254,7 +13224,9 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-agent" }, + (0, import_react18.createElement)( -+ "button", + "button", +- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, +- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) + { + type: "button", + className: "bt-row", @@ -12276,9 +13248,26 @@ diff --git a/client.js b/client.js + preview ? (0, import_react18.createElement)("span", { className: "bt-row-preview" }, preview) : null + ) : null, + wide && unread && !active ? (0, import_react18.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ ), + ), +- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( + (0, import_react18.createElement)( -+ "div", + "div", +- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, +- groups.map((group, index) => (0, import_react8.createElement)( +- import_react8.Fragment, +- { key: index }, +- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, +- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { +- key: label, +- type: "button", +- role: "menuitem", +- onClick: () => { +- setOpen(false); +- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); +- } +- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) +- )) +- ), document.body) : null + { className: "bt-agent-sub", "data-open": open || void 0 }, + (0, import_react18.createElement)( + "div", @@ -12301,11 +13290,29 @@ diff --git a/client.js b/client.js + })) + ) + ) -+ ); -+} -+var GLIDE = springEasing(1e3, 63); -+function useListGlide(listRef, layout, key) { -+ const tops = (0, import_react18.useRef)({ layout, map: /* @__PURE__ */ new Map() }); + ); + } +-function useLandOnMainBot(roster, useSessions, actions) { +- const mainView = useSessions((list) => { +- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); +- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; +- }); +- const lastBot = (0, import_react8.useRef)(null); +- (0, import_react8.useEffect)(() => { +- if (!roster.ready || !roster.mainBotId) return void 0; +- const [id, blank3] = mainView.split("|"); +- const bot = id === "" ? void 0 : roster.byId[id]; +- if (bot !== void 0) { +- lastBot.current = { botId: bot.id, sessionId: id }; +- if (currentPart(bot) !== id) actions.landOn(bot.id); +- return void 0; +- } +- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { +- lastBot.current = null; +- return void 0; ++var GLIDE = springEasing(1e3, 63); ++function useListGlide(listRef, layout, key) { ++ const tops = (0, import_react18.useRef)({ layout, map: /* @__PURE__ */ new Map() }); + (0, import_react18.useLayoutEffect)(() => { + const list = listRef.current; + if (!list) return; @@ -12320,29 +13327,134 @@ diff --git a/client.js b/client.js + row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); + } } -- case "list_bots": -- return { label: t("Checking the team"), state: "searching" }; -- case "read_group_chat": -- return { label: t("Reading {name}", { name: arg("group") ?? t("a group chat") }), state: "searching" }; -- case "read_own_chat": -- return { label: t("Looking back through the chat"), state: "searching" }; -- case "remember": -- return { label: t("Saving to memory"), state: "working" }; -- case "forget": -- return { label: t("Updating memory"), state: "working" }; -- case "recall": -- return { label: t("Checking memory"), state: "searching" }; -- default: -- if (GROUP_PHRASES[name]) return { label: t(GROUP_PHRASES[name]), state: "sending" }; -- const phrase = TOOL_PHRASES.find(([pattern]) => pattern.test(name))?.[1]; -- return { label: phrase ? t(phrase) : null, state: toolState(name) }; -- } +- const last = lastBot.current; +- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); +- return () => clearTimeout(timer); +- }, [mainView, roster]); +-} +-function FooterAccount(props) { +- const seated = props.useUi((value) => value.accountSeated === true); +- if (seated) return null; +- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); +-} +-function BotReturn({ wide, useSurface, actions }) { +- const mode = useSurface((value) => value.mode); +- if (mode !== "agent") return null; +- return (0, import_react8.createElement)( +- "button", +- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, +- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), +- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null +- ); +-} +-var RETURN_CSS = ` +-.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} +-.bt-return:hover{background:rgba(127,127,127,.14)} +-.bt-return:active{transform:scale(.96)} +-.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} +-.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} +-/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ +-.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} +-.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-`; +-function BrandMark({ size = 24 }) { +- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); +-} +-function BrandName() { +- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); +-} +-var SIDEBAR_CSS = ` +-.bt-side-top .bt-brand{cursor:default;padding-left:6px} +-.bt-side-top .bt-brand:hover{background:none} +-.bt-brand .bt-mark{flex:none} +-.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} +-.bt-rail .bt-row{position:relative} +-.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} +-.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +-[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} +-.bt-foot-account{order:-1;flex:none;display:inline-flex} +-[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} +-[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} +- +-/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a +- loop while the install runs. */ +-.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-round.bt-update svg{width:17px;height:17px} +-.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} +-@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} +-.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} +-@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} +-.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} +-.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} +-.bt-update-title{font-size:14px;line-height:20px;font-weight:600} +-.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} +-.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} +-.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} +-.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} +-.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} +-.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} +-.bt-update-go:hover:not(:disabled){opacity:.9} +-.bt-update-go:active:not(:disabled){transform:scale(.96)} +-.bt-update-go:disabled{opacity:.6;cursor:default} +-.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} +-@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} +- +-/* The DSH Agent row and its expanding Session list. The black whale is the +- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ +-.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} +-@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} +-.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} +-.bt-agent-sub[data-open]{grid-template-rows:1fr} +-.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} +-.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} +-.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} +-.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} +-.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} +-.bt-agent-new{color:var(--bt-ink-3)} +-.bt-agent-new svg{width:14px;height:14px;flex:none} +-.bt-agent-edit{padding:3px 10px 3px 4px} +-.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} +-.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} +-@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} +-@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} +-`; +- +-// src/client/header.js +-var import_react9 = require("react"); +-var import_react_dom4 = require("react-dom"); +-var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); +-var MEMORY_NOTICE_MS = 3200; +-var noNews = (select) => select({}); +-function useMemoryNotice(ids, useMemoryNews) { +- const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); +- const [shown, setShown] = (0, import_react9.useState)(false); +- (0, import_react9.useEffect)(() => { +- const left = seen + MEMORY_NOTICE_MS - Date.now(); +- setShown(seen > 0 && left > 0); +- if (seen === 0 || left <= 0) return void 0; +- const timer = setTimeout(() => setShown(false), left); +- return () => clearTimeout(timer); +- }, [seen]); +- return shown; + tops.current = { layout, map: next }; + }, [layout, key]); } --function workPhrase(tool) { -- if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +-function MemoryNotice({ shown }) { +- const inner = (0, import_react9.useRef)(null); +- const [width, setWidth] = (0, import_react9.useState)(0); +- const label = t("Memory updated"); +- (0, import_react9.useLayoutEffect)(() => { +- if (shown) setWidth(inner.current?.scrollWidth ?? 0); +- }, [label, shown]); +- return (0, import_react9.createElement)( +- "span", +- { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, +- (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) +var NodeActions = class extends import_react18.Component { + constructor(props) { + super(props); @@ -12376,26 +13488,62 @@ diff --git a/client.js b/client.js + typeof actions === "function" ? (0, import_react18.createElement)(NodeActions, { component: actions, bot }) : null + ), + closed ? null : children.map((child) => (0, import_react18.createElement)(TreeNode, { key: child.bot.id, node: child, rowProps, markedUnread, collapsed, toggle, actions })) -+ ); + ); } --function useLiveNote(actions, sessionId, id, entry) { -- const key = entry ? JSON.stringify(entry) : ""; -- (0, import_react6.useEffect)(() => { -- if (!key) return void 0; -- actions.noteLive(sessionId, id, JSON.parse(key)); -- return () => actions.noteLive(sessionId, id, null); -- }, [sessionId, id, key]); +-function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { +function TeamSidebar(props) { + const { wide, useRoster, useUi, useSessions, actions } = props; + const listRef = (0, import_react18.useRef)(null); -+ const roster = useRoster((value) => value); + const roster = useRoster((value) => value); +- const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); +- const firstTurns = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.slice(0, 8).map((entry) => entry.turn).join(",") : ""; +- }); +- const hiddenTurns = roster.exchangeTurns?.[sessionId]; +- const visit = useUi((state) => state.visits?.[sessionId]); +- const opening = useTurnClock((state) => { +- const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); +- return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; + const markedUnread = useUi((value) => value.unread); + props.useRegistry((value) => value); + const updated = useSessions((list) => { + const stamps = {}; + for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; + return JSON.stringify(stamps); -+ }); + }); +- (0, import_react9.useEffect)(() => { +- 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"); +- }, [opening]); +- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); +- const owner = roster.byId[sessionId]?.id ?? sessionId; +- const doing = useActivity((map) => map[owner]); +- const bot = roster.byId[sessionId]; +- const room2 = roster.roomsById[sessionId]; +- const detailsOpen = useUi((state) => state.details === sessionId); +- const name = bot?.name ?? room2?.name ?? ""; +- const useNews = useMemoryNews ?? noNews; +- const memoryIds = bot ? [bot.id] : room2?.members ?? []; +- const notice = useMemoryNotice(memoryIds, useNews); +- const changed = useNews((map) => latestMemoryChange(map, memoryIds)); +- (0, import_react9.useEffect)(() => { +- document.body.style.setProperty("--bt-placeholder", JSON.stringify(name ? t("Message {name}", { name }) : t("Message"))); +- }, [name, t("Message")]); +- (0, import_react9.useEffect)(() => { +- actions.beginVisit(sessionId); +- return () => actions.endVisit(sessionId); +- }, [sessionId]); +- (0, import_react9.useEffect)(() => { +- if (room2) document.body.dataset.btRoom = ""; +- else delete document.body.dataset.btRoom; +- return () => { +- delete document.body.dataset.btRoom; +- }; +- }, [Boolean(room2)]); +- const memoryNotice = notice && changed !== null && roster.memory !== false; +- return (0, import_react9.createElement)( + const order = (0, import_react18.useMemo)(() => { + const stamps = JSON.parse(updated); + const entries2 = [ @@ -12411,6 +13559,13 @@ diff --git a/client.js b/client.js + }, [roster, updated]); + const main = roster.byId[roster.mainBotId]; + const readOnlySaid = typeof roster.readOnly === "string" && roster.readOnly.trim() !== ""; ++ const readOnlyReload = (0, import_react18.createElement)("button", { ++ type: "button", ++ className: "bt-read-only-reload", ++ onClick: () => { ++ void Promise.resolve(actions.reloadTeam?.()).catch((error) => actions.reportError(error, "reload")); ++ } ++ }, t("Reload")); + const structure = useSidebarStructure(); + const [collapsed, setCollapsed] = (0, import_react18.useState)({}); + const toggle = (id) => setCollapsed((value) => ({ ...value, [id]: !value[id] })); @@ -12427,7 +13582,22 @@ diff --git a/client.js b/client.js + actions.openPalette(event.key); + }; + return (0, import_react18.createElement)( -+ "div", + "div", +- { className: "bt-head" }, +- (0, import_react9.createElement)( +- "button", +- { +- type: "button", +- className: "bt-pill", +- "aria-expanded": detailsOpen, +- "data-memory": memoryNotice ? "updated" : void 0, +- "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) +- }, +- bot ? (0, import_react9.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, +- (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), +- (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), +- (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) + { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, + wide ? (0, import_react18.createElement)( + "div", @@ -12441,9 +13611,14 @@ diff --git a/client.js b/client.js + null, + (0, import_react18.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), + (0, import_react18.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react18.createElement)(PlusIcon)) -+ ), + ), +- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), +- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), +- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) + wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, ++ wide && readOnlySaid ? readOnlyReload : null, + !wide && readOnlySaid ? (0, import_react18.createElement)("div", { className: "bt-hint bt-read-only", role: "status", title: hostText(roster.readOnly), "aria-label": hostText(roster.readOnly) }, hostText(roster.readOnly)) : null, ++ !wide && readOnlySaid ? readOnlyReload : null, + (0, import_react18.createElement)(InboxRow, { wide, useRoster, useWork: props.useWork, useUi, actions }), + (0, import_react18.createElement)(KanbanRow, { wide, useUi, actions }), + (0, import_react18.createElement)(ProjectsNav, { wide }), @@ -12454,23 +13629,43 @@ diff --git a/client.js b/client.js + order.filter((entry) => !(tree && inTree(entry)) && !(inTree(entry) && structure?.include?.(entry.item.id) === false)).map((entry) => entry.kind === "bot" ? (0, import_react18.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react18.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react18.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })), + wide ? (0, import_react18.createElement)(FlyBotSections, { roster, actions, renderBot: (bot) => (0, import_react18.createElement)(BotRow, { key: bot.id, ...rowProps, bot, asRow: true, marked: markedUnread[bot.id] === true }) }) : null + ) -+ ); + ); } --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { -- if (done) return void 0; -- let stop = false; -- let timer; -- let seen = ""; -- const tick = async () => { -- const result = await actions.roomProgress(sessionId); -- if (stop) return; -- const entry = (result ?? []).find((item) => item.callId === callId) ?? (result ?? [])[0]; -- const key = entry ? JSON.stringify([entry.replies, entry.speaking]) : ""; -- if (entry && key !== seen) { -- seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); +-function useTranscriptView() { +- const [view, setView] = (0, import_react9.useState)(null); +- (0, import_react9.useEffect)(() => { +- let frame = 0; +- let watched = null; +- const observer = new MutationObserver(() => schedule()); +- const measure = () => { +- frame = 0; +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (scroller !== watched) { +- observer.disconnect(); +- watched = scroller; +- if (scroller) observer.observe(scroller, { childList: true, subtree: true }); +- } +- if (!scroller) { +- setView(null); +- return; +- } +- const box = scroller.getBoundingClientRect(); +- const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; +- const sep = scroller.querySelector(".bt-new-sep"); +- let divider = null; +- if (sep) { +- const at = sep.getBoundingClientRect(); +- divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; +- } +- const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; +- const next = { +- unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, +- divider, +- center: Math.round(box.left + box.width / 2), +- top: Math.round(headerBottom + 8), +- bottom: Math.round(window.innerHeight - box.bottom + composer + 8) +- }; +- setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); +function UpdateButton({ useUpdate, actions }) { + const update = useUpdate((value) => value); + const [open, setOpen] = (0, import_react18.useState)(false); @@ -12483,74 +13678,31 @@ diff --git a/client.js b/client.js + const onDown = (event) => { + if (event.target instanceof Element && (event.target.closest(".bt-update-pop") || button.current?.contains(event.target))) return; + setOpen(false); -+ }; + }; +- const schedule = () => { +- if (!frame) frame = requestAnimationFrame(measure); + const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); + setOpen(false); + button.current?.focus(); - } -- timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); ++ } }; -- void tick(); +- document.addEventListener("scroll", schedule, true); +- window.addEventListener("resize", schedule); +- schedule(); + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); return () => { -- stop = true; -- clearTimeout(timer); +- cancelAnimationFrame(frame); +- observer.disconnect(); +- document.removeEventListener("scroll", schedule, true); +- window.removeEventListener("resize", schedule); + window.removeEventListener("mousedown", onDown); + window.removeEventListener("keydown", onKey, true); }; -- }, [done, sessionId, callId]); -- let replies = live.replies; -- if (done) { -- try { -- replies = JSON.parse(contentText(content)).replies ?? []; -- } catch { -- replies = []; -- } -- } -- const pending = done || !live.speaking ? [] : [live.speaking]; -- 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]; -- return (0, import_react6.createElement)( -- "div", -- { className: "bt-group-replies" }, -- replies.map((reply, index) => { -- const bot = roster.byId[reply.botId] ?? { id: reply.botId, name: reply.name, color: reply.color ?? "gray" }; -- if (reply.kind === "user") { -- return (0, import_react6.createElement)("div", { key: `user-${index}`, className: "bt-urow" }, (0, import_react6.createElement)( -- "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); -- } -- if (reply.kind === "failed") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, -- t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) -- ); -- } -- if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( -- "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- reply.text ? t("{name} looked up its own chat for “{text}”", { name: bot.name, text: reply.text }) : t("{name} looked up its own chat", { name: bot.name }) -- ); -- } -- if (reply.kind === "missed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} didn't answer in time", { name: bot.name })); -- } -- if (reply.kind === "resumed") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart and picked its work back up", { name: bot.name })); -- } -- if (reply.kind === "cut") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("{name} was cut off by a restart again and stopped", { name: bot.name })); -- } -- if (reply.kind === "approval" || reply.kind === "working") { -- return (0, import_react6.createElement)( +- }, []); +- return view; + }, [open]); + const phase = update?.phase ?? "idle"; + const dsh = update?.dsh; @@ -12599,25 +13751,7 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using FlyBot core {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), + message ? (0, import_react18.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, + (0, import_react18.createElement)( - "div", -- { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( -- "button", -- { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), -- (0, import_react6.createElement)("span", { className: "bt-event-name" }, t(reply.kind === "working" ? "{name} started on it" : "{name} paused for your approval", { name: bot.name })) -- ) -- ); -- } -- if (reply.kind === "stopped") { -- return (0, import_react6.createElement)("div", { key: `${reply.botId}-${index}`, className: "bt-event" }, t("You stopped {name}'s work", { name: bot.name })); -- } -- return (0, import_react6.createElement)(GroupMessage, { key: `${reply.botId}-${index}`, bot, text: reply.text, room: room2, roster, actions, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); -- }), -- pending.map((id) => { -- const bot = roster.byId[id]; -- if (!bot) return null; -- return (0, import_react6.createElement)( ++ "div", + { className: "bt-update-actions" }, + update.notesUrl ? (0, import_react18.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, + phase === "installed" ? null : (0, import_react18.createElement)("button", { @@ -12629,12 +13763,7 @@ diff --git a/client.js b/client.js + ) + ) : null, + dshRow ? (0, import_react18.createElement)( - "div", -- { key: `pending-${id}`, className: "bt-activity" }, -- (0, import_react6.createElement)(BotMark, { bot, size: 22, state: speakerTool ? toolState(speakerTool) : "thinking" }), -- (0, import_react6.createElement)(Shimmer, null, speakerTool ? `${bot.name} · ${workPhrase(speakerTool)}…` : t("{name} is typing…", { name: bot.name })) -- ); -- }) ++ "div", + { className: "bt-update-dsh" }, + (0, import_react18.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), + (0, import_react18.createElement)("div", { className: "bt-update-text" }, dshText), @@ -12652,22 +13781,19 @@ diff --git a/client.js b/client.js + ) + ) : null + ), document.body) : null - ); ++ ); } --var DRAWN = /* @__PURE__ */ new Set(["message_bot", "stop_bot", "create_bot", "update_bot", "group_relay", "create_group", "update_group", "delete_group", "post_to_group", "ask_user", "request_secret", "ask_user_question", "read_group_chat", "remember", "forget"]); --var CARDS = /* @__PURE__ */ new Set(["ask_user", "request_secret"]); --function callTree(root) { -- const calls = []; -- const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; -- if (seen.has(id)) return; -- seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); -- }; -- visit(root); -- return calls; +-function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +- const replies = useSessions((list) => { +- const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; +- return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; +- }); +- const visit = useUi((state) => state.visits?.[sessionId]); +- const unread = useTurnClock((state) => { +- if (typeof visit?.seen !== "number") return 0; +- let count = 0; +- for (const entry of Object.values(state[sessionId] ?? {})) { +- if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; +function ConnectApps({ wide, actions }) { + if (!wide) return null; + return (0, import_react18.createElement)( @@ -12680,15 +13806,7 @@ diff --git a/client.js b/client.js + t("Connect apps") + ) + ); - } --function useShownCards(actions, sessionId, callIds) { -- const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { -- const ids = JSON.parse(key); -- if (ids.length === 0 || !actions.noteCards) return void 0; -- actions.noteCards(sessionId, ids, 1); -- return () => actions.noteCards(sessionId, ids, -1); -- }, [sessionId, key]); ++} +function YouAvatar() { + return (0, import_react18.createElement)( + "span", @@ -12696,76 +13814,7 @@ diff --git a/client.js b/client.js + (0, import_react18.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react18.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), + (0, import_react18.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) + ); - } --function renderCall(call2, { sessionId, roster, actions, useUi, useWork, findBot }) { -- const tool = toolName(call2); -- const done = call2.kind === "tool-result"; -- const { text: arg, value: argValue } = toolArgs(call2); -- const self = roster.byId[sessionId]; -- switch (tool) { -- case "message_bot": { -- const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Messaged"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "stop_bot": { -- const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; -- if (done && call2.isError) return (0, import_react6.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); -- return (0, import_react6.createElement)(EventLine, { prefix: t("Stopped"), bots: [target], onOpen: (bot) => bot.id && actions.openExchange(self?.id ?? sessionId, bot.id), whole: true }); -- } -- case "create_bot": { -- const created = findBot(arg("name")); -- if (!done || created === void 0) return null; -- return (0, import_react6.createElement)(EventLine, { prefix: t("Created"), bots: [created], onOpen: (bot) => actions.openSession(bot.id) }); -- } -- case "update_bot": { -- const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); -- return done && target ? (0, import_react6.createElement)(EventLine, { prefix: t("Updated"), bots: [target], onOpen: (bot) => actions.openSession(bot.id) }) : null; -- } -- case "group_relay": -- return (0, import_react6.createElement)(GroupReplies, { sessionId, callId: call2.callId, done, content: call2.content, roster, actions, useUi, useWork }); -- case "create_group": -- case "update_group": -- case "delete_group": -- case "post_to_group": { -- const verbs = { -- create_group: ["Created group", "Couldn't create the group"], -- update_group: ["Updated group", "Couldn't update the group"], -- delete_group: ["Deleted group", "Couldn't delete the group"], -- post_to_group: ["Posted in", "Couldn't post in the group"] -- }[tool]; -- if (!done) return null; -- const result = contentText(call2.content); -- const named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); -- if (call2.isError || !named) return (0, import_react6.createElement)("div", { className: "bt-event", title: result }, t(verbs[1])); -- const name = named[2]; -- const room2 = roster.rooms.find((entry) => entry.name === name); -- return (0, import_react6.createElement)(GroupEvent, { prefix: t(verbs[0]), room: tool === "delete_group" ? void 0 : room2, name, roster, actions }); -- } -- case "ask_user": { -- const question = roster.questions?.[sessionId]; -- if (!question || question.callId !== call2.callId) return (0, import_react6.createElement)("span", { className: "bt-q-gone", hidden: true }); -- return (0, import_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: true }); -- } -- case "request_secret": -- return (0, import_react6.createElement)(SecretToolCell, { sessionId, callId: call2.callId, name: arg("name"), done, content: call2.content, roster, actions }); -- case "ask_user_question": { -- if (!done) return null; -- let parsed = null; -- try { -- parsed = JSON.parse(contentText(call2.content)); -- } catch { -- parsed = null; -- } -- const answer = parsed?.answers?.[0]; -- const question = argValue("questions")?.[0]; -- if (!answer) return null; -- return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, (0, import_react6.createElement)(SettledQuestion, { question, answer })); -- } -- case "read_group_chat": { -- if (!done) return null; -- const room2 = roster.rooms.find((entry) => entry.name === arg("group")); -- return room2 ? (0, import_react6.createElement)(GroupEvent, { prefix: t("Read"), room: room2, roster, actions }) : null; ++} +var openShellSettings = null; +var openShellOnboarding = null; +var shellShortcuts = null; @@ -12781,49 +13830,53 @@ diff --git a/client.js b/client.js + openShellSettings(); + return; } -- case "remember": -- case "forget": -- return done && !call2.isError ? (0, import_react6.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call2.content), roster, actions, findBot }) : null; -- default: -- return null; -- } +- return count; + const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); + if (desktop) openThroughMenu(desktop); + else pressSettingsShortcut(); -+ }); - } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { -- const roster = useRoster((value) => value); -- const root = node.data.root; -- const turn = node.location?.turn?.turn ?? root.turn; -- 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); -- const drawn = calls.filter((call2) => DRAWN.has(toolName(call2))); -- const running = root.kind === "tool-result" ? void 0 : calls.findLast((call2) => call2.kind !== "tool-result"); -- const note = running ? toolActivity(toolName(running), toolArgs(running).text, findBot, roster.byId[sessionId]) : null; -- useLiveNote(actions, sessionId, `tool:${root.callId ?? node.key}`, note); -- useShownCards(actions, sessionId, drawn.filter((call2) => CARDS.has(toolName(call2)) && call2.callId).map((call2) => call2.callId)); -- if (drawn.length === 0) return null; -- const env = { sessionId, roster, actions, useUi, useWork, findBot }; -- return (0, import_react6.createElement)(import_react6.Fragment, null, drawn.map((call2, index) => (0, import_react6.createElement)(import_react6.Fragment, { key: call2.callId ?? index }, renderCall(call2, env)))); + }); +- const view = useTranscriptView(); +- const unpinned = view?.unpinned === true; +- const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); +- const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false }); +- (0, import_react9.useEffect)(() => { +- setRelease({ base: unpinned ? replies : null, dismissed: false }); +- }, [unpinned]); +- (0, import_react9.useEffect)(() => { +- if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); +- }, [view?.divider]); +- const fresh = release.base === null ? 0 : replies - release.base; +- const showDown = unpinned && fresh > 0 && !release.dismissed; +- const showUp = view?.divider === "above" && !above.seen && !above.dismissed; +- (0, import_react9.useEffect)(() => { +- const scroller = document.querySelector("[data-conversation-scroll]"); +- if (!scroller) return void 0; +- scroller.toggleAttribute("data-bt-news", showDown); +- return () => scroller.removeAttribute("data-bt-news"); +- }, [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"); +- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( +- "div", +- { className: "bt-news", "data-direction": direction, style }, +- (0, import_react9.createElement)( +- "button", +- { type: "button", className: "bt-news-jump", onClick: onJump }, +- (0, import_react9.createElement)("span", { className: "bt-news-ico" }, (0, import_react9.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), +- label(count) +- ), +- (0, import_react9.createElement)("button", { +- type: "button", +- className: "bt-news-x", +- "aria-label": t("Dismiss new messages"), +- onClick: (event) => { ++} +function openHarnessOnboarding(id) { + requestAnimationFrame(() => { + if (openShellOnboarding) openShellOnboarding(id); + else openHarnessSettings(); + }); - } -- --// src/client/activity-row.js --var import_react7 = require("react"); --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))]); --function elapsedLabel(ms) { -- const minutes = Math.floor(ms / 6e4); -- if (minutes < 60) return `${Math.max(1, minutes)}m`; -- const hours = Math.floor(minutes / 60); -- return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++} +var MODELS_STEP_ID = "flybot-models"; +var MODELS_STEP_ORDER = 1e3; +function ModelsSettingsStep({ explicit, complete, openSection }) { @@ -12832,19 +13885,7 @@ diff --git a/client.js b/client.js + complete(); + }, []); + return null; - } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { -- if (!active) { -- setHost(null); -- return void 0; -- } -- let frame = 0; -- const find = () => { -- frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); -- setHost((prev) => prev === node ? prev : node); ++} +function openHarnessModels() { + openHarnessOnboarding(MODELS_STEP_ID); +} @@ -12893,91 +13934,55 @@ diff --git a/client.js b/client.js + openShellSettings = openSettings; + return () => { + if (openShellSettings === openSettings) openShellSettings = null; - }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(find); -- }); -- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); -- find(); ++ }; + }, [openSettings]); + (0, import_react18.useEffect)(() => { + if (!openOnboarding) return void 0; + openShellOnboarding = openOnboarding; - return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); ++ return () => { + if (openShellOnboarding === openOnboarding) openShellOnboarding = null; - }; -- }, [active]); -- return host; --} --function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); -- check.current = replacing; -- (0, import_react7.useEffect)(() => { -- const seen = /* @__PURE__ */ new WeakSet(); -- const started = Date.now(); -- let column = null; -- let frame = 0; -- const settle = (event) => { -- if (event.target !== event.currentTarget || !/^bt-row/.test(event.animationName)) return; -- delete event.currentTarget.dataset.btEnter; -- event.currentTarget.removeEventListener("animationend", settle); ++ }; + }, [openOnboarding]); + (0, import_react18.useEffect)(() => { + 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; + setOpen(false); - }; -- const scan = () => { -- frame = 0; -- const next = document.querySelector("[data-chat-flow]"); -- if (next !== column) { -- observer.disconnect(); -- column = next; -- if (column) observer.observe(column, { childList: true, subtree: true }); -- } -- if (!column) return; -- const items = [...column.querySelectorAll(":scope > [data-chat-flow-key]")]; -- let last = -1; -- items.forEach((item, index) => { -- if (seen.has(item)) last = index; -- }); -- const fresh = items.filter((item) => !seen.has(item) && item.offsetHeight > 0); -- for (const item of fresh) seen.add(item); -- if (last < 0 || Date.now() - started < 800 || fresh.length > 3) return; -- for (const item of fresh) { -- if (items.indexOf(item) < last) continue; -- const reply = [void 0, "assistant-step", "tool-call"].includes(item.dataset.chatFlowKind); -- item.dataset.btEnter = reply && check.current() ? "after-collapse" : "new"; -- item.addEventListener("animationend", settle); ++ }; + const onKey = (event) => { + if (event.key === "Escape") { -+ event.stopPropagation(); + event.stopPropagation(); +- onDismiss(); + setOpen(false); + button.current?.focus(); } - }; -- const observer = new MutationObserver(() => { -- if (!frame) frame = requestAnimationFrame(scan); -- }); -- scan(); +- }, (0, import_react9.createElement)(CloseIcon)) +- ); +- const toLatest = () => { +- const scroller = document.querySelector("[data-conversation-scroll]"); +- const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); +- if (back) back.click(); +- else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); ++ }; + window.addEventListener("mousedown", onDown); + window.addEventListener("keydown", onKey, true); - return () => { -- cancelAnimationFrame(frame); -- observer.disconnect(); ++ return () => { + window.removeEventListener("mousedown", onDown); + window.removeEventListener("keydown", onKey, true); - }; -- }, []); ++ }; + }, [open]); + const toggle = () => { + const rect = button.current?.getBoundingClientRect(); + if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); + setOpen((value) => !value); -+ }; + }; +- const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); +- return (0, import_react_dom4.createPortal)( +- (0, import_react9.createElement)( +- import_react9.Fragment, +- 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 + const groups = [ + [ + [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -12997,7 +14002,8 @@ diff --git a/client.js b/client.js + "button", + { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, + (0, import_react18.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react18.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) -+ ), + ), +- document.body + open && box ? (0, import_react_dom4.createPortal)((0, import_react18.createElement)( + "div", + { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, @@ -13017,35 +14023,31 @@ diff --git a/client.js b/client.js + }, (0, import_react18.createElement)(Icon), (0, import_react18.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) + )) + ), document.body) : null -+ ); + ); } --function ActivityIndicator({ sessionId, useRoster, useSessionStatus, useLive, useSession, actions }) { -- const roster = useRoster((value) => value); -- const running = useSessionStatus((map) => map.get(sessionId)?.running === true); -- const liveKey = useLive((state) => JSON.stringify(state[sessionId] ?? {})); -- 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]; -- const entries2 = (0, import_react7.useMemo)(() => Object.values(JSON.parse(liveKey)), [liveKey]); -- const streaming = entries2.some((entry) => entry.text); -- const tools = entries2.filter((entry) => !entry.text).sort((a, b) => a.at - b.at); -- const relaying = tools.some((entry) => entry.relay); -- const visible = running && !streaming && !relaying && (bot !== void 0 || room2 !== void 0); -- const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); -- const inputAt = Math.max(inputRef.current, pendingInput); -- inputRef.current = inputAt; -- const want = !visible ? null : inputAt > (tool?.at ?? 0) ? { key: `input:${inputAt}`, pool: PHRASES.thinking, state: "thinking" } : tool ? { key: tool.label ? `tool:${tool.label}` : "working", label: tool.label, pool: PHRASES.working, state: tool.state ?? "working" } : { key: "thinking", pool: PHRASES.thinking, state: "thinking" }; -- const [shown, setShown] = (0, import_react7.useState)(null); -- const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); -- if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { -- 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; -- if (wait <= 0) { -- make(); +- +-// src/client/composer.js +-var import_react10 = require("react"); +-var import_react_dom5 = require("react-dom"); +-var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; +-var keepComposerFocus = (event) => event.preventDefault(); +-function useDictation(onText) { +- const [listening, setListening] = (0, import_react10.useState)(false); +- const [note, setNote] = (0, import_react10.useState)(""); +- const recognition = (0, import_react10.useRef)(null); +- const latest = (0, import_react10.useRef)(onText); +- latest.current = onText; +- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); +- (0, import_react10.useEffect)(() => { +- if (!note) return void 0; +- const timer = setTimeout(() => setNote(""), 2600); +- return () => clearTimeout(timer); +- }, [note]); +- const start = ({ continuous = true, onEnd } = {}) => { +- const Api = speechApi(); +- if (!Api) { +- setNote(t("Dictation is not available in this browser")); +- return false; +function useLandOnMainBot(roster, useSessions, actions) { + const mainView = useSessions((list) => { + const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); @@ -13059,215 +14061,119 @@ diff --git a/client.js b/client.js + if (bot !== void 0) { + lastBot.current = { botId: bot.id, sessionId: id }; + if (currentPart(bot) !== id) actions.landOn(bot.id); - return void 0; ++ return void 0; } -- const timer = setTimeout(make, wait); -- return () => clearTimeout(timer); -- }, [wantKey, shown]); -- const [phase, setPhase] = (0, import_react7.useState)(null); -- const endedAt = (0, import_react7.useRef)(0); -- const shift = (0, import_react7.useRef)(null); -- const leaving = (0, import_react7.useRef)(false); -- leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { -- if (visible) { -- setPhase("in"); +- const engine = new Api(); +- engine.lang = navigator.language || "zh-CN"; +- engine.interimResults = true; +- engine.continuous = continuous; +- engine.onresult = (event) => { +- let interim = ""; +- for (let index = event.resultIndex; index < event.results.length; index += 1) { +- const result = event.results[index]; +- const text = result[0]?.transcript ?? ""; +- if (result.isFinal) { +- if (text.trim()) latest.current(text, true); +- } else interim += text; +- } +- if (interim) latest.current(interim, false); +- }; +- engine.onerror = (event) => { +- if (event.error === "aborted" || event.error === "no-speech") return; +- setNote(event.error === "not-allowed" || event.error === "service-not-allowed" ? t("Microphone access is blocked") : t("Couldn't hear that")); +- }; +- engine.onend = () => { +- setListening(false); +- recognition.current = null; +- onEnd?.(); +- }; +- recognition.current = engine; +- try { +- engine.start(); +- setListening(true); +- return true; +- } catch { +- setNote(t("Couldn't start dictation")); +- return false; + if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { + lastBot.current = null; - return void 0; ++ return void 0; } -- setPhase((current) => current === "in" ? "out" : current); -- const timer = setTimeout(() => { -- shift.current = document.querySelector("[data-conversation-scroll]")?.scrollTop ?? null; -- setPhase((current) => current === "out" ? null : current); -- }, 140); +- }; +- const stop = () => recognition.current?.stop(); +- return { listening, note, start, stop }; + const last = lastBot.current; + const timer = setTimeout(() => actions.landOn(roster.mainBotId, last, { stay: id === "", view: id }), last ? 0 : 400); - return () => clearTimeout(timer); -- }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { -- if (phase !== null) return; -- endedAt.current = Date.now(); -- setShown(null); -- const before = shift.current; -- shift.current = null; -- const scroller = document.querySelector("[data-conversation-scroll]"); -- if (before === null || !scroller || reducedMotion()) return; -- 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]); -- (0, import_react7.useEffect)(() => { -- if (!running) actions.clearLive(sessionId); -- }, [running, sessionId]); -- useRowEntrances(() => leaving.current || Date.now() - endedAt.current < 300); -- const [now, setNow] = (0, import_react7.useState)(Date.now); -- (0, import_react7.useEffect)(() => { -- if (phase !== "in") return void 0; -- const timer = setInterval(() => setNow(Date.now()), 1e4); -- return () => clearInterval(timer); -- }, [phase]); -- const host = useRunningHost(running); -- if (!host || phase === null || !shown) return null; -- const elapsed = now - shown.at >= 6e4 ? elapsedLabel(now - shown.at) : null; -- return (0, import_react_dom2.createPortal)((0, import_react7.createElement)( -- "div", -- { className: "bt-typing", "data-exiting": phase === "out" ? "" : void 0, "aria-hidden": true }, -- bot ? (0, import_react7.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react7.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), -- (0, import_react7.createElement)( -- "span", -- { className: "bt-typing-text" }, -- (0, import_react7.createElement)( -- "span", -- { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, -- (0, import_react7.createElement)(Shimmer, null, `${shown.text}…`), -- elapsed ? (0, import_react7.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null -- ) -- ) -- ), host); --} -- --// src/client/dsh-offer.js --var APP_OFFER = /* @__PURE__ */ new Set(["available", "downloading", "verifying", "installing", "ready"]); --var bare = (value) => String(value ?? "").trim().replace(/^v/i, "").split("+")[0]; --var sameVersion = (a, b) => bare(a) !== "" && bare(a) === bare(b); --function showsDshRow(dsh, app) { -- if (!dsh?.verified || !dsh.current || sameVersion(dsh.verified, dsh.current)) return false; -- if (dsh.kind === "npm") return true; -- return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ return () => clearTimeout(timer); + }, [mainView, roster]); } +-function ComposerVoice({ sessionId, inputActions, actions, useUi }) { +- const dictation = useDictation((text, final) => { +- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); +- }); +- const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); +- (0, import_react10.useEffect)(() => { +- if (!quote || !inputActions) return; +- actions.clearQuote(quote.token); +- const excerpt = quote.text.replace(/\s+/g, " ").trim(); +- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} - --// src/client/sidebar.js --var import_react8 = require("react"); --var import_react_dom3 = require("react-dom"); --var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); --function useMarkedUnread(id, active, marked, actions) { -- (0, import_react8.useEffect)(() => { -- if (active && marked) actions.clearUnread(id); -- }, [active, marked]); +-`; +- inputActions.insertText(line, inputActions.captureInsertion()); +- const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); +- if (box instanceof HTMLElement) { +- box.focus(); +- const selection = window.getSelection(); +- selection?.selectAllChildren(box); +- selection?.collapseToEnd(); +- } +- }, [quote?.token]); +- if (sessionId === void 0) return null; +- const pluginMic = actions.speech() !== void 0; +- return (0, import_react10.createElement)( +- import_react10.Fragment, +- null, +- pluginMic ? null : (0, import_react10.createElement)("button", { +- type: "button", +- className: "bt-mic", +- "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), +- "aria-pressed": dictation.listening, +- title: dictation.listening ? t("Stop dictation") : t("Dictate"), +- onMouseDown: keepComposerFocus, +- onClick: () => dictation.listening ? dictation.stop() : dictation.start() +- }, (0, import_react10.createElement)(MicIcon)), +- (0, import_react10.createElement)("button", { +- type: "button", +- className: "bt-voice", +- "aria-label": t("Start voice chat"), +- title: t("Voice chat"), +- onMouseDown: keepComposerFocus, +- onClick: () => actions.openVoice(sessionId) +- }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), +- dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null +function FooterAccount(props) { + const seated = props.useUi((value) => value.accountSeated === true); + if (seated) return null; + return (0, import_react18.createElement)("span", { className: "bt-foot-account" }, (0, import_react18.createElement)(AccountLauncher, { ...props, seat: false })); ++} ++function BotReturn({ wide, useSurface, actions }) { ++ const mode = useSurface((value) => value.mode); ++ if (mode !== "agent") return null; ++ return (0, import_react18.createElement)( ++ "button", ++ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, ++ (0, import_react18.createElement)(FlyBotMark, { size: 20 }), ++ wide ? (0, import_react18.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null + ); } --var PinMark = () => (0, import_react8.createElement)("span", { className: "bt-pin", "aria-hidden": true }, (0, import_react8.createElement)(PinMarkIcon)); --var useApproval = (useWork, match) => useWork ? useWork((work) => (work.approvals ?? []).some(match)) : false; --var useGroupWork = (useWork, pick2) => useWork ? useWork((work) => pick2(work.rooms ?? {}).join(",")) : ""; --var workKeys = (text) => text === "" ? [] : text.split(",").map((key) => { -- const [id, tool] = key.split("|"); -- return { id, tool: tool || void 0 }; --}); --var workLine = (lead, tool, more) => (0, import_react8.createElement)(Shimmer, null, `${lead} · ${workPhrase(tool)}…${more > 0 ? ` +${more}` : ""}`); --function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessionRetainInfo, useActivity, useWork, wide, marked }) { -- const id = bot.id; -- const part = currentPart(bot); -- const preview = useSessions((list) => previewOf(list, part, roster.exchangeTurns[part])); -- 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); -- const tile = roster.mainBotId === id; -- const approval = useApproval(useWork, (entry) => entry.botId === id); -- const alert = roster.questions?.[part] !== void 0 || approval; -- const doing = useActivity((map) => map[id]); -- const groups = workKeys(useGroupWork(useWork, (rooms) => Object.entries(rooms).flatMap(([roomId, entries2]) => entries2.filter((entry) => entry.botId === id).map((entry) => `${roomId}|${entry.tool ?? ""}`)))); -- const away = !running && groups.length > 0; -- const state = botState(running || away, doing, alert); -- const awayLine = away ? workLine(roster.roomsById[groups[0].id]?.name ?? t("a group chat"), groups[0].tool, groups.length - 1) : null; -- if (bot.hidden && !active && !main) return null; -- const onContextMenu = (event) => { -- event.preventDefault(); -- actions.openMenu({ kind: "bot", id, x: event.clientX, y: event.clientY }); +-function menuIcons(roster) { +- const marks = /* @__PURE__ */ new Map(); +- return (row) => { +- if (!row.bot) return PeopleIcon; +- const id = row.bot.id; +- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); +- return marks.get(id); - }; -- 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) { -- return (0, import_react8.createElement)("div", { className: "bt-tile-wrap" }, (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-tile", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-bot-id": id, -- title: preview || bot.name, -- onClick: () => actions.openSession(id), -- onContextMenu -- }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 60, state, main: true }), -- (0, import_react8.createElement)("span", { className: "bt-tile-name" }, bot.name), -- bot.role ? (0, import_react8.createElement)("span", { className: "bt-chip" }, bot.role) : null -- )); -- } -- return (0, import_react8.createElement)( -+function BotReturn({ wide, useSurface, actions }) { -+ const mode = useSurface((value) => value.mode); -+ if (mode !== "agent") return null; -+ return (0, import_react18.createElement)( - "button", -- { -- type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-bot-id": id, -- title: wide ? void 0 : bot.name, -- onClick: () => actions.openSession(id), -- onContextMenu -- }, -- (0, import_react8.createElement)(BotAvatar, { bot, size: 36, state, main }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( -- "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, bot.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- awayLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, awayLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -+ { type: "button", className: "bt-return", "aria-label": t("FlyBot"), title: t("FlyBot"), onClick: () => actions.setSurface("bot") }, -+ (0, import_react18.createElement)(FlyBotMark, { size: 20 }), -+ wide ? (0, import_react18.createElement)("span", { className: "bt-return-name" }, t("FlyBot")) : null - ); - } --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 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); -- const approval = useApproval(useWork, (entry) => entry.roomId === id); -- const members = workKeys(useGroupWork(useWork, (rooms) => (rooms[id] ?? []).map((entry) => `${entry.botId}|${entry.tool ?? ""}`))); -- const first = roster.byId[members[0]?.id]; -- const workingLine = first ? workLine(first.name, members[0].tool, members.length - 1) : null; -- 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(", "); -- return (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-room-id": id, -- onClick: () => actions.openSession(id), -- onContextMenu: (event) => { -- event.preventDefault(); -- actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); -- } -- }, -- (0, import_react8.createElement)(RoomAvatar, { room: room2, roster, size: 36 }), -- pinned && !wide ? (0, import_react8.createElement)(PinMark) : null, -- wide ? (0, import_react8.createElement)( -- "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, (0, import_react8.createElement)("span", { className: "bt-row-title" }, room2.name), pinned ? (0, import_react8.createElement)(PinMark) : null), -- workingLine ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, workingLine) : preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && approval && !active ? (0, import_react8.createElement)("span", { className: "bt-unread bt-unread-wait", "aria-hidden": true }) : wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -- ); +var RETURN_CSS = ` +.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} +.bt-return:hover{background:rgba(127,127,127,.14)} @@ -13293,6 +14199,9 @@ diff --git a/client.js b/client.js +.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} +.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} +.bt-rail .bt-read-only{width:100%;margin:4px 0 6px;padding:4px 2px;font-size:10px;line-height:1.3;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} ++.bt-read-only-reload{margin:0 8px 8px;padding:3px 10px;border:1px solid var(--bt-line);border-radius:8px;background:none;color:inherit;font:inherit;font-size:12px;line-height:18px;cursor:pointer;transition:background-color .12s ease} ++.bt-read-only-reload:hover{background:var(--bt-hover)} ++.bt-rail .bt-read-only-reload{width:100%;margin:0 0 6px;padding:2px 0;font-size:10px;line-height:16px;text-align:center} +[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} +[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} +.bt-foot-account{order:-1;flex:none;display:inline-flex} @@ -13363,76 +14272,26 @@ diff --git a/client.js b/client.js +function busyAckPending(hostAck) { + return busyAckText(hostAck) === ""; } --function AgentSessionRow({ session, agent, actions, renaming, index }) { -- const input = (0, import_react8.useRef)(null); -- (0, import_react8.useLayoutEffect)(() => { -- if (!renaming) return; -- input.current?.focus(); -- input.current?.select(); -- }, [renaming]); -- if (renaming) { -- const commit = (keep) => { -- const value = input.current?.value ?? ""; -- if (!keep || value.trim() === "" || value.trim() === session.title) { -- actions.endAgentRename(); -- return; -- } -- void (async () => { -- try { -- await actions.renameAgentSession(agent.id, session.id, value); -- actions.endAgentRename(); -- } catch (error) { -- actions.reportError(error, "agent-rename"); -- } -- })(); +-function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { +- const roster = useRoster((value) => value); +- const [open, setOpen] = (0, import_react10.useState)(null); +- const [shown, leaving] = useLinger(open, 120); +- const button = (0, import_react10.useRef)(null); +- const menu = (0, import_react10.useRef)(null); +- (0, import_react10.useEffect)(() => { +- if (!open) return void 0; +- const onDown = (event) => { +- if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); - }; -- return (0, import_react8.createElement)( -- "div", -- { className: "bt-agent-sub-row bt-agent-edit", style: { "--i": index } }, -- (0, import_react8.createElement)("input", { -- ref: input, -- defaultValue: session.title, -- "aria-label": t("Rename"), -- onKeyDown: (event) => { -- if (event.nativeEvent.isComposing) return; -- if (event.key === "Enter") commit(true); -- else if (event.key === "Escape") commit(false); -- }, -- onBlur: () => commit(true) -- }) -- ); -- } -- return (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-agent-sub-row", -- "aria-current": session.active ? "page" : void 0, -- style: { "--i": index }, -- onClick: () => actions.openAgentSession(session.id), -- onContextMenu: (event) => { -- event.preventDefault(); +- const onKey = (event) => { +- if (event.key === "Escape") { - event.stopPropagation(); -- actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); +- setOpen(null); - } -- }, -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, session.title), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-time" }, session.at > 0 ? relativeShort(session.at) : "") -- ); +function pendingBusyPlacement(submissions) { + if (!Array.isArray(submissions)) return false; + return submissions.some((item) => item?.placement === "queued" || item?.placement === "steering"); - } --function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -- const info = useSessions((list) => JSON.stringify(agent.sessions.map((id) => { -- const row = list.byId[id]; -- return { -- id, -- // A blank Session's title is the workspace folder name; show the label instead. -- title: row?.blank === true ? "" : row?.displayTitle ?? row?.title ?? "", -- at: row?.updatedAt ?? 0, -- running: row?.running === true, -- active: (row?.retainedBy?.mainView ?? 0) > 0 ++} +function busyAckVisible({ hostAck, pendingBusy, streamingText }) { + if (streamingText) return false; + return busyAckText(hostAck) !== "" || pendingBusy === true; @@ -13464,37 +14323,29 @@ diff --git a/client.js b/client.js + const node = document.querySelector(RUNNING_HOST); + setHost((prev) => prev === node ? prev : node); }; -- }).sort((a, b) => b.at - a.at))); -- const statusInfo = useSessionStatus((map) => JSON.stringify(agent.sessions.map((id) => ({ -- id, -- running: map.get(id)?.running === true, -- unread: map.get(id)?.completionUnread === true -- })))); -- const statuses = Object.fromEntries(JSON.parse(statusInfo).map((entry) => [entry.id, entry])); -- const sessions = JSON.parse(info).map((session) => ({ -- ...session, -- running: statuses[session.id]?.running ?? session.running, -- unread: statuses[session.id]?.unread ?? false, -- title: session.title === "" ? t("New session") : session.title -- })); -- const renaming = useUi((value) => value.renameAgent); -- const active = sessions.some((session) => session.active); -- const running = sessions.some((session) => session.running); -- const unread = sessions.some((session) => session.unread) || marked && !active; -- useMarkedUnread(agent.id, active, marked, actions); -- 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; -- return (0, import_react8.createElement)( +- window.addEventListener("mousedown", onDown, true); +- window.addEventListener("keydown", onKey, true); + const observer = new MutationObserver(() => { + if (!frame) frame = requestAnimationFrame(find); + }); + observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); + find(); -+ return () => { + return () => { +- window.removeEventListener("mousedown", onDown, true); +- window.removeEventListener("keydown", onKey, true); + cancelAnimationFrame(frame); + observer.disconnect(); -+ }; + }; +- }, [open]); +- const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; +- if (sessionId === void 0 || target === void 0) return null; +- const toggle = () => { +- if (open) { +- setOpen(null); +- return; +- } +- const rect = button.current.getBoundingClientRect(); +- setOpen({ left: rect.left, bottom: window.innerHeight - rect.top + 8 }); + }, [active]); + return host; +} @@ -13514,33 +14365,7 @@ diff --git a/client.js b/client.js + const text2 = shown.current?.text ?? ""; + if (!visible || !host || !shown.current || !bot) return null; + return (0, import_react_dom5.createPortal)((0, import_react19.createElement)( - "div", -- { className: "bt-agent" }, -- (0, import_react8.createElement)( -- "button", -- { -- type: "button", -- className: "bt-row", -- "aria-label": label, -- "aria-current": active ? "page" : void 0, -- "data-agent-id": agent.id, -- title: wide ? void 0 : "DSH Agent", -- onClick: () => actions.openAgent(agent.id), -- onContextMenu: (event) => { -- event.preventDefault(); -- actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); -- } -- }, -- (0, import_react8.createElement)("span", { className: `bt-agent-ava${running ? " bt-run" : ""}` }, (0, import_react8.createElement)(import_dsh_client_ui_primitives2.FishLogo, { size: 22 })), -- wide ? (0, import_react8.createElement)( -- "span", -- { className: "bt-row-body" }, -- (0, import_react8.createElement)("span", { className: "bt-row-name" }, "DSH Agent"), -- preview ? (0, import_react8.createElement)("span", { className: "bt-row-preview" }, preview) : null -- ) : null, -- wide && unread && !active ? (0, import_react8.createElement)("span", { className: "bt-unread", "aria-hidden": true }) : null -- ), -- (0, import_react8.createElement)( ++ "div", + { + className: "bt-busy-ack", + "data-flybot": "busy-ack", @@ -13551,29 +14376,10 @@ diff --git a/client.js b/client.js + }, + (0, import_react19.createElement)(BotMark, { bot, size: 28, state: "thinking", live: true }), + (0, import_react19.createElement)( - "div", -- { className: "bt-agent-sub", "data-open": open || void 0 }, -- (0, import_react8.createElement)( ++ "div", + { className: "bt-astack" }, + (0, import_react19.createElement)( - "div", -- { className: "bt-agent-sub-in" }, -- (0, import_react8.createElement)( -- "button", -- { type: "button", className: "bt-agent-sub-row bt-agent-new", style: { "--i": 0 }, onClick: () => { -- void actions.newAgentSession(agent.id).catch((error) => actions.reportError(error, "agent-session")); -- } }, -- (0, import_react8.createElement)(PlusIcon), -- (0, import_react8.createElement)("span", { className: "bt-agent-sub-title" }, t("New session")) -- ), -- sessions.map((session, index) => (0, import_react8.createElement)(AgentSessionRow, { -- key: session.id, -- session, -- agent, -- actions, -- index: index + 1, -- renaming: renaming === session.id -- })) ++ "div", + { className: "bt-msg" }, + (0, import_react19.createElement)( + "div", @@ -13586,27 +14392,10 @@ diff --git a/client.js b/client.js + (0, import_react19.createElement)("span", null) + ) + ) - ) - ) -- ); ++ ) ++ ) + ), host); - } --var GLIDE = springEasing(1e3, 63); --function useListGlide(listRef, layout, key) { -- const tops = (0, import_react8.useRef)({ layout, map: /* @__PURE__ */ new Map() }); -- (0, import_react8.useLayoutEffect)(() => { -- const list = listRef.current; -- if (!list) return; -- const next = /* @__PURE__ */ new Map(); -- const still = reducedMotion() || tops.current.layout !== layout; -- for (const row of list.querySelectorAll("[data-bot-id],[data-room-id],[data-agent-id]")) { -- const id = row.dataset.botId ?? row.dataset.roomId ?? row.dataset.agentId; -- const top = row.offsetTop; -- next.set(id, top); -- const before = tops.current.map.get(id); -- if (!still && before !== void 0 && Math.abs(before - top) > 0.5) { -- row.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], GLIDE); -- } ++} +var BUSY_ACK_CSS = ` +.bt-busy-ack{display:flex;align-items:flex-end;gap:8px;width:100%;margin-top:var(--dsh-chat-flow-gap,12px);margin-bottom:4px} +.bt-busy-ack .bt-astack{align-items:flex-start} @@ -13777,7 +14566,10 @@ diff --git a/client.js b/client.js + title: text(file.title), + sha256: text(file.sha256), + shortHash: text(file.sha256).slice(0, 12) -+ }; + }; +- const attach = () => { +- setOpen(null); +- button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); +}; +var meetingOf = (meeting, status) => { + if (meeting === null || typeof meeting !== "object") return null; @@ -13801,7 +14593,10 @@ diff --git a/client.js b/client.js + // #1861: the host's participants with who each position speaks for, so the row can say a + // Project Chief or an agent held it for someone rather than for themselves. + participants: Array.isArray(meeting.participants) ? meeting.participants.map((participant) => ({ name: text(participant?.name), speaksFor: text(participant?.speaksFor) || null })).filter((participant) => participant.name !== "") : [] -+ }; + }; +- const schedule = () => { +- setOpen(null); +- actions.openSettings("schedules", target); +}; +var listOf = (value) => Array.isArray(value) ? value.map((item) => decisionOf(item)).filter((item) => item !== null) : []; +function projectBoardRecord({ decisions, meetings } = {}) { @@ -13834,41 +14629,12 @@ diff --git a/client.js b/client.js + pendingHidden: hiddenOf(pendingTotal, pending.length), + empty: decided.length === 0 && pending.length === 0, + emptyText: text(meetings?.reason) || "No meeting of this Project is recorded." - } -- tops.current = { layout, map: next }; -- }, [layout, key]); -+ }; - } --function TeamSidebar(props) { -- const { wide, useRoster, useUi, useSessions, actions } = props; -- const listRef = (0, import_react8.useRef)(null); -- const roster = useRoster((value) => value); -- const markedUnread = useUi((value) => value.unread); -- props.useRegistry((value) => value); -- const updated = useSessions((list) => { -- const stamps = {}; -- for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; -- return JSON.stringify(stamps); -- }); -- const order = (0, import_react8.useMemo)(() => { -- const stamps = JSON.parse(updated); -- const entries2 = [ -- ...roster.bots.filter((bot) => bot.id !== roster.mainBotId).map((bot) => ({ kind: "bot", item: bot, at: stamps[currentPart(bot)] ?? bot.createdAt })), -- ...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 })) -- ]; -- 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]; -- 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 }; -- const typeToSearch = (event) => { -- if (event.defaultPrevented || event.key.length !== 1 || event.key === " " || event.metaKey || event.ctrlKey || event.altKey || event.nativeEvent.isComposing) return; -- if (event.target instanceof Element && event.target.closest("input,textarea,[contenteditable]")) return; -- event.preventDefault(); -- actions.openPalette(event.key); ++ } + }; +- const reference = () => { +- setOpen(null); +- actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); ++} +function projectBoardView({ project, rows, done, canManage, tasks, decisions, meetings, queues, metrics } = {}) { + const isRow = (row) => row !== null && typeof row === "object" && text(row.id) !== ""; + const byId = new Map((Array.isArray(tasks) ? tasks : []).filter((task) => task !== null && typeof task === "object" && text(task.id) !== "").map((task) => [text(task.id), task])); @@ -13902,7 +14668,35 @@ diff --git a/client.js b/client.js + board: boardOfRow(row, task) + }; }; -- return (0, import_react8.createElement)( +- const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); +- return (0, import_react10.createElement)( +- import_react10.Fragment, +- null, +- (0, import_react10.createElement)("button", { +- ref: button, +- type: "button", +- className: "bt-plus", +- "aria-label": t("Add"), +- title: t("Add"), +- "aria-haspopup": "menu", +- "aria-expanded": Boolean(open), +- onMouseDown: keepComposerFocus, +- onClick: toggle +- }, (0, import_react10.createElement)(PlusIcon)), +- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( +- "div", +- { +- ref: menu, +- className: "bt-menu bt-menu-up bt-plus-menu", +- role: "menu", +- "aria-label": t("Add"), +- style: { left: shown.left, bottom: shown.bottom }, +- "data-leaving": leaving || void 0 +- }, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), +- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, +- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) +- ), document.body) : null + const groups = PROJECT_SOURCES.map((source) => ({ + source, + label: SOURCE_LABELS[source], @@ -13944,20 +14738,7 @@ diff --git a/client.js b/client.js +function projectRowNode(row, onOpen) { + const open = row.board === null ? void 0 : () => onOpen?.(row.board); + return (0, import_react20.createElement)( - "div", -- { className: wide ? "bt-side" : "bt-side bt-rail", onKeyDown: typeToSearch }, -- wide ? (0, import_react8.createElement)( -- "div", -- { className: "bt-side-top" }, -- (0, import_react8.createElement)("span", { className: "bt-brand" }, (0, import_react8.createElement)(BrandMark, { size: 22 }), (0, import_react8.createElement)(BrandName)), -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("Search"), title: t("Search"), onClick: () => actions.openPalette() }, (0, import_react8.createElement)(SearchIcon)), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) -- ) : (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)(UpdateButton, { useUpdate: props.useUpdate, actions }), -- (0, import_react8.createElement)("button", { type: "button", className: "bt-round", "aria-label": t("New…"), title: t("New…"), onClick: () => actions.openNewChat("new") }, (0, import_react8.createElement)(PlusIcon)) ++ "div", + { + key: row.key, + className: "bt-project-row", @@ -13979,14 +14760,7 @@ diff --git a/client.js b/client.js + // The audit text the row already carries (#1730): shown inside the card detail, never a + // second read. A row without one renders exactly as before. + row.audit === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-audit" }, row.audit) - ), -- wide && roster.readOnly ? (0, import_react8.createElement)("div", { className: "bt-hint bt-read-only", role: "status" }, hostText(roster.readOnly)) : null, -- (0, import_react8.createElement)( -- "div", -- { ref: listRef, className: "bt-list", role: "region", "aria-label": t("Bot list") }, -- main ? (0, import_react8.createElement)(BotRow, { ...rowProps, bot: main, marked: markedUnread[main.id] === true }) : roster.ready ? null : (0, import_react8.createElement)("div", { className: "bt-hint" }, t("Setting up your Main Bot…")), -- order.map((entry) => entry.kind === "bot" ? (0, import_react8.createElement)(BotRow, { key: entry.item.id, ...rowProps, bot: entry.item, marked: markedUnread[entry.item.id] === true }) : entry.kind === "room" ? (0, import_react8.createElement)(RoomRow, { key: entry.item.id, ...rowProps, room: entry.item, marked: markedUnread[entry.item.id] === true }) : (0, import_react8.createElement)(AgentRow, { key: entry.item.id, ...rowProps, agent: entry.item, marked: markedUnread[entry.item.id] === true })) -- ) ++ ), + // "Who is doing" is the claim, never the assignee label; a card the Assistant speaks for + // says so, so the three identities (Bot, Assistant, user) never read as one. + row.holder === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-holder", "data-holder": row.holder }, `@${row.holder}`), @@ -13995,31 +14769,30 @@ diff --git a/client.js b/client.js + row.runId === null ? null : (0, import_react20.createElement)("span", { className: "bt-project-ref" }, row.nodeId === null ? row.runId : `${row.runId} · ${row.nodeId}`) ); } --function UpdateButton({ useUpdate, actions }) { -- const update = useUpdate((value) => value); -- const [open, setOpen] = (0, import_react8.useState)(false); -- const [box, setBox] = (0, import_react8.useState)(null); -- const [error, setError] = (0, import_react8.useState)(""); -- const [dshError, setDshError] = (0, import_react8.useState)(""); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- 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; -- setOpen(false); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setOpen(false); -- button.current?.focus(); -- } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); +- +-// src/client/at-menu.js +-var BOTS_SOURCE = "ds-bot"; +-var CHATS_SOURCE = "ds-bot-chats"; +-var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; +-var OTHER_CHATS_MAX = 8; +-var conversationOf = (roster, sessionId) => { +- if (!roster.ready) return null; +- const room2 = roster.roomsById[sessionId]; +- if (room2) return { room: room2 }; +- const self = roster.byId[sessionId]; +- return self ? { self } : null; +-}; +-function mentionRows(roster, sessionId, query = "") { +- const here = conversationOf(roster, sessionId); +- if (here === null) return null; +- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); +- const needle = query.trim().toLowerCase(); +- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); +- const everyone = t("Everyone"); +- if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { +- rows.push({ room: here.room, text: `@${everyone} ` }); +- } +- return rows; +function ProjectBoardRows({ view, onOpen }) { + if (view.empty) return (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t(view.emptyText)); + return view.groups.map((group) => (0, import_react20.createElement)( @@ -14028,7 +14801,14 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)("h4", null, t(group.label)), + group.rows.map((row) => projectRowNode(row, onOpen)) + )); -+} + } +-function chatRows(roster, list, sessionId) { +- const here = conversationOf(roster, sessionId); +- if (here === null) return []; +- const byId = list?.byId ?? {}; +- const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { +- const id = currentPart(bot); +- return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; +function ProjectDoneGroup({ view, onOpen }) { + if (view.done.rows.length === 0) return null; + return (0, import_react20.createElement)( @@ -14082,27 +14862,14 @@ diff --git a/client.js b/client.js + lastActivityAt: Number.isFinite(row.lastActivityAt) ? row.lastActivityAt : null, + overdue: Number.isFinite(row.sinceMs) && row.sinceMs > RUNNING_OVERDUE_MS, + board: boardOfRow({ source: text(row.source) }, task) - }; -- }, [open]); -- const phase = update?.phase ?? "idle"; -- const dsh = update?.dsh; -- const dshPhase = dsh?.phase ?? "idle"; -- const dshRow = showsDshRow(dsh, update?.app); -- if (!update?.available && !dshRow && phase === "idle" && dshPhase === "idle") return null; -- const cloudPhase = phase === "installing" || dshPhase === "installing" ? "installing" : phase !== "idle" ? phase : dshPhase; -- const toggle = () => { -- const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)), top: rect.bottom + 6 }); -- setOpen((value) => !value); -- }; -- const start = () => { -- setError(""); -- void actions.installUpdate().catch((failure) => setError(hostText(failure?.message ?? String(failure)))); -- }; -- const startDsh = () => { -- setDshError(""); -- void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); -+ }); ++ }; + }); +- const team = /* @__PURE__ */ new Set([ +- ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), +- ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) +- ]); +- const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); +- return [...bots, ...others]; + return { + available: source?.ok === true, + reason: text(source?.reason) || null, @@ -14110,99 +14877,48 @@ diff --git a/client.js b/client.js + running, + empty: source === null, + emptyText: "No metrics read yet." - }; -- const label = phase === "installing" ? t("Updating DS Bot…") : dshPhase === "installing" ? t("Updating DSH…") : phase === "installed" ? t("DS Bot {version} is installed. Restart DSH to use it.", { version: update.installed ?? update.latest }) : dshPhase === "installed" ? t("Restart DSH to start using DSH {version}.", { version: dsh.installed ?? dsh.verified }) : update?.available ? t("DS Bot {version} is available", { version: update.latest }) : t("DSH {version} is available", { version: dsh?.verified }); -- 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 }); -- return (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)("button", { -- ref: button, -- type: "button", -- className: "bt-round bt-update", -- "data-phase": cloudPhase, -- "aria-label": label, -- title: label, -- "aria-haspopup": "dialog", -- "aria-expanded": open, -- onClick: toggle -- }, (0, import_react8.createElement)(CloudDownloadIcon, { done: phase === "installed" }), phase === "failed" || dshPhase === "failed" ? (0, import_react8.createElement)("span", { className: "bt-update-dot", "aria-hidden": true }) : null), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -+} ++ }; + } +-function sessionMention(sessionId, label) { +- let binary = ""; +- for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); +- const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +- return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; +function ProjectMetrics({ view }) { + return (0, import_react20.createElement)( + "section", + { className: "bt-project-metrics", "data-available": view.available ? "true" : "false" }, + view.available || view.reason === null ? null : (0, import_react20.createElement)("p", { className: "bt-project-metrics-note" }, t("Metrics unavailable") + ": " + view.reason), + (0, import_react20.createElement)( - "div", -- { className: "bt-update-pop", role: "dialog", "aria-label": update?.available || phase !== "idle" ? t("DS Bot update") : t("DSH update"), style: box }, -- update?.available || phase !== "idle" ? (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, phase === "installed" ? t("Update installed") : t("DS Bot {version}", { version: update.latest })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, phase === "installed" ? t("Restart DSH to start using DS Bot {version}.", { version: update.installed ?? update.latest }) : t("You have {version}.", { version: update.current })), -- message ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: message })) : null, -- (0, import_react8.createElement)( -- "div", -- { className: "bt-update-actions" }, -- update.notesUrl ? (0, import_react8.createElement)("a", { className: "bt-update-notes", href: update.notesUrl, target: "_blank", rel: "noopener noreferrer" }, t("What's new")) : null, -- phase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: phase === "installing", -- onClick: start -- }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) -- ) -- ) : null, -- dshRow ? (0, import_react8.createElement)( ++ "div", + { className: "bt-project-metric-cards" }, + view.cards.map((card) => (0, import_react20.createElement)( - "div", -- { className: "bt-update-dsh" }, -- (0, import_react8.createElement)("div", { className: "bt-update-title" }, t("DSH {version}", { version: dsh.verified })), -- (0, import_react8.createElement)("div", { className: "bt-update-text" }, dshText), -- dshMessage ? (0, import_react8.createElement)("div", { className: "bt-update-error", role: "alert" }, t("The update did not finish: {reason}", { reason: dshMessage })) : null, -- (0, import_react8.createElement)( -- "div", -- { className: "bt-update-actions" }, -- dsh.kind === "app" ? dsh.available ? (0, import_react8.createElement)("button", { type: "button", className: "bt-update-go", onClick: () => void actions.openAppUpdates(dsh.verified).catch(() => { -- }) }, t("Open updates")) : null : !dsh.available && dshPhase === "idle" ? null : dshPhase === "installed" ? null : (0, import_react8.createElement)("button", { -- type: "button", -- className: "bt-update-go", -- disabled: dshPhase === "installing", -- onClick: startDsh -- }, dshPhase === "installing" ? t("Updating…") : dshPhase === "failed" ? t("Try again") : t("Update")) -- ) -- ) : null -- ), document.body) : null ++ "div", + { key: card.key, className: "bt-project-metric", "data-metric": card.key }, + (0, import_react20.createElement)("span", { className: "bt-project-metric-value" }, card.value), + (0, import_react20.createElement)("span", { className: "bt-project-metric-label" }, t(card.label)), + (0, import_react20.createElement)("span", { className: "bt-project-metric-caption" }, t(card.caption)) + )) + ) - ); ++ ); } --function ConnectPlugins({ wide, actions }) { -- const tipId = (0, import_react8.useId)(); -- const [tip, setTip] = (0, import_react8.useState)(false); -- const timer = (0, import_react8.useRef)(0); -- const button = (0, import_react8.useRef)(null); -- const [box, setBox] = (0, import_react8.useState)(null); -- (0, import_react8.useEffect)(() => () => clearTimeout(timer.current), []); -- if (!wide) return null; -- const show = () => { -- clearTimeout(timer.current); -- timer.current = setTimeout(() => { -- const rect = button.current?.getBoundingClientRect(); -- if (!rect) return; -- const left = Math.max(8, Math.min(rect.left, window.innerWidth - 268)); -- setBox({ left, bottom: window.innerHeight - rect.top + 10 }); -- setTip(true); -- }, 250); +-function atSources({ roster, sessions, launched, iconFor }) { +- const bots = { +- trigger: "@", +- name: BOTS_SOURCE, +- order: -20, +- showGroupTitle: false, +- async candidates(session, { query }) { +- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ +- name: row.bot ? row.bot.name : t("Everyone"), +- ...row.bot?.role ? { description: row.bot.role } : {}, +- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, +- icon: iconFor(row), +- section: t("Bots"), +- value: row.text +- })); +- }, +- onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 +function ProjectRunningTable({ view, onOpen }) { + if (view.running.length === 0) return (0, import_react20.createElement)("section", { className: "bt-project-running", "data-running": "0" }, (0, import_react20.createElement)("h4", null, t("Running tasks")), (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t("Nothing is running."))); + return (0, import_react20.createElement)( @@ -14288,62 +15004,50 @@ diff --git a/client.js b/client.js + const settle = (answer) => setOpen((current) => current !== null && current.path === path ? { path, answer } : current); + call4("project-decision", { id: projectId, path }).then(settle, (error) => settle({ ok: false, reason: String(error?.message ?? error) })); }; -- const hide = () => { -- clearTimeout(timer.current); -- setTip(false); +- const chats = { +- trigger: "@", +- name: CHATS_SOURCE, +- order: -10, +- showGroupTitle: false, +- async candidates(session) { +- if (!launched(session.sessionId)) return []; +- return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ +- name: row.bot ? row.bot.name : row.label, +- ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, +- icon: row.bot ? iconFor(row) : "session", +- section: row.bot ? t("Bots") : t("Conversations"), +- value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) +- })); +- }, +- onPick({ candidate }) { +- const { sessionId, label } = JSON.parse(candidate.value); +- const mention = sessionMention(sessionId, label); +- return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; + const decisionBody = (file) => { + if (open === null || open.path !== file.path) return null; + const answer = open.answer; + if (answer === null) return (0, import_react20.createElement)("div", { className: "bt-project-decision-text", "data-open-decision": file.path }, t("Reading the decision…")); + if (answer.ok !== true) return (0, import_react20.createElement)("div", { className: "bt-project-decision-text bt-project-decision-refusal", "data-open-decision": file.path, "data-refused": "true" }, text(answer.reason) || t("not listed")); + return (0, import_react20.createElement)("pre", { className: "bt-project-decision-text", "data-open-decision": file.path }, typeof answer.text === "string" ? answer.text : ""); - }; -- return (0, import_react8.createElement)( -- "span", -- { className: "bt-connect-wrap" }, -- (0, import_react8.createElement)( ++ }; + const decisionRows = decisions.files.map((file) => (0, import_react20.createElement)( + "div", + { key: file.key, className: "bt-project-decision-wrap" }, + (0, import_react20.createElement)( - "button", - { -- ref: button, - type: "button", -- className: "bt-connect bt-connect-foot", -- "aria-describedby": tip ? tipId : void 0, -- onMouseEnter: show, -- onMouseLeave: hide, -- onFocus: show, -- onBlur: hide, -- onClick: () => actions.selectPanel("plugins") ++ "button", ++ { ++ type: "button", + className: "bt-project-decision", + "data-path": file.path, + "data-sha256": file.sha256, + "data-open": open !== null && open.path === file.path ? "true" : void 0, + "aria-expanded": open !== null && open.path === file.path ? "true" : "false", + onClick: () => toggle(file.path) - }, -- (0, import_react8.createElement)(PlugIcon), -- t("Connect plugins"), -- (0, import_react8.createElement)("span", { className: "bt-connect-warn", "aria-hidden": true }, "!") ++ }, + (0, import_react20.createElement)("span", { className: "bt-project-date" }, file.date), + (0, import_react20.createElement)("span", { className: "bt-project-title" }, file.title === "" ? file.name : file.title), + (0, import_react20.createElement)("span", { className: "bt-project-hash", title: file.sha256 }, file.shortHash) - ), -- tip && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( -- "span", -- { role: "tooltip", id: tipId, className: "bt-connect-tip", style: box }, -- t("Plugin compatibility is not fully verified.") -- ), document.body) : null -- ); --} --function YouAvatar() { -- return (0, import_react8.createElement)( -- "span", -- { className: "bt-you-wrap" }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })), -- (0, import_react8.createElement)("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)" } }, t("Settings")) ++ ), + decisionBody(file) + )); + const recipientText = (list) => list.length === 0 ? null : list.map((name) => `@${name}`).join(", "); @@ -14381,7 +15085,8 @@ diff --git a/client.js b/client.js + "data-decisions": String(decisions.count), + ...decisions.total === null ? {} : { "data-decisions-total": String(decisions.total) }, + "data-meetings": String(meetings.count) -+ }, + }, +- codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } + (0, import_react20.createElement)("h4", null, t("Decisions")), + decisions.empty ? (0, import_react20.createElement)("div", { className: "bt-project-empty bt-project-record-empty" }, t(decisions.emptyText)) : decisionRows, + moreLine(decisions.hidden, "decisions-more"), @@ -14391,15 +15096,8 @@ diff --git a/client.js b/client.js + meetings.pending.length === 0 ? null : (0, import_react20.createElement)("h5", null, t("Not decided")), + meetings.pending.length === 0 ? null : pendingRows, + moreLine(meetings.pendingHidden, "pending-more") - ); - } --var openShellSettings = null; --var openShellOnboarding = null; --var shellShortcuts = null; --function holdShellShortcuts(shortcuts) { -- shellShortcuts = shortcuts; -- return () => { -- if (shellShortcuts === shortcuts) shellShortcuts = null; ++ ); ++} +var botIdOf = (entry) => entry !== null && typeof entry === "object" ? text(entry.botId) : ""; +var idList = (value) => Array.isArray(value) ? value.map((id) => text(id)).filter((id) => id !== "") : []; +function projectTeamView(project) { @@ -14423,12 +15121,18 @@ diff --git a/client.js b/client.js + role: text(project?.role), + choices: idList(project?.choices) }; +- return [bots, chats]; } --function openHarnessSettings() { -- requestAnimationFrame(() => { -- if (openShellSettings) { -- openShellSettings(); -- return; +- +-// src/client/speech.js +-var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; +-var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); +-async function speechCatalog(speech) { +- const result = await speech.catalog(); +- if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); +- const catalog = result.value; +- const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); +- return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; +var teamRow = (role, label, botId, onOpenBot, onOpenDossier) => (0, import_react20.createElement)( + "div", + { @@ -14448,30 +15152,7 @@ diff --git a/client.js b/client.js + onOpenBot?.(botId); + } + } - } -- const desktop = document.querySelector('button[aria-haspopup="menu"][data-signed-out]'); -- if (desktop) openThroughMenu(desktop); -- else pressSettingsShortcut(); -- }); --} --function openHarnessOnboarding(id) { -- requestAnimationFrame(() => { -- if (openShellOnboarding) openShellOnboarding(id); -- else openHarnessSettings(); -- }); --} --function openThroughMenu(trigger) { -- const before = new Set(document.querySelectorAll('[role="menu"]')); -- trigger.click(); -- let frames = 0; -- const look = () => { -- const menu = [...document.querySelectorAll('[role="menu"]')].find((node) => !before.has(node)); -- const item = menu?.querySelector('button[role="menuitem"]'); -- if (item) item.click(); -- else if (++frames < 30) requestAnimationFrame(look); -- else pressSettingsShortcut(); -- }; -- requestAnimationFrame(look); ++ } + }, + (0, import_react20.createElement)("span", { key: "label", className: "bt-project-team-label" }, label), + botId === "" ? (0, import_react20.createElement)("span", { key: "value", className: "bt-project-missing" }, t("Not set")) : (0, import_react20.createElement)("span", { key: "value", className: "bt-project-team-bot" }, `@${botId}`), @@ -14536,20 +15217,29 @@ diff --git a/client.js b/client.js + ) + ); } --function pressSettingsShortcut() { -- const binding = shellShortcuts?.catalog.getSnapshot().find((row) => row.id === "settings.open")?.binding; -- if (!binding) return; -- const held = new Set(binding.modifiers); -- window.dispatchEvent(new KeyboardEvent("keydown", { -- code: binding.code, -- key: binding.code === "Comma" ? "," : "", -- bubbles: true, -- cancelable: true, -- ctrlKey: held.has("control"), -- altKey: held.has("alt"), -- shiftKey: held.has("shift"), -- metaKey: held.has("meta") -- })); +-function waveBytes(samples, rate = 16e3) { +- const bytes = new Uint8Array(44 + samples.length * 2); +- const view = new DataView(bytes.buffer); +- const ascii = (at, text) => { +- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); +- }; +- ascii(0, "RIFF"); +- view.setUint32(4, 36 + samples.length * 2, true); +- ascii(8, "WAVEfmt "); +- view.setUint32(16, 16, true); +- view.setUint16(20, 1, true); +- view.setUint16(22, 1, true); +- view.setUint32(24, rate, true); +- view.setUint32(28, rate * 2, true); +- view.setUint16(32, 2, true); +- view.setUint16(34, 16, true); +- ascii(36, "data"); +- view.setUint32(40, samples.length * 2, true); +- samples.forEach((sample, index) => { +- const value = Math.max(-1, Math.min(1, sample)); +- view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); +- }); +- return bytes; +function dossierRowOf(team, botId) { + const id = text(botId); + if (id === "") return null; @@ -14600,43 +15290,10 @@ diff --git a/client.js b/client.js + ) + ); } --function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { -- const [open, setOpen] = (0, import_react8.useState)(false); -- (0, import_react8.useLayoutEffect)(() => { -- if (!seat) return void 0; -- actions.seatAccount(true); -- return () => actions.seatAccount(false); -- }, [seat]); -- const [box, setBox] = (0, import_react8.useState)(null); -- const button = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (settingsOpen) setOpen(false); -- }, [settingsOpen]); -- (0, import_react8.useEffect)(() => { -- if (!openSettings) return void 0; -- openShellSettings = openSettings; -- return () => { -- if (openShellSettings === openSettings) openShellSettings = null; -- }; -- }, [openSettings]); -- (0, import_react8.useEffect)(() => { -- if (!openOnboarding) return void 0; -- openShellOnboarding = openOnboarding; -- return () => { -- if (openShellOnboarding === openOnboarding) openShellOnboarding = null; -- }; -- }, [openOnboarding]); -- (0, import_react8.useEffect)(() => { -- 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; -- setOpen(false); -- }; -- const onKey = (event) => { -- if (event.key === "Escape") { -- event.stopPropagation(); -- setOpen(false); -- button.current?.focus(); +-function base64Of(bytes) { +- let text = ""; +- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); +- return btoa(text); +function projectAgentFor(bot, selfId) { + if (bot === null || typeof bot !== "object" || bot.remote !== void 0) return null; + const projectId = text(bot.projectId); @@ -14657,41 +15314,11 @@ diff --git a/client.js b/client.js + target = botIdOf(record?.project?.assistant); + } catch { + target = ""; - } -- }; -- window.addEventListener("mousedown", onDown); -- window.addEventListener("keydown", onKey, true); -- return () => { -- window.removeEventListener("mousedown", onDown); -- window.removeEventListener("keydown", onKey, true); -- }; -- }, [open]); -- const toggle = () => { -- const rect = button.current?.getBoundingClientRect(); -- if (rect) setBox({ left: rect.left, bottom: window.innerHeight - rect.top + 5 }); -- setOpen((value) => !value); ++ } + if (target !== "") setAgentId(target); + } + if (target !== "") onOpen?.(target); - }; -- const groups = [ -- [ -- [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], -- [t("Connectors"), ConnectorIcon, () => actions.openConnectors()], -- [t("Automation tasks"), ClockIcon, () => actions.selectPanel("schedules")], -- [t("Bot settings"), GearIcon, () => actions.openSettings("general")] -- ], -- [ -- [t("Switch to classic Agent mode"), SwitchIcon, () => actions.setSurface("agent")] -- ] -- ]; -- return (0, import_react8.createElement)( -- import_react8.Fragment, -- null, -- (0, import_react8.createElement)( -- "button", -- { ref: button, type: "button", className: "bt-account", "aria-label": t("Account"), "aria-haspopup": "menu", "aria-expanded": open, onClick: toggle }, -- (0, import_react8.createElement)("span", { className: "bt-you", "aria-hidden": true }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 24 })) ++ }; + if (id === "") return null; + return (0, import_react20.createElement)("button", { + type: "button", @@ -14718,20 +15345,12 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)("span", null, [t("In flight"), ": ", (0, import_react20.createElement)("b", { key: "f" }, countText(view.counts.inflight))]), + (0, import_react20.createElement)("span", null, [t("Blocked"), ": ", (0, import_react20.createElement)("b", { key: "b" }, countText(view.counts.blocked))]), + (0, import_react20.createElement)("span", null, [t("Done"), ": ", (0, import_react20.createElement)("b", { key: "d" }, countText(view.counts.done))]) - ), -- open && box ? (0, import_react_dom3.createPortal)((0, import_react8.createElement)( ++ ), + // #1749: both buttons open a panel and neither is hidden from a non-owner. The host refuses + // every write a non-owner attempts, so the refusal is an answer the reader sees, not a + // control that silently disappeared. + (0, import_react20.createElement)( - "div", -- { className: "bt-menu bt-menu-up bt-account-menu", role: "menu", "aria-label": t("Account"), style: box }, -- groups.map((group, index) => (0, import_react8.createElement)( -- import_react8.Fragment, -- { key: index }, -- index > 0 ? (0, import_react8.createElement)("div", { className: "bt-menu-sep", role: "separator" }) : null, -- group.map(([label, Icon, run, hint]) => (0, import_react8.createElement)("button", { -- key: label, ++ "div", + { className: "bt-project-actions" }, + (0, import_react20.createElement)("button", { type: "button", "data-action": "project-settings", onClick: () => onSettings?.() }, t("Project settings")), + (0, import_react20.createElement)("button", { type: "button", "data-action": "project-members", onClick: () => onMembers?.() }, t("Members")) @@ -14743,7 +15362,26 @@ diff --git a/client.js b/client.js + (0, import_react20.createElement)(ProjectDoneGroup, { view, onOpen }), + (0, import_react20.createElement)(ProjectBoardRecord, { record: view.record, projectId: view.projectId, call: call4 }) + ); -+} + } +-var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); +-async function openCapture() { +- const devices = navigator.mediaDevices; +- if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); +- let stream; +- try { +- stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); +- } catch (error) { +- throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); +- } +- const audio = new AudioContext(); +- const analyser = audio.createAnalyser(); +- analyser.fftSize = 512; +- audio.createMediaStreamSource(stream).connect(analyser); +- const frame = new Float32Array(analyser.fftSize); +- const recorder = new MediaRecorder(stream); +- const chunks = []; +- recorder.ondataavailable = (event) => { +- if (event.data.size > 0) chunks.push(event.data); +function ProjectSettingsPanel({ project, assistantMine, call: request, onClose, onSaved } = {}) { + const save = request ?? call; + const [kind, setKind] = (0, import_react20.useState)("bot"); @@ -14765,7 +15403,16 @@ diff --git a/client.js b/client.js + setStatus(hostText(error?.message ?? String(error))); + return null; + } -+ }; + }; +- recorder.start(); +- let closed = false; +- const close = () => { +- if (closed) return; +- closed = true; +- if (recorder.state !== "inactive") recorder.stop(); +- stream.getTracks().forEach((track) => track.stop()); +- void audio.close().catch(() => { +- }); + return (0, import_react20.createElement)( + "div", + { className: "bt-project-panel", "data-panel": "project-settings" }, @@ -14811,20 +15458,16 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-project-row-actions" }, + (0, import_react20.createElement)("button", { - type: "button", -- role: "menuitem", ++ type: "button", + className: "bt-soft bt-soft-sm", + "data-action": "project-memory-save", + disabled: noteSaveDisabled, + // A disabled button draws no click, but the guard is what a keyboard or a stale tree + // could still reach: neither state sends a request or writes anything. - onClick: () => { -- setOpen(false); -- void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); ++ onClick: () => { + if (noteSaveDisabled) return; + run("project-memory-write", { id, kind: "decision", text: text(note) }, t("Memory saved.")); - } -- }, (0, import_react8.createElement)(Icon), (0, import_react8.createElement)("span", { className: "bt-menu-label" }, label), hint ? (0, import_react8.createElement)("span", { className: "bt-menu-hint" }, hint) : null)) ++ } + }, t("Save memory")) + ) + ), @@ -14841,7 +15484,7 @@ diff --git a/client.js b/client.js + const add = (id) => { + const clean = text(id); + if (clean !== "" && !owners.includes(clean)) owners.push(clean); -+ }; + }; + add(project?.ownerUserId); + for (const id of Array.isArray(project?.ownerUserIds) ? project.ownerUserIds : []) add(id); + const boardMembers = []; @@ -14874,8 +15517,7 @@ diff --git a/client.js b/client.js + { key: `board:${name}`, className: "bt-project-member", "data-member-role": "board", "data-member-id": name }, + (0, import_react20.createElement)("span", null, `@${name}`), + (0, import_react20.createElement)("span", { className: "bt-project-member-kind" }, t("On the board")) - )) -- ), document.body) : null ++ )) + ), + owners.length === 0 && boardMembers.length === 0 && chief === "" ? (0, import_react20.createElement)("div", { className: "bt-project-empty" }, t("No member is named yet.")) : null, + (0, import_react20.createElement)("p", { className: "bt-project-note" }, t("Member management is not part of this slice; owners and the members their cards name are listed here.")), @@ -14884,21 +15526,8 @@ diff --git a/client.js b/client.js + { className: "bt-project-row-actions" }, + (0, import_react20.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => onClose?.() }, t("Back to board")) + ) - ); - } --function useLandOnMainBot(roster, useSessions, actions) { -- const mainView = useSessions((list) => { -- const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); -- return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; -- }); -- const lastBot = (0, import_react8.useRef)(null); -- (0, import_react8.useEffect)(() => { -- if (!roster.ready || !roster.mainBotId) return void 0; -- const [id, blank3] = mainView.split("|"); -- const bot = id === "" ? void 0 : roster.byId[id]; -- if (bot !== void 0) { -- lastBot.current = { botId: bot.id, sessionId: id }; -- if (currentPart(bot) !== id) actions.landOn(bot.id); ++ ); ++} +async function readProjectBoard(read, wanted, knownTeam) { + const listing = await read("projects"); + const projects = Array.isArray(listing?.projects) ? listing.projects : []; @@ -14921,7 +15550,20 @@ diff --git a/client.js b/client.js + const queues = { waiting: board?.waiting, inflight: board?.inflight, blocked: board?.blocked, counts: board?.counts ?? null }; + const base = record?.project ?? project; + const withRole = { ...base, role: project.role ?? null, choices: Array.isArray(project.choices) ? project.choices : [] }; -+ return { + return { +- level() { +- if (closed) return 0; +- analyser.getFloatTimeDomainData(frame); +- let sum = 0; +- for (const sample of frame) sum += sample * sample; +- return Math.min(1, Math.sqrt(sum / frame.length) * 4); +- }, +- cancel: close, +- async finish(maxSeconds) { +- if (closed || recorder.state === "inactive") { +- close(); +- return null; +- } + project: withRole, + assistantMine: record?.assistantMine ?? null, + rows: board?.tasks ?? [], @@ -14972,11 +15614,8 @@ diff --git a/client.js b/client.js + if (typeof window === "undefined") return void 0; + if (!hash.startsWith("#project")) { + setState(null); - return void 0; - } -- if (id !== "" && (roster.roomsById[id] || (actions.isAgentSession?.(id) ?? roster.agentOf[id] !== void 0) || blank3 !== "1")) { -- lastBot.current = null; -- return void 0; ++ return void 0; ++ } + const wanted = hash.slice("#project".length).replace(/^=/, ""); + let gone = false; + team.current = void 0; @@ -14984,7 +15623,24 @@ diff --git a/client.js b/client.js + const loadBoard = () => readProjectBoard(read, wanted, team.current); + const refresh = async (first) => { + const gen = teamGen.current; -+ try { + try { +- await new Promise((resolve) => { +- recorder.onstop = resolve; +- recorder.stop(); +- }); +- stream.getTracks().forEach((track) => track.stop()); +- const blob = new Blob(chunks, { type: recorder.mimeType }); +- if (blob.size === 0) return null; +- const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); +- const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); +- const offline = new OfflineAudioContext(1, length, 16e3); +- const source = offline.createBufferSource(); +- source.buffer = decoded; +- source.connect(offline.destination); +- source.start(); +- return waveBytes((await offline.startRendering()).getChannelData(0)); +- } finally { +- close(); + const next = await loadBoard(); + if (!gone) { + const newer = teamGen.current !== gen; @@ -14993,7 +15649,7 @@ diff --git a/client.js b/client.js + } + } catch { + if (!gone && first) setState({ project: null, rows: [], canManage: false, tasks: [] }); -+ } + } + }; + void refresh(true); + const timer = setInterval(() => { @@ -15018,103 +15674,7 @@ diff --git a/client.js b/client.js + setState((previous) => previous === null ? previous : { ...previous, team: answer }); + } catch { } -- const last = lastBot.current; -- const timer = setTimeout(() => actions.landOn(roster.mainBotId, last), last ? 0 : 400); -- return () => clearTimeout(timer); -- }, [mainView, roster]); --} --function FooterAccount(props) { -- const seated = props.useUi((value) => value.accountSeated === true); -- if (seated) return null; -- return (0, import_react8.createElement)("span", { className: "bt-foot-account" }, (0, import_react8.createElement)(AccountLauncher, { ...props, seat: false })); --} --function BotReturn({ wide, useSurface, actions }) { -- const mode = useSurface((value) => value.mode); -- if (mode !== "agent") return null; -- return (0, import_react8.createElement)( -- "button", -- { type: "button", className: "bt-return", "aria-label": t("DS Bot"), title: t("DS Bot"), onClick: () => actions.setSurface("bot") }, -- (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size: 20 }), -- wide ? (0, import_react8.createElement)("span", { className: "bt-return-name" }, t("DS Bot")) : null -- ); --} --var RETURN_CSS = ` --.bt-return{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:36px;padding:0 14px;border:0;border-radius:999px;background:none;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;transition:background-color .12s ease} --.bt-return:hover{background:rgba(127,127,127,.14)} --.bt-return:active{transform:scale(.96)} --.bt-return .bt-mark{display:inline-flex;flex:none;line-height:0} --.bt-return .bt-mark svg{width:100%;height:100%;overflow:visible} --/* Shell styles carry --bt-ink-deepseek and Agent mode withdraws them; pin it here. */ --.bt-return .bt-mark{--bt-ink-deepseek:#4D6BFE} --.bt-return-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --`; --function BrandMark({ size = 24 }) { -- return (0, import_react8.createElement)("span", { className: "bt-brand-whale" }, (0, import_react8.createElement)(BotMark, { look: MAIN_LOOK, size })); --} --function BrandName() { -- return (0, import_react8.createElement)("span", { style: { fontWeight: 600, fontSize: 14 } }, "DS Bot"); --} --var SIDEBAR_CSS = ` --.bt-side-top .bt-brand{cursor:default;padding-left:6px} --.bt-side-top .bt-brand:hover{background:none} --.bt-brand .bt-mark{flex:none} --.bt-pin{flex:none;display:inline-flex;color:var(--bt-ink-3)} --.bt-rail .bt-row{position:relative} --.bt-rail .bt-pin{position:absolute;top:3px;left:calc(50% + 12px)} --.bt-side .bt-read-only{margin:4px 8px 6px;padding:8px 10px;border-radius:10px;background:var(--bt-hover);font-size:12px;line-height:1.45;text-align:left} --[class*="_footArea"]:has(.bt-foot-account){display:flex!important;flex-direction:row!important;align-items:center;gap:6px} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"],[class*="_footArea"]:has(.bt-foot-account)>[class*="_footerActions"]>div{display:contents!important} --.bt-foot-account{order:-1;flex:none;display:inline-flex} --[class*="_footArea"]:has(.bt-foot-account) .bt-connect-foot{order:0;flex:1 1 auto;min-width:0;width:auto} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"]{order:1;flex:none} --[class*="_footArea"]:has(.bt-foot-account)>[class*="_settingsArea"] button[class*="_trigger"] [class*="_label"]{display:none!important} -- --/* The update cloud springs in when a newer DS Bot shows up; its arrow drops in a -- loop while the install runs. */ --.bt-round.bt-update{position:relative;color:var(--bt-accent,#4D6BFE);animation:bt-update-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-round.bt-update svg{width:17px;height:17px} --.bt-round.bt-update[data-phase=installed]{color:var(--bt-ink-2)} --@keyframes bt-update-in{from{opacity:0;transform:scale(.6);filter:blur(4px)}} --.bt-update[data-phase=installing] .bt-cloud-arrow{animation:bt-cloud-drop 1.1s cubic-bezier(.45,0,.55,1) infinite} --@keyframes bt-cloud-drop{0%{transform:translateY(-3px);opacity:0}35%{opacity:1}70%{transform:translateY(1px);opacity:1}100%{transform:translateY(2px);opacity:0}} --.bt-update-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;border-radius:50%;background:#E5484D;box-shadow:0 0 0 1.5px var(--bt-sidebar)} --.bt-update-pop{position:fixed;z-index:60;width:280px;padding:14px 14px 12px;border-radius:14px;background:var(--bt-main);border:.5px solid var(--bt-line-2);box-shadow:0 10px 20px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);color:var(--bt-ink);box-sizing:border-box;transform-origin:top left;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;animation:bt-update-pop ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --@keyframes bt-update-pop{from{opacity:0;transform:scale(.96);filter:blur(4px)}} --.bt-update-title{font-size:14px;line-height:20px;font-weight:600} --.bt-update-text{margin-top:2px;font-size:12.5px;line-height:18px;color:var(--bt-ink-2)} --.bt-update-error{margin-top:8px;font-size:12px;line-height:17px;color:#c21d2e;overflow-wrap:anywhere;max-height:90px;overflow:auto} --.bt-update-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;margin-top:12px} --.bt-update-notes{margin-right:auto;font-size:12.5px;color:var(--bt-ink-2);text-decoration:none} --.bt-update-notes:hover{color:var(--bt-ink);text-decoration:underline} --.bt-update-go{height:30px;padding:0 14px;border:0;border-radius:999px;background:var(--bt-accent,#4D6BFE);color:var(--bt-accent-ink,#fff);font:inherit;font-size:13px;font-weight:500;cursor:pointer;transition:opacity .12s ease,transform .12s ease} --.bt-update-go:hover:not(:disabled){opacity:.9} --.bt-update-go:active:not(:disabled){transform:scale(.96)} --.bt-update-go:disabled{opacity:.6;cursor:default} --.bt-update-dsh:not(:first-child){margin-top:12px;padding-top:12px;border-top:.5px solid var(--bt-line-2)} --@media (prefers-reduced-motion:reduce){.bt-round.bt-update,.bt-update-pop,.bt-update .bt-cloud-arrow{animation:none}} -- --/* The DSH Agent row and its expanding Session list. The black whale is the -- harness's own mark, so the row reads as "plain dsh" next to the Bot marks. */ --.bt-agent-ava{flex:none;width:36px;height:36px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-ava.bt-run{animation:bt-agent-pulse 1.4s ease-in-out infinite} --@keyframes bt-agent-pulse{0%,100%{opacity:1}50%{opacity:.55}} --.bt-agent-sub{display:grid;grid-template-rows:0fr;transition:grid-template-rows ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing}} --.bt-agent-sub[data-open]{grid-template-rows:1fr} --.bt-agent-sub-in{overflow:hidden;min-height:0;margin-left:44px} --.bt-agent-sub-row{display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px 5px 8px;border:0;border-radius:8px;background:none;color:var(--bt-ink-2);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer} --.bt-agent-sub-row:hover{background:var(--bt-hover);color:var(--bt-ink)} --.bt-agent-sub-row[aria-current=page]{background:var(--bt-hover);color:var(--bt-ink);font-weight:500} --.bt-agent-sub-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-agent-sub-time{flex:none;font-size:11px;color:var(--bt-ink-3)} --.bt-agent-new{color:var(--bt-ink-3)} --.bt-agent-new svg{width:14px;height:14px;flex:none} --.bt-agent-edit{padding:3px 10px 3px 4px} --.bt-agent-edit input{width:100%;border:0;border-radius:6px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;font-size:13px;line-height:20px;padding:2px 8px;outline:none;box-shadow:inset 0 0 0 .5px var(--bt-line-2)} --.bt-agent-sub[data-open] .bt-agent-sub-row{animation:bt-agent-row-in .24s ease both;animation-delay:calc(var(--i, 0)*30ms)} --@keyframes bt-agent-row-in{from{opacity:0;filter:blur(4px)}to{opacity:1;filter:blur(0)}} --@media (prefers-reduced-motion:reduce){.bt-agent-sub{transition:none}.bt-agent-sub .bt-agent-sub-row{animation:none}} --`; -+ }; + }; + if (state === null) return null; + return (0, import_react20.createElement)( + "div", @@ -15185,122 +15745,121 @@ diff --git a/client.js b/client.js + ) + ) + ); -+} - - // src/client/header.js --var import_react9 = require("react"); --var import_react_dom4 = require("react-dom"); --var GoArrow = () => (0, import_react9.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react9.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); + } +-async function transcribeWave(speech, catalog, wave, signal) { +- if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); +- const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); +- if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); +- return String(result.value?.text ?? "").trim(); ++ ++// src/client/header.js +var import_react21 = require("react"); +var import_react_dom6 = require("react-dom"); +var GoArrow = () => (0, import_react21.createElement)("svg", { width: 14, height: 14, viewBox: "-12 -12 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react21.createElement)("path", { d: "M-7 0H7M2 -5L7 0L2 5" })); - var MEMORY_NOTICE_MS = 3200; - var noNews = (select) => select({}); - function useMemoryNotice(ids, useMemoryNews) { - const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); -- const [shown, setShown] = (0, import_react9.useState)(false); -- (0, import_react9.useEffect)(() => { ++var MEMORY_NOTICE_MS = 3200; ++var noNews = (select) => select({}); ++function useMemoryNotice(ids, useMemoryNews) { ++ const seen = useMemoryNews((map) => Math.max(0, ...ids.map((id) => map?.[id]?.seen ?? 0))); + const [shown, setShown] = (0, import_react21.useState)(false); + (0, import_react21.useEffect)(() => { - const left = seen + MEMORY_NOTICE_MS - Date.now(); - setShown(seen > 0 && left > 0); - if (seen === 0 || left <= 0) return void 0; -@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { - return shown; ++ const left = seen + MEMORY_NOTICE_MS - Date.now(); ++ setShown(seen > 0 && left > 0); ++ if (seen === 0 || left <= 0) return void 0; ++ const timer = setTimeout(() => setShown(false), left); ++ return () => clearTimeout(timer); ++ }, [seen]); ++ return shown; } - function MemoryNotice({ shown }) { -- const inner = (0, import_react9.useRef)(null); -- const [width, setWidth] = (0, import_react9.useState)(0); +-function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { +- let spokeAt = 0; +- let quietSince = 0; +- return (level, now) => { +- if (level >= loud) { +- spokeAt ||= now; +- quietSince = 0; +- return "speech"; +- } +- if (!spokeAt) return "waiting"; +- if (level < quiet) quietSince ||= now; +- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; +- }; ++function MemoryNotice({ shown }) { + const inner = (0, import_react21.useRef)(null); + const [width, setWidth] = (0, import_react21.useState)(0); - const label = t("Memory updated"); -- (0, import_react9.useLayoutEffect)(() => { ++ const label = t("Memory updated"); + (0, import_react21.useLayoutEffect)(() => { - if (shown) setWidth(inner.current?.scrollWidth ?? 0); - }, [label, shown]); -- return (0, import_react9.createElement)( ++ if (shown) setWidth(inner.current?.scrollWidth ?? 0); ++ }, [label, shown]); + return (0, import_react21.createElement)( - "span", - { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, -- (0, import_react9.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react9.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react9.createElement)(MemoryIcon)), label) ++ "span", ++ { className: "bt-pill-mem", "aria-hidden": true, style: { "--bt-mem-w": `${width}px` } }, + (0, import_react21.createElement)("span", { className: "bt-pill-mem-in", ref: inner }, (0, import_react21.createElement)("span", { className: "bt-pill-mem-ico" }, (0, import_react21.createElement)(MemoryIcon)), label) - ); ++ ); } --function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useMemoryNews, useTurnClock, useUi, useLive, actions }) { +- +-// src/client/overlay-hooks.js +-var import_react11 = require("react"); +-var escapeLayers = []; +-var onEscapeKey = (event) => { +- if (event.key === "Escape") escapeLayers.at(-1)?.current(); +-}; +-function useEscape(onClose) { +- const ref = (0, import_react11.useRef)(onClose); +- ref.current = onClose; +- (0, import_react11.useEffect)(() => { +- if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); +- escapeLayers.push(ref); +function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSession, useActivity, useBusyAcks, useMemoryNews, useTurnClock, useUi, useLive, actions }) { - const roster = useRoster((value) => value); - const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); - const firstTurns = useSessions((list) => { -@@ -5537,7 +5537,7 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe - const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); - return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; - }); -- (0, import_react9.useEffect)(() => { ++ const roster = useRoster((value) => value); ++ const title = useSessions((list) => list.byId[sessionId]?.displayTitle ?? list.byId[sessionId]?.title ?? ""); ++ const firstTurns = useSessions((list) => { ++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; ++ return Array.isArray(outline) ? outline.slice(0, 8).map((entry) => entry.turn).join(",") : ""; ++ }); ++ const hiddenTurns = roster.exchangeTurns?.[sessionId]; ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const opening = useTurnClock((state) => { ++ const first = openingTurn(state[sessionId], firstTurns, hiddenTurns); ++ return first && !unseenSince(visit, first.start) ? clockLabel(first.start) : ""; ++ }); + (0, import_react21.useEffect)(() => { - 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"); -@@ -5546,54 +5546,49 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe - const owner = roster.byId[sessionId]?.id ?? sessionId; - const doing = useActivity((map) => map[owner]); - const bot = roster.byId[sessionId]; ++ 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"); ++ }, [opening]); ++ const running = useSessionStatus((map) => map.get(sessionId)?.running === true); ++ const owner = roster.byId[sessionId]?.id ?? sessionId; ++ const doing = useActivity((map) => map[owner]); ++ const bot = roster.byId[sessionId]; + const projectAgent = projectAgentFor(bot, roster.self?.memberId); - const room2 = roster.roomsById[sessionId]; - const detailsOpen = useUi((state) => state.details === sessionId); -- const name = bot?.name ?? room2?.name ?? ""; ++ const room2 = roster.roomsById[sessionId]; ++ const detailsOpen = useUi((state) => state.details === sessionId); + const full2 = bot?.remote ? friendBotTitle(bot) : roomTitle(room2) || bot?.name || ""; + const face = room2?.origin === "face-to-face"; + const name = face ? faceRowTitle(full2) : full2; - const useNews = useMemoryNews ?? noNews; - const memoryIds = bot ? [bot.id] : room2?.members ?? []; - const notice = useMemoryNotice(memoryIds, useNews); -- const changed = useNews((map) => latestMemoryChange(map, memoryIds)); -- (0, import_react9.useEffect)(() => { -- document.body.style.setProperty("--bt-placeholder", JSON.stringify(name ? t("Message {name}", { name }) : t("Message"))); -- }, [name, t("Message")]); -- (0, import_react9.useEffect)(() => { -- actions.beginVisit(sessionId); -- return () => actions.endVisit(sessionId); -- }, [sessionId]); -- (0, import_react9.useEffect)(() => { -- if (room2) document.body.dataset.btRoom = ""; -- else delete document.body.dataset.btRoom; -- return () => { -- delete document.body.dataset.btRoom; -- }; -- }, [Boolean(room2)]); -- const memoryNotice = notice && changed !== null && roster.memory !== false; -- return (0, import_react9.createElement)( ++ const useNews = useMemoryNews ?? noNews; ++ const memoryIds = bot ? [bot.id] : room2?.members ?? []; ++ const notice = useMemoryNotice(memoryIds, useNews); + const changed2 = useNews((map) => latestMemoryChange(map, memoryIds)); + const memoryNotice = notice && changed2 !== null && roster.memory !== false; + return (0, import_react21.createElement)( - "div", - { className: "bt-head" }, -- (0, import_react9.createElement)( ++ "div", ++ { className: "bt-head" }, + (0, import_react21.createElement)( - "button", - { - type: "button", - className: "bt-pill", - "aria-expanded": detailsOpen, - "data-memory": memoryNotice ? "updated" : void 0, -- "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) ++ "button", ++ { ++ type: "button", ++ className: "bt-pill", ++ "aria-expanded": detailsOpen, ++ "data-memory": memoryNotice ? "updated" : void 0, + "aria-label": memoryNotice ? t("Memory updated. Open {name}'s memory", { name: roster.byId[changed2]?.name ?? full2 }) : t("View conversation details"), + onClick: () => memoryNotice ? actions.openMemory(changed2) : actions.toggleDetails(sessionId) - }, -- bot ? (0, import_react9.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, -- (0, import_react9.createElement)("span", { className: "bt-pill-name" }, bot?.name ?? room2?.name ?? (title || t("New chat"))), -- (0, import_react9.createElement)(MemoryNotice, { shown: memoryNotice }), -- (0, import_react9.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react9.createElement)(GoArrow)) ++ }, + bot ? (0, import_react21.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react21.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react21.createElement)("span", { className: face ? "bt-pill-name bt-pill-name-face" : "bt-pill-name", title: face ? full2 : void 0 }, name || title || t("New chat")), + (0, import_react21.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react21.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react21.createElement)(GoArrow)) - ), -- (0, import_react9.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), -- (0, import_react9.createElement)(NewMessagePills, { key: sessionId, sessionId, useSessions, useUi, useTurnClock }), -- (0, import_react9.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }) ++ ), + (0, import_react21.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + projectAgent === null ? null : (0, import_react21.createElement)(ProjectAgentEntry, { + projectId: projectAgent.projectId, @@ -15310,100 +15869,364 @@ diff --git a/client.js b/client.js + (0, import_react21.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), + (0, import_react21.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), + (0, import_react21.createElement)(BusyAckBubble, { key: `busy-ack:${sessionId}`, sessionId, useRoster, useBusyAcks, useSessionStatus, useSession, useLive }) - ); - } - function useTranscriptView() { -- const [view, setView] = (0, import_react9.useState)(null); -- (0, import_react9.useEffect)(() => { ++ ); ++} ++function useTranscriptView() { + const [view, setView] = (0, import_react21.useState)(null); + (0, import_react21.useEffect)(() => { - let frame = 0; - let watched = null; - const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +5642,7 @@ function useTranscriptView() { ++ let frame = 0; ++ let watched = null; ++ const observer = new MutationObserver(() => schedule()); ++ const measure = () => { ++ frame = 0; ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ if (scroller !== watched) { ++ observer.disconnect(); ++ watched = scroller; ++ if (scroller) observer.observe(scroller, { childList: true, subtree: true }); ++ } ++ if (!scroller) { ++ setView(null); ++ return; ++ } ++ const box = scroller.getBoundingClientRect(); ++ const headerBottom = document.querySelector('header[class*="_header"]')?.getBoundingClientRect().bottom ?? box.top + 50; ++ const sep = scroller.querySelector(".bt-new-sep"); ++ let divider = null; ++ if (sep) { ++ const at = sep.getBoundingClientRect(); ++ divider = at.bottom <= headerBottom ? "above" : at.top >= box.bottom ? "below" : "in-view"; ++ } ++ const composer = parseFloat(getComputedStyle(scroller).getPropertyValue("--dsh-composer-height")) || 152; ++ const next = { ++ unpinned: scroller.querySelector('[class*="_toBottomSlot"]>button') !== null, ++ divider, ++ center: Math.round(box.left + box.width / 2), ++ top: Math.round(headerBottom + 8), ++ bottom: Math.round(window.innerHeight - box.bottom + composer + 8) ++ }; ++ setView((prev) => prev && Object.keys(next).every((key) => prev[key] === next[key]) ? prev : next); ++ }; ++ const schedule = () => { ++ if (!frame) frame = requestAnimationFrame(measure); ++ }; ++ document.addEventListener("scroll", schedule, true); ++ window.addEventListener("resize", schedule); ++ schedule(); + return () => { +- escapeLayers.splice(escapeLayers.indexOf(ref), 1); +- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ document.removeEventListener("scroll", schedule, true); ++ window.removeEventListener("resize", schedule); + }; }, []); - return view; ++ return view; } --function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { +-function usePaneLeft() { +- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); +- (0, import_react11.useLayoutEffect)(() => { +- const column = document.querySelector('[class*="_footArea"]')?.parentElement; +- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); +- }, []); +- return paneLeft; +-} +- +-// src/client/palette.js +-var import_react12 = require("react"); +-var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); +-var PALETTE_EXIT_MS = 160; +-function CommandPalette({ start, token, roster, actions, useSessions }) { +- const [query, setQuery] = (0, import_react12.useState)(start ?? ""); +- const [cursor, setCursor] = (0, import_react12.useState)(0); +- const [leaving, setLeaving] = (0, import_react12.useState)(false); +- const wanted = query.trim().toLowerCase(); +- const close = () => { +- if (leaving) return; +- setLeaving(true); +- setTimeout(() => actions.closePalette(token), PALETTE_EXIT_MS); +- }; +- useEscape(close); +- const ids = [...roster.bots.map(currentPart), ...roster.rooms.map((item) => item.id)]; +- const found = useSessions((list) => { +- if (wanted.length < 2) return "[]"; +- const hits = []; +- for (const id of ids) { +- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; +- if (!Array.isArray(outline)) continue; +- const hidden = roster.exchangeTurns[id] ?? []; +- for (let index2 = outline.length - 1; index2 >= 0 && hits.length < 30; index2 -= 1) { +- const entry = outline[index2]; +- if (hidden.includes(entry.turn)) continue; +- for (const raw of [entry.response, entry.prompt]) { +- const text = stripHeader(raw ?? "").replace(/\s+/g, " ").trim(); +- const at = text.toLowerCase().indexOf(wanted); +- if (at < 0) continue; +- const from = Math.max(0, at - 24); +- hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); +- break; +- } +- } +function NewMessagePills({ sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }) { - const replies = useSessions((list) => { - const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; - return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; -@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { ++ const replies = useSessions((list) => { ++ const outline = list.projectionsBySession?.[sessionId]?.values?.turnOutline ?? list.byId[sessionId]?.projectionValues?.turnOutline; ++ return Array.isArray(outline) ? outline.reduce((count, entry) => count + (entry.response ? 1 : 0), 0) : 0; ++ }); ++ const visit = useUi((state) => state.visits?.[sessionId]); ++ const unread = useTurnClock((state) => { ++ if (typeof visit?.seen !== "number") return 0; ++ let count = 0; ++ for (const entry of Object.values(state[sessionId] ?? {})) { ++ if (entry.first !== void 0 && entry.first > visit.seen && entry.first <= visit.entered) count += 1; + } +- return JSON.stringify(hits); ++ return count; }); - const view = useTranscriptView(); - const unpinned = view?.unpinned === true; -- const [release, setRelease] = (0, import_react9.useState)({ base: null, dismissed: false }); -- const [above, setAbove] = (0, import_react9.useState)({ seen: false, dismissed: false }); -- (0, import_react9.useEffect)(() => { -+ const [release, setRelease] = (0, import_react21.useState)({ base: null, dismissed: false }); -+ const [above, setAbove] = (0, import_react21.useState)({ seen: false, dismissed: false }); -+ (0, import_react21.useEffect)(() => { - setRelease({ base: unpinned ? replies : null, dismissed: false }); - }, [unpinned]); -- (0, import_react9.useEffect)(() => { -+ (0, import_react21.useEffect)(() => { - if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); - }, [view?.divider]); -+ const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); -+ (0, import_react21.useEffect)(() => { -+ if (above.seen && movedOn) actions?.settleVisit?.(sessionId); -+ }, [above.seen, movedOn, sessionId]); - const fresh = release.base === null ? 0 : replies - release.base; - const showDown = unpinned && fresh > 0 && !release.dismissed; - const showUp = view?.divider === "above" && !above.seen && !above.dismissed; -- (0, import_react9.useEffect)(() => { -+ (0, import_react21.useEffect)(() => { - const scroller = document.querySelector("[data-conversation-scroll]"); - if (!scroller) return void 0; - scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +5677,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"); -- const pill = (direction, count, style, onJump, onDismiss) => (0, import_react9.createElement)( -+ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react21.createElement)( - "div", - { className: "bt-news", "data-direction": direction, style }, -- (0, import_react9.createElement)( +- const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); +- const sections = []; +- const people = [ +- ...roster.bots.filter((bot) => matchesName(bot.name, bot.role)).map((bot) => ({ +- key: bot.id, +- avatar: (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), +- title: bot.name, +- subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), +- badge: bot.hidden ? t("Hidden") : void 0, +- run: async () => { +- if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); +- actions.openSession(bot.id); +- } +- })), +- ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ +- key: room2.id, +- avatar: (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), +- title: room2.name, +- subtitle: t("{count} members", { count: room2.members.length }), +- badge: room2.hidden ? t("Hidden") : void 0, +- run: async () => { +- if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); +- actions.openSession(room2.id); ++ const view = useTranscriptView(); ++ const unpinned = view?.unpinned === true; ++ const [release, setRelease] = (0, import_react21.useState)({ base: null, dismissed: false }); ++ const [above, setAbove] = (0, import_react21.useState)({ seen: false, dismissed: false }); ++ (0, import_react21.useEffect)(() => { ++ setRelease({ base: unpinned ? replies : null, dismissed: false }); ++ }, [unpinned]); ++ (0, import_react21.useEffect)(() => { ++ if (view?.divider === "in-view") setAbove((state) => state.seen ? state : { ...state, seen: true }); ++ }, [view?.divider]); ++ const movedOn = useTurnClock((state) => movedOnSince(visit, state[sessionId], hiddenTurns)); ++ (0, import_react21.useEffect)(() => { ++ if (above.seen && movedOn) actions?.settleVisit?.(sessionId); ++ }, [above.seen, movedOn, sessionId]); ++ const fresh = release.base === null ? 0 : replies - release.base; ++ const showDown = unpinned && fresh > 0 && !release.dismissed; ++ const showUp = view?.divider === "above" && !above.seen && !above.dismissed; ++ (0, import_react21.useEffect)(() => { ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ if (!scroller) return void 0; ++ scroller.toggleAttribute("data-bt-news", showDown); ++ return () => scroller.removeAttribute("data-bt-news"); ++ }, [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"); ++ const pill = (direction, count, style, onJump, onDismiss) => (0, import_react21.createElement)( ++ "div", ++ { className: "bt-news", "data-direction": direction, style }, + (0, import_react21.createElement)( - "button", - { type: "button", className: "bt-news-jump", onClick: onJump }, -- (0, import_react9.createElement)("span", { className: "bt-news-ico" }, (0, import_react9.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), ++ "button", ++ { type: "button", className: "bt-news-jump", onClick: onJump }, + (0, import_react21.createElement)("span", { className: "bt-news-ico" }, (0, import_react21.createElement)(direction === "up" ? ArrowUpIcon : ArrowDownIcon)), - label(count) - ), -- (0, import_react9.createElement)("button", { ++ label(count) ++ ), + (0, import_react21.createElement)("button", { - type: "button", - className: "bt-news-x", - "aria-label": t("Dismiss new messages"), -@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - event.stopPropagation(); - onDismiss(); ++ type: "button", ++ className: "bt-news-x", ++ "aria-label": t("Dismiss new messages"), ++ onClick: (event) => { ++ event.stopPropagation(); ++ onDismiss(); } -- }, (0, import_react9.createElement)(CloseIcon)) +- })) +- ]; +- if (people.length) sections.push([t("Bots"), people]); +- const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ +- key: agent.id, +- avatar: (0, import_react12.createElement)("span", { className: "bt-palette-agent" }, (0, import_react12.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), +- title: "DSH Agent", +- subtitle: t("Plain dsh session"), +- run: () => actions.openAgent(agent.id) +- })); +- if (agents.length) sections.push(["DSH Agent", agents]); +- const messages = JSON.parse(found).map(([id, turn, snippet]) => { +- const bot = roster.byId[id]; +- const room2 = roster.roomsById[id]; +- return { +- key: `${id}:${turn}`, +- title: bot?.name ?? room2?.name ?? t("Bot"), +- subtitle: snippet, +- avatar: bot ? (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), +- run: () => actions.openSession(id) +- }; +- }); +- if (messages.length) sections.push([t("Messages"), messages]); +- const commands = [ +- { key: "cmd:new", icon: (0, import_react12.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, +- { key: "cmd:bot", icon: (0, import_react12.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, +- { key: "cmd:group", icon: (0, import_react12.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, +- { key: "cmd:settings", icon: (0, import_react12.createElement)(GearIcon), title: t("Bot settings"), run: () => actions.openSettings("general") } +- ].filter((command) => matchesName(command.title)); +- if (commands.length) sections.push([t("Commands"), commands]); +- const rows = sections.flatMap(([, items]) => items); +- const active = Math.min(cursor, Math.max(rows.length - 1, 0)); +- const shown = rows.map((row) => row.key).join("\n"); +- const glide = useGlide(`${active} +-${shown}`, { axis: "y", ...QUICK_EDGES }); +- const listRef = glide.box; +- useFlip(listRef, shown, "[data-flip]"); +- (0, import_react12.useEffect)(() => { +- listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); +- }, [active, query]); +- const choose = (row) => { +- if (!row || leaving) return; +- void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); +- close(); + }, (0, import_react21.createElement)(CloseIcon)) - ); - const toLatest = () => { - const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5703,9 +5703,9 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { - else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); ++ ); ++ const toLatest = () => { ++ const scroller = document.querySelector("[data-conversation-scroll]"); ++ const back = scroller?.querySelector('[class*="_toBottomSlot"]>button'); ++ if (back) back.click(); ++ else scroller?.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); }; - const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); -- return (0, import_react_dom4.createPortal)( -- (0, import_react9.createElement)( -- import_react9.Fragment, +- let index = -1; +- return (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, +- (0, import_react12.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), +- (0, import_react12.createElement)( +- "div", +- { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, +- (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-head" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), +- (0, import_react12.createElement)("input", { +- autoFocus: true, +- value: query, +- placeholder: t("Search Bots, chats and messages"), +- "aria-label": t("Search"), +- role: "combobox", +- "aria-expanded": true, +- "aria-controls": "bt-palette-list", +- onChange: (event) => { +- setQuery(event.target.value); +- setCursor(0); +- }, +- onKeyDown: (event) => { +- if (event.nativeEvent.isComposing) return; +- if (event.key === "ArrowDown") { +- event.preventDefault(); +- setCursor(Math.min(active + 1, rows.length - 1)); +- } else if (event.key === "ArrowUp") { +- event.preventDefault(); +- setCursor(Math.max(active - 1, 0)); +- } else if (event.key === "Enter") { +- event.preventDefault(); +- choose(rows[active]); +- } +- } +- }), +- query ? (0, import_react12.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { +- setQuery(""); +- setCursor(0); +- } }, (0, import_react12.createElement)(CloseIcon)) : null +- ), +- (0, import_react12.createElement)( +- "div", +- { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, +- (0, import_react12.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- rows.length === 0 ? (0, import_react12.createElement)( +- "div", +- { className: "bt-palette-empty" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), +- (0, import_react12.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) +- ) : sections.map(([title, items]) => (0, import_react12.createElement)( +- import_react12.Fragment, +- { key: title }, +- (0, import_react12.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), +- items.map((row) => { +- index += 1; +- const at = index; +- return (0, import_react12.createElement)( +- "button", +- { +- key: row.key, +- type: "button", +- role: "option", +- className: "bt-palette-row", +- "aria-selected": at === active, +- "data-flip": row.key, +- onMouseMove: () => { +- if (cursor !== at) setCursor(at); +- }, +- onClick: () => choose(row) +- }, +- (0, import_react12.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react12.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), +- (0, import_react12.createElement)( +- "span", +- { className: "bt-palette-text" }, +- (0, import_react12.createElement)("span", { className: "bt-palette-title" }, row.title), +- row.subtitle ? (0, import_react12.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null +- ), +- row.badge ? (0, import_react12.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null +- ); +- }) +- )) +- ) +- ) ++ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); + return (0, import_react_dom6.createPortal)( + (0, import_react21.createElement)( + import_react21.Fragment, - 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 -@@ -5714,19 +5714,135 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { ++ 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 ++ ), ++ document.body ); } +-// src/client/model-menu.js +-var import_react13 = require("react"); +-var import_react_dom6 = require("react-dom"); +-var MENU_EXIT_MS = 120; +-function groupModels(models) { +- const groups = /* @__PURE__ */ new Map(); +- for (const model of models ?? []) { +- const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0]; +- if (!groups.has(provider)) groups.set(provider, []); +- groups.get(provider).push(model); +- } +- const names = /* @__PURE__ */ new Map(); +- for (const [provider, list] of groups) { +- const name = list[0].providerName ?? provider; +- names.set(name, (names.get(name) ?? 0) + 1); +- } +- return [...groups].map(([provider, list]) => { +- const name = list[0].providerName ?? provider; +- return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; +- }); +-} +-var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); +-function modelOf(bot, roster) { +- const ref = bot.model ?? bot.appliedModel; +- if (!ref) return void 0; +- const served = servedOf(roster, ref); +- const [provider, ...rest] = ref.split("/"); +- return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; +// src/client/composer-label.js +var ATTRIBUTES = ["aria-label", "data-placeholder"]; +function holdComposerLabel(doc, label) { @@ -15434,7 +16257,20 @@ diff --git a/client.js b/client.js + } + } + }; -+} + } +-var effortCache = /* @__PURE__ */ new Map(); +-function useEfforts(served, ref, actions) { +- const [fetched, setFetched] = (0, import_react13.useState)(void 0); +- (0, import_react13.useEffect)(() => { +- setFetched(void 0); +- if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; +- let live = true; +- if (effortCache.has(ref)) setFetched(effortCache.get(ref)); +- else void Promise.resolve(actions.efforts(ref)).then((value) => { +- const reasoning = value?.efforts ? value : null; +- effortCache.set(ref, reasoning); +- if (live) setFetched(reasoning); +- }).catch(() => { + +// src/client/header-effects.js +function installHeaderEffects({ sessions, roster, actions, doc = globalThis.document }) { @@ -15464,7 +16300,12 @@ diff --git a/client.js b/client.js + return () => { + delete doc.body.dataset.btFriendChat; + }; -+ }); + }); +- return () => { +- live = false; +- }; +- }, [ref, served?.reasoning !== void 0, actions]); +- return served?.reasoning ?? fetched; + run("remote-empty", `${sessionId}|${bot?.remote === true}|${row?.blank === true}`, () => { + if (!bot?.remote || row?.blank !== true) return void 0; + doc.body.dataset.btRemoteChat = "empty"; @@ -15518,50 +16359,77 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; -+} + } +-function effortRows(reasoning) { +- if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; +- return [ +- { effort: void 0, label: t("Default") }, +- ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) +- ]; + - // src/client/composer.js --var import_react10 = require("react"); --var import_react_dom5 = require("react-dom"); ++// src/client/composer.js +var import_react22 = require("react"); +var import_react_dom7 = require("react-dom"); - var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; - var keepComposerFocus = (event) => event.preventDefault(); - function useDictation(onText) { -- const [listening, setListening] = (0, import_react10.useState)(false); -- const [note, setNote] = (0, import_react10.useState)(""); -- const recognition = (0, import_react10.useRef)(null); -- const latest = (0, import_react10.useRef)(onText); ++var speechApi = () => window.SpeechRecognition ?? window.webkitSpeechRecognition; ++var keepComposerFocus = (event) => event.preventDefault(); ++function useDictation(onText) { + const [listening, setListening] = (0, import_react22.useState)(false); + const [note, setNote] = (0, import_react22.useState)(""); + const recognition = (0, import_react22.useRef)(null); + const latest = (0, import_react22.useRef)(onText); - latest.current = onText; -- (0, import_react10.useEffect)(() => () => recognition.current?.abort(), []); -- (0, import_react10.useEffect)(() => { ++ latest.current = onText; + (0, import_react22.useEffect)(() => () => recognition.current?.abort(), []); + (0, import_react22.useEffect)(() => { - if (!note) return void 0; - const timer = setTimeout(() => setNote(""), 2600); - return () => clearTimeout(timer); -@@ -5745,10 +5745,10 @@ function useDictation(onText) { - let interim = ""; - for (let index = event.resultIndex; index < event.results.length; index += 1) { - const result = event.results[index]; -- const text = result[0]?.transcript ?? ""; ++ if (!note) return void 0; ++ const timer = setTimeout(() => setNote(""), 2600); ++ return () => clearTimeout(timer); ++ }, [note]); ++ const start = ({ continuous = true, onEnd } = {}) => { ++ const Api = speechApi(); ++ if (!Api) { ++ setNote(t("Dictation is not available in this browser")); ++ return false; ++ } ++ const engine = new Api(); ++ engine.lang = navigator.language || "zh-CN"; ++ engine.interimResults = true; ++ engine.continuous = continuous; ++ engine.onresult = (event) => { ++ let interim = ""; ++ for (let index = event.resultIndex; index < event.results.length; index += 1) { ++ const result = event.results[index]; + const text2 = result[0]?.transcript ?? ""; - if (result.isFinal) { -- if (text.trim()) latest.current(text, true); -- } else interim += text; ++ if (result.isFinal) { + if (text2.trim()) latest.current(text2, true); + } else interim += text2; - } - if (interim) latest.current(interim, false); - }; -@@ -5774,19 +5774,38 @@ function useDictation(onText) { - const stop = () => recognition.current?.stop(); - return { listening, note, start, stop }; ++ } ++ if (interim) latest.current(interim, false); ++ }; ++ engine.onerror = (event) => { ++ if (event.error === "aborted" || event.error === "no-speech") return; ++ setNote(event.error === "not-allowed" || event.error === "service-not-allowed" ? t("Microphone access is blocked") : t("Couldn't hear that")); ++ }; ++ engine.onend = () => { ++ setListening(false); ++ recognition.current = null; ++ onEnd?.(); ++ }; ++ recognition.current = engine; ++ try { ++ engine.start(); ++ setListening(true); ++ return true; ++ } catch { ++ setNote(t("Couldn't start dictation")); ++ return false; ++ } ++ }; ++ const stop = () => recognition.current?.stop(); ++ return { listening, note, start, stop }; } +-function EffortRows({ reasoning, current, onPick }) { +- return (0, import_react13.createElement)( +- import_react13.Fragment, +function ComposerIdentity({ sessionId, useRoster }) { + const roster = useRoster((value) => value); + const room2 = roster.roomsById?.[sessionId]; @@ -15581,134 +16449,246 @@ diff --git a/client.js b/client.js + t("Sending as {name}", { name: self.displayName || self.memberId }) + ); +} - function ComposerVoice({ sessionId, inputActions, actions, useUi }) { -- const dictation = useDictation((text, final) => { -- if (final && inputActions) inputActions.insertText(text, inputActions.captureInsertion()); ++function ComposerVoice({ sessionId, inputActions, actions, useUi }) { + const dictation = useDictation((text2, final) => { + if (final && inputActions) inputActions.insertText(text2, inputActions.captureInsertion()); - }); - const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); -- (0, import_react10.useEffect)(() => { ++ }); ++ const quote = useUi((state) => state.quote?.sessionId === sessionId ? state.quote : null); + (0, import_react22.useEffect)(() => { - if (!quote || !inputActions) return; - actions.clearQuote(quote.token); - const excerpt = quote.text.replace(/\s+/g, " ").trim(); -- const line = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} ++ if (!quote || !inputActions) return; ++ actions.clearQuote(quote.token); ++ const excerpt = quote.text.replace(/\s+/g, " ").trim(); + const line2 = `> ${excerpt.length > 200 ? `${excerpt.slice(0, 200)}…` : excerpt} - - `; -- inputActions.insertText(line, inputActions.captureInsertion()); ++ ++`; + inputActions.insertText(line2, inputActions.captureInsertion()); - const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); - if (box instanceof HTMLElement) { - box.focus(); -@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - }, [quote?.token]); - if (sessionId === void 0) return null; - const pluginMic = actions.speech() !== void 0; -- return (0, import_react10.createElement)( -- import_react10.Fragment, ++ const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); ++ if (box instanceof HTMLElement) { ++ box.focus(); ++ const selection = window.getSelection(); ++ selection?.selectAllChildren(box); ++ selection?.collapseToEnd(); ++ } ++ }, [quote?.token]); ++ if (sessionId === void 0) return null; ++ const pluginMic = actions.speech() !== void 0; + return (0, import_react22.createElement)( + import_react22.Fragment, null, -- pluginMic ? null : (0, import_react10.createElement)("button", { -+ pluginMic ? null : (0, import_react22.createElement)("button", { - type: "button", - className: "bt-mic", - "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +5808,16 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { - title: dictation.listening ? t("Stop dictation") : t("Dictate"), - onMouseDown: keepComposerFocus, - onClick: () => dictation.listening ? dictation.stop() : dictation.start() -- }, (0, import_react10.createElement)(MicIcon)), -- (0, import_react10.createElement)("button", { +- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), +- effortRows(reasoning).map((row) => (0, import_react13.createElement)( +- "button", +- { +- key: row.effort ?? "default", +- type: "button", +- role: "menuitemradio", +- "aria-checked": row.effort === current, +- onClick: () => onPick(row.effort) +- }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) +- )) ++ pluginMic ? null : (0, import_react22.createElement)("button", { ++ type: "button", ++ className: "bt-mic", ++ "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), ++ "aria-pressed": dictation.listening, ++ title: dictation.listening ? t("Stop dictation") : t("Dictate"), ++ onMouseDown: keepComposerFocus, ++ onClick: () => dictation.listening ? dictation.stop() : dictation.start() + }, (0, import_react22.createElement)(MicIcon)), + (0, import_react22.createElement)("button", { - type: "button", - className: "bt-voice", - "aria-label": t("Start voice chat"), - title: t("Voice chat"), - onMouseDown: keepComposerFocus, - onClick: () => actions.openVoice(sessionId) -- }, (0, import_react10.createElement)(WaveIcon, { size: 18 })), -- dictation.note ? (0, import_react10.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ++ type: "button", ++ className: "bt-voice", ++ "aria-label": t("Start voice chat"), ++ title: t("Voice chat"), ++ onMouseDown: keepComposerFocus, ++ onClick: () => actions.openVoice(sessionId) + }, (0, import_react22.createElement)(WaveIcon, { size: 18 })), + dictation.note ? (0, import_react22.createElement)("span", { className: "bt-mic-note", role: "status" }, dictation.note) : null ); } - function menuIcons(roster) { -@@ -5825,17 +5825,17 @@ function menuIcons(roster) { - return (row) => { - if (!row.bot) return PeopleIcon; - const id = row.bot.id; -- if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react10.createElement)(BotMark, { bot: roster.getSnapshot().byId[id] ?? row.bot, size })); +-function ModelRows({ roster, current, onPick }) { +- return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)( +- import_react13.Fragment, +- { key: list[0].provider ?? provider }, +- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), +- list.map((model) => (0, import_react13.createElement)( +- "button", +- { +- key: model.ref, +- type: "button", +- role: "menuitemradio", +- "aria-checked": model.ref === current, +- title: model.ref, +- onClick: () => onPick(model.ref) +- }, +- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), +- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, +- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) +- )) +- )); ++function menuIcons(roster) { ++ const marks = /* @__PURE__ */ new Map(); ++ return (row) => { ++ if (!row.bot) return PeopleIcon; ++ const id = row.bot.id; + if (!marks.has(id)) marks.set(id, ({ size = 14 }) => (0, import_react22.createElement)(BotMark, { bot: markBotForMention(row.bot, roster.getSnapshot()), size })); - return marks.get(id); - }; ++ return marks.get(id); ++ }; } - function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - const roster = useRoster((value) => value); -- const [open, setOpen] = (0, import_react10.useState)(null); +-var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); +-function useAnchoredMenu() { +- const [place, setPlace] = (0, import_react13.useState)(null); +- const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); +- const button = (0, import_react13.useRef)(null); +- const menu = (0, import_react13.useRef)(null); +- const close = () => setPlace(null); +- (0, import_react13.useEffect)(() => { +- if (place === null) return void 0; ++function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { ++ const roster = useRoster((value) => value); + const [open, setOpen] = (0, import_react22.useState)(null); - const [shown, leaving] = useLinger(open, 120); -- const button = (0, import_react10.useRef)(null); -- const menu = (0, import_react10.useRef)(null); -- (0, import_react10.useEffect)(() => { ++ const [shown, leaving] = useLinger(open, 120); + const button = (0, import_react22.useRef)(null); + const menu = (0, import_react22.useRef)(null); + (0, import_react22.useEffect)(() => { - if (!open) return void 0; ++ if (!open) return void 0; const onDown = (event) => { - if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5855,6 +5855,7 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - }, [open]); - const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; - if (sessionId === void 0 || target === void 0) return null; +- if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; +- close(); ++ if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); + }; + const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); +- close(); +- button.current?.focus(); ++ setOpen(null); + } + }; +- const onScroll = (event) => { +- if (!menu.current?.contains(event.target)) close(); +- }; +- window.addEventListener("mousedown", onDown); ++ window.addEventListener("mousedown", onDown, true); + window.addEventListener("keydown", onKey, true); +- window.addEventListener("scroll", onScroll, true); + return () => { +- window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("mousedown", onDown, true); + window.removeEventListener("keydown", onKey, true); +- window.removeEventListener("scroll", onScroll, true); + }; +- }, [place]); +- const open = () => { +- const rect = button.current.getBoundingClientRect(); +- const width = Math.max(240, rect.width); +- const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8)); +- const below = window.innerHeight - rect.bottom; +- setPlace(below >= 260 || below >= rect.top ? { left, width, top: rect.bottom + 4, maxHeight: below - 16, "--bt-origin": "top right" } : { left, width, bottom: window.innerHeight - rect.top + 4, maxHeight: rect.top - 16, "--bt-origin": "bottom right" }); +- }; +- return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; +-} +-function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { +- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); +- const shown = modelOf(bot, roster); +- const current = bot.model ?? bot.appliedModel; +- const served = servedOf(roster, current); +- const reasoning = useEfforts(served, current, actions); +- const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0; +- const currentEffort = picked ?? reasoning?.defaultEffort; +- const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort; ++ }, [open]); ++ const target = roster.byId?.[sessionId]?.id ?? roster.roomsById?.[sessionId]?.id; ++ if (sessionId === void 0 || target === void 0) return null; + const locked = lockedWindow(roster); const toggle = () => { - if (open) { - setOpen(null); -@@ -5867,19 +5867,15 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - setOpen(null); - button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); +- if (isOpen) { +- close(); ++ if (open) { ++ setOpen(null); + return; + } +- void actions?.refreshModels?.(); +- open(); ++ const rect = button.current.getBoundingClientRect(); ++ setOpen({ left: rect.left, bottom: window.innerHeight - rect.top + 8 }); }; -- const schedule = () => { -- setOpen(null); -- actions.openSettings("schedules", target); -- }; - const reference = () => { - setOpen(null); - actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); +- const pick2 = (ref) => { +- close(); +- if (onPick) onPick(ref); +- else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); ++ const attach = () => { ++ setOpen(null); ++ button.current?.closest("[data-composer-card]")?.querySelector("input[type=file]")?.click(); }; - const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); -- return (0, import_react10.createElement)( -- import_react10.Fragment, +- const pickEffort = (effort) => { +- close(); +- if (onPickEffort) onPickEffort(effort); +- else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); ++ const reference = () => { ++ setOpen(null); ++ actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); + }; +- const models = roster.models ?? []; +- return (0, import_react13.createElement)( +- import_react13.Fragment, ++ const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); + return (0, import_react22.createElement)( + import_react22.Fragment, null, -- (0, import_react10.createElement)("button", { +- (0, import_react13.createElement)( +- "button", + (0, import_react22.createElement)("button", { - ref: button, - type: "button", - className: "bt-plus", -@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - "aria-expanded": Boolean(open), - onMouseDown: keepComposerFocus, - onClick: toggle -- }, (0, import_react10.createElement)(PlusIcon)), -- shown ? (0, import_react_dom5.createPortal)((0, import_react10.createElement)( ++ ref: button, ++ type: "button", ++ className: "bt-plus", ++ "aria-label": t("Add"), ++ title: t("Add"), ++ "aria-haspopup": "menu", ++ "aria-expanded": Boolean(open), ++ onMouseDown: keepComposerFocus, ++ onClick: toggle + }, (0, import_react22.createElement)(PlusIcon)), + shown ? (0, import_react_dom7.createPortal)((0, import_react22.createElement)( - "div", ++ "div", { - ref: menu, -@@ -5900,9 +5900,12 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { - style: { left: shown.left, bottom: shown.bottom }, - "data-leaving": leaving || void 0 +- ref: button, +- type: "button", +- className: "bt-model-trigger", +- "data-wide": wide || void 0, +- "aria-haspopup": "menu", +- "aria-expanded": isOpen, +- "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), +- "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, +- onClick: toggle ++ ref: menu, ++ className: "bt-menu bt-menu-up bt-plus-menu", ++ role: "menu", ++ "aria-label": t("Add"), ++ style: { left: shown.left, bottom: shown.bottom }, ++ "data-leaving": leaving || void 0 }, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react10.createElement)(PaperclipIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), -- canReference ? (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react10.createElement)(ChatsIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, -- (0, import_react10.createElement)("button", { type: "button", role: "menuitem", onClick: schedule }, (0, import_react10.createElement)(ClockIcon), (0, import_react10.createElement)("span", { className: "bt-menu-label" }, t("Scheduled task"))) +- (0, import_react13.createElement)( +- "span", +- { className: "bt-model-copy" }, +- (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), +- shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null +- ), +- (0, import_react13.createElement)(Caret) +- ), +- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( +- "div", +- { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, +- models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), +- effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, +- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => { +- close(); +- openHarnessSettings(); +- } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) + (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: attach }, (0, import_react22.createElement)(PaperclipIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Add files"))), + canReference ? (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onMouseDown: keepComposerFocus, onClick: reference }, (0, import_react22.createElement)(ChatsIcon), (0, import_react22.createElement)("span", { className: "bt-menu-label" }, t("Reference a chat"))) : null, + locked ? null : (0, import_react22.createElement)("button", { type: "button", role: "menuitem", onClick: () => { @@ -15718,11 +16698,95 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -5922,15 +5922,57 @@ var conversationOf = (roster, sessionId) => { - function mentionRows(roster, sessionId, query = "") { - const here = conversationOf(roster, sessionId); - if (here === null) return null; -- const bots = here.room ? here.room.members.map((id) => roster.byId[id]).filter(Boolean) : roster.bots.filter((bot) => bot.id !== here.self.id); +-function ModelChoiceDock({ sessionId, useRoster, actions }) { +- const roster = useRoster((value) => value); +- const bot = roster.byId[sessionId]; +- const [busy, setBusy] = (0, import_react13.useState)(""); +- const [error, setError] = (0, import_react13.useState)(""); +- const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; +- (0, import_react13.useEffect)(() => { +- if (shown) void actions.refreshModels?.(); +- }, [shown, sessionId]); +- if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; +- 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; +- const pick2 = (ref) => { +- setBusy(ref); +- setError(""); +- void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { +- setError(failure?.message ?? String(failure)); +- void actions.refreshModels?.(); +- }).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 }); +- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ +- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( +- "div", +- { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, +- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), +- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { +- key: model.ref, +- type: "button", +- className: "bt-model-chip", +- disabled: busy !== "", +- "aria-busy": busy === model.ref || void 0, +- "aria-label": `${model.name} · ${provider}`, +- title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, +- onClick: () => pick2(model.ref) +- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) +- )), +- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) +- ]; +- return (0, import_react13.createElement)( +- "div", +- { className: "bt-model-dock" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-astack", "data-lead": "" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-msg" }, +- (0, import_react13.createElement)( +- "div", +- { className: "bt-msg-line" }, +- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) +- ) +- ) +- ), +- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), +- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null +- ); +-} +-function setupChoices(actions) { +- return [ +- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, +- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], +- [t("Open settings to set up a custom model"), openHarnessSettings] +- ].filter(Boolean); +-} +-function DeepSeekSignIn({ useUi, actions }) { +- const open = useUi((state) => state.signIn); +- const entry = open ? actions.signInEntry?.() : null; +- if (!entry) return null; +- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); ++ ++// src/client/at-menu.js ++var BOTS_SOURCE = "ds-bot"; ++var CHATS_SOURCE = "ds-bot-chats"; ++var ROOM_ALL = ["all", "everyone", "所有人", "全体"]; ++var OTHER_CHATS_MAX = 8; ++var conversationOf = (roster, sessionId) => { ++ if (!roster.ready) return null; ++ const room2 = roster.roomsById[sessionId]; ++ if (room2) return { room: room2 }; ++ const self = roster.byId[sessionId]; ++ return self ? { self } : null; ++}; ++function mentionRows(roster, sessionId, query = "") { ++ const here = conversationOf(roster, sessionId); ++ if (here === null) return null; + const face = isFaceRoom(here.room); + const live = (here.room?.participants ?? []).filter((p) => p.leftSeq === void 0); + const bots = here.room ? [ @@ -15740,15 +16804,12 @@ diff --git a/client.js b/client.js + label: face ? faceLabel(ownerNameOf(p), cleanPeerName(p.displayName || p.name, "Bot")) : remoteBotTitle(p) + })) + ] : listedChatBots(roster).filter((bot) => bot.id !== here.self.id); - const needle = query.trim().toLowerCase(); -- const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle)).map((bot) => ({ bot, text: `@${bot.name} ` })); ++ const needle = query.trim().toLowerCase(); + const rows = bots.filter((bot) => bot.name.toLowerCase().includes(needle) || String(bot.label ?? "").toLowerCase().includes(needle) || bot.remote && normHandle(remoteRecord(bot)?.handle || bot.handle).includes(normHandle(needle))).map((bot) => ({ bot, text: `@${bot.name} ` })); - const everyone = t("Everyone"); - if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { -- rows.push({ room: here.room, text: `@${everyone} ` }); ++ const everyone = t("Everyone"); ++ if (here.room && bots.length > 1 && [everyone, ...ROOM_ALL].some((word) => word.toLowerCase().startsWith(needle))) { + rows.push({ room: here.room, count: bots.filter((bot) => bot.kind !== "human").length, text: `@${everyone} ` }); - } -- return rows; ++ } + if (face) return rows; + const seatHandles = new Set(bots.filter((bot) => bot.remote).map((bot) => normHandle(remoteRecord(bot)?.handle)).filter(Boolean)); + if (here.room) return [...rows, ...remoteMentionRows(roster, query).filter((row) => !seatHandles.has(normHandle(row.remote.handle)))]; @@ -15758,6 +16819,20 @@ diff --git a/client.js b/client.js + ...remoteMentionRows(roster, query) + ]; } +-function SignInHost({ entry, actions }) { +- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); +- const close = () => actions.closeSignIn(); +- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); +- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); +- return (0, import_react13.createElement)(entry.component, { +- ...ops, +- ...hooks, +- t: actions.accountT(), +- complete: close, +- useApiKey: () => { +- close(); +- openHarnessOnboarding("deepseek-official"); +- } +var isFaceRoom = (room2) => room2?.origin === "face-to-face"; +var memberCount = (count) => t(count === 1 ? "{count} member" : "{count} members", { count }); +var botCount = (count) => count === 1 ? t("1 Bot") : t("{count} Bots", { count }); @@ -15777,17 +16852,34 @@ diff --git a/client.js b/client.js + }); +}; +var faceLabel = (person, bot) => person ? `${bot} (${person})` : bot; - function chatRows(roster, list, sessionId) { - const here = conversationOf(roster, sessionId); - if (here === null) return []; -@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { - order: -20, - showGroupTitle: false, - async candidates(session, { query }) { -- return (mentionRows(roster.getSnapshot(), session.sessionId, query) ?? []).map((row) => ({ -- name: row.bot ? row.bot.name : t("Everyone"), -- ...row.bot?.role ? { description: row.bot.role } : {}, -- ...row.room ? { description: t("{count} members", { count: row.room.members.length }) } : {}, ++function chatRows(roster, list, sessionId) { ++ const here = conversationOf(roster, sessionId); ++ if (here === null) return []; ++ const byId = list?.byId ?? {}; ++ const bots = roster.bots.filter((bot) => bot.id !== here.self?.id).map((bot) => { ++ const id = currentPart(bot); ++ return { sessionId: id, label: t("Chat with {name}", { name: bot.name }), bot, updatedAt: byId[id]?.updatedAt }; + }); ++ const team = /* @__PURE__ */ new Set([ ++ ...roster.bots.flatMap((bot) => [bot.id, currentPart(bot), ...bot.parts ?? []]), ++ ...roster.rooms.flatMap((room2) => [room2.id, ...room2.parts ?? []]) ++ ]); ++ const others = (list?.ids ?? []).map((id) => byId[id]).filter((row) => row !== void 0 && row.id !== sessionId && !team.has(row.id) && row.origin !== "subagent" && row.blank !== true).sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)).slice(0, OTHER_CHATS_MAX).map((row) => ({ sessionId: row.id, label: row.displayTitle || row.title || row.id, updatedAt: row.updatedAt })); ++ return [...bots, ...others]; ++} ++function sessionMention(sessionId, label) { ++ let binary = ""; ++ for (const byte of new TextEncoder().encode(JSON.stringify(sessionId))) binary += String.fromCharCode(byte); ++ const payload = btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); ++ return `@[${label.replace(/[\\\]]/g, (mark) => `\\${mark}`)}](dsh-session:${payload})`; ++} ++function atSources({ roster, sessions, launched, iconFor }) { ++ const bots = { ++ trigger: "@", ++ name: BOTS_SOURCE, ++ order: -20, ++ showGroupTitle: false, ++ async candidates(session, { query }) { + const snapshot = roster.getSnapshot(); + const face = isFaceRoom(snapshot.roomsById?.[session.sessionId]); + const team = (mentionRows(snapshot, session.sessionId, query) ?? []).map((row) => ({ @@ -15800,10 +16892,10 @@ diff --git a/client.js b/client.js + ...row.bot?.remote && !face && remoteRecord(row.bot).handle ? { description: remoteRecord(row.bot).handle } : {}, + // A face room counts the Bots in it, mine and everyone else's (#305, #457). + ...row.room ? { description: face ? botCount(row.count) : memberCount(row.count) } : {}, - icon: iconFor(row), - section: t("Bots"), - value: row.text - })); ++ icon: iconFor(row), ++ section: t("Bots"), ++ value: row.text ++ })); + if (mentionRows(snapshot, session.sessionId, "") === null || face) return team; + const extra = (await mentionCandidates(query, { sessionId: session.sessionId, roster: snapshot })).map((row) => ({ + name: row.name, @@ -15813,23 +16905,102 @@ diff --git a/client.js b/client.js + value: row.value + })); + return [...team, ...extra]; - }, - onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 - }; -@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { - return [bots, chats]; ++ }, ++ onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 ++ }; ++ const chats = { ++ trigger: "@", ++ name: CHATS_SOURCE, ++ order: -10, ++ showGroupTitle: false, ++ async candidates(session) { ++ if (!launched(session.sessionId)) return []; ++ return chatRows(roster.getSnapshot(), sessions.list.getSnapshot(), session.sessionId).map((row) => ({ ++ name: row.bot ? row.bot.name : row.label, ++ ...row.updatedAt ? { description: relativeShort(row.updatedAt) } : {}, ++ icon: row.bot ? iconFor(row) : "session", ++ section: row.bot ? t("Bots") : t("Conversations"), ++ value: JSON.stringify({ sessionId: row.sessionId, label: row.label }) ++ })); ++ }, ++ onPick({ candidate }) { ++ const { sessionId, label } = JSON.parse(candidate.value); ++ const mention = sessionMention(sessionId, label); ++ return { insert: { source: CHATS_SOURCE, ref: mention, label, appearance: "session", clipboardText: mention } }; ++ }, ++ codec: { clipboardText: (ref) => ref, serialize: (ref) => Promise.resolve(ref) } ++ }; ++ return [bots, chats]; } +-var MODEL_CSS = ` +-.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} +-.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} +-.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} +-.bt-model-trigger[data-missing] .bt-model-provider{color:#c21d2e} +-.bt-model-trigger[data-wide]{display:flex;width:100%;min-height:48px;padding:7px 12px;justify-content:space-between;border-radius:12px;background:var(--bt-card);box-shadow:inset 0 0 0 1px var(--bt-line)} +-.bt-model-trigger[data-wide]:hover,.bt-model-trigger[data-wide][aria-expanded=true]{background:var(--bt-hover)} +-.bt-model-copy{display:flex;flex-direction:column;min-width:0} +-.bt-model-name{font-size:13px;line-height:18px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-model-provider{font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-model-caret{flex:none;color:var(--bt-ink-3);transition:${spring(SPRING_SHAPE, "transform")}} +-.bt-model-trigger[aria-expanded=true] .bt-model-caret{transform:rotate(180deg)} +-.bt-model-menu{overflow-y:auto;scrollbar-width:thin} +-.bt-model-group{flex:none;padding:8px 8px 2px;font-size:11px;line-height:14px;font-weight:500;color:var(--bt-ink-3);text-transform:none} +-.bt-model-group:first-child{padding-top:2px} +-.bt-model-tick{flex:none;width:16px;height:16px;display:inline-flex;color:var(--bt-ink-2)} +-.bt-model-tick svg{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 60ms backwards} +-.bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} +-.bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;margin-left:30px;max-width:min(560px,calc(100% - 30px));max-height:min(240px,36vh);overflow-y:auto;scrollbar-width:thin} +-.bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0} +-.bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere} +-.bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px} +-.bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease} +-.bt-model-chip:hover:not(:disabled){background:var(--bt-hover)} +-.bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} +-.bt-model-chip-quiet{color:var(--bt-ink-2)} +-.bt-model-chip svg{width:14px;height:14px} +-.bt-model-chip:disabled{cursor:default} +-.bt-model-chip:disabled:not([aria-busy]){opacity:.5} +-.bt-model-error{margin-left:30px} +-@keyframes bt-model-in{from{opacity:0;transform:translateY(6px)}} +-.bt-model-spin{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--bt-line-2);border-top-color:var(--bt-ink-2);animation:bt-model-spin .7s linear infinite} +-@keyframes bt-model-spin{to{transform:rotate(360deg)}} +-@media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} +-`; -+// src/client/face-at-filter.js -+var REFERENCE_SOURCE = "reference"; -+var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; -+var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; -+var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; -+function planFaceMenu(state, previous = null) { -+ const hidden = []; -+ const ready = []; -+ let shown = 0; -+ let loading = false; +-// src/client/folder-menu.js +-var import_react14 = require("react"); +-var import_react_dom7 = require("react-dom"); +-var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); +-var Tick = ({ on }) => (0, import_react14.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react14.createElement)(CheckIcon) : null); +-function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { +- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); +- const [info, setInfo] = (0, import_react14.useState)(null); +- const [view, setView] = (0, import_react14.useState)("list"); +- const [draft, setDraft] = (0, import_react14.useState)(""); +- const [listing, setListing] = (0, import_react14.useState)(null); +- const [problem, setProblem] = (0, import_react14.useState)(""); +- const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); +- const toggle = () => { +- if (isOpen) { +- close(); +- return; +- } +- setView("list"); +- setProblem(""); +- void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); +- open(); ++// src/client/face-at-filter.js ++var REFERENCE_SOURCE = "reference"; ++var optionId = (source, index) => `dsh-slash-option-${source}-${index}`; ++var isSessionRow = (group, item) => group.source === REFERENCE_SOURCE && item?.icon === "session"; ++var same = (a, b) => a != null && b != null && a.source === b.source && a.index === b.index; ++function planFaceMenu(state, previous = null) { ++ const hidden = []; ++ const ready = []; ++ let shown = 0; ++ let loading = false; + for (const group of state?.groups ?? []) { + const items = group.items ?? []; + if (group.status !== "ready" && items.length === 0) loading = true; @@ -15867,7 +17038,10 @@ diff --git a/client.js b/client.js +} +function watchFaceMenu(controller, doc = globalThis.document) { + if (typeof controller?.menu?.subscribe !== "function" || !doc?.head) return () => { -+ }; + }; +- const choose = (path) => { +- close(); +- if (path !== (value ?? "")) void onPick(path); + const style = doc.createElement("style"); + style.setAttribute("data-bt-face-at", ""); + doc.head.appendChild(style); @@ -15887,773 +17061,715 @@ diff --git a/client.js b/client.js + if (!stopped) controller.hover(plan.target.source, plan.target.index); + }); + else previous = state.open ? state.highlight : null; -+ }; + }; +- const browse = (path) => { +- setProblem(""); +- void Promise.resolve(actions.folderList(path)).then((next) => { +- setListing(next); +- setView("browse"); +- }).catch((failure) => setProblem(failure?.message ?? String(failure))); + const unsubscribe = controller.menu.subscribe(apply2); + apply2(); + return () => { + stopped = true; + unsubscribe?.(); + style.remove(); -+ }; + }; +- const pickNative = () => { +- close(); +- void run(Promise.resolve(actions.folderPick()).then((path) => path ? onPick(path) : void 0)); +} + - // src/client/speech.js - var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; - var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); -@@ -6008,8 +6008,8 @@ async function speechCatalog(speech) { - function waveBytes(samples, rate = 16e3) { - const bytes = new Uint8Array(44 + samples.length * 2); - const view = new DataView(bytes.buffer); -- const ascii = (at, text) => { -- for (let index = 0; index < text.length; index += 1) bytes[at + index] = text.charCodeAt(index); ++// src/client/speech.js ++var VOICE_BUNDLE = "@deepseek-ai/dsh-experimental-voice-input-bundle"; ++var USABLE = /* @__PURE__ */ new Set(["ready", "standby", "waking"]); ++async function speechCatalog(speech) { ++ const result = await speech.catalog(); ++ if (!result?.ok) throw new Error(result?.error?.message ?? "Speech catalog failed"); ++ const catalog = result.value; ++ const provider = catalog.providers.find((item) => item.id === catalog.selection.providerId); ++ return { catalog, usable: USABLE.has(provider?.preparation?.phase) }; ++} ++function waveBytes(samples, rate = 16e3) { ++ const bytes = new Uint8Array(44 + samples.length * 2); ++ const view = new DataView(bytes.buffer); + const ascii = (at, text2) => { + for (let index = 0; index < text2.length; index += 1) bytes[at + index] = text2.charCodeAt(index); }; - ascii(0, "RIFF"); - view.setUint32(4, 36 + samples.length * 2, true); -@@ -6030,9 +6030,9 @@ function waveBytes(samples, rate = 16e3) { - return bytes; - } - function base64Of(bytes) { -- let text = ""; -- for (let index = 0; index < bytes.length; index += 32768) text += String.fromCharCode(...bytes.subarray(index, index + 32768)); -- return btoa(text); +- const typed = () => { +- const path = draft.trim(); +- if (path !== "") choose(path); ++ ascii(0, "RIFF"); ++ view.setUint32(4, 36 + samples.length * 2, true); ++ ascii(8, "WAVEfmt "); ++ view.setUint32(16, 16, true); ++ view.setUint16(20, 1, true); ++ view.setUint16(22, 1, true); ++ view.setUint32(24, rate, true); ++ view.setUint32(28, rate * 2, true); ++ view.setUint16(32, 2, true); ++ view.setUint16(34, 16, true); ++ ascii(36, "data"); ++ view.setUint32(40, samples.length * 2, true); ++ samples.forEach((sample, index) => { ++ const value = Math.max(-1, Math.min(1, sample)); ++ view.setInt16(44 + index * 2, value < 0 ? Math.round(value * 32768) : Math.round(value * 32767), true); ++ }); ++ return bytes; ++} ++function base64Of(bytes) { + let text2 = ""; + for (let index = 0; index < bytes.length; index += 32768) text2 += String.fromCharCode(...bytes.subarray(index, index + 32768)); + return btoa(text2); - } - var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); - async function openCapture() { -@@ -6115,239 +6115,15 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { - quietSince = 0; - return "speech"; - } -- if (!spokeAt) return "waiting"; -- if (level < quiet) quietSince ||= now; -- return quietSince && now - quietSince >= quietMs ? "done" : "speech"; -- }; --} -- --// src/client/overlay-hooks.js --var import_react11 = require("react"); --var escapeLayers = []; --var onEscapeKey = (event) => { -- if (event.key === "Escape") escapeLayers.at(-1)?.current(); --}; --function useEscape(onClose) { -- const ref = (0, import_react11.useRef)(onClose); -- ref.current = onClose; -- (0, import_react11.useEffect)(() => { -- if (escapeLayers.length === 0) window.addEventListener("keydown", onEscapeKey); -- escapeLayers.push(ref); -- return () => { -- escapeLayers.splice(escapeLayers.indexOf(ref), 1); -- if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); -- }; -- }, []); --} --function usePaneLeft() { -- const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); -- (0, import_react11.useLayoutEffect)(() => { -- const column = document.querySelector('[class*="_footArea"]')?.parentElement; -- if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); -- }, []); -- return paneLeft; --} -- --// src/client/palette.js --var import_react12 = require("react"); --var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives"); --var PALETTE_EXIT_MS = 160; --function CommandPalette({ start, token, roster, actions, useSessions }) { -- const [query, setQuery] = (0, import_react12.useState)(start ?? ""); -- const [cursor, setCursor] = (0, import_react12.useState)(0); -- const [leaving, setLeaving] = (0, import_react12.useState)(false); -- const wanted = query.trim().toLowerCase(); -- const close = () => { -- if (leaving) return; -- setLeaving(true); -- setTimeout(() => actions.closePalette(token), PALETTE_EXIT_MS); -- }; -- useEscape(close); -- const ids = [...roster.bots.map(currentPart), ...roster.rooms.map((item) => item.id)]; -- const found = useSessions((list) => { -- if (wanted.length < 2) return "[]"; -- const hits = []; -- for (const id of ids) { -- const outline = list.projectionsBySession?.[id]?.values?.turnOutline ?? list.byId[id]?.projectionValues?.turnOutline; -- if (!Array.isArray(outline)) continue; -- const hidden = roster.exchangeTurns[id] ?? []; -- for (let index2 = outline.length - 1; index2 >= 0 && hits.length < 30; index2 -= 1) { -- const entry = outline[index2]; -- if (hidden.includes(entry.turn)) continue; -- for (const raw of [entry.response, entry.prompt]) { -- const text = stripHeader(raw ?? "").replace(/\s+/g, " ").trim(); -- const at = text.toLowerCase().indexOf(wanted); -- if (at < 0) continue; -- const from = Math.max(0, at - 24); -- hits.push([id, entry.turn, (from > 0 ? "…" : "") + text.slice(from, at + wanted.length + 60)]); -- break; -- } -- } -- } -- return JSON.stringify(hits); -- }); -- const matchesName = (...fields) => wanted === "" || fields.some((field) => (field ?? "").toLowerCase().includes(wanted)); -- const sections = []; -- const people = [ -- ...roster.bots.filter((bot) => matchesName(bot.name, bot.role)).map((bot) => ({ -- key: bot.id, -- avatar: (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }), -- title: bot.name, -- subtitle: [bot.role, isMainOf(roster, bot.id) ? t("Main Bot") : ""].filter(Boolean).join(" · "), -- badge: bot.hidden ? t("Hidden") : void 0, -- run: async () => { -- if (bot.hidden) await actions.setFlags(bot.id, { hidden: false }); -- actions.openSession(bot.id); -- } -- })), -- ...roster.rooms.filter((room2) => matchesName(room2.name)).map((room2) => ({ -- key: room2.id, -- avatar: (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -- title: room2.name, -- subtitle: t("{count} members", { count: room2.members.length }), -- badge: room2.hidden ? t("Hidden") : void 0, -- run: async () => { -- if (room2.hidden) await actions.setFlags(room2.id, { hidden: false }); -- actions.openSession(room2.id); -- } -- })) -- ]; -- if (people.length) sections.push([t("Bots"), people]); -- const agents = (roster.agents ?? []).filter(() => matchesName("DSH Agent")).map((agent) => ({ -- key: agent.id, -- avatar: (0, import_react12.createElement)("span", { className: "bt-palette-agent" }, (0, import_react12.createElement)(import_dsh_client_ui_primitives3.FishLogo, { size: 18 })), -- title: "DSH Agent", -- subtitle: t("Plain dsh session"), -- run: () => actions.openAgent(agent.id) -- })); -- if (agents.length) sections.push(["DSH Agent", agents]); -- const messages = JSON.parse(found).map(([id, turn, snippet]) => { -- const bot = roster.byId[id]; -- const room2 = roster.roomsById[id]; -- return { -- key: `${id}:${turn}`, -- title: bot?.name ?? room2?.name ?? t("Bot"), -- subtitle: snippet, -- avatar: bot ? (0, import_react12.createElement)(BotAvatar, { bot, size: 24, badge: false, live: false }) : (0, import_react12.createElement)(RoomAvatar, { room: room2, roster, size: 24 }), -- run: () => actions.openSession(id) -- }; -- }); -- if (messages.length) sections.push([t("Messages"), messages]); -- const commands = [ -- { key: "cmd:new", icon: (0, import_react12.createElement)(PlusIcon), title: t("New chat"), run: () => actions.openNewChat("new") }, -- { key: "cmd:bot", icon: (0, import_react12.createElement)(PlusIcon), title: t("Create Bot"), run: () => actions.openNewChat("create") }, -- { key: "cmd:group", icon: (0, import_react12.createElement)(PeopleIcon), title: t("Create group chat"), run: () => actions.openNewChat("group") }, -- { key: "cmd:settings", icon: (0, import_react12.createElement)(GearIcon), title: t("Bot settings"), run: () => actions.openSettings("general") } -- ].filter((command) => matchesName(command.title)); -- if (commands.length) sections.push([t("Commands"), commands]); -- const rows = sections.flatMap(([, items]) => items); -- const active = Math.min(cursor, Math.max(rows.length - 1, 0)); -- const shown = rows.map((row) => row.key).join("\n"); -- const glide = useGlide(`${active} --${shown}`, { axis: "y", ...QUICK_EDGES }); -- const listRef = glide.box; -- useFlip(listRef, shown, "[data-flip]"); -- (0, import_react12.useEffect)(() => { -- listRef.current?.querySelector("[aria-selected=true]")?.scrollIntoView({ block: "nearest" }); -- }, [active, query]); -- const choose = (row) => { -- if (!row || leaving) return; -- void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); -- close(); -+ if (!spokeAt) return "waiting"; -+ if (level < quiet) quietSince ||= now; -+ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; ++} ++var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); ++async function openCapture() { ++ const devices = navigator.mediaDevices; ++ if (!devices?.getUserMedia || typeof MediaRecorder === "undefined") throw captureError("unavailable"); ++ let stream; ++ try { ++ stream = await devices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false }); ++ } catch (error) { ++ throw captureError(error?.name === "NotAllowedError" ? "blocked" : "failed", error); ++ } ++ const audio = new AudioContext(); ++ const analyser = audio.createAnalyser(); ++ analyser.fftSize = 512; ++ audio.createMediaStreamSource(stream).connect(analyser); ++ const frame = new Float32Array(analyser.fftSize); ++ const recorder = new MediaRecorder(stream); ++ const chunks = []; ++ recorder.ondataavailable = (event) => { ++ if (event.data.size > 0) chunks.push(event.data); }; -- let index = -1; -- return (0, import_react12.createElement)( +- const known = info?.folders ?? []; +- const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; +- const listView = (0, import_react14.createElement)( - "div", -- { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, -- (0, import_react12.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), -- (0, import_react12.createElement)( -- "div", -- { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, -- (0, import_react12.createElement)( -- "div", -- { className: "bt-palette-head" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), -- (0, import_react12.createElement)("input", { -- autoFocus: true, -- value: query, -- placeholder: t("Search Bots, chats and messages"), -- "aria-label": t("Search"), -- role: "combobox", -- "aria-expanded": true, -- "aria-controls": "bt-palette-list", -- onChange: (event) => { -- setQuery(event.target.value); -- setCursor(0); -- }, -- onKeyDown: (event) => { -- if (event.nativeEvent.isComposing) return; -- if (event.key === "ArrowDown") { -- event.preventDefault(); -- setCursor(Math.min(active + 1, rows.length - 1)); -- } else if (event.key === "ArrowUp") { -- event.preventDefault(); -- setCursor(Math.max(active - 1, 0)); -- } else if (event.key === "Enter") { -- event.preventDefault(); -- choose(rows[active]); -- } -- } -- }), -- query ? (0, import_react12.createElement)("button", { type: "button", className: "bt-palette-clear", "aria-label": t("Clear search"), onClick: () => { -- setQuery(""); -- setCursor(0); -- } }, (0, import_react12.createElement)(CloseIcon)) : null -- ), -- (0, import_react12.createElement)( -- "div", -- { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, -- (0, import_react12.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- rows.length === 0 ? (0, import_react12.createElement)( -- "div", -- { className: "bt-palette-empty" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-icon", "aria-hidden": true }, (0, import_react12.createElement)(SearchIcon)), -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-label" }, t("No results")), -- (0, import_react12.createElement)("span", { className: "bt-palette-empty-hint" }, t("Try a Bot name or words from a message")) -- ) : sections.map(([title, items]) => (0, import_react12.createElement)( -- import_react12.Fragment, -- { key: title }, -- (0, import_react12.createElement)("div", { className: "bt-palette-section", role: "presentation", "data-flip": `section:${title}` }, title), -- items.map((row) => { -- index += 1; -- const at = index; -- return (0, import_react12.createElement)( -- "button", -- { -- key: row.key, -- type: "button", -- role: "option", -- className: "bt-palette-row", -- "aria-selected": at === active, -- "data-flip": row.key, -- onMouseMove: () => { -- if (cursor !== at) setCursor(at); -- }, -- onClick: () => choose(row) -- }, -- (0, import_react12.createElement)("span", { className: "bt-palette-lead" }, row.avatar ?? (0, import_react12.createElement)("span", { className: "bt-palette-cmd" }, row.icon)), -- (0, import_react12.createElement)( -- "span", -- { className: "bt-palette-text" }, -- (0, import_react12.createElement)("span", { className: "bt-palette-title" }, row.title), -- row.subtitle ? (0, import_react12.createElement)("span", { className: "bt-palette-sub" }, row.subtitle) : null -- ), -- row.badge ? (0, import_react12.createElement)("span", { className: "bt-palette-badge" }, row.badge) : null -- ); -- }) -- )) -- ) -- ) +- { key: "list", className: "bt-folder-view" }, +- (0, import_react14.createElement)( +- "button", +- { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, +- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, fallback), +- (0, import_react14.createElement)(Tick, { on: !value }) +- ), +- folders.length > 0 ? (0, import_react14.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, +- folders.map((folder) => (0, import_react14.createElement)( +- "button", +- { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, +- (0, import_react14.createElement)("span", { className: "bt-folder-copy" }, (0, import_react14.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react14.createElement)("span", { className: "bt-folder-path" }, folder.path)), +- (0, import_react14.createElement)(Tick, { on: folder.path === value }) +- )), +- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- info?.picker === "native" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, +- info?.picker === "browse" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, +- (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => { +- setDraft(value ?? ""); +- setView("type"); +- } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) +- ); +- const typeView = (0, import_react14.createElement)( +- "div", +- { key: "type", className: "bt-folder-view" }, +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), +- (0, import_react14.createElement)("input", { +- className: "bt-folder-input", +- autoFocus: true, +- value: draft, +- spellCheck: false, +- "aria-label": t("Folder path"), +- placeholder: t("Full path of a folder"), +- onChange: (event) => setDraft(event.target.value), +- onKeyDown: (event) => { +- if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); ++ recorder.start(); ++ let closed = false; ++ const close = () => { ++ if (closed) return; ++ closed = true; ++ if (recorder.state !== "inactive") recorder.stop(); ++ stream.getTracks().forEach((track) => track.stop()); ++ void audio.close().catch(() => { ++ }); ++ }; ++ return { ++ level() { ++ if (closed) return 0; ++ analyser.getFloatTimeDomainData(frame); ++ let sum = 0; ++ for (const sample of frame) sum += sample * sample; ++ return Math.min(1, Math.sqrt(sum / frame.length) * 4); ++ }, ++ cancel: close, ++ async finish(maxSeconds) { ++ if (closed || recorder.state === "inactive") { ++ close(); ++ return null; + } +- }), +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) +- ); +- const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; +- const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; +- const browseView = listing ? (0, import_react14.createElement)( +- "div", +- { key: `browse:${listing.path}`, className: "bt-folder-view" }, +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), +- (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), +- parent ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, +- inside.map((entry) => (0, import_react14.createElement)( +- "button", +- { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, +- (0, import_react14.createElement)(FolderIcon), +- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, entry.name), +- (0, import_react14.createElement)(ChevronRightIcon) +- )), +- inside.length === 0 ? (0, import_react14.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, +- listing.truncated ? (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, +- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), +- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) +- ) : null; +- const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; +- return (0, import_react14.createElement)( +- import_react14.Fragment, +- null, +- (0, import_react14.createElement)( +- "button", +- { +- ref: button, +- type: "button", +- className: "bt-model-trigger", +- "data-wide": wide || void 0, +- "aria-haspopup": "menu", +- "aria-expanded": isOpen, +- "aria-label": t("Working folder: {folder}", { folder: value ?? fallback }), +- title: value, +- onClick: toggle +- }, +- (0, import_react14.createElement)( +- "span", +- { className: "bt-model-copy" }, +- (0, import_react14.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), +- value ? (0, import_react14.createElement)("span", { className: "bt-model-provider" }, value) : null +- ), +- (0, import_react14.createElement)(Caret) +- ), +- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( +- "div", +- { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, +- shownView, +- problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null +- ), document.body) : null - ); ++ try { ++ await new Promise((resolve) => { ++ recorder.onstop = resolve; ++ recorder.stop(); ++ }); ++ stream.getTracks().forEach((track) => track.stop()); ++ const blob = new Blob(chunks, { type: recorder.mimeType }); ++ if (blob.size === 0) return null; ++ const decoded = await audio.decodeAudioData(await blob.arrayBuffer()); ++ const length = Math.max(1, Math.floor(Math.min(decoded.duration, maxSeconds) * 16e3)); ++ const offline = new OfflineAudioContext(1, length, 16e3); ++ const source = offline.createBufferSource(); ++ source.buffer = decoded; ++ source.connect(offline.destination); ++ source.start(); ++ return waveBytes((await offline.startRendering()).getChannelData(0)); ++ } finally { ++ close(); ++ } ++ } ++ }; ++} ++async function transcribeWave(speech, catalog, wave, signal) { ++ if (wave.byteLength > catalog.maxAudioBytes) throw captureError("too-long"); ++ const result = await speech.transcribe({ audioBase64: base64Of(wave), ...catalog.selection }, signal); ++ if (!result?.ok) throw Object.assign(new Error(result?.error?.message ?? "Transcription failed"), { kind: "service" }); ++ return String(result.value?.text ?? "").trim(); ++} ++function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { ++ let spokeAt = 0; ++ let quietSince = 0; ++ return (level, now) => { ++ if (level >= loud) { ++ spokeAt ||= now; ++ quietSince = 0; ++ return "speech"; ++ } ++ if (!spokeAt) return "waiting"; ++ if (level < quiet) quietSince ||= now; ++ return quietSince && now - quietSince >= quietMs ? "done" : "speech"; ++ }; } +-var FOLDER_CSS = ` +-.bt-folder-view{display:flex;flex-direction:column;gap:2px;animation:bt-veil-in .14s ease-out} +-.bt-folder-copy{flex:1;display:flex;flex-direction:column;min-width:0} +-.bt-folder-copy .bt-menu-label{flex:none} +-.bt-folder-path{min-width:0;font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-folder-here{padding:0 8px 4px;white-space:normal;overflow-wrap:anywhere} +-.bt-menu .bt-folder-back{color:var(--bt-ink-2);font-weight:500} +-.bt-folder-input{height:30px;margin:2px 4px;padding:0 8px;border:1px solid var(--bt-line-2);border-radius:8px;background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;outline:none;box-sizing:border-box} +-.bt-folder-input:focus{border-color:var(--bt-accent)} +-.bt-menu .bt-folder-use{justify-content:center;font-weight:500;color:var(--bt-accent)} +-.bt-menu .bt-folder-use:disabled{color:var(--bt-ink-3);cursor:default;background:none} +-.bt-folder-error{padding:4px 8px;font-size:12px;line-height:16px;color:#c21d2e;overflow-wrap:anywhere} +-@media (prefers-reduced-motion:reduce){.bt-folder-view{animation:none}} +-`; - // src/client/model-menu.js --var import_react13 = require("react"); --var import_react_dom6 = require("react-dom"); +-// src/client/schedules.js +-var import_react15 = require("react"); +-var POLL_MS = 3e4; +-var shared = null; +-var listeners = /* @__PURE__ */ new Set(); +-function publish(view) { +- shared = view; +- for (const listener of listeners) listener(view); ++// src/client/model-menu.js +var import_react23 = require("react"); +var import_react_dom8 = require("react-dom"); - var MENU_EXIT_MS = 120; - function groupModels(models) { - const groups = /* @__PURE__ */ new Map(); -@@ -6367,17 +6367,10 @@ function groupModels(models) { - }); ++var MENU_EXIT_MS = 120; ++function groupModels(models) { ++ const groups = /* @__PURE__ */ new Map(); ++ for (const model of models ?? []) { ++ const provider = model.provider ?? model.providerName ?? model.ref.split("/")[0]; ++ if (!groups.has(provider)) groups.set(provider, []); ++ groups.get(provider).push(model); ++ } ++ const names = /* @__PURE__ */ new Map(); ++ for (const [provider, list] of groups) { ++ const name = list[0].providerName ?? provider; ++ names.set(name, (names.get(name) ?? 0) + 1); ++ } ++ return [...groups].map(([provider, list]) => { ++ const name = list[0].providerName ?? provider; ++ return [names.get(name) > 1 ? `${name} (${provider})` : name, list]; ++ }); } - var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); --function modelOf(bot, roster) { -- const ref = bot.model ?? bot.appliedModel; -- if (!ref) return void 0; -- const served = servedOf(roster, ref); -- const [provider, ...rest] = ref.split("/"); -- return { ref, name: served?.name ?? rest.join("/"), provider: served?.providerName ?? provider, served: served !== void 0 }; --} - var effortCache = /* @__PURE__ */ new Map(); - function useEfforts(served, ref, actions) { -- const [fetched, setFetched] = (0, import_react13.useState)(void 0); -- (0, import_react13.useEffect)(() => { +-function useSchedules(actions) { +- const [view, setView] = (0, import_react15.useState)(shared); +- (0, import_react15.useEffect)(() => { +- listeners.add(setView); +- let stopped = false; +- let timer; +- const load = () => actions.schedules?.().then((next) => { +- if (!stopped) publish(next); ++var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); ++var effortCache = /* @__PURE__ */ new Map(); ++function useEfforts(served, ref, actions) { + const [fetched, setFetched] = (0, import_react23.useState)(void 0); + (0, import_react23.useEffect)(() => { - setFetched(void 0); - if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; - let live = true; -@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { - ]; ++ setFetched(void 0); ++ if (ref === void 0 || served?.reasoning !== void 0 || actions?.efforts === void 0) return void 0; ++ let live = true; ++ if (effortCache.has(ref)) setFetched(effortCache.get(ref)); ++ else void Promise.resolve(actions.efforts(ref)).then((value) => { ++ const reasoning = value?.efforts ? value : null; ++ effortCache.set(ref, reasoning); ++ if (live) setFetched(reasoning); + }).catch(() => { +- }).finally(() => { +- if (!stopped) timer = setTimeout(load, POLL_MS); + }); +- load(); + return () => { +- stopped = true; +- clearTimeout(timer); +- listeners.delete(setView); ++ live = false; + }; +- }, []); +- return view; ++ }, [ref, served?.reasoning !== void 0, actions]); ++ return served?.reasoning ?? fetched; } - function EffortRows({ reasoning, current, onPick }) { -- return (0, import_react13.createElement)( -- import_react13.Fragment, +-var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone; +-var clock2 = (time) => String(time ?? "").slice(0, 5); +-var when2 = (ms, options) => new Intl.DateTimeFormat(dateLocale(), options).format(ms); +-function dayAndTime(ms) { +- const sameYear = new Date(ms).getFullYear() === (/* @__PURE__ */ new Date()).getFullYear(); +- return when2(ms, { ...sameYear ? {} : { year: "numeric" }, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); ++function effortRows(reasoning) { ++ if (reasoning?.efforts === void 0 || reasoning.efforts.length === 0) return []; ++ return [ ++ { effort: void 0, label: t("Default") }, ++ ...reasoning.efforts.map((effort) => ({ effort: effort.id, label: effort.name })) ++ ]; + } +-function weekdayNames(days) { +- const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); +- return names.join(dateLocale().startsWith("zh") ? "、" : ", "); ++function EffortRows({ reasoning, current, onPick }) { + return (0, import_react23.createElement)( + import_react23.Fragment, - null, -- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), -- effortRows(reasoning).map((row) => (0, import_react13.createElement)( ++ null, + (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Thinking level")), + effortRows(reasoning).map((row) => (0, import_react23.createElement)( - "button", - { - key: row.effort ?? "default", -@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { - "aria-checked": row.effort === current, - onClick: () => onPick(row.effort) - }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, row.label), -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react13.createElement)(CheckIcon) : null) ++ "button", ++ { ++ key: row.effort ?? "default", ++ type: "button", ++ role: "menuitemradio", ++ "aria-checked": row.effort === current, ++ onClick: () => onPick(row.effort) ++ }, + (0, import_react23.createElement)("span", { className: "bt-menu-label" }, row.label), + (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, row.effort === current ? (0, import_react23.createElement)(CheckIcon) : null) - )) - ); ++ )) ++ ); } - function ModelRows({ roster, current, onPick }) { -- return groupModels(roster.models).map(([provider, list]) => (0, import_react13.createElement)( -- import_react13.Fragment, +-function everyText(seconds) { +- if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); +- if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); +- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); ++function ModelRows({ roster, current, onPick }) { + return groupModels(roster.models).map(([provider, list]) => (0, import_react23.createElement)( + import_react23.Fragment, - { key: list[0].provider ?? provider }, -- (0, import_react13.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), -- list.map((model) => (0, import_react13.createElement)( ++ { key: list[0].provider ?? provider }, + (0, import_react23.createElement)("div", { className: "bt-model-group", role: "presentation" }, provider), + list.map((model) => (0, import_react23.createElement)( - "button", - { - key: model.ref, -@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { - title: model.ref, - onClick: () => onPick(model.ref) - }, -- (0, import_react13.createElement)("span", { className: "bt-menu-label" }, model.name), -- model.ref === roster.defaultModel ? (0, import_react13.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, -- (0, import_react13.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react13.createElement)(CheckIcon) : null) ++ "button", ++ { ++ key: model.ref, ++ type: "button", ++ role: "menuitemradio", ++ "aria-checked": model.ref === current, ++ title: model.ref, ++ onClick: () => onPick(model.ref) ++ }, + (0, import_react23.createElement)("span", { className: "bt-menu-label" }, model.name), + model.ref === roster.defaultModel ? (0, import_react23.createElement)("span", { className: "bt-menu-hint" }, t("Default")) : null, + (0, import_react23.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, model.ref === current ? (0, import_react23.createElement)(CheckIcon) : null) - )) - )); ++ )) ++ )); } --var Caret = () => (0, import_react13.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react13.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); +-function timingText(task) { +- const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; +- switch (task.kind) { +- case "after": +- case "at": +- return t("Once, {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }); +- case "every": +- return everyText(task.everySeconds); +- case "daily": +- return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; +- case "weekly": { +- const days = (task.weekdays ?? []).join(); +- const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); +- return `${text}${zone}`; +- } +- case "cron": +- return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; +- default: +- return ""; +- } +var Caret = () => (0, import_react23.createElement)("svg", { className: "bt-model-caret", width: 12, height: 12, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true }, (0, import_react23.createElement)("path", { d: "M5.5 8l4.5 4.5L14.5 8" })); - function useAnchoredMenu() { -- const [place, setPlace] = (0, import_react13.useState)(null); ++function useAnchoredMenu() { + const [place, setPlace] = (0, import_react23.useState)(null); - const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); -- const button = (0, import_react13.useRef)(null); -- const menu = (0, import_react13.useRef)(null); ++ const [shownPlace, leaving] = useLinger(place, MENU_EXIT_MS); + const button = (0, import_react23.useRef)(null); + const menu = (0, import_react23.useRef)(null); - const close = () => setPlace(null); -- (0, import_react13.useEffect)(() => { ++ const close = () => setPlace(null); + (0, import_react23.useEffect)(() => { - if (place === null) return void 0; - const onDown = (event) => { - if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; -@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { - return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; ++ if (place === null) return void 0; ++ const onDown = (event) => { ++ if (menu.current?.contains(event.target) || button.current?.contains(event.target)) return; ++ close(); ++ }; ++ const onKey = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ close(); ++ button.current?.focus(); ++ } ++ }; ++ const onScroll = (event) => { ++ if (!menu.current?.contains(event.target)) close(); ++ }; ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ window.addEventListener("scroll", onScroll, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ window.removeEventListener("scroll", onScroll, true); ++ }; ++ }, [place]); ++ const open = () => { ++ const rect = button.current.getBoundingClientRect(); ++ const width = Math.max(240, rect.width); ++ const left = Math.max(8, Math.min(rect.right - width, window.innerWidth - width - 8)); ++ const below = window.innerHeight - rect.bottom; ++ setPlace(below >= 260 || below >= rect.top ? { left, width, top: rect.bottom + 4, maxHeight: below - 16, "--bt-origin": "top right" } : { left, width, bottom: window.innerHeight - rect.top + 4, maxHeight: rect.top - 16, "--bt-origin": "bottom right" }); ++ }; ++ return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } - function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { +-var nextText = (task) => task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); +-function TaskRow({ task, actions, onView, showBot, roster }) { +- const [confirming, setConfirming] = (0, import_react15.useState)(false); +- const [busy, setBusy] = (0, import_react15.useState)(false); +- (0, import_react15.useEffect)(() => { +- if (!confirming) return void 0; +- const timer = setTimeout(() => setConfirming(false), 3e3); +- return () => clearTimeout(timer); +- }, [confirming]); +- const remove = () => { +- if (!confirming) { +- setConfirming(true); ++function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { + if (bot?.remote !== void 0) return null; - const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); - const shown = modelOf(bot, roster); - const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6511,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 { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); ++ const shown = modelOf(bot, roster); ++ const current = bot.model ?? bot.appliedModel; ++ const served = servedOf(roster, current); ++ const reasoning = useEfforts(served, current, actions); ++ const picked = reasoning?.efforts?.some((effort) => effort.id === bot.effort) ? bot.effort : void 0; ++ const currentEffort = picked ?? reasoning?.defaultEffort; ++ const effortLabel = currentEffort === void 0 ? reasoning?.efforts?.length > 0 ? t("Default") : void 0 : reasoning.efforts.find((effort) => effort.id === currentEffort)?.name ?? currentEffort; ++ const toggle = () => { ++ if (isOpen) { ++ close(); + return; + } +- setBusy(true); +- void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); ++ void actions?.refreshModels?.(); ++ open(); }; - const models = roster.models ?? []; -- return (0, import_react13.createElement)( -- import_react13.Fragment, +- const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; +- return (0, import_react15.createElement)( +- "div", +- { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, +- showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, +- (0, import_react15.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), +- task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null ++ const pick2 = (ref) => { ++ close(); ++ if (onPick) onPick(ref); ++ else if (ref !== current || !bot.model) void run(actions.updateBot(bot.id, { model: ref })); ++ }; ++ const pickEffort = (effort) => { ++ close(); ++ if (onPickEffort) onPickEffort(effort); ++ else if (effort !== bot.effort) void run(actions.updateBot(bot.id, { effort: effort ?? null })); ++ }; ++ const models = roster.models ?? []; + return (0, import_react23.createElement)( + import_react23.Fragment, - null, -- (0, import_react13.createElement)( ++ null, + (0, import_react23.createElement)( - "button", - { - ref: button, -@@ -6527,37 +6527,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 - }, -- (0, import_react13.createElement)( ++ "button", ++ { ++ ref: button, ++ type: "button", ++ className: "bt-model-trigger", ++ "data-wide": wide || void 0, ++ "aria-haspopup": "menu", ++ "aria-expanded": isOpen, ++ "aria-label": t("Model: {model}", { model: shown?.name ?? t("none") }), ++ "data-missing": shown !== void 0 && !shown.served && models.length > 0 ? "" : void 0, ++ onClick: toggle ++ }, + (0, import_react23.createElement)( - "span", - { className: "bt-model-copy" }, -- (0, import_react13.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), -- shown ? (0, import_react13.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null ++ "span", ++ { className: "bt-model-copy" }, + (0, import_react23.createElement)("span", { className: "bt-model-name" }, shown?.name ?? t("Choose a model")), + shown ? (0, import_react23.createElement)("span", { className: "bt-model-provider" }, shown.served || models.length === 0 ? effortLabel === void 0 ? shown.provider : `${shown.provider} · ${effortLabel}` : t("{provider} · not available", { provider: shown.provider })) : null - ), -- (0, import_react13.createElement)(Caret) ++ ), + (0, import_react23.createElement)(Caret) ), -- shownPlace ? (0, import_react_dom6.createPortal)((0, import_react13.createElement)( +- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), +- (0, import_react15.createElement)("button", { +- type: "button", +- className: "bt-mini bt-task-remove", +- "data-confirm": confirming || void 0, +- disabled: busy, +- "aria-label": confirming ? t("Click again to delete") : t("Delete task"), +- title: confirming ? void 0 : t("Delete task"), +- onClick: remove +- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) + shownPlace ? (0, import_react_dom8.createPortal)((0, import_react23.createElement)( - "div", - { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, -- models.length > 0 ? (0, import_react13.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react13.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), -- effortRows(reasoning).length > 0 ? (0, import_react13.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, -- (0, import_react13.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react13.createElement)("button", { type: "button", role: "menuitem", onClick: () => { ++ "div", ++ { ref: menu, className: `bt-menu bt-model-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Model for {name}", { name: bot.name }), style: shownPlace, "data-leaving": leaving || void 0 }, + models.length > 0 ? (0, import_react23.createElement)(ModelRows, { roster, current, onPick: pick2 }) : (0, import_react23.createElement)("div", { className: "bt-model-none" }, t("No models are set up in DeepSeek Harness yet.")), + effortRows(reasoning).length > 0 ? (0, import_react23.createElement)(EffortRows, { reasoning, current: picked, onPick: pickEffort }) : null, + (0, import_react23.createElement)("div", { className: "bt-menu-sep", role: "separator" }), + (0, import_react23.createElement)("button", { type: "button", role: "menuitem", onClick: () => { - close(); -- openHarnessSettings(); -- } }, (0, import_react13.createElement)(SlidersIcon), (0, import_react13.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) ++ close(); + openHarnessModels(); + } }, (0, import_react23.createElement)(SlidersIcon), (0, import_react23.createElement)("span", { className: "bt-menu-label" }, t("Models and providers…"))) - ), document.body) : null ++ ), document.body) : null ); } - function ModelChoiceDock({ sessionId, useRoster, actions }) { - const roster = useRoster((value) => value); - const bot = roster.byId[sessionId]; -- const [busy, setBusy] = (0, import_react13.useState)(""); -- const [error, setError] = (0, import_react13.useState)(""); +-var pad = (number) => String(number).padStart(2, "0"); +-function inAnHour() { +- const date = new Date(Date.now() + 36e5); +- date.setMinutes(0, 0, 0); +- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; +-} +-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 }) { +- const [target, setTarget] = (0, import_react15.useState)(start ?? roster.bots[0]?.id ?? ""); +- const [title, setTitle] = (0, import_react15.useState)(""); +- const [prompt, setPrompt] = (0, import_react15.useState)(""); +- const [kind, setKind] = (0, import_react15.useState)("daily"); +- const [at, setAt] = (0, import_react15.useState)(inAnHour); +- const [time, setTime] = (0, import_react15.useState)("09:00"); +- const [weekdays, setWeekdays] = (0, import_react15.useState)([1, 2, 3, 4, 5]); +- const [count, setCount] = (0, import_react15.useState)(1); +- const [unit, setUnit] = (0, import_react15.useState)("hours"); +- const [busy, setBusy] = (0, import_react15.useState)(false); +- const [error, setError] = (0, import_react15.useState)(""); +- 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]) }; +- return { kind, time, timeZone: localZone(), ...kind === "weekly" ? { weekdays } : {} }; +- }; +- const submit = (event) => { +- event.preventDefault(); +- if (busy) return; +- setBusy(true); ++function ModelChoiceDock({ sessionId, useRoster, actions }) { ++ const roster = useRoster((value) => value); ++ const bot = roster.byId[sessionId]; + const [busy, setBusy] = (0, import_react23.useState)(""); + const [error, setError] = (0, import_react23.useState)(""); - const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; -- (0, import_react13.useEffect)(() => { ++ const shown = Boolean(bot) && currentPart(bot) === sessionId && roster.ready; + (0, import_react23.useEffect)(() => { - if (shown) void actions.refreshModels?.(); - }, [shown, sessionId]); - if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; ++ if (shown) void actions.refreshModels?.(); ++ }, [shown, sessionId]); ++ if (!bot || currentPart(bot) !== sessionId || !roster.ready) return null; + if (bot.remote) return bot.remote.gone ? (0, import_react23.createElement)(RemoteGoneDock, { bot }) : null; - 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; -@@ -6569,13 +6569,13 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { - void actions.refreshModels?.(); - }).finally(() => setBusy("")); ++ 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; ++ const pick2 = (ref) => { ++ setBusy(ref); + setError(""); +- let when3; +- try { +- when3 = timing(); +- } catch { +- setError(t("Pick a date and time")); +- setBusy(false); +- return; +- } +- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { +- setError(hostText(failure?.message ?? String(failure))); +- setBusy(false); +- }); ++ void Promise.resolve(actions.updateBot(bot.id, { model: ref })).catch((failure) => { ++ setError(failure?.message ?? String(failure)); ++ void actions.refreshModels?.(); ++ }).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 }); -- const choices = models.length === 0 ? setupChoices(actions).map(([label, run]) => (0, import_react13.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)) : [ -- ...groupModels(models).map(([provider, list]) => (0, import_react13.createElement)( +- const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); +- return (0, import_react15.createElement)( +- "form", +- { className: "bt-set-card bt-task-form", onSubmit: submit }, +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), +- (0, import_react15.createElement)( +- "select", +- { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, +- (0, import_react15.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react15.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), +- roster.rooms.length > 0 ? (0, import_react15.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react15.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null + const text2 = 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 }); + const choices = models.length === 0 ? [...setupChoices(actions).map(([label, run]) => (0, import_react23.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), (0, import_react23.createElement)(KeyHelpLink, { key: "key-help" })] : [ + ...groupModels(models).map(([provider, list]) => (0, import_react23.createElement)( - "div", - { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, -- (0, import_react13.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), -- (0, import_react13.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react13.createElement)("button", { ++ "div", ++ { key: list[0].provider ?? provider, className: "bt-model-choice-group", role: "group", "aria-label": provider }, + (0, import_react23.createElement)("div", { className: "bt-model-choice-provider", "aria-hidden": true }, provider), + (0, import_react23.createElement)("div", { className: "bt-model-choice-chips" }, list.map((model) => (0, import_react23.createElement)("button", { - key: model.ref, - type: "button", - className: "bt-model-chip", -@@ -6584,586 +6584,411 @@ 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) -- }, model.name, busy === model.ref ? (0, import_react13.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) -+ }, model.name, busy === model.ref ? (0, import_react23.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) - )), -- (0, import_react13.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessSettings }, (0, import_react13.createElement)(SlidersIcon), t("Manage models")) -+ (0, import_react23.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react23.createElement)(SlidersIcon), t("Manage models")) - ]; -- return (0, import_react13.createElement)( -+ return (0, import_react23.createElement)( - "div", - { className: "bt-model-dock" }, -- (0, import_react13.createElement)( -+ (0, import_react23.createElement)( - "div", - { className: "bt-astack", "data-lead": "" }, -- (0, import_react13.createElement)( -+ (0, import_react23.createElement)( - "div", - { className: "bt-msg" }, -- (0, import_react13.createElement)( -+ (0, import_react23.createElement)( - "div", -- { className: "bt-msg-line" }, -- (0, import_react13.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react13.createElement)("div", { className: "bt-bubble" }, text) -- ) -- ) -- ), -- (0, import_react13.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), -- error ? (0, import_react13.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null -- ); --} --function setupChoices(actions) { -- return [ -- actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, -- [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], -- [t("Open settings to set up a custom model"), openHarnessSettings] -- ].filter(Boolean); --} --function DeepSeekSignIn({ useUi, actions }) { -- const open = useUi((state) => state.signIn); -- const entry = open ? actions.signInEntry?.() : null; -- if (!entry) return null; -- return (0, import_react13.createElement)(SignInHost, { key: open, entry, actions }); --} --function SignInHost({ entry, actions }) { -- const [ops] = (0, import_react13.useState)(() => entry.inject?.() ?? {}); -- const close = () => actions.closeSignIn(); -- const useHook = (source) => (selector) => (0, import_react13.useSyncExternalStore)(source.subscribe, () => selector(source.getSnapshot())); -- const [hooks] = (0, import_react13.useState)(() => ({ useAccount: useHook(ops.hooks.account), useTheme: useHook(ops.hooks.theme) })); -- return (0, import_react13.createElement)(entry.component, { -- ...ops, -- ...hooks, -- t: actions.accountT(), -- complete: close, -- useApiKey: () => { -- close(); -- openHarnessOnboarding("deepseek-official"); -- } -- }); --} --var MODEL_CSS = ` --.bt-model-trigger{display:inline-flex;align-items:center;gap:8px;max-width:100%;min-width:0;height:auto;min-height:32px;padding:4px 8px 4px 10px;border:0;border-radius:8px;background:var(--bt-hover);color:var(--bt-ink);font:inherit;text-align:left;cursor:pointer;transition:background-color .12s ease} --.bt-model-trigger:hover,.bt-model-trigger[aria-expanded=true]{background:var(--bt-active)} --.bt-model-trigger:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} --.bt-model-trigger[data-missing] .bt-model-provider{color:#c21d2e} --.bt-model-trigger[data-wide]{display:flex;width:100%;min-height:48px;padding:7px 12px;justify-content:space-between;border-radius:12px;background:var(--bt-card);box-shadow:inset 0 0 0 1px var(--bt-line)} --.bt-model-trigger[data-wide]:hover,.bt-model-trigger[data-wide][aria-expanded=true]{background:var(--bt-hover)} --.bt-model-copy{display:flex;flex-direction:column;min-width:0} --.bt-model-name{font-size:13px;line-height:18px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-model-provider{font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-model-caret{flex:none;color:var(--bt-ink-3);transition:${spring(SPRING_SHAPE, "transform")}} --.bt-model-trigger[aria-expanded=true] .bt-model-caret{transform:rotate(180deg)} --.bt-model-menu{overflow-y:auto;scrollbar-width:thin} --.bt-model-group{flex:none;padding:8px 8px 2px;font-size:11px;line-height:14px;font-weight:500;color:var(--bt-ink-3);text-transform:none} --.bt-model-group:first-child{padding-top:2px} --.bt-model-tick{flex:none;width:16px;height:16px;display:inline-flex;color:var(--bt-ink-2)} --.bt-model-tick svg{animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} 60ms backwards} --.bt-model-none{padding:8px;font-size:13px;line-height:18px;color:var(--bt-ink-2)} --.bt-model-dock{width:100%;max-width:var(--dsh-chat-content-width,748px);margin:0 auto;display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:0 0 10px;box-sizing:border-box;animation:bt-model-in ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} --.bt-model-chips{display:flex;flex-wrap:wrap;gap:6px;margin-left:30px;max-width:min(560px,calc(100% - 30px));max-height:min(240px,36vh);overflow-y:auto;scrollbar-width:thin} --.bt-model-choice-group{display:flex;flex-direction:column;gap:6px;width:100%;min-width:0} --.bt-model-choice-provider{padding-top:4px;font-size:12px;line-height:16px;font-weight:500;color:var(--bt-ink-2);overflow-wrap:anywhere} --.bt-model-choice-chips{display:flex;flex-wrap:wrap;gap:6px} --.bt-model-chip{display:inline-flex;align-items:center;gap:6px;min-height:30px;padding:5px 12px;border:.5px solid var(--bt-line-2);border-radius:999px;background:var(--bt-surface,transparent);color:var(--bt-ink);font:inherit;font-size:13px;line-height:18px;cursor:pointer;transition:background-color .12s ease,border-color .12s ease} --.bt-model-chip:hover:not(:disabled){background:var(--bt-hover)} --.bt-model-chip:focus-visible{outline:2px solid var(--bt-accent);outline-offset:1px} --.bt-model-chip-quiet{color:var(--bt-ink-2)} --.bt-model-chip svg{width:14px;height:14px} --.bt-model-chip:disabled{cursor:default} --.bt-model-chip:disabled:not([aria-busy]){opacity:.5} --.bt-model-error{margin-left:30px} --@keyframes bt-model-in{from{opacity:0;transform:translateY(6px)}} --.bt-model-spin{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--bt-line-2);border-top-color:var(--bt-ink-2);animation:bt-model-spin .7s linear infinite} --@keyframes bt-model-spin{to{transform:rotate(360deg)}} --@media (prefers-reduced-motion:reduce){.bt-model-dock{animation:none}.bt-model-spin{animation-duration:2s}.bt-model-caret{transition:none}.bt-model-tick svg{animation:none}} --`; -- --// src/client/folder-menu.js --var import_react14 = require("react"); --var import_react_dom7 = require("react-dom"); --var folderName = (path) => String(path).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || String(path); --var Tick = ({ on }) => (0, import_react14.createElement)("span", { className: "bt-model-tick", "aria-hidden": true }, on ? (0, import_react14.createElement)(CheckIcon) : null); --function FolderMenu({ value, group = false, actions, run, onPick, wide = false }) { -- const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); -- const [info, setInfo] = (0, import_react14.useState)(null); -- const [view, setView] = (0, import_react14.useState)("list"); -- const [draft, setDraft] = (0, import_react14.useState)(""); -- const [listing, setListing] = (0, import_react14.useState)(null); -- const [problem, setProblem] = (0, import_react14.useState)(""); -- const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); -- const toggle = () => { -- if (isOpen) { -- close(); -- return; -- } -- setView("list"); -- setProblem(""); -- void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); -- open(); -- }; -- const choose = (path) => { -- close(); -- if (path !== (value ?? "")) void onPick(path); -- }; -- const browse = (path) => { -- setProblem(""); -- void Promise.resolve(actions.folderList(path)).then((next) => { -- setListing(next); -- setView("browse"); -- }).catch((failure) => setProblem(failure?.message ?? String(failure))); -- }; -- const pickNative = () => { -- close(); -- void run(Promise.resolve(actions.folderPick()).then((path) => path ? onPick(path) : void 0)); -- }; -- const typed = () => { -- const path = draft.trim(); -- if (path !== "") choose(path); -- }; -- const known = info?.folders ?? []; -- const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; -- const listView = (0, import_react14.createElement)( -- "div", -- { key: "list", className: "bt-folder-view" }, -- (0, import_react14.createElement)( -- "button", -- { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, -- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, fallback), -- (0, import_react14.createElement)(Tick, { on: !value }) -- ), -- folders.length > 0 ? (0, import_react14.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, -- folders.map((folder) => (0, import_react14.createElement)( -- "button", -- { key: folder.path, type: "button", role: "menuitemradio", "aria-checked": folder.path === value, title: folder.path, onClick: () => choose(folder.path) }, -- (0, import_react14.createElement)("span", { className: "bt-folder-copy" }, (0, import_react14.createElement)("span", { className: "bt-menu-label" }, folder.title), (0, import_react14.createElement)("span", { className: "bt-folder-path" }, folder.path)), -- (0, import_react14.createElement)(Tick, { on: folder.path === value }) -- )), -- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- info?.picker === "native" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: pickNative }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Choose a folder…"))) : null, -- info?.picker === "browse" ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => browse(value) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Browse folders…"))) : null, -- (0, import_react14.createElement)("button", { type: "button", role: "menuitem", onClick: () => { -- setDraft(value ?? ""); -- setView("type"); -- } }, (0, import_react14.createElement)(PencilIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) -- ); -- const typeView = (0, import_react14.createElement)( -- "div", -- { key: "type", className: "bt-folder-view" }, -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react14.createElement)("input", { -- className: "bt-folder-input", -- autoFocus: true, -- value: draft, -- spellCheck: false, -- "aria-label": t("Folder path"), -- placeholder: t("Full path of a folder"), -- onChange: (event) => setDraft(event.target.value), -- onKeyDown: (event) => { -- if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); -- } -- }), -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) -- ); -- const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; -- const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; -- const browseView = listing ? (0, import_react14.createElement)( -- "div", -- { key: `browse:${listing.path}`, className: "bt-folder-view" }, -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react14.createElement)(ChevronLeftIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Back"))), -- (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, listing.path), -- parent ? (0, import_react14.createElement)("button", { type: "button", role: "menuitem", title: parent.path, onClick: () => browse(parent.path) }, (0, import_react14.createElement)(FolderIcon), (0, import_react14.createElement)("span", { className: "bt-menu-label" }, t("Up one level"))) : null, -- inside.map((entry) => (0, import_react14.createElement)( -- "button", -- { key: entry.path, type: "button", role: "menuitem", onClick: () => browse(entry.path) }, -- (0, import_react14.createElement)(FolderIcon), -- (0, import_react14.createElement)("span", { className: "bt-menu-label" }, entry.name), -- (0, import_react14.createElement)(ChevronRightIcon) -- )), -- inside.length === 0 ? (0, import_react14.createElement)("div", { className: "bt-model-none" }, t("No folders inside.")) : null, -- listing.truncated ? (0, import_react14.createElement)("div", { className: "bt-folder-path bt-folder-here" }, t("Some folders are not shown.")) : null, -- (0, import_react14.createElement)("div", { className: "bt-menu-sep", role: "separator" }), -- (0, import_react14.createElement)("button", { type: "button", className: "bt-folder-use", onClick: () => choose(listing.path) }, t("Use this folder")) -- ) : null; -- const shownView = view === "type" ? typeView : view === "browse" && browseView ? browseView : listView; -- return (0, import_react14.createElement)( -- import_react14.Fragment, -- null, -- (0, import_react14.createElement)( -- "button", -- { -- ref: button, -- type: "button", -- className: "bt-model-trigger", -- "data-wide": wide || void 0, -- "aria-haspopup": "menu", -- "aria-expanded": isOpen, -- "aria-label": t("Working folder: {folder}", { folder: value ?? fallback }), -- title: value, -- onClick: toggle -- }, -- (0, import_react14.createElement)( -- "span", -- { className: "bt-model-copy" }, -- (0, import_react14.createElement)("span", { className: "bt-model-name" }, value ? folderName(value) : fallback), -- value ? (0, import_react14.createElement)("span", { className: "bt-model-provider" }, value) : null -- ), -- (0, import_react14.createElement)(Caret) ++ key: model.ref, ++ type: "button", ++ className: "bt-model-chip", ++ disabled: busy !== "", ++ "aria-busy": busy === model.ref || void 0, ++ "aria-label": `${model.name} · ${provider}`, ++ title: model.ref === roster.defaultModel ? `${model.ref} · ${t("Harness default")}` : model.ref, ++ onClick: () => pick2(model.ref) ++ }, model.name, busy === model.ref ? (0, import_react23.createElement)("span", { className: "bt-model-spin", "aria-label": t("Saving…") }) : null))) ++ )), ++ (0, import_react23.createElement)("button", { key: "manage", type: "button", className: "bt-model-chip bt-model-chip-quiet", onClick: openHarnessModels }, (0, import_react23.createElement)(SlidersIcon), t("Manage models")) ++ ]; ++ return (0, import_react23.createElement)( ++ "div", ++ { className: "bt-model-dock" }, ++ (0, import_react23.createElement)( ++ "div", ++ { className: "bt-astack", "data-lead": "" }, ++ (0, import_react23.createElement)( ++ "div", ++ { className: "bt-msg" }, ++ (0, import_react23.createElement)( ++ "div", + { className: "bt-msg-line" }, + (0, import_react23.createElement)(BotMark, { bot, size: 22 }), + (0, import_react23.createElement)("div", { className: "bt-bubble" }, text2) + ) -+ ) + ) ), -- shownPlace ? (0, import_react_dom7.createPortal)((0, import_react14.createElement)( -- "div", -- { ref: menu, className: `bt-menu bt-model-menu bt-folder-menu${shownPlace.bottom !== void 0 ? " bt-menu-up" : ""}`, role: "menu", "aria-label": t("Working folder"), style: shownPlace, "data-leaving": leaving || void 0 }, -- shownView, -- problem ? (0, import_react14.createElement)("div", { className: "bt-folder-error", role: "alert" }, problem) : null -- ), document.body) : null +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react15.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) +- ), +- (0, import_react15.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), +- (0, import_react15.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) +- ), +- (0, import_react15.createElement)( + (0, import_react23.createElement)("div", { className: "bt-model-chips", role: "group", "aria-label": t("Choose a model") }, choices), + error ? (0, import_react23.createElement)("div", { className: "bt-secret-error bt-model-error", role: "alert" }, error) : null - ); - } --var FOLDER_CSS = ` --.bt-folder-view{display:flex;flex-direction:column;gap:2px;animation:bt-veil-in .14s ease-out} --.bt-folder-copy{flex:1;display:flex;flex-direction:column;min-width:0} --.bt-folder-copy .bt-menu-label{flex:none} --.bt-folder-path{min-width:0;font-size:11px;line-height:14px;color:var(--bt-ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} --.bt-folder-here{padding:0 8px 4px;white-space:normal;overflow-wrap:anywhere} --.bt-menu .bt-folder-back{color:var(--bt-ink-2);font-weight:500} --.bt-folder-input{height:30px;margin:2px 4px;padding:0 8px;border:1px solid var(--bt-line-2);border-radius:8px;background:var(--bt-main);color:var(--bt-ink);font:inherit;font-size:13px;outline:none;box-sizing:border-box} --.bt-folder-input:focus{border-color:var(--bt-accent)} --.bt-menu .bt-folder-use{justify-content:center;font-weight:500;color:var(--bt-accent)} --.bt-menu .bt-folder-use:disabled{color:var(--bt-ink-3);cursor:default;background:none} --.bt-folder-error{padding:4px 8px;font-size:12px;line-height:16px;color:#c21d2e;overflow-wrap:anywhere} --@media (prefers-reduced-motion:reduce){.bt-folder-view{animation:none}} --`; -- --// src/client/schedules.js --var import_react15 = require("react"); --var POLL_MS = 3e4; --var shared = null; --var listeners = /* @__PURE__ */ new Set(); --function publish(view) { -- shared = view; -- for (const listener of listeners) listener(view); --} --function useSchedules(actions) { -- const [view, setView] = (0, import_react15.useState)(shared); -- (0, import_react15.useEffect)(() => { -- listeners.add(setView); -- let stopped = false; -- let timer; -- const load = () => actions.schedules?.().then((next) => { -- if (!stopped) publish(next); -- }).catch(() => { -- }).finally(() => { -- if (!stopped) timer = setTimeout(load, POLL_MS); -- }); -- load(); -- return () => { -- stopped = true; -- clearTimeout(timer); -- listeners.delete(setView); -- }; -- }, []); -- return view; ++ ); ++} +function RemoteGoneDock({ bot }) { + return (0, import_react23.createElement)( + "div", + { className: "bt-model-dock", "data-flybot": "remote-gone" }, + (0, import_react23.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("When"))), +- (0, import_react15.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react15.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), +- (0, import_react15.createElement)( + { className: "bt-astack", "data-lead": "" }, + (0, import_react23.createElement)( -+ "div", + "div", +- { key: kind, className: "bt-task-when bt-veil" }, +- kind === "at" ? (0, import_react15.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, +- kind === "weekly" ? (0, import_react15.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react15.createElement)("button", { +- key: day, +- type: "button", +- className: "bt-task-day", +- "aria-pressed": weekdays.includes(day), +- onClick: () => toggleDay(day) +- }, when2(Date.UTC(2024, 0, day), { weekday: "narrow", timeZone: "UTC" })))) : null, +- kind === "daily" || kind === "weekly" ? (0, import_react15.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, +- kind === "every" ? (0, import_react15.createElement)( +- import_react15.Fragment, +- null, +- (0, import_react15.createElement)("span", { className: "bt-task-every" }, t("Every")), +- (0, import_react15.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), +- (0, import_react15.createElement)( +- "select", +- { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, +- UNITS.map(([id, , label]) => (0, import_react15.createElement)("option", { key: id, value: id }, t(label))) +- ) +- ) : null + { className: "bt-msg" }, + (0, import_react23.createElement)( + "div", @@ -16661,26 +17777,17 @@ diff --git a/client.js b/client.js + (0, import_react23.createElement)(BotMark, { bot, size: 22 }), + (0, import_react23.createElement)("div", { className: "bt-bubble" }, t("{name} ({handle}) is no longer shared with you, so messages to it cannot be delivered. It runs on its owner's machine, so another model cannot stand in. Ask its owner for a new invite.", { name: bot.name, handle: bot.remote.handle })) + ) -+ ) + ) + ) + ); - } --var localZone = () => Intl.DateTimeFormat().resolvedOptions().timeZone; --var clock2 = (time) => String(time ?? "").slice(0, 5); --var when2 = (ms, options) => new Intl.DateTimeFormat(dateLocale(), options).format(ms); --function dayAndTime(ms) { -- const sameYear = new Date(ms).getFullYear() === (/* @__PURE__ */ new Date()).getFullYear(); -- return when2(ms, { ...sameYear ? {} : { year: "numeric" }, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); ++} +function setupChoices(actions) { + return [ + actions.canSignIn?.() ? [t("Sign in to DeepSeek"), () => actions.signIn()] : null, + [t("Enter a DeepSeek API key"), () => openHarnessOnboarding("deepseek-official")], + [t("Open settings to set up a custom model"), openHarnessModels] + ].filter(Boolean); - } --function weekdayNames(days) { -- const names = days.map((day) => when2(Date.UTC(2024, 0, day), { weekday: "short", timeZone: "UTC" })); -- return names.join(dateLocale().startsWith("zh") ? "、" : ", "); ++} +var KEY_HELP_URL = "https://github.com/ThinkFlowLab/FlyBot/blob/main/docs/deepseek-api-key.md"; +function KeyHelpLink() { + return (0, import_react23.createElement)("a", { className: "bt-model-help", href: KEY_HELP_URL, target: "_blank", rel: "noopener noreferrer" }, t("How to set a DeepSeek API key")); @@ -16717,39 +17824,27 @@ diff --git a/client.js b/client.js + (0, import_react23.createElement)("span", { className: "bt-turn-error-title" }, title), + (0, import_react23.createElement)("span", { className: "bt-turn-error-message" }, message), + code ? (0, import_react23.createElement)("code", { className: "bt-turn-error-code" }, code) : null -+ ), + ), +- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, +- (0, import_react15.createElement)( + choices.length > 0 ? (0, import_react23.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) +- ) + { className: "bt-turn-error-actions" }, + choices.map(([label, run]) => (0, import_react23.createElement)("button", { key: label, type: "button", className: "bt-model-chip", onClick: run }, label)), + deepseek ? (0, import_react23.createElement)(KeyHelpLink, { key: "key-help" }) : null + ) : null + ); - } --function everyText(seconds) { -- if (seconds % 86400 === 0) return seconds === 86400 ? t("Every day") : t("Every {count} days", { count: seconds / 86400 }); -- if (seconds % 3600 === 0) return seconds === 3600 ? t("Every hour") : t("Every {count} hours", { count: seconds / 3600 }); -- return t("Every {count} minutes", { count: Math.round(seconds / 60) }); ++} +function DeepSeekSignIn({ useUi, actions }) { + const open = useUi((state) => state.signIn); + const entry = open ? actions.signInEntry?.() : null; + if (!entry) return null; + return (0, import_react23.createElement)(SignInHost, { key: open, entry, actions }); - } --function timingText(task) { -- const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -- switch (task.kind) { -- case "after": -- case "at": -- return t("Once, {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }); -- case "every": -- return everyText(task.everySeconds); -- case "daily": -- return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; -- case "weekly": { -- const days = (task.weekdays ?? []).join(); -- const text = days === "1,2,3,4,5" ? t("Every weekday at {time}", { time: clock2(task.time) }) : days === "6,7" ? t("Every weekend at {time}", { time: clock2(task.time) }) : days === "1,2,3,4,5,6,7" ? t("Every day at {time}", { time: clock2(task.time) }) : t("Every {days} at {time}", { days: weekdayNames(task.weekdays ?? []), time: clock2(task.time) }); -- return `${text}${zone}`; ++} +function SignInHost({ entry, actions }) { + const [ops] = (0, import_react23.useState)(() => entry.inject?.() ?? {}); + const close = () => actions.closeSignIn(); @@ -16763,30 +17858,9 @@ diff --git a/client.js b/client.js + useApiKey: () => { + close(); + openHarnessOnboarding("deepseek-official"); - } -- case "cron": -- return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -- default: -- return ""; -- } ++ } + }); - } --var nextText = (task) => task.status === "active" ? t("Next: {time}", { time: dayAndTime(Date.parse(task.scheduledAt)) }) : t("Finished"); --function TaskRow({ task, actions, onView, showBot, roster }) { -- const [confirming, setConfirming] = (0, import_react15.useState)(false); -- const [busy, setBusy] = (0, import_react15.useState)(false); -- (0, import_react15.useEffect)(() => { -- if (!confirming) return void 0; -- const timer = setTimeout(() => setConfirming(false), 3e3); -- return () => clearTimeout(timer); -- }, [confirming]); -- const remove = () => { -- if (!confirming) { -- setConfirming(true); -- return; -- } -- setBusy(true); -- void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); ++} +var MODEL_CSS = ` +.bt-turn-error{display:grid;grid-template-columns:10px minmax(0,1fr);column-gap:8px;row-gap:8px;padding:2px 0;font-size:13px;line-height:20px} +.bt-turn-error-dot{width:6px;height:6px;margin-top:7px;border-radius:50%;background:#e02135} @@ -16877,22 +17951,14 @@ diff --git a/client.js b/client.js + ).then((outcome2) => { + if (live.current) setState({ busy: false, outcome: outcome2 }); + }); - }; -- const room2 = task.roomId ? roster.roomsById[task.roomId] : void 0; -- return (0, import_react15.createElement)( ++ }; + const { busy, outcome } = state; + return (0, import_react24.createElement)( - "div", -- { className: "bt-set-row bt-task", "data-done": task.status === "active" ? void 0 : "", "data-busy": busy || void 0 }, -- showBot ? (0, import_react15.createElement)(BotAvatar, { bot: roster.byId[task.botId], size: 28, badge: false, live: false }) : null, -- (0, import_react15.createElement)( ++ "div", + { className: "bt-set-row bt-bs-row bt-model-test" }, + (0, import_react24.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react15.createElement)("span", { className: "bt-task-title" }, task.title), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, [timingText(task), room2 ? t("in {name}", { name: room2.name }) : ""].filter(Boolean).join(" · ")), -- task.prompt && task.prompt !== task.title ? (0, import_react15.createElement)("span", { className: "bt-task-prompt" }, task.prompt) : null ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react24.createElement)("span", { className: "bt-set-hint" }, t("Sends one short request to this model. It may be billed.")), + (0, import_react24.createElement)( + "span", @@ -16900,25 +17966,19 @@ diff --git a/client.js b/client.js + busy ? t("Testing…") : outcome === void 0 ? null : outcome.text, + outcome?.code ? (0, import_react24.createElement)("code", { className: "bt-model-test-code" }, outcome.code) : null + ) - ), -- (0, import_react15.createElement)("span", { className: "bt-task-next" }, nextText(task)), -- (0, import_react15.createElement)("button", { -- type: "button", -- className: "bt-mini bt-task-remove", -- "data-confirm": confirming || void 0, -- disabled: busy, -- "aria-label": confirming ? t("Click again to delete") : t("Delete task"), -- title: confirming ? void 0 : t("Delete task"), -- onClick: remove -- }, confirming ? t("Delete") : (0, import_react15.createElement)(TrashIcon)) ++ ), + (0, import_react24.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, "aria-busy": busy || void 0, onClick: start }, t("Test connection")) ); } --var pad = (number) => String(number).padStart(2, "0"); --function inAnHour() { -- const date = new Date(Date.now() + 36e5); -- date.setMinutes(0, 0, 0); -- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:00`; +-function AutomationOff() { +- return (0, import_react15.createElement)( +- "div", +- { className: "bt-set-card bt-task-off" }, +- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), +- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), +- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) +- ); +var MODEL_TEST_CSS = ` +.bt-model-test-result{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +.bt-model-test-result:empty{display:none} @@ -16932,37 +17992,15 @@ diff --git a/client.js b/client.js +@container bt-panel (max-width:300px){ + .bt-model-test{flex-wrap:wrap;row-gap:6px} + .bt-model-test>.bt-set-copy{flex:1 1 100%} - } --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 }) { -- const [target, setTarget] = (0, import_react15.useState)(start ?? roster.bots[0]?.id ?? ""); -- const [title, setTitle] = (0, import_react15.useState)(""); -- const [prompt, setPrompt] = (0, import_react15.useState)(""); -- const [kind, setKind] = (0, import_react15.useState)("daily"); -- const [at, setAt] = (0, import_react15.useState)(inAnHour); -- const [time, setTime] = (0, import_react15.useState)("09:00"); -- const [weekdays, setWeekdays] = (0, import_react15.useState)([1, 2, 3, 4, 5]); -- const [count, setCount] = (0, import_react15.useState)(1); -- const [unit, setUnit] = (0, import_react15.useState)("hours"); -- const [busy, setBusy] = (0, import_react15.useState)(false); -- const [error, setError] = (0, import_react15.useState)(""); -- 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]) }; -- return { kind, time, timeZone: localZone(), ...kind === "weekly" ? { weekdays } : {} }; -- }; -- const submit = (event) => { -- event.preventDefault(); -- if (busy) return; -- setBusy(true); -- setError(""); -- let when3; -- try { -- when3 = timing(); -- } catch { -- setError(t("Pick a date and time")); -- setBusy(false); + } +-function SchedulesPage({ roster, actions, start }) { +- const view = useSchedules(actions); +- const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); +- if (view === null) return (0, import_react15.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); +- if (!view.available) return (0, import_react15.createElement)(AutomationOff); +- const shown = (next) => { +- publish(next); +- setAdding(false); +`; + +// src/client/folder-menu.js @@ -16981,75 +18019,48 @@ diff --git a/client.js b/client.js + const toggle = () => { + if (isOpen) { + close(); - return; -- } -- void actions.scheduleCreate({ target, title, prompt, when: when3 }).then(onDone, (failure) => { -- setError(hostText(failure?.message ?? String(failure))); -- setBusy(false); -- }); -- }; -- const toggleDay = (day) => setWeekdays((days) => days.includes(day) ? days.filter((item) => item !== day) : [...days, day].sort()); -- return (0, import_react15.createElement)( -- "form", -- { className: "bt-set-card bt-task-form", onSubmit: submit }, -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Send to"))), -- (0, import_react15.createElement)( -- "select", -- { className: "bt-select bt-task-bot", value: target, onChange: (event) => setTarget(event.target.value) }, -- (0, import_react15.createElement)("optgroup", { label: t("Bots") }, roster.bots.map((bot) => (0, import_react15.createElement)("option", { key: bot.id, value: bot.id }, bot.name))), -- roster.rooms.length > 0 ? (0, import_react15.createElement)("optgroup", { label: t("Group chats") }, roster.rooms.map((room2) => (0, import_react15.createElement)("option", { key: room2.id, value: room2.id }, room2.name))) : null -- ) -- ), -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react15.createElement)("input", { className: "bt-bs-input", value: title, maxLength: 120, placeholder: t("Morning briefing"), autoFocus: true, onChange: (event) => setTitle(event.target.value) }) -- ), -- (0, import_react15.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("What to do"))), -- (0, import_react15.createElement)("textarea", { className: "bt-bs-input bt-bs-area", value: prompt, rows: 3, placeholder: t("Sum up what changed since yesterday and what needs me today."), onChange: (event) => setPrompt(event.target.value) }) -- ), -- (0, import_react15.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-bs-label" }, t("When"))), -- (0, import_react15.createElement)(Segmented, { label: t("When"), value: kind }, KINDS.map(([id, label]) => (0, import_react15.createElement)("button", { key: id, type: "button", "aria-pressed": kind === id, onClick: () => setKind(id) }, t(label)))), -- (0, import_react15.createElement)( -- "div", -- { key: kind, className: "bt-task-when bt-veil" }, -- kind === "at" ? (0, import_react15.createElement)("input", { className: "bt-input", type: "datetime-local", value: at, "aria-label": t("Date and time"), onChange: (event) => setAt(event.target.value) }) : null, -- kind === "weekly" ? (0, import_react15.createElement)("div", { className: "bt-task-days", role: "group", "aria-label": t("Days") }, [1, 2, 3, 4, 5, 6, 7].map((day) => (0, import_react15.createElement)("button", { -- key: day, -- type: "button", -- className: "bt-task-day", -- "aria-pressed": weekdays.includes(day), -- onClick: () => toggleDay(day) -- }, when2(Date.UTC(2024, 0, day), { weekday: "narrow", timeZone: "UTC" })))) : null, -- kind === "daily" || kind === "weekly" ? (0, import_react15.createElement)("input", { className: "bt-input bt-task-time", type: "time", value: time, "aria-label": t("Time"), onChange: (event) => setTime(event.target.value) }) : null, -- kind === "every" ? (0, import_react15.createElement)( -- import_react15.Fragment, -- null, -- (0, import_react15.createElement)("span", { className: "bt-task-every" }, t("Every")), -- (0, import_react15.createElement)("input", { className: "bt-input bt-task-count", type: "number", min: 1, value: count, "aria-label": t("How many"), onChange: (event) => setCount(event.target.value) }), -- (0, import_react15.createElement)( -- "select", -- { className: "bt-select bt-task-unit", value: unit, "aria-label": t("Unit"), onChange: (event) => setUnit(event.target.value) }, -- UNITS.map(([id, , label]) => (0, import_react15.createElement)("option", { key: id, value: id }, t(label))) -- ) -- ) : null -- ) ++ return; + } + setView("list"); + setProblem(""); + void Promise.resolve(actions.folders()).then(setInfo).catch(() => setInfo((last) => last ?? { team: null, picker: null, folders: [] })); + open(); -+ }; + }; +- const byChat = [ +- ...roster.bots.map((bot) => [bot.id, (0, import_react15.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), +- ...roster.rooms.map((room2) => [room2.id, (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) +- ].filter(([, , , tasks]) => tasks.length > 0); +- return [ +- adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))), +- byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)( +- "section", +- { key: id, className: "bt-set-section" }, +- (0, import_react15.createElement)("h3", { className: "bt-task-head" }, avatar, name), +- (0, import_react15.createElement)("div", { className: "bt-set-card" }, tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish }))) +- )) +- ]; +-} +-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) : []; +- return (0, import_react15.createElement)( +- "section", +- { className: "bt-set-section" }, +- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), +- (0, import_react15.createElement)( +- "div", +- { className: "bt-set-card" }, +- !view.available ? (0, import_react15.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings."))), +- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) +- ) : [ +- ...tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish })), +- (0, import_react15.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react15.createElement)(PlusIcon), t("New task")) +- ] +- ) + const choose = (path) => { + close(); + if (path !== (value ?? "")) void onPick(path); @@ -17079,14 +18090,7 @@ diff --git a/client.js b/client.js + { type: "button", role: "menuitemradio", "aria-checked": !value, title: group ? void 0 : info?.team ?? void 0, onClick: () => choose("") }, + (0, import_react25.createElement)("span", { className: "bt-menu-label" }, fallback), + (0, import_react25.createElement)(Tick, { on: !value }) - ), -- error ? (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-danger bt-set-hint" }, error)) : null, -- (0, import_react15.createElement)( -- "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react15.createElement)("button", { type: "submit", className: "bt-send bt-soft-sm", disabled: busy || !target || title.trim() === "" || prompt.trim() === "" }, busy ? t("Creating…") : t("Create task")) -- ) ++ ), + folders.length > 0 ? (0, import_react25.createElement)("div", { className: "bt-model-group", role: "presentation" }, t("Folders")) : null, + folders.map((folder) => (0, import_react25.createElement)( + "button", @@ -17103,61 +18107,12 @@ diff --git a/client.js b/client.js + } }, (0, import_react25.createElement)(PencilIcon), (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Enter a path…"))) ); -} --function AutomationOff() { -- return (0, import_react15.createElement)( -+ const typeView = (0, import_react25.createElement)( - "div", -- { className: "bt-set-card bt-task-off" }, -- (0, import_react15.createElement)("span", { className: "bt-task-off-mark" }, (0, import_react15.createElement)(ClockIcon)), -- (0, import_react15.createElement)("span", { className: "bt-task-off-title" }, t("Turn on Automation tasks in DSH")), -- (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings.")), -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -- ); --} --function SchedulesPage({ roster, actions, start }) { -- const view = useSchedules(actions); -- const [adding, setAdding] = (0, import_react15.useState)(Boolean(start)); -- if (view === null) return (0, import_react15.createElement)("div", { className: "bt-set-hint" }, t("Loading…")); -- if (!view.available) return (0, import_react15.createElement)(AutomationOff); -- const shown = (next) => { -- publish(next); -- setAdding(false); -- }; -- const byChat = [ -- ...roster.bots.map((bot) => [bot.id, (0, import_react15.createElement)(BotAvatar, { bot, size: 18, badge: false, live: false }), bot.name, view.tasks.filter((task) => task.botId === bot.id)]), -- ...roster.rooms.map((room2) => [room2.id, (0, import_react15.createElement)(RoomAvatar, { room: room2, roster, size: 18 }), room2.name, view.tasks.filter((task) => task.botId === void 0 && task.roomId === room2.id)]) -- ].filter(([, , , tasks]) => tasks.length > 0); -- return [ -- adding ? (0, import_react15.createElement)(TaskForm, { key: "form", roster, actions, start, onDone: shown, onCancel: () => setAdding(false) }) : (0, import_react15.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-with-icon", onClick: () => setAdding(true) }, (0, import_react15.createElement)(PlusIcon), t("New task"))), -- byChat.length === 0 && !adding ? (0, import_react15.createElement)("div", { key: "none", className: "bt-set-card" }, (0, import_react15.createElement)("div", { className: "bt-set-row" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("No scheduled tasks yet.")))) : byChat.map(([id, avatar, name, tasks]) => (0, import_react15.createElement)( -- "section", -- { key: id, className: "bt-set-section" }, -- (0, import_react15.createElement)("h3", { className: "bt-task-head" }, avatar, name), -- (0, import_react15.createElement)("div", { className: "bt-set-card" }, tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish }))) -- )) -- ]; --} --function BotSchedules({ bot, roster, actions }) { +-function ScheduleSummary({ bot, actions }) { - const view = useSchedules(actions); -- if (view === null) return null; -- const tasks = view.available ? view.tasks.filter((task) => task.botId === bot.id) : []; +- const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; - return (0, import_react15.createElement)( -- "section", -- { className: "bt-set-section" }, -- (0, import_react15.createElement)("h3", null, t("Scheduled tasks")), -- (0, import_react15.createElement)( -- "div", -- { className: "bt-set-card" }, -- !view.available ? (0, import_react15.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react15.createElement)("span", { className: "bt-set-copy" }, (0, import_react15.createElement)("span", { className: "bt-set-hint" }, t("Switch on Automation tasks on the Plugins page in Harness settings."))), -- (0, import_react15.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: openHarnessSettings }, t("Open Harness settings")) -- ) : [ -- ...tasks.map((task) => (0, import_react15.createElement)(TaskRow, { key: task.id, task, actions, roster, onView: publish })), -- (0, import_react15.createElement)("button", { key: "new", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => actions.openSettings("schedules", bot.id) }, (0, import_react15.createElement)(PlusIcon), t("New task")) -- ] -- ) ++ const typeView = (0, import_react25.createElement)( ++ "div", + { key: "type", className: "bt-folder-view" }, + (0, import_react25.createElement)("button", { type: "button", className: "bt-folder-back", onClick: () => setView("list") }, (0, import_react25.createElement)(ChevronLeftIcon), (0, import_react25.createElement)("span", { className: "bt-menu-label" }, t("Back"))), + (0, import_react25.createElement)("input", { @@ -17173,12 +18128,7 @@ diff --git a/client.js b/client.js + } + }), + (0, import_react25.createElement)("button", { type: "button", className: "bt-folder-use", disabled: draft.trim() === "", onClick: typed }, t("Use this folder")) - ); --} --function ScheduleSummary({ bot, actions }) { -- const view = useSchedules(actions); -- const tasks = view?.available ? view.tasks.filter((task) => task.botId === bot.id && task.status === "active") : []; -- return (0, import_react15.createElement)( ++ ); + const parent = listing?.crumbs?.length > 1 ? listing.crumbs.at(-2) : void 0; + const inside = listing?.entries?.filter((entry) => !entry.hidden) ?? []; + const browseView = listing ? (0, import_react25.createElement)( @@ -20244,7 +21194,7 @@ diff --git a/client.js b/client.js + key: `${item.from.id}>${item.to.id}`, + back: true, + d: `M ${x1} ${y1} L ${x1} ${yA} L ${lane} ${yA} L ${lane} ${yB} L ${x2} ${yB} L ${x2} ${y2}` - }); ++ }); + taken.push({ x: lane, yA: Math.min(yA, yB), yB: Math.max(yA, yB) }); + }); + const centres = []; @@ -20270,7 +21220,7 @@ diff --git a/client.js b/client.js + key: `${item.from.id}>${item.to.id}`, + back: false, + d: straight ? `M ${x1} ${y1} L ${x1} ${y2}` : `M ${x1} ${y1} L ${x1} ${yA} L ${lane} ${yA} L ${lane} ${yB} L ${x2} ${yB} L ${x2} ${y2}` -+ }); + }); + } + return routes; +} @@ -20418,89 +21368,25 @@ diff --git a/client.js b/client.js - Card, - { title: t("Who answers") }, - (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { -- key: id, -- type: "button", -- "aria-pressed": mode === id, -- disabled: id === "admin" && !admin, -- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, -- onClick: () => { -- if (mode !== id) void update({ mode: id }); -- } -- }, t(label)))), -- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Working folder") }, -- (0, import_react23.createElement)( -- "div", -- { className: "bt-set-row bt-bs-row" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) -- ) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Members · {count}", { count: members.length }) }, -- members.map((member) => { -- const leads = room2.admin === member.id; -- return (0, import_react23.createElement)( -- "div", -- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, -- (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, -- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), -- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), -- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null -- ), -- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), -- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null -- ); -- }), -- adding ? [ -- ...outside.map((bot) => (0, import_react23.createElement)( -- "button", -- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { -- setAdding(false); -- void update({ add: [bot.id] }); -- } }, -- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) -- )), -- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) -- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, -- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( -- "span", -- { className: "bt-set-hint" }, -- // The host checks for two members on every change, even a rename. -- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") -- )) -- ), -- (0, import_react23.createElement)( -- Card, -- { title: t("Sidebar") }, -- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), -- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) +- "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)(Segmented, { label: t("Who answers"), value: mode }, ROOM_MODES.map(([id, label]) => (0, import_react23.createElement)("button", { +- key: id, +- type: "button", +- "aria-pressed": mode === id, +- disabled: id === "admin" && !admin, +- title: id === "admin" && !admin ? t("Choose an admin first") : void 0, +- onClick: () => { +- if (mode !== id) void update({ mode: id }); +- } +- }, t(label)))), +- (0, import_react23.createElement)("span", { key: mode, className: "bt-set-hint bt-veil" }, `${t(modeHint)} ${t("@mentions always pick who answers.")}`) +- ) - ), - (0, import_react23.createElement)( - Card, -- { title: t("Manage") }, +- { title: t("Working folder") }, - (0, import_react23.createElement)( -- "button", -- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react23.createElement)( -- "span", -- { className: "bt-set-copy" }, -- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), -- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) -- ), -- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) + { className: "bt-wf-palette" }, + (0, import_react37.createElement)("span", { className: "bt-wf-hint" }, t("Drag a Bot here")), + locals.map((bot) => (0, import_react37.createElement)("button", { @@ -20536,7 +21422,10 @@ diff --git a/client.js b/client.js + "div", + { className: "bt-wf-stage", style: pad2 > 0 ? { height: drawnHeight, paddingLeft: pad2 } : { height: drawnHeight } }, + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react23.createElement)(FolderMenu, { value: room2.workDir, group: true, actions, run, onPick: (workDir) => update({ workDir }) }) + { className: "bt-wf-fit", style: { height, transform: `scale(${scale})` } }, + (0, import_react37.createElement)( + "div", @@ -20659,38 +21548,9 @@ diff --git a/client.js b/client.js + }) + ) ) - ) - ); - } --function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { -- const [admin, setAdmin] = (0, import_react23.useState)(null); -- const [members, setMembers] = (0, import_react23.useState)([]); -- const [name, setName] = (0, import_react23.useState)(""); -- const [busy, setBusy] = (0, import_react23.useState)(false); -- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); -- const pickAdmin = (id) => { -- setAdmin(id); -- setMembers((list) => list.filter((item) => item !== id)); -- }; -- const create = async () => { -- if (busy || admin === null || members.length === 0) return; -- setBusy(true); -- let room2; -- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { -- room2 = value; -- })); -- if (done && room2) onCreated(room2); -- else setBusy(false); -- }; -- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { -- key: bot.id, -- type: "button", -- role, -- className: "bt-ng-chip", -- "aria-checked": on, -- onClick -- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); -- return (0, import_react23.createElement)( ++ ) ++ ); ++} +function Row2({ item, source, onOpen, onRename, onDelete, editable = true }) { + return (0, import_react37.createElement)( + "button", @@ -20706,7 +21566,13 @@ diff --git a/client.js b/client.js + { className: "bt-set-copy" }, + (0, import_react37.createElement)("span", null, item.name), + (0, import_react37.createElement)("span", { className: "bt-set-hint" }, item.preview || chainOf(item) || item.description || "") -+ ), + ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Members · {count}", { count: members.length }) }, +- members.map((member) => { +- const leads = room2.admin === member.id; +- return (0, import_react23.createElement)( + source === "own" && editable ? (0, import_react37.createElement)( + "span", + { className: "bt-set-actions" }, @@ -20747,19 +21613,11 @@ diff --git a/client.js b/client.js + const locked = readOnly !== ""; + const showUse = !locked && (opened?.source === "factory" || opened?.fromFactory !== void 0); + return (0, import_react37.createElement)( - "div", -- { className: "bt-set-card bt-ng" }, -- (0, import_react23.createElement)( ++ "div", + { className: "bt-wf-page" }, + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("A workflow is a group of your Bots. Drag the flowchart to change who hands work to whom.")), + error ? (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), -- (0, import_react23.createElement)( -- "div", -- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, -- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) ++ "div", + { className: "bt-error", role: "alert" }, + alertCopy(error), + phase === "error" ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onRetry?.() }, t("Retry")) : null @@ -20775,7 +21633,58 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("h3", null, t("Factory presets")), + (0, import_react37.createElement)("p", { className: "bt-wf-hint" }, t("Ready-made pipelines. Use one to start your own.")), + (0, import_react37.createElement)( -+ "div", + "div", +- { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-gs-who", title: t("Open {name}'s chat", { name: member.name }), onClick: () => openBot(member.id) }, +- (0, import_react23.createElement)(BotAvatar, { bot: member, size: 26, main: isMainOf(roster, member.id), admin: leads, live: false }), +- (0, import_react23.createElement)("span", { className: "bt-row-title" }, member.name), +- leads ? (0, import_react23.createElement)("span", { className: "bt-tag" }, t("Admin")) : null +- ), +- leads ? (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", title: t("Leave the group without an admin"), onClick: () => update({ admin: null }) }, t("Unset")) : (0, import_react23.createElement)("button", { type: "button", className: "bt-mini", onClick: () => update({ admin: member.id }) }, t("Make admin")), +- members.length > 2 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-x", "aria-label": t("Remove {name} from the group", { name: member.name }), title: t("Remove from group"), onClick: () => update({ remove: [member.id] }) }, "×") : null +- ); +- }), +- adding ? [ +- ...outside.map((bot) => (0, import_react23.createElement)( +- "button", +- { key: bot.id, type: "button", className: "bt-set-row bt-bs-row bt-gs-pick", onClick: () => { +- setAdding(false); +- void update({ add: [bot.id] }); +- } }, +- (0, import_react23.createElement)(BotMark, { bot, size: 22 }), +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, bot.name)), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, t("Add")) +- )), +- (0, import_react23.createElement)("button", { key: "cancel", type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(false) }, t("Cancel")) +- ] : outside.length > 0 ? (0, import_react23.createElement)("button", { type: "button", className: "bt-set-row bt-bs-row bt-gs-add", onClick: () => setAdding(true) }, (0, import_react23.createElement)(PlusIcon), t("Add member")) : null, +- (0, import_react23.createElement)("div", { className: "bt-set-row bt-gs-note" }, (0, import_react23.createElement)( +- "span", +- { className: "bt-set-hint" }, +- // The host checks for two members on every change, even a rename. +- members.length < 2 ? t("Add a Bot first: a group chat needs at least two.") : admin ? t("{name} leads this group: it can call members with @Name, change the group, and post here.", { name: admin.name }) : t("No admin yet. An admin can call members with @Name, change the group, and post here.") +- )) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react23.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: room2.pinned === true, onChange: (pinned) => run(actions.setFlags(room2.id, { pinned })) }), +- (0, import_react23.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: room2.hidden === true, onChange: (hidden) => run(actions.setFlags(room2.id, { hidden })) }) +- ), +- (0, import_react23.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react23.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react23.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react23.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: room2.name }) : t("Delete group")), +- (0, import_react23.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its history is archived.") : t("Removes the group chat. Its members stay on the team.")) +- ), +- (0, import_react23.createElement)("span", { className: "bt-chevron" }, (0, import_react23.createElement)(ChevronRightIcon)) + { className: "bt-set-card" }, + templates.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No workflows yet."))) : templates.map((item) => { + const live = (pack.workflows ?? []).find((workflow) => workflow.id === item.id); @@ -20795,19 +21704,13 @@ diff --git a/client.js b/client.js + yours.length === 0 ? (0, import_react37.createElement)("div", { className: "bt-set-row" }, (0, import_react37.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react37.createElement)(Row2, { key: item.id, item, source: "own", editable: !locked, onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) + ) ) -- ), -- admin === null ? null : (0, import_react23.createElement)( +- ) + ) : null, + graph ? (0, import_react37.createElement)( - "div", -- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), -- (0, import_react23.createElement)( ++ "div", + { className: "bt-wf-editor" }, + (0, import_react37.createElement)( - "div", -- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, -- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) ++ "div", + { className: "bt-wf-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), + showUse ? (0, import_react37.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, @@ -20824,17 +21727,51 @@ diff --git a/client.js b/client.js + editing: opened?.source === "own" && editing === true && !locked + }) + ) : null -+ ); -+} + ); + } +-function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { +- const [admin, setAdmin] = (0, import_react23.useState)(null); +- const [members, setMembers] = (0, import_react23.useState)([]); +- const [name, setName] = (0, import_react23.useState)(""); +- const [busy, setBusy] = (0, import_react23.useState)(false); +- const toggle = (id) => setMembers((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); +- const pickAdmin = (id) => { +- setAdmin(id); +- setMembers((list) => list.filter((item) => item !== id)); +- }; +- const create = async () => { +- if (busy || admin === null || members.length === 0) return; +- setBusy(true); +- let room2; +- const done = await run(actions.createRoom([admin, ...members], { admin, greet: true, ...name.trim() ? { name: name.trim() } : {} }).then((value) => { +- room2 = value; +- })); +- if (done && room2) onCreated(room2); +- else setBusy(false); +- }; +- const chip = (bot, on, onClick, role) => (0, import_react23.createElement)("button", { +- key: bot.id, +- type: "button", +- role, +- className: "bt-ng-chip", +- "aria-checked": on, +- onClick +- }, (0, import_react23.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react23.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react23.createElement)(AdminStar) : null); +- return (0, import_react23.createElement)( +function RenameModal({ workflow, onClose, onConfirm }) { + const [name, setName] = (0, import_react37.useState)(workflow?.name ?? ""); + return (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-card bt-ng" }, +- (0, import_react23.createElement)( + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row bt-bs-stack" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react23.createElement)(AdminStar), t("Choose the group admin"))), +- (0, import_react23.createElement)( + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, + (0, import_react37.createElement)("h3", null, t("Rename workflow")), + (0, import_react37.createElement)( @@ -20844,19 +21781,15 @@ diff --git a/client.js b/client.js + (0, import_react37.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) + ), + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, +- roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) - ), -- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( -- "label", -- { className: "bt-set-row bt-bs-row bt-ng-step" }, -- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), -- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) -- ), -- (0, import_react23.createElement)( +- admin === null ? null : (0, import_react23.createElement)( + ) + ); +} @@ -20868,18 +21801,32 @@ diff --git a/client.js b/client.js + } }, + (0, import_react37.createElement)( "div", -- { className: "bt-set-row bt-task-foot" }, -- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), -- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) +- { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), +- (0, import_react23.createElement)( + { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, + (0, import_react37.createElement)("h3", null, t("Delete workflow")), + (0, import_react37.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), + (0, import_react37.createElement)( -+ "div", + "div", +- { className: "bt-ng-picks", role: "group", "aria-label": t("Members") }, +- roster.bots.filter((bot) => bot.id !== admin).map((bot) => chip(bot, members.includes(bot.id), () => toggle(bot.id), "checkbox")) + { className: "bt-modal-actions" }, + (0, import_react37.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), + (0, import_react37.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) -+ ) + ) +- ), +- admin === null || members.length === 0 ? null : (0, import_react23.createElement)( +- "label", +- { className: "bt-set-row bt-bs-row bt-ng-step" }, +- (0, import_react23.createElement)("span", { className: "bt-set-copy" }, (0, import_react23.createElement)("span", { className: "bt-bs-label" }, t("Name"))), +- (0, import_react23.createElement)("input", { className: "bt-bs-input", value: name, maxLength: 60, placeholder: t("Name after members"), onChange: (event) => setName(event.target.value) }) +- ), +- (0, import_react23.createElement)( +- "div", +- { className: "bt-set-row bt-task-foot" }, +- (0, import_react23.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), +- (0, import_react23.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) ) ); } @@ -22122,8 +23069,10 @@ diff --git a/client.js b/client.js - (0, import_react25.createElement)( + (0, import_react39.createElement)("label", { className: "bt-q-field" }, (0, import_react39.createElement)("input", { className: "bt-input", type: "text", value: message, "aria-label": t("Message to send to Feishu"), placeholder: t("Type a message for the Feishu group"), onChange: (event) => setMessage(event.target.value) })), + (0, import_react39.createElement)( -+ "div", -+ { className: "bt-connector-form-row" }, + "div", + { className: "bt-connector-form-row" }, +- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + (0, import_react39.createElement)("span", { className: "bt-connector-link" }), + (0, import_react39.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || message.trim() === "" || actions.connectorSend === void 0, onClick: () => void run(actions.connectorSend(connector.id, message), () => setMessage("")) }, t("Send")) + ) @@ -22152,10 +23101,8 @@ diff --git a/client.js b/client.js + })) : (0, import_react39.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react39.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react39.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react39.createElement)( - "div", - { className: "bt-connector-form-row" }, -- (0, import_react25.createElement)("a", { className: "bt-connector-link", href: TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, t("Create a token on GitHub")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { ++ "div", ++ { className: "bt-connector-form-row" }, + (0, import_react39.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, @@ -23294,15 +24241,11 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11025,257 +11025,21 @@ function migrateStorage(storage = globalThis.localStorage) { - } - } +@@ -11029,253 +11029,17 @@ function migrateStorage(storage = globalThis.localStorage) { + migrateBrowsers(storage); + } catch { } - try { -- migrateBrowsers(storage); -- } catch { -- } -- try { - const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); - if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { - storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); @@ -23477,10 +24420,6 @@ diff --git a/client.js b/client.js - entry.approvals.map((approval) => (0, import_react30.createElement)(ApprovalCard, { key: approval.id, approval, bot, actions })) - ); + try { -+ migrateBrowsers(storage); -+ } catch { -+ } -+ try { + const prefs = JSON.parse(storage.getItem(STORAGE.prefs) ?? "null"); + if (prefs && Object.hasOwn(LEGACY_THEMES, prefs.theme ?? "")) { + storage.setItem(STORAGE.prefs, JSON.stringify({ ...prefs, theme: LEGACY_THEMES[prefs.theme] })); @@ -25286,7 +26225,7 @@ diff --git a/client.js b/client.js const work = createSource({ bots: {}, approvals: [] }); let workKey = ""; const live = createSource({}); -@@ -13344,13 +13344,25 @@ function apply(ctx) { +@@ -13344,13 +13344,26 @@ function apply(ctx) { } return { ...state, unread }; }); @@ -25305,11 +26244,13 @@ diff --git a/client.js b/client.js const refreshed = /* @__PURE__ */ new Set(); let pulls = 0; let applied = 0; - const pull = async () => { +- const pull = async () => { ++ let reloading = 0; ++ const pull = async ({ fresh = false } = {}) => { const order = ++pulls; const known = roster.getSnapshot(); - const value = await call("state", { since: known.revision, models: known.modelsKey ?? "", locale: language() }); -+ const value = await call("state", { since: known.revision, models: known.modelsKey ?? "", locale: language(), watching: !document.hidden }); ++ const value = await call("state", { ...stateRequest(known, { fresh }), locale: language(), watching: !document.hidden }); if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); @@ -25334,13 +26275,25 @@ diff --git a/client.js b/client.js + live2.pluginTools = value.pluginTools; + } if (Object.keys(live2).length > 0 && current.ready) roster.set({ ...current, ...live2 }); - if (value.unchanged || value.revision === current.revision && value.modelsKey === current.modelsKey) return; +- if (value.unchanged || value.revision === current.revision && value.modelsKey === current.modelsKey) return; ++ if (stateAnswerIsStale(value, current, { fresh })) return; if (Object.keys(seenAt).length === 0) leaveVisits([...value.bots, ...value.rooms].map((entry) => entry.id)); roster.set(indexRoster(value)); + noteFriendLines(value); try { const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []); localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)); +@@ -13395,6 +13395,10 @@ function apply(ctx) { + const loop = async () => { + clearTimeout(timer); + if (running || stopped) return; ++ if (reloading > 0) { ++ timer = setTimeout(loop, 200); ++ return; ++ } + running = true; + try { + await pull(); @@ -13488,20 +13488,12 @@ function apply(ctx) { const style = installStyles("", "mentions"); let last = ""; @@ -25390,7 +26343,7 @@ diff --git a/client.js b/client.js if (isAgentSessionId(id)) return actions.openAgentSession(id); ui2.set((state) => state.newChat || state.exchange || state.menu || state.palette ? { ...state, newChat: null, exchange: null, menu: null, palette: null } : state); setPlain(false); -@@ -13653,12 +13653,13 @@ function apply(ctx) { +@@ -13653,18 +13653,27 @@ function apply(ctx) { }, // `last` is the Bot conversation the view held before it cleared: when that part // was archived for a fresh one, the view goes on in the fresh one. @@ -25407,6 +26360,20 @@ diff --git a/client.js b/client.js expected = target; ctx.uiWorkspace.openSession(target); }, + selectPanel: (id) => { + ctx.layout.selectPanel(id); + }, ++ // The read-only banner's Reload: read the whole team again, so a window whose other holder let ++ // go becomes writable without a page reload that would drop what is in the composer (#2247). ++ reloadTeam: () => { ++ reloading += 1; ++ return pull({ fresh: true }).finally(() => { ++ reloading -= 1; ++ }); ++ }, + openNewChat: (mode) => { + ui2.set((state) => ({ ...state, newChat: mode, menu: null, palette: null })); + }, @@ -13708,10 +13708,15 @@ function apply(ctx) { }, // `id` opens one Bot (page 'bots') or one group chat (page 'groups'). diff --git a/patches/ds-bot/0012-room-membership.patch b/patches/ds-bot/0012-room-membership.patch index db902b3e..01c6d6dc 100644 --- a/patches/ds-bot/0012-room-membership.patch +++ b/patches/ds-bot/0012-room-membership.patch @@ -816,7 +816,7 @@ diff --git a/src/client/sources.js b/src/client/sources.js // ------------------------------------------------------------------------- // Small observable sources for the inject `hooks` compartment. -@@ -16,7 +16,7 @@ export function createSource(initial) { +@@ -16,17 +16,41 @@ export function createSource(initial) { } } @@ -825,7 +825,20 @@ diff --git a/src/client/sources.js b/src/client/sources.js // Any Main Bot holds the Main Bot powers; `mainBotId`, the first one, is the sidebar tile. export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ?? []).includes(id) -@@ -24,9 +24,20 @@ export const isMainOf = (roster, id) => id !== undefined && (roster.mainBotIds ? ++// The `state` request the poll sends. `since` asks for a revision delta; a person who pressed ++// Reload asks for the whole team, so the answer carries `readOnly` re-read by the host (#2247). ++export const stateRequest = (current, { fresh = false } = {}) => ({ ++ since: fresh ? undefined : current?.revision, ++ models: current?.modelsKey ?? '', ++}) ++ ++// Whether a `state` answer may be skipped. An `unchanged` answer carries no team, so it is never ++// indexed; the poll also skips the same revision and models key. A reload draws that same revision: ++// that is how a stale read-only sentence clears without a page reload (#2247). ++export const stateAnswerIsStale = (value, current, { fresh = false } = {}) => ++ value?.unchanged === true || (!fresh && value?.revision === current?.revision && value?.modelsKey === current?.modelsKey) ++ + // A Bot's chat is a chain of Sessions ("parts"): the Bot id is its first part's id // and `sessionId` its current one. Every part id finds the Bot; Session state // (running, unread, turns) is read from the current part, team state from the Bot id. export function indexRoster(value) { diff --git a/patches/ds-bot/files/test/client-reload-state.test.js b/patches/ds-bot/files/test/client-reload-state.test.js new file mode 100644 index 00000000..cb977668 --- /dev/null +++ b/patches/ds-bot/files/test/client-reload-state.test.js @@ -0,0 +1,55 @@ +// #2247: a read-only window told the person to reload, and there was no reload entry. The +// sidebar's new Reload button asks for the team again. Two rules make that read useful, and +// they belong together: the request must carry no `since` (otherwise the host answers +// `unchanged`, with no team at all), and the answer must be drawn even though the revision did +// not move (otherwise the stale read-only sentence stays). Neither rule alone is enough: a +// skipped answer still drawn would be an empty roster, and a drawn answer still skipped is +// today's behaviour. +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { stateRequest, stateAnswerIsStale } from './src/client/sources.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) +const module = { exports: {} } +runInNewContext(outputFiles[0].text, { + module, exports: module.exports, console, Date, JSON, Promise, Symbol, + require: (name) => { + if (name === 'react') return { createElement: () => ({}) } + throw new Error(`Unexpected platform module ${name}`) + }, +}) +const { stateRequest, stateAnswerIsStale } = module.exports + +test('the poll asks for a revision delta; a reload asks for the whole team', () => { + const known = { revision: 7, modelsKey: 'models-1' } + const polled = stateRequest(known) + assert.equal(polled.since, 7) + assert.equal(polled.models, 'models-1') + const fresh = stateRequest(known, { fresh: true }) + assert.equal(fresh.since, undefined, 'no `since`, or the host answers `unchanged` and sends no team') + assert.equal(fresh.models, 'models-1') + // An empty roster before the first pull sends no number either. + assert.equal(stateRequest({ revision: undefined, modelsKey: undefined }).since, undefined) +}) + +test('a reload draws the same revision; the poll still skips it', () => { + const current = { revision: 7, modelsKey: 'models-1', readOnly: 'open in another dsh' } + const answer = { revision: 7, modelsKey: 'models-1', readOnly: null, bots: [] } + assert.equal(stateAnswerIsStale(answer, current), true, 'the poll skips an unchanged revision') + assert.equal(stateAnswerIsStale(answer, current, { fresh: true }), false, 'a reload draws it, which is how the sentence can clear') + // An `unchanged` answer carries no team, so it is never indexed, reload or not. + assert.equal(stateAnswerIsStale({ revision: 7, unchanged: true }, current, { fresh: true }), true) + // A new revision is always drawn. + assert.equal(stateAnswerIsStale({ revision: 8, modelsKey: 'models-1' }, current), false) + // A new models key is drawn even at the same revision. + assert.equal(stateAnswerIsStale({ revision: 7, modelsKey: 'models-2' }, current), false) +}) diff --git a/patches/ds-bot/files/test/sidebar-readonly-reload.test.js b/patches/ds-bot/files/test/sidebar-readonly-reload.test.js new file mode 100644 index 00000000..51a40332 --- /dev/null +++ b/patches/ds-bot/files/test/sidebar-readonly-reload.test.js @@ -0,0 +1,166 @@ +// #2247: the host's read-only sentence tells the person to "close the other one and reload", +// but the window offered no reload entry at all. The banner now carries one Reload button in +// both sidebar shapes. It sits beside the status line, not inside it: a `role="status"` region +// must stay text, or a screen reader reads the button as part of every announcement. The button +// runs `actions.reloadTeam`, the client's full-state read, so the window becomes writable once +// the other holder let go — without a page reload that would drop what is in the composer. +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { build } from 'esbuild' + +const { outputFiles } = await build({ + stdin: { + contents: "export { TeamSidebar, SIDEBAR_CSS } from './src/client/sidebar.js'", + resolveDir: fileURLToPath(new URL('..', import.meta.url)), + }, + bundle: true, format: 'cjs', platform: 'browser', write: false, logLevel: 'silent', + external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], +}) + +const h = (type, props, ...children) => ({ type, props: { ...props, children: children.flat(Infinity) } }) +class Component { + constructor(props) { this.props = props } + setState() {} +} +const react = { + createElement: h, + Fragment: Symbol('Fragment'), + Component, + PureComponent: Component, + createRef: () => ({ current: null }), + useState: initial => [typeof initial === 'function' ? initial() : initial, () => {}], + useEffect() {}, + useLayoutEffect() {}, + useRef: value => ({ current: value }), + useCallback: fn => fn, + useMemo: fn => fn(), +} +const slots = { entries: () => [], subscribe: () => () => {}, version: () => 0 } + +const load = () => { + const module = { exports: {} } + runInNewContext(outputFiles[0].text, { + module, + exports: module.exports, + console, + Date, + JSON, + Promise, + Symbol, + __flybot__: { slots, call: () => {} }, + require: (name) => { + if (name === 'react') return react + if (name === 'react-dom') return { createPortal: node => node } + if (name === '@deepseek-ai/dsh-client-ui-primitives') return {} + throw new Error(`Unexpected platform module ${name}`) + }, + }) + return module.exports +} + +const { TeamSidebar, SIDEBAR_CSS } = load() + +const HOST_TEXT = 'The Bot team is open in another dsh.' + +function renderSidebar({ wide, readOnly = HOST_TEXT, readOnlyKey = true, reloadTeam, reportError } = {}) { + const roster = { ready: true, bots: [], rooms: [], agents: [], byId: {}, roomsById: {}, mainBotId: undefined, ...readOnlyKey ? { readOnly } : {} } + const actions = { + openInbox() {}, openBoard() {}, openPalette() {}, openNewChat() {}, + landOnMainBot() {}, clearUnread() {}, isAgentSession: () => false, + reloadTeam, reportError, + } + return TeamSidebar({ + wide, + useRoster: select => select(roster), + useUi: select => select({}), + useSessions: select => select({ ids: [], byId: {} }), + useRegistry: () => {}, + useUpdate: select => select({}), + useWork: select => select({}), + useSessionStatus: select => select({}), + useSessionRetainInfo: select => select({}), + useActivity: select => select({}), + actions, + }) +} + +// The element tree down to the child components, which stay opaque. +const walk = (node, out = []) => { + if (Array.isArray(node)) { + for (const child of node) walk(child, out) + return out + } + if (!node || typeof node !== 'object') return out + out.push(node) + walk(node.props?.children, out) + return out +} +const all = tree => walk(tree) +const byClass = (tree, name) => all(tree).filter(node => { + const value = node.props?.className + return typeof value === 'string' && value.split(' ').includes(name) +}) +const textOf = node => (node.props?.children ?? []) + .filter(child => typeof child === 'string' || typeof child === 'number') + .join('') + +test('both sidebar shapes offer one Reload button beside the read-only sentence', () => { + for (const wide of [true, false]) { + const shape = wide ? 'wide' : 'rail' + const tree = renderSidebar({ wide }) + const hints = byClass(tree, 'bt-read-only') + assert.equal(hints.length, 1, `${shape}: the host's sentence is still drawn once`) + const buttons = byClass(tree, 'bt-read-only-reload') + assert.equal(buttons.length, 1, `${shape}: the banner offers one Reload entry`) + const button = buttons[0] + assert.equal(button.type, 'button', `${shape}: it is a button`) + assert.equal(button.props.type, 'button', `${shape}: it does not submit a form`) + assert.equal(textOf(button), 'Reload', `${shape}: its label comes from the shared copy`) + // The sentence keeps its status line; the control lives outside it, in reading order after it. + assert.equal(all(hints[0]).includes(button), false, `${shape}: the live region stays text-only`) + assert.ok(all(tree).indexOf(hints[0]) < all(tree).indexOf(button), `${shape}: the sentence is read first`) + } +}) + +test('the Reload button starts the client\'s full-state read', async () => { + let calls = 0 + const tree = renderSidebar({ wide: true, reloadTeam: () => { calls += 1; return Promise.resolve() } }) + const [button] = byClass(tree, 'bt-read-only-reload') + button.props.onClick() + await Promise.resolve() + await Promise.resolve() + assert.equal(calls, 1, 'one press reads the team once') +}) + +test('a failed reload says so through the sidebar error channel, not an unhandled rejection', async () => { + const failure = new Error('network error: Failed to fetch') + const seen = [] + const tree = renderSidebar({ wide: false, reloadTeam: () => Promise.reject(failure), reportError: (error, where) => seen.push([error, where]) }) + const [button] = byClass(tree, 'bt-read-only-reload') + button.props.onClick() + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() + assert.deepEqual(seen, [[failure, 'reload']]) +}) + +test('a writable window, or a host sentence of only whitespace, offers no Reload entry', () => { + for (const wide of [true, false]) { + const shape = wide ? 'wide' : 'rail' + for (const readOnly of [null, '', ' ', '\n\t ']) { + const tree = renderSidebar({ wide, readOnly }) + assert.equal(byClass(tree, 'bt-read-only-reload').length, 0, `${shape}, ${JSON.stringify(readOnly)}`) + } + assert.equal(byClass(renderSidebar({ wide, readOnlyKey: false }), 'bt-read-only-reload').length, 0, `${shape}, no field`) + } +}) + +test('both shapes carry a style for the Reload button, and the rail rule wins over the generic one', () => { + assert.match(SIDEBAR_CSS, /\.bt-read-only-reload\{[^}]*cursor:pointer/) + const rail = SIDEBAR_CSS.match(/\.bt-rail \.bt-read-only-reload\{([^}]*)\}/) + assert.ok(rail, 'the rail needs its own compact rule') + assert.match(rail[1], /text-align:center/) + assert.ok(SIDEBAR_CSS.indexOf('.bt-rail .bt-read-only-reload') > SIDEBAR_CSS.indexOf('.bt-read-only-reload{')) +})