diff --git a/changelog.d/1810-settings-workflows-poll.fixed.md b/changelog.d/1810-settings-workflows-poll.fixed.md new file mode 100644 index 000000000..7cee97791 --- /dev/null +++ b/changelog.d/1810-settings-workflows-poll.fixed.md @@ -0,0 +1 @@ +Settings → Workflows follows a live run instead of freezing at the moment the page opened: while any workflow has work running the page re-reads `workflow/state` every 2 seconds — the room drawer's own beat — so a step that goes running → done changes its chip without reopening the page, an edit in progress suspends the refresh, and the page stops asking when the last run finishes or it is closed ([#1810](https://github.com/ThinkFlowLab/FlyBot/issues/1810)). diff --git a/core/client.js b/core/client.js index c767a925a..b234d04e8 100644 --- a/core/client.js +++ b/core/client.js @@ -15072,6 +15072,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } // src/client/workflows.js var import_react36 = require("react"); var READ_MS = 8e3; +var POLL_MS6 = 2e3; var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; function workflowCallMissing(failure) { @@ -15559,6 +15560,20 @@ function WorkflowsPage({ call: call5, roster }) { gone = true; }; }, []); + const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); + (0, import_react36.useEffect)(() => { + if (!live || editing) return void 0; + let gone = false; + const timer = setInterval(() => { + readWorkflows(invoke).then((settled) => { + if (!gone) apply2(settled); + }); + }, POLL_MS6); + return () => { + gone = true; + clearInterval(timer); + }; + }, [live, editing]); const retry = () => { setPhase("loading"); setError(""); @@ -15567,8 +15582,8 @@ function WorkflowsPage({ call: call5, roster }) { }; const open = (item, source) => { if (source === "factory") { - const live = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); - setOpened(live ? { source: "own", id: live.id, fromFactory: item.id } : { source: "factory", id: item.id }); + const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); + setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); setEditing(false); return; } diff --git a/core/src/client/workflows.js b/core/src/client/workflows.js index 5da52dcc6..ea009a9f5 100644 --- a/core/src/client/workflows.js +++ b/core/src/client/workflows.js @@ -7,6 +7,10 @@ import { t } from './i18n.js' export const READ_MS = 8000 +// The room's Workflow drawer re-reads a live run on this beat; Settings shows the +// same data, so it moves at the same speed instead of freezing at mount (#1810). +export const POLL_MS = 2000 + // Shown when @flybot/workflow is not registered. A default install includes it; // the page says so instead of the host's "Unknown FlyBot endpoint workflow/…" string. const WORKFLOW_PLUGIN_HINT = 'The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.' @@ -418,6 +422,19 @@ export function WorkflowsPage({ call, roster }) { return () => { gone = true } }, []) + // The page used to read once, so a running step's chip kept the colour it had at + // mount. While any workflow is live it asks again on the room's beat; an edit + // suspends that, because a refresh would land in the middle of one (#1810). + const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0) + useEffect(() => { + if (!live || editing) return undefined + let gone = false + const timer = setInterval(() => { + readWorkflows(invoke).then((settled) => { if (!gone) apply(settled) }) + }, POLL_MS) + return () => { gone = true; clearInterval(timer) } + }, [live, editing]) + const retry = () => { setPhase('loading') setError('') diff --git a/core/test/workflows-page.test.js b/core/test/workflows-page.test.js index 7062ed403..701869863 100644 --- a/core/test/workflows-page.test.js +++ b/core/test/workflows-page.test.js @@ -17,23 +17,137 @@ const { outputFiles } = await build({ external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], }) const h = (type, props, ...children) => ({ type, props: { ...props, children } }) -let states = [] +// #1810: the old fixture faked React with a no-op useEffect and a setter that never +// re-rendered, so no case in this file could observe an effect or a second read — a +// polling case written against it passed no matter what the page did. This runtime +// keeps hook slots per component, re-runs an effect only when its deps change, and +// re-renders when a setter fires. The 2 s beat stays in the test's hands (the env +// `setInterval` below is a fake clock); real `setTimeout` stays real so the +// read-timeout cases still settle. +const runtime = { + instances: new Map(), + key: 'root', + index: 0, + dirty: false, + unmounted: false, + reset() { + this.instances = new Map() + this.key = 'root' + this.index = 0 + this.dirty = false + this.unmounted = false + }, + instance(key) { + let inst = this.instances.get(key) + if (!inst) { + inst = { slots: [], refs: [], pending: [], deps: new Map(), cleanups: new Map() } + this.instances.set(key, inst) + } + return inst + }, +} +function withInstance(key, run) { + const previousKey = runtime.key + const previousIndex = runtime.index + runtime.key = key + runtime.index = 0 + try { return run() } finally { runtime.key = previousKey; runtime.index = previousIndex } +} +function recordEffect(run, deps) { + const inst = runtime.instance(runtime.key) + inst.pending.push({ index: runtime.index++, run, deps }) +} +function commitEffects() { + for (const inst of runtime.instances.values()) { + const pending = inst.pending + inst.pending = [] + for (const { index, run, deps } of pending) { + const previous = inst.deps.get(index) + const changed = !deps || !previous || deps.length !== previous.length || deps.some((dep, at) => !Object.is(dep, previous[at])) + if (!changed) continue + const cleanup = inst.cleanups.get(index) + if (typeof cleanup === 'function') cleanup() + inst.cleanups.delete(index) + inst.deps.set(index, deps ? [...deps] : null) + const next = run() + if (typeof next === 'function') inst.cleanups.set(index, next) + } + } +} +function unmountAll() { + runtime.unmounted = true + for (const inst of runtime.instances.values()) { + for (const cleanup of inst.cleanups.values()) if (typeof cleanup === 'function') cleanup() + inst.cleanups.clear() + inst.pending = [] + } + runtime.instances.clear() +} const react = { - createElement: h, Fragment: Symbol('F'), useEffect() {}, useLayoutEffect() {}, useMemo: (fn) => fn(), useCallback: (fn) => fn(), - useRef: (current) => ({ current }), useSyncExternalStore: (_s, get) => get(), useState: (value) => [states.length > 0 ? states.shift() : value, () => {}], + createElement: h, Fragment: Symbol('F'), + useEffect: (run, deps) => recordEffect(run, deps), + useLayoutEffect: (run, deps) => recordEffect(run, deps), + useMemo: (fn) => fn(), useCallback: (fn) => fn(), + useRef: (current) => { + const inst = runtime.instance(runtime.key) + const index = runtime.index++ + if (!(index in inst.refs)) inst.refs[index] = { current } + return inst.refs[index] + }, + useSyncExternalStore: (_s, get) => get(), + useState: (initial) => { + const inst = runtime.instance(runtime.key) + const index = runtime.index++ + if (!(index in inst.slots)) inst.slots[index] = typeof initial === 'function' ? initial() : initial + const set = (next) => { + const value = typeof next === 'function' ? next(inst.slots[index]) : next + if (Object.is(value, inst.slots[index])) return + inst.slots[index] = value + if (!runtime.unmounted) runtime.dirty = true + } + return [inst.slots[index], set] + }, } -const module = { exports: {} } -runInNewContext(outputFiles[0].text, { - module, exports: module.exports, document: { hidden: false }, navigator: { language: 'en' }, localStorage: { getItem: () => null, setItem() {} }, - setTimeout, clearTimeout, - 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}`) +// The fake beat: only `setInterval` is faked, so `advance` is the test's sole clock. +const beat = { + now: 0, nextId: 0, timers: new Map(), + reset() { this.now = 0; this.nextId = 0; this.timers.clear() }, + setInterval(fn, ms) { + const id = ++this.nextId + this.timers.set(id, { at: this.now + (Number(ms) || 0), every: Math.max(1, Number(ms) || 1), fn }) + return id }, -}) -const { WorkflowsPage, WorkflowsView, readWorkflows, settleWorkflowRead, moveNode, connectNodes, dropBot, removeNode, installLocale, wf } = module.exports + clearInterval(id) { this.timers.delete(id) }, + due(target) { + let found = null + for (const [id, timer] of this.timers) { + if (timer.at > target) continue + if (!found || timer.at < found.timer.at) found = { id, timer } + } + return found + }, + fire(found) { + this.now = found.timer.at + this.timers.set(found.id, { ...found.timer, at: found.timer.at + found.timer.every }) + found.timer.fn() + }, +} +const env = { + module: { exports: {} }, + document: { hidden: false }, navigator: { language: 'en' }, localStorage: { getItem: () => null, setItem() {} }, + setTimeout, clearTimeout, + setInterval: (fn, ms) => beat.setInterval(fn, ms), + clearInterval: (id) => beat.clearInterval(id), +} +env.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}`) +} +env.module.exports = env.module.exports +runInNewContext(outputFiles[0].text, env) +const { WorkflowsPage, WorkflowsView, readWorkflows, settleWorkflowRead, moveNode, connectNodes, dropBot, removeNode, installLocale, wf } = env.module.exports const PLUGIN_HINT = 'The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.' @@ -49,12 +163,12 @@ function fakeLocale(active) { } } -const expand = (node) => { +const expand = (node, path = 'r') => { if (node === null || node === undefined || typeof node === 'boolean') return null - if (Array.isArray(node)) return node.map(expand) + if (Array.isArray(node)) return node.map((child, at) => expand(child, `${path}.${at}`)) if (typeof node !== 'object') return node - if (typeof node.type === 'function') return expand(node.type(node.props)) - return { ...node, props: { ...node.props, children: expand(node.props.children) } } + if (typeof node.type === 'function') return withInstance(path, () => expand(node.type(node.props), path)) + return { ...node, props: { ...node.props, children: expand(node.props.children, path) } } } const all = (node, out = []) => { if (node && typeof node === 'object') { @@ -64,9 +178,70 @@ const all = (node, out = []) => { return out } const text = (node) => (node === null || node === undefined ? '' : Array.isArray(node) ? node.map(text).join('') : typeof node === 'object' ? text(node.props.children) : String(node)) -const renderPage = (props, given = []) => { states = [...given]; const tree = expand(h(WorkflowsPage, props)); states = []; return tree } +// A render pass: drop every fresh effect record, render, then commit the effects +// whose deps changed. A setter fired inside an effect marks the tree dirty, and the +// caller's `settle` renders again — the shape of a page that reacts to a read. +function renderTree(node) { + runtime.dirty = false + for (const inst of runtime.instances.values()) inst.pending = [] + const tree = expand(node) + commitEffects() + return tree +} +const renderPage = (props) => { + runtime.reset() + return renderTree(h(WorkflowsPage, props)) +} const renderView = (props) => expand(h(WorkflowsView, props)) +// Mounts the page and hands the test the clock and the tree it can drive. Nothing +// here is a second implementation of the page: the same component renders, the same +// effects run, and `advance` is the only thing that moves time. +function mountPage(props) { + runtime.reset() + beat.reset() + let tree = renderTree(h(WorkflowsPage, props)) + const settle = async () => { + for (let guard = 0; guard < 50; guard += 1) { + await new Promise((resolve) => setImmediate(resolve)) + if (!runtime.dirty) return + tree = renderTree(h(WorkflowsPage, props)) + } + throw new Error('the page never settled') + } + const page = { + get tree() { return tree }, + settle, + async advance(ms) { + const target = beat.now + ms + for (let guard = 0; guard < 200; guard += 1) { + const found = beat.due(target) + if (!found) break + beat.fire(found) + // A real 2 s tick lets its read land and re-render before the next tick, so a + // poll that stops the interval (the run finished) stops it here too. + await settle() + } + beat.now = Math.max(beat.now, target) + }, + unmount() { unmountAll() }, + async click(match) { + const button = all(tree).find((entry) => entry.type === 'button' && match(entry)) + assert.ok(button, 'the button the test clicks exists') + button.props.onClick({ stopPropagation() {}, preventDefault() {}, target: {} }) + await settle() + return button + }, + } + return page +} +const chipOf = (tree, id) => { + const card = all(tree).find((entry) => entry.props?.['data-node'] === id) + const chip = card ? all(card).find((entry) => typeof entry.props?.className === 'string' && entry.props.className.startsWith('bt-wf-chip')) : null + return chip ? text(chip) : '' +} +const openWorkflow = (page, id) => page.click((entry) => entry.props?.['data-workflow'] === id) + const HOSTILE = '' const factory = { id: 'delivery', @@ -501,3 +676,105 @@ test('the measured width is the canvas clientWidth, the same box the scroll rang assert.equal(wf.measureCanvasWidth({ clientWidth: -1 }), 0) assert.equal(wf.measureCanvasWidth({ clientWidth: Number.NaN }), 0) }) + +// #1810: Settings → Workflows read once at mount, so a step that went running → done +// left its chip frozen until the page was reopened, while the room drawer for the same +// run already moved every 2 s. These cases drive the page through a fake clock: the +// mount read, then the ticks, then the render each answer causes. +const livePack = (nodeStatus, { activeCount = 1, status = 'running' } = {}) => ({ + ok: true, + templates: [factory], + workflows: [{ ...own, activeCount, run: { status, nodeStatus } }], +}) + +test('#1810: a running workflow is read again every 2 s and the chip follows the run', async () => { + let reads = 0 + const answers = [ + livePack({ planner: 'done', coder: 'running' }), + livePack({ planner: 'done', coder: 'done' }, { activeCount: 0, status: 'done' }), + ] + const page = mountPage({ + call: async () => { + reads += 1 + return answers[Math.min(reads - 1, answers.length - 1)] + }, + roster: { bots: [] }, + }) + await page.settle() + assert.equal(reads, 1, 'the mount read') + await openWorkflow(page, 'wf-mine') + assert.equal(chipOf(page.tree, 'coder'), 'Running') + + await page.advance(6000) + assert.ok(reads >= 2, `the page asked again while the run was live (${reads} read(s))`) + assert.equal(chipOf(page.tree, 'coder'), 'Done', 'the chip moved without remounting the page') + assert.equal(chipOf(page.tree, 'planner'), 'Done') + + // The last answer said every run is finished, so the beat stops: no more reads. + assert.equal(reads, 2) + await page.advance(6000) + assert.equal(reads, 2, 'a finished run is not read again') +}) + +test('#1810: nothing running means one read in ten seconds, not a poll', async () => { + let reads = 0 + const idle = { ok: true, templates: [factory], workflows: [{ ...own, activeCount: 0, run: { status: 'done', nodeStatus: { planner: 'done', coder: 'done' } } }] } + const page = mountPage({ call: async () => { reads += 1; return idle }, roster: { bots: [] } }) + await page.settle() + assert.equal(reads, 1) + await page.advance(10000) + assert.equal(reads, 1, 'an idle page never asks a second time') +}) + +test('#1810: an edit suspends the poll and finishing it brings the poll back', async () => { + let reads = 0 + const page = mountPage({ call: async () => { reads += 1; return livePack({ planner: 'done', coder: 'running' }) }, roster: { bots: [] } }) + await page.settle() + await openWorkflow(page, 'wf-mine') + await page.click((entry) => text(entry) === 'Edit') + assert.equal(reads, 1) + await page.advance(10000) + assert.equal(reads, 1, 'a refresh does not land in the middle of an edit') + await page.click((entry) => text(entry) === 'Finish editing') + await page.advance(2500) + assert.equal(reads, 2, 'leaving the editor resumes the beat') +}) + +test('#1810: a page that unmounts stops asking', async () => { + let reads = 0 + const page = mountPage({ call: async () => { reads += 1; return livePack({ planner: 'done', coder: 'running' }) }, roster: { bots: [] } }) + await page.settle() + assert.equal(reads, 1) + page.unmount() + await page.advance(10000) + assert.equal(reads, 1, 'no read after the page is gone') +}) + +test('#1810: a failed poll shows the error line and Retry, not an empty page', async () => { + const unhandled = [] + const onUnhandled = (reason) => unhandled.push(reason) + process.on('unhandledRejection', onUnhandled) + try { + let reads = 0 + const page = mountPage({ + call: async () => { + reads += 1 + if (reads === 1) return livePack({ planner: 'done', coder: 'running' }) + throw new Error('workflow host down') + }, + roster: { bots: [] }, + }) + await page.settle() + await openWorkflow(page, 'wf-mine') + await page.advance(2000) + assert.equal(reads, 2, 'the poll happened and failed') + const body = text(page.tree) + assert.match(body, /workflow host down/) + assert.match(body, /Retry/) + assert.doesNotMatch(body, /No workflows yet/) + await new Promise((resolve) => setImmediate(resolve)) + assert.deepEqual(unhandled, []) + } finally { + process.off('unhandledRejection', onUnhandled) + } +}) diff --git a/docs/subsystems/core-client.md b/docs/subsystems/core-client.md index f92b23a3d..5635e0c05 100644 --- a/docs/subsystems/core-client.md +++ b/docs/subsystems/core-client.md @@ -198,7 +198,7 @@ The member's own chat keeps its `busyAcks[sessionId]` bubble and its visibility - The + menu is a popover anchored next to the sidebar **+** (#195, #173): its `.bt-pane` carries `data-menu` and stays see-through, the menu is a 280px card at the top left with no title row, and a press on the pane outside the card (or Escape) closes it. New chat, New group chat, New Bot and plugin rows open their full pane as before. - Settings → **Workflows** flowchart wire plane (#1459): `GraphCanvas` in `workflows.js` places nodes on a zero-width center line (`.bt-wf-plane`). Connector paths are drawn in an SVG shifted by `WIRE_SPAN` (480): the plane sets `--bt-wf-wire-span` on its inline style and `.bt-wf-wires` in `styles.js` uses `left:calc(-1 * var(--bt-wf-wire-span))` and `width:calc(2 * var(--bt-wf-wire-span))`, so coordinates `node.x + WIRE_SPAN` line up with each card's center instead of fanning to a corner. The `@flybot/workflow` stage drawer uses the same anchoring (`--wf-wire-span` on `.wf-plane`, matching rules on `.wf-wires`). `NODE_WIRE_OUT` is the bottom port offset for the cubic segment. A Settings graph whose authored 960px plane is wider than the canvas is fitted instead of clipped (#1752): `fitScale(nodes, width)` returns one `scale()` from the widest content reach (`|x| + 108`, so the dashed frame counts), capped at 1 and floored at 0.5 when the canvas is too narrow for half size; `GraphCanvas` measures the canvas (`getBoundingClientRect` plus a `ResizeObserver`) and wraps the plane in `.bt-wf-fit`, so cards, frames and the wire SVG share the transform. The parallel frame is the union of its members' boxes (`min(x) - 108` … `max(x) + 108`, `min(y) - 18` … `max(y) + 72 + 10`), so a member the editor stored lower is enclosed; pointer deltas and drop offsets are divided by the scale, so dragging still commits authored plane coordinates. Below the 0.5 floor the plane is wider than the canvas by design, and a scroller cannot go left of `scrollLeft` 0, so #1769 reserves the leftover on the left instead of letting it spill out of both edges: `fitLayout(nodes, width)` returns `{ reach, scale, pad, left, right, scrollWidth }` from the same reach, `GraphCanvas` puts `pad` on the stage's `padding-left`, everything then sits at `x >= 0` and only the right side scrolls. A graph that fits returns `pad` 0 and keeps the #1752 scale unchanged; the width can be seeded through the `canvasWidth` prop before the first measure, and the measurement still wins. Tests: `plugins/workflow/test/client.test.js` (`wirePath` regression); Settings: `core/test/workflows-page.test.js`. -- Settings → **Workflows** (`src/client/workflows.js`, #1364) sits right after **Scheduled tasks**, beside the other automation page, so the page list's existing order and the plugin-slot rule (a `flybot.settings.page` entry is listed after Connectors) are unchanged. It is not a second row in the account menu: the menu keeps its one name per destination. The page reads `@flybot/workflow` through `call('workflow/state')`: **Factory presets** are the template catalog (a drawable graph, so a pipeline can be opened before it is copied) and **Yours** are the workflows saved on this machine whose ids are not those templates. Each user-owned workflow row shows rename (✎) and delete (×) action buttons; factory presets show no actions and refuse both operations (#1434). Rename opens a modal with a name field and calls `workflow/rename`; delete confirms ("Are you sure you want to delete '{name}'? This cannot be undone.") and calls `workflow/delete`, which removes the workflow from state, releases ephemeral Bots if a run exists, and cleans logs/runs/routes. Opening one draws the flowchart read-only: a saved workflow gets **Edit** (`WorkflowsView` in `src/client/workflows.js`), and only then can a node be moved, an edge drawn, a local Bot dropped, a node removed, and each node's **system prompt** typed in a field beside the canvas — that prompt is what the node's run Bot receives — and the picture is saved with `workflow/graph`; **Finish editing** leaves that mode (#1393, #1378). A factory preset stays read-only until **Use this** copies it with `workflow/create`, and that copy opens in Edit. A read that rejects or never returns settles as an error with **Retry**; an empty store says there are no workflows. When that read is `bot/not-found`, or the host says `Unknown FlyBot endpoint workflow/…`, the page says the Workflow plugin is not installed or not enabled (a default install includes `@flybot/workflow`) in the interface language, and **Retry** stays (#1381). Any other failure still shows its own reason. The page does not teach `workflows/.json`. The definition engine that still stores those files stays on the host and is not this page. Tests: `core/test/workflows-page.test.js`. +- Settings → **Workflows** (`src/client/workflows.js`, #1364) sits right after **Scheduled tasks**, beside the other automation page, so the page list's existing order and the plugin-slot rule (a `flybot.settings.page` entry is listed after Connectors) are unchanged. It is not a second row in the account menu: the menu keeps its one name per destination. The page reads `@flybot/workflow` through `call('workflow/state')`: **Factory presets** are the template catalog (a drawable graph, so a pipeline can be opened before it is copied) and **Yours** are the workflows saved on this machine whose ids are not those templates. Each user-owned workflow row shows rename (✎) and delete (×) action buttons; factory presets show no actions and refuse both operations (#1434). Rename opens a modal with a name field and calls `workflow/rename`; delete confirms ("Are you sure you want to delete '{name}'? This cannot be undone.") and calls `workflow/delete`, which removes the workflow from state, releases ephemeral Bots if a run exists, and cleans logs/runs/routes. Opening one draws the flowchart read-only: a saved workflow gets **Edit** (`WorkflowsView` in `src/client/workflows.js`), and only then can a node be moved, an edge drawn, a local Bot dropped, a node removed, and each node's **system prompt** typed in a field beside the canvas — that prompt is what the node's run Bot receives — and the picture is saved with `workflow/graph`; **Finish editing** leaves that mode (#1393, #1378). A factory preset stays read-only until **Use this** copies it with `workflow/create`, and that copy opens in Edit. While any workflow of the page has `activeCount > 0` the page reads `workflow/state` again every 2 s — the room drawer's own predicate and beat — so a chip that goes running → done moves without reopening the page and a finished run stops the beat; an **Edit** in progress suspends it and unmount clears it (#1810). A read that rejects or never returns settles as an error with **Retry**; an empty store says there are no workflows. When that read is `bot/not-found`, or the host says `Unknown FlyBot endpoint workflow/…`, the page says the Workflow plugin is not installed or not enabled (a default install includes `@flybot/workflow`) in the interface language, and **Retry** stays (#1381). Any other failure still shows its own reason. The page does not teach `workflows/.json`. The definition engine that still stores those files stays on the host and is not this page. Tests: `core/test/workflows-page.test.js`. - The sidebar footer button (`ConnectApps` in `sidebar.js`, label **Connect apps** / 连接应用) opens Settings > Connectors through `actions.openConnectors()`, the same call and the same words as the account menu's **Connect apps** row. The shell's plugin panel stays reachable in the Bot surface through the account menu's **Plugins** row (`actions.selectPanel('plugins')`, 插件), without switching to classic Agent mode. - One name per destination in the account menu and command palette (#392, #393, #394): **Settings** opens the Settings dialog (General), since **Bot settings** / **Edit Bot** open one Bot; **Scheduled tasks** opens Settings → Scheduled tasks, the name Settings and a Bot's details use (DSH's own bundle stays "Automation tasks" in the Plugins hints); and **Switch to classic Agent mode** is the only way to classic Agent mode, so Settings → General is Appearance only. - Create Bot starts on the Main Bot's model (#230): `startModel()` (`bot-draft.js`) picks the Main Bot's chosen model, else the one it last ran on, when it is served; then `roster.defaultModel`, then the first served model. Only served models are picked, so a provider without a credential is never preselected. diff --git a/patches/ds-bot/0008-client-bundle.patch b/patches/ds-bot/0008-client-bundle.patch index ca2ca388d..3d649cea1 100644 --- a/patches/ds-bot/0008-client-bundle.patch +++ b/patches/ds-bot/0008-client-bundle.patch @@ -3,7 +3,7 @@ FlyBot patch 0008-client-bundle on Fei2411/DeepSeek-Bot@2b10c70 (ds-bot/). Gener diff --git a/client.js b/client.js --- a/client.js +++ b/client.js -@@ -42,6 +42,57 @@ +@@ -42,6 +42,57 @@ module.exports = __toCommonJS(index_exports); // src/client/i18n.js var NAMESPACE = "ds-bot"; var ZH = { @@ -61,7 +61,7 @@ diff --git a/client.js b/client.js // Shared "Main Bot": "主 Bot", Working: "工作中", -@@ -49,6 +49,67 @@ +@@ -49,6 +49,67 @@ var ZH = { "Unread activity": "有未读", Search: "搜索", "New chat": "新对话", @@ -129,7 +129,7 @@ diff --git a/client.js b/client.js "New…": "新建", "Animation": "动画", "Bot animation": "Bot 动画", -@@ -63,6 +63,21 @@ +@@ -63,6 +63,21 @@ var ZH = { Duplicate: "复制一份", Hidden: "已隐藏", Admin: "管理员", @@ -151,7 +151,7 @@ diff --git a/client.js b/client.js Add: "添加", Close: "关闭", Copy: "复制", -@@ -78,21 +78,72 @@ +@@ -78,21 +78,72 @@ var ZH = { " and ": "、", and: "和", "{count} members": "{count} 位成员", @@ -227,7 +227,7 @@ diff --git a/client.js b/client.js Connectors: "连接器", "Search connectors": "搜索连接器", "No connectors": "暂无连接器", -@@ -129,6 +129,59 @@ +@@ -129,6 +129,59 @@ var ZH = { "Connecting…": "正在连接…", "Could not connect: {error}": "连接失败:{error}", "Connected as {account}": "已连接:{account}", @@ -287,7 +287,7 @@ diff --git a/client.js b/client.js Connected: "已连接", Disconnect: "断开", Connect: "连接", -@@ -141,17 +141,64 @@ +@@ -141,17 +141,64 @@ var ZH = { "GitHub addresses must start with https://": "GitHub 地址必须以 https:// 开头", "This DSH cannot load connectors": "这个 DSH 不能加载连接器", "No such connector": "没有这个连接器", @@ -356,7 +356,7 @@ diff --git a/client.js b/client.js // Header "Message {name}": "给 {name} 发消息", Message: "发消息", -@@ -162,8 +162,6 @@ +@@ -162,8 +162,6 @@ var ZH = { "Dismiss new messages": "关闭新消息提示", New: "新", // New chat @@ -365,7 +365,7 @@ diff --git a/client.js b/client.js "Add to group chat": "加入群聊", "Message Bot": "给 Bot 发消息", "To:": "发给:", -@@ -187,9 +187,24 @@ +@@ -187,9 +187,24 @@ var ZH = { Everyone: "所有人", "Every member answers in turn, admin first.": "每位成员轮流回答,管理员先说。", "Members · {count}": "成员 · {count}", @@ -390,7 +390,7 @@ diff --git a/client.js b/client.js "Remove {name} from the group": "把 {name} 移出群聊", "Remove from group": "移出群聊", "Add member": "添加成员", -@@ -205,6 +205,16 @@ +@@ -205,6 +205,16 @@ var ZH = { "Close details": "关闭详情", "Copied conversation": "已复制对话", "Share conversation": "分享对话", @@ -407,7 +407,7 @@ diff --git a/client.js b/client.js "Copy conversation as Markdown": "把对话复制为 Markdown", "This conversation is not part of your Bot team.": "这个对话不属于你的 Bot 团队。", "Admin {name}": "管理员 {name}", -@@ -247,6 +247,25 @@ +@@ -247,6 +247,25 @@ var ZH = { "This model is not available now. Pick another one.": "这个模型现在不可用,请换一个。", "Every message of this Bot runs on it.": "这个 Bot 的每条消息都用它。", "Applies from the next message.": "从下一条消息开始生效。", @@ -433,7 +433,7 @@ diff --git a/client.js b/client.js Profile: "资料", Label: "标签", "Optional. Shown under the name.": "可选,显示在名字下方。", -@@ -270,6 +270,7 @@ +@@ -270,6 +270,7 @@ var ZH = { "Kept above the other chats.": "排在其他对话上面。", "Search still finds it.": "仍然可以搜索到。", Manage: "管理", @@ -441,7 +441,7 @@ diff --git a/client.js b/client.js // Group chat settings "Leave it empty to name the group after its members.": "留空就按成员命名。", "Every member reads it before speaking in this group.": "每位成员在这个群里发言前都会读到。", -@@ -303,6 +303,8 @@ +@@ -303,6 +303,8 @@ var ZH = { "Harness default": "Harness 默认", Saving: "保存中", "Manage models": "管理模型", @@ -450,7 +450,7 @@ diff --git a/client.js b/client.js // Shapes Whale: "鲸鱼", Star: "星星", -@@ -374,9 +374,10 @@ +@@ -374,9 +374,10 @@ var ZH = { "No group chats yet.": "还没有群聊。", "A group chat lets several Bots work on one thing with you.": "在群聊里,几个 Bot 和你一起做一件事。", "Back to group chats": "返回群聊列表", @@ -463,7 +463,7 @@ diff --git a/client.js b/client.js "Harness settings": "Harness 设置", "Back to Bots": "返回 Bot 列表", "Close settings": "关闭设置", -@@ -385,7 +385,6 @@ +@@ -385,7 +385,6 @@ var ZH = { Pin: "置顶", Pinned: "已置顶", "Mark as Unread": "标为未读", @@ -471,7 +471,7 @@ diff --git a/client.js b/client.js "Group settings": "群设置", "Rename group": "重命名群聊", "Copy conversation ID": "复制对话 ID", -@@ -394,6 +394,8 @@ +@@ -394,6 +394,8 @@ var ZH = { // The DSH Agent's own row and Session list. "DSH Agent" is a product name and // stays in English. "Add DSH Agent": "添加 DSH Agent", @@ -480,7 +480,7 @@ diff --git a/client.js b/client.js "Remove DSH Agent": "移除 DSH Agent", "New session": "新会话", Archive: "归档", -@@ -404,13 +404,13 @@ +@@ -404,13 +404,13 @@ var ZH = { "{n} h": "{n} 小时", "{n} d": "{n} 天", // Plugin updates @@ -500,7 +500,7 @@ diff --git a/client.js b/client.js "You have {version}.": "当前版本 {version}。", "The update did not finish: {reason}": "更新没有完成:{reason}", "What's new": "更新说明", -@@ -420,8 +420,98 @@ +@@ -420,8 +420,98 @@ var ZH = { "DSH update": "DSH 更新", "DSH {version}": "DSH {version}", "Restart DSH to start using DSH {version}.": "重启 DSH 后开始使用 DSH {version}。", @@ -600,7 +600,7 @@ diff --git a/client.js b/client.js // Command palette Messages: "消息", Commands: "命令", -@@ -446,10 +446,13 @@ +@@ -446,10 +446,13 @@ var ZH = { Reply: "回复", "More message actions": "更多操作", More: "更多", @@ -614,7 +614,7 @@ diff --git a/client.js b/client.js "Earlier messages": "更早的消息", "Loading earlier messages…": "正在加载更早的消息…", "Couldn’t load earlier messages · Retry": "更早的消息没加载出来 · 重试", -@@ -459,6 +459,9 @@ +@@ -459,6 +459,9 @@ var ZH = { "Replies from": "群聊回复:", "Reply from": "收到回复:", "Message from": "收到消息:", @@ -624,7 +624,7 @@ diff --git a/client.js b/client.js "Stopped by": "叫停者:", "{count} messages with": "{count} 条消息,来自", "Routine started": "例行任务开始", -@@ -468,6 +468,8 @@ +@@ -468,6 +468,8 @@ var ZH = { "Working out a plan": "理一下思路", "Sorting out the details": "梳理细节", "On it": "在做了", @@ -633,7 +633,7 @@ diff --git a/client.js b/client.js "Busy with it": "正在处理", "Moving ahead": "推进中", "Taking care of it": "处理中", -@@ -495,7 +495,6 @@ +@@ -495,7 +495,6 @@ var ZH = { "Looking back through the chat": "翻看聊天记录", "{name} looked up its own chat": "{name} 查看了自己的对话", "{name} looked up its own chat for “{text}”": "{name} 在自己的对话里查找「{text}」", @@ -641,7 +641,7 @@ diff --git a/client.js b/client.js "{name} didn't answer in time": "{name} 没有及时回应", "{name} paused for your approval": "{name} 停下来等你批准", "{name} started on it": "{name} 开始做了", -@@ -506,12 +506,31 @@ +@@ -506,12 +506,31 @@ var ZH = { "Asked to make the tool call it wrote as text": "工具调用被写成了文字,已让它重新调用", "A Bot": "一个 Bot", "{name} is waiting for your approval": "{name} 等你批准", @@ -674,7 +674,7 @@ diff --git a/client.js b/client.js "Couldn't message {name}": "没能给 {name} 发消息", Messaged: "已发消息给", "Couldn't stop {name}": "没能叫停 {name}", -@@ -533,13 +533,14 @@ +@@ -533,13 +533,14 @@ var ZH = { Selected: "已选", Skip: "跳过", Submit: "提交", @@ -690,7 +690,7 @@ diff --git a/client.js b/client.js "No Bot": "没有 Bot", "Paste or type the key": "粘贴或输入密钥", "Only {name}": "只给 {name}", -@@ -555,7 +555,6 @@ +@@ -555,7 +555,6 @@ var ZH = { "At least {count} characters.": "至少 {count} 个字符。", "Replace value": "替换值", "Saving…": "保存中…", @@ -698,7 +698,7 @@ diff --git a/client.js b/client.js "Allowing…": "允许中…", Allow: "允许", "{bot} asked for {name}": "{bot} 请求了 {name}", -@@ -582,7 +582,7 @@ +@@ -582,7 +582,7 @@ var ZH = { "NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN", "What it is for": "用途", Value: "值", @@ -707,7 +707,7 @@ diff --git a/client.js b/client.js "Save key": "保存密钥", "Keys your Bots asked for. Bots never see the values: they use them as ": "你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ", " in their shell, and every value is replaced by ": " 使用,而且发给模型之前,每个值都会被替换成 ", -@@ -621,7 +621,6 @@ +@@ -621,7 +621,6 @@ var ZH = { Remove: "删除", "Ask or update": "询问或更新", "Ask about or update {name}'s memory": "询问或更新 {name} 的记忆", @@ -715,7 +715,7 @@ diff --git a/client.js b/client.js "Added: {text}": "已添加:{text}", "Added to team memory: {text}": "已加入团队记忆:{text}", "Updated: {text}": "已更新:{text}", -@@ -747,11 +747,11 @@ +@@ -747,11 +747,11 @@ var ZH = { "Compared with the {count} days before": "与再往前 {count} 天相比", "{share}% of all usage from {span}": "占 {span} 全部用量的 {share}%", // About @@ -730,7 +730,7 @@ diff --git a/client.js b/client.js Updates: "更新", "Check for updates": "检查更新", "Checking…": "正在检查…", -@@ -761,6 +761,13 @@ +@@ -761,6 +761,13 @@ var ZH = { "Update checks are off.": "已关闭更新检查。", "Installed from a local folder; update it there.": "从本地文件夹安装,请在那里更新。", "Updates are not offered for this install.": "这种安装方式不提供更新。", @@ -744,7 +744,7 @@ diff --git a/client.js b/client.js Links: "链接", "Source code": "源代码", "Release notes": "发布说明", -@@ -776,7 +776,7 @@ +@@ -776,7 +776,7 @@ var ZH = { Show: "查看", Hide: "收起", "GitHub issue": "GitHub issue", @@ -753,7 +753,7 @@ diff --git a/client.js b/client.js "Report on GitHub": "在 GitHub 上报告", "Copied the diagnostics.": "已复制诊断信息。", "Could not copy. Select the text below and copy it.": "无法复制。请选中下面的文字手动复制。", -@@ -788,7 +788,7 @@ +@@ -788,7 +788,7 @@ var ZH = { "Recent errors": "最近的错误", "Recent errors · {count}": "最近的错误 · {count}", "No errors recorded.": "没有错误记录。", @@ -762,7 +762,7 @@ diff --git a/client.js b/client.js "Model call": "模型调用", "This window": "这个窗口", "Error log": "错误日志", -@@ -821,12 +821,21 @@ +@@ -821,12 +821,21 @@ var ZH = { "Add at least one more Bot": "再至少添加一个 Bot", Members: "成员", // Connectors and scheduled tasks @@ -784,7 +784,7 @@ diff --git a/client.js b/client.js "Once, {time}": "一次,{time}", "Every day at {time}": "每天 {time}", "Every {days} at {time}": "每{days} {time}", -@@ -834,7 +834,21 @@ +@@ -834,7 +834,21 @@ var ZH = { "Every weekend at {time}": "每个周末 {time}", "On the schedule {expression}": "按计划 {expression}", "Next: {time}": "下次 {time}", @@ -806,7 +806,7 @@ diff --git a/client.js b/client.js "in {name}": "在 {name}", "Click again to delete": "再点一次删除", "Delete task": "删除任务", -@@ -844,6 +844,7 @@ +@@ -844,6 +844,7 @@ var ZH = { minutes: "分钟", hours: "小时", days: "天", @@ -814,7 +814,7 @@ diff --git a/client.js b/client.js "Morning briefing": "早间简报", "What to do": "要做什么", "Sum up what changed since yesterday and what needs me today.": "总结一下昨天以来的变化,以及今天需要我处理的事。", -@@ -856,9 +856,20 @@ +@@ -856,9 +856,20 @@ var ZH = { Unit: "单位", "Create task": "创建任务", "Turn on Automation tasks in DSH": "在 DSH 里打开「自动化任务」", @@ -837,7 +837,7 @@ diff --git a/client.js b/client.js "No scheduled tasks yet.": "还没有定时任务。", "No scheduled tasks yet. Add one": "还没有定时任务,去添加", "Automation tasks are off in DSH": "DSH 的「自动化任务」没有打开", -@@ -869,10 +869,19 @@ +@@ -869,10 +869,19 @@ var ZH = { "Pick a date and time": "选一个日期和时间", "Pick a time in the future": "选一个将来的时间", "Pick a time of day": "选一个时间", @@ -857,7 +857,7 @@ diff --git a/client.js b/client.js "Working folder": "工作文件夹", "Working folder: {folder}": "工作文件夹:{folder}", Folder: "文件夹", -@@ -887,7 +887,70 @@ +@@ -887,7 +887,70 @@ var ZH = { "Use this folder": "使用这个文件夹", "Up one level": "上一级", "No folders inside.": "里面没有文件夹。", @@ -929,7 +929,7 @@ diff --git a/client.js b/client.js }; var HOST_ERRORS = [ [/^Model (.+) is not available\. Available: (.*)$/, "模型 $1 不可用。可用的:$2"], -@@ -900,8 +900,11 @@ +@@ -900,8 +900,11 @@ var HOST_ERRORS = [ [/^Only the user, a Main Bot, or the admin of "(.+)" can change it$/, "只有你、主 Bot 或「$1」的管理员可以修改"], [/^A key must have at least (\d+) characters$/, "密钥至少 $1 个字符"], [/^A key can have at most (\d+) characters$/, "密钥最多 $1 个字符"], @@ -941,7 +941,7 @@ diff --git a/client.js b/client.js [/^HTTP (\d+)$/, "请求失败(HTTP $1)"], [/^GitHub answered (\d+)$/, "GitHub 返回了 $1"], [/^Keep the name under (\d+) characters$/, "名字不能超过 $1 个字"], -@@ -918,7 +918,14 @@ +@@ -918,7 +918,14 @@ var HOST_ERRORS = [ [/^There is no entry (.+)\.$/, "没有第 $1 条"], [/^No entry in [\s\S]* matches "([\s\S]*)"\.[\s\S]*$/, "找不到「$1」这条记忆"], [/^"([\s\S]*)" matches (\d+) entries in [\s\S]*$/, "「$1」对应了 $2 条记忆,请说得更具体"], @@ -957,7 +957,7 @@ diff --git a/client.js b/client.js ]; var HOST_ZH = { "Request failed": "请求失败", -@@ -930,6 +930,20 @@ +@@ -930,6 +930,20 @@ var HOST_ZH = { "Unknown Bot": "找不到这个 Bot", "Unknown conversation": "找不到这个对话", "A group chat needs at least two Bots": "群聊至少要两个 Bot", @@ -978,7 +978,7 @@ diff --git a/client.js b/client.js "The admin must be a member of the group": "管理员必须是群成员", "Unknown group chat": "找不到这个群聊", "Hand the admin role to another member instead of leaving the group without one": "请把管理员交给其他成员,不要让群聊没有管理员", -@@ -951,10 +951,12 @@ +@@ -951,10 +951,12 @@ var HOST_ZH = { "The model wrote nothing": "模型没有输出内容", "Write the entry to save.": "请写下要保存的内容。", "This DeepSeek Harness has no folder chooser": "这个 DeepSeek Harness 没有文件夹选择窗口", @@ -993,7 +993,7 @@ diff --git a/client.js b/client.js var service = null; var bound = null; function installLocale(locale) { -@@ -1005,15 +1005,23 @@ +@@ -1005,15 +1005,23 @@ function noteClientError(where, error) { } const message = String(error?.message ?? error ?? "error").slice(0, 300); const code = typeof error?.code === "string" ? error.code : void 0; @@ -1020,7 +1020,7 @@ diff --git a/client.js b/client.js if (entries.length > MAX) entries.splice(0, entries.length - MAX); } var quietly = (where) => (error) => noteClientError(where, error); -@@ -1042,16 +1042,21 @@ +@@ -1042,16 +1042,21 @@ var FAILURES = { }; function failureLabel(code, message) { if (FAILURES[code]) return t(FAILURES[code]); @@ -1048,7 +1048,7 @@ diff --git a/client.js b/client.js const full2 = build(diagnostics); if (full2.length <= URL_LIMIT) return { url: full2, inline: true }; const facts = build(`${diagnostics.split("\n\n")[0]} -@@ -1066,10 +1066,16 @@ +@@ -1066,10 +1066,16 @@ var REFUSALS = /* @__PURE__ */ new Set(["bot/refused", "bot/invalid", "bot/read- var call = async (endpoint, payload, signal) => { let response; try { @@ -1066,7 +1066,7 @@ diff --git a/client.js b/client.js body: JSON.stringify({ endpoint, payload: payload ?? null }), signal }); -@@ -1087,7 +1087,7 @@ +@@ -1087,7 +1087,7 @@ var call = async (endpoint, payload, signal) => { if (!result.ok) { const code = result.error?.code ?? "bot/error"; const message = hostText(result.error?.message ?? "Request failed"); @@ -1075,7 +1075,7 @@ diff --git a/client.js b/client.js if (!REFUSALS.has(code)) { noteClientError(endpoint, Object.assign(new Error(result.error?.message ?? "Request failed"), { code })); markNoted(error); -@@ -1097,6 +1097,388 @@ +@@ -1097,6 +1097,388 @@ var call = async (endpoint, payload, signal) => { return result.value; }; @@ -1464,7 +1464,7 @@ diff --git a/client.js b/client.js // src/client/inks.js var INKS = { deepseek: ["#4D6BFE", "#6A84FF"], -@@ -1160,9 +1160,9 @@ +@@ -1160,9 +1160,9 @@ var MAIN_LOOK = Object.freeze({ shape: "whale", color: "deepseek" }); var CHARACTER_COLORS = ["black", "brown", "red", "orange", "yellow", "green", "cyan", "blue", "violet", "magenta", "gray", "aurora"]; var DEFAULT_COLORS = CHARACTER_COLORS.filter((color) => !["black", "gray", "brown"].includes(color) && !GRADIENTS[color]); var isColor = (color) => isHex(color) || Object.hasOwn(INKS, color); @@ -1476,7 +1476,7 @@ diff --git a/client.js b/client.js return value >>> 0; } function defaultColor(id) { -@@ -1177,19 +1177,96 @@ +@@ -1177,19 +1177,96 @@ function defaultShape(id) { value = Math.imul(value ^ value >>> 13, 3266489909); return DEFAULT_SHAPES[((value ^ value >>> 16) >>> 0) % DEFAULT_SHAPES.length]; } @@ -1576,7 +1576,7 @@ diff --git a/client.js b/client.js ``, `` ].join(""); -@@ -1206,8 +1206,8 @@ +@@ -1206,8 +1206,8 @@ function bodyOf(def, paint, flat = false) { return ""; } } @@ -1587,7 +1587,7 @@ diff --git a/client.js b/client.js var maskBox = (size) => `maskUnits="userSpaceOnUse" x="${-size}" y="${-size}" width="${size * 3}" height="${size * 3}"`; var holes = (def, eyes) => `${eyes}`; var MASK_ID = "%MASK%"; -@@ -1242,192 +1242,46 @@ +@@ -1242,192 +1242,46 @@ function maskOf(shape) { } var DEFAULT_MASK = maskOf(FALLBACK_SHAPE); @@ -1688,16 +1688,7 @@ diff --git a/client.js b/client.js - "--bt-tag-bg": rgba(mode.text, dark ? 0.09 : 0.06), - "--bt-tag-ink": rgba(mode.text, 0.58) - }; -+// src/client/cluster-order.js -+function clusterOrder(people2) { -+ const list = Array.isArray(people2) ? people2 : []; -+ const remote = (bot) => Boolean(bot?.remote); -+ if (!list.some(remote)) return list; -+ const locals = list.filter((bot) => !remote(bot)); -+ const remotes = list.filter(remote); -+ if (list.length <= 4) return [...remotes, ...locals]; -+ return [...locals, ...remotes]; - } +-} -var declarations = (vars) => Object.entries(vars).map(([name, value]) => `${name}:${value}`).join(";"); -var themeCss = (theme, accent) => `html body{${declarations(themeVars(theme.light, false, accent ?? theme.light.accent))}} -html body[data-ds-dark-theme]{${declarations(themeVars(theme.dark, true, accent ?? theme.dark.accent))}} @@ -1740,7 +1731,16 @@ diff --git a/client.js b/client.js - else delete SHAPES[id]; - refresh(); - }; --} ++// src/client/cluster-order.js ++function clusterOrder(people2) { ++ const list = Array.isArray(people2) ? people2 : []; ++ const remote = (bot) => Boolean(bot?.remote); ++ if (!list.some(remote)) return list; ++ const locals = list.filter((bot) => !remote(bot)); ++ const remotes = list.filter(remote); ++ if (list.length <= 4) return [...remotes, ...locals]; ++ return [...locals, ...remotes]; + } -function registerTheme(id, theme) { - if (typeof id !== "string" || !/^[a-z][\w-]{0,31}$/.test(id)) throw new Error("Theme ids are lowercase words"); - for (const mode of ["light", "dark"]) { @@ -1799,10 +1799,15 @@ diff --git a/client.js b/client.js const skip = [ ...[...value.matchAll(LITERAL)].map((match) => ({ start: match.index, end: match.index + match[0].length })), ...referenceSpans(value, references) -@@ -1445,24 +1445,86 @@ +@@ -1445,24 +1445,86 @@ function mentionSpans(text, bots, { references = [] } = {}) { } return spans; } +- +-// src/client/text.js +-var MENTION_ORIGIN = "https://bots.local/"; +-function linkMentions(text, bots, selfId) { +- if (!text || !text.includes("@")) return text; +function unmatchedMentions(text2, bots, { references = [] } = {}) { + const value = String(text2 ?? ""); + const skip = [ @@ -1850,11 +1855,9 @@ diff --git a/client.js b/client.js + } + return stripFence(out).replace(/\s+/g, " ").trim(); +} - - // src/client/text.js - var MENTION_ORIGIN = "https://bots.local/"; --function linkMentions(text, bots, selfId) { -- if (!text || !text.includes("@")) return text; ++ ++// src/client/text.js ++var MENTION_ORIGIN = "https://bots.local/"; +function linkMentions(text2, bots, selfId) { + if (!text2 || !text2.includes("@")) return text2; + const ordered = [...bots ?? []].sort((a, b) => Number(Boolean(a?.remote)) - Number(Boolean(b?.remote))); @@ -1891,7 +1894,7 @@ diff --git a/client.js b/client.js function clock(ms) { return new Date(ms).toLocaleTimeString(dateLocale(), { hour: "numeric", minute: "2-digit" }); } -@@ -1483,9 +1483,18 @@ +@@ -1483,9 +1483,18 @@ function relativeShort(ms) { var contentText = (content) => (content ?? []).filter((block) => block?.type === "text").map((block) => block.text).join("\n"); var HEADER = /^\[(Message from|Reply from|Group chat|Group post from|Bot team setup|Handoff · part|Secret card)[^\]]*\]\n?/; var currentPart = (bot) => bot.sessionId ?? bot.id; @@ -1913,7 +1916,7 @@ diff --git a/client.js b/client.js try { const prompts = [...one ? [JSON.parse(one[1])] : [], ...batch ? JSON.parse(batch[1]).map((entry) => entry.reminder_prompt) : []]; return prompts.filter((prompt) => typeof prompt === "string" && prompt.trim() !== ""); -@@ -1493,38 +1493,121 @@ +@@ -1493,38 +1493,121 @@ function reminderPrompts(text) { return []; } } @@ -2042,7 +2045,7 @@ diff --git a/client.js b/client.js var historyLoading = (snapshot) => snapshot?.openState === "cold" || snapshot?.openState === "loading"; function toolArgs(root) { if (typeof root.args?.text === "function") { -@@ -1597,17 +1597,46 @@ +@@ -1597,17 +1597,46 @@ function commRunOf(order, peers, turn, hiddenTurns = []) { } return { count, peerIds }; } @@ -2092,16 +2095,65 @@ 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,9 +1624,982 @@ +@@ -1624,201 +1624,1174 @@ 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 }); } +- +-// src/client/motion.js +-var import_react = require("react"); +-var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; +-function springEasing(stiffness, damping) { +- const w0 = Math.sqrt(stiffness); +- const zeta = damping / (2 * w0); +- const at = (t2) => { +- if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); +- const wd = w0 * Math.sqrt(1 - zeta * zeta); +- return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); +- }; +- let end = 0.05; +- while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; +- const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); +- return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; +function faceRowTitle(title) { + const text2 = String(title ?? ""); + const named = text2.match(/^Face-to-face · (.+) · (\d{6})$/); + if (named) return `${named[2]} · ${named[1]}`; + return text2.match(/^Face-to-face · (\d{6})$/)?.[1] ?? text2; -+} + } +-var SPRING_SHAPE = springEasing(400, 31.2); +-var SPRING_TAP = springEasing(900, 48); +-var SPRING_LIST = springEasing(484, 35.2); +-var SPRING_STRETCH = springEasing(320, 20); +-var SPRING_POP = springEasing(500, 22); +-var SPRING_LEAD = springEasing(784, 48.2); +-var SPRING_TRAIL = springEasing(289, 29.2); +-var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; +-var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); +-function liveSprings(apply2) { +- const values = /* @__PURE__ */ new Map(); +- let frame = 0; +- let last = 0; +- const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); +- const step = (now) => { +- const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); +- last = now; +- const steps = Math.ceil(dt * 240); +- const h34 = dt / steps; +- let moving = false; +- for (const value of values.values()) { +- for (let index = 0; index < steps; index += 1) { +- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; +- value.x += value.v * h34; +- } +- if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { +- value.x = value.to; +- value.v = 0; +- } else moving = true; +- } +- apply2(read()); +- frame = moving ? requestAnimationFrame(step) : 0; +- }; +function modelOf(bot, roster) { + const ref = bot.model ?? bot.appliedModel; + if (!ref) return void 0; @@ -2116,28 +2168,84 @@ diff --git a/client.js b/client.js +function awayGroups(groups, roomsById) { + return (groups ?? []).filter((entry) => roomsById?.[entry.id]?.origin !== "face-to-face"); +} - --// src/client/motion.js ++ +// src/client/sources.js +function createSource(initial) { + let value = initial; + const listeners2 = /* @__PURE__ */ new Set(); -+ return { + return { +- target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, +- set(targets, curves, jump) { +- for (const [key, to] of Object.entries(targets)) { +- const value = values.get(key); +- const { stiffness: k, damping: c } = curves[key]; +- if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); +- else Object.assign(value, { to, k, c }); +- } +- if (jump) { +- cancelAnimationFrame(frame); +- frame = 0; +- apply2(read()); +- return; +- } +- if (!frame) { +- last = performance.now(); +- frame = requestAnimationFrame(step); +- } + getSnapshot: () => value, + subscribe: (listener) => { + listeners2.add(listener); + return () => { + listeners2.delete(listener); + }; -+ }, + }, +- stop: () => cancelAnimationFrame(frame) + set(next) { + const resolved = typeof next === "function" ? next(value) : next; + if (resolved === value) return; + value = resolved; + for (const listener of [...listeners2]) listener(); + } -+ }; -+} + }; + } +-var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; +-function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { +- const ownBox = (0, import_react.useRef)(null); +- const box = given ?? ownBox; +- const thumb = (0, import_react.useRef)(null); +- const own = (0, import_react.useRef)(null); +- if (own.current === null) { +- own.current = { +- key: void 0, +- node: null, +- observer: null, +- springs: liveSprings(({ start, end }) => { +- const knob = thumb.current; +- if (!knob) return; +- knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; +- knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; +- }) +- }; +- } +- const place = (jump) => { +- const container = box.current; +- const knob = thumb.current; +- if (!container || !knob) return; +- const chosen = container.querySelector(selector); +- if (!chosen) { +- knob.dataset.hidden = ""; +- return; +- } +- const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; +- const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); +- const { springs } = own.current; +- const before = springs.target(); +- const hidden = knob.dataset.hidden !== void 0; +- if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; +- delete knob.dataset.hidden; +- const forward = before === null || start >= before.start; +- springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); +- 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); +function indexRoster(value) { @@ -2150,7 +2258,26 @@ diff --git a/client.js b/client.js + if (prev && prev !== bot && !prev.remote && bot?.remote) return; + if (prev && prev !== bot && prev.id === id) return; + byId[id] = bot; -+ }; + }; +- (0, import_react.useLayoutEffect)(() => { +- const state = own.current; +- const node = box.current; +- if (node === state.node && key === state.key) return; +- const fresh = node !== state.node; +- state.key = key; +- if (fresh) { +- state.observer?.disconnect(); +- state.node = node; +- state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; +- state.observer?.observe(node); +- } +- place(fresh); +- }); +- (0, import_react.useEffect)(() => () => { +- own.current.observer?.disconnect(); +- own.current.springs.stop(); +- }, []); +- return { box, thumb }; + for (const bot of value.bots) { + for (const id of [...bot.parts ?? [], bot.sessionId ?? bot.id, bot.id]) claim(bot, id); + } @@ -2163,20 +2290,74 @@ diff --git a/client.js b/client.js + for (const sessionId of agent.sessions ?? []) agentOf[sessionId] = agent.id; + } + return { ...value, agents: value.agents ?? [], byId, roomsById, agentsById, agentOf, ready: true }; -+} + } +-function Segmented({ label, value, className, children }) { +- const glide = useGlide(value); +- return (0, import_react.createElement)( +- "div", +- { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, +- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- children +- ); +function rosterBot(roster, id) { + if (id === void 0 || id === null || id === "") return void 0; + const list = roster?.bots ?? []; + const own = list.find((bot) => bot && !bot.remote && (bot.id === id || bot.sessionId === id || (bot.parts ?? []).includes(id))); + if (own) return own; + return roster?.byId?.[id]; -+} + } +-function useFlip(list, key, selector, id = (node) => node.dataset.flip) { +- const tops = (0, import_react.useRef)(null); +- (0, import_react.useLayoutEffect)(() => { +- const container = list.current; +- if (!container) return; +- const next = /* @__PURE__ */ new Map(); +- const moving = tops.current !== null && !reducedMotion(); +- for (const node of container.querySelectorAll(selector)) { +- const name = id(node); +- const top = node.offsetTop; +- next.set(name, top); +- if (!moving) continue; +- const before = tops.current.get(name); +- if (before === void 0) { +- node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); +- } else if (Math.abs(before - top) > 0.5) { +- node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); +- } +- } +- tops.current = next; +- }, [key]); +function botByName(bots, name) { + const wanted = String(name ?? "").toLowerCase(); + if (wanted === "") return void 0; + const list = Array.isArray(bots) ? bots : []; + return list.find((bot) => bot && !bot.remote && String(bot.name ?? "").toLowerCase() === wanted) ?? list.find((bot) => bot && String(bot.name ?? "").toLowerCase() === wanted); -+} + } +-function useMorph(box, key) { +- const last = (0, import_react.useRef)({ key, height: null }); +- (0, import_react.useLayoutEffect)(() => { +- const node = box.current; +- const state = last.current; +- if (!node) { +- last.current = { key, height: null }; +- return; +- } +- const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); +- if (key === state.key) { +- if (running.length === 0) state.height = node.offsetHeight; +- return; +- } +- const from = running.length ? node.offsetHeight : state.height; +- for (const animation2 of running) animation2.cancel(); +- const to = node.offsetHeight; +- last.current = { key, height: to }; +- if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; +- node.dataset.morph = ""; +- const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); +- animation.onfinish = () => { +- delete node.dataset.morph; +- }; +- animation.oncancel = () => { +function isAgentSession(roster, cache, sessionId) { + if (sessionId === void 0 || sessionId === null || sessionId === "") return false; + if (roster.ready) return roster.agentOf[sessionId] !== void 0; @@ -2193,7 +2374,7 @@ diff --git a/client.js b/client.js +} + +// src/client/mark.js - var import_react = require("react"); ++var import_react = 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"; @@ -3073,90 +3254,193 @@ diff --git a/client.js b/client.js + +// src/client/motion.js +var import_react4 = require("react"); - var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; - function springEasing(stiffness, damping) { - const w0 = Math.sqrt(stiffness); -@@ -1659,12 +1659,12 @@ - const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); - last = now; - const steps = Math.ceil(dt * 240); -- const h34 = dt / steps; ++var reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; ++function springEasing(stiffness, damping) { ++ const w0 = Math.sqrt(stiffness); ++ const zeta = damping / (2 * w0); ++ const at = (t2) => { ++ if (zeta >= 1) return 1 - (1 + w0 * t2) * Math.exp(-w0 * t2); ++ const wd = w0 * Math.sqrt(1 - zeta * zeta); ++ return 1 - Math.exp(-zeta * w0 * t2) * (Math.cos(wd * t2) + zeta * w0 / wd * Math.sin(wd * t2)); ++ }; ++ let end = 0.05; ++ while (end < 2 && Math.abs(1 - at(end)) > 1e-3) end += 0.01; ++ const points = Array.from({ length: 25 }, (_, index) => (index === 24 ? 1 : at(end * index / 24)).toFixed(4)); ++ return { duration: Math.round(end * 1e3), easing: `linear(${points.join(",")})`, stiffness, damping }; ++} ++var SPRING_SHAPE = springEasing(400, 31.2); ++var SPRING_TAP = springEasing(900, 48); ++var SPRING_LIST = springEasing(484, 35.2); ++var SPRING_STRETCH = springEasing(320, 20); ++var SPRING_POP = springEasing(500, 22); ++var SPRING_LEAD = springEasing(784, 48.2); ++var SPRING_TRAIL = springEasing(289, 29.2); ++var QUICK_EDGES = { lead: springEasing(900, 51), trail: springEasing(400, 34) }; ++var spring = (curve, ...properties) => properties.map((property) => `${property} ${curve.duration}ms ${curve.easing}`).join(","); ++function liveSprings(apply2) { ++ const values = /* @__PURE__ */ new Map(); ++ let frame = 0; ++ let last = 0; ++ const read = () => Object.fromEntries([...values].map(([key, value]) => [key, value.x])); ++ const step = (now) => { ++ const dt = Math.min(0.5, Math.max(1e-3, (now - last) / 1e3)); ++ last = now; ++ const steps = Math.ceil(dt * 240); + const h50 = dt / steps; - let moving = false; - for (const value of values.values()) { - for (let index = 0; index < steps; index += 1) { -- value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h34; -- value.x += value.v * h34; ++ let moving = false; ++ for (const value of values.values()) { ++ for (let index = 0; index < steps; index += 1) { + value.v += (-value.k * (value.x - value.to) - value.c * value.v) * h50; + value.x += value.v * h50; - } - if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { - value.x = value.to; -@@ -1699,10 +1699,10 @@ - } - var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; - function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { -- const ownBox = (0, import_react.useRef)(null); ++ } ++ if (Math.abs(value.x - value.to) < 0.05 && Math.abs(value.v) < 1) { ++ value.x = value.to; ++ value.v = 0; ++ } else moving = true; ++ } ++ apply2(read()); ++ frame = moving ? requestAnimationFrame(step) : 0; ++ }; ++ return { ++ target: () => values.size ? Object.fromEntries([...values].map(([key, value]) => [key, value.to])) : null, ++ set(targets, curves, jump) { ++ for (const [key, to] of Object.entries(targets)) { ++ const value = values.get(key); ++ const { stiffness: k, damping: c } = curves[key]; ++ if (!value || jump) values.set(key, { x: to, v: 0, to, k, c }); ++ else Object.assign(value, { to, k, c }); ++ } ++ if (jump) { ++ cancelAnimationFrame(frame); ++ frame = 0; ++ apply2(read()); ++ return; ++ } ++ if (!frame) { ++ last = performance.now(); ++ frame = requestAnimationFrame(step); ++ } ++ }, ++ stop: () => cancelAnimationFrame(frame) ++ }; ++} ++var CHOSEN = '[aria-pressed="true"],[aria-selected="true"],[aria-current="page"]'; ++function useGlide(key, { axis = "x", selector = CHOSEN, lead = SPRING_LEAD, trail = SPRING_TRAIL, box: given } = {}) { + const ownBox = (0, import_react4.useRef)(null); - const box = given ?? ownBox; -- const thumb = (0, import_react.useRef)(null); -- const own = (0, import_react.useRef)(null); ++ const box = given ?? ownBox; + const thumb = (0, import_react4.useRef)(null); + const own = (0, import_react4.useRef)(null); - if (own.current === null) { - own.current = { - key: void 0, -@@ -1736,7 +1736,7 @@ - springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); - container.dataset.glide = ""; - }; -- (0, import_react.useLayoutEffect)(() => { ++ if (own.current === null) { ++ own.current = { ++ key: void 0, ++ node: null, ++ observer: null, ++ springs: liveSprings(({ start, end }) => { ++ const knob = thumb.current; ++ if (!knob) return; ++ knob.style.transform = axis === "x" ? `translateX(${start}px)` : `translateY(${start}px)`; ++ knob.style[axis === "x" ? "width" : "height"] = `${Math.max(0, end - start)}px`; ++ }) ++ }; ++ } ++ const place = (jump) => { ++ const container = box.current; ++ const knob = thumb.current; ++ if (!container || !knob) return; ++ const chosen = container.querySelector(selector); ++ if (!chosen) { ++ knob.dataset.hidden = ""; ++ return; ++ } ++ const start = axis === "x" ? chosen.offsetLeft : chosen.offsetTop; ++ const end = start + (axis === "x" ? chosen.offsetWidth : chosen.offsetHeight); ++ const { springs } = own.current; ++ const before = springs.target(); ++ const hidden = knob.dataset.hidden !== void 0; ++ if (jump && !hidden && before?.start === start && before?.end === end && container.dataset.glide !== void 0) return; ++ delete knob.dataset.hidden; ++ const forward = before === null || start >= before.start; ++ springs.set({ start, end }, forward ? { start: trail, end: lead } : { start: lead, end: trail }, jump || hidden || before === null || reducedMotion()); ++ container.dataset.glide = ""; ++ }; + (0, import_react4.useLayoutEffect)(() => { - const state = own.current; - const node = box.current; - if (node === state.node && key === state.key) return; -@@ -1750,7 +1750,7 @@ - } - place(fresh); - }); -- (0, import_react.useEffect)(() => () => { ++ const state = own.current; ++ const node = box.current; ++ if (node === state.node && key === state.key) return; ++ const fresh = node !== state.node; ++ state.key = key; ++ if (fresh) { ++ state.observer?.disconnect(); ++ state.node = node; ++ state.observer = node && typeof ResizeObserver === "function" ? new ResizeObserver(() => place(true)) : null; ++ state.observer?.observe(node); ++ } ++ place(fresh); ++ }); + (0, import_react4.useEffect)(() => () => { - own.current.observer?.disconnect(); - own.current.springs.stop(); - }, []); -@@ -1758,16 +1758,16 @@ - } - function Segmented({ label, value, className, children }) { - const glide = useGlide(value); -- return (0, import_react.createElement)( ++ own.current.observer?.disconnect(); ++ own.current.springs.stop(); ++ }, []); ++ return { box, thumb }; ++} ++function Segmented({ label, value, className, children }) { ++ const glide = useGlide(value); + return (0, import_react4.createElement)( - "div", - { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, -- (0, import_react.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), ++ "div", ++ { ref: glide.box, className: className ? `bt-seg ${className}` : "bt-seg", role: "group", "aria-label": label }, + (0, import_react4.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), - children - ); - } - function useFlip(list, key, selector, id = (node) => node.dataset.flip) { -- const tops = (0, import_react.useRef)(null); -- (0, import_react.useLayoutEffect)(() => { ++ children ++ ); ++} ++function useFlip(list, key, selector, id = (node) => node.dataset.flip) { + const tops = (0, import_react4.useRef)(null); + (0, import_react4.useLayoutEffect)(() => { - const container = list.current; - if (!container) return; - const next = /* @__PURE__ */ new Map(); -@@ -1788,8 +1788,8 @@ - }, [key]); - } - function useMorph(box, key) { -- const last = (0, import_react.useRef)({ key, height: null }); -- (0, import_react.useLayoutEffect)(() => { ++ const container = list.current; ++ if (!container) return; ++ const next = /* @__PURE__ */ new Map(); ++ const moving = tops.current !== null && !reducedMotion(); ++ for (const node of container.querySelectorAll(selector)) { ++ const name = id(node); ++ const top = node.offsetTop; ++ next.set(name, top); ++ if (!moving) continue; ++ const before = tops.current.get(name); ++ if (before === void 0) { ++ node.animate([{ opacity: 0, filter: "blur(4px)" }, { opacity: 1, filter: "blur(0)" }], { duration: 180, easing: "ease-out" }); ++ } else if (Math.abs(before - top) > 0.5) { ++ node.animate([{ transform: `translateY(${before - top}px)` }, { transform: "none" }], { duration: SPRING_LIST.duration, easing: SPRING_LIST.easing }); ++ } ++ } ++ tops.current = next; ++ }, [key]); ++} ++function useMorph(box, key) { + const last = (0, import_react4.useRef)({ key, height: null }); + (0, import_react4.useLayoutEffect)(() => { - const node = box.current; - const state = last.current; - if (!node) { -@@ -1817,8 +1817,8 @@ ++ const node = box.current; ++ const state = last.current; ++ if (!node) { ++ last.current = { key, height: null }; ++ return; ++ } ++ const running = node.getAnimations().filter((animation2) => animation2.id === "bt-morph"); ++ if (key === state.key) { ++ if (running.length === 0) state.height = node.offsetHeight; ++ return; ++ } ++ const from = running.length ? node.offsetHeight : state.height; ++ for (const animation2 of running) animation2.cancel(); ++ const to = node.offsetHeight; ++ last.current = { key, height: to }; ++ if (from === null || Math.abs(from - to) < 1 || reducedMotion()) return; ++ node.dataset.morph = ""; ++ const animation = node.animate([{ height: `${from}px` }, { height: `${to}px` }], { duration: SPRING_SHAPE.duration, easing: SPRING_SHAPE.easing, id: "bt-morph" }); ++ animation.onfinish = () => { ++ delete node.dataset.morph; ++ }; ++ animation.oncancel = () => { + if (!node.getAnimations().some((other) => other.id === "bt-morph" && other !== animation)) delete node.dataset.morph; + }; }); } function useLinger(value, ms) { @@ -3167,7 +3451,7 @@ diff --git a/client.js b/client.js if (value) { setKept(value); return void 0; -@@ -1838,6 +1838,7 @@ +@@ -1838,6 +1838,7 @@ ${Object.entries(INKS).map(([name, [, dark]]) => `body[data-ds-dark-theme]{--bt- .bt-mark{display:inline-block;flex:none;position:relative;line-height:0} .bt-mark svg{width:100%;height:100%;overflow:visible} .bt-mark-img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;box-shadow:inset 0 0 0 1px var(--bt-line)} @@ -3175,7 +3459,7 @@ diff --git a/client.js b/client.js .bt-mark .bt-b,.bt-mark .bt-eye,.bt-mark .bt-eyes,.bt-mark .bt-gaze{transform-box:fill-box} .bt-mark .bt-b{transform-origin:50% 100%} .bt-mark .bt-eye,.bt-mark .bt-eyes{transform-origin:center} -@@ -1856,6 +1856,14 @@ +@@ -1856,6 +1856,14 @@ body[data-bt-motion=lively] .bt-mark[data-live][data-state=idle][data-fidget=loo @keyframes bt-fidget-look{0%,22%,100%{transform:none}3%,9%{transform:translateX(-14%)}12%,19%{transform:translateX(14%)}} body[data-bt-motion=lively] .bt-badge-alert{right:auto;bottom:auto;left:50%;top:calc(var(--bt-size,36px) * -.14);translate:-50% 0;animation:bt-q-ride 1.3s cubic-bezier(.3,.7,.4,1) infinite} body[data-bt-motion=lively] :is(.bt-row,.bt-tile)>.bt-mark:hover .bt-badge-alert{animation:none} @@ -3190,7 +3474,7 @@ diff --git a/client.js b/client.js @keyframes bt-q-ride{0%,55%,100%{transform:translateY(0)}20%{transform:translateY(calc(var(--bt-size,36px) * -.14))}40%{transform:translateY(0)}} /* Petting: with the pointer on a sidebar avatar or just around it (a ring a fifth of its size wide), a resting mark leans into the pointer and keeps nuzzling it, its eyes -@@ -1933,15 +1933,23 @@ +@@ -1933,15 +1933,23 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-side{display:flex;flex-direction:column;min-height:0;flex:1 1 auto;gap:2px;padding:0 12px 8px 0;color:var(--bt-ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} .bt-side-top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:6px 5px 0 0} .bt-brand{display:inline-flex;align-items:center;gap:6px;margin-right:auto;padding:4px 8px 4px 4px;border:0;border-radius:8px;background:none;cursor:pointer;font-family:inherit;font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.01em;color:var(--bt-ink);white-space:nowrap} @@ -3216,7 +3500,7 @@ diff --git a/client.js b/client.js .bt-tile-name{font-size:13px;line-height:18px;font-weight:400;max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-chip{font-size:11px;line-height:16px;padding:1px 4px;border-radius:4px;background:var(--bt-hover);border:1px solid var(--bt-line);color:var(--bt-ink-2);white-space:nowrap;max-width:84px;overflow:hidden;text-overflow:ellipsis} .bt-row{display:flex;align-items:center;gap:8px;min-height:54px;padding:8px;border:0;border-radius:10px;background:transparent;color:var(--bt-ink);text-align:left;cursor:pointer;width:100%;box-sizing:border-box;font-family:inherit;transition:background-color .12s ease} -@@ -1950,27 +1950,32 @@ +@@ -1950,27 +1950,32 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-row-body{display:flex;flex-direction:column;min-width:0;flex:1 1 auto} .bt-row-name{display:flex;align-items:center;gap:6px;min-width:0;font-size:14px;line-height:20px;font-weight:400} .bt-row-title{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -3253,7 +3537,7 @@ diff --git a/client.js b/client.js .bt-pill-mem{flex:none;display:inline-flex;width:0;margin-left:-8px;overflow:hidden;transition:${spring(SPRING_SHAPE, "width", "margin-left")}} .bt-pill[data-memory=updated] .bt-pill-mem{width:var(--bt-mem-w);margin-left:0;transition:${spring(SPRING_STRETCH, "width", "margin-left")}} .bt-pill-mem-in{flex:none;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;color:var(--bt-ink-2);font-size:13px;font-weight:500;opacity:0;filter:blur(6px);transform:translateX(-8px);transition:opacity .14s ease,filter .18s ease,${spring(SPRING_SHAPE, "transform")}} -@@ -1992,6 +1992,7 @@ +@@ -1992,6 +1992,7 @@ body[data-bt-motion=quiet] .bt-mark[data-poke]{cursor:default} .bt-astack{display:flex;flex-direction:column;align-items:flex-start;gap:4px;margin:0} .bt-bubble{max-width:min(80%,560px,calc(100% - 82px));background:var(--bt-bubble);color:var(--bt-ink);border-radius:18px;padding:7px 12px;font-size:var(--dsh-content-font-size,14px);line-height:calc(var(--dsh-content-font-size,14px) + 8px);box-sizing:border-box;overflow-wrap:anywhere} @@ -3261,7 +3545,7 @@ diff --git a/client.js b/client.js .bt-msg:not(:first-child)>.bt-msg-line>.bt-bubble{border-start-start-radius:6px} .bt-msg:not(:last-child)>.bt-msg-line>.bt-bubble{border-end-start-radius:6px} .bt-bubble [class*="_markdown"]>*+*{margin-top:10px} -@@ -2085,6 +2085,7 @@ +@@ -2085,6 +2085,7 @@ button.bt-q-opt:active{background:var(--bt-active)} @media (prefers-reduced-motion:reduce){.bt-q{animation:bt-fade .12s ease-out backwards}.bt-q-settled{animation:none}.bt-q-submit{animation:none}} .bt-newchat{flex:1;display:flex;flex-direction:column;min-width:0;position:relative;animation:bt-veil-in .18s ease-out} @@ -3269,7 +3553,7 @@ diff --git a/client.js b/client.js .bt-to{display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-height:48px;padding:6px 12px;border-bottom:1px solid var(--bt-line);font-size:14px;box-sizing:border-box} .bt-to-label{color:var(--bt-ink-2)} .bt-to input{flex:1;min-width:120px;border:0;outline:none;background:none;color:var(--bt-ink);font-size:14px;height:26px} -@@ -2098,7 +2098,9 @@ +@@ -2098,7 +2098,9 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-option[aria-selected=true]{background:var(--bt-active)} .bt-drop-list>.bt-option:not([aria-selected=true]):hover{background:var(--bt-hover)} .bt-option-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} @@ -3280,7 +3564,7 @@ diff --git a/client.js b/client.js .bt-option-icon{width:22px;height:22px;border-radius:50%;background:var(--bt-hover);color:var(--bt-ink-2);display:inline-flex;align-items:center;justify-content:center;flex:none} .bt-hint{padding:14px;text-align:center;font-size:13px;color:var(--bt-ink-2)} .bt-nc-spacer{flex:1} -@@ -2127,6 +2127,10 @@ +@@ -2127,6 +2127,10 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-send{height:40px;min-width:40px;border-radius:999px;border:0;background:var(--bt-accent);color:var(--bt-accent-ink,#fff);font-size:14px;padding:0 14px;cursor:pointer} .bt-send:disabled{opacity:.35;cursor:default} .bt-error{padding:0 14px 10px;color:#e02135;font-size:12px} @@ -3291,7 +3575,7 @@ diff --git a/client.js b/client.js .bt-pane{position:fixed;top:var(--dsh-frame-chrome-top,0px);right:0;bottom:0;z-index:45;pointer-events:auto;background:var(--bt-main);color:var(--bt-ink);display:flex;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif} .bt-dialog{flex:1;display:flex;flex-direction:column;min-width:0} -@@ -2150,11 +2150,11 @@ +@@ -2150,11 +2150,11 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-soft:hover{background:var(--bt-active)} .bt-time{display:block;text-align:center;font-size:12px;line-height:16px;color:var(--bt-ink-2);padding:6px 0} @@ -3305,7 +3589,7 @@ diff --git a/client.js b/client.js .bt-panel-grip{position:absolute;left:-5px;top:0;bottom:0;width:8px;cursor:col-resize;z-index:3;touch-action:none} .bt-panel-grip:hover,.bt-panel[data-dragging] .bt-panel-grip{background:linear-gradient(to right,transparent 4px,var(--bt-line-2) 4px,var(--bt-line-2) 6px,transparent 6px)} .bt-drag-shield{position:fixed;inset:0;z-index:45;cursor:col-resize} -@@ -2163,6 +2163,8 @@ +@@ -2163,6 +2163,8 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-drawer-id{display:flex;flex-direction:column;align-items:center;padding:14px 16px 0} .bt-drawer-id>.bt-mark,.bt-drawer-id>.bt-cluster{margin-bottom:16px} .bt-drawer-name{font-size:17px;line-height:24px;font-weight:500;border:0;background:none;color:inherit;cursor:text;padding:0 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @@ -3314,7 +3598,7 @@ diff --git a/client.js b/client.js .bt-drawer-name:hover{background:var(--bt-hover)} .bt-drawer-name-input{font-size:17px;line-height:24px;text-align:center;border:.5px solid var(--bt-line-2);border-radius:6px;padding:0 6px;background:var(--bt-main);color:var(--bt-ink);outline:none;font-weight:500} .bt-drawer-role{font-size:12px;line-height:16px;color:var(--bt-ink-3);margin-top:6px;text-align:center} -@@ -2197,7 +2197,7 @@ +@@ -2197,7 +2197,7 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-avatar-anchor{position:relative} .bt-avatar-trigger{position:relative;width:64px;height:64px;border:0;border-radius:50%;background:none;cursor:pointer;padding:0} .bt-avatar-pencil{position:absolute;right:-2px;bottom:-2px;width:22px;height:22px;border-radius:50%;background:var(--bt-main);border:1px solid var(--bt-line-2);color:var(--bt-ink-2);display:flex;align-items:center;justify-content:center;box-shadow:0 1px 4px rgba(0,0,0,.12)} @@ -3323,7 +3607,7 @@ diff --git a/client.js b/client.js .bt-bs .bt-avedit{transform-origin:32px 0} .bt-avedit-top{display:flex;align-items:center;justify-content:space-between} .bt-avedit-tabs{position:relative;display:flex;gap:2px;background:var(--bt-hover);border-radius:999px;padding:2px} -@@ -2237,9 +2237,23 @@ +@@ -2237,9 +2237,23 @@ button.bt-q-opt:active{background:var(--bt-active)} .bt-seg button.bt-perm-danger,.bt-seg button.bt-perm-danger[aria-pressed=true]{color:#e02135} /* Settings dialog: a 198px page list beside the page. */ @@ -3348,7 +3632,7 @@ diff --git a/client.js b/client.js .bt-settings{--bt-set-w:min(800px,calc(100vw - 48px));--bt-set-h:min(620px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));position:relative;width:var(--bt-set-w);height:var(--bt-set-h);background:var(--bt-main);border-radius:16px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;overflow:hidden;color:var(--bt-ink);transition:${spring(SPRING_SHAPE, "width", "height")};animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-settings[data-wide]{--bt-set-w:min(1100px,calc(100vw - 48px));--bt-set-h:min(780px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))))} :is(.bt-settings-layer,.bt-mem-layer)[data-leaving]{pointer-events:none} -@@ -2255,6 +2255,7 @@ +@@ -2255,6 +2255,7 @@ body[data-ds-dark-theme] .bt-scrim{background:rgba(0,0,0,.5)} .bt-settings-nav>.bt-thumb{left:8px;right:8px;border-radius:8px;background:var(--bt-active)} .bt-settings-nav[data-glide] .bt-settings-item[aria-current=page]{background:transparent} .bt-settings-shell{margin-top:auto} @@ -3356,7 +3640,7 @@ diff --git a/client.js b/client.js /* Usage while another page is open: out of the flow at Usage's own width, so it keeps its layout, and not rendered. */ .bt-settings-usage[data-off]{position:absolute;top:0;left:198px;width:calc(min(1100px,calc(100vw - 48px)) - 198px);height:100%;box-sizing:border-box;content-visibility:hidden;pointer-events:none} -@@ -2288,13 +2288,63 @@ +@@ -2288,13 +2288,63 @@ button.bt-set-row:hover{background:var(--bt-hover)} .bt-set-card>.bt-set-row+.bt-set-row{border-top:.5px solid var(--bt-line)} .bt-set-row:first-child{border-top-left-radius:12px;border-top-right-radius:12px} .bt-set-row:last-child{border-bottom-left-radius:12px;border-bottom-right-radius:12px} @@ -3420,7 +3704,7 @@ diff --git a/client.js b/client.js /* Settings → Connectors. */ .bt-connector{display:flex;flex-direction:column;gap:10px;padding:12px} -@@ -2308,8 +2308,17 @@ +@@ -2308,8 +2308,17 @@ button.bt-set-row:hover{background:var(--bt-hover)} .bt-connector-actions{flex:none;display:flex;gap:6px} .bt-connector-form{display:flex;flex-direction:column;gap:8px;padding-left:48px;animation:bt-q-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-connector-form-row{display:flex;align-items:center;gap:6px} @@ -3438,7 +3722,7 @@ diff --git a/client.js b/client.js /* Shell chrome reshaped for the team view. Selectors use stable data attributes where the shell offers them and CSS-module name suffixes elsewhere. */ -@@ -2361,7 +2361,7 @@ +@@ -2361,7 +2361,7 @@ button.bt-set-row:hover{background:var(--bt-hover)} .bt-tool:hover,.bt-tool[aria-expanded=true]{background:var(--bt-hover);color:var(--bt-ink)} .bt-tool:active{background:var(--bt-active)} .bt-tool:focus-visible{outline:2px solid var(--bt-accent);outline-offset:-2px} @@ -3447,7 +3731,7 @@ diff --git a/client.js b/client.js .bt-react-strip button{width:32px;height:32px;border:0;border-radius:50%;background:none;font-size:18px;line-height:1;cursor:pointer;transition:background-color .12s ease,${spring(SPRING_POP, "transform")};animation:bt-check-pop ${SPRING_POP.duration}ms ${SPRING_POP.easing} backwards} .bt-react-strip button:nth-child(2){animation-delay:20ms} .bt-react-strip button:nth-child(3){animation-delay:40ms} -@@ -2380,7 +2380,7 @@ +@@ -2380,7 +2380,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side .bt-you-wrap{display:inline-flex;padding:0} .bt-connect-foot{width:100%;height:36px} @@ -3456,7 +3740,7 @@ diff --git a/client.js b/client.js .bt-menu button{display:flex;align-items:center;gap:4px;width:100%;min-height:30px;text-align:left;border:0;background:none;border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;line-height:18px;color:var(--bt-ink);cursor:pointer;box-sizing:border-box} .bt-menu button:hover{background:var(--bt-hover)} .bt-menu button svg{flex:none;width:16px;height:16px;margin:0 1px} -@@ -2390,7 +2390,7 @@ +@@ -2390,7 +2390,7 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side .bt-menu[data-leaving]{pointer-events:none;animation:bt-pop-out .12s cubic-bezier(.4,0,1,1) forwards} .bt-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-menu-hint{flex:none;color:var(--bt-ink-3);font-size:12px;font-variant-numeric:tabular-nums} @@ -3465,7 +3749,7 @@ diff --git a/client.js b/client.js .bt-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.46);animation:bt-fade .22s ease-out} .bt-palette{position:absolute;top:50%;left:50%;width:min(560px,92vw);transform:translate(-50%,-50%);display:flex;flex-direction:column;border-radius:12px;background:var(--bt-main);border:1px solid var(--bt-line-2);box-shadow:0 24px 48px -12px rgba(0,0,0,.25);overflow:hidden;color:var(--bt-ink);animation:bt-palette-in ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-palette-layer[data-leaving]{pointer-events:none} -@@ -2427,7 +2427,8 @@ +@@ -2427,7 +2427,8 @@ button:hover .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-side .bt-palette-empty-label{font-size:14px;font-weight:500;color:var(--bt-ink-2)} .bt-palette-empty-hint{font-size:13px;line-height:16px;color:var(--bt-ink-3)} @media (prefers-reduced-motion:reduce){.bt-palette{animation:bt-fade .1s ease-out}} @@ -3475,7 +3759,7 @@ diff --git a/client.js b/client.js .bt-account{flex:none;width:36px;height:36px;border:0;padding:0;background:none;border-radius:50%;cursor:pointer;display:inline-flex} .bt-account:hover .bt-you,.bt-account[aria-expanded=true] .bt-you{background:color-mix(in srgb,var(--bt-ink) 9%,var(--bt-sidebar))} /* Popover keyframes move translate and scale, which compose with a transform the popover -@@ -2494,7 +2494,7 @@ +@@ -2494,7 +2494,7 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} .bt-mem-row>svg{flex:none;color:var(--bt-ink-2)} .bt-mem-row-copy{flex:1;text-align:left} .bt-mem-row-go{display:inline-flex;align-items:center;gap:2px;color:var(--bt-ink-3);font-size:12px} @@ -3484,7 +3768,7 @@ diff --git a/client.js b/client.js .bt-mem-scrim{animation:bt-fade .2s ease-out} .bt-mem{position:relative;width:min(680px,calc(100vw - 48px));height:min(760px,calc(100vh - 2*max(32px,var(--dsh-frame-overlay-top,32px))));background:var(--bt-main);border-radius:20px;border:.5px solid var(--bt-line-2);box-shadow:0 24px 80px rgba(0,0,0,.24);display:flex;flex-direction:column;overflow:hidden;color:var(--bt-ink);animation:bt-dialog-rise ${SPRING_SHAPE.duration}ms ${SPRING_SHAPE.easing},bt-veil-in .2s ease-out} .bt-mem-head{position:relative;display:flex;align-items:center;gap:8px;height:60px;flex:none;padding:0 14px 0 24px;border-bottom:.5px solid var(--bt-line);box-sizing:border-box} -@@ -2597,9 +2597,20 @@ +@@ -2597,9 +2597,20 @@ body[data-ds-dark-theme] .bt-theme-chip i{background:var(--da)} `; var CONVERSATION_CSS = ` .bt-think{font-size:13px;line-height:18px;color:var(--bt-ink-2);margin:2px 0 4px;max-width:min(560px,78%)} @@ -3507,7 +3791,7 @@ diff --git a/client.js b/client.js .bt-event{display:flex;justify-content:center;align-items:center;gap:2px;min-height:24px;margin:2px 0;font-size:12px;line-height:16px;color:var(--bt-ink-2);flex-wrap:wrap} .bt-event>span{padding:0 2px} .bt-event button{border:0;background:none;color:inherit;font:inherit;display:inline-flex;align-items:center;gap:4px;cursor:pointer;height:24px;padding:4px 6px 4px 4px;border-radius:999px;box-sizing:border-box;transition:background-color .12s ease} -@@ -2612,7 +2612,6 @@ +@@ -2612,7 +2612,6 @@ var CONVERSATION_CSS = ` .bt-event-whole>.bt-event-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .bt-time-sep{display:flex;justify-content:center;align-items:center;height:28px;margin:14px 0 8px;font-size:12px;line-height:16px;color:var(--bt-ink-2);white-space:nowrap} [class*="_scroll"]>[class*="_column"]::before{content:var(--bt-first-sep,none);display:block;flex:none;height:28px;margin:17px 0 12px;font-size:12px;line-height:28px;text-align:center;color:var(--bt-ink-2);white-space:nowrap} @@ -3515,7 +3799,7 @@ diff --git a/client.js b/client.js .bt-group-replies{display:contents} /* A Bot chat is flat in every work-details mode, without changing the user's mode (it is a DSH-wide setting): step groups lose their header and stay open, and what a -@@ -2622,21 +2622,48 @@ +@@ -2622,21 +2622,48 @@ var CONVERSATION_CSS = ` [data-step-process]>[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} :is([data-step-process],[data-turn-process-hidden])[hidden]{content-visibility:visible!important} [class*="_flowItem"]:has(> [data-slot="conversation.chat.node"]:empty){display:none!important} @@ -3572,7 +3856,7 @@ diff --git a/client.js b/client.js [data-content-phase=hero] [class*="_composerHero"]{align-self:stretch!important;width:auto!important;padding-bottom:0!important} [data-content-phase=hero] [class*="_composerHero"]>[class$="_root"],[data-content-phase=hero] [class*="_heroWorkspaceRow"]{display:none!important} [data-content-phase=hero] [class*="_composerHero"] [class*="_input"]{min-height:0!important} -@@ -2646,8 +2646,19 @@ +@@ -2646,8 +2646,19 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} [data-chat-running]{display:block!important;width:100%;min-height:0!important;margin:0!important;padding:0!important} [data-chat-running]>span:not([role=status]){display:none!important} [data-chat-running]:has(> .bt-typing){margin-top:var(--dsh-chat-flow-gap,12px)!important} @@ -3592,7 +3876,7 @@ diff --git a/client.js b/client.js .bt-typing>.bt-mark,.bt-typing>.bt-cluster{flex-shrink:0} .bt-typing>.bt-mark{cursor:pointer;transition:filter .16s ease-out} .bt-typing>.bt-mark:hover{filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))} -@@ -2679,7 +2679,8 @@ +@@ -2679,7 +2679,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} @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} @@ -3602,7 +3886,7 @@ diff --git a/client.js b/client.js [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} -@@ -2691,6 +2691,8 @@ +@@ -2691,6 +2691,8 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width: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} @@ -3611,7 +3895,7 @@ diff --git a/client.js b/client.js [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} -@@ -2722,7 +2722,9 @@ +@@ -2722,7 +2722,9 @@ header[class*="_header"] .bt-head{flex:1 1 auto;width:100%} .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} @@ -3621,7 +3905,7 @@ diff --git a/client.js b/client.js .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} -@@ -2776,6 +2776,7 @@ +@@ -2776,6 +2776,7 @@ div:has(> nav [class*="_marks"]){display:none!important} .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} @@ -3629,7 +3913,7 @@ diff --git a/client.js b/client.js .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} -@@ -2784,10 +2784,10 @@ +@@ -2784,10 +2784,10 @@ div:has(> nav [class*="_marks"]){display:none!important} .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)} `; @@ -3642,7 +3926,7 @@ diff --git a/client.js b/client.js document.head.appendChild(style); return style; } -@@ -2807,9 +2807,9 @@ +@@ -2807,9 +2807,9 @@ function sheetClassNames(sheet) { } return names; } @@ -3654,7 +3938,7 @@ diff --git a/client.js b/client.js if (!compiled.has(fragment)) { const exact = []; for (const name of names) if (name.includes(fragment)) exact.push(`.${name}`); -@@ -2818,7 +2818,7 @@ +@@ -2818,7 +2818,7 @@ function compileFragmentSelectors(text, names) { return compiled.get(fragment); }); } @@ -3663,7 +3947,7 @@ diff --git a/client.js b/client.js const style = installStyles("", tag); const cache = /* @__PURE__ */ new WeakMap(); const render = () => { -@@ -2832,7 +2832,7 @@ +@@ -2832,1948 +2832,4376 @@ function installCompiledStyles(text, tag) { } for (const name of list) names.add(name); } @@ -3671,94 +3955,31 @@ diff --git a/client.js b/client.js + const next = compileFragmentSelectors(text2, names); if (style.textContent !== next) style.textContent = next; }; - render(); -@@ -2855,226 +2855,204 @@ - }; - } - --// 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]); --} --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 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 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" }) -- ); +- render(); +- let timer; +- const schedule = () => { +- clearTimeout(timer); +- timer = setTimeout(render, 120); ++ 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(); ++ }; ++} ++ +// src/shared/face-title.js +var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function isGenericFaceTitle(name) { @@ -3775,45 +3996,7 @@ diff --git a/client.js b/client.js +function isAutoFaceTitle(name) { + const n = String(name ?? "").trim(); + return isGenericFaceTitle(n) || n.startsWith("Face-to-face · "); - } --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; -- } -- 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 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 ownersFromMembers(members) { + const rows = []; + const seen = /* @__PURE__ */ new Set(); @@ -3824,43 +4007,10 @@ diff --git a/client.js b/client.js + seen.add(key); + const handle = String(member?.handle ?? member?.id ?? "").trim().toLowerCase(); + rows.push({ owner, sort: handle || key }); - } -- const half = frame / 2; -- return [[0, 0, half], [half, 0, half], [0, half, half], [half, half, half]]; ++ } + rows.sort((a, b) => a.sort.localeCompare(b.sort)); + return rows.slice(0, 3).map((row) => row.owner); - } --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}`); -- } -- const bot = people[index]; -- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); -- }) -- ); ++} +function uniqueOwners(owners) { + const seen = /* @__PURE__ */ new Set(); + const out = []; @@ -3931,14 +4081,19 @@ diff --git a/client.js b/client.js + if (!label) return; + const next = brandTitle(`${label} — FlyBot`); + if (doc.title !== next) doc.title = next; -+ }; + }; +- const onLoad = (event) => { +- if (event.target instanceof HTMLLinkElement) schedule(); + sync(); + const offSessions = sessions.subscribe(sync); + const offRoster = roster.subscribe(sync); + return () => { + offSessions?.(); + offRoster?.(); -+ }; + }; +- const observer = new MutationObserver(schedule); +- observer.observe(document.head, { childList: true }); +- document.addEventListener("load", onLoad, true); +} +function installBrandChrome() { + if (typeof document === "undefined") return () => { @@ -3961,139 +4116,158 @@ diff --git a/client.js b/client.js + const observer = typeof MutationObserver === "function" ? new MutationObserver(retitle) : null; + const node = document.querySelector("title") ?? document.head; + observer?.observe(node, { childList: true, characterData: true, subtree: true }); -+ return () => { + return () => { +- observer.disconnect(); +- document.removeEventListener("load", onLoad, true); +- clearTimeout(timer); +- style.remove(); + observer?.disconnect(); + link.remove(); -+ }; + }; } - // src/client/icons.js --var import_react3 = require("react"); +-// 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); ++// src/client/icons.js +var import_react5 = 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 PlusIcon() { + return (0, import_react5.createElement)("svg", { width: 18, height: 18, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.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 SearchIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("circle", { cx: 8.5, cy: 8.5, r: 5.5, stroke: "currentColor", strokeWidth: 1.6, fill: "none" }), (0, import_react5.createElement)("path", { d: "M13 13l4 4", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); - } - function PeopleIcon() { -- return (0, import_react3.createElement)( ++} ++function PeopleIcon() { + return (0, import_react5.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" }) ++ "svg", ++ { width: 14, height: 14, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": true }, + (0, import_react5.createElement)("circle", { cx: 7.5, cy: 7, r: 3 }), + (0, import_react5.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_react5.createElement)("path", { d: "M13 4.3a3 3 0 0 1 0 5.4M15 12.4c1.6.6 2.6 2 3 4.1" }) - ); - } - 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 ArrowUpIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.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)( ++} ++function MicIcon({ size = 16 }) { + return (0, import_react5.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" }) ++ "svg", ++ { width: size, height: size, viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", "aria-hidden": true }, + (0, import_react5.createElement)("rect", { x: 7, y: 2.5, width: 6, height: 10, rx: 3, fill: "currentColor", stroke: "none" }), + (0, import_react5.createElement)("path", { d: "M4.5 9.5a5.5 5.5 0 0 0 11 0M10 15v2.5" }) - ); - } - function WaveIcon({ size = 16 }) { -- return (0, import_react3.createElement)( ++ ); ++} ++function WaveIcon({ size = 16 }) { + return (0, import_react5.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 })) ++ "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_react5.createElement)("rect", { key: x, x: x - 1, y, width: 2, height, rx: 1 })) - ); - } - function ShareIcon() { -- return (0, import_react3.createElement)( ++ ); ++} ++function ShareIcon() { + return (0, import_react5.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" }) ++ "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_react5.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 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 CloseIcon() { + return (0, import_react5.createElement)("svg", { width: 16, height: 16, viewBox: "0 0 20 20", "aria-hidden": true }, (0, import_react5.createElement)("path", { d: "M5 5l10 10M15 5L5 15", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" })); - } --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 ChevronLeftIcon = () => (0, import_react5.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_react5.createElement)("path", { d: "M12.5 4.5L7 10l5.5 5.5" })); +var ChevronRightIcon = () => (0, import_react5.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_react5.createElement)("path", { d: "M7.5 4.5L13 10l-5.5 5.5" })); - var lineIcon = (paths) => function LineIcon() { -- return (0, import_react3.createElement)( ++var lineIcon = (paths) => function LineIcon() { + return (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 }, -- paths.map((d, index) => (0, import_react3.createElement)("path", { key: index, d })) ++ "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_react5.createElement)("path", { key: index, d })) - ); - }; - 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)( ++ ); ++}; ++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_react5.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" })) ++ "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_react5.createElement)("path", { d: CLOUD }), + done ? (0, import_react5.createElement)("path", { d: "M7.75 13.25l1.75 1.75 3-3.25" }) : (0, import_react5.createElement)("g", { className: "bt-cloud-arrow" }, (0, import_react5.createElement)("path", { d: "M10 9.75v7" }), (0, import_react5.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"]); -@@ -3086,6 +3086,7 @@ - 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 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 WorkflowIcon = lineIcon(["M4.5 4h4v3.5h-4z", "M11.5 12.5h4V16h-4z", "M6.5 7.5v3.5a2 2 0 0 0 2 2h3"]); - 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"]); -@@ -3093,14 +3093,14 @@ - 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)( ++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_react5.createElement)( - "svg", - { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, -- (0, import_react3.createElement)( ++ "svg", ++ { width: 12, height: 12, viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true }, + (0, import_react5.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" }) ++ "g", ++ { transform: "rotate(45 10 10)" }, + (0, import_react5.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_react5.createElement)("path", { d: "M9.35 13.3h1.3v3.6L10 18l-.65-1.1z" }) - ) - ); - } -@@ -3112,21 +3112,48 @@ - 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 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 LinkIcon = lineIcon(["M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1", "M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1"]); - 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 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 ChatIcon = lineIcon(["M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5z"]); +var BotIcon = lineIcon(["M6 7.5h8a2 2 0 0 1 2 2V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V9.5a2 2 0 0 1 2-2z", "M10 4.5v3", "M10 3.5h.01", "M7.75 11.5h.01M12.25 11.5h.01"]); +var TerminalIcon = lineIcon(["M4.5 4.5h11a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14V6a1.5 1.5 0 0 1 1.5-1.5z", "M6.5 8.25L8.75 10 6.5 11.75", "M10.5 12h3"]); - 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 }; ++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 }; +function FlyBotMark({ size = 24 }) { + return (0, import_react5.createElement)( + "svg", @@ -4117,670 +4291,799 @@ diff --git a/client.js b/client.js + (0, import_react5.createElement)("rect", { className: "bt-flybot-body", x: 248, y: 104, width: 16, height: 34 }) + ); +} - - // src/client/questions.js --var import_react4 = require("react"); ++ ++// src/client/questions.js +var import_react6 = 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); ++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_react6.useRef)(onPick); - pick2.current = onPick; -- (0, import_react4.useEffect)(() => { ++ pick2.current = onPick; + (0, import_react6.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; -@@ -3146,10 +3146,10 @@ - }, [count, ownRef]); - } - 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)(); ++ 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(); ++ } + }; +- }, [enabled]); ++ document.addEventListener("keydown", onKey, true); ++ return () => document.removeEventListener("keydown", onKey, true); ++ }, [count, ownRef]); ++} ++function QuestionView({ question, onAnswer, onDismiss, ownAnswer = false, raised = false }) { + const [own, setOwn] = (0, import_react6.useState)(""); + const [picked, setPicked] = (0, import_react6.useState)([]); + const ownRef = (0, import_react6.useRef)(null); + const titleId = (0, import_react6.useId)(); - const options = optionsOf(question); - const multi = question.multiSelect === true; - const custom = own.trim(); -@@ -3167,7 +3167,7 @@ - }; - 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", { ++ 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_react6.createElement)("textarea", { - ref: ownRef, - rows: 1, - value: own, -@@ -3186,7 +3186,7 @@ - }) : null; - const rows = options.map((option, optionIndex) => { - const selected = multi && picked.includes(option.label); -- return (0, import_react4.createElement)( ++ 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_react6.createElement)( - "button", - { - key: option.label, -@@ -3196,36 +3196,36 @@ - "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)( ++ "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_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)), + (0, import_react6.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 ++ "span", ++ { className: "bt-q-body" }, + (0, import_react6.createElement)("span", { className: "bt-q-label" }, option.label), + option.description ? (0, import_react6.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 ++ ), + selected && !raised ? (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) : null - ); - }); - if (raised && field) { -- rows.push((0, import_react4.createElement)( ++ ); ++ }); ++ if (raised && field) { + rows.push((0, import_react6.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) ++ "label", ++ { key: "own", className: "bt-q-opt bt-q-ownrow", "data-filled": custom !== "" ? "" : void 0 }, + ownLetter ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, ownLetter) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, field) - )); - } - let foot = null; - if (raised && multi) { -- foot = (0, import_react4.createElement)( ++ )); ++ } ++ let foot = null; ++ if (raised && multi) { + foot = (0, import_react6.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")) ++ "div", ++ { className: "bt-q-foot" }, + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-btn", onClick: onDismiss }, t("Skip")), + (0, import_react6.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"))); ++ ); ++ } else if ((raised || multi) && canSubmit) { + foot = (0, import_react6.createElement)("div", { className: "bt-q-foot" }, (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit"))); - } -- return (0, import_react4.createElement)( ++ } + return (0, import_react6.createElement)( - "form", - { - className: raised ? "bt-q bt-q-raised" : "bt-q", -@@ -3235,24 +3235,24 @@ - submit(); - } - }, -- (0, import_react4.createElement)( ++ "form", ++ { ++ className: raised ? "bt-q bt-q-raised" : "bt-q", ++ "aria-labelledby": titleId, ++ onSubmit: (event) => { ++ event.preventDefault(); ++ submit(); ++ } ++ }, + (0, import_react6.createElement)( - "div", - { className: "bt-q-head" }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.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 ++ "div", ++ { className: "bt-q-text" }, + (0, import_react6.createElement)("p", { className: "bt-q-title", id: titleId }, question.header ? `${question.header} · ` : "", question.question), + question.detail ? (0, import_react6.createElement)("div", { className: "bt-q-detail" }, question.detail) : null, + raised && multi ? (0, import_react6.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)) ++ ), + (0, import_react6.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Dismiss question"), title: t("Dismiss"), onClick: onDismiss }, (0, import_react6.createElement)(CloseIcon)) - ), -- rows.length > 0 ? (0, import_react4.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, -- field && !raised ? (0, import_react4.createElement)( ++ ), + rows.length > 0 ? (0, import_react6.createElement)("div", { className: "bt-q-list", role: "group" }, rows) : null, + field && !raised ? (0, import_react6.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 ++ "div", ++ { className: "bt-q-custom" }, + (0, import_react6.createElement)("label", { className: "bt-q-field" }, field), + !multi && canSubmit ? (0, import_react6.createElement)("button", { type: "submit", className: "bt-q-submit" }, t("Submit")) : null - ) : null, - foot - ); -@@ -3262,29 +3262,29 @@ - const prompt = question?.question ?? ""; - const chosen = [...answer.selected ?? [], ...answer.custom ? [answer.custom] : []]; - if (chosen.length === 0) { -- return (0, import_react4.createElement)( ++ ) : null, ++ foot ++ ); + } +-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 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_react6.createElement)( - "div", - { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, -- (0, import_react4.createElement)( ++ "div", ++ { className: "bt-q bt-q-settled bt-q-dismissed", role: "group", "aria-label": prompt }, + (0, import_react6.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")) ++ "div", ++ { className: "bt-q-head" }, + (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt)), + (0, import_react6.createElement)("span", { className: "bt-q-badge" }, t("Dismissed")) - ) - ); - } -- return (0, import_react4.createElement)( ++ ) ++ ); ++ } + return (0, import_react6.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) => { ++ "div", ++ { className: "bt-q bt-q-settled", role: "group", "aria-label": prompt }, + prompt ? (0, import_react6.createElement)("div", { className: "bt-q-head" }, (0, import_react6.createElement)("div", { className: "bt-q-text" }, (0, import_react6.createElement)("p", { className: "bt-q-title" }, prompt))) : null, + (0, import_react6.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)( ++ const optionIndex = options.findIndex((option) => option.label === label); + return (0, import_react6.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", ++ { key: `${label}-${index}`, className: "bt-q-opt" }, + optionIndex >= 0 ? (0, import_react6.createElement)("span", { className: "bt-key", "aria-hidden": true }, keyLetter(optionIndex)) : null, + (0, import_react6.createElement)("span", { className: "bt-q-body" }, (0, import_react6.createElement)("span", { className: "bt-q-label" }, label)), + (0, import_react6.createElement)("span", { className: "bt-q-check", title: t("Selected") }, (0, import_react6.createElement)(CheckIcon)) - ); - })) - ); -@@ -3292,8 +3292,8 @@ - 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)([]); ++ ); ++ })) ++ ); ++} ++function QuestionCard({ matched }) { ++ const pending = matched; ++ const questions = pending.questions ?? []; + const [index, setIndex] = (0, import_react6.useState)(0); + const [answers, setAnswers] = (0, import_react6.useState)([]); - const question = questions[index]; - if (question === void 0) return null; - const onAnswer = (answer) => { -@@ -3305,10 +3305,10 @@ - } - pending.answer({ answers: next }); ++ 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 }); +- 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_react6.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); +-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, inline }) { + const [answered, setAnswered] = (0, import_react6.useState)(null); - if (answered === question.id) return null; - const onAnswer = (answer) => { - setAnswered(question.id); -@@ -3318,10 +3318,10 @@ - setAnswered(question.id); - void actions.dismissQuestion(sessionId); - }; -- return (0, import_react4.createElement)( ++ 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_react6.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 }) ++ "div", ++ { className: inline ? "bt-q-inline" : "bt-q-dock" }, + (0, import_react6.createElement)(QuestionView, { key: question.id, question, onAnswer, onDismiss, ownAnswer: question.allowCustom === true, raised: !inline }) ); } - function QuestionDock({ sessionId, useRoster, useCards, useSession, actions }) { -@@ -3329,16 +3329,16 @@ - 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 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 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_react6.createElement)(PendingQuestion, { sessionId, question, actions, inline: false }); } - 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; +-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 : []; ++ if (questions.length === 0) { + return data.text ? (0, import_react6.createElement)("div", { className: "bt-urow" }, (0, import_react6.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, { ++ } ++ const answers = Array.isArray(data.answers) ? data.answers : []; + return (0, import_react6.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react6.createElement)(SettledQuestion, { - key: question.id, - question, - answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } -@@ -3346,37 +3346,37 @@ ++ key: question.id, ++ question, ++ answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] } ++ }))); } - - // src/client/secret-card.js --var import_react5 = require("react"); +-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_react7 = require("react"); - var MIN_VALUE = 4; - var NONE = {}; --var KeyIcon = () => (0, import_react5.createElement)( ++var MIN_VALUE = 4; ++var NONE = {}; +var KeyIcon = () => (0, import_react7.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" }) ++ "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_react7.createElement)("path", { d: "M7 9.5a3.5 3.5 0 1 1 0 .01z" }), + (0, import_react7.createElement)("path", { d: "M10.4 10.6L17 10.6M14.5 10.6v2.4M16.6 10.6v1.8" }) - ); --var LockIcon = () => (0, import_react5.createElement)( ++); +var LockIcon = () => (0, import_react7.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 }) ++ "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_react7.createElement)("path", { d: "M6.5 9V7a3.5 3.5 0 0 1 7 0v2" }), + (0, import_react7.createElement)("rect", { x: 4.5, y: 9, width: 11, height: 8, rx: 2 }) - ); --var WarnIcon = () => (0, import_react5.createElement)( ++); +var WarnIcon = () => (0, import_react7.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" }) ++ "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_react7.createElement)("path", { d: "M10 3.5l7 12.5H3z" }), + (0, import_react7.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"); ++); ++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 my 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", { ++ 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_react7.createElement)("input", { - className: "bt-secret-input", - type: MASK_BY_CSS ? "text" : "password", - "data-masked": MASK_BY_CSS ? "" : void 0, -@@ -3401,42 +3401,42 @@ - }); ++ 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?.(); + } +- if (was === "idle" || was === "alert") return void 0; +- setSettled(true); +- const timer = setTimeout(() => setSettled(false), 1800); +- return () => clearTimeout(timer); +- }, [state]); +- return settled; ++ }); } - function ScopeSwitch({ value, onChange, bot }) { -- const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]]; -- return (0, import_react5.createElement)( +-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_react7.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", { ++ "div", ++ { className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") }, + (0, import_react7.createElement)("span", { className: "bt-secret-glide", "data-at": value === "all" ? 0 : 1, "aria-hidden": true }), + options.map(([id, text2]) => (0, import_react7.createElement)("button", { - key: id, - type: "button", - role: "radio", - "aria-checked": value === id, - className: "bt-secret-seg", - onClick: () => onChange(id) -- }, text)) ++ key: id, ++ type: "button", ++ role: "radio", ++ "aria-checked": value === id, ++ className: "bt-secret-seg", ++ onClick: () => onChange(id) + }, text2)) ); } - function SecretHead({ bot, title, detail, name, onClose }) { -- return (0, import_react5.createElement)( +-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 SecretHead({ bot, title, detail, name, onClose }) { + return (0, import_react7.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)( ++ "div", ++ { className: "bt-q-head bt-secret-head" }, + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 28 }), (0, import_react7.createElement)("span", { className: "bt-secret-lock" }, (0, import_react7.createElement)(LockIcon))), + (0, import_react7.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}` : "") ++ "div", ++ { className: "bt-q-text" }, + (0, import_react7.createElement)("p", { className: "bt-q-title" }, title), + (0, import_react7.createElement)("div", { className: "bt-q-detail" }, (0, import_react7.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 ++ ), + onClose ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-x", "aria-label": t("Cancel the request"), title: t("Cancel"), onClick: onClose }, (0, import_react7.createElement)(CloseIcon)) : 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)(); +-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}`); +- } +- const bot = people[index]; +- return (0, import_react2.createElement)("span", { key: bot?.id ?? "you", style }, seat(bot, s)); +- }) ++function SecretCard({ bot, request, roster, actions }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [scope, setScope] = (0, import_react7.useState)("all"); + const [replacing, setReplacing] = (0, import_react7.useState)(false); + const [busy, setBusy] = (0, import_react7.useState)(false); + const [error, setError] = (0, import_react7.useState)(""); + const noteId = (0, import_react7.useId)(); - const secret = (roster.secrets ?? []).find((entry) => entry.name === request.name); - const run = (promise) => { - setBusy(true); -@@ -3452,73 +3452,73 @@ - if (!busy) void run(actions.secretAllow(request.id)); - }; - 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)( ++ 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)); ++ }; ++ const filling = request.kind === "fill" || replacing; + const warn = insecure() ? (0, import_react7.createElement)("div", { className: "bt-secret-warn", role: "note" }, (0, import_react7.createElement)(WarnIcon), t("This page is not on HTTPS: the key crosses the network unencrypted.")) : null; + return (0, import_react7.createElement)( - "div", - { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, -- (0, import_react5.createElement)(SecretHead, { ++ "div", ++ { className: "bt-q bt-secret", role: "group", "aria-describedby": noteId, "data-kind": request.kind }, + (0, import_react7.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)( ++ 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_react7.createElement)("div", { className: "bt-secret-saved" }, (0, import_react7.createElement)(KeyIcon), t("Saved for {scope}", { scope: scopeLabel(secret?.scope, roster) })) : null, + filling ? (0, import_react7.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 ++ "div", ++ { className: "bt-secret-body" }, + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value of {name}", { name: request.name }), autoFocus: replacing })), + request.kind === "fill" ? (0, import_react7.createElement)(ScopeSwitch, { value: scope, onChange: setScope, bot }) : null - ) : null, -- (0, import_react5.createElement)( ++ ) : null, + (0, import_react7.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}`), ++ "div", ++ { className: "bt-secret-note", id: noteId }, ++ t("{bot} never sees the value. It reaches {bot}'s shell as ", { bot: bot.name }), + (0, import_react7.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)( ++ ".", ++ filling && value !== "" && !long ? t(" At least {count} characters.", { count: MIN_VALUE }) : "" ++ ), ++ warn, + error ? (0, import_react7.createElement)("div", { className: "bt-secret-error", role: "alert" }, error) : null, + (0, import_react7.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-q-foot" }, + request.kind === "allow" && !replacing ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: () => setReplacing(true) }, t("Replace value")) : null, + (0, import_react7.createElement)("button", { type: "button", className: "bt-q-btn", disabled: busy, onClick: cancel }, t("Cancel")), + filling ? (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: !long || busy, onClick: save }, busy ? t("Saving…") : t("Save")) : (0, import_react7.createElement)("button", { type: "button", className: "bt-q-submit", disabled: busy, onClick: allow }, busy ? t("Allowing…") : t("Allow")) - ) - ); - } - 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 SettledSecret({ bot, request, roster }) { ++ const canceled = request.status === "canceled"; + const text2 = 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_react7.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 bt-q-settled bt-secret bt-secret-done", role: "group", "aria-label": text2, "data-status": request.status }, + (0, import_react7.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)( ++ "div", ++ { className: "bt-q-head" }, + (0, import_react7.createElement)("span", { className: "bt-secret-who" }, (0, import_react7.createElement)(BotMark, { bot, size: 22 })), + (0, import_react7.createElement)("div", { className: "bt-q-text" }, (0, import_react7.createElement)( - "p", - { className: "bt-q-title" }, -- text, -- canceled ? null : (0, import_react5.createElement)("span", { className: "bt-secret-scope-tag" }, ` · ${scopeLabel(request.scope, roster)}`) ++ "p", ++ { className: "bt-q-title" }, + text2, + canceled ? null : (0, import_react7.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)) ++ )), + canceled ? (0, import_react7.createElement)("span", { className: "bt-q-badge" }, t("Canceled")) : (0, import_react7.createElement)("span", { className: "bt-q-check bt-secret-check", title: t("Done") }, (0, import_react7.createElement)(CheckIcon)) - ) ++ ) ); } - 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 }); +- +-// 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 RequestView({ bot, request, roster, actions }) { + return request.status === "pending" ? (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }) : (0, import_react7.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 })); +-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 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_react7.createElement)("div", { className: "bt-q-inline" }, (0, import_react7.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") }))); ++ if (done && /already saved/.test(contentText(content))) { + return (0, import_react7.createElement)("div", { className: "bt-event" }, (0, import_react7.createElement)("span", { className: "bt-secret-event" }, (0, import_react7.createElement)(KeyIcon), t("{bot} is using {name}", { bot: bot.name, name: name ?? t("a saved key") }))); - } - return null; ++ } ++ return null; } -@@ -3531,44 +3531,46 @@ - 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 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 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_react7.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react7.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions }))); } +-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" })); +var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some"; - 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); ++function ScopeEditor({ secret, roster, onSave }) { + const [mode, setMode] = (0, import_react7.useState)(scopeMode(secret.scope)); + const [picked, setPicked] = (0, import_react7.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)( ++ const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]); + const next = mode === "all" ? "all" : mode === "none" ? [] : picked; + const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join(); + return (0, import_react7.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-edit-block" }, + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")), + (0, import_react7.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)( ++ "div", ++ { className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) }, + [["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text2]) => (0, import_react7.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 ++ "label", ++ { key: id, className: "bt-secret-radio" }, + (0, import_react7.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), + text2 - )) - ), -- mode === "some" ? (0, import_react5.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react5.createElement)("button", { ++ )) ++ ), + mode === "some" ? (0, import_react7.createElement)("div", { className: "bt-secret-bots" }, roster.bots.map((bot) => (0, import_react7.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)( ++ key: bot.id, ++ type: "button", ++ className: "bt-secret-bot", ++ "aria-pressed": picked.includes(bot.id), ++ onClick: () => toggle(bot.id) + }, (0, import_react7.createElement)(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? (0, import_react7.createElement)(CheckIcon) : null))) : null, + (0, import_react7.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")) ++ "div", ++ { className: "bt-secret-row-actions" }, + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access")) - ) - ); ++ ) ++ ); } - function ValueEditor({ secret, onSave }) { -- const [value, setValue] = (0, import_react5.useState)(""); -- const [replaced, setReplaced] = (0, import_react5.useState)(false); +-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 ValueEditor({ secret, onSave }) { + const [value, setValue] = (0, import_react7.useState)(""); + const [replaced, setReplaced] = (0, import_react7.useState)(false); - const long = value.trim().length >= MIN_VALUE; - const save = () => { - if (!long) return; -@@ -3583,66 +3583,66 @@ - setValue(next); - setReplaced(false); - }; -- return (0, import_react5.createElement)( ++ 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_react7.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-edit-block" }, + (0, import_react7.createElement)("div", { className: "bt-secret-edit-label" }, t("Replace the value")), + (0, import_react7.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")) ++ "div", ++ { className: "bt-secret-inline" }, + (0, import_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: edit, onEnter: save, label: t("New value of {name}", { name: secret.name }) })), + (0, import_react7.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 ++ ), + value !== "" && !long ? (0, import_react7.createElement)("div", { className: "bt-set-hint" }, t("At least {count} characters.", { count: MIN_VALUE })) : replaced ? (0, import_react7.createElement)("div", { className: "bt-set-hint bt-secret-ok", role: "status" }, (0, import_react7.createElement)(CheckIcon), t("Replaced. Bots get the new value from their next command.")) : null ); } - function SecretRow({ secret, roster, actions, run, open, onToggle }) { -- const [confirm, setConfirm] = (0, import_react5.useState)(false); -- (0, import_react5.useEffect)(() => { +-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 SecretRow({ secret, roster, actions, run, open, onToggle }) { + const [confirm, setConfirm] = (0, import_react7.useState)(false); + (0, import_react7.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)( ++ 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_react7.createElement)( - "div", - { className: "bt-secret-item", "data-open": open ? "" : void 0 }, -- (0, import_react5.createElement)( ++ "div", ++ { className: "bt-secret-item", "data-open": open ? "" : void 0 }, + (0, import_react7.createElement)( - "button", - { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, -- (0, import_react5.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-secret-summary", "aria-expanded": open, onClick: onToggle }, + (0, import_react7.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) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react7.createElement)("span", { className: "bt-secret-title" }, (0, import_react7.createElement)(KeyIcon), (0, import_react7.createElement)("code", { className: "bt-secret-name" }, secret.name), secret.purpose ? (0, import_react7.createElement)("span", { className: "bt-secret-purpose" }, secret.purpose) : null), + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, meta) - ), -- (0, import_react5.createElement)( ++ ), + (0, import_react7.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 }))) ++ "span", ++ { className: "bt-set-control" }, + secret.scope === "all" ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react7.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react7.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react7.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))) - ) - ), -- open ? (0, import_react5.createElement)( ++ ) ++ ), + open ? (0, import_react7.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-edit" }, + (0, import_react7.createElement)(ScopeEditor, { key: JSON.stringify(secret.scope), secret, roster, onSave: (scope) => run(actions.secretUpdate(secret.name, scope)) }), + (0, import_react7.createElement)(ValueEditor, { secret, onSave: (value) => run(actions.secretSet({ name: secret.name, value })) }), + (0, import_react7.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")) ++ "div", ++ { className: "bt-secret-row-actions" }, ++ confirm ? [ + (0, import_react7.createElement)("span", { key: "q", className: "bt-set-hint" }, t("Delete {name}? Bots lose it at once.", { name: secret.name })), + (0, import_react7.createElement)("button", { key: "no", type: "button", className: "bt-soft bt-soft-sm", onClick: () => setConfirm(false) }, t("Keep")), + (0, import_react7.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_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm bt-secret-danger", onClick: () => setConfirm(true) }, (0, import_react7.createElement)(TrashIcon), t("Delete")) - ) - ) : null ++ ) ++ ) : null ); } - 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)(""); +-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 AddSecret({ actions, run, onDone }) { + const [name, setName] = (0, import_react7.useState)(""); + const [purpose, setPurpose] = (0, import_react7.useState)(""); + const [value, setValue] = (0, import_react7.useState)(""); - const ready = /^[A-Za-z][\w-]{0,63}$/.test(name.trim()) && value.trim().length >= MIN_VALUE; - const save = () => { - if (!ready) return; -@@ -3653,43 +3653,43 @@ - } - }); - }; -- return (0, import_react5.createElement)( ++ 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_react7.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-set-row bt-set-paste bt-secret-add" }, + (0, import_react7.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_react7.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_react7.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react7.createElement)(MaskedInput, { value, onChange: setValue, onEnter: save, label: t("Value") })), + (0, import_react7.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-secret-row-actions" }, + (0, import_react7.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")), + (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key")) - ) ++ ) ); } - function SecretsPage({ roster, actions, run, Section }) { -- const [open, setOpen] = (0, import_react5.useState)(null); -- const [adding, setAdding] = (0, import_react5.useState)(false); +-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 SecretsPage({ roster, actions, run, Section }) { + const [open, setOpen] = (0, import_react7.useState)(null); + const [adding, setAdding] = (0, import_react7.useState)(false); - const secrets = roster.secrets ?? []; - return [ -- (0, import_react5.createElement)( ++ const secrets = roster.secrets ?? []; ++ return [ + (0, import_react7.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"), ++ "p", ++ { key: "about", className: "bt-secret-about" }, ++ t("Keys your Bots asked for. Bots never see the values: they use them as "), + (0, import_react7.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(" in their shell, and every value is replaced by "), + (0, import_react7.createElement)("code", null, "[secret:NAME]"), - t(" before anything goes to a model.") - ), -- (0, import_react5.createElement)( ++ t(" before anything goes to a model.") ++ ), + (0, import_react7.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 ++ Section, ++ { key: "list", title: t("Secrets · {count}", { count: secrets.length }) }, + secrets.length === 0 && !adding ? (0, import_react7.createElement)("div", { className: "bt-set-row" }, (0, import_react7.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_react7.createElement)(SecretRow, { key: secret.name, secret, roster, actions, run, open: open === secret.name, onToggle: () => setOpen(open === secret.name ? null : secret.name) })), + adding ? (0, import_react7.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"))) ++ ), + adding ? null : (0, import_react7.createElement)("div", { key: "add", className: "bt-actions" }, (0, import_react7.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => setAdding(true) }, t("Add a key"))) - ]; ++ ]; } - var SECRET_CSS = ` -@@ -3756,6 +3756,144 @@ - @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 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 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/face-seats.js +function faceSeats(room2, roster, { left = false } = {}) { + const records = (room2.presentedParticipants ?? room2.participants ?? []).filter((p) => p.kind !== "human"); @@ -4858,7 +5161,14 @@ 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" })) +function useInlineEntry(policy) { + const slots = registry(); + const [, setVersion] = (0, import_react8.useState)(0); @@ -4911,70 +5221,122 @@ diff --git a/client.js b/client.js + (0, import_react8.createElement)("button", { type: "button", className: "bt-flybot-approve", disabled: busy, onClick: () => decide("approve") }, t("Approve")) + ), + error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null -+ ); + ); +-} +-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" }) +- ) + return (0, import_react8.createElement)( + "div", + { className: "bt-held-actions", "data-flybot": "held-reply-actions" }, + custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain -+ ); -+} -+ - // src/client/turn-reasoning.js - function turnReasoning(steps, current) { - const members = steps.length ? steps : [current]; -@@ -3767,7 +3767,7 @@ - var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim(); + ); + } +-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/cells.js --var import_react6 = require("react"); +-// 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_react9 = 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") }); -@@ -3780,29 +3780,29 @@ - actions.openSession(target); - }; - const reactions = useUi ? useUi((state) => msgKey ? state.reactions?.[msgKey] ?? null : null) : null; -- return (0, import_react6.createElement)( ++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_react9.createElement)( - "div", - { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, -- texts2.map((text, index) => { ++ "div", ++ { className: "bt-astack", "data-lead": lead ? "" : void 0, onClickCapture: onClick }, + texts2.map((text2, 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)( ++ 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_react9.createElement)( - "div", - { key: index, className: "bt-msg" }, -- (0, import_react6.createElement)( ++ "div", ++ { key: index, className: "bt-msg" }, + (0, import_react9.createElement)( - "div", - { className: "bt-msg-line" }, - lead && last ? lead : null, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-msg-line" }, ++ lead && last ? lead : null, + (0, import_react9.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 }) ++ "div", ++ { className: "bt-bubble" }, + (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: linkMentions(text2, 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 }) ++ ), + live ? null : (0, import_react9.createElement)(MessageTools, { text: text2, 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", { ++ ), + picked.length ? (0, import_react9.createElement)("div", { className: "bt-reacts" }, picked.map((emoji) => (0, import_react9.createElement)("button", { - key: emoji, - type: "button", - className: "bt-react", -@@ -3813,9 +3813,22 @@ - }) - ); - } ++ key: emoji, ++ type: "button", ++ className: "bt-react", ++ "aria-label": t("Remove reaction {emoji}", { emoji }), ++ onClick: () => actions.toggleReaction(msgKey, index, emoji) ++ }, emoji))) : null ++ ); ++ }) ++ ); ++} +var WAITING_OTHERS_NOTICE = "Waiting for the owner's approval"; - var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; ++var REACTIONS = ["👍", "❤️", "😂", "😮", "😢", "🙏"]; +function leaveNoticeCoveredByClose(room2, text2) { + if (room2?.origin !== "face-to-face" || room2.face?.status !== "closed") return false; + const match = String(text2 ?? "").trim().match(/^(.+) left(?: the room)?$/); @@ -4987,85 +5349,151 @@ diff --git a/client.js b/client.js + } + return false; +} - function useDismiss(open, close, refs) { -- (0, import_react6.useEffect)(() => { ++function useDismiss(open, close, refs) { + (0, import_react9.useEffect)(() => { - if (!open) return void 0; - const onDown = (event) => { - if (refs.some((ref) => ref.current?.contains(event.target))) return; -@@ -3835,12 +3835,12 @@ ++ if (!open) return void 0; ++ const onDown = (event) => { ++ if (refs.some((ref) => ref.current?.contains(event.target))) return; ++ close(); ++ }; + 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(); + } }; - }, [open]); +- document.addEventListener("keydown", onKey, true); +- return () => document.removeEventListener("keydown", onKey, true); +- }, [count, ownRef]); ++ 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); +-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_react9.useState)(null); + const [place, setPlace] = (0, import_react9.useState)(null); + const [copied, setCopied] = (0, import_react9.useState)(false); + const tools = (0, import_react9.useRef)(null); + const pop = (0, import_react9.useRef)(null); - const close = () => setPanel(null); - useDismiss(panel !== null, close, [tools, pop]); - const open = (kind, event) => { -@@ -3854,36 +3854,36 @@ - setPanel(kind); ++ const close = () => setPanel(null); ++ useDismiss(panel !== null, close, [tools, pop]); ++ const open = (kind, event) => { ++ if (panel === kind) { ++ close(); + 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 copy2 = () => { -- void navigator.clipboard?.writeText(text).then(() => { +- 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); - }); ++ setCopied(true); ++ setTimeout(() => setCopied(false), 1e3); ++ }); }; - const download2 = () => { -- const url = URL.createObjectURL(new Blob([text], { type: "text/markdown" })); +- 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 ++ 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` }); - link.click(); - setTimeout(() => URL.revokeObjectURL(url), 1e3); - }; -- const item = (label, Icon, run) => (0, import_react6.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { ++ const link = Object.assign(document.createElement("a"), { href: url, download: `${name}-message.md` }); ++ link.click(); ++ setTimeout(() => URL.revokeObjectURL(url), 1e3); ++ }; + const item = (label, Icon, run) => (0, import_react9.createElement)("button", { key: label, type: "button", role: "menuitem", onClick: () => { - close(); - run(); -- } }, (0, import_react6.createElement)(Icon), label); -- return (0, import_react6.createElement)( ++ close(); ++ run(); + } }, (0, import_react9.createElement)(Icon), label); + return (0, import_react9.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: tools, className: "bt-tools", "data-open": panel !== null || void 0 }, + react ? (0, import_react9.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_react9.createElement)(SmileIcon)) : null, + sessionId ? (0, import_react9.createElement)("button", { type: "button", className: "bt-tool", "aria-label": t("Reply"), title: t("Reply"), onClick: () => actions.quote(sessionId, text2) }, (0, import_react9.createElement)(ReplyIcon)) : null, + (0, import_react9.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_react9.createElement)(CheckIcon) : (0, import_react9.createElement)(DotsIcon)), + panel === "react" ? (0, import_react_dom.createPortal)((0, import_react9.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: () => { ++ "div", ++ { ref: pop, className: "bt-react-strip", role: "menu", "aria-label": t("Reactions"), style: place }, + REACTIONS.map((emoji) => (0, import_react9.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)( ++ close(); ++ react(emoji); ++ } }, emoji)) ++ ), document.body) : null, + panel === "more" ? (0, import_react_dom.createPortal)((0, import_react9.createElement)( - "div", - { ref: pop, className: "bt-menu", role: "menu", "aria-label": t("Message actions"), style: place }, - item(t("Copy"), CopyIcon, copy2), -@@ -3891,12 +3891,40 @@ - ), document.body) : null - ); - } --function ThinkingRow({ text }) { -- return (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 ++ ); ++} +var PEEK_MAX = 160; +var CUT = "…"; +var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]; @@ -5090,10 +5518,8 @@ diff --git a/client.js b/client.js + if (typeof text2 !== "string" || text2.trim() === "") return null; + const peek = peekLine(text2); + return (0, import_react9.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) ++ "details", ++ { className: "bt-think" }, + (0, import_react9.createElement)( + "summary", + null, @@ -5102,19 +5528,18 @@ diff --git a/client.js b/client.js + peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null + ), + (0, import_react9.createElement)("div", { className: "bt-think-text" }, text2) - ); - } - function useFileMentions({ node, openFile, fileMentions, useTurnData }) { -@@ -3904,19 +3904,33 @@ - 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)(() => { ++ ); ++} ++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_react9.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]); - } ++ 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 useClosingText(node, useTurnData) { + const tail = useTurnData?.("turn-tail"); + const seq = node.data.finalNode?.seq; @@ -5126,45 +5551,41 @@ diff --git a/client.js b/client.js + if (texts2.every((text2) => /^Not sent:/.test(text2.trim()))) return void 0; + return (0, import_react9.createElement)("span", { className: "bt-lead", title: bot.name, "data-flybot": "friend-lead" }, (0, import_react9.createElement)(BotMark, { bot, size: 22 })); +} - 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 }); ++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 held = groupPart === "reasoning" ? void 0 : heldReplyOf(roster.byId[sessionId], sessionId, data.turn); + const closing = useClosingText(node, 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 hidden = Boolean(roster.roomsById[sessionId]) || isHiddenTurn(roster, sessionId, data.turn); + const person = typedBy(roster, sessionId, data.turn); + const texts2 = groupPart === "reasoning" ? [] : data.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "").map((block, index) => index === 0 ? stripTrace(block.text) : block.text).filter((text2) => text2.trim() !== ""); - const running = data.status === "running"; - useLiveNote(actions, sessionId, `text:${node.key ?? data.seq}`, !hidden && running && texts2.length > 0 ? { text: true } : null); - if (hidden) return null; -@@ -3924,13 +3924,14 @@ - 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, ++ 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; + const bubbles = texts2.length ? person ? (0, import_react9.createElement)(PersonMessage, { name: person, texts: texts2, roster, selfId, actions, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi }) : (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, streaming: running, sessionId, msgKey: `${sessionId}:${data.turn}:${node.key ?? data.seq ?? ""}`, useUi, mentions, lead: friendLead(roster.byId[sessionId], texts2) }) : null; + return (0, import_react9.createElement)( + import_react9.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, { ++ 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_react9.createElement)(EventLine, { - key: relay.seq, - prefix: source.role === "stopped" ? t("Stopped by") : source.role === "reply" ? t("Reply from") : t("Message from"), - bots: [sender], -@@ -3940,61 +3940,230 @@ - 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 ++ 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_react9.createElement)(ThinkingRow, { text: reasoning }) : null, + held && bubbles ? (0, import_react9.createElement)(HeldReply, { held, roster, sessionId, actions, closing }, bubbles) : bubbles + ); @@ -5187,47 +5608,46 @@ diff --git a/client.js b/client.js + { className: "bt-group-msg bt-person-msg", "data-person": name }, + (0, import_react9.createElement)("span", { className: "bt-group-name" }, name), + (0, import_react9.createElement)(Bubbles, { texts: texts2, roster, selfId, actions, sessionId, msgKey, useUi, lead }) - ); - } - function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { - if (whole && bots.length === 1) { -- return (0, import_react6.createElement)( ++ ); ++} ++function EventLine({ prefix, bots, joiner = t("and"), onOpen, whole = false }) { ++ if (whole && bots.length === 1) { + return (0, import_react9.createElement)( - "div", - { className: "bt-event" }, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-event" }, + (0, import_react9.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) ++ "button", ++ { type: "button", className: "bt-event-whole", title: bots[0].name, onClick: () => onOpen?.(bots[0]) }, + (0, import_react9.createElement)("span", null, prefix), + (0, import_react9.createElement)(BotMark, { bot: bots[0], size: 16 }), + (0, import_react9.createElement)("span", { className: "bt-event-name" }, bots[0].name) - ) ++ ) ); +- }); +- 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) +- )); } - const parts = []; -- parts.push((0, import_react6.createElement)("span", { key: "p" }, prefix)); +- let foot = null; +- if (raised && multi) { +- foot = (0, import_react4.createElement)( ++ const parts = []; + parts.push((0, import_react9.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)( ++ bots.forEach((bot, index) => { + if (index > 0) parts.push((0, import_react9.createElement)("span", { key: `j${index}` }, joiner)); + parts.push((0, import_react9.createElement)( - "button", - { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, -- (0, import_react6.createElement)(BotMark, { bot, size: 16 }), ++ "button", ++ { key: bot.id ?? index, type: "button", onClick: () => onOpen?.(bot) }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), - bot.name - )); - }); -- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); ++ bot.name ++ )); ++ }); + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts); - } --function GroupMessage({ bot, text, room: room2, roster, actions, msgKey, useUi }) { -- const lead = (0, import_react6.createElement)( ++} +var heldForm = (text2) => String(text2 ?? "").replace(/\s+/g, " ").trim().replace(/^(to (the )?face[ -]?room|to the room)\s*:\s*/i, "").trim(); +var sameName = (a, b) => String(a ?? "").replace(/\s+/g, " ").trim().toLowerCase() === String(b ?? "").replace(/\s+/g, " ").trim().toLowerCase(); +function isHeldLine(room2, text2, bot) { @@ -5256,20 +5676,34 @@ diff --git a/client.js b/client.js + const held = heldLineState(room2, { seq, botId: bot.id, text: text2 }); + if (held === "rejected") { + return (0, import_react9.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")) + { className: "bt-event bt-not-sent", "data-flybot": "held-not-sent", role: "status" }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.createElement)("span", null, t("{name} · Not sent", { name: bot.name })) -+ ); -+ } + ); +- } 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)( + if (held === "held" || isHeldLine(room2, text2, bot)) return null; + const lead = (0, import_react9.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 }) ++ "button", ++ { type: "button", className: "bt-lead", "aria-label": t("Open {name}'s chat", { name: bot.name }), onClick: () => actions.openSession(bot.id) }, + (0, import_react9.createElement)(BotMark, { bot, size: 22 }) - ); -- return (0, import_react6.createElement)( ++ ); + const name = (0, import_react9.createElement)( + "span", + { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, @@ -5277,9 +5711,8 @@ diff --git a/client.js b/client.js + room2?.admin === bot.id ? (0, import_react9.createElement)("span", { className: "bt-group-role" }, t("Admin")) : null + ); + return (0, import_react9.createElement)( - "div", - { className: "bt-group-msg" }, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-group-msg" }, + name, + (0, import_react9.createElement)(Bubbles, { texts: [text2], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) + ); @@ -5324,7 +5757,14 @@ diff --git a/client.js b/client.js + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + if (person) { + return (0, import_react9.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 + { className: "bt-group-msg bt-person-msg bt-remote-msg", "data-person": person, "data-remote": handle }, + // One short header: the person, and the Bot that carried the line only when it is known (#275). + (0, import_react9.createElement)( @@ -5332,7 +5772,8 @@ diff --git a/client.js b/client.js + { className: "bt-group-name", title: handle }, + person, + carrier ? (0, import_react9.createElement)("span", { className: "bt-remote-handle" }, t("via {name}", { name: carrier })) : 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)) + (0, import_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead: (0, import_react9.createElement)("span", { className: "bt-lead", title: person }, (0, import_react9.createElement)(PersonMark, { name: person })) }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null + ); @@ -5344,18 +5785,29 @@ diff --git a/client.js b/client.js + { 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_react9.createElement)("span", { className: "bt-group-name", style: { color: inkText(colorOf(bot)) }, title: handle }, name) : (0, import_react9.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 ++ "span", ++ { className: "bt-group-name", style: { color: inkText(colorOf(bot)) } }, + name, + (0, import_react9.createElement)("span", { className: "bt-remote-handle", title: source.remote.home ?? handle, "aria-label": t("Friend's Bot") }, (0, import_react9.createElement)(GlobeMark), handle) ), -- (0, import_react6.createElement)(Bubbles, { texts: [text], roster, selfId: bot.id, actions, sessionId: room2?.id, msgKey, useUi, lead }) +- 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_react9.createElement)(Bubbles, { texts: [stripTrace(text2)], roster, selfId: bot.id, actions, sessionId, msgKey, useUi, lead }), + files.length ? (0, import_react9.createElement)(RemoteFiles, { files, plugin: member?.plugin ?? "social", botId: roster.byId[sessionId]?.id ?? sessionId }) : null -+ ); -+} + ); + } +-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 FirstContact({ source, roster, sessionId }) { + const person = typeof source.remote.typedBy === "string" ? source.remote.typedBy : ""; + const who = person || remoteSpeaker(source, roster).name; @@ -5399,231 +5851,298 @@ diff --git a/client.js b/client.js + `📎 ${file.name} · ${fileSize(file.size)}` + )), + error ? (0, import_react9.createElement)("span", { className: "bt-file-error", role: "alert" }, error) : null - ); - } - function GroupEvent({ prefix, room: room2, name, roster, actions }) { -- return (0, import_react6.createElement)( ++ ); ++} ++function GroupEvent({ prefix, room: room2, name, roster, actions }) { + return (0, import_react9.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 ++ "div", ++ { className: "bt-event" }, + (0, import_react9.createElement)("span", null, prefix), + room2 ? (0, import_react9.createElement)("button", { type: "button", onClick: () => actions.openSession(room2.id) }, (0, import_react9.createElement)(RoomAvatar, { room: room2, roster, size: 16 }), room2.name) : name ? (0, import_react9.createElement)("span", null, name) : null - ); - } - function MemoryEvent({ sessionId, scope, result, roster, actions, findBot }) { -@@ -4005,14 +4005,14 @@ - 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)( ++ ); ++} ++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_react9.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) ++ "div", ++ { className: "bt-event" }, + (0, import_react9.createElement)("button", { type: "button", title: result, onClick: () => actions.openMemory(target.id) }, (0, import_react9.createElement)(MemoryIcon), label) - ); - } - 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); ++ ); ++} ++function PartLine({ time }) { + return (0, import_react9.createElement)("div", { className: "bt-part-line", role: "separator" }, Number.isFinite(time) ? (0, import_react9.createElement)("span", null, dayLabel(time)) : null); - } - var scrollParent = (node) => { - for (let at = node.parentElement; at; at = at.parentElement) { -@@ -4021,11 +4021,11 @@ - 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); ++} ++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_react9.useState)({ items: [], cursor: void 0, loading: true, failed: false, startedAt: null }); + const top = (0, import_react9.useRef)(null); + const busy = (0, import_react9.useRef)(false); + const alive = (0, import_react9.useRef)(true); + const restore = (0, import_react9.useRef)(null); - const load = (cursor) => { - if (busy.current) return; - busy.current = true; -@@ -4041,19 +4041,19 @@ - if (alive.current) setView((current) => ({ ...current, loading: false, failed: true })); - }); - }; -- (0, import_react6.useEffect)(() => { ++ 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_react9.useEffect)(() => { - alive.current = true; - load(void 0); - return () => { - alive.current = false; - }; - }, [sessionId]); -- (0, import_react6.useLayoutEffect)(() => { ++ alive.current = true; ++ load(void 0); ++ return () => { ++ alive.current = false; ++ }; ++ }, [sessionId]); + (0, import_react9.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 pending = restore.current; ++ restore.current = null; ++ if (pending) pending.scroller.scrollTop = pending.scroller.scrollHeight - pending.fromEnd; ++ }, [view.items]); + (0, import_react9.useEffect)(() => { - const node = top.current; - if (!node || view.loading || view.failed || !view.cursor || typeof IntersectionObserver !== "function") return void 0; - const observer = new IntersectionObserver((entries2) => { -@@ -4069,35 +4069,35 @@ - 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)( ++ 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_react9.createElement)( - "div", - { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, -- (0, import_react6.createElement)( ++ "div", ++ { className: "bt-earlier", role: "feed", "aria-label": t("Earlier messages"), "aria-busy": view.loading }, + (0, import_react9.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 +- { 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")) +- ) +- ); ++ { ref: top, className: "bt-earlier-top" }, + view.loading ? (0, import_react9.createElement)(Shimmer, null, t("Loading earlier messages…")) : view.failed ? (0, import_react9.createElement)("button", { type: "button", className: "bt-soft", onClick: () => load(view.cursor) }, t("Couldn’t load earlier messages · Retry")) : view.cursor ? (0, import_react9.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 }); ++ ), ++ rows.map((row, index) => { ++ const key = rows.length - index; ++ switch (row.kind) { ++ case "divider": + return (0, import_react9.createElement)(PartLine, { key, time: row.time }); - case "user": -- return (0, import_react6.createElement)("div", { key, className: "bt-urow" }, (0, import_react6.createElement)( ++ case "user": + return (0, import_react9.createElement)("div", { key, className: "bt-urow" }, (0, import_react9.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 ++ "div", ++ { className: "bt-ububble" }, + row.text ? (0, import_react9.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: row.text, labels: mdLabels(), variant: "compact" }) : null, + row.images ? (0, import_react9.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 "answer": + return (0, import_react9.createElement)(AnsweredCell, { key, node: { data: row } }); - case "bot": -- return (0, import_react6.createElement)(Bubbles, { key, texts: row.texts, roster, selfId, actions }); ++ case "bot": + return (0, import_react9.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 }); ++ case "event": { + if (row.role === "report") return (0, import_react9.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: () => { ++ 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_react9.createElement)(EventLine, { key, prefix, bots: [other], onOpen: () => { - if (selfId && other.id) actions.openExchange(selfId, other.id); - }, whole: true }); - } -@@ -4105,7 +4105,7 @@ - return null; - } - }), -- (0, import_react6.createElement)(PartLine, { time: view.startedAt }) ++ if (selfId && other.id) actions.openExchange(selfId, other.id); ++ }, whole: true }); ++ } ++ default: ++ return null; ++ } ++ }), + (0, import_react9.createElement)(PartLine, { time: view.startedAt }) - ); - } - function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { -@@ -4116,7 +4116,7 @@ - const deferred = source.kind === "bot" && source.deferred === true; - const relayed = isRelayContext(data); - const turn = node.location?.turn?.turn ?? data.turn; -- (0, import_react6.useLayoutEffect)(() => { ++ ); ++} ++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_react9.useLayoutEffect)(() => { - if (relayed) actions.noteTurnPeer(sessionId, turn, source.senderSessionId); - }, [relayed, sessionId, turn, source.senderSessionId]); - const order = useSessions((list) => { -@@ -4135,23 +4135,31 @@ - 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; ++ 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_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)("div", { className: "bt-open-new" }, (0, import_react9.createElement)(NewDivider), (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, unreadOpening)), body) : body; - function triggerBody() { ++ function triggerBody() { + if (source.form === "remote-chat") return null; + if (source.shown === true) return null; + if (source.remote?.handle) { + const message = (0, import_react9.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_react9.createElement)(import_react9.Fragment, null, (0, import_react9.createElement)(FirstContact, { source, roster, sessionId }), message) : message; + } - 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.kind === "bot") { + if (source.handoff === true) return roster.roomsById[sessionId] ? null : (0, import_react9.createElement)(EarlierParts, { sessionId, roster, actions }); + if (source.role === "room" || source.role === "kickoff") return null; + if (source.role === "mention") return (0, import_react9.createElement)("div", { className: "bt-event" }, (0, import_react9.createElement)("span", null, contentText(data.content))); - if (source.role === "secret" || deferred) return null; - if (source.role === "post") { - if (source.failed !== void 0 || source.resumed === true || source.stopped === true) 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; + if (source.notice === true || source.senderSessionId === "face-notice") 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 }); ++ 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_react9.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 === "report") { + return (0, import_react9.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")); ++ } + if (source.role === "resume" || source.resumed === true) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Picked up its work after a restart")); + if (source.role === "retry" || source.retried === true) return (0, import_react9.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)])), -@@ -4165,7 +4165,7 @@ - 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, { ++ 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_react9.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), -@@ -4174,24 +4174,24 @@ - } - 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")); ++ 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_react9.createElement)("div", { className: "bt-event" }, prompts.length > 0 ? t("Scheduled task: {text}", { text: prompts.join(" · ").slice(0, 80) }) : t("Routine started")); - } - 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")); ++ } ++ if (source.kind === "user-question-reply") return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, contentText(data.content).slice(0, 80) || t("Update")); } +- return (0, import_react4.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)) +- ); +- })) +- ); } - 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 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_react9.createElement)("div", { className: "bt-new-sep", role: "separator", "aria-label": t("New messages") }, (0, import_react9.createElement)("span", null, t("New"))); } - function TurnProcessCell({ node, sessionId, turnProcess, useRoster, useUi, useTurnClock, actions }) { - const open = turnProcess === void 0 || !turnProcess.foldable || turnProcess.open; -- (0, import_react6.useEffect)(() => { +-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_react9.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)(() => { ++ 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_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "first", start); - }, [sessionId, turn, start]); - const visit = useUi((state) => state.visits?.[sessionId]); -@@ -4199,11 +4199,67 @@ - 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; ++ 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_react9.createElement)(NewDivider) : null; } - function Nothing() { - return 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 Nothing() { ++ return null; } +-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; +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([ @@ -5676,17 +6195,186 @@ diff --git a/client.js b/client.js + if (isHiddenNotice(block)) continue; + if (block?.type === "tool-removal" && block.toolName) parts.push(`Tool removed: ${block.toolName}`); + else if (block?.type === "tool-addition" && block.toolName) parts.push(`Tool added: ${block.toolName}`); -+ } + } +- 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: [] } +- }))); + if (parts.length === 0) return null; + return (0, import_react9.createElement)("div", { className: "bt-event" }, parts.join(" · ")); -+} - var SEPARATOR_GAP_MS = 30 * 60 * 1e3; - function clockLabel(time, now = Date.now()) { - const date = new Date(time); -@@ -4233,10 +4233,19 @@ - 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; +- +-// 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?.(); +- } +- }); ++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 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)) +- ); ++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; + } +-function SecretHead({ bot, title, detail, name, onClose }) { +- return (0, import_react5.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)( +- "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 +- ); ++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 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)); +- }; +- 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)( +- "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-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")) +- ) +- ); ++var unseenSince = (visit, time) => typeof visit?.seen === "number" && time > visit.seen && time <= visit.entered; +function movedOnSince(visit, turns, hiddenTurns) { + if (typeof visit?.entered !== "number") return false; + for (const key in turns ?? {}) { @@ -5694,107 +6382,225 @@ diff --git a/client.js b/client.js + if (turns[key].first !== void 0 && turns[key].first > visit.entered) return true; + } + return false; -+} + } +-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)) +- ) +var settledVisit = (visit) => typeof visit?.seen === "number" && visit.seen < visit.entered ? { ...visit, seen: visit.entered } : visit; - function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { - const turn = location2.turn; - const time = location2.end.time; -- (0, import_react6.useEffect)(() => { ++function TurnTailCell({ turn: location2, sessionId, useTurnClock, useUi, actions }) { ++ const turn = location2.turn; ++ const time = location2.end.time; + (0, import_react9.useEffect)(() => { - actions.noteTurnTime(sessionId, turn, "end", time); - }, [sessionId, turn, time]); - const label = useTurnClock((state) => { -@@ -4250,15 +4250,15 @@ - return start !== void 0 && start > visit.seen && start <= visit.entered; - }); - if (!label && !fresh) return null; -- return (0, import_react6.createElement)( -- import_react6.Fragment, -+ return (0, import_react9.createElement)( -+ import_react9.Fragment, - null, -- label ? (0, import_react6.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, -- fresh ? (0, import_react6.createElement)(NewDivider) : null ++ 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_react9.createElement)( ++ import_react9.Fragment, ++ null, + label ? (0, import_react9.createElement)("div", { className: "bt-time-sep", role: "separator" }, label) : null, + fresh ? (0, import_react9.createElement)(NewDivider) : null ); } - function Shimmer({ children }) { -- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +-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 Shimmer({ children }) { + return (0, import_react9.createElement)("span", { className: "bt-shimmer" }, children); - } - var PHRASES = { - thinking: ["Thinking about it", "Weighing the options", "Working out a plan", "Sorting out the details"], -@@ -4320,19 +4320,22 @@ - } - function workPhrase(tool) { - if (!tool) return t("Thinking about it"); -- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); -+ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); +} ++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) }; ++ } ++} ++function workPhrase(tool) { ++ if (!tool) return t("Thinking about it"); ++ return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("Using {tool}", { tool }); + } +-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 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_react6.useEffect)(() => { +-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 }))); ++function useLiveNote(actions, sessionId, id, entry) { ++ const key = entry ? JSON.stringify(entry) : ""; + (0, import_react9.useEffect)(() => { - if (!key) return void 0; - actions.noteLive(sessionId, id, JSON.parse(key)); - return () => actions.noteLive(sessionId, id, null); - }, [sessionId, id, key]); ++ if (!key) return void 0; ++ actions.noteLive(sessionId, id, JSON.parse(key)); ++ return () => actions.noteLive(sessionId, id, null); ++ }, [sessionId, id, key]); } --function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi, useWork }) { -- const [live, setLive] = (0, import_react6.useState)({ replies: [], speaking: null }); -- (0, import_react6.useEffect)(() => { +-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)( +function GroupReplies({ sessionId, callId, done, content, roster, actions, useUi }) { + const [live, setLive] = (0, import_react9.useState)({ replies: [] }); + (0, import_react9.useEffect)(() => { - if (done) return void 0; - let stop = false; - let timer; -@@ -4341,10 +4341,10 @@ - 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 (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) : ""; - if (entry && key !== seen) { - seen = key; -- setLive({ replies: entry.replies ?? [], speaking: entry.speaking ?? null }); ++ if (entry && key !== seen) { ++ seen = key; + setLive({ replies: entry.replies ?? [] }); - } - timer = setTimeout(tick, document.hidden ? 5e3 : 1e3); - }; -@@ -4362,71 +4362,89 @@ - 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; ++ } ++ 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 = []; ++ } ++ } + if (replies.length === 0) return null; - const room2 = roster.roomsById[sessionId]; -- return (0, import_react6.createElement)( ++ const room2 = roster.roomsById[sessionId]; + return (0, import_react9.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" }; +- { 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")) +- ) ++ { className: "bt-group-replies" }, ++ replies.map((reply, index) => { + const stored = roster.byId[reply.botId]; + const resolved = reply.remote ? stored : rosterBot(roster, reply.botId) ?? stored; + const bot = resolved ?? { 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)( ++ if (reply.kind === "user") { + const who = reply.name && reply.name !== "User" ? reply.name : reply.authorId; + if (reply.other) { + return (0, import_react9.createElement)(PersonMessage, { key: `user-${index}`, name: who, texts: [remoteMessageText(reply.text)], roster, selfId: sessionId, actions, sessionId, msgKey: callId ? `${sessionId}:${callId}:${index}` : void 0, useUi }); + } + return (0, import_react9.createElement)( - "div", -- { className: "bt-ububble" }, -- (0, import_react6.createElement)(import_dsh_client_ui_primitives.MarkdownText, { text: reply.text, labels: mdLabels(), variant: "compact" }) -- )); ++ "div", + { key: `user-${index}`, className: "bt-urow", "data-author": reply.authorId, "data-author-name": who }, + who ? (0, import_react9.createElement)("span", { className: "bt-uauthor" }, who) : null, + (0, import_react9.createElement)( @@ -5804,67 +6610,45 @@ diff --git a/client.js b/client.js + ), + room2?.origin === "face-to-face" ? (0, import_react9.createElement)("span", { className: "bt-lead", title: who || void 0 }, (0, import_react9.createElement)(PersonMark, { name: who || void 0 })) : null + ); - } - if (reply.kind === "failed") { -- return (0, import_react6.createElement)( ++ } ++ if (reply.kind === "failed") { + return (0, import_react9.createElement)( - "div", -- { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status", title: [reply.code, reply.status, reply.text].filter(Boolean).join(" · ") }, ++ "div", + { key: `${reply.botId}-${index}`, className: "bt-event bt-event-error", role: "status" }, - t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) - ); - } - if (reply.kind === "lookup") { -- return (0, import_react6.createElement)( ++ t("{name} could not answer: {reason}", { name: bot.name, reason: failureLabel(reply.code, reply.text) }) ++ ); ++ } ++ if (reply.kind === "lookup") { + return (0, import_react9.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 })); ++ "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_react9.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 === "resumed") { + return (0, import_react9.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 === "cut") { + return (0, import_react9.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)( ++ } ++ if (reply.kind === "approval" || reply.kind === "working") { + return (0, import_react9.createElement)( - "div", - { key: `${reply.botId}-${index}`, className: "bt-event" }, -- (0, import_react6.createElement)( ++ "div", ++ { key: `${reply.botId}-${index}`, className: "bt-event" }, + (0, import_react9.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 })) ++ "button", ++ { type: "button", className: "bt-event-whole", onClick: () => actions.showDetails(sessionId) }, + (0, import_react9.createElement)(BotMark, { bot, size: 16 }), + (0, import_react9.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 })); ++ ) ++ ); ++ } ++ if (reply.kind === "stopped") { + return (0, import_react9.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 })) -- ); ++ } + if (reply.kind === "notice" && room2?.origin === "face-to-face" && String(reply.text ?? "").trim() === WAITING_OTHERS_NOTICE) return null; + if ((reply.kind === "notice" || reply.kind === "left") && leaveNoticeCoveredByClose(room2, reply.text)) return null; + if (reply.kind === "notice" || reply.kind === "joined" || reply.kind === "left" || reply.kind === "started") { @@ -5884,154 +6668,224 @@ diff --git a/client.js b/client.js + }); + } + return (0, import_react9.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 }); - }) ++ }) ); } -@@ -4435,52 +4435,52 @@ - function callTree(root) { - const calls = []; - const seen = /* @__PURE__ */ new Set(); -- const visit = (call2) => { -- const id = call2.callId ?? call2; +-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); ++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 = (call5) => { + const id = call5.callId ?? call5; - if (seen.has(id)) return; - seen.add(id); -- calls.push(call2); -- for (const child of call2.subCalls ?? []) visit(child); ++ if (seen.has(id)) return; ++ seen.add(id); + calls.push(call5); + for (const child of call5.subCalls ?? []) visit(child); }; - visit(root); - return calls; - } - function useShownCards(actions, sessionId, callIds) { - const key = JSON.stringify(callIds); -- (0, import_react6.useLayoutEffect)(() => { +- 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 +- ); ++ visit(root); ++ return calls; ++} ++function useShownCards(actions, sessionId, callIds) { ++ const key = JSON.stringify(callIds); + (0, import_react9.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 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 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 renderCall(call5, { sessionId, roster, actions, useUi, findBot }) { + const tool = toolName(call5); + const done = call5.kind === "tool-result"; + const { text: arg, value: argValue } = toolArgs(call5); - 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 }); ++ const self = roster.byId[sessionId]; ++ switch (tool) { ++ case "message_bot": { ++ const target = findBot(arg("to")) ?? { name: arg("to") ?? "…", color: "gray" }; + if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't message {name}", { name: target.name })); + return (0, import_react9.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 "stop_bot": { ++ const target = findBot(arg("bot")) ?? { name: arg("bot") ?? "…", color: "gray" }; + if (done && call5.isError) return (0, import_react9.createElement)("div", { className: "bt-event" }, t("Couldn't stop {name}", { name: target.name })); + return (0, import_react9.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 "create_bot": { ++ const created = findBot(arg("name")); ++ if (!done || created === void 0) return null; + return (0, import_react9.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 "update_bot": { ++ const target = findBot(arg("name")) ?? findBot(arg("bot")) ?? (arg("bot") ? void 0 : self); + return done && target ? (0, import_react9.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 "group_relay": + return (0, import_react9.createElement)(GroupReplies, { sessionId, callId: call5.callId, done, content: call5.content, roster, actions, useUi }); - case "create_group": - case "update_group": - case "delete_group": -@@ -4492,64 +4492,90 @@ - post_to_group: ["Posted in", "Couldn't post in the group"] - }[tool]; - if (!done) return null; -- const result = contentText(call2.content); ++ 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(call5.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 named = /^(Created group|Updated|Nothing changed in|Deleted|Posted in) "([^"]+)"/.exec(result); + if (call5.isError || !named) return (0, import_react9.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 }); ++ const name = named[2]; ++ const room2 = roster.rooms.find((entry) => entry.name === name); + return (0, import_react9.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 "ask_user": { ++ const question = roster.questions?.[sessionId]; + if (!question || question.callId !== call5.callId) return (0, import_react9.createElement)("span", { className: "bt-q-gone", hidden: true }); + return (0, import_react9.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 "request_secret": + return (0, import_react9.createElement)(SecretToolCell, { sessionId, callId: call5.callId, name: arg("name"), done, content: call5.content, roster, actions }); - case "ask_user_question": { - if (!done) return null; - let parsed = null; - try { -- parsed = JSON.parse(contentText(call2.content)); ++ case "ask_user_question": { ++ if (!done) return null; ++ let parsed = null; ++ try { + parsed = JSON.parse(contentText(call5.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 })); ++ } catch { ++ parsed = null; ++ } ++ const answer = parsed?.answers?.[0]; ++ const question = argValue("questions")?.[0]; ++ if (!answer) return null; + return (0, import_react9.createElement)("div", { className: "bt-q-inline" }, (0, import_react9.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 "read_group_chat": { ++ if (!done) return null; ++ const room2 = roster.rooms.find((entry) => entry.name === arg("group")); + return room2 ? (0, import_react9.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; ++ } ++ case "remember": ++ case "forget": + return done && !call5.isError ? (0, import_react9.createElement)(MemoryEvent, { sessionId, scope: arg("scope"), result: contentText(call5.content), roster, actions, findBot }) : null; - default: - return null; - } ++ default: ++ return null; ++ } } --function ToolCell({ node, sessionId, useRoster, useUi, useWork, actions }) { +-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 ToolCell({ node, sessionId, useRoster, useUi, 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 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] ?? botByName(roster.bots, nameOrId); - 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 calls = hidden ? [] : callTree(root); + const drawn = calls.filter((call5) => DRAWN.has(toolName(call5))); + const running = root.kind === "tool-result" ? void 0 : calls.findLast((call5) => call5.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)); ++ 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((call5) => CARDS.has(toolName(call5)) && call5.callId).map((call5) => call5.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)))); ++ if (drawn.length === 0) return null; + const env = { sessionId, roster, actions, useUi, findBot }; + return (0, import_react9.createElement)(import_react9.Fragment, null, drawn.map((call5, index) => (0, import_react9.createElement)(import_react9.Fragment, { key: call5.callId ?? index }, renderCall(call5, env)))); -+} + } +-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")) +- ) +- ); + +// src/client/awaiting.js +var AWAIT_REPLY_MS = 10 * 60 * 1e3; @@ -6039,7 +6893,30 @@ diff --git a/client.js b/client.js + const at = roster?.remoteAwaiting?.[sessionId]; + if (typeof at !== "number" || roster?.byId?.[sessionId]?.remote === void 0) return false; + return now - at < AWAIT_REPLY_MS; -+} + } +-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"))) +- ]; + +// src/client/remote-approval-wait.js +var REMOTE_APPROVAL_LABEL = "Waiting for their approval"; @@ -6047,8 +6924,76 @@ diff --git a/client.js b/client.js + if (!remote) return null; + if (tool && inputAt <= (tool.at ?? 0)) return null; + return { key: "remote-approval", label: REMOTE_APPROVAL_LABEL, state: "thinking" }; -+} -+ + } +-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"); +// src/client/activity-row-host.js +var RUNNING_HOST = "[data-chat-flow] [data-chat-running]"; +var TAIL_HOST = "tail"; @@ -6058,20 +7003,74 @@ 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); } +-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/activity-row.js --var import_react7 = require("react"); +-// 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)( +- "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/activity-row.js +var import_react10 = 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))]); -@@ -4559,9 +4559,69 @@ - const hours = Math.floor(minutes / 60); - return minutes % 60 === 0 ? `${hours}h` : `${hours}h ${minutes % 60}m`; ++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`; } --function useRunningHost(active) { -- const [host, setHost] = (0, import_react7.useState)(null); -- (0, import_react7.useEffect)(() => { +-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(); +var REMOTE_GRACE_MS = 1500; +function useAwaitingReply(roster, sessionId) { + const at = roster?.remoteAwaiting?.[sessionId]; @@ -6135,45 +7134,107 @@ diff --git a/client.js b/client.js +function useRunningHost(active, where = RUNNING_HOST) { + const [host, setHost] = (0, import_react10.useState)(null); + (0, import_react10.useEffect)(() => { - if (!active) { - setHost(null); - return void 0; -@@ -4569,7 +4569,7 @@ - let frame = 0; - const find = () => { - frame = 0; -- const node = document.querySelector("[data-chat-flow] > [data-chat-running]"); ++ if (!active) { ++ setHost(null); ++ return void 0; ++ } ++ let frame = 0; ++ const find = () => { ++ frame = 0; + const node = findHost(where); - setHost((prev) => prev === node ? prev : node); - }; - const observer = new MutationObserver(() => { -@@ -4581,13 +4581,13 @@ - cancelAnimationFrame(frame); - observer.disconnect(); - }; -- }, [active]); ++ setHost((prev) => prev === node ? prev : node); ++ }; ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(find); ++ }); ++ observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); ++ find(); ++ return () => { ++ cancelAnimationFrame(frame); ++ observer.disconnect(); ++ }; + }, [active, where]); - return host; - } - function useRowEntrances(replacing) { -- const check = (0, import_react7.useRef)(replacing); ++ return host; ++} ++function useRowEntrances(replacing) { + const check = (0, import_react10.useRef)(replacing); - check.current = replacing; -- (0, import_react7.useEffect)(() => { ++ check.current = replacing; + (0, import_react10.useEffect)(() => { - const seen = /* @__PURE__ */ new WeakSet(); - const started = Date.now(); - let column = null; -@@ -4638,21 +4638,31 @@ - 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 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); + }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- close(); ++ 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); + } + }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); ++ const observer = new MutationObserver(() => { ++ if (!frame) frame = requestAnimationFrame(scan); ++ }); ++ scan(); + return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); ++ cancelAnimationFrame(frame); ++ observer.disconnect(); + }; +- }, [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; ++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_react10.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 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 awaiting = useAwaitingReply(roster, sessionId); + const sendRun = (0, import_react10.useRef)(false); + if (!running) sendRun.current = false; @@ -6184,49 +7245,59 @@ diff --git a/client.js b/client.js + const bridging = bot?.remote !== void 0 && !streaming && lastRun.current.running && lastRun.current.send && !running; + lastRun.current = { running, send: sendRun.current }; + const steady = bridging; - const tool = tools.at(-1); -- const inputRef = (0, import_react7.useRef)(0); ++ const tool = tools.at(-1); + const inputRef = (0, import_react10.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 inputAt = Math.max(inputRef.current, pendingInput); ++ inputRef.current = inputAt; + const phrase = visible ? remoteApprovalPhrase(bot?.remote !== void 0, { tool, inputAt }) : null; + const want = !visible ? null : phrase ? { ...phrase, label: t(phrase.label) } : 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_react10.useState)(null); - const wantKey = want?.key ?? null; -- const markState = (0, import_react7.useRef)("thinking"); ++ const wantKey = want?.key ?? null; + const markState = (0, import_react10.useRef)("thinking"); - if (want) markState.current = want.state; -- (0, import_react7.useEffect)(() => { ++ if (want) markState.current = want.state; + (0, import_react10.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; -@@ -4663,26 +4663,31 @@ - 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); ++ 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; + } +- 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 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)( ++ const timer = setTimeout(make, wait); ++ return () => clearTimeout(timer); ++ }, [wantKey, shown]); + const [phase, setPhase] = (0, import_react10.useState)(null); + const endedAt2 = (0, import_react10.useRef)(0); + const shift = (0, import_react10.useRef)(null); + const leaving = (0, import_react10.useRef)(false); - leaving.current = phase !== null && !visible; -- (0, import_react7.useEffect)(() => { ++ leaving.current = phase !== null && !visible; + (0, import_react10.useEffect)(() => { - if (visible) { - setPhase("in"); - 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); ++ if (visible) { ++ setPhase("in"); ++ return void 0; ++ } + let timer; + const leave = () => { + setPhase((current) => current === "in" ? "out" : current); @@ -6237,35 +7308,29 @@ diff --git a/client.js b/client.js + }; + if (steady) timer = setTimeout(leave, REMOTE_GRACE_MS); + else leave(); - return () => clearTimeout(timer); - }, [visible]); -- (0, import_react7.useLayoutEffect)(() => { ++ return () => clearTimeout(timer); ++ }, [visible]); + (0, import_react10.useLayoutEffect)(() => { - if (phase !== null) return; -- endedAt.current = Date.now(); ++ if (phase !== null) return; + endedAt2.current = Date.now(); - setShown(null); - const before = shift.current; - shift.current = null; -@@ -4691,31 +4691,37 @@ - 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)(() => { ++ 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_react10.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 (!running) actions.clearLive(sessionId); ++ }, [running, sessionId]); + useRowEntrances(() => leaving.current || Date.now() - endedAt2.current < 300); + const [now, setNow] = (0, import_react10.useState)(Date.now); + (0, import_react10.useEffect)(() => { - if (phase !== "in") return void 0; - const timer = setInterval(() => setNow(Date.now()), 1e4); - return () => clearInterval(timer); - }, [phase]); -- const host = useRunningHost(running); ++ if (phase !== "in") return void 0; ++ const timer = setInterval(() => setNow(Date.now()), 1e4); ++ return () => clearInterval(timer); ++ }, [phase]); + const remote = bot?.remote !== void 0; + const host = useRunningHost(running || remote && phase !== null, running || !remote ? RUNNING_HOST : TAIL_HOST); + const drawn = (0, import_react10.useRef)(false); @@ -6273,36 +7338,70 @@ diff --git a/client.js b/client.js + (0, import_react10.useEffect)(() => { + drawn.current = Boolean(host && phase !== null && shown); + }, [host, phase, shown]); - 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)( ++ 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_react10.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)( +- { 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 +- ); + { 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_react10.createElement)(BotMark, { bot, size: 28, state: markState.current, live: true, pokeable: true }) : (0, import_react10.createElement)(RoomAvatar, { room: room2, roster, size: 28 }), + (0, import_react10.createElement)( - "span", - { className: "bt-typing-text" }, -- (0, import_react7.createElement)( ++ "span", ++ { className: "bt-typing-text" }, + (0, import_react10.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 ++ "span", ++ { key: `${shown.key}:${shown.at}`, className: "bt-typing-label" }, + (0, import_react10.createElement)(Shimmer, null, `${shown.text}…`), + elapsed ? (0, import_react10.createElement)("span", { className: "bt-typing-time" }, `· ${elapsed}`) : null - ) - ) - ), host); -@@ -4731,49 +4731,1905 @@ - return dsh.kind === "app" && APP_OFFER.has(app?.phase) && sameVersion(app.version, dsh.verified); ++ ) ++ ) ++ ), host); } - --// src/client/sidebar.js --var import_react8 = require("react"); +-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) +- ); ++ ++// 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); + } +-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]); ++ +// src/client/overlay-hooks.js +var import_react11 = require("react"); +var escapeLayers = []; @@ -6320,7 +7419,41 @@ diff --git a/client.js b/client.js + if (escapeLayers.length === 0) window.removeEventListener("keydown", onEscapeKey); + }; + }, []); -+} + } +-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 +- ); +function usePaneLeft() { + const [paneLeft, setPaneLeft] = (0, import_react11.useState)(280); + (0, import_react11.useLayoutEffect)(() => { @@ -6328,7 +7461,31 @@ diff --git a/client.js b/client.js + if (column) setPaneLeft(Math.round(column.getBoundingClientRect().right)); + }, []); + return paneLeft; -+} + } +-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 +- )); + +// src/client/member-status.js +var import_react12 = require("react"); @@ -6350,8 +7507,26 @@ diff --git a/client.js b/client.js + const doing = workIn(room2, work, id); + const approvals = askedIn(room2, work, id, doing); + return { id, approvals, doing, here: doing !== void 0, state: approvals.length > 0 ? "ask" : doing ? "work" : "idle" }; -+ }); -+} + }); +- return (0, import_react6.createElement)("div", { className: "bt-event" }, parts); + } +-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 }) +- ); +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] })); @@ -6367,7 +7542,14 @@ diff --git a/client.js b/client.js + const room2 = roster.roomsById[roomId]; + return entry && room2 ? [{ room: room2, tool: entry.tool, step: stepIn(work, botId, roomId, entry.tool), since: entry.since }] : []; + }); -+} + } +-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 summaryText(busy) { + const asks = busy.filter((entry) => entry.state === "ask"); + const workers = busy.filter((entry) => entry.state === "work"); @@ -6376,14 +7558,28 @@ 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 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 Face({ entry, size }) { + return (0, import_react12.createElement)( + "span", + { className: "bt-mw-face", "data-ask": entry.state === "ask" || void 0 }, + (0, import_react12.createElement)(BotMark, { bot: entry.bot, size, state: entry.state === "ask" ? "alert" : toolState(entry.doing?.tool ?? "") }) -+ ); -+} + ); + } +-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 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)}`; @@ -6395,7 +7591,69 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("span", { className: "bt-mw-row-text" }, (0, import_react12.createElement)(Shimmer, null, text2)), + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : 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); +- return () => { +- alive.current = false; +- }; +- }, [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 +function SummaryLine({ busy, now, onOpen }) { + const { waiting, doing } = summaryText(busy); + const elapsed = doing ? elapsedSince(now, busy.find((entry) => entry.state === "work")?.doing.since) : ""; @@ -6409,12 +7667,110 @@ diff --git a/client.js b/client.js + waiting ? (0, import_react12.createElement)("span", { className: "bt-mw-ask" }, waiting) : null, + waiting && doing ? " · " : null, + doing ? (0, import_react12.createElement)(Shimmer, null, doing) : 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 }) + elapsed ? (0, import_react12.createElement)("span", { className: "bt-mw-row-time" }, `· ${elapsed}`) : null -+ ); -+} + ); + } +-function TriggerCell({ node, sessionId, useRoster, useSessions, useTurnClock, useUi, actions }) { +function MemberDock({ sessionId, useRoster, useWork, actions }) { -+ const roster = useRoster((value) => value); + 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")); +- } +- 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")); + const work = useWork((value) => value); + const room2 = roster.roomsById[sessionId]; + const bot = room2 ? void 0 : roster.byId[sessionId]; @@ -6435,7 +7791,7 @@ diff --git a/client.js b/client.js + rows.map((entry) => (0, import_react12.createElement)(WorkRow, { key: entry.id, bot: entry.bot, tool: entry.doing.tool, step: entry.doing.step, label: entry.bot.name, at: entry.doing.since, now, onOpen: open })), + rest.length > 0 ? (0, import_react12.createElement)(SummaryLine, { busy: rest, now, onOpen: open }) : null + ); -+ } + } + return (0, import_react12.createElement)("div", { ref, className: "bt-mw-dock-stack" }, groups.slice(0, ROWS_MAX).map((item) => (0, import_react12.createElement)(WorkRow, { + key: item.room.id, + bot, @@ -6446,7 +7802,41 @@ diff --git a/client.js b/client.js + now, + onOpen: () => actions.openSession(item.room.id) + }))); -+} + } +-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 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 Nothing() { +- return null; +-} +-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 useFlowColumn(ref, active, key) { + (0, import_react12.useLayoutEffect)(() => { + const node = ref.current; @@ -6463,7 +7853,14 @@ 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 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; +function useNow(active) { + const [now, setNow] = (0, import_react12.useState)(Date.now); + (0, import_react12.useEffect)(() => { @@ -6472,7 +7869,15 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [active]); + return now; -+} + } +-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 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"], @@ -6503,7 +7908,30 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("button", { type: "button", className: "bt-q-btn", onClick: () => actions.openSession(bot.id) }, t("Open {name}'s chat", { name: bot.name })) + ) + ); -+} + } +-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 MemberWork({ entry, bot, room: room2, actions, now }) { + const [stopping, setStopping] = (0, import_react12.useState)(false); + if (entry.state === "ask") { @@ -6528,14 +7956,19 @@ diff --git a/client.js b/client.js + (0, import_react12.createElement)("span", null, (0, import_react12.createElement)(Shimmer, null, workText(entry.doing.tool, entry.doing.step)), since(now, entry.doing.since)), + entry.here && entry.doing.handed !== true ? (0, import_react12.createElement)("button", { type: "button", className: "bt-mw-stop", disabled: stopping, onClick: stop }, t("Stop")) : null + ) -+ ); -+} + ); + } +-function Shimmer({ children }) { +- return (0, import_react6.createElement)("span", { className: "bt-shimmer" }, children); +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 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 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} @@ -6581,7 +8014,15 @@ diff --git a/client.js b/client.js + accepted: "done", + cancelled: "done", + failed: "done" -+}; + }; +-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"] +function columnOf(task) { + return COLUMN_OF[task?.status] ?? "not-started"; +} @@ -6657,7 +8098,23 @@ diff --git a/client.js b/client.js + { id: "needs", title: "Needs you", empty: "Nothing needs you right now." }, + { id: "progress", title: "In progress", empty: "Nothing is running." }, + { id: "done", title: "Done", empty: "Nothing finished in the last 7 days." } -+]; + ]; +-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" }; +var sharedTasks = []; +var taskListeners = /* @__PURE__ */ new Set(); +function publishTasks(list) { @@ -6702,7 +8159,10 @@ diff --git a/client.js b/client.js + }); + load(); + workflowPoll = setInterval(load, POLL_MS2); -+ } + } +- case "stop_bot": { +- const target = findBot(arg("bot")); +- return { label: target ? t("Stopping {name}", { name: target.name }) : t("Stopping another Bot"), state: "sending" }; + return () => { + workflowListeners.delete(setWorkflows); + if (workflowListeners.size === 0 && workflowPoll !== null) { @@ -6894,7 +8354,12 @@ diff --git a/client.js b/client.js + const column = columnOf(task); + if (column === "in-progress") progress.push({ ...card, status: "task", since: startedAtOf(task) }); + else if (column === "done" && fresh(task.updatedAt)) done.push({ ...card, status: task.status === "cancelled" ? "cancelled" : "done" }); -+ } + } +- 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" }; + } + for (const graph of Array.isArray(workflows) ? workflows : []) { + if (graph === null || typeof graph !== "object") continue; @@ -6906,7 +8371,23 @@ diff --git a/client.js b/client.js + const at2 = endedAt(run); + if (!fresh(at2)) continue; + if (latest === void 0 || at2 > endedAt(latest)) latest = run; -+ } + } +- 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) }; + if (latest === void 0) continue; + const at = endedAt(latest); + const name = typeof graph.name === "string" && graph.name.trim() !== "" ? graph.name.trim() : String(graph.id ?? t("A workflow")); @@ -7122,7 +8603,7 @@ diff --git a/client.js b/client.js +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"); -+ } + } + return typeof card.at === "number" ? relativeShort(card.at) : ""; +} +function InboxCard({ card, roster, now, busy, error, onAnswer, onOpen }) { @@ -7184,7 +8665,10 @@ diff --git a/client.js b/client.js + (0, import_react13.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 workPhrase(tool) { +- if (!tool) return t("Thinking about it"); +- return toolActivity(tool, () => void 0, () => void 0)?.label ?? t("On it"); +function useNow2(ms) { + const [now, setNow] = (0, import_react13.useState)(Date.now); + (0, import_react13.useEffect)(() => { @@ -7192,7 +8676,14 @@ diff --git a/client.js b/client.js + return () => clearInterval(timer); + }, [ms]); + return now; -+} + } +-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 openCard(actions, card) { + actions.closeInbox(); + if (card?.kind === "workflow") { @@ -7200,7 +8691,37 @@ diff --git a/client.js b/client.js + return; + } + actions.openSession(card.chat); -+} + } +-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 = []; +function InboxDialog({ roster, useWork, useSessions, actions, leaving = false }) { + const work = useWork((value) => value); + const sessions = useSessions((value) => value); @@ -7227,7 +8748,13 @@ diff --git a/client.js b/client.js + if (pending === void 0) { + setBusy(""); + return; -+ } + } +- } +- 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)( + void Promise.resolve(pending).then(() => { + setAnswered((value) => ({ ...value, [card.id]: true })); + const name = card.bot?.name ?? ""; @@ -7236,14 +8763,73 @@ diff --git a/client.js b/client.js + }; + const open = (card) => openCard(actions, card); + return (0, import_react13.createElement)( -+ "div", + "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)( +- "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)( + { className: "bt-mem-layer bt-inbox-layer", "data-leaving": leaving || void 0 }, + (0, import_react13.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react13.createElement)( + "div", + { className: "bt-mem bt-inbox", role: "dialog", "aria-modal": true, "aria-labelledby": "bt-inbox-heading" }, + (0, import_react13.createElement)( -+ "div", + "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 })) +- ); +- }) + { className: "bt-mem-head" }, + (0, import_react13.createElement)("h2", { id: "bt-inbox-heading" }, t("Inbox")), + (0, import_react13.createElement)("span", { className: "bt-mem-status" }, t("Your Bots")), @@ -7276,8 +8862,19 @@ diff --git a/client.js b/client.js + )) + ) + ) -+ ); -+} + ); + } +-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); +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)} @@ -7341,8 +8938,18 @@ diff --git a/client.js b/client.js + key, + id: String(task.id), + title: String(task.title ?? "") -+ }; -+} + }; +- visit(root); +- return calls; + } +-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]); + +// src/client/board-card.js +var import_react14 = require("react"); @@ -7405,7 +9012,51 @@ diff --git a/client.js b/client.js + footer, + actions + ); -+} + } +-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 }); + +// src/client/kanban.js +var import_react15 = require("react"); @@ -7484,7 +9135,11 @@ 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); -+ } + } +- 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 }); + }; + 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 })); @@ -7543,7 +9198,16 @@ diff --git a/client.js b/client.js + if (!map.has(id)) { + map.set(id, []); + order.push(id); -+ } + } +- 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; + map.get(id).push(task); + } + return order.map((id) => ({ id, name: botName(roster, map.get(id)[0]) || id, tasks: map.get(id) })); @@ -7680,7 +9344,11 @@ diff --git a/client.js b/client.js + const id = event.dataTransfer?.getData("text/plain"); + if (id) onMove?.(id, column); + onDrag?.(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 })); + }, + (0, import_react15.createElement)( + "h3", @@ -7824,7 +9492,11 @@ diff --git a/client.js b/client.js + replace(await actions.taskUpdate({ id, column })); + } catch (failure) { + setError(failure?.message ?? String(failure)); -+ } + } +- 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; + }; + const create = async (title) => { + const text2 = String(title ?? "").trim(); @@ -7835,7 +9507,13 @@ diff --git a/client.js b/client.js + setDraft(""); + } catch (failure) { + setError(failure?.message ?? String(failure)); -+ } + } +- 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; +- } + }; + const name = scopeName(roster, scope); + return (0, import_react15.createElement)( @@ -7881,7 +9559,22 @@ diff --git a/client.js b/client.js + }) + ) + ); -+} + } +-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)))); +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)} @@ -7943,12 +9636,22 @@ diff --git a/client.js b/client.js + .bt-mem.bt-board{width:100vw;border-radius:0;border:0} + .bt-board-columns{flex-direction:column} + .bt-board-column{min-width:0} -+} + } +`; -+ + +-// 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/client/projects-nav.js +var import_react16 = require("react"); - var import_react_dom3 = require("react-dom"); ++var import_react_dom3 = require("react-dom"); +var call4 = (endpoint, payload) => call(`flybot/${endpoint}`, payload); +var PROJECTS_NAV_CSS = ` +.bt-projects-row { display: flex; align-items: center; gap: 8px; } @@ -7989,12 +9692,71 @@ diff --git a/client.js b/client.js + items.push({ role, label: roleText(role), current: false, pick: true }); + } + return items; -+} + } +-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); +- }); +- observer.observe(document.querySelector("[data-conversation-scroll]") ?? document.body, { childList: true, subtree: true }); +- find(); +- return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); +- }; +- }, [active]); +- return host; +function roleNote(answer) { + if (answer?.changed === true) return t("Role changed."); + const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : t("refused"); + return t("Role unchanged: {reason}", { reason }); -+} + } +-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); +- }; +- 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); +function ProjectList({ projects, error, note, onOpen, onRole }) { + const rows = projects === null ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, t("Reading Projects…")) : error !== "" ? (0, import_react16.createElement)("div", { className: "bt-projects-note" }, error) : projects.length === 0 ? (0, import_react16.createElement)("div", { className: "bt-projects-empty" }, t("No Projects yet")) : projects.map((project) => (0, import_react16.createElement)( + "button", @@ -8009,7 +9771,7 @@ diff --git a/client.js b/client.js + onContextMenu: (event) => { + event.preventDefault(); + onRole?.(project, event); -+ } + } + }, + (0, import_react16.createElement)("span", { className: "bt-project-entry-name" }, projectName(project)), + roleText(project.role) === "" ? null : (0, import_react16.createElement)("span", { className: "bt-project-entry-role" }, roleText(project.role)) @@ -8041,16 +9803,52 @@ diff --git a/client.js b/client.js + (0, import_react16.useEffect)(() => { + const onDown = (event) => { + if (!ref.current?.contains(event.target)) onClose(); -+ }; + }; +- const observer = new MutationObserver(() => { +- if (!frame) frame = requestAnimationFrame(scan); +- }); +- scan(); + const onKey = (event) => { + if (event.key === "Escape") onClose(); + }; + window.addEventListener("mousedown", onDown, true); + window.addEventListener("keydown", onKey, true); -+ return () => { + return () => { +- cancelAnimationFrame(frame); +- observer.disconnect(); + window.removeEventListener("mousedown", onDown, true); + window.removeEventListener("keydown", onKey, true); -+ }; + }; +- }, []); +-} +-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; + }, [onClose]); + const items = projectMenuItems(project); + return (0, import_react_dom3.createPortal)((0, import_react16.createElement)( @@ -8084,7 +9882,19 @@ diff --git a/client.js b/client.js + if (onOpen) { + onOpen(id); + 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 (typeof window !== "undefined") window.location.hash = projectHash(id); + }, [onOpen]); + const [expanded, setExpanded] = (0, import_react16.useState)(false); @@ -8101,7 +9911,63 @@ diff --git a/client.js b/client.js + } catch (failure) { + setProjects([]); + setError(failure?.message ?? String(failure)); -+ } + } +- 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)( +- "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); + }, [read]); + (0, import_react16.useEffect)(() => { + if (!expanded) return; @@ -8152,9 +10018,11 @@ diff --git a/client.js b/client.js + } + }) + ); -+} -+ -+// src/client/sidebar.js + } + + // src/client/sidebar.js +-var import_react8 = require("react"); +-var import_react_dom3 = require("react-dom"); +var import_react17 = require("react"); +var import_react_dom4 = require("react-dom"); var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives"); @@ -8217,7 +10085,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4785,55 +4785,131 @@ +@@ -4785,55 +4785,131 @@ function BotRow({ bot, roster, actions, useSessions, useSessionStatus, useSessio onClick: () => actions.openSession(id), onContextMenu }, @@ -8364,7 +10232,7 @@ diff --git a/client.js b/client.js "data-room-id": id, onClick: () => actions.openSession(id), onContextMenu: (event) => { -@@ -4841,20 +4841,20 @@ +@@ -4841,20 +4841,20 @@ function RoomRow({ room: room2, roster, actions, useSessions, useSessionStatus, actions.openMenu({ kind: "room", id, x: event.clientX, y: event.clientY }); } }, @@ -8393,7 +10261,7 @@ diff --git a/client.js b/client.js if (!renaming) return; input.current?.focus(); input.current?.select(); -@@ -4875,10 +4875,10 @@ +@@ -4875,10 +4875,10 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { } })(); }; @@ -8406,7 +10274,7 @@ diff --git a/client.js b/client.js ref: input, defaultValue: session.title, "aria-label": t("Rename"), -@@ -4891,7 +4891,7 @@ +@@ -4891,7 +4891,7 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { }) ); } @@ -8415,7 +10283,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4905,8 +4905,8 @@ +@@ -4905,8 +4905,8 @@ function AgentSessionRow({ session, agent, actions, renaming, index }) { actions.openMenu({ kind: "agent-session", id: session.id, agentId: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -8426,7 +10294,7 @@ diff --git a/client.js b/client.js ); } function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, marked }) { -@@ -4941,10 +4941,10 @@ +@@ -4941,10 +4941,10 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, const preview = sessions[0]?.title ?? t("New session"); const label = ["DSH Agent", running ? t("Working") : "", unread ? t("Unread activity") : ""].filter(Boolean).join(", "); const open = active && wide; @@ -8439,7 +10307,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -4959,30 +4959,30 @@ +@@ -4959,30 +4959,30 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, actions.openMenu({ kind: "agent", id: agent.id, x: event.clientX, y: event.clientY }); } }, @@ -8481,7 +10349,7 @@ diff --git a/client.js b/client.js key: session.id, session, agent, -@@ -4996,8 +4996,8 @@ +@@ -4996,8 +4996,8 @@ function AgentRow({ agent, actions, useSessions, useSessionStatus, useUi, wide, } var GLIDE = springEasing(1e3, 63); function useListGlide(listRef, layout, key) { @@ -8492,7 +10360,7 @@ diff --git a/client.js b/client.js const list = listRef.current; if (!list) return; const next = /* @__PURE__ */ new Map(); -@@ -5014,9 +5014,44 @@ +@@ -5014,9 +5014,44 @@ function useListGlide(listRef, layout, key) { tops.current = { layout, map: next }; }, [layout, key]); } @@ -8538,7 +10406,7 @@ diff --git a/client.js b/client.js const roster = useRoster((value) => value); const markedUnread = useUi((value) => value.unread); props.useRegistry((value) => value); -@@ -5025,17 +5025,26 @@ +@@ -5025,17 +5025,26 @@ function TeamSidebar(props) { for (const id of list.ids) stamps[id] = list.byId[id]?.updatedAt ?? 0; return JSON.stringify(stamps); }); @@ -8569,7 +10437,7 @@ diff --git a/client.js b/client.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 }; -@@ -5045,39 +5045,43 @@ +@@ -5045,39 +5045,43 @@ function TeamSidebar(props) { event.preventDefault(); actions.openPalette(event.key); }; @@ -8633,7 +10501,7 @@ diff --git a/client.js b/client.js 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; -@@ -5116,14 +5116,14 @@ +@@ -5116,14 +5116,14 @@ function UpdateButton({ useUpdate, actions }) { setDshError(""); void actions.installDshUpdate().catch((failure) => setDshError(hostText(failure?.message ?? String(failure)))); }; @@ -8653,7 +10521,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-round bt-update", -@@ -5133,21 +5133,21 @@ +@@ -5133,21 +5133,21 @@ function UpdateButton({ useUpdate, actions }) { "aria-haspopup": "dialog", "aria-expanded": open, onClick: toggle @@ -8686,7 +10554,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: phase === "installing", -@@ -5155,17 +5155,17 @@ +@@ -5155,17 +5155,17 @@ function UpdateButton({ useUpdate, actions }) { }, phase === "installing" ? t("Updating…") : phase === "failed" ? t("Try again") : t("Update")) ) ) : null, @@ -8711,7 +10579,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-update-go", disabled: dshPhase === "installing", -@@ -5176,61 +5176,25 @@ +@@ -5176,61 +5176,25 @@ function UpdateButton({ useUpdate, actions }) { ), document.body) : null ); } @@ -8783,7 +10651,7 @@ diff --git a/client.js b/client.js ); } var openShellSettings = null; -@@ -5259,6 +5259,18 @@ +@@ -5259,6 +5259,18 @@ function openHarnessOnboarding(id) { else openHarnessSettings(); }); } @@ -8802,7 +10670,7 @@ diff --git a/client.js b/client.js function openThroughMenu(trigger) { const before = new Set(document.querySelectorAll('[role="menu"]')); trigger.click(); -@@ -5288,32 +5288,32 @@ +@@ -5288,32 +5288,32 @@ function pressSettingsShortcut() { })); } function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboarding, actions }) { @@ -8843,7 +10711,7 @@ diff --git a/client.js b/client.js 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; -@@ -5341,30 +5341,32 @@ +@@ -5341,30 +5341,32 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi const groups = [ [ [t("Usage"), GaugeIcon, () => actions.openSettings("usage")], @@ -8888,7 +10756,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -5372,7 +5372,7 @@ +@@ -5372,7 +5372,7 @@ function AccountLauncher({ seat = true, settingsOpen, openSettings, openOnboardi setOpen(false); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -8897,7 +10765,7 @@ diff --git a/client.js b/client.js )) ), document.body) : null ); -@@ -5382,8 +5382,8 @@ +@@ -5382,8 +5382,8 @@ function useLandOnMainBot(roster, useSessions, actions) { const row = Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0); return row ? `${row.id}|${row.blank ? 1 : 0}` : ""; }); @@ -8908,7 +10776,7 @@ diff --git a/client.js b/client.js if (!roster.ready || !roster.mainBotId) return void 0; const [id, blank3] = mainView.split("|"); const bot = id === "" ? void 0 : roster.byId[id]; -@@ -5397,40 +5397,40 @@ +@@ -5397,40 +5397,40 @@ function useLandOnMainBot(roster, useSessions, actions) { return void 0; } const last = lastBot.current; @@ -8959,7 +10827,7 @@ diff --git a/client.js b/client.js } var SIDEBAR_CSS = ` .bt-side-top .bt-brand{cursor:default;padding-left:6px} -@@ -5456,7 +5456,7 @@ +@@ -5456,7 +5456,7 @@ var SIDEBAR_CSS = ` .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)} @@ -8968,7 +10836,7 @@ diff --git a/client.js b/client.js @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)} -@@ -5491,18 +5491,140 @@ +@@ -5491,18 +5491,140 @@ var SIDEBAR_CSS = ` .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}} @@ -9114,7 +10982,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -5512,19 +5512,19 @@ function useMemoryNotice(ids, useMemoryNews) { return shown; } function MemoryNotice({ shown }) { @@ -9140,7 +11008,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -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) : ""; }); @@ -9149,7 +11017,7 @@ diff --git a/client.js b/client.js 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"); -@@ -5548,52 +5548,41 @@ +@@ -5548,52 +5548,41 @@ function HeaderPill({ sessionId, useRoster, useSessions, useSessionStatus, useSe const bot = roster.byId[sessionId]; const room2 = roster.roomsById[sessionId]; const detailsOpen = useUi((state) => state.details === sessionId); @@ -9199,15 +11067,14 @@ diff --git a/client.js b/client.js - (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)) -- ), -- (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 }) + bot ? (0, import_react19.createElement)(BotAvatar, { bot, size: 24, state: botState(running, doing, roster.questions?.[currentPart(bot)] !== void 0), badge: false }) : room2 ? (0, import_react19.createElement)(RoomAvatar, { room: room2, roster, size: 24 }) : null, + (0, import_react19.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_react19.createElement)(MemoryNotice, { shown: memoryNotice }), + (0, import_react19.createElement)("span", { className: "bt-pill-go", "aria-hidden": true }, (0, import_react19.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_react19.createElement)("span", { className: "bt-sr", role: "status" }, memoryNotice ? t("Memory updated") : ""), + (0, import_react19.createElement)(NewMessagePills, { key: sessionId, sessionId, hiddenTurns, useSessions, useUi, useTurnClock, actions }), + (0, import_react19.createElement)(ActivityIndicator, { key: `activity:${sessionId}`, sessionId, useRoster, useSessionStatus, useSession, useLive, actions }), @@ -9222,7 +11089,7 @@ diff --git a/client.js b/client.js let frame = 0; let watched = null; const observer = new MutationObserver(() => schedule()); -@@ -5642,7 +5642,7 @@ +@@ -5642,7 +5642,7 @@ function useTranscriptView() { }, []); return view; } @@ -9231,7 +11098,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -5658,18 +5658,22 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { }); const view = useTranscriptView(); const unpinned = view?.unpinned === true; @@ -9259,7 +11126,7 @@ diff --git a/client.js b/client.js const scroller = document.querySelector("[data-conversation-scroll]"); if (!scroller) return void 0; scroller.toggleAttribute("data-bt-news", showDown); -@@ -5677,16 +5677,16 @@ +@@ -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"); @@ -9280,7 +11147,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-news-x", "aria-label": t("Dismiss new messages"), -@@ -5694,7 +5694,7 @@ +@@ -5694,7 +5694,7 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { event.stopPropagation(); onDismiss(); } @@ -9289,23 +11156,32 @@ diff --git a/client.js b/client.js ); const toLatest = () => { const scroller = document.querySelector("[data-conversation-scroll]"); -@@ -5703,9 +5703,9 @@ +@@ -5702,31 +5702,147 @@ function NewMessagePills({ sessionId, useSessions, useUi, useTurnClock }) { + 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" }); +- 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 +- ), +- document.body +- ); ++ const toDivider = () => document.querySelector("[data-conversation-scroll] .bt-new-sep")?.scrollIntoView({ block: "center", behavior: "smooth" }); + return (0, import_react_dom6.createPortal)( + (0, import_react19.createElement)( + import_react19.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 @@ - ); - } - ++ 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/composer-label.js +var ATTRIBUTES = ["aria-label", "data-placeholder"]; +function holdComposerLabel(doc, label) { @@ -9420,8 +11296,8 @@ diff --git a/client.js b/client.js + for (const held of slots.values()) held.off(); + slots.clear(); + }; -+} -+ + } + // src/client/composer.js -var import_react10 = require("react"); -var import_react_dom5 = require("react-dom"); @@ -9446,7 +11322,7 @@ diff --git a/client.js b/client.js if (!note) return void 0; const timer = setTimeout(() => setNote(""), 2600); return () => clearTimeout(timer); -@@ -5745,10 +5745,10 @@ +@@ -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]; @@ -9460,7 +11336,7 @@ diff --git a/client.js b/client.js } if (interim) latest.current(interim, false); }; -@@ -5774,19 +5774,38 @@ +@@ -5774,19 +5774,38 @@ function useDictation(onText) { const stop = () => recognition.current?.stop(); return { listening, note, start, stop }; } @@ -9504,7 +11380,7 @@ diff --git a/client.js b/client.js const box = document.querySelector('[class*="_centerCol"] [role=textbox][contenteditable=true]'); if (box instanceof HTMLElement) { box.focus(); -@@ -5797,10 +5797,10 @@ +@@ -5797,10 +5797,10 @@ function ComposerVoice({ sessionId, inputActions, actions, useUi }) { }, [quote?.token]); if (sessionId === void 0) return null; const pluginMic = actions.speech() !== void 0; @@ -9518,7 +11394,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mic", "aria-label": dictation.listening ? t("Stop dictation") : t("Dictate"), -@@ -5808,16 +5808,16 @@ +@@ -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() @@ -9539,7 +11415,7 @@ diff --git a/client.js b/client.js ); } function menuIcons(roster) { -@@ -5825,17 +5825,17 @@ +@@ -5825,17 +5825,17 @@ function menuIcons(roster) { return (row) => { if (!row.bot) return PeopleIcon; const id = row.bot.id; @@ -9562,7 +11438,7 @@ diff --git a/client.js b/client.js if (!open) return void 0; const onDown = (event) => { if (![button.current, menu.current].some((node) => node?.contains(event.target))) setOpen(null); -@@ -5876,10 +5876,10 @@ +@@ -5876,10 +5876,10 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { actions.referenceChat(sessionId, inputActions?.captureInsertion?.()); }; const canReference = typeof inputActions?.captureInsertion === "function" && actions.canReferenceChat(); @@ -9576,7 +11452,7 @@ diff --git a/client.js b/client.js ref: button, type: "button", className: "bt-plus", -@@ -5889,8 +5889,8 @@ +@@ -5889,8 +5889,8 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { "aria-expanded": Boolean(open), onMouseDown: keepComposerFocus, onClick: toggle @@ -9587,7 +11463,7 @@ diff --git a/client.js b/client.js "div", { ref: menu, -@@ -5900,9 +5900,9 @@ +@@ -5900,9 +5900,9 @@ function ComposerPlus({ sessionId, inputActions, useRoster, actions }) { style: { left: shown.left, bottom: shown.bottom }, "data-leaving": leaving || void 0 }, @@ -9600,7 +11476,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -5922,15 +5922,57 @@ +@@ -5922,15 +5922,57 @@ var conversationOf = (roster, sessionId) => { function mentionRows(roster, sessionId, query = "") { const here = conversationOf(roster, sessionId); if (here === null) return null; @@ -9662,7 +11538,7 @@ diff --git a/client.js b/client.js function chatRows(roster, list, sessionId) { const here = conversationOf(roster, sessionId); if (here === null) return []; -@@ -5959,14 +5959,31 @@ +@@ -5959,14 +5959,31 @@ function atSources({ roster, sessions, launched, iconFor }) { order: -20, showGroupTitle: false, async candidates(session, { query }) { @@ -9698,7 +11574,7 @@ diff --git a/client.js b/client.js }, onPick: ({ candidate }) => candidate.value ? { text: candidate.value } : void 0 }; -@@ -5995,6 +5995,83 @@ +@@ -5995,6 +5995,83 @@ function atSources({ roster, sessions, launched, iconFor }) { return [bots, chats]; } @@ -9782,7 +11658,7 @@ diff --git a/client.js b/client.js // 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 @@ +@@ -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); @@ -9793,7 +11669,7 @@ diff --git a/client.js b/client.js }; ascii(0, "RIFF"); view.setUint32(4, 36 + samples.length * 2, true); -@@ -6030,9 +6030,9 @@ +@@ -6030,9 +6030,9 @@ function waveBytes(samples, rate = 16e3) { return bytes; } function base64Of(bytes) { @@ -9806,7 +11682,7 @@ diff --git a/client.js b/client.js } var captureError = (kind, cause) => Object.assign(new Error(kind), { kind, cause }); async function openCapture() { -@@ -6121,233 +6121,9 @@ +@@ -6121,233 +6121,9 @@ function turnDetector({ loud = 0.12, quiet = 0.06, quietMs = 1200 } = {}) { }; } @@ -10042,17 +11918,26 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS = 120; function groupModels(models) { const groups = /* @__PURE__ */ new Map(); -@@ -6367,17 +6367,10 @@ - }); - } - var servedOf = (roster, ref) => (roster.models ?? []).find((model) => model.ref === ref); +@@ -6362,22 +6362,15 @@ function groupModels(models) { + 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 }; --} ++ 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); var effortCache = /* @__PURE__ */ new Map(); function useEfforts(served, ref, actions) { - const [fetched, setFetched] = (0, import_react13.useState)(void 0); @@ -10062,7 +11947,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -6402,12 +6402,12 @@ function effortRows(reasoning) { ]; } function EffortRows({ reasoning, current, onPick }) { @@ -10080,7 +11965,7 @@ diff --git a/client.js b/client.js "button", { key: row.effort ?? "default", -@@ -6416,17 +6416,17 @@ +@@ -6416,17 +6416,17 @@ function EffortRows({ reasoning, current, onPick }) { "aria-checked": row.effort === current, onClick: () => onPick(row.effort) }, @@ -10104,7 +11989,7 @@ diff --git a/client.js b/client.js "button", { key: model.ref, -@@ -6436,20 +6436,20 @@ +@@ -6436,20 +6436,20 @@ function ModelRows({ roster, current, onPick }) { title: model.ref, onClick: () => onPick(model.ref) }, @@ -10133,7 +12018,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -6484,6 +6484,7 @@ function useAnchoredMenu() { return { isOpen: place !== null, shownPlace, leaving, button, menu, open, close }; } function ModelMenu({ bot, roster, actions, run, wide = false, onPick, onPickEffort }) { @@ -10141,7 +12026,7 @@ diff --git a/client.js b/client.js const { isOpen, shownPlace, leaving, button, menu, open, close } = useAnchoredMenu(); const shown = modelOf(bot, roster); const current = bot.model ?? bot.appliedModel; -@@ -6511,10 +6511,10 @@ +@@ -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 models = roster.models ?? []; @@ -10155,7 +12040,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6527,37 +6527,38 @@ +@@ -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 }, @@ -10208,7 +12093,7 @@ diff --git a/client.js b/client.js 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 @@ +@@ -6569,13 +6569,13 @@ function ModelChoiceDock({ sessionId, useRoster, actions }) { void actions.refreshModels?.(); }).finally(() => setBusy("")); }; @@ -10227,7 +12112,7 @@ diff --git a/client.js b/client.js key: model.ref, type: "button", className: "bt-model-chip", -@@ -6584,50 +6584,115 @@ +@@ -6584,50 +6584,115 @@ 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) @@ -10359,7 +12244,7 @@ diff --git a/client.js b/client.js ...ops, ...hooks, t: actions.accountT(), -@@ -6639,6 +6639,16 @@ +@@ -6639,6 +6639,16 @@ function SignInHost({ entry, actions }) { }); } var MODEL_CSS = ` @@ -10376,7 +12261,7 @@ diff --git a/client.js b/client.js .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} -@@ -6675,18 +6675,96 @@ +@@ -6675,18 +6675,96 @@ var MODEL_CSS = ` @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}} `; @@ -10481,7 +12366,7 @@ diff --git a/client.js b/client.js const fallback = group ? t("Each Bot's own folder") : t("Team workspace"); const toggle = () => { if (isOpen) { -@@ -6719,35 +6719,35 @@ +@@ -6719,35 +6719,35 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } }; const known = info?.folders ?? []; const folders = value && !known.some((folder) => folder.path === value) ? [{ path: value, title: folderName(value) }, ...known] : known; @@ -10533,7 +12418,7 @@ diff --git a/client.js b/client.js className: "bt-folder-input", autoFocus: true, value: draft, -@@ -6759,33 +6759,33 @@ +@@ -6759,33 +6759,33 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } if (event.key === "Enter" && !event.nativeEvent.isComposing) typed(); } }), @@ -10583,7 +12468,7 @@ diff --git a/client.js b/client.js "button", { ref: button, -@@ -6798,19 +6798,19 @@ +@@ -6798,19 +6798,19 @@ function FolderMenu({ value, group = false, actions, run, onPick, wide = false } title: value, onClick: toggle }, @@ -10609,7 +12494,7 @@ diff --git a/client.js b/client.js ), document.body) : null ); } -@@ -6830,8 +6830,8 @@ +@@ -6830,8 +6830,8 @@ var FOLDER_CSS = ` `; // src/client/schedules.js @@ -10620,7 +12505,7 @@ diff --git a/client.js b/client.js var shared = null; var listeners = /* @__PURE__ */ new Set(); function publish(view) { -@@ -6839,8 +6839,8 @@ +@@ -6839,8 +6839,8 @@ function publish(view) { for (const listener of listeners) listener(view); } function useSchedules(actions) { @@ -10631,7 +12516,7 @@ diff --git a/client.js b/client.js listeners.add(setView); let stopped = false; let timer; -@@ -6848,7 +6848,7 @@ +@@ -6848,7 +6848,7 @@ function useSchedules(actions) { if (!stopped) publish(next); }).catch(() => { }).finally(() => { @@ -10640,7 +12525,7 @@ diff --git a/client.js b/client.js }); load(); return () => { -@@ -6873,7 +6873,8 @@ +@@ -6873,7 +6873,8 @@ function weekdayNames(days) { 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 }); @@ -10650,7 +12535,7 @@ diff --git a/client.js b/client.js } function timingText(task) { const zone = task.timeZone && task.timeZone !== localZone() ? ` (${task.timeZone})` : ""; -@@ -6887,8 +6887,8 @@ +@@ -6887,8 +6887,8 @@ function timingText(task) { return `${t("Every day at {time}", { time: clock2(task.time) })}${zone}`; case "weekly": { const days = (task.weekdays ?? []).join(); @@ -10661,7 +12546,7 @@ diff --git a/client.js b/client.js } case "cron": return `${t("On the schedule {expression}", { expression: task.expression })}${zone}`; -@@ -6896,11 +6896,25 @@ +@@ -6896,11 +6896,25 @@ function timingText(task) { return ""; } } @@ -10692,7 +12577,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -6913,20 +6913,49 @@ +@@ -6913,20 +6913,49 @@ function TaskRow({ task, actions, onView, showBot, roster }) { setBusy(true); void actions.scheduleDelete(task.sessionId, task.id).then(onView, () => setBusy(false)); }; @@ -10751,7 +12636,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mini bt-task-remove", "data-confirm": confirming || void 0, -@@ -6934,7 +6934,7 @@ +@@ -6934,7 +6934,7 @@ function TaskRow({ task, actions, onView, showBot, roster }) { "aria-label": confirming ? t("Click again to delete") : t("Delete task"), title: confirming ? void 0 : t("Delete task"), onClick: remove @@ -10760,7 +12645,7 @@ diff --git a/client.js b/client.js ); } var pad = (number) => String(number).padStart(2, "0"); -@@ -6944,27 +6944,49 @@ +@@ -6944,27 +6944,49 @@ function inAnHour() { 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"]]; @@ -10827,7 +12712,7 @@ diff --git a/client.js b/client.js setBusy(true); setError(""); let when3; -@@ -6975,151 +6975,298 @@ +@@ -6975,151 +6975,298 @@ function TaskForm({ roster, actions, start, onDone, onCancel }) { setBusy(false); return; } @@ -11188,7 +13073,7 @@ diff --git a/client.js b/client.js .bt-task[data-done]{opacity:.6} .bt-task[data-busy]{opacity:.4} .bt-task-title{font-size:13px;line-height:18px;font-weight:500;overflow-wrap:anywhere} -@@ -7127,6 +7127,12 @@ +@@ -7127,6 +7127,12 @@ var SCHEDULE_CSS = ` .bt-task-next{flex:none;font-size:12px;line-height:16px;color:var(--bt-ink-3);font-variant-numeric:tabular-nums;white-space:nowrap} .bt-task-remove{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:26px;color:var(--bt-ink-3)} .bt-task-remove svg{width:14px;height:14px} @@ -11201,7 +13086,7 @@ diff --git a/client.js b/client.js .bt-task-remove[data-confirm]{background:rgba(224,33,53,.1);color:#e02135} .bt-task-head{display:flex;align-items:center;gap:6px} .bt-task-form{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} -@@ -7158,12 +7158,12 @@ +@@ -7158,12 +7158,12 @@ var SCHEDULE_CSS = ` `; // src/client/capability-settings.js @@ -11217,7 +13102,7 @@ diff --git a/client.js b/client.js } catch { throw new Error(t("{label} must be valid JSON.", { label })); } -@@ -7172,11 +7172,11 @@ +@@ -7172,11 +7172,11 @@ var parseMap = (text, label) => { }; var statusText = (status) => !status ? void 0 : status.state === "ready" ? t("Ready") : status.state === "needs-approval" ? t("Needs approval") : status.state === "error" ? t("Could not connect: {error}", { error: status.error ?? "" }) : t("Connecting…"); function Field({ label, value, onChange, multiline = false, disabled = false, placeholder }) { @@ -11232,7 +13117,7 @@ diff --git a/client.js b/client.js className: multiline ? "bt-input bt-input-area" : "bt-input", value, disabled, -@@ -7188,7 +7188,7 @@ +@@ -7188,7 +7188,7 @@ function Field({ label, value, onChange, multiline = false, disabled = false, pl ); } function CapabilityForm({ draft, setDraft, items, actions, onClose }) { @@ -11241,7 +13126,7 @@ diff --git a/client.js b/client.js const set = (key, value) => setDraft((previous) => ({ ...previous, [key]: value })); const submit = async (event) => { event.preventDefault(); -@@ -7199,7 +7199,7 @@ +@@ -7199,7 +7199,7 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { if (draft.kind === "mcp" && draft.transport === "stdio") { payload.transport = "stdio"; payload.command = draft.command; @@ -11250,7 +13135,7 @@ diff --git a/client.js b/client.js payload.env = parseMap(draft.envText, t("Environment")); } else if (draft.kind === "mcp") { payload.transport = "streamable-http"; -@@ -7219,95 +7219,95 @@ +@@ -7219,95 +7219,95 @@ function CapabilityForm({ draft, setDraft, items, actions, onClose }) { } }; const available = items.filter((item) => item.name !== draft.name && ["mcp", "skill"].includes(item.kind)); @@ -11386,7 +13271,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(""), 4e3); return () => clearTimeout(timer); -@@ -7330,20 +7330,20 @@ +@@ -7330,20 +7330,20 @@ function CapabilitiesPage({ roster, actions, run }) { setConfirming(""); void run(actions.deleteCapability(item.name)); }; @@ -11414,23 +13299,34 @@ diff --git a/client.js b/client.js key: item.name, item, confirming: confirming === item.name, -@@ -7359,10 +7359,10 @@ - const attached = new Set(bot.capabilities ?? []); - const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; - if (items.length === 0) return null; +@@ -7354,27 +7354,73 @@ function CapabilitiesPage({ roster, actions, run }) { + ) + ); + } +-function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { +- const items = [...roster.capabilities ?? []].sort((a, b) => Number(b.kind === "pack") - Number(a.kind === "pack") || a.name.localeCompare(b.name)); +- const attached = new Set(bot.capabilities ?? []); +- const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; +- if (items.length === 0) return null; - return (0, import_react16.createElement)( ++function BotCapabilitiesCard({ bot, roster, actions, run, Card: Card2, SwitchRow: SwitchRow2 }) { ++ const items = [...roster.capabilities ?? []].sort((a, b) => Number(b.kind === "pack") - Number(a.kind === "pack") || a.name.localeCompare(b.name)); ++ const attached = new Set(bot.capabilities ?? []); ++ const statuses = roster.capabilityStatuses?.[bot.id] ?? {}; ++ if (items.length === 0) return null; + return (0, import_react25.createElement)( - Card2, - { title: t("Capabilities") }, -- items.map((item) => (0, import_react16.createElement)(SwitchRow2, { ++ Card2, ++ { title: t("Capabilities") }, + items.map((item) => (0, import_react25.createElement)(SwitchRow2, { - key: item.name, - label: item.name, - hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, -@@ -7372,9 +7372,55 @@ - ); - } - ++ key: item.name, ++ label: item.name, ++ hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, ++ on: attached.has(item.name), ++ onChange: (enabled) => void run(actions.setBotCapability(bot.id, item.name, enabled)) ++ })) ++ ); ++} ++ +// src/client/connector-bots.js +var import_react26 = require("react"); +function ownBots(roster) { @@ -11467,16 +13363,24 @@ diff --git a/client.js b/client.js + const connected = connectedConnectors(roster); + if (connected.length === 0) return null; + return (0, import_react26.createElement)( -+ Card2, + Card2, +- { title: t("Capabilities") }, +- items.map((item) => (0, import_react16.createElement)(SwitchRow2, { +- key: item.name, +- label: item.name, +- hint: attached.has(item.name) ? statusText(statuses[item.name]) : void 0, +- on: attached.has(item.name), +- onChange: (enabled) => void run(actions.setBotCapability(bot.id, item.name, enabled)) +- })) + { title: t("Connectors") }, + connected.map((connector) => (0, import_react26.createElement)( + "div", + { key: connector.id, className: "bt-connector-bot" }, + (0, import_react26.createElement)(BotSwitch, { bot, connector, name: connector.name, actions, run, SwitchRow: SwitchRow2 }) + )) -+ ); -+} -+ + ); + } + // src/client/memory-panel.js -var import_react17 = require("react"); -var POLL_MS2 = 1500; @@ -11485,7 +13389,7 @@ diff --git a/client.js b/client.js var MENU_EXIT_MS2 = 120; var counts = /* @__PURE__ */ new Map(); var countListeners = /* @__PURE__ */ new Set(); -@@ -7384,13 +7384,13 @@ +@@ -7384,13 +7384,13 @@ function noteCount(botId, count) { for (const listener of countListeners) listener(); } function useMemoryCount(botId, actions) { @@ -11502,7 +13406,7 @@ diff --git a/client.js b/client.js if (!stop) noteCount(botId, view.entries.length); }).catch(() => { }); -@@ -7404,16 +7404,16 @@ +@@ -7404,16 +7404,16 @@ function useMemoryCount(botId, actions) { function MemoryRow({ bot, actions }) { const count = useMemoryCount(bot.id, actions); const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); @@ -11525,7 +13429,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7428,45 +7428,45 @@ +@@ -7428,45 +7428,45 @@ function ago(at, now) { return format.format(-Math.round(hours / 24), "day"); } function Skeleton({ label }) { @@ -11590,7 +13494,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -7478,9 +7478,9 @@ +@@ -7478,9 +7478,9 @@ var groupEntries = (entries2) => entries2.reduce((groups, entry) => { return groups; }, []); function EntryRow({ entry, remove }) { @@ -11603,7 +13507,7 @@ diff --git a/client.js b/client.js if (!confirming) return void 0; const timer = setTimeout(() => setConfirming(false), 3e3); return () => clearTimeout(timer); -@@ -7495,16 +7495,16 @@ +@@ -7495,16 +7495,16 @@ function EntryRow({ entry, remove }) { setBusy(true); if (!await remove(entry)) setBusy(false); }; @@ -11625,7 +13529,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-mem-remove", "data-confirm": confirming || void 0, -@@ -7512,7 +7512,7 @@ +@@ -7512,7 +7512,7 @@ function EntryRow({ entry, remove }) { "aria-label": confirming ? t("Click again to remove") : t("Remove from memory"), title: confirming ? void 0 : t("Remove from memory"), onClick: click @@ -11634,7 +13538,7 @@ diff --git a/client.js b/client.js ); } function Entries({ bot, view, remove }) { -@@ -7521,82 +7521,82 @@ +@@ -7521,82 +7521,82 @@ function Entries({ bot, view, remove }) { { scope: "own", title: t("{name}'s memory", { name: bot.name }), size: view.sizes.own }, { scope: "team", title: t("Team memory"), hint: t("Every Bot reads it"), size: view.sizes.team } ]; @@ -11752,35 +13656,30 @@ diff --git a/client.js b/client.js alive.current = true; void load({ summarize: true }); return () => { -@@ -7606,9 +7606,9 @@ +@@ -7606,38 +7606,331 @@ function useMemoryView(botId, actions) { }, [botId]); return { view, error, load, show }; } -function useNow(every) { - const [now, setNow] = (0, import_react17.useState)(Date.now()); - (0, import_react17.useEffect)(() => { +- const id = setInterval(() => setNow(Date.now()), every); +- return () => clearInterval(id); +- }, [every]); +- return now; +function useNow3(every) { + const [now, setNow] = (0, import_react27.useState)(Date.now()); + (0, import_react27.useEffect)(() => { - const id = setInterval(() => setNow(Date.now()), every); - return () => clearInterval(id); - }, [every]); -@@ -7616,18 +7616,18 @@ - } - function MemoryDialog({ bot, actions, leaving = false }) { - const { view, error, load, show } = useMemoryView(bot.id, actions); -- const [page, setPage] = (0, import_react17.useState)("summary"); -- const [menu, setMenu] = (0, import_react17.useState)(false); ++ const id = setInterval(() => setNow(Date.now()), every); ++ return () => clearInterval(id); ++ }, [every]); ++ return now; ++} ++function MemoryDialog({ bot, actions, leaving = false }) { ++ const { view, error, load, show } = useMemoryView(bot.id, actions); + const [page, setPage] = (0, import_react27.useState)("summary"); + const [menu, setMenu] = (0, import_react27.useState)(false); - const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); -- const [text, setText] = (0, import_react17.useState)(""); -- const [answer, setAnswer] = (0, import_react17.useState)(null); -- const menuRef = (0, import_react17.useRef)(null); -- const dotsRef = (0, import_react17.useRef)(null); -- const inputRef = (0, import_react17.useRef)(null); -- const now = useNow(3e4); -- const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); ++ const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); + const [text2, setText] = (0, import_react27.useState)(""); + const [answer, setAnswer] = (0, import_react27.useState)(null); + const menuRef = (0, import_react27.useRef)(null); @@ -11788,46 +13687,79 @@ diff --git a/client.js b/client.js + const inputRef = (0, import_react27.useRef)(null); + const now = useNow3(3e4); + const close = (0, import_react27.useCallback)(() => actions.closeMemory(), []); - useEscape(close); -- (0, import_react17.useEffect)(() => { ++ useEscape(close); + (0, import_react27.useEffect)(() => { - if (!menu) return void 0; - const onDown = (event) => { - if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; -@@ -7671,7 +7671,7 @@ - }; - const ask = async (event) => { - event.preventDefault(); -- const asked = text.trim(); ++ if (!menu) return void 0; ++ const onDown = (event) => { ++ if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; ++ setMenu(false); ++ }; ++ const onKey = (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ setMenu(false); ++ dotsRef.current?.focus(); ++ } ++ }; ++ window.addEventListener("mousedown", onDown); ++ window.addEventListener("keydown", onKey, true); ++ return () => { ++ window.removeEventListener("mousedown", onDown); ++ window.removeEventListener("keydown", onKey, true); ++ }; ++ }, [menu]); ++ const regenerate = () => { ++ setMenu(false); ++ setPage("summary"); ++ void load({ regenerate: true }); ++ }; ++ const openEntries = () => { ++ setMenu(false); ++ setPage("entries"); ++ }; ++ const backToSummary = () => { ++ setPage("summary"); ++ void load({ summarize: true }); ++ }; ++ const remove = async (entry) => { ++ try { ++ show(await actions.memoryForget(bot.id, entry)); ++ return true; ++ } catch (failure) { ++ setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) }); ++ return false; ++ } ++ }; ++ const ask = async (event) => { ++ event.preventDefault(); + const asked = text2.trim(); - if (asked === "" || answer?.pending) return; - setText(""); - setAnswer({ asked, pending: true }); -@@ -7689,65 +7689,65 @@ - const when3 = view?.summary ? ago(view.summary.at, now) : null; - const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; - const busy = answer?.pending === true; -- return (0, import_react17.createElement)( ++ if (asked === "" || answer?.pending) return; ++ setText(""); ++ setAnswer({ asked, pending: true }); ++ try { ++ const result = await actions.memoryAsk(bot.id, asked); ++ setAnswer({ asked, reply: result.reply, changes: result.changes }); ++ show(result.view); ++ if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" }); ++ } catch (failure) { ++ setAnswer({ asked, error: failure?.message ?? String(failure) }); ++ } ++ inputRef.current?.focus(); ++ }; ++ const count = view?.entries.length ?? 0; ++ const when3 = view?.summary ? ago(view.summary.at, now) : null; ++ const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; ++ const busy = answer?.pending === true; + return (0, import_react27.createElement)( - "div", - { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, -- (0, import_react17.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), -- (0, import_react17.createElement)( ++ "div", ++ { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, + (0, import_react27.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), + (0, import_react27.createElement)( - "div", - { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, -- (0, import_react17.createElement)( ++ "div", ++ { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, + (0, import_react27.createElement)( - "div", - { className: "bt-mem-head" }, -- page === "entries" ? (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react17.createElement)(ChevronLeftIcon)) : null, -- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), -- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), -- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }), -- (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)), -- (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)), -- menuShown ? (0, import_react17.createElement)( ++ "div", ++ { className: "bt-mem-head" }, + page === "entries" ? (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react27.createElement)(ChevronLeftIcon)) : null, + (0, import_react27.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), + (0, import_react27.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), @@ -11835,71 +13767,55 @@ diff --git a/client.js b/client.js + (0, import_react27.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react27.createElement)(DotsIcon)), + (0, import_react27.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react27.createElement)(CloseIcon)), + menuShown ? (0, import_react27.createElement)( - "div", - { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, -- (0, import_react17.createElement)( ++ "div", ++ { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, + (0, import_react27.createElement)( - "button", - { type: "button", role: "menuitem", onClick: openEntries }, -- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("All entries")), -- (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, String(count)) ++ "button", ++ { type: "button", role: "menuitem", onClick: openEntries }, + (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("All entries")), + (0, import_react27.createElement)("span", { className: "bt-menu-hint" }, String(count)) - ), -- (0, import_react17.createElement)( ++ ), + (0, import_react27.createElement)( - "button", - { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, -- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) ++ "button", ++ { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, + (0, import_react27.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) - ) - ) : null - ), -- (0, import_react17.createElement)( ++ ) ++ ) : null ++ ), + (0, import_react27.createElement)( - "div", - { className: "bt-mem-body" }, -- error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, -- view === null ? error ? null : (0, import_react17.createElement)(Skeleton) : page === "entries" ? (0, import_react17.createElement)(Entries, { bot, view, remove }) : (0, import_react17.createElement)(Summary, { bot, view, regenerate }) ++ "div", ++ { className: "bt-mem-body" }, + error ? (0, import_react27.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, + view === null ? error ? null : (0, import_react27.createElement)(Skeleton) : page === "entries" ? (0, import_react27.createElement)(Entries, { bot, view, remove }) : (0, import_react27.createElement)(Summary, { bot, view, regenerate }) - ), -- (0, import_react17.createElement)( ++ ), + (0, import_react27.createElement)( - "div", - { className: "bt-mem-foot" }, -- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, -- (0, import_react17.createElement)( ++ "div", ++ { className: "bt-mem-foot" }, + answer ? (0, import_react27.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, + (0, import_react27.createElement)( - "form", - { className: "bt-mem-ask", onSubmit: ask }, -- (0, import_react17.createElement)("input", { ++ "form", ++ { className: "bt-mem-ask", onSubmit: ask }, + (0, import_react27.createElement)("input", { - ref: inputRef, - className: "bt-mem-input", -- value: text, ++ ref: inputRef, ++ className: "bt-mem-input", + value: text2, - maxLength: 2e3, - disabled: view === null, - placeholder: t("Ask or update"), - "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), - onChange: (event) => setText(event.target.value) - }), -- (0, import_react17.createElement)( ++ maxLength: 2e3, ++ disabled: view === null, ++ placeholder: t("Ask or update"), ++ "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), ++ onChange: (event) => setText(event.target.value) ++ }), + (0, import_react27.createElement)( - "button", -- { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, -- busy ? (0, import_react17.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react17.createElement)(ArrowUpIcon) ++ "button", + { type: "submit", className: "bt-mem-send", disabled: busy || text2.trim() === "", "aria-label": t("Send") }, + busy ? (0, import_react27.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react27.createElement)(ArrowUpIcon) - ) - ) - ) -@@ -7755,8 +7755,40 @@ - ); - } - ++ ) ++ ) ++ ) ++ ) ++ ); ++} ++ +// src/client/standing-approvals.js +var import_react28 = require("react"); +var standingOf = (target, roster) => { @@ -11932,341 +13848,440 @@ diff --git a/client.js b/client.js + ); +} + - // src/client/bot-settings.js --var import_react18 = require("react"); ++// src/client/bot-settings.js +var import_react29 = require("react"); - var capitalize = (word) => word[0].toUpperCase() + word.slice(1); - async function avatarImage(file) { - if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); -@@ -7780,8 +7780,8 @@ - throw new Error(t("That image is too large")); - } - function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { -- const [draft, setDraft] = (0, import_react18.useState)(null); -- const reverting = (0, import_react18.useRef)(false); ++var capitalize = (word) => word[0].toUpperCase() + word.slice(1); ++async function avatarImage(file) { ++ if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); ++ if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB")); ++ const bitmap = await createImageBitmap(file); ++ const side = 160; ++ const canvas = document.createElement("canvas"); ++ canvas.width = side; ++ canvas.height = side; ++ const context = canvas.getContext("2d"); ++ context.imageSmoothingQuality = "high"; ++ const crop = Math.min(bitmap.width, bitmap.height); ++ context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side); ++ bitmap.close?.(); ++ for (const quality of [0.9, 0.75, 0.6]) { ++ const url = canvas.toDataURL("image/webp", quality); ++ if (url.startsWith("data:image/webp") && url.length < 15e4) return url; ++ } ++ const png = canvas.toDataURL("image/png"); ++ if (png.length < 15e4) return png; ++ throw new Error(t("That image is too large")); ++} ++function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { + const [draft, setDraft] = (0, import_react29.useState)(null); + const reverting = (0, import_react29.useRef)(false); - const commit = () => { - const next = draft?.trim(); - setDraft(null); -@@ -7792,7 +7792,7 @@ - if (next === void 0 || next === (value ?? "") || required && next === "") return; - void onSave(next); - }; -- return (0, import_react18.createElement)(multiline ? "textarea" : "input", { ++ const commit = () => { ++ const next = draft?.trim(); ++ setDraft(null); ++ if (reverting.current) { ++ reverting.current = false; ++ return; ++ } ++ if (next === void 0 || next === (value ?? "") || required && next === "") return; ++ void onSave(next); ++ }; + return (0, import_react29.createElement)(multiline ? "textarea" : "input", { - className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", - value: draft ?? value ?? "", - placeholder, -@@ -7813,29 +7813,29 @@ - }); - } - function Row({ label, hint, stack = false, children }) { -- return (0, import_react18.createElement)( ++ className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", ++ value: draft ?? value ?? "", ++ placeholder, ++ maxLength, ++ required, ++ "aria-label": label, ++ spellCheck: false, ++ onChange: (event) => setDraft(event.target.value), ++ onBlur: commit, ++ onKeyDown: (event) => { ++ if (event.key === "Escape") { ++ event.stopPropagation(); ++ reverting.current = true; ++ event.currentTarget.blur(); ++ } ++ if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur(); ++ } ++ }); ++} ++function Row({ label, hint, stack = false, children }) { + return (0, import_react29.createElement)( - "label", - { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), ++ "label", ++ { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), - children - ); - } - function Card({ title, children }) { -- return (0, import_react18.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react18.createElement)("h3", null, title) : null, (0, import_react18.createElement)("div", { className: "bt-set-card" }, children)); ++ children ++ ); ++} ++function Card({ title, children }) { + return (0, import_react29.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react29.createElement)("h3", null, title) : null, (0, import_react29.createElement)("div", { className: "bt-set-card" }, children)); - } - function Switch({ on, label, disabled, onChange }) { -- return (0, import_react18.createElement)( ++} ++function Switch({ on, label, disabled, onChange }) { + return (0, import_react29.createElement)( - "button", - { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, -- (0, import_react18.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) ++ "button", ++ { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, + (0, import_react29.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) - ); - } --function SwitchRow({ label, hint, on, disabled, onChange }) { -- return (0, import_react18.createElement)( ++ ); ++} +function SwitchRow({ label, hint, on, disabled, onChange, showLabel = true }) { + return (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), -- (0, import_react18.createElement)(Switch, { on, label, disabled, onChange }) ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, showLabel ? (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label) : null, hint ? (0, import_react29.createElement)("span", { className: "bt-set-hint" }, hint) : null), + (0, import_react29.createElement)(Switch, { on, label, disabled, onChange }) - ); - } - function modelHint(bot, roster) { -@@ -7846,22 +7846,22 @@ - return t("Applies from the next message."); - } - function ShapeRing({ shape, size }) { -- const maskId = (0, import_react18.useRef)(""); ++ ); ++} ++function modelHint(bot, roster) { ++ const shown = modelOf(bot, roster); ++ if (shown === void 0) return t("Pick the model this Bot runs on."); ++ if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one."); ++ if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it."); ++ return t("Applies from the next message."); ++} ++function ShapeRing({ shape, size }) { + const maskId = (0, import_react29.useRef)(""); - if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; - const def = shapeOf(shape); - const unit = def.size / size; - const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; -- return (0, import_react18.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); ++ if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; ++ const def = shapeOf(shape); ++ const unit = def.size / size; ++ const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; + return (0, import_react29.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); - } - function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { - const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; - if (!shapes.includes(look.shape)) shapes.push(look.shape); - const rows = []; - for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); -- return (0, import_react18.createElement)( -- import_react18.Fragment, ++} ++function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { ++ const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; ++ if (!shapes.includes(look.shape)) shapes.push(look.shape); ++ const rows = []; ++ for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); + return (0, import_react29.createElement)( + import_react29.Fragment, - null, -- (0, import_react18.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react18.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react18.createElement)("button", { ++ null, + (0, import_react29.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react29.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react29.createElement)("button", { - key: id, - type: "button", - className: "bt-shape", -@@ -7869,8 +7869,8 @@ - title: t(shapeOf(id).label), - "aria-pressed": !hasPhoto && look.shape === id, - onClick: () => onShape(id) -- }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), -- (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", { ++ key: id, ++ type: "button", ++ className: "bt-shape", ++ "aria-label": t(shapeOf(id).label), ++ title: t(shapeOf(id).label), ++ "aria-pressed": !hasPhoto && look.shape === id, ++ onClick: () => onShape(id) + }, (0, import_react29.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react29.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), + (0, import_react29.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react29.createElement)("button", { - key: color, - type: "button", - className: "bt-color", -@@ -7878,19 +7878,19 @@ - title: t(capitalize(color)), - "aria-pressed": look.color === color, - onClick: () => onColor(color) -- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } })))) ++ key: color, ++ type: "button", ++ className: "bt-color", ++ "aria-label": t(capitalize(color)), ++ title: t(capitalize(color)), ++ "aria-pressed": look.color === color, ++ onClick: () => onColor(color) + }, (0, import_react29.createElement)("span", { style: { background: paintCss(color) } })))) - ); ++ ); } - function AvatarEditor({ bot, actions, run, onClose }) { - const look = lookOf(bot); -- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot"); -- const [over, setOver] = (0, import_react18.useState)(false); -- const ref = (0, import_react18.useRef)(null); -- const fileRef = (0, import_react18.useRef)(null); +-function MemoryDialog({ bot, actions, leaving = false }) { +- const { view, error, load, show } = useMemoryView(bot.id, actions); +- const [page, setPage] = (0, import_react17.useState)("summary"); +- const [menu, setMenu] = (0, import_react17.useState)(false); +- const [menuShown, menuLeaving] = useLinger(menu, MENU_EXIT_MS2); +- const [text, setText] = (0, import_react17.useState)(""); +- const [answer, setAnswer] = (0, import_react17.useState)(null); +- const menuRef = (0, import_react17.useRef)(null); +- const dotsRef = (0, import_react17.useRef)(null); +- const inputRef = (0, import_react17.useRef)(null); +- const now = useNow(3e4); +- const close = (0, import_react17.useCallback)(() => actions.closeMemory(), []); +- useEscape(close); +- (0, import_react17.useEffect)(() => { +- if (!menu) return void 0; ++function AvatarEditor({ bot, actions, run, onClose }) { ++ const look = lookOf(bot); + const [tab, setTab] = (0, import_react29.useState)(look.image ? "upload" : "bot"); + const [over, setOver] = (0, import_react29.useState)(false); + const ref = (0, import_react29.useRef)(null); + const fileRef = (0, import_react29.useRef)(null); - const upload = (file) => { - if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); - }; -- (0, import_react18.useEffect)(() => { ++ const upload = (file) => { ++ if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); ++ }; + (0, import_react29.useEffect)(() => { const onDown = (event) => { - if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; - onClose(); -@@ -7908,7 +7908,7 @@ +- if (menuRef.current?.contains(event.target) || dotsRef.current?.contains(event.target)) return; +- setMenu(false); ++ if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; ++ onClose(); + }; + const onKey = (event) => { + if (event.key === "Escape") { + event.stopPropagation(); +- setMenu(false); +- dotsRef.current?.focus(); ++ onClose(); + } + }; + window.addEventListener("mousedown", onDown); +@@ -7646,514 +7646,917 @@ function MemoryDialog({ bot, actions, leaving = false }) { + window.removeEventListener("mousedown", onDown); window.removeEventListener("keydown", onKey, true); }; - }, [onClose]); -- (0, import_react18.useEffect)(() => { +- }, [menu]); +- const regenerate = () => { +- setMenu(false); +- setPage("summary"); +- void load({ regenerate: true }); +- }; +- const openEntries = () => { +- setMenu(false); +- setPage("entries"); +- }; +- const backToSummary = () => { +- setPage("summary"); +- void load({ summarize: true }); +- }; +- const remove = async (entry) => { +- try { +- show(await actions.memoryForget(bot.id, entry)); +- return true; +- } catch (failure) { +- setAnswer({ asked: entry.text, error: failure?.message ?? String(failure) }); +- return false; +- } +- }; +- const ask = async (event) => { +- event.preventDefault(); +- const asked = text.trim(); +- if (asked === "" || answer?.pending) return; +- setText(""); +- setAnswer({ asked, pending: true }); +- try { +- const result = await actions.memoryAsk(bot.id, asked); +- setAnswer({ asked, reply: result.reply, changes: result.changes }); +- show(result.view); +- if (result.changes.some((change) => change.ok)) void load({ summarize: page === "summary" }); +- } catch (failure) { +- setAnswer({ asked, error: failure?.message ?? String(failure) }); +- } +- inputRef.current?.focus(); ++ }, [onClose]); + (0, import_react29.useEffect)(() => { - if (tab !== "upload") return void 0; - const onPaste = (event) => { - const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); -@@ -7922,32 +7922,32 @@ - }, [tab, bot]); - const hasPhoto = Boolean(look.image); - const glide = useGlide(tab); -- return (0, import_react18.createElement)( ++ if (tab !== "upload") return void 0; ++ const onPaste = (event) => { ++ const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); ++ if (file) { ++ event.preventDefault(); ++ upload(file); ++ } ++ }; ++ window.addEventListener("paste", onPaste); ++ return () => window.removeEventListener("paste", onPaste); ++ }, [tab, bot]); ++ const hasPhoto = Boolean(look.image); ++ const glide = useGlide(tab); + return (0, import_react29.createElement)( - "div", - { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, -- (0, import_react18.createElement)( ++ "div", ++ { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, + (0, import_react29.createElement)( - "div", - { className: "bt-avedit-top" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-avedit-top" }, + (0, import_react29.createElement)( - "div", - { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, -- (0, import_react18.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), -- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react18.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) ++ "div", ++ { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, + (0, import_react29.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react29.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) - ), -- (0, import_react18.createElement)("button", { ++ ), + (0, import_react29.createElement)("button", { - type: "button", - className: "bt-avedit-tab bt-avedit-reset", - title: t("Reset character to default"), - onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) - }, t("Reset")) - ), -- tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, { ++ type: "button", ++ className: "bt-avedit-tab bt-avedit-reset", ++ title: t("Reset character to default"), ++ onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) ++ }, t("Reset")) ++ ), + tab === "bot" ? (0, import_react29.createElement)(CharacterGrid, { - look, - hasPhoto, - onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), - onColor: (color) => run(actions.updateBot(bot.id, { color })) - }) : null, -- tab === "upload" ? (0, import_react18.createElement)( ++ look, ++ hasPhoto, ++ onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), ++ onColor: (color) => run(actions.updateBot(bot.id, { color })) ++ }) : null, + tab === "upload" ? (0, import_react29.createElement)( - "div", - { - className: "bt-dropzone", -@@ -7963,15 +7963,15 @@ - upload(event.dataTransfer.files?.[0]); - } - }, -- hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null, -- (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")), -- (0, import_react18.createElement)( ++ "div", ++ { ++ className: "bt-dropzone", ++ "data-over": over || void 0, ++ onDragOver: (event) => { ++ event.preventDefault(); ++ setOver(true); ++ }, ++ onDragLeave: () => setOver(false), ++ onDrop: (event) => { ++ event.preventDefault(); ++ setOver(false); ++ upload(event.dataTransfer.files?.[0]); ++ } ++ }, + hasPhoto ? (0, import_react29.createElement)("img", { src: look.image, alt: "" }) : null, + (0, import_react29.createElement)("div", null, t("Drag, drop, or paste an image")), + (0, import_react29.createElement)( - "div", - { className: "bt-actions", style: { justifyContent: "center" } }, -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), -- hasPhoto ? (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null ++ "div", ++ { className: "bt-actions", style: { justifyContent: "center" } }, + (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), + hasPhoto ? (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null - ), -- (0, import_react18.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { ++ ), + (0, import_react29.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { - const file = event.target.files?.[0]; - event.target.value = ""; - upload(file); -@@ -7982,26 +7982,27 @@ - function MemoryCard({ bot, actions }) { - const count = useMemoryCount(bot.id, actions); - const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); -- return (0, import_react18.createElement)( ++ const file = event.target.files?.[0]; ++ event.target.value = ""; ++ upload(file); ++ } }) ++ ) : null ++ ); ++} ++function MemoryCard({ bot, actions }) { ++ const count = useMemoryCount(bot.id, actions); ++ const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); + return (0, import_react29.createElement)( - Card, - { title: t("Memory") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Memory") }, + (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label)), -- (0, import_react18.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react18.createElement)(ChevronRightIcon)) ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, label)), + (0, import_react29.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react29.createElement)(ChevronRightIcon)) - ) - ); - } - function BotSettingsForm({ bot, roster, actions, run }) { -- const [editing, setEditing] = (0, import_react18.useState)(false); -- const [confirming, setConfirming] = (0, import_react18.useState)(false); -- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []); -- (0, import_react18.useEffect)(() => { ++ ) ++ ); ++} ++function BotSettingsForm({ bot, roster, actions, run }) { + const [editing, setEditing] = (0, import_react29.useState)(false); + const [confirming, setConfirming] = (0, import_react29.useState)(false); + const closeEditor = (0, import_react29.useCallback)(() => setEditing(false), []); + (0, import_react29.useEffect)(() => { - if (!confirming) return void 0; - const timer = setTimeout(() => setConfirming(false), 4e3); - return () => clearTimeout(timer); - }, [confirming]); ++ if (!confirming) return void 0; ++ const timer = setTimeout(() => setConfirming(false), 4e3); ++ return () => clearTimeout(timer); ++ }, [confirming]); + if (bot.remote) return null; - const isMainBot = isMainOf(roster, bot.id); - const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; - const isTile = roster.mainBotId === bot.id; -@@ -8011,112 +8011,116 @@ - if (confirming) void run(actions.deleteBot(bot.id)); - else setConfirming(true); ++ const isMainBot = isMainOf(roster, bot.id); ++ const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; ++ const isTile = roster.mainBotId === bot.id; ++ const save = (patch) => run(actions.updateBot(bot.id, patch)); ++ const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id))); ++ const remove = () => { ++ if (confirming) void run(actions.deleteBot(bot.id)); ++ else setConfirming(true); }; -- (0, import_react18.useEffect)(() => { +- const count = view?.entries.length ?? 0; +- const when3 = view?.summary ? ago(view.summary.at, now) : null; +- const status = view === null ? "" : page === "entries" ? count === 1 ? t("1 entry") : t("{count} entries", { count }) : view.summarizing && view.summary ? t("Updating…") : view.summary && view.fresh ? when3 ? t("Updated {time}", { time: when3 }) : t("Updated just now") : ""; +- const busy = answer?.pending === true; +- return (0, import_react17.createElement)( + (0, import_react29.useEffect)(() => { - void actions.refreshModels?.(); - }, [bot.id]); -- return (0, import_react18.createElement)( ++ void actions.refreshModels?.(); ++ }, [bot.id]); + return (0, import_react29.createElement)( "div", - { className: "bt-bs" }, -- (0, import_react18.createElement)( +- { className: "bt-mem-layer", "data-leaving": leaving || void 0 }, +- (0, import_react17.createElement)("div", { className: "bt-scrim bt-mem-scrim", onMouseDown: close }), +- (0, import_react17.createElement)( ++ { className: "bt-bs" }, + (0, import_react29.createElement)( "div", - { className: "bt-bs-hero" }, -- (0, import_react18.createElement)( +- { className: "bt-mem", role: "dialog", "aria-modal": true, "aria-label": t("{name}'s memory", { name: bot.name }) }, +- (0, import_react17.createElement)( ++ { className: "bt-bs-hero" }, + (0, import_react29.createElement)( "div", - { className: "bt-avatar-anchor" }, -- (0, import_react18.createElement)( +- { className: "bt-mem-head" }, +- page === "entries" ? (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Back to the summary"), onClick: backToSummary }, (0, import_react17.createElement)(ChevronLeftIcon)) : null, +- (0, import_react17.createElement)("h2", null, page === "entries" ? t("All entries") : t("Memory summary")), +- (0, import_react17.createElement)("span", { className: "bt-mem-status" }, page === "summary" ? [bot.name, status].filter(Boolean).join(" · ") : status), +- (0, import_react17.createElement)("span", { className: "bt-mem-head-gap" }), +- (0, import_react17.createElement)("button", { ref: dotsRef, type: "button", className: "bt-icon-btn", "aria-label": t("Memory options"), "aria-haspopup": "menu", "aria-expanded": menu, onClick: () => setMenu((value) => !value) }, (0, import_react17.createElement)(DotsIcon)), +- (0, import_react17.createElement)("button", { type: "button", className: "bt-icon-btn", "aria-label": t("Close memory"), onClick: close }, (0, import_react17.createElement)(CloseIcon)), +- menuShown ? (0, import_react17.createElement)( +- "div", +- { ref: menuRef, className: "bt-menu bt-mem-menu", role: "menu", "aria-label": t("Memory options"), "data-leaving": menuLeaving || void 0 }, +- (0, import_react17.createElement)( +- "button", +- { type: "button", role: "menuitem", onClick: openEntries }, +- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("All entries")), +- (0, import_react17.createElement)("span", { className: "bt-menu-hint" }, String(count)) +- ), +- (0, import_react17.createElement)( +- "button", +- { type: "button", role: "menuitem", disabled: count === 0 || view?.summarizing, onClick: regenerate }, +- (0, import_react17.createElement)("span", { className: "bt-menu-label" }, t("Regenerate summary")) +- ) +- ) : null ++ { className: "bt-avatar-anchor" }, + (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, -- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), -- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) ++ "button", ++ { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, + (0, import_react29.createElement)(BotMark, { bot, size: 64, live: true }), + (0, import_react29.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react29.createElement)(PencilIcon)) - ), -- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ++ ), + editing ? (0, import_react29.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null ), -- (0, import_react18.createElement)( +- (0, import_react17.createElement)( + (0, import_react29.createElement)( "div", - { className: "bt-bs-who" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), -- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) +- { className: "bt-mem-body" }, +- error ? (0, import_react17.createElement)("div", { className: "bt-error", role: "alert", style: { padding: 0 } }, hostText(error)) : null, +- view === null ? error ? null : (0, import_react17.createElement)(Skeleton) : page === "entries" ? (0, import_react17.createElement)(Entries, { bot, view, remove }) : (0, import_react17.createElement)(Summary, { bot, view, regenerate }) ++ { className: "bt-bs-who" }, + (0, import_react29.createElement)("span", { className: "bt-bs-name" }, bot.name), + (0, import_react29.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) - ) - ), -- (0, import_react18.createElement)( ++ ) ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Profile") }, -- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), -- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) ++ Card, ++ { title: t("Profile") }, + (0, import_react29.createElement)(Row, { label: t("Name") }, (0, import_react29.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), + (0, import_react29.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react29.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Model") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Model") }, + (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), -- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) -- ) ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), + (0, import_react29.createElement)(ModelMenu, { bot, roster, actions, run }) -+ ), + ), +- (0, import_react17.createElement)( + bot.model ? (0, import_react29.createElement)(ModelTestRow, { bot, modelName: modelOf(bot, roster)?.name }) : null - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Working folder") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Working folder") }, + (0, import_react29.createElement)( "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), -- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) +- { className: "bt-mem-foot" }, +- answer ? (0, import_react17.createElement)(Reply, { answer, dismiss: () => setAnswer(null) }) : null, +- (0, import_react17.createElement)( +- "form", +- { className: "bt-mem-ask", onSubmit: ask }, +- (0, import_react17.createElement)("input", { +- ref: inputRef, +- className: "bt-mem-input", +- value: text, +- maxLength: 2e3, +- disabled: view === null, +- placeholder: t("Ask or update"), +- "aria-label": t("Ask about or update {name}'s memory", { name: bot.name }), +- onChange: (event) => setText(event.target.value) +- }), +- (0, import_react17.createElement)( +- "button", +- { type: "submit", className: "bt-mem-send", disabled: busy || text.trim() === "", "aria-label": t("Send") }, +- busy ? (0, import_react17.createElement)("span", { className: "bt-mem-spin", "aria-hidden": true }) : (0, import_react17.createElement)(ArrowUpIcon) +- ) +- ) ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), + (0, import_react29.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) - ) - ), -- (0, import_react18.createElement)( ++ ) ++ ), + (0, import_react29.createElement)( - Card, - { title: t("Instructions") }, -- (0, import_react18.createElement)( ++ Card, ++ { title: t("Instructions") }, + (0, import_react29.createElement)( - Row, - { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, -- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) ++ Row, ++ { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, + (0, import_react29.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) - ) - ), -- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), -- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), -- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), -- (0, import_react18.createElement)( ++ ) ++ ), + // Sharing sits near the top with a title like Profile and Model, not at the end of the page (#406). + (0, import_react29.createElement)(FlyBotDetails, { bot, roster, actions, wrap: (children) => (0, import_react29.createElement)(Card, { title: t("Sharing") }, children) }), + (0, import_react29.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), @@ -12275,73 +14290,59 @@ diff --git a/client.js b/client.js + (0, import_react29.createElement)(BotSchedules, { bot, roster, actions }), + (0, import_react29.createElement)(StandingApprovalsCard, { scope: "bot", target: bot, roster, actions, run, Card }), + (0, import_react29.createElement)( - Card, - { title: t("Main Bot") }, -- onlyMain ? (0, import_react18.createElement)( ++ Card, ++ { title: t("Main Bot") }, + onlyMain ? (0, import_react29.createElement)( - "div", - { className: "bt-set-row bt-bs-row" }, -- (0, import_react18.createElement)( ++ "div", ++ { className: "bt-set-row bt-bs-row" }, + (0, import_react29.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) - ), -- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) -- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) ++ ), + (0, import_react29.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) + ) : (0, import_react29.createElement)(SwitchRow, { label: t("Main Bot"), showLabel: false, hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) - ), - // The Main Bot tile is always first and always shown, like in the context menu. -- isTile ? null : (0, import_react18.createElement)( ++ ), ++ // The Main Bot tile is always first and always shown, like in the context menu. + isTile ? null : (0, import_react29.createElement)( - Card, - { title: t("Sidebar") }, -- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), -- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) ++ Card, ++ { title: t("Sidebar") }, + (0, import_react29.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), + isMainBot ? null : (0, import_react29.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) - ), -- (0, import_react18.createElement)( ++ ), + (0, import_react29.createElement)( - Card, -- { title: t("Manage") }, -- (0, import_react18.createElement)( ++ Card, + { title: t("More") }, + (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, -- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, + (0, import_react29.createElement)("span", { className: "bt-set-copy" }, (0, import_react29.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react29.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), + (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) - ), -- isMainBot ? null : (0, import_react18.createElement)( ++ ), + isMainBot ? null : (0, import_react29.createElement)( - "button", - { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, -- (0, import_react18.createElement)( ++ "button", ++ { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, + (0, import_react29.createElement)( - "span", - { className: "bt-set-copy" }, -- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), -- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) ++ "span", ++ { className: "bt-set-copy" }, + (0, import_react29.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), + (0, import_react29.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) - ), -- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) ++ ), + (0, import_react29.createElement)("span", { className: "bt-chevron" }, (0, import_react29.createElement)(ChevronRightIcon)) ) ) ); -@@ -8130,9 +8130,17 @@ - .bt-bs .bt-avedit{left:0;transform:none} - .bt-bs-row{min-height:48px} - label.bt-bs-row{cursor:text} --.bt-bs-row>.bt-set-copy{flex:1 1 auto} - .bt-bs-label{font-size:13px;line-height:18px;font-weight:500} + } ++var BOT_SETTINGS_CSS = ` ++.bt-bs{display:flex;flex-direction:column;gap:18px} ++.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} ++.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} ++.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} ++.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} ++.bt-bs .bt-avedit{left:0;transform:none} ++.bt-bs-row{min-height:48px} ++label.bt-bs-row{cursor:text} ++.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +/* #1347: a row keeps its label and its control side by side. The *label* is one line — "Name" used to + break into "Nam" + "e" inside a 36px column — and a control never breaks its own words: "Test + connection" came out as "Test connecti" / "on" and Main Bot's "Change" as "Ch/an/ge". The hint under @@ -12349,16 +14350,29 @@ diff --git a/client.js b/client.js +.bt-bs-row>.bt-set-copy{flex:1 1 8ch;min-width:8ch} +.bt-bs-row>.bt-set-copy>.bt-bs-label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bt-bs-row .bt-soft,.bt-bs-row .bt-mini{flex:none;white-space:nowrap} - .bt-bs-row .bt-model-trigger{flex:none;max-width:58%} ++.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +.bt-bs-row .bt-model-trigger :is(.bt-model-name,.bt-model-provider){white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +${MODEL_TEST_CSS} - .bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} - .bt-bs-input:hover{background:var(--bt-hover)} - .bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} -@@ -8153,8 +8153,659 @@ - .bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} - .bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} - @media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} ++.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} ++.bt-bs-input:hover{background:var(--bt-hover)} ++.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} ++.bt-bs-input::placeholder{color:var(--bt-ink-3)} ++.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} ++.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} ++.bt-bs-area:hover{background:var(--bt-active)} ++.bt-bs-danger .bt-bs-label{color:#c21d2e} ++.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} ++.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} ++.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} ++.bt-switch[aria-checked=true]{background:var(--bt-accent)} ++.bt-switch:disabled{opacity:.45;cursor:default} ++.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} ++.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} ++.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} ++.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} ++.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} ++.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} ++@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +/* #1347: the panel's floor is 240px, where a label and a 96px field cannot share a line honestly. Below + 300px the row stacks instead: the copy keeps its whole width on top, the control sits under it and + keeps its own words. Nothing is clipped to fit a width that is not there. */ @@ -12367,8 +14381,27 @@ diff --git a/client.js b/client.js + .bt-bs-row>.bt-set-copy{flex:1 1 100%;min-width:0} + .bt-bs-input{flex:1 1 100%;width:auto;min-width:0;margin:0;text-align:left} +} - `; ++`; +-// src/client/bot-settings.js +-var import_react18 = require("react"); +-var capitalize = (word) => word[0].toUpperCase() + word.slice(1); +-async function avatarImage(file) { +- if (!/^image\/(png|jpeg|webp|gif)$/.test(file.type)) throw new Error(t("Choose a PNG, JPEG, WebP, or GIF image")); +- if (file.size > 25 * 1024 * 1024) throw new Error(t("Choose an image smaller than 25 MB")); +- const bitmap = await createImageBitmap(file); +- const side = 160; +- const canvas = document.createElement("canvas"); +- canvas.width = side; +- canvas.height = side; +- const context = canvas.getContext("2d"); +- context.imageSmoothingQuality = "high"; +- const crop = Math.min(bitmap.width, bitmap.height); +- context.drawImage(bitmap, (bitmap.width - crop) / 2, (bitmap.height - crop) / 2, crop, crop, 0, 0, side, side); +- bitmap.close?.(); +- for (const quality of [0.9, 0.75, 0.6]) { +- const url = canvas.toDataURL("image/webp", quality); +- if (url.startsWith("data:image/webp") && url.length < 15e4) return url; +// src/client/group-settings.js +var import_react30 = require("react"); +var ROOM_MODES = [ @@ -12403,7 +14436,42 @@ diff --git a/client.js b/client.js + const bot = faceSeatBot(seat, roster); + if (!bot) continue; + marks.push((0, import_react30.createElement)(BotMark, { key: seat.key, bot, size, live: false })); -+ } + } +- const png = canvas.toDataURL("image/png"); +- if (png.length < 15e4) return png; +- throw new Error(t("That image is too large")); +-} +-function InlineText({ label, value, placeholder, required = false, maxLength, multiline = false, onSave }) { +- const [draft, setDraft] = (0, import_react18.useState)(null); +- const reverting = (0, import_react18.useRef)(false); +- const commit = () => { +- const next = draft?.trim(); +- setDraft(null); +- if (reverting.current) { +- reverting.current = false; +- return; +- } +- if (next === void 0 || next === (value ?? "") || required && next === "") return; +- void onSave(next); +- }; +- return (0, import_react18.createElement)(multiline ? "textarea" : "input", { +- className: multiline ? "bt-bs-input bt-bs-area" : "bt-bs-input", +- value: draft ?? value ?? "", +- placeholder, +- maxLength, +- required, +- "aria-label": label, +- spellCheck: false, +- onChange: (event) => setDraft(event.target.value), +- onBlur: commit, +- onKeyDown: (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- reverting.current = true; +- event.currentTarget.blur(); +- } +- if (event.key === "Enter" && !multiline && !event.nativeEvent.isComposing) event.currentTarget.blur(); +- } + if (marks.length === 0) return [(0, import_react30.createElement)(PersonMark, { key: "person", size, name: person?.name })]; + return marks; +} @@ -12421,7 +14489,24 @@ diff --git a/client.js b/client.js + const name = seat.local && onOpenBot ? (0, import_react30.createElement)("button", { key: seat.key, type: "button", className: "bt-face-bot", title: t("Open {name}'s chat", { name: seat.bot }), onClick: () => onOpenBot(seat.local.id) }, seat.bot) : (0, import_react30.createElement)("span", { key: seat.key, className: "bt-face-bot", title: seat.record?.handle || void 0 }, seat.bot); + const marked = isLeadSeat(seat, lead) ? [name, (0, import_react30.createElement)("span", { key: `${seat.key}-lead`, className: "bt-tag", "data-flybot": "face-lead-tag" }, t("Lead"))] : [name]; + return index === 0 ? marked : [(0, import_react30.createElement)("span", { key: `${seat.key}-sep` }, t(", ")), ...marked]; -+ }); + }); +-} +-function Row({ label, hint, stack = false, children }) { +- return (0, import_react18.createElement)( +- "label", +- { className: stack ? "bt-set-row bt-bs-row bt-bs-stack" : "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), +- children +- ); +-} +-function Card({ title, children }) { +- return (0, import_react18.createElement)("section", { className: "bt-set-section" }, title ? (0, import_react18.createElement)("h3", null, title) : null, (0, import_react18.createElement)("div", { className: "bt-set-card" }, children)); +-} +-function Switch({ on, label, disabled, onChange }) { +- return (0, import_react18.createElement)( +- "button", +- { type: "button", role: "switch", className: "bt-switch", "aria-checked": on, "aria-label": label, disabled, onClick: () => onChange(!on) }, +- (0, import_react18.createElement)("span", { className: "bt-switch-knob", "aria-hidden": true }) + return (0, import_react30.createElement)( + "div", + { className, role: "listitem", "data-flybot": "face-person", "data-mine": person.mine || void 0 }, @@ -12436,8 +14521,10 @@ diff --git a/client.js b/client.js + (0, import_react30.createElement)("span", { className: "bt-face-bots" }, bots) + ) + ) -+ ); -+} + ); + } +-function SwitchRow({ label, hint, on, disabled, onChange }) { +- return (0, import_react18.createElement)( +function FaceLead({ room: room2, roster, pick: pick2 }) { + const lead = room2.face?.lead; + const [open, setOpen] = (0, import_react30.useState)(false); @@ -12460,7 +14547,10 @@ diff --git a/client.js b/client.js + } + }; + return (0, import_react30.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label), hint ? (0, import_react18.createElement)("span", { className: "bt-set-hint" }, hint) : null), +- (0, import_react18.createElement)(Switch, { on, label, disabled, onChange }) + { className: "bt-face-lead", "data-flybot": "face-lead" }, + (0, import_react30.createElement)("div", { className: "bt-section-title" }, t("Room lead")), + (0, import_react30.createElement)( @@ -12483,20 +14573,59 @@ diff --git a/client.js b/client.js + open && canPick && lead.picked ? (0, import_react30.createElement)("button", { type: "button", className: "bt-more", disabled: busy, onClick: () => void choose(null) }, t("Use the default lead")) : null, + canPick || !lead.supported ? null : (0, import_react30.createElement)("div", { className: "bt-set-hint" }, t("Only the person who created this room can change it.")), + error ? (0, import_react30.createElement)("div", { className: "bt-set-hint", role: "alert" }, error) : null -+ ); -+} + ); + } +-function modelHint(bot, roster) { +- const shown = modelOf(bot, roster); +- if (shown === void 0) return t("Pick the model this Bot runs on."); +- if (!shown.served && (roster.models ?? []).length > 0) return t("This model is not available now. Pick another one."); +- if (bot.appliedModel && bot.appliedModel === bot.model) return t("Every message of this Bot runs on it."); +- return t("Applies from the next message."); +function seatedCount(room2) { + const live = room2?.presentedParticipants ?? room2?.participants ?? []; + const humans = live.filter((p) => p.kind === "human" && p.leftSeq === void 0).length; + const remotes = live.filter((p) => p.remote && p.leftSeq === void 0).length; + return humans + remotes + (room2?.members?.length ?? 0); -+} + } +-function ShapeRing({ shape, size }) { +- const maskId = (0, import_react18.useRef)(""); +- if (maskId.current === "") maskId.current = `bt-ring-${nextMarkSerial()}`; +- const def = shapeOf(shape); +- const unit = def.size / size; +- const markup = `${holes(def, `${bodyOf(def, "#000", true)}`)}${bodyOf(def, "currentColor", true)}`; +- return (0, import_react18.createElement)("svg", { className: "bt-ring", viewBox: `0 0 ${def.size} ${def.size}`, "aria-hidden": true, dangerouslySetInnerHTML: { __html: markup } }); +function roomSubtitle(room2, roster) { + if (room2.origin === "face-to-face") return faceRoomCount(room2, roster); + const admin = roster.byId[room2.admin]; + const count = seatedCount(room2); + return [t(count === 1 ? "{count} member" : "{count} members", { count }), admin ? t("Admin {name}", { name: admin.name }) : t("No admin")].join(" · "); -+} + } +-function CharacterGrid({ look, hasPhoto = false, onShape, onColor }) { +- const shapes = [...PICKER_SHAPES, ...Object.keys(SHAPES).filter((id) => !Object.hasOwn(CHARACTERS, id))]; +- if (!shapes.includes(look.shape)) shapes.push(look.shape); +- const rows = []; +- for (let index = 0; index < shapes.length; index += PICKER_COLUMNS) rows.push(shapes.slice(index, index + PICKER_COLUMNS)); +- return (0, import_react18.createElement)( +- import_react18.Fragment, +- null, +- (0, import_react18.createElement)("div", { className: "bt-shapes", role: "group", "aria-label": t("Character shape") }, rows.map((row, index) => (0, import_react18.createElement)("div", { key: index, className: "bt-shape-row" }, row.map((id) => (0, import_react18.createElement)("button", { +- key: id, +- type: "button", +- className: "bt-shape", +- "aria-label": t(shapeOf(id).label), +- title: t(shapeOf(id).label), +- "aria-pressed": !hasPhoto && look.shape === id, +- onClick: () => onShape(id) +- }, (0, import_react18.createElement)(ShapeRing, { shape: id, size: 36 }), (0, import_react18.createElement)(BotMark, { look: { shape: id, color: look.color }, size: 36 })))))), +- (0, import_react18.createElement)("div", { className: "bt-colors", role: "group", "aria-label": t("Character color") }, CHARACTER_COLORS.map((color) => (0, import_react18.createElement)("button", { +- key: color, +- type: "button", +- className: "bt-color", +- "aria-label": t(capitalize(color)), +- title: t(capitalize(color)), +- "aria-pressed": look.color === color, +- onClick: () => onColor(color) +- }, (0, import_react18.createElement)("span", { style: { background: paintCss(color) } })))) +function GroupSettingsForm({ room: room2, roster, actions, run, onDeleted }) { + const [adding, setAdding] = (0, import_react30.useState)(false); + const [confirming, setConfirming] = (0, import_react30.useState)(false); @@ -12715,8 +14844,16 @@ diff --git a/client.js b/client.js + (0, import_react30.createElement)("span", { className: "bt-chevron" }, (0, import_react30.createElement)(ChevronRightIcon)) + ) + ) -+ ); -+} + ); + } +-function AvatarEditor({ bot, actions, run, onClose }) { +- const look = lookOf(bot); +- const [tab, setTab] = (0, import_react18.useState)(look.image ? "upload" : "bot"); +- const [over, setOver] = (0, import_react18.useState)(false); +- const ref = (0, import_react18.useRef)(null); +- const fileRef = (0, import_react18.useRef)(null); +- const upload = (file) => { +- if (file) void run(avatarImage(file).then((image) => actions.updateBot(bot.id, { avatar: { ...bot.avatar ?? {}, image } }))); +function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { + const [admin, setAdmin] = (0, import_react30.useState)(null); + const [members, setMembers] = (0, import_react30.useState)([]); @@ -12726,7 +14863,40 @@ diff --git a/client.js b/client.js + const pickAdmin = (id) => { + setAdmin(id); + setMembers((list) => list.filter((item) => item !== id)); -+ }; + }; +- (0, import_react18.useEffect)(() => { +- const onDown = (event) => { +- if (!(event.target instanceof Element) || ref.current?.contains(event.target) || event.target.closest(".bt-avatar-trigger")) return; +- onClose(); +- }; +- const onKey = (event) => { +- if (event.key === "Escape") { +- event.stopPropagation(); +- onClose(); +- } +- }; +- window.addEventListener("mousedown", onDown); +- window.addEventListener("keydown", onKey, true); +- return () => { +- window.removeEventListener("mousedown", onDown); +- window.removeEventListener("keydown", onKey, true); +- }; +- }, [onClose]); +- (0, import_react18.useEffect)(() => { +- if (tab !== "upload") return void 0; +- const onPaste = (event) => { +- const file = [...event.clipboardData?.files ?? []].find((item) => item.type.startsWith("image/")); +- if (file) { +- event.preventDefault(); +- upload(file); +- } +- }; +- window.addEventListener("paste", onPaste); +- return () => window.removeEventListener("paste", onPaste); +- }, [tab, bot]); +- const hasPhoto = Boolean(look.image); +- const glide = useGlide(tab); +- return (0, import_react18.createElement)( + const create = async () => { + if (busy || admin === null || members.length === 0) return; + setBusy(true); @@ -12746,24 +14916,85 @@ diff --git a/client.js b/client.js + onClick + }, (0, import_react30.createElement)(BotAvatar, { bot, size: 22, badge: false, live: false }), (0, import_react30.createElement)("span", { className: "bt-ng-name" }, bot.name), on && role === "radio" ? (0, import_react30.createElement)(AdminStar) : null); + return (0, import_react30.createElement)( -+ "div", + "div", +- { ref, className: "bt-avedit", role: "dialog", "aria-label": t("Avatar editor") }, +- (0, import_react18.createElement)( + { className: "bt-set-card bt-ng" }, + (0, import_react30.createElement)( -+ "div", + "div", +- { className: "bt-avedit-top" }, +- (0, import_react18.createElement)( + { className: "bt-set-row bt-bs-row bt-bs-stack" }, + (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label bt-ng-label" }, (0, import_react30.createElement)(AdminStar), t("Choose the group admin"))), + (0, import_react30.createElement)( -+ "div", + "div", +- { ref: glide.box, role: "tablist", "aria-label": t("Avatar source"), className: "bt-avedit-tabs" }, +- (0, import_react18.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), +- [["bot", "Bot"], ["upload", "Upload"]].map(([id, label]) => (0, import_react18.createElement)("button", { key: id, type: "button", role: "tab", className: "bt-avedit-tab", "aria-selected": tab === id, onClick: () => setTab(id) }, t(label))) +- ), +- (0, import_react18.createElement)("button", { +- type: "button", +- className: "bt-avedit-tab bt-avedit-reset", +- title: t("Reset character to default"), +- onClick: () => run(actions.updateBot(bot.id, { avatar: null, color: null })) +- }, t("Reset")) + { className: "bt-ng-picks", role: "radiogroup", "aria-label": t("Group admin") }, + roster.bots.map((bot) => chip(bot, admin === bot.id, () => pickAdmin(bot.id), "radio")) + ) -+ ), + ), +- tab === "bot" ? (0, import_react18.createElement)(CharacterGrid, { +- look, +- hasPhoto, +- onShape: (shape) => run(actions.updateBot(bot.id, { avatar: { shape } })), +- onColor: (color) => run(actions.updateBot(bot.id, { color })) +- }) : null, +- tab === "upload" ? (0, import_react18.createElement)( + admin === null ? null : (0, import_react30.createElement)( -+ "div", + "div", +- { +- className: "bt-dropzone", +- "data-over": over || void 0, +- onDragOver: (event) => { +- event.preventDefault(); +- setOver(true); +- }, +- onDragLeave: () => setOver(false), +- onDrop: (event) => { +- event.preventDefault(); +- setOver(false); +- upload(event.dataTransfer.files?.[0]); +- } +- }, +- hasPhoto ? (0, import_react18.createElement)("img", { src: look.image, alt: "" }) : null, +- (0, import_react18.createElement)("div", null, t("Drag, drop, or paste an image")), +- (0, import_react18.createElement)( + { className: "bt-set-row bt-bs-row bt-bs-stack bt-ng-step" }, + (0, import_react30.createElement)("span", { className: "bt-set-copy" }, (0, import_react30.createElement)("span", { className: "bt-bs-label" }, t("Who to chat with…"))), + (0, import_react30.createElement)( -+ "div", + "div", +- { className: "bt-actions", style: { justifyContent: "center" } }, +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => fileRef.current?.click() }, hasPhoto ? t("Replace") : t("Browse files")), +- hasPhoto ? (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => run(actions.updateBot(bot.id, { avatar: hasShape(bot.avatar?.shape) ? { shape: bot.avatar.shape } : null })) }, t("Remove photo")) : null +- ), +- (0, import_react18.createElement)("input", { ref: fileRef, type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", hidden: true, onChange: (event) => { +- const file = event.target.files?.[0]; +- event.target.value = ""; +- upload(file); +- } }) +- ) : null +- ); +-} +-function MemoryCard({ bot, actions }) { +- const count = useMemoryCount(bot.id, actions); +- const label = count === null ? t("Loading…") : count === 0 ? t("Nothing saved yet") : count === 1 ? t("1 entry") : t("{count} entries", { count }); +- return (0, import_react18.createElement)( +- Card, +- { title: t("Memory") }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row", "aria-label": t("Manage {name}'s memory", { name: bot.name }), onClick: () => actions.openMemory(bot.id) }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, label)), +- (0, import_react18.createElement)("span", { className: "bt-mem-row-go" }, t("Manage"), (0, import_react18.createElement)(ChevronRightIcon)) + { 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")) + ) @@ -12779,9 +15010,26 @@ diff --git a/client.js b/client.js + { className: "bt-set-row bt-task-foot" }, + (0, import_react30.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onCancel }, t("Cancel")), + (0, import_react30.createElement)("button", { type: "button", className: "bt-send bt-soft-sm", disabled: busy || admin === null || members.length === 0, onClick: () => void create() }, t("Create group chat")) -+ ) -+ ); -+} + ) + ); + } +-function BotSettingsForm({ bot, roster, actions, run }) { +- const [editing, setEditing] = (0, import_react18.useState)(false); +- const [confirming, setConfirming] = (0, import_react18.useState)(false); +- const closeEditor = (0, import_react18.useCallback)(() => setEditing(false), []); +- (0, import_react18.useEffect)(() => { +- if (!confirming) return void 0; +- const timer = setTimeout(() => setConfirming(false), 4e3); +- return () => clearTimeout(timer); +- }, [confirming]); +- const isMainBot = isMainOf(roster, bot.id); +- const onlyMain = isMainBot && (roster.mainBotIds ?? []).length === 1; +- const isTile = roster.mainBotId === bot.id; +- const save = (patch) => run(actions.updateBot(bot.id, patch)); +- const duplicate = () => run(actions.duplicateBot(bot.id).then((copy2) => actions.openSession(copy2.id))); +- const remove = () => { +- if (confirming) void run(actions.deleteBot(bot.id)); +- else setConfirming(true); +var GROUP_SETTINGS_CSS = ` +.bt-ng{animation:bt-q-in .4s cubic-bezier(.22,1,.36,1),bt-veil-in .2s ease-out} +.bt-ng-step{animation:bt-veil-in .22s ease-out} @@ -12921,17 +15169,33 @@ diff --git a/client.js b/client.js + if (!row || leaving) return; + void Promise.resolve(row.run()).catch((error) => actions.reportError(error, "menu")); + close(); -+ }; + }; +- (0, import_react18.useEffect)(() => { +- void actions.refreshModels?.(); +- }, [bot.id]); +- return (0, import_react18.createElement)( + let index = -1; + return (0, import_react31.createElement)( -+ "div", + "div", +- { className: "bt-bs" }, +- (0, import_react18.createElement)( + { className: "bt-palette-layer", "data-leaving": leaving || void 0 }, + (0, import_react31.createElement)("div", { className: "bt-backdrop", onMouseDown: close }), + (0, import_react31.createElement)( -+ "div", + "div", +- { className: "bt-bs-hero" }, +- (0, import_react18.createElement)( + { className: "bt-palette", role: "dialog", "aria-label": t("Search"), "aria-modal": true }, + (0, import_react31.createElement)( -+ "div", + "div", +- { className: "bt-avatar-anchor" }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-avatar-trigger", "aria-label": t("Edit Bot avatar"), "aria-expanded": editing, onClick: () => setEditing((value) => !value) }, +- (0, import_react18.createElement)(BotMark, { bot, size: 64, live: true }), +- (0, import_react18.createElement)("span", { className: "bt-avatar-pencil", "aria-hidden": true }, (0, import_react18.createElement)(PencilIcon)) +- ), +- editing ? (0, import_react18.createElement)(AvatarEditor, { bot, actions, run, onClose: closeEditor }) : null + { className: "bt-palette-head" }, + (0, import_react31.createElement)("span", { className: "bt-palette-glyph", "aria-hidden": true }, (0, import_react31.createElement)(SearchIcon)), + (0, import_react31.createElement)("input", { @@ -12964,9 +15228,94 @@ diff --git a/client.js b/client.js + setQuery(""); + setCursor(0); + } }, (0, import_react31.createElement)(CloseIcon)) : null -+ ), + ), +- (0, import_react18.createElement)( +- "div", +- { className: "bt-bs-who" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-name" }, bot.name), +- (0, import_react18.createElement)("span", { className: "bt-bs-sub" }, [bot.role, isMainBot ? t("Main Bot") : ""].filter(Boolean).join(" · ") || t("Bot")) +- ) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Profile") }, +- (0, import_react18.createElement)(Row, { label: t("Name") }, (0, import_react18.createElement)(InlineText, { label: t("Name"), value: bot.name, placeholder: "Bob", required: true, maxLength: 40, onSave: (name) => save({ name }) })), +- (0, import_react18.createElement)(Row, { label: t("Label"), hint: t("Optional. Shown under the name.") }, (0, import_react18.createElement)(InlineText, { label: t("Label"), value: bot.role, placeholder: t("Research, admin…"), maxLength: 24, onSave: (role) => save({ role }) })) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Model") }, +- (0, import_react18.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Model")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, modelHint(bot, roster))), +- (0, import_react18.createElement)(ModelMenu, { bot, roster, actions, run }) +- ) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Working folder") }, +- (0, import_react18.createElement)( +- "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Folder"))), +- (0, import_react18.createElement)(FolderMenu, { value: bot.workDir, actions, run, onPick: (workDir) => save({ workDir }) }) +- ) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Instructions") }, +- (0, import_react18.createElement)( +- Row, +- { label: t("What this Bot does"), hint: t("Its job on the team. Part of the Bot's system prompt."), stack: true }, +- (0, import_react18.createElement)(InlineText, { label: t("Instructions"), value: bot.instructions, multiline: true, placeholder: t("What this Bot is responsible for…"), onSave: (instructions) => save({ instructions }) }) +- ) +- ), +- (0, import_react18.createElement)(BotCapabilitiesCard, { bot, roster, actions, run, Card, SwitchRow }), +- roster.memory === false ? null : (0, import_react18.createElement)(MemoryCard, { bot, actions }), +- (0, import_react18.createElement)(BotSchedules, { bot, roster, actions }), +- (0, import_react18.createElement)( +- Card, +- { title: t("Main Bot") }, +- onlyMain ? (0, import_react18.createElement)( + (0, import_react31.createElement)( -+ "div", + "div", +- { className: "bt-set-row bt-bs-row" }, +- (0, import_react18.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Main Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("The only Main Bot. To hand the role to another Bot, use Main Bot on the Bots page.")) +- ), +- (0, import_react18.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: () => actions.openSettings("bots") }, t("Change")) +- ) : (0, import_react18.createElement)(SwitchRow, { label: t("Main Bot"), hint: t("Can create and change Bots, and runs every group chat."), on: isMainBot, onChange: (on) => run(actions.setMain(bot.id, on)) }) +- ), +- // The Main Bot tile is always first and always shown, like in the context menu. +- isTile ? null : (0, import_react18.createElement)( +- Card, +- { title: t("Sidebar") }, +- (0, import_react18.createElement)(SwitchRow, { label: t("Pin"), hint: t("Kept above the other chats."), on: bot.pinned === true, onChange: (pinned) => run(actions.setFlags(bot.id, { pinned })) }), +- isMainBot ? null : (0, import_react18.createElement)(SwitchRow, { label: t("Hide from sidebar"), hint: t("Search still finds it."), on: bot.hidden === true, onChange: (hidden) => run(actions.setFlags(bot.id, { hidden })) }) +- ), +- (0, import_react18.createElement)( +- Card, +- { title: t("Manage") }, +- (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row", onClick: duplicate }, +- (0, import_react18.createElement)("span", { className: "bt-set-copy" }, (0, import_react18.createElement)("span", { className: "bt-bs-label" }, t("Duplicate")), (0, import_react18.createElement)("span", { className: "bt-set-hint" }, t("A copy with the same job and model, and fresh memory."))), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) +- ), +- isMainBot ? null : (0, import_react18.createElement)( +- "button", +- { type: "button", className: "bt-set-row bt-bs-row bt-bs-danger", "data-confirm": confirming || void 0, onClick: remove }, +- (0, import_react18.createElement)( +- "span", +- { className: "bt-set-copy" }, +- (0, import_react18.createElement)("span", { className: "bt-bs-label" }, confirming ? t("Delete {name}?", { name: bot.name }) : t("Delete Bot")), +- (0, import_react18.createElement)("span", { className: "bt-set-hint" }, confirming ? t("Click again to delete. Its chats are archived.") : t("Removes the Bot from the team.")) +- ), +- (0, import_react18.createElement)("span", { className: "bt-chevron" }, (0, import_react18.createElement)(ChevronRightIcon)) + { ref: listRef, id: "bt-palette-list", className: "bt-palette-list", role: "listbox", "aria-label": t("Results") }, + (0, import_react31.createElement)("span", { ref: glide.thumb, className: "bt-thumb", "aria-hidden": true }), + rows.length === 0 ? (0, import_react31.createElement)( @@ -13007,15 +15356,47 @@ diff --git a/client.js b/client.js + ); + }) + )) -+ ) -+ ) -+ ); -+} -+ + ) + ) + ); + } +-var BOT_SETTINGS_CSS = ` +-.bt-bs{display:flex;flex-direction:column;gap:18px} +-.bt-bs-hero{display:flex;align-items:center;gap:14px;padding:2px 4px} +-.bt-bs-who{display:flex;flex-direction:column;min-width:0;gap:2px} +-.bt-bs-name{font-size:17px;line-height:24px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +-.bt-bs-sub{font-size:13px;line-height:18px;color:var(--bt-ink-2)} +-.bt-bs .bt-avedit{left:0;transform:none} +-.bt-bs-row{min-height:48px} +-label.bt-bs-row{cursor:text} +-.bt-bs-row>.bt-set-copy{flex:1 1 auto} +-.bt-bs-label{font-size:13px;line-height:18px;font-weight:500} +-.bt-bs-row .bt-model-trigger{flex:none;max-width:58%} +-.bt-bs-input{flex:0 1 220px;width:0;min-width:96px;height:30px;box-sizing:border-box;margin:-4px -6px -4px 0;padding:0 8px;border:1px solid transparent;border-radius:8px;background:none;color:var(--bt-ink);font:inherit;font-size:13px;text-align:right;outline:none;transition:background-color .12s ease,border-color .12s ease} +-.bt-bs-input:hover{background:var(--bt-hover)} +-.bt-bs-input:focus{background:var(--bt-main);border-color:var(--bt-line-2);text-align:left} +-.bt-bs-input::placeholder{color:var(--bt-ink-3)} +-.bt-bs-stack{flex-direction:column;align-items:stretch;gap:8px} +-.bt-bs-area{flex:none;width:auto;min-height:112px;height:auto;margin:0 -6px -4px;padding:8px;line-height:19px;text-align:left;resize:vertical;background:var(--bt-hover)} +-.bt-bs-area:hover{background:var(--bt-active)} +-.bt-bs-danger .bt-bs-label{color:#c21d2e} +-.bt-bs-danger[data-confirm]{background:color-mix(in srgb,#e02135 8%,transparent)} +-.bt-bs-danger[data-confirm]:hover{background:color-mix(in srgb,#e02135 12%,transparent)} +-.bt-switch{position:relative;flex:none;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--bt-line-2);cursor:pointer;transition:background-color .2s ease} +-.bt-switch[aria-checked=true]{background:var(--bt-accent)} +-.bt-switch:disabled{opacity:.45;cursor:default} +-.bt-switch:focus-visible{outline:2px solid var(--bt-accent);outline-offset:2px} +-.bt-switch-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:${spring(SPRING_SHAPE, "transform")},${spring(SPRING_TAP, "width")}} +-.bt-switch[aria-checked=true] .bt-switch-knob{transform:translateX(16px)} +-.bt-switch:active:not(:disabled) .bt-switch-knob{width:21px} +-.bt-switch[aria-checked=true]:active:not(:disabled) .bt-switch-knob{transform:translateX(11px)} +-.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:bt-veil-in .18s ease-out} +-@media (prefers-reduced-motion:reduce){.bt-switch,.bt-switch-knob{transition:none}.bt-avedit :is(.bt-shapes,.bt-colors,.bt-dropzone){animation:none}} +-`; + // src/client/bot-draft.js function freshBotName(roster, base = "New Bot") { - const taken = new Set(roster.bots.map((bot) => bot.name.toLowerCase())); -@@ -8165,36 +8165,48 @@ +@@ -8165,36 +8165,48 @@ function freshBotName(roster, base = "New Bot") { } function startModel(roster) { const models = roster.models ?? []; @@ -13079,7 +15460,7 @@ diff --git a/client.js b/client.js if (inline) frame.current?.scrollIntoView({ block: "start", behavior: "smooth" }); }, []); const fallbackName = freshBotName(roster, t("New Bot")); -@@ -8206,8 +8206,8 @@ +@@ -8206,8 +8206,8 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr setError(""); setBusy(true); try { @@ -13090,7 +15471,7 @@ diff --git a/client.js b/client.js } catch (failure) { setError(failure?.message ?? String(failure)); setBusy(false); -@@ -8219,25 +8219,25 @@ +@@ -8219,25 +8219,25 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr avatarImage(file).then(setImage, (failure) => setError(failure?.message ?? String(failure))); }; const previewLook = image ? { ...look, image } : look; @@ -13124,7 +15505,7 @@ diff --git a/client.js b/client.js look, hasPhoto: Boolean(image), onShape: (shape) => { -@@ -8249,27 +8249,27 @@ +@@ -8249,27 +8249,27 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr setLook((current) => ({ ...current, color })); } }), @@ -13160,7 +15541,7 @@ diff --git a/client.js b/client.js className: "bt-input", autoFocus: !inline, value: name, -@@ -8284,75 +8284,113 @@ +@@ -8284,75 +8284,113 @@ function CreateBot({ roster, actions, start, brief = "", onBack, canGoBack, onCr } } }), @@ -13304,7 +15685,7 @@ diff --git a/client.js b/client.js const pick2 = (bot) => { setPicked((current) => [...current, bot.id]); setQuery(""); -@@ -8386,71 +8386,104 @@ +@@ -8386,71 +8386,104 @@ function NewChat({ mode, roster, actions }) { }; const created = (bot) => { close(); @@ -13429,7 +15810,7 @@ diff --git a/client.js b/client.js canGoBack: draft.fromList, onBack: draft.fromList ? () => { setDraft(null); -@@ -8467,33 +8467,41 @@ +@@ -8467,33 +8467,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); if (picked.length > 0) setPicked((current) => current.slice(0, -1)); else if (admin !== null) setAdmin(null); }; @@ -13486,7 +15867,7 @@ diff --git a/client.js b/client.js ref: inputRef, autoFocus: true, value: query, -@@ -8517,20 +8517,20 @@ +@@ -8517,20 +8517,20 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } else if (event.key === "Backspace" && query === "") unpick(); } }), @@ -13512,7 +15893,7 @@ diff --git a/client.js b/client.js "button", { key: option.key, -@@ -8542,21 +8542,36 @@ +@@ -8542,21 +8542,36 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); onMouseEnter: () => setCursor(index), onClick: option.run }, @@ -13558,7 +15939,7 @@ diff --git a/client.js b/client.js rows: 1, value: message, placeholder, -@@ -8569,7 +8569,7 @@ +@@ -8569,7 +8569,7 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } } }), @@ -13567,7 +15948,7 @@ diff --git a/client.js b/client.js ) ) ) -@@ -8577,18 +8577,41 @@ +@@ -8577,18 +8577,41 @@ ${listed}`, { axis: "y", ...QUICK_EDGES }); } // src/client/exchange.js @@ -13613,7 +15994,7 @@ diff --git a/client.js b/client.js let stop = false; let timer; let seen = null; -@@ -8612,49 +8612,42 @@ +@@ -8612,49 +8612,42 @@ function ExchangeDialog({ pair, roster, actions }) { const right = roster.byId[pair[1]]; let lastDay = 0; const paneLeft = usePaneLeft(); @@ -13677,7 +16058,7 @@ diff --git a/client.js b/client.js var VOICE_LABELS = { listening: "Listening", thinking: "Thinking", -@@ -8665,14 +8665,14 @@ +@@ -8665,14 +8665,14 @@ var VOICE_LABELS = { }; var CAPTURE_NOTES = { blocked: "Microphone access is blocked", unavailable: "Voice chat is not available in this browser", "too-long": "That was too long to recognize" }; function useMicLevel(enabled) { @@ -13695,7 +16076,7 @@ diff --git a/client.js b/client.js if (stopped) { media.getTracks().forEach((track) => track.stop()); return; -@@ -8716,18 +8716,18 @@ +@@ -8716,18 +8716,18 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const entry = Array.isArray(outline) ? outline[outline.length - 1] : void 0; return entry ? `${outline.length}|${entry.response ?? ""}` : "0|"; }); @@ -13725,7 +16106,7 @@ diff --git a/client.js b/client.js const paneLeft = usePaneLeft(); const close = () => { turn.current += 1; -@@ -8735,24 +8735,24 @@ +@@ -8735,24 +8735,24 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } actions.closeVoice(); }; useEscape(close); @@ -13758,7 +16139,7 @@ diff --git a/client.js b/client.js if (!speech) return void 0; let live = true; speechCatalog(speech).then(({ catalog: next, usable }) => { -@@ -8767,16 +8767,16 @@ +@@ -8767,16 +8767,16 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } }; }, [speech]); const listen = phase === "listening"; @@ -13778,7 +16159,7 @@ diff --git a/client.js b/client.js if (!speech || !catalog || !listen) return void 0; const own = turn.current; let capture = null; -@@ -8789,9 +8789,9 @@ +@@ -8789,9 +8789,9 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } setPhase("transcribing"); try { const wave = await current.finish(catalog.maxDurationSeconds); @@ -13790,7 +16171,7 @@ diff --git a/client.js b/client.js else setPhase("listening"); } catch (error) { if (turn.current !== own) return; -@@ -8838,12 +8838,12 @@ +@@ -8838,12 +8838,12 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } setServiceLevel(0); }; }, [speech, catalog, listen, cycle]); @@ -13807,7 +16188,7 @@ diff --git a/client.js b/client.js setSaid(spoken); setHeard(""); if (!window.speechSynthesis || !spoken) { -@@ -8858,7 +8858,7 @@ +@@ -8858,7 +8858,7 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); }, [phase, running, lastReply]); @@ -13816,7 +16197,7 @@ diff --git a/client.js b/client.js const micLevel = useMicLevel(!speech && phase === "listening" && dictation.listening); const level = speech ? serviceLevel : micLevel; const markState = phase === "thinking" || phase === "transcribing" ? "thinking" : phase === "speaking" ? "sending" : "idle"; -@@ -8875,291 +8875,663 @@ +@@ -8875,291 +8875,678 @@ function VoiceMode({ sessionId, roster, actions, useSessions, useSessionStatus } const openSetup = phase === "setup" ? actions.voiceSetup() : null; const caption = phase === "speaking" ? said : heard; const shownNote = note || dictation.note; @@ -13891,6 +16272,7 @@ diff --git a/client.js b/client.js +// src/client/workflows.js +var import_react36 = require("react"); +var READ_MS = 8e3; ++var POLL_MS6 = 2e3; +var WORKFLOW_PLUGIN_HINT = "The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow."; +var COLORS = { Planner: "#8b5cf6", Writer: "#f0509a", Coder: "#1fbf6a", Reviewer: "#f5a524", Tester: "#14b8a6", Deployer: "#2f7cf6" }; +function workflowCallMissing(failure) { @@ -14319,28 +16701,13 @@ diff --git a/client.js b/client.js - Row, - { label: t("Notice"), hint: t("Every member reads it before speaking in this group."), stack: true }, - (0, import_react23.createElement)(InlineText, { label: t("Group notice"), value: room2.notice, multiline: true, maxLength: 2e3, placeholder: t("Shared instructions every member reads before speaking…"), onSave: (notice) => update({ notice }) }) -+ (0, import_react36.createElement)( -+ "section", -+ { className: "bt-set-section" }, -+ (0, import_react36.createElement)("h3", null, t("Yours")), -+ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-set-card" }, -+ yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) -+ ) - ) +- ) - ), - (0, import_react23.createElement)( - Card, - { title: t("Who answers") }, - (0, import_react23.createElement)( -+ ) : null, -+ graph ? (0, import_react36.createElement)( -+ "div", -+ { className: "bt-wf-editor" }, -+ (0, import_react36.createElement)( - "div", +- "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, @@ -14359,42 +16726,7 @@ diff --git a/client.js b/client.js - Card, - { title: t("Working folder") }, - (0, import_react23.createElement)( -+ { className: "bt-wf-actions" }, -+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), -+ showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, -+ opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null -+ ), -+ (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name), -+ graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, -+ (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, { -+ graph, -+ roster, -+ onCommit, -+ width: canvasWidth, -+ editing: opened?.source === "own" && editing === true -+ }) -+ ) : null -+ ); -+} -+function RenameModal({ workflow, onClose, onConfirm }) { -+ const [name, setName] = (0, import_react36.useState)(workflow?.name ?? ""); -+ return (0, import_react36.createElement)( -+ "div", -+ { className: "bt-modal-mask", onClick: (event) => { -+ if (event.target === event.currentTarget) onClose?.(); -+ } }, -+ (0, import_react36.createElement)( -+ "div", -+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Rename workflow") }, -+ (0, import_react36.createElement)("h3", null, t("Rename workflow")), -+ (0, import_react36.createElement)( -+ "label", -+ { className: "bt-field" }, -+ t("Name"), -+ (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) -+ ), -+ (0, import_react36.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 }) }) @@ -14406,7 +16738,13 @@ diff --git a/client.js b/client.js - members.map((member) => { - const leads = room2.admin === member.id; - return (0, import_react23.createElement)( -- "div", ++ (0, import_react36.createElement)( ++ "section", ++ { className: "bt-set-section" }, ++ (0, import_react36.createElement)("h3", null, t("Yours")), ++ (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, t("Workflows saved on this machine.")), ++ (0, import_react36.createElement)( + "div", - { key: member.id, className: "bt-set-row bt-bs-row bt-gs-member" }, - (0, import_react23.createElement)( - "button", @@ -14458,11 +16796,32 @@ diff --git a/client.js b/client.js - (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-modal-actions" }, -+ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -+ (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ++ { className: "bt-set-card" }, ++ yours.length === 0 ? (0, import_react36.createElement)("div", { className: "bt-set-row" }, (0, import_react36.createElement)("span", { className: "bt-set-hint" }, t("No workflows of your own yet."))) : yours.map((item) => (0, import_react36.createElement)(Row2, { key: item.id, item, source: "own", onOpen: () => onOpen?.(item, "own"), onRename, onDelete })) ++ ) ) - ) +- ) ++ ) : null, ++ graph ? (0, import_react36.createElement)( ++ "div", ++ { className: "bt-wf-editor" }, ++ (0, import_react36.createElement)( ++ "div", ++ { className: "bt-wf-actions" }, ++ (0, import_react36.createElement)("button", { type: "button", onClick: () => onBack?.() }, t("Back")), ++ showUse ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onUse?.(opened.source === "factory" ? opened.id : opened.fromFactory) }, t("Use this")) : null, ++ opened?.source === "own" ? (0, import_react36.createElement)("button", { type: "button", onClick: () => onEditing?.(!editing) }, editing ? t("Finish editing") : t("Edit")) : null ++ ), ++ (0, import_react36.createElement)("h3", { className: "bt-wf-title" }, graph.name), ++ graph.description ? (0, import_react36.createElement)("p", { className: "bt-wf-hint" }, graph.description) : null, ++ (graph.nodes ?? []).length === 0 ? (0, import_react36.createElement)("p", { className: "bt-wf-status" }, t("This workflow has no flowchart yet.")) : (0, import_react36.createElement)(GraphCanvas, { ++ graph, ++ roster, ++ onCommit, ++ width: canvasWidth, ++ editing: opened?.source === "own" && editing === true ++ }) ++ ) : null ); } -function NewGroupForm({ roster, actions, run, onCreated, onCancel }) { @@ -14494,41 +16853,63 @@ diff --git a/client.js b/client.js - 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 DeleteModal({ workflow, onClose, onConfirm }) { ++function RenameModal({ workflow, onClose, onConfirm }) { ++ const [name, setName] = (0, import_react36.useState)(workflow?.name ?? ""); + return (0, import_react36.createElement)( "div", - { className: "bt-set-card bt-ng" }, - (0, import_react23.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)( + { className: "bt-modal-mask", onClick: (event) => { + if (event.target === event.currentTarget) onClose?.(); + } }, + (0, import_react36.createElement)( -+ "div", -+ { className: "bt-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, -+ (0, import_react36.createElement)("h3", null, t("Delete workflow")), -+ (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), + "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_react36.createElement)("h3", null, t("Rename workflow")), ++ (0, import_react36.createElement)( ++ "label", ++ { className: "bt-field" }, ++ t("Name"), ++ (0, import_react36.createElement)("input", { type: "text", value: name, onChange: (event) => setName(event.target.value), autoFocus: true }) ++ ), + (0, import_react36.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")) + { className: "bt-modal-actions" }, + (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), -+ (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) ++ (0, import_react36.createElement)("button", { type: "button", onClick: () => onConfirm?.(name) }, t("Rename")) ) - ), - admin === null ? null : (0, import_react23.createElement)( -- "div", ++ ) ++ ); ++} ++function DeleteModal({ workflow, onClose, onConfirm }) { ++ return (0, import_react36.createElement)( ++ "div", ++ { className: "bt-modal-mask", onClick: (event) => { ++ if (event.target === event.currentTarget) onClose?.(); ++ } }, ++ (0, import_react36.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-modal", role: "dialog", "aria-modal": true, "aria-label": t("Delete workflow") }, ++ (0, import_react36.createElement)("h3", null, t("Delete workflow")), ++ (0, import_react36.createElement)("p", null, t('Are you sure you want to delete "{name}"? This cannot be undone.', { name: workflow?.name ?? "" })), ++ (0, import_react36.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")) -- ) ++ { className: "bt-modal-actions" }, ++ (0, import_react36.createElement)("button", { type: "button", onClick: onClose }, t("Cancel")), ++ (0, import_react36.createElement)("button", { type: "button", className: "bt-danger", onClick: onConfirm }, t("Delete")) + ) - ), - admin === null || members.length === 0 ? null : (0, import_react23.createElement)( - "label", @@ -14588,6 +16969,20 @@ diff --git a/client.js b/client.js + gone = true; + }; + }, []); ++ const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0); ++ (0, import_react36.useEffect)(() => { ++ if (!live || editing) return void 0; ++ let gone = false; ++ const timer = setInterval(() => { ++ readWorkflows(invoke).then((settled) => { ++ if (!gone) apply2(settled); ++ }); ++ }, POLL_MS6); ++ return () => { ++ gone = true; ++ clearInterval(timer); ++ }; ++ }, [live, editing]); + const retry = () => { + setPhase("loading"); + setError(""); @@ -14596,8 +16991,8 @@ diff --git a/client.js b/client.js + }; + const open = (item, source) => { + if (source === "factory") { -+ const live = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); -+ setOpened(live ? { source: "own", id: live.id, fromFactory: item.id } : { source: "factory", id: item.id }); ++ const live2 = (pack?.workflows ?? []).find((workflow) => workflow.id === item.id); ++ setOpened(live2 ? { source: "own", id: live2.id, fromFactory: item.id } : { source: "factory", id: item.id }); + setEditing(false); + return; + } @@ -14716,7 +17111,7 @@ diff --git a/client.js b/client.js // src/client/usage-data.js var HOUR = 0; -@@ -9277,13 +9277,13 @@ +@@ -9277,13 +9277,13 @@ function summarize(view, range, keep = () => true, seriesOf = () => "all") { } return result; } @@ -14734,7 +17129,7 @@ diff --git a/client.js b/client.js } return String(Math.round(count)); } -@@ -9294,11 +9294,11 @@ +@@ -9294,11 +9294,11 @@ function percent(part, whole) { } // src/client/usage-page.js @@ -14748,7 +17143,7 @@ diff --git a/client.js b/client.js var rangeLabel = (id) => ({ "24h": t("24 hours"), "7d": t("7 days"), "30d": t("30 days"), "12w": t("12 weeks"), "12m": t("12 months") })[id]; var unitLabel = (unit) => ({ hour: t("Per hour"), day: t("Per day"), week: t("Per week"), month: t("Per month") })[unit]; var MODEL_INKS = ["deepseek", "tangerine", "green", "violet", "sky", "rose", "yellow", "cyan", "magenta", "brown"]; -@@ -9341,9 +9341,9 @@ +@@ -9341,9 +9341,9 @@ function titleOf(range, bucket) { return dates({ year: "numeric", month: "long" }).format(bucket.start); } function useTween(value) { @@ -14761,7 +17156,7 @@ diff --git a/client.js b/client.js if (reducedMotion() || typeof requestAnimationFrame !== "function") { from.current = value; setShown(value); -@@ -9363,9 +9363,9 @@ +@@ -9363,9 +9363,9 @@ function useTween(value) { }, [value]); return shown; } @@ -14773,7 +17168,7 @@ diff --git a/client.js b/client.js className: "bt-us-model", "aria-hidden": true, style: { width: size, height: size, fontSize: Math.round(size * 0.42), background: `color-mix(in srgb, ${paint} 20%, var(--bt-card))`, color: `color-mix(in srgb, ${paint} 72%, var(--bt-ink))` } -@@ -9382,7 +9382,7 @@ +@@ -9382,7 +9382,7 @@ function ownerFace(owner, roster) { sub: owner.deleted ? t("Deleted") : role.toLowerCase() === name.toLowerCase() ? "" : role, paint: paintCss(colorOf(bot)), stops: GRADIENTS[colorOf(bot)], @@ -14782,7 +17177,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "room") { -@@ -9392,7 +9392,7 @@ +@@ -9392,7 +9392,7 @@ function ownerFace(owner, roster) { name: owner.name || t("Deleted group chat"), sub: owner.deleted ? t("Deleted") : t("Group chat"), paint: "var(--bt-us-rest)", @@ -14791,7 +17186,7 @@ diff --git a/client.js b/client.js }; } if (owner.kind === "agent") { -@@ -9411,33 +9411,33 @@ +@@ -9411,33 +9411,33 @@ function kindLabel(kind, botFocus) { return labels[kind] ?? kind; } function Tabs({ label, value, options, onChange, boxRef }) { @@ -14836,7 +17231,7 @@ diff --git a/client.js b/client.js "span", { className: "bt-us-delta", title: compareText(range) }, `${change >= 0 ? "↑" : "↓"} ${Math.abs(change)}%` -@@ -9455,83 +9455,83 @@ +@@ -9455,83 +9455,83 @@ function niceTop(max) { return [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10].map((scale) => scale * step).find((value) => value >= max); } function Chart({ range, columns, series, tipSeries, by, onBy, onPick }) { @@ -14948,7 +17343,7 @@ diff --git a/client.js b/client.js onClick ? "button" : "div", { className: "bt-us-row", -@@ -9540,69 +9540,69 @@ +@@ -9540,69 +9540,69 @@ function Row2({ kind, glyph, name, sub, sum, whole, fill: fill2, index, onClick title: t("{tokens} tokens · {calls} calls", { tokens: full(sum.total), calls: full(sum.calls) }), ...onClick ? { type: "button", onClick } : {} }, @@ -15046,7 +17441,7 @@ diff --git a/client.js b/client.js const onDown = (event) => { if (![box, near].some((ref) => ref.current?.contains(event.target))) onClose(); }; -@@ -9628,17 +9628,17 @@ +@@ -9628,17 +9628,17 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { const [from, to] = [anchor, key].sort(); onPick(from, to); }; @@ -15067,7 +17462,7 @@ diff --git a/client.js b/client.js key, type: "button", className: "bt-us-day", -@@ -9654,15 +9654,15 @@ +@@ -9654,15 +9654,15 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { "data-has": flag(active.has(key)), onMouseEnter: () => setHover(key), onClick: () => press(key) @@ -15089,7 +17484,7 @@ diff --git a/client.js b/client.js ); }; const shortcuts = [ -@@ -9671,30 +9671,30 @@ +@@ -9671,30 +9671,30 @@ function SpanPicker({ span, today, oldest, active, near, onPick, onClose }) { ["year", t("This year"), `${today.slice(0, 4)}-01-01`, today] ]; const count = low === null ? 0 : Math.round((hourMs(high) - hourMs(low)) / DAY_MS) + 1; @@ -15128,7 +17523,7 @@ diff --git a/client.js b/client.js const today = hourMs(`${view.now.slice(0, 10)} 00`); const first = mondayOf(today) - (HEAT_WEEKS - 1) * 7 * DAY_MS; const from = dayKey(first); -@@ -9710,13 +9710,13 @@ +@@ -9710,13 +9710,13 @@ function Heat({ view, keep, paint, index }) { for (let week = 0; week < HEAT_WEEKS; week += 1) { const monday = first + week * 7 * DAY_MS; if (week > 0 && new Date(monday).getUTCMonth() !== new Date(monday - 7 * DAY_MS).getUTCMonth()) { @@ -15144,7 +17539,7 @@ diff --git a/client.js b/client.js key: ms, className: "bt-us-cell", style: { "--w": week }, -@@ -9724,7 +9724,7 @@ +@@ -9724,7 +9724,7 @@ function Heat({ view, keep, paint, index }) { "data-future": ms > today ? "" : void 0, "data-today": ms === today ? "" : void 0, onMouseEnter: ms > today ? void 0 : () => setHover({ ms, week, day }) @@ -15153,7 +17548,7 @@ diff --git a/client.js b/client.js } } const tipSide = hover ? { -@@ -9733,61 +9733,61 @@ +@@ -9733,61 +9733,61 @@ function Heat({ view, keep, paint, index }) { } : null; const active = [...daily.values()].filter((value) => value > 0).length; const summary = active === 1 ? t("Active on 1 day in the last year") : t("Active on {count} days in the last year", { count: active }); @@ -15241,7 +17636,7 @@ diff --git a/client.js b/client.js let stopped = false; let timer; const resized = Promise.all((document.querySelector(".bt-settings")?.getAnimations() ?? []).filter((animation) => animation.transitionProperty === "width" || animation.transitionProperty === "height").map((animation) => animation.finished.catch(() => { -@@ -9797,7 +9797,7 @@ +@@ -9797,7 +9797,7 @@ function UsagePage({ roster, actions, focusId }) { const value = await actions.usage(); await resized; if (stopped) return; @@ -15250,7 +17645,7 @@ diff --git a/client.js b/client.js setError(""); timer = setTimeout(load, value.scanning ? 3e3 : 2e4); } catch (failure) { -@@ -9814,7 +9814,7 @@ +@@ -9814,7 +9814,7 @@ function UsagePage({ roster, actions, focusId }) { }, []); const today = view?.now.slice(0, 10); const range = rangeId === "custom" && span ? { ...spanRange(span.from, span.to), ends: span.to >= today } : RANGES.find((item) => item.id === rangeId); @@ -15259,7 +17654,7 @@ diff --git a/client.js b/client.js if (!view) return null; const faces2 = view.owners.map((owner) => ownerFace(owner, roster)); const lifetime = /* @__PURE__ */ new Map(); -@@ -9823,7 +9823,7 @@ +@@ -9823,7 +9823,7 @@ function UsagePage({ roster, actions, focusId }) { const models2 = view.models.map((model, at) => { const name = model.name || model.model || t("Unknown model"); const paint = modelPaint.get(at) ?? "var(--bt-us-rest)"; @@ -15268,7 +17663,7 @@ diff --git a/client.js b/client.js }); const ownerAt2 = focus?.kind === "owner" ? view.owners.findIndex((owner) => owner.key === focus.key) : -1; const modelAt = focus?.kind === "model" ? view.models.findIndex((model) => model.key === focus.key) : -1; -@@ -9871,23 +9871,23 @@ +@@ -9871,23 +9871,23 @@ function UsagePage({ roster, actions, focusId }) { }); return { faces: faces2, models: models2, ownerAt: ownerAt2, modelAt, face: face2, keep: keep2, all: all2, part: part2, series: series2, tipSeries: tipSeries2, fillOf: fillOf2, columns: columns2, ranked: ranked2, isBot: isBot2, activeDays: activeDays2, oldest: oldest2 }; }, [view, rangeId, span, by, focusId, roster]); @@ -15300,7 +17695,7 @@ diff --git a/client.js b/client.js ) ); } -@@ -9912,7 +9912,7 @@ +@@ -9912,7 +9912,7 @@ function UsagePage({ roster, actions, focusId }) { ["custom", rangeId === "custom" && span ? spanText(span.from, span.to) : t("Custom"), CalendarIcon] ]; const whole = part.total.total; @@ -15309,7 +17704,7 @@ diff --git a/client.js b/client.js key: `o${at}`, kind: faces[at].kind, glyph: faces[at].glyph(32), -@@ -9924,7 +9924,7 @@ +@@ -9924,7 +9924,7 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`o${at}`) ?? soften(faces[at].paint), onClick: () => pick2({ kind: "owner", key: view.owners[at].key }) }); @@ -15318,7 +17713,7 @@ diff --git a/client.js b/client.js key: `m${at}`, kind: "model", glyph: models[at].glyph(32), -@@ -9936,30 +9936,30 @@ +@@ -9936,30 +9936,30 @@ function UsagePage({ roster, actions, focusId }) { fill: face ? soften(face.paint, 0.8) : fillOf.get(`m${at}`) ?? soften(models[at].paint), onClick: () => pick2({ kind: "model", key: view.models[at].key }) }); @@ -15359,7 +17754,7 @@ diff --git a/client.js b/client.js ) ]; } else { -@@ -9968,39 +9968,39 @@ +@@ -9968,39 +9968,39 @@ function UsagePage({ roster, actions, focusId }) { const rest = owners.filter(([at]) => !isBot(at)); const usedModels = ranked(all.models); body = [ @@ -15413,7 +17808,7 @@ diff --git a/client.js b/client.js body ); } -@@ -10150,33 +10150,216 @@ +@@ -10150,33 +10150,216 @@ button.bt-us-row:hover{background:var(--bt-hover)} @keyframes bt-us-drop{from{opacity:0;transform:translateY(-4px) scale(.96)}} @keyframes bt-us-fade{from{opacity:0}} @keyframes bt-us-cell{from{opacity:0;transform:scale(.5)}} @@ -15641,7 +18036,7 @@ diff --git a/client.js b/client.js const run = (task, after) => { setBusy(true); setError(null); -@@ -10184,83 +10184,195 @@ +@@ -10184,83 +10184,195 @@ function ConnectorRow({ connector, actions }) { after?.(); }, (failure) => setError(failure?.message ?? String(failure))).finally(() => setBusy(false)); }; @@ -15758,10 +18153,8 @@ diff --git a/client.js b/client.js - (0, import_react25.createElement)( + (0, import_react38.createElement)("label", { className: "bt-q-field" }, (0, import_react38.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_react38.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_react38.createElement)("span", { className: "bt-connector-link" }), + (0, import_react38.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")) + ) @@ -15790,8 +18183,10 @@ diff --git a/client.js b/client.js + })) : (0, import_react38.createElement)("label", { className: "bt-q-field bt-secret-field" }, (0, import_react38.createElement)(MaskedInput, { value: token, onChange: setToken, onEnter: connect, label: gateway ? t("Composio API key") : t("GitHub token"), autoFocus: true })), + gateway ? (0, import_react38.createElement)(AppPicker, { apps, onChange: setApps, search: token.trim() === "" ? void 0 : search }) : null, + (0, import_react38.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_react38.createElement)( + "a", + { className: "bt-connector-link", href: channel ? chosen?.link ?? FEISHU_WEBHOOK_URL : gateway ? COMPOSIO_KEY_URL : TOKEN_URL, target: "_blank", rel: "noopener noreferrer" }, @@ -15801,13 +18196,11 @@ diff --git a/client.js b/client.js setForm(false); setToken(""); + setValues({}); - setError(null); - } }, t("Cancel")), -- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) ++ setError(null); ++ } }, t("Cancel")), + (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || (channel ? !filled : token.trim() === ""), onClick: connect }, busy ? t("Connecting…") : t("Connect")) - ) - ) : null, -- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ++ ) ++ ) : null, + on && gateway && picking ? (0, import_react38.createElement)( + "div", + { className: "bt-connector-form" }, @@ -15819,11 +18212,13 @@ diff --git a/client.js b/client.js + (0, import_react38.createElement)("span", { className: "bt-connector-link" }), + (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy, onClick: () => { + setPicking(false); -+ setError(null); -+ } }, t("Cancel")), + setError(null); + } }, t("Cancel")), +- (0, import_react25.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || token.trim() === "", onClick: connect }, busy ? t("Connecting…") : t("Connect")) + (0, import_react38.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: busy || apps.length === 0, onClick: saveApps }, busy ? t("Connecting…") : t("Save")) -+ ) -+ ) : null, + ) + ) : null, +- error ? (0, import_react25.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null + error ? (0, import_react38.createElement)("div", { className: "bt-connector-error", role: "alert" }, error) : null ); } @@ -15867,7 +18262,7 @@ diff --git a/client.js b/client.js const date = new Date(time); const today = (/* @__PURE__ */ new Date()).toDateString() === date.toDateString(); const clock4 = date.toLocaleTimeString(dateLocale(), { hour: "2-digit", minute: "2-digit" }); -@@ -10270,18 +10270,18 @@ +@@ -10270,18 +10270,18 @@ function entrySummary(entry) { if (entry.source === "turn") return failureLabel(entry.code, entry.text); return entry.text; } @@ -15893,7 +18288,7 @@ diff --git a/client.js b/client.js void run(load()); }, []); const copyReport = () => run(fetchReport().then(async (next) => { -@@ -10289,7 +10289,7 @@ +@@ -10289,7 +10289,7 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { setNote(copied ? t("Copied the diagnostics.") : t("Could not copy. Select the text below and copy it.")); if (!copied) setPreview(true); })); @@ -15902,7 +18297,7 @@ diff --git a/client.js b/client.js const openIssue = () => { const win = window.open("", "_blank"); if (win !== null) win.opener = null; -@@ -10307,8 +10307,8 @@ +@@ -10307,8 +10307,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { if (!inline && !copied) setPreview(true); })); }; @@ -15913,7 +18308,7 @@ diff --git a/client.js b/client.js setNote(t("Downloaded the log. Keys, tokens, and your home folder are hidden in it.")); })); const clearLog = () => run(actions.diagnosticsClear().then(clearClientErrors).then(load).then(() => setNote(t("Cleared the error log.")))); -@@ -10317,59 +10317,59 @@ +@@ -10317,59 +10317,59 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { const total = (report?.total ?? 0) + browserErrors.length; const shown = entries2.slice(0, 8); return [ @@ -16001,7 +18396,7 @@ diff --git a/client.js b/client.js ) ) ]; -@@ -10377,8 +10377,8 @@ +@@ -10377,8 +10377,8 @@ function FeedbackPage({ actions, run, Section, Row: Row3 }) { var TOAST_MS = 6e3; function ErrorToast({ useUi, actions }) { const toast = useUi((state) => state.toast); @@ -16012,7 +18407,7 @@ diff --git a/client.js b/client.js if (!toast) return void 0; setLeaving(false); const fade = setTimeout(() => setLeaving(true), TOAST_MS); -@@ -10389,15 +10389,16 @@ +@@ -10389,15 +10389,16 @@ function ErrorToast({ useUi, actions }) { }; }, [toast?.token]); if (!toast) return null; @@ -16034,7 +18429,7 @@ diff --git a/client.js b/client.js ); } var FEEDBACK_CSS = ` -@@ -10416,47 +10416,56 @@ +@@ -10416,47 +10416,56 @@ var FEEDBACK_CSS = ` .bt-fb-text{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;word-break:break-word} .bt-fb-code{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px} .bt-event-error{color:#e02135} @@ -16105,7 +18500,7 @@ diff --git a/client.js b/client.js let live = true; actions.repoStars().then((count) => { if (live && Number.isInteger(count)) setStars(count); -@@ -10466,87 +10466,72 @@ +@@ -10466,87 +10466,72 @@ function AboutPage({ useUpdate, actions, run, Section, Row: Row3 }) { live = false; }; }, []); @@ -16216,7 +18611,7 @@ diff --git a/client.js b/client.js .bt-about-copy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} .bt-about-name{font-size:16px;line-height:22px;font-weight:600} .bt-about-version{font-size:12px;line-height:16px;color:var(--bt-ink-2);font-variant-numeric:tabular-nums} -@@ -10569,54 +10569,59 @@ +@@ -10569,54 +10569,59 @@ body:not([data-bt-motion=quiet]) .bt-about-mark:hover .bt-mark[data-live][data-s `; // src/client/settings.js @@ -16294,7 +18689,7 @@ diff --git a/client.js b/client.js key: ink, type: "button", className: "bt-swatch", -@@ -10626,75 +10626,86 @@ +@@ -10626,75 +10626,86 @@ function GeneralPage({ roster, actions, run }) { style: { background: INKS[ink][0] }, onClick: () => run(actions.setPrefs({ accent: INKS[ink][0] })) })), @@ -16404,7 +18799,7 @@ diff --git a/client.js b/client.js key: id, type: "button", "aria-pressed": motion === id, -@@ -10706,69 +10706,69 @@ +@@ -10706,69 +10706,69 @@ function MotionSection({ roster, actions, run }) { ); } function BotsPage({ roster, actions, run }) { @@ -16504,7 +18899,7 @@ diff --git a/client.js b/client.js useEscape(close); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -10779,104 +10779,250 @@ +@@ -10779,104 +10779,250 @@ function SettingsDialog({ page, id, roster, actions, useUpdate, leaving = false }); const bot = page === "bots" && id ? roster.byId[id] : void 0; const room2 = page === "groups" && id ? roster.roomsById[id] : void 0; @@ -16796,7 +19191,7 @@ diff --git a/client.js b/client.js const bot = roster.byId[menu.id]; const room2 = roster.roomsById[menu.id]; const entry = bot ?? room2; -@@ -10884,13 +10884,22 @@ +@@ -10884,13 +10884,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { const isMainBot = isMainOf(roster, menu.id); const isTile = roster.mainBotId === menu.id; const mainCount = (roster.mainBotIds ?? []).length; @@ -16821,7 +19216,7 @@ diff --git a/client.js b/client.js bot ? [t("Edit Bot"), GearIcon, () => actions.openSettings("bots", bot.id)] : null, room2 ? [t("Group settings"), GearIcon, () => actions.openSettings("groups", room2.id)] : null, room2 ? [t("Rename group"), PencilIcon, () => actions.renameBot(room2.id)] : null, -@@ -10901,20 +10901,22 @@ +@@ -10901,20 +10901,22 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { bot && !isMainBot ? [t("Make Main Bot"), StarLineIcon, () => actions.setMain(bot.id, true)] : null, bot && isMainBot && mainCount > 1 ? [t("Remove Main Bot role"), StarLineIcon, () => actions.setMain(bot.id, false)] : null ], @@ -16850,7 +19245,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10923,14 +10923,14 @@ +@@ -10923,14 +10923,14 @@ function ContextMenu({ menu, roster, actions, leaving = false }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => actions.reportError(error, "menu")); } @@ -16869,7 +19264,7 @@ diff --git a/client.js b/client.js const box = ref.current?.getBoundingClientRect(); if (!box) return; setPlace({ left: Math.max(8, Math.min(menu.x, window.innerWidth - box.width - 8)), top: Math.max(8, Math.min(menu.y, window.innerHeight - box.height - 8)) }); -@@ -10952,14 +10952,14 @@ +@@ -10952,14 +10952,14 @@ function AgentMenu({ menu, roster, actions, leaving }) { [t("Archive"), TrashIcon, () => actions.archiveAgentSession(agent.id, menu.id), true] ] ]; @@ -16889,7 +19284,7 @@ diff --git a/client.js b/client.js key: label, type: "button", role: "menuitem", -@@ -10968,16 +10968,31 @@ +@@ -10968,16 +10968,31 @@ function AgentMenu({ menu, roster, actions, leaving }) { actions.closeMenu(); void Promise.resolve(run()).catch((error) => console.warn("[ds-bot]", error)); } @@ -16922,7 +19317,7 @@ diff --git a/client.js b/client.js prefs: "ds-bot:prefs", surface: "ds-bot:surface", agentSessions: "ds-bot:agent-sessions", -@@ -11038,244 +11038,8 @@ +@@ -11038,244 +11038,8 @@ function migrateStorage(storage = globalThis.localStorage) { } } @@ -17168,7 +19563,7 @@ diff --git a/client.js b/client.js var TAB_LABELS = { details: "Details", browser: "Browser" }; var hostOf = (url) => { try { -@@ -11289,10 +11289,10 @@ +@@ -11289,10 +11289,10 @@ var EMPTY_TABS = { ids: [], active: null }; var PERMISSION_LABELS = { "read-only": "Read only", "workspace-write": "Workspace", "danger-full-access": "Full access" }; var DANGER = /* @__PURE__ */ new Set(["danger-full-access"]); function PermissionRow({ bot, roster, actions, run }) { @@ -17181,7 +19576,7 @@ diff --git a/client.js b/client.js if (pending === saved) setPending(null); }, [pending, saved]); if (!catalog?.options?.length) return null; -@@ -11302,14 +11302,14 @@ +@@ -11302,14 +11302,14 @@ function PermissionRow({ bot, roster, actions, run }) { setPending(value); void run(actions.updateBot(bot.id, { permission: value })).catch(() => setPending(null)); }; @@ -17200,7 +19595,7 @@ diff --git a/client.js b/client.js key: option.value, type: "button", "aria-pressed": option.value === current, -@@ -11323,42 +11323,42 @@ +@@ -11323,42 +11323,42 @@ function PermissionRow({ bot, roster, actions, run }) { function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrowserPages }) { const state = useBrowserTabs((map) => map[bot.id] ?? EMPTY_TABS); const pages = useBrowserPages((map) => map); @@ -17258,7 +19653,7 @@ diff --git a/client.js b/client.js type: "button", className: "bt-bcard-x", "aria-label": t("Close tab"), -@@ -11367,87 +11367,195 @@ +@@ -11367,87 +11367,195 @@ function BrowserCards({ bot, sessionId, roster, actions, useBrowserTabs, useBrow void actions.browserCloseTab(bot.id, tabId); } }, "×"), @@ -17290,11 +19685,8 @@ diff --git a/client.js b/client.js useMorph(body, open); - return (0, import_react31.createElement)( + return (0, import_react43.createElement)( - "div", - null, -- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), -- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), -- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null ++ "div", ++ null, + (0, import_react43.createElement)("div", { className: "bt-section-title" }, t(title)), + (0, import_react43.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text2), + long ? (0, import_react43.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null @@ -17347,8 +19739,11 @@ diff --git a/client.js b/client.js + const people2 = facePeople(room2, roster); + const pick2 = (bot) => call("social/face-lead", { roomId: room2.id, bot }); + return (0, import_react43.createElement)( -+ "div", -+ null, + "div", + null, +- (0, import_react31.createElement)("div", { className: "bt-section-title" }, t(title)), +- (0, import_react31.createElement)("div", { ref: body, className: "bt-about", "data-open": open || void 0 }, text), +- long ? (0, import_react31.createElement)("button", { type: "button", className: "bt-more", onClick: () => setOpen((value) => !value) }, open ? t("Show less") : t("Show more")) : null + (0, import_react43.createElement)(FaceLead, { room: room2, roster, pick: pick2 }), + (0, import_react43.createElement)("div", { className: "bt-section-title" }, t("Members · {count}", { count: people2.length })), + (0, import_react43.createElement)("div", { className: "bt-members", role: "list" }, people2.map((person) => (0, import_react43.createElement)(FacePersonRow, { @@ -17499,7 +19894,7 @@ diff --git a/client.js b/client.js if (last.current.view !== view) last.current = { view, tab, move: view === "settings" ? "in" : "out" }; else if (last.current.tab !== tab) last.current = { view, tab, move: "tab" }; const move = last.current.move; -@@ -11457,7 +11457,7 @@ +@@ -11457,7 +11457,7 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const bot = owner; const running = useSessionStatus((map) => map.get(part)?.running === true); const doing = useActivity((map) => map[bot?.id ?? sessionId]); @@ -17508,7 +19903,7 @@ diff --git a/client.js b/client.js const usage = useSessions((list) => usageOf(list, [part, ...bot?.parts ?? room2?.parts ?? []])); const run = (promise) => Promise.resolve(promise).then(() => { setError(""); -@@ -11467,41 +11467,67 @@ +@@ -11467,41 +11467,67 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a return false; }); const share = () => { @@ -17592,7 +19987,7 @@ diff --git a/client.js b/client.js ]; } const current = bot?.name ?? room2.name; -@@ -11515,12 +11515,12 @@ +@@ -11515,12 +11515,12 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a const subtitle = bot ? bot.role : roomSubtitle(room2, roster); const tabs = bot ? ["details", "browser"] : ["details"]; return [ @@ -17609,7 +20004,7 @@ diff --git a/client.js b/client.js className: "bt-drawer-name-input", autoFocus: true, value: draftName, -@@ -11535,98 +11535,97 @@ +@@ -11535,98 +11535,97 @@ function DetailsDrawer({ sessionId, startRenaming, startTab, hasStrip, roster, a setEditing(false); } } @@ -17753,7 +20148,7 @@ diff --git a/client.js b/client.js var room = { rule: null, selector: "" }; var roomRule = () => { const live = room.rule?.parentStyleSheet?.ownerNode?.isConnected; -@@ -11674,11 +11674,11 @@ +@@ -11674,11 +11674,11 @@ var reducedMotion2 = () => typeof matchMedia === "function" && matchMedia("(pref var tabLabel = (page) => page.title || shortAddress(page.url) || t("New tab"); function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, useBrowserPages }) { const pages = useBrowserPages((map) => map); @@ -17770,7 +20165,7 @@ diff --git a/client.js b/client.js const box = wrap.current; const capsule = ind.current; if (!box || !capsule) return void 0; -@@ -11707,7 +11707,7 @@ +@@ -11707,7 +11707,7 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, trail.cancel(); }; }); @@ -17779,7 +20174,7 @@ diff --git a/client.js b/client.js const box = wrap.current; if (!box) return void 0; const observer = new ResizeObserver(() => { -@@ -11721,13 +11721,13 @@ +@@ -11721,13 +11721,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, observer.observe(box); return () => observer.disconnect(); }, []); @@ -17796,7 +20191,7 @@ diff --git a/client.js b/client.js "button", { type: "button", -@@ -11737,13 +11737,13 @@ +@@ -11737,13 +11737,13 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, title: bot.name, onClick: () => onView("details") }, @@ -17813,7 +20208,7 @@ diff --git a/client.js b/client.js "button", { key: tabId, -@@ -11764,9 +11764,9 @@ +@@ -11764,9 +11764,9 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, } } }, @@ -17826,7 +20221,7 @@ diff --git a/client.js b/client.js className: "bt-ptab-x", role: "button", "aria-label": t("Close tab"), -@@ -11778,20 +11778,20 @@ +@@ -11778,20 +11778,20 @@ function PanelStrip({ bot, subject, view, ids, active, reading, onView, actions, }, "×") ); }), @@ -17854,7 +20249,7 @@ diff --git a/client.js b/client.js const element = frame.current; if (!element || leaving || !tabId) { void actions.browserShow(null); -@@ -11817,43 +11817,43 @@ +@@ -11817,43 +11817,43 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) window.removeEventListener("resize", schedule); }; }, [tabId, showGuest, leaving]); @@ -17911,7 +20306,7 @@ diff --git a/client.js b/client.js ref: input, type: "text", spellCheck: false, -@@ -11877,25 +11877,25 @@ +@@ -11877,25 +11877,25 @@ function BrowserView({ bot, tabId, leaving, landed, actions, useBrowserPages }) } }) ), @@ -17947,7 +20342,7 @@ diff --git a/client.js b/client.js ) : null ) ); -@@ -11907,41 +11907,41 @@ +@@ -11907,41 +11907,41 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu const tabState = useBrowserTabs((map) => bot ? map[bot.id] ?? EMPTY_TABS2 : EMPTY_TABS2); const active = tabState.active; const page = useBrowserPages((pages) => (active ? pages[active] : null) ?? EMPTY_PAGE); @@ -18005,7 +20400,7 @@ diff --git a/client.js b/client.js let timer; const onResize = () => { setResizing(true); -@@ -11955,28 +11955,28 @@ +@@ -11955,28 +11955,28 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu window.removeEventListener("resize", onResize); }; }, []); @@ -18041,7 +20436,7 @@ diff --git a/client.js b/client.js if (!page.readAt || Date.now() - page.readAt > READ_FLASH_MS) return void 0; setReading(true); const timer = setTimeout(() => setReading(false), READ_FLASH_MS); -@@ -12013,7 +12013,7 @@ +@@ -12013,7 +12013,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu return actions[name](...args); }; } @@ -18050,7 +20445,7 @@ diff --git a/client.js b/client.js const move = fly.current; if (!move) return; fly.current = null; -@@ -12096,7 +12096,7 @@ +@@ -12096,7 +12096,7 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu for (const name of ends) grip.addEventListener(name, onEnd); }; const hasStrip = canBrowse && tabState.ids.length > 0; @@ -18059,7 +20454,7 @@ diff --git a/client.js b/client.js "aside", { ref: root, -@@ -12113,15 +12113,15 @@ +@@ -12113,15 +12113,15 @@ function BotPanel({ pane, leaving, roster, actions, useSessions, useSessionStatu if (event.target === event.currentTarget) setLanded(true); } }, @@ -18080,7 +20475,7 @@ diff --git a/client.js b/client.js sessionId: pane.id, startRenaming: pane.renaming, startTab: pane.tab, -@@ -12217,9 +12217,79 @@ +@@ -12217,9 +12217,79 @@ var BROWSER_CSS = ` @media (prefers-reduced-motion:reduce){.bt-ptabs,.bt-panel-view,.bt-bcard,.bt-ptab-name{animation:none}} `; @@ -18162,7 +20557,7 @@ diff --git a/client.js b/client.js var DIALOG_EXIT_MS = 160; var MENU_EXIT_MS3 = 120; function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity, useWork, useRegistry, useBrowserPages, useBrowserTabs, useUpdate, actions }) { -@@ -12227,15 +12227,15 @@ +@@ -12227,15 +12227,15 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity const ui2 = useUi((value) => value); useRegistry((value) => value); const current = useSessions((list) => Object.values(list.byId).find((session) => (session?.retainedBy?.mainView ?? 0) > 0)?.id ?? null); @@ -18182,7 +20577,7 @@ diff --git a/client.js b/client.js if (open) { setKept(open); return void 0; -@@ -12245,19 +12245,26 @@ +@@ -12245,19 +12245,26 @@ function Overlays({ useRoster, useUi, useSessions, useSessionStatus, useActivity }, [ui2.details, ui2.view, ui2.renaming, ui2.tab]); const pane = open ?? kept; const [settings, settingsLeaving] = useLinger(ui2.settings, DIALOG_EXIT_MS); @@ -18219,7 +20614,7 @@ diff --git a/client.js b/client.js ); } -@@ -12294,19 +12294,19 @@ +@@ -12294,19 +12294,19 @@ function readPage(limits) { const auto = String(el.getAttribute("autocomplete") || "").toLowerCase(); return el.type === "password" || /(^|\s)(cc-|one-time-code)/.test(auto) || SECRET_HINT.test(`${el.getAttribute("name") || ""} ${el.id || ""}`); }; @@ -18246,7 +20641,7 @@ diff --git a/client.js b/client.js const styleOf = (el) => (el.ownerDocument.defaultView || window).getComputedStyle(el); const childrenOf = (el) => { if (el.shadowRoot) return el.shadowRoot.childNodes; -@@ -12340,8 +12340,8 @@ +@@ -12340,8 +12340,8 @@ function readPage(limits) { items.push(item); return { item, number: items.length, secret }; }; @@ -18257,7 +20652,7 @@ diff --git a/client.js b/client.js ).slice(0, 80); const fenced = (code, lang) => { const longest = Math.max(2, ...(code.match(/`{3,}/g) || []).map((run) => run.length)); -@@ -12372,7 +12372,7 @@ +@@ -12372,7 +12372,7 @@ ${fence}`; const marker = ordered ? `${number++}. ` : "- "; if (body2 === "") continue; const pad2 = " ".repeat(marker.length); @@ -18266,7 +20661,7 @@ diff --git a/client.js b/client.js } return keep(lines.join("\n")); }; -@@ -12398,11 +12398,11 @@ +@@ -12398,11 +12398,11 @@ ${children(cell, ctx)} let width = 0; for (const row of rows.slice(0, limits.tableRows + 1)) { if (over()) break; @@ -18280,7 +20675,7 @@ diff --git a/client.js b/client.js carry[column].left -= 1; column += 1; } -@@ -12410,18 +12410,18 @@ +@@ -12410,18 +12410,18 @@ ${children(cell, ctx)} for (const cell of row.cells) { place(); if (styleOf(cell).display === "none") continue; @@ -18304,7 +20699,7 @@ diff --git a/client.js b/client.js } width = Math.min(width, limits.tableCols); if (grid.length === 0 || width === 0) return caption ? ` -@@ -12480,9 +12480,9 @@ +@@ -12480,9 +12480,9 @@ ${body2 || "(no text)"} if (node.nodeType === 3) { if (!ctx.shown) return ""; const data = node.data.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, ""); @@ -18317,7 +20712,7 @@ diff --git a/client.js b/client.js } if (node.nodeType !== 1) return ""; const el = node; -@@ -12552,7 +12552,7 @@ +@@ -12552,7 +12552,7 @@ ${"#".repeat(Math.min(6, Number(level) || 2))} ${title}${mark} } if (tag === "blockquote") { const quoted = block(body2); @@ -18326,7 +20721,7 @@ diff --git a/client.js b/client.js } const end = body2.trimEnd().length; const marked = mark ? `${body2.slice(0, end)}${mark}${body2.slice(end)}` : body2; -@@ -12570,12 +12570,12 @@ +@@ -12570,12 +12570,12 @@ ${marked} return marked; } const body = document.body; @@ -18343,7 +20738,7 @@ diff --git a/client.js b/client.js stop ||= `the text passed ${limits.chars} characters`; } const page = { -@@ -12585,7 +12585,7 @@ +@@ -12585,7 +12585,7 @@ ${marked} description: document.querySelector('meta[name="description"]')?.content || "", selection: String(getSelection() || "").slice(0, 4e3), format: "markdown", @@ -18352,7 +20747,7 @@ diff --git a/client.js b/client.js items, stats: { ...stats, ms: Math.round(performance.now() - started) } }; -@@ -12613,15 +12613,15 @@ +@@ -12613,15 +12613,15 @@ var METADATA_HOST = /^(169\.254\.\d+\.\d+|\[fd00:ec2::254\]|metadata\.google\.in var BLOCKED = "Cloud metadata addresses do not open here."; var blockedAddress = (url) => URL.canParse(url) && METADATA_HOST.test(new URL(url).hostname); function browserAddress(input) { @@ -18376,7 +20771,7 @@ diff --git a/client.js b/client.js } function createBotBrowsers({ bridge, createSource: createSource2, storage = globalThis.localStorage, document: doc = globalThis.document }) { const views = /* @__PURE__ */ new Map(); -@@ -13052,11 +13052,821 @@ +@@ -13052,11 +13052,821 @@ function serveBrowserReads({ browsers, post, clientId = globalThis.crypto.random }; } @@ -19199,7 +21594,7 @@ diff --git a/client.js b/client.js function disposeAll(disposers, label) { for (const dispose of [...disposers].reverse()) { try { -@@ -13075,8 +13075,8 @@ +@@ -13075,8 +13075,8 @@ function surfaceGroups(install) { }; return { reconcile(wanted) { @@ -19210,7 +21605,7 @@ diff --git a/client.js b/client.js if (!wanted[group] || mounted[group].length > 0) continue; try { mounted[group] = install[group](); -@@ -13086,7 +13086,7 @@ +@@ -13086,7 +13086,7 @@ function surfaceGroups(install) { } }, dispose() { @@ -19219,7 +21614,7 @@ diff --git a/client.js b/client.js } }; } -@@ -13105,7 +13105,7 @@ +@@ -13105,7 +13105,7 @@ function writeMode(mode, storage = globalThis.localStorage) { } // src/client/user-message.js @@ -19228,7 +21623,7 @@ diff --git a/client.js b/client.js var ui = __toESM(require("@deepseek-ai/dsh-client-ui-primitives"), 1); var canDrawUserMessages = () => typeof ui.projectUserText === "function"; function contentParts(content) { -@@ -13119,87 +13119,165 @@ +@@ -13119,87 +13119,165 @@ function contentParts(content) { } return { text: texts2.join("\n"), attachments, rest }; } @@ -19428,7 +21823,7 @@ diff --git a/client.js b/client.js .bt-ubody a{color:inherit} .bt-umention{display:inline-flex;align-items:center;gap:3px;vertical-align:-3px;max-width:100%;padding:0 1px;border:0;background:none;color:inherit;font:inherit;font-weight:600;line-height:inherit;cursor:pointer;border-radius:6px;white-space:nowrap} .bt-umention:hover{text-decoration:underline;text-underline-offset:2px} -@@ -13216,42 +13216,115 @@ +@@ -13216,42 +13216,115 @@ var USER_MESSAGE_CSS = ` `; // src/client/approval-notices.js @@ -19510,8 +21905,8 @@ diff --git a/client.js b/client.js + off(); + timers.clearTimeout?.(timer); + }; - } - ++} ++ +// src/client/stats-dock.js +var import_react49 = require("react"); +var STATS_DOCK = "conversation.composer.dock"; @@ -19525,7 +21920,7 @@ diff --git a/client.js b/client.js + const { turns, steps } = params; + return text2 === `${turns} turns ${steps} steps` ? `${noun(turns, "turn", "turns")} ${noun(steps, "step", "steps")}` : text2; + }; -+} + } +function statsDock(entriesOf, id = STATS_ID) { + const shell = () => (entriesOf(STATS_DOCK) ?? []).find((entry) => entry?.component !== StatsDock && entry?.options?.id === id); + function StatsDock(props) { @@ -19537,7 +21932,7 @@ diff --git a/client.js b/client.js + return { component: StatsDock, inject: (...args) => shell()?.inject?.(...args) ?? {} }; +} +var statsDocks = (entriesOf) => STATS_IDS.map((id) => ({ id, ...statsDock(entriesOf, id) })); -+ + // src/client/index.js +var import_react50 = require("react"); var inject = ["sessions", "uiWorkspace", "uiConversation", "slots", "theme", "layout"]; @@ -19557,7 +21952,7 @@ diff --git a/client.js b/client.js migrateStorage(); const UNREAD_KEY = STORAGE.unread; let savedUnread = {}; -@@ -13274,7 +13274,13 @@ +@@ -13274,7 +13274,13 @@ function apply(ctx) { } catch { seenAt = {}; } @@ -19572,7 +21967,7 @@ diff --git a/client.js b/client.js const surface = createSource({ mode: readMode(), plain: false }); let agentSessionCache = /* @__PURE__ */ new Set(); try { -@@ -13308,6 +13308,8 @@ +@@ -13308,6 +13308,8 @@ function apply(ctx) { return () => window.removeEventListener("pagehide", onPageHide); }, "ds-bot: remember visits on page hide"); const activity = createSource({}); @@ -19581,7 +21976,7 @@ diff --git a/client.js b/client.js const work = createSource({ bots: {}, approvals: [] }); let workKey = ""; const live = createSource({}); -@@ -13344,13 +13344,25 @@ +@@ -13344,13 +13344,25 @@ function apply(ctx) { } return { ...state, unread }; }); @@ -19608,7 +22003,7 @@ diff --git a/client.js b/client.js if (order < applied) return; applied = order; const nextActivity = JSON.stringify(value.activity ?? {}); -@@ -13358,6 +13358,11 @@ +@@ -13358,6 +13358,11 @@ function apply(ctx) { activityKey = nextActivity; activity.set(value.activity ?? {}); } @@ -19620,7 +22015,7 @@ diff --git a/client.js b/client.js const nextWork = JSON.stringify(value.work ?? null); if (nextWork !== workKey) { workKey = nextWork; -@@ -13368,10 +13368,15 @@ +@@ -13368,10 +13368,15 @@ function apply(ctx) { const live2 = {}; if (value.connectors !== void 0 && JSON.stringify(value.connectors) !== JSON.stringify(current.connectors ?? [])) live2.connectors = value.connectors; if (value.capabilityStatuses !== void 0 && JSON.stringify(value.capabilityStatuses) !== JSON.stringify(current.capabilityStatuses ?? {})) live2.capabilityStatuses = value.capabilityStatuses; @@ -19636,7 +22031,7 @@ diff --git a/client.js b/client.js try { const ids = (value.agents ?? []).flatMap((agent) => agent.sessions ?? []); localStorage.setItem(STORAGE.agentSessions, JSON.stringify(ids)); -@@ -13488,20 +13488,12 @@ +@@ -13488,20 +13488,12 @@ function apply(ctx) { const style = installStyles("", "mentions"); let last = ""; const render = () => { @@ -19659,7 +22054,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13520,7 +13520,7 @@ +@@ -13520,7 +13520,7 @@ function apply(ctx) { const accent = isHex(prefs.accent) ? prefs.accent : void 0; const motion = prefs.motion === "quiet" || prefs.motion === "lively" ? prefs.motion : "normal"; document.body.dataset.btMotion = motion; @@ -19668,7 +22063,7 @@ diff --git a/client.js b/client.js if (next === key) return; key = next; style.textContent = themeCss(themes[id], accent); -@@ -13536,7 +13536,7 @@ +@@ -13536,7 +13536,7 @@ function apply(ctx) { }; render(); const offRoster = roster.subscribe(render); @@ -19677,7 +22072,7 @@ diff --git a/client.js b/client.js return () => { offRoster(); offRegistry(); -@@ -13588,6 +13588,7 @@ +@@ -13588,6 +13588,7 @@ function apply(ctx) { // or the click appears to do nothing. isAgentSession: (id) => isAgentSessionId(id), openSession: (id) => { @@ -19685,7 +22080,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,12 @@ +@@ -13653,12 +13653,12 @@ 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. @@ -19701,7 +22096,7 @@ diff --git a/client.js b/client.js expected = target; ctx.uiWorkspace.openSession(target); }, -@@ -13708,10 +13708,15 @@ +@@ -13708,10 +13708,15 @@ function apply(ctx) { }, // `id` opens one Bot (page 'bots') or one group chat (page 'groups'). openSettings: (page = "general", id = null) => { @@ -19719,7 +22114,7 @@ diff --git a/client.js b/client.js }, openMemory: (botId) => { ui2.set((state) => ({ ...state, memory: botId, menu: null, palette: null })); -@@ -13719,6 +13719,19 @@ +@@ -13719,6 +13719,19 @@ function apply(ctx) { closeMemory: () => { ui2.set((state) => state.memory ? { ...state, memory: null } : state); }, @@ -19739,7 +22134,7 @@ diff --git a/client.js b/client.js openConnectors: () => { ui2.set((state) => ({ ...state, settings: { page: "connectors", id: null }, menu: null, palette: null, newChat: null })); }, -@@ -13728,7 +13728,7 @@ +@@ -13728,7 +13728,7 @@ function apply(ctx) { if (mode !== "bot" && mode !== "agent") return; if (surface.getSnapshot().mode === mode) return; if (mode === "agent") { @@ -19748,7 +22143,7 @@ diff --git a/client.js b/client.js } writeMode(mode); surface.set((state) => ({ ...state, mode })); -@@ -13757,6 +13757,12 @@ +@@ -13757,6 +13757,12 @@ function apply(ctx) { const seen = seenAt[seenKey(id)] ?? (made?.createdBy && Number.isFinite(made.createdAt) ? made.createdAt : null); ui2.set((state) => ({ ...state, visits: { ...state.visits, [id]: { seen, entered: Date.now() } } })); }, @@ -19761,7 +22156,7 @@ diff --git a/client.js b/client.js endVisit: (id) => { leaveVisits([id]); ui2.set((state) => { -@@ -13780,7 +13780,7 @@ +@@ -13780,7 +13780,7 @@ function apply(ctx) { } return { ...state, reactions }; }), @@ -19770,7 +22165,7 @@ diff --git a/client.js b/client.js clearQuote: (token) => ui2.set((state) => state.quote?.token === token ? { ...state, quote: null } : state), clearUnread: (id) => setUnread(id, false), setFlags: (id, flags) => after(call("set-flags", { id, ...flags })), -@@ -13808,10 +13808,16 @@ +@@ -13808,10 +13808,16 @@ function apply(ctx) { setPrefs: (prefs) => after(call("set-prefs", prefs)), setMain: (id, main = true, options = {}) => after(call("set-main", { id, main, ...options })), updateRoom: (id, patch) => after(call("update-room", { id, ...patch })), @@ -19788,7 +22183,7 @@ diff --git a/client.js b/client.js stopMemberWork: (roomId, botId) => after(call("stop-member-work", { roomId, botId })), // Values go out in these requests and never come back in a response. secretSet: (payload) => after(call("secret-set", payload)), -@@ -13819,15 +13819,31 @@ +@@ -13819,15 +13819,31 @@ function apply(ctx) { secretCancel: (requestId) => after(call("secret-cancel", { requestId })), secretUpdate: (name, scope) => after(call("secret-update", { name, scope })), secretDelete: (name) => after(call("secret-delete", { name })), @@ -19821,7 +22216,7 @@ diff --git a/client.js b/client.js scheduleDelete: (sessionId, id) => call("schedule-delete", { sessionId, id }), exchange: (a, b) => call("exchange", { a, b }).catch(() => []), roomProgress: (roomId) => call("room-progress", { roomId }).catch(() => []), -@@ -13836,7 +13836,7 @@ +@@ -13836,7 +13836,7 @@ function apply(ctx) { usage: () => call("usage", { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }), // `summarize` starts a summary when the entries changed; `regenerate` always does. memoryView: (botId, flags = {}) => call("memory-view", { botId, ...flags }), @@ -19830,7 +22225,7 @@ diff --git a/client.js b/client.js memoryForget: (botId, entry) => call("memory-forget", { botId, scope: entry.scope, topic: entry.topic, text: entry.text }), diagnostics: (payload) => call("diagnostics", payload), diagnosticsLog: () => call("diagnostics-log"), -@@ -13846,6 +13846,8 @@ +@@ -13846,6 +13846,8 @@ function apply(ctx) { noteClientError(where, error); ui2.set((state) => ({ ...state, toast: { text: error?.message ?? String(error), token: Date.now() } })); }, @@ -19839,7 +22234,7 @@ diff --git a/client.js b/client.js dismissToast: (token) => ui2.set((state) => state.toast && (token === void 0 || state.toast.token === token) ? { ...state, toast: null } : state) }; if (browsers) { -@@ -13890,6 +13890,7 @@ +@@ -13890,6 +13890,7 @@ function apply(ctx) { actions.closePanel = () => { ui2.set((state) => state.details ? { ...state, details: null, view: "details", renaming: false, tab: null } : state); }; @@ -19847,7 +22242,7 @@ diff --git a/client.js b/client.js actions.signInEntry = () => ctx.slots.entries?.("settings.models.sign-in")?.[0] ?? null; actions.canSignIn = () => Boolean(actions.signInEntry()); actions.signIn = () => ui2.set((state) => ({ ...state, signIn: (state.signIn ?? 0) + 1 })); -@@ -13922,6 +13922,9 @@ +@@ -13922,6 +13922,9 @@ function apply(ctx) { wakeUpdate(); }; actions.repoStars = () => call("repo-stars"); @@ -19857,7 +22252,7 @@ diff --git a/client.js b/client.js actions.noteTurnTime = (sessionId, turn, key, time) => { if (sessionId === void 0 || !Number.isFinite(turn) || !Number.isFinite(time)) return; turnClock.set((state) => { -@@ -13964,7 +13964,13 @@ +@@ -13964,7 +13964,13 @@ function apply(ctx) { }); const browserPages = browsers?.pages ?? createSource({}); const browserTabs = browsers?.tabs ?? createSource({}); @@ -19872,7 +22267,7 @@ diff --git a/client.js b/client.js ctx.effect(() => noticeApprovals({ work, roster, -@@ -13989,7 +13989,17 @@ +@@ -13989,7 +13989,17 @@ function apply(ctx) { } }; }; const shadow = (name, options, component) => ctx.slots.inject(name, () => ctx.slots.register({ name, ...options }, component)); @@ -19890,7 +22285,7 @@ diff --git a/client.js b/client.js const installAtMenu = () => { const fiber = ctx.inject(["inputTriggers"], (scope) => { scope.effect(() => { -@@ -14001,7 +14001,12 @@ +@@ -14001,7 +14001,12 @@ function apply(ctx) { const launched = (sessionId) => controllerOf(sessionId)?.launcher.getSnapshot() === CHATS_SOURCE; const offs = atSources({ roster, sessions: ctx.sessions, launched, iconFor: menuIcons(roster) }).map((source) => triggers.registerSource(source)); atMenu = { controllerOf }; @@ -19903,7 +22298,7 @@ diff --git a/client.js b/client.js atMenu = null; for (const off of offs) off(); }; -@@ -14010,6 +14010,14 @@ +@@ -14010,6 +14010,14 @@ function apply(ctx) { return () => fiber.dispose(); }; actions.canReferenceChat = () => atMenu !== null; @@ -19918,7 +22313,7 @@ diff --git a/client.js b/client.js actions.referenceChat = (sessionId, span) => { const controller = atMenu?.controllerOf(sessionId); if (controller === void 0 || span === void 0) return; -@@ -14019,33 +14019,50 @@ +@@ -14019,33 +14019,50 @@ function apply(ctx) { installCompiledStyles(SHELL_CSS, "shell"), installCompiledStyles(SIDEBAR_CSS, "sidebar"), installCompiledStyles(SECRET_CSS, "secrets"), @@ -19973,7 +22368,7 @@ diff --git a/client.js b/client.js shadow("conversation.chat.node", { key: "assistant-step", priority: -10, locale: "chat", inject: assistantFace }, AssistantCell), shadow("conversation.chat.node", { key: "turn-trigger", priority: -10, locale: "chat", inject: face }, TriggerCell), shadow("conversation.chat.node", { key: "tool-call", priority: -10, locale: "chat", inject: face }, ToolCell), -@@ -14057,15 +14057,24 @@ +@@ -14057,15 +14057,24 @@ function apply(ctx) { // every message. shadow("conversation.chat.node", { key: "question-reply", priority: -10, locale: "chat" }, AnsweredCell), shadow("conversation.chat.node", { key: "compaction", priority: -10, locale: "chat" }, Nothing), @@ -20001,7 +22396,7 @@ diff --git a/client.js b/client.js shadow("conversation.input.right", { id: "bot-voice", order: 100, inject: face }, ComposerVoice), shadow("conversation.composer", { priority: -10, -@@ -14074,9 +14074,18 @@ +@@ -14074,8 +14074,17 @@ function apply(ctx) { shadow("conversation.input.dock", { id: "bot-members", order: -120, inject: face }, MemberDock), shadow("conversation.input.dock", { id: "bot-model", order: -110, inject: face }, ModelChoiceDock), shadow("conversation.input.dock", { id: "bot-question", order: -100, inject: face }, QuestionDock), @@ -20009,7 +22404,7 @@ diff --git a/client.js b/client.js + shadow("conversation.input.dock", { id: "bot-secret", order: -90, inject: face }, SecretDock), + shadow("conversation.input.dock", { id: "flybot-session", order: -80, inject: face }, FlyBotSessionDock) ]; - ctx.effect(() => { ++ ctx.effect(() => { + const style = installStyles(TURN_FAILURES_CSS, "turn-failures"); + const off = registry().register("flybot.session.dock", { id: "flybot-turn-failures", order: 20, component: (props) => (0, import_react50.createElement)(TurnFailures, { ...props, work }) }); + return () => { @@ -20017,7 +22412,6 @@ diff --git a/client.js b/client.js + style.remove(); + }; + }, "ds-bot: dead turn lines"); -+ ctx.effect(() => { + ctx.effect(() => { const style = installStyles(RETURN_CSS, "return"); return () => style.remove(); - }, "ds-bot: return styles"); diff --git a/patches/ds-bot/files/src/client/workflows.js b/patches/ds-bot/files/src/client/workflows.js index 5da52dcc6..ea009a9f5 100644 --- a/patches/ds-bot/files/src/client/workflows.js +++ b/patches/ds-bot/files/src/client/workflows.js @@ -7,6 +7,10 @@ import { t } from './i18n.js' export const READ_MS = 8000 +// The room's Workflow drawer re-reads a live run on this beat; Settings shows the +// same data, so it moves at the same speed instead of freezing at mount (#1810). +export const POLL_MS = 2000 + // Shown when @flybot/workflow is not registered. A default install includes it; // the page says so instead of the host's "Unknown FlyBot endpoint workflow/…" string. const WORKFLOW_PLUGIN_HINT = 'The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.' @@ -418,6 +422,19 @@ export function WorkflowsPage({ call, roster }) { return () => { gone = true } }, []) + // The page used to read once, so a running step's chip kept the colour it had at + // mount. While any workflow is live it asks again on the room's beat; an edit + // suspends that, because a refresh would land in the middle of one (#1810). + const live = (pack?.workflows ?? []).some((item) => (item.activeCount ?? 0) > 0) + useEffect(() => { + if (!live || editing) return undefined + let gone = false + const timer = setInterval(() => { + readWorkflows(invoke).then((settled) => { if (!gone) apply(settled) }) + }, POLL_MS) + return () => { gone = true; clearInterval(timer) } + }, [live, editing]) + const retry = () => { setPhase('loading') setError('') diff --git a/patches/ds-bot/files/test/workflows-page.test.js b/patches/ds-bot/files/test/workflows-page.test.js index 7062ed403..701869863 100644 --- a/patches/ds-bot/files/test/workflows-page.test.js +++ b/patches/ds-bot/files/test/workflows-page.test.js @@ -17,23 +17,137 @@ const { outputFiles } = await build({ external: ['react', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives'], }) const h = (type, props, ...children) => ({ type, props: { ...props, children } }) -let states = [] +// #1810: the old fixture faked React with a no-op useEffect and a setter that never +// re-rendered, so no case in this file could observe an effect or a second read — a +// polling case written against it passed no matter what the page did. This runtime +// keeps hook slots per component, re-runs an effect only when its deps change, and +// re-renders when a setter fires. The 2 s beat stays in the test's hands (the env +// `setInterval` below is a fake clock); real `setTimeout` stays real so the +// read-timeout cases still settle. +const runtime = { + instances: new Map(), + key: 'root', + index: 0, + dirty: false, + unmounted: false, + reset() { + this.instances = new Map() + this.key = 'root' + this.index = 0 + this.dirty = false + this.unmounted = false + }, + instance(key) { + let inst = this.instances.get(key) + if (!inst) { + inst = { slots: [], refs: [], pending: [], deps: new Map(), cleanups: new Map() } + this.instances.set(key, inst) + } + return inst + }, +} +function withInstance(key, run) { + const previousKey = runtime.key + const previousIndex = runtime.index + runtime.key = key + runtime.index = 0 + try { return run() } finally { runtime.key = previousKey; runtime.index = previousIndex } +} +function recordEffect(run, deps) { + const inst = runtime.instance(runtime.key) + inst.pending.push({ index: runtime.index++, run, deps }) +} +function commitEffects() { + for (const inst of runtime.instances.values()) { + const pending = inst.pending + inst.pending = [] + for (const { index, run, deps } of pending) { + const previous = inst.deps.get(index) + const changed = !deps || !previous || deps.length !== previous.length || deps.some((dep, at) => !Object.is(dep, previous[at])) + if (!changed) continue + const cleanup = inst.cleanups.get(index) + if (typeof cleanup === 'function') cleanup() + inst.cleanups.delete(index) + inst.deps.set(index, deps ? [...deps] : null) + const next = run() + if (typeof next === 'function') inst.cleanups.set(index, next) + } + } +} +function unmountAll() { + runtime.unmounted = true + for (const inst of runtime.instances.values()) { + for (const cleanup of inst.cleanups.values()) if (typeof cleanup === 'function') cleanup() + inst.cleanups.clear() + inst.pending = [] + } + runtime.instances.clear() +} const react = { - createElement: h, Fragment: Symbol('F'), useEffect() {}, useLayoutEffect() {}, useMemo: (fn) => fn(), useCallback: (fn) => fn(), - useRef: (current) => ({ current }), useSyncExternalStore: (_s, get) => get(), useState: (value) => [states.length > 0 ? states.shift() : value, () => {}], + createElement: h, Fragment: Symbol('F'), + useEffect: (run, deps) => recordEffect(run, deps), + useLayoutEffect: (run, deps) => recordEffect(run, deps), + useMemo: (fn) => fn(), useCallback: (fn) => fn(), + useRef: (current) => { + const inst = runtime.instance(runtime.key) + const index = runtime.index++ + if (!(index in inst.refs)) inst.refs[index] = { current } + return inst.refs[index] + }, + useSyncExternalStore: (_s, get) => get(), + useState: (initial) => { + const inst = runtime.instance(runtime.key) + const index = runtime.index++ + if (!(index in inst.slots)) inst.slots[index] = typeof initial === 'function' ? initial() : initial + const set = (next) => { + const value = typeof next === 'function' ? next(inst.slots[index]) : next + if (Object.is(value, inst.slots[index])) return + inst.slots[index] = value + if (!runtime.unmounted) runtime.dirty = true + } + return [inst.slots[index], set] + }, } -const module = { exports: {} } -runInNewContext(outputFiles[0].text, { - module, exports: module.exports, document: { hidden: false }, navigator: { language: 'en' }, localStorage: { getItem: () => null, setItem() {} }, - setTimeout, clearTimeout, - 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}`) +// The fake beat: only `setInterval` is faked, so `advance` is the test's sole clock. +const beat = { + now: 0, nextId: 0, timers: new Map(), + reset() { this.now = 0; this.nextId = 0; this.timers.clear() }, + setInterval(fn, ms) { + const id = ++this.nextId + this.timers.set(id, { at: this.now + (Number(ms) || 0), every: Math.max(1, Number(ms) || 1), fn }) + return id }, -}) -const { WorkflowsPage, WorkflowsView, readWorkflows, settleWorkflowRead, moveNode, connectNodes, dropBot, removeNode, installLocale, wf } = module.exports + clearInterval(id) { this.timers.delete(id) }, + due(target) { + let found = null + for (const [id, timer] of this.timers) { + if (timer.at > target) continue + if (!found || timer.at < found.timer.at) found = { id, timer } + } + return found + }, + fire(found) { + this.now = found.timer.at + this.timers.set(found.id, { ...found.timer, at: found.timer.at + found.timer.every }) + found.timer.fn() + }, +} +const env = { + module: { exports: {} }, + document: { hidden: false }, navigator: { language: 'en' }, localStorage: { getItem: () => null, setItem() {} }, + setTimeout, clearTimeout, + setInterval: (fn, ms) => beat.setInterval(fn, ms), + clearInterval: (id) => beat.clearInterval(id), +} +env.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}`) +} +env.module.exports = env.module.exports +runInNewContext(outputFiles[0].text, env) +const { WorkflowsPage, WorkflowsView, readWorkflows, settleWorkflowRead, moveNode, connectNodes, dropBot, removeNode, installLocale, wf } = env.module.exports const PLUGIN_HINT = 'The Workflow plugin is not installed or not enabled. A default install includes @flybot/workflow.' @@ -49,12 +163,12 @@ function fakeLocale(active) { } } -const expand = (node) => { +const expand = (node, path = 'r') => { if (node === null || node === undefined || typeof node === 'boolean') return null - if (Array.isArray(node)) return node.map(expand) + if (Array.isArray(node)) return node.map((child, at) => expand(child, `${path}.${at}`)) if (typeof node !== 'object') return node - if (typeof node.type === 'function') return expand(node.type(node.props)) - return { ...node, props: { ...node.props, children: expand(node.props.children) } } + if (typeof node.type === 'function') return withInstance(path, () => expand(node.type(node.props), path)) + return { ...node, props: { ...node.props, children: expand(node.props.children, path) } } } const all = (node, out = []) => { if (node && typeof node === 'object') { @@ -64,9 +178,70 @@ const all = (node, out = []) => { return out } const text = (node) => (node === null || node === undefined ? '' : Array.isArray(node) ? node.map(text).join('') : typeof node === 'object' ? text(node.props.children) : String(node)) -const renderPage = (props, given = []) => { states = [...given]; const tree = expand(h(WorkflowsPage, props)); states = []; return tree } +// A render pass: drop every fresh effect record, render, then commit the effects +// whose deps changed. A setter fired inside an effect marks the tree dirty, and the +// caller's `settle` renders again — the shape of a page that reacts to a read. +function renderTree(node) { + runtime.dirty = false + for (const inst of runtime.instances.values()) inst.pending = [] + const tree = expand(node) + commitEffects() + return tree +} +const renderPage = (props) => { + runtime.reset() + return renderTree(h(WorkflowsPage, props)) +} const renderView = (props) => expand(h(WorkflowsView, props)) +// Mounts the page and hands the test the clock and the tree it can drive. Nothing +// here is a second implementation of the page: the same component renders, the same +// effects run, and `advance` is the only thing that moves time. +function mountPage(props) { + runtime.reset() + beat.reset() + let tree = renderTree(h(WorkflowsPage, props)) + const settle = async () => { + for (let guard = 0; guard < 50; guard += 1) { + await new Promise((resolve) => setImmediate(resolve)) + if (!runtime.dirty) return + tree = renderTree(h(WorkflowsPage, props)) + } + throw new Error('the page never settled') + } + const page = { + get tree() { return tree }, + settle, + async advance(ms) { + const target = beat.now + ms + for (let guard = 0; guard < 200; guard += 1) { + const found = beat.due(target) + if (!found) break + beat.fire(found) + // A real 2 s tick lets its read land and re-render before the next tick, so a + // poll that stops the interval (the run finished) stops it here too. + await settle() + } + beat.now = Math.max(beat.now, target) + }, + unmount() { unmountAll() }, + async click(match) { + const button = all(tree).find((entry) => entry.type === 'button' && match(entry)) + assert.ok(button, 'the button the test clicks exists') + button.props.onClick({ stopPropagation() {}, preventDefault() {}, target: {} }) + await settle() + return button + }, + } + return page +} +const chipOf = (tree, id) => { + const card = all(tree).find((entry) => entry.props?.['data-node'] === id) + const chip = card ? all(card).find((entry) => typeof entry.props?.className === 'string' && entry.props.className.startsWith('bt-wf-chip')) : null + return chip ? text(chip) : '' +} +const openWorkflow = (page, id) => page.click((entry) => entry.props?.['data-workflow'] === id) + const HOSTILE = '' const factory = { id: 'delivery', @@ -501,3 +676,105 @@ test('the measured width is the canvas clientWidth, the same box the scroll rang assert.equal(wf.measureCanvasWidth({ clientWidth: -1 }), 0) assert.equal(wf.measureCanvasWidth({ clientWidth: Number.NaN }), 0) }) + +// #1810: Settings → Workflows read once at mount, so a step that went running → done +// left its chip frozen until the page was reopened, while the room drawer for the same +// run already moved every 2 s. These cases drive the page through a fake clock: the +// mount read, then the ticks, then the render each answer causes. +const livePack = (nodeStatus, { activeCount = 1, status = 'running' } = {}) => ({ + ok: true, + templates: [factory], + workflows: [{ ...own, activeCount, run: { status, nodeStatus } }], +}) + +test('#1810: a running workflow is read again every 2 s and the chip follows the run', async () => { + let reads = 0 + const answers = [ + livePack({ planner: 'done', coder: 'running' }), + livePack({ planner: 'done', coder: 'done' }, { activeCount: 0, status: 'done' }), + ] + const page = mountPage({ + call: async () => { + reads += 1 + return answers[Math.min(reads - 1, answers.length - 1)] + }, + roster: { bots: [] }, + }) + await page.settle() + assert.equal(reads, 1, 'the mount read') + await openWorkflow(page, 'wf-mine') + assert.equal(chipOf(page.tree, 'coder'), 'Running') + + await page.advance(6000) + assert.ok(reads >= 2, `the page asked again while the run was live (${reads} read(s))`) + assert.equal(chipOf(page.tree, 'coder'), 'Done', 'the chip moved without remounting the page') + assert.equal(chipOf(page.tree, 'planner'), 'Done') + + // The last answer said every run is finished, so the beat stops: no more reads. + assert.equal(reads, 2) + await page.advance(6000) + assert.equal(reads, 2, 'a finished run is not read again') +}) + +test('#1810: nothing running means one read in ten seconds, not a poll', async () => { + let reads = 0 + const idle = { ok: true, templates: [factory], workflows: [{ ...own, activeCount: 0, run: { status: 'done', nodeStatus: { planner: 'done', coder: 'done' } } }] } + const page = mountPage({ call: async () => { reads += 1; return idle }, roster: { bots: [] } }) + await page.settle() + assert.equal(reads, 1) + await page.advance(10000) + assert.equal(reads, 1, 'an idle page never asks a second time') +}) + +test('#1810: an edit suspends the poll and finishing it brings the poll back', async () => { + let reads = 0 + const page = mountPage({ call: async () => { reads += 1; return livePack({ planner: 'done', coder: 'running' }) }, roster: { bots: [] } }) + await page.settle() + await openWorkflow(page, 'wf-mine') + await page.click((entry) => text(entry) === 'Edit') + assert.equal(reads, 1) + await page.advance(10000) + assert.equal(reads, 1, 'a refresh does not land in the middle of an edit') + await page.click((entry) => text(entry) === 'Finish editing') + await page.advance(2500) + assert.equal(reads, 2, 'leaving the editor resumes the beat') +}) + +test('#1810: a page that unmounts stops asking', async () => { + let reads = 0 + const page = mountPage({ call: async () => { reads += 1; return livePack({ planner: 'done', coder: 'running' }) }, roster: { bots: [] } }) + await page.settle() + assert.equal(reads, 1) + page.unmount() + await page.advance(10000) + assert.equal(reads, 1, 'no read after the page is gone') +}) + +test('#1810: a failed poll shows the error line and Retry, not an empty page', async () => { + const unhandled = [] + const onUnhandled = (reason) => unhandled.push(reason) + process.on('unhandledRejection', onUnhandled) + try { + let reads = 0 + const page = mountPage({ + call: async () => { + reads += 1 + if (reads === 1) return livePack({ planner: 'done', coder: 'running' }) + throw new Error('workflow host down') + }, + roster: { bots: [] }, + }) + await page.settle() + await openWorkflow(page, 'wf-mine') + await page.advance(2000) + assert.equal(reads, 2, 'the poll happened and failed') + const body = text(page.tree) + assert.match(body, /workflow host down/) + assert.match(body, /Retry/) + assert.doesNotMatch(body, /No workflows yet/) + await new Promise((resolve) => setImmediate(resolve)) + assert.deepEqual(unhandled, []) + } finally { + process.off('unhandledRejection', onUnhandled) + } +})